Files

247 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# go-nc-files guide
How to use the library. Symbol signatures and doc comments are in [API](api.md). Words for the domain are in [CONTEXT.md](../CONTEXT.md).
Sentinel errors are `ErrNotExist`, `ErrForbidden`, and `ErrNotDir`. `IsNotExist` and `IsForbidden` report whether an error wraps the first two. The features below name which calls return them.
## Files seam
### Purpose
`Root` opens a folder by a User Files Root–relative path. `Folder` is flat storage inside that folder: list, read, write, exists, and remove, by basename only.
### When to use it
Write product code against `Root` and `Folder`. Pass a `Local` root in tests and a `WebDAVRoot` against Nextcloud. The rest of the call sequence stays the same.
### Call sequence
1. Obtain a `Root` (`Local` or `WebDAVRoot`).
2. `OpenFolder` when the folder must already exist. `EnsureFolder` when missing parents should be created.
3. On the `Folder`, call `List`, `Read`, `Write`, `Exists`, or `Remove` with a basename.
`List` returns first-level names. `Exists` reports a non-directory file. `Entry.IsDir` is true for a child directory.
### Errors
`OpenFolder` and `EnsureFolder` return a plain error when the relative path is invalid (`CleanRel` yields empty). A missing folder from `OpenFolder` wraps `ErrNotExist`. A path that exists and is not a directory wraps `ErrNotDir`. Permission failures wrap `ErrForbidden`.
`Read`, `Write`, `Exists`, and `Remove` return an error from `RequireBasename` when the name is not a single basename. `Remove` and WebDAV `Read` wrap `ErrNotExist` when the child is missing. `Exists` returns false and a nil error when the child is missing.
### Example
```go
folder, err := root.OpenFolder("Zones")
if err != nil {
return err
}
if err := folder.Write("note.txt", []byte("hi")); err != nil {
return err
}
data, err := folder.Read("note.txt")
```
## Local
### Purpose
`Local` is a User Files Root on the local filesystem. `RootDir` is the absolute directory that corresponds to the user's files root.
### When to use it
Use `Local` for tests and for `go run` without Nextcloud. Use `LocalFolder` when the caller already has an absolute directory and only needs a `Folder`. `LocalPath` reports that absolute directory when the `Folder` came from the local adapter.
### Call sequence
1. Set `Local.RootDir`.
2. Call `OpenFolder` or `EnsureFolder` with a relative path. `EnsureFolder` creates the directory with mode `0755`.
3. Or call `LocalFolder` with an absolute directory and skip the root.
`OpenLocal` is the Visit helper that pairs `Local` with a `FileStore`. It is described under Visit.
### Errors
`OpenFolder` wraps `ErrNotExist`, `ErrForbidden`, or `ErrNotDir`, or returns a plain error for an invalid relative path. `EnsureFolder` wraps `ErrForbidden` when the process cannot create the directory.
### Example
```go
root := goncfiles.Local{RootDir: filesRoot}
folder, err := root.EnsureFolder("myapp")
if err != nil {
return err
}
_ = folder
```
## WebDAVRoot
### Purpose
`WebDAVRoot` is a User Files Root at `/remote.php/dav/files/{user}/`. `Cred` supplies the user id and AppAPI auth headers from go-nc-exapp. A nil `Client` uses `http.DefaultClient`.
### When to use it
Use `WebDAVRoot` when the Working Folder is the user's Nextcloud files. The same `Root` and `Folder` methods apply as with `Local`.
### Call sequence
1. Fill `gonexapp.Credentials`, including `BaseURL` and `UserID`.
2. Store them on `WebDAVRoot.Cred`.
3. `OpenFolder` checks the folder with PROPFIND and does not create it.
4. `EnsureFolder` creates missing parents with MKCOL, then opens the folder.
5. `List` uses PROPFIND with depth 1. `Read` uses GET. `Write` uses PUT. `Remove` uses DELETE. `Exists` uses HEAD and falls back to GET when the status is neither success, not found, nor forbidden.
### Errors
An invalid relative path returns a plain error and does not call the server. `OpenFolder` wraps `ErrNotExist` on HTTP 404, `ErrForbidden` on 401 or 403, and `ErrNotDir` when the resource is not a collection. `EnsureFolder` wraps `ErrForbidden` on 401 or 403. Other unexpected statuses are plain errors that include the HTTP status.
### Example
```go
root := goncfiles.WebDAVRoot{Cred: cred}
folder, err := root.OpenFolder("Zones")
if err != nil {
if goncfiles.IsNotExist(err) {
return fmt.Errorf("folder missing")
}
return err
}
_ = folder
```
## CleanRel
### Purpose
`CleanRel` normalizes a User Files Root–relative path. The result uses forward slashes and has no leading or trailing slash.
### When to use it
The roots call `CleanRel` themselves. Call it directly when the product must reject a path before opening a folder, or must show the normalized form.
### Call sequence
1. Pass the raw path from a query parameter or a saved preference.
2. Treat an empty result as invalid. Do not open it.
Backslashes are treated as slashes. `..`, URLs, and a bare numeric Nextcloud file ID are rejected.
### Errors
`CleanRel` does not return an error. An empty string means the path is not usable. Callers that need an error, such as `OpenFolder`, turn that empty result into `invalid Working Folder path`.
### Example
```go
rel := goncfiles.CleanRel(r.URL.Query().Get("folder"))
if rel == "" {
http.Error(w, "invalid folder", http.StatusBadRequest)
return
}
```
## RequireBasename
### Purpose
`RequireBasename` accepts a single file name inside a Working Folder and rejects a path.
### When to use it
`Folder` methods call it before touching a child. Call it directly when the product checks a name before building a request.
### Call sequence
1. Pass the child name.
2. Continue only when the error is nil.
An empty name, a name containing `/` or `\`, a name containing `..`, or a name that is not `path.Base` of itself is rejected.
### Errors
The error text is `name must be a basename in the Working Folder`. It does not wrap `ErrNotExist` or `ErrForbidden`.
### Example
```go
if err := goncfiles.RequireBasename(name); err != nil {
return err
}
```
## DefaultPathStore
### Purpose
`DefaultPathStore` persists the Saved Default Working Folder path, relative to the User Files Root. `GetDefault` returns that path, or empty when none is stored. `SetDefault` writes a new path.
### When to use it
Visit reads and writes the Saved Default through this interface. `FileStore` is a JSON file (`defaultPath`) for the local adapter. `Memory` is an in-memory store for tests. An ExApp that stores the path in Nextcloud preferences implements the same two methods around `gonexapp.AppAPIPreferences`. This library does not call OCS.
### Call sequence
1. Construct a `FileStore` with a file path, a `Memory`, or a product store.
2. Pass it to `Open`.
3. `GetDefault` on a missing file returns empty and a nil error.
4. `SetDefault` writes the path. `FileStore` creates parent directories and writes indented JSON.
`Visit.SetDefault` is the call that also ensures the folder exists. It is described under Visit.
### Errors
`FileStore.GetDefault` returns an error when the file exists and cannot be read or is not JSON. A missing file is not an error. `FileStore.SetDefault` returns an error when the directory cannot be created or the file cannot be written. `Memory` does not return an error.
### Example
```go
store := goncfiles.FileStore{Path: filepath.Join(dir, "settings.json")}
saved, err := store.GetDefault()
if err != nil {
return err
}
_ = saved
```
## Visit
### Purpose
A Visit is one opening of the ExApp: the Working Folder for this request, and the Saved Default. App-icon launch uses the Saved Default and creates the initial folder when it is missing. A Files-view folder is used only for this visit and does not change the Saved Default.
### When to use it
Call `Open` at the start of a request that needs the user's Working Folder. Use `OpenLocal` when both the files root and the settings file are on local disk.
### Call sequence
1. Call `Open` with a `Root`, a `DefaultPathStore`, the product's initial folder name, and the visit-relative path.
2. Pass an empty visit-relative path for an app-icon launch. `Open` reads the Saved Default, uses `initialFolderName` when nothing is stored, and `EnsureFolder`s that path.
3. Pass the Files-view folder for a visit-only path. `Open` uses `OpenFolder` and does not create it and does not fall back to the Saved Default.
4. Use `Visit.FS` as the `Folder`, `WorkingRel` as the relative path, and `SavedDefault` as the stored default.
5. Call `SetDefault` when the user picks a new default. It `EnsureFolder`s the path, stores it, and updates `SavedDefault`. It does not switch `FS` to that folder.
6. `WorkingDir` returns a local absolute path when `FS` is a local folder, and `WorkingRel` otherwise.
### Errors
`Open` returns the error from `GetDefault`. An invalid saved path or an invalid visit path is a plain error (`invalid Working Folder path` or `invalid saved Working Folder path`). A missing or forbidden visit path fails closed: the error from `OpenFolder` is wrapped with the path, and the Saved Default is not opened instead.
`SetDefault` returns a plain error for an invalid path, or the error from `EnsureFolder` or `SetDefault` on the store.
### Example
```go
visit, err := goncfiles.Open(root, store, "myapp", visitRelative)
if err != nil {
if goncfiles.IsForbidden(err) {
http.Error(w, "forbidden", http.StatusForbidden)
return err
}
return err
}
data, err := visit.FS.Read("note.txt")
_ = visit.WorkingRel
_ = data
```