diff --git a/CONTEXT.md b/CONTEXT.md index 3fffc8a..725cbdc 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -9,7 +9,7 @@ The root of that Nextcloud user's files tree. Working Folder paths are relative _Avoid_: absolute WebDAV URLs, numeric file IDs **Working Folder**: -The single folder an ExApp uses for the current operation. Its path is relative to the User Files Root. Each ExApp defines what files inside that folder mean; this Library only opens, lists, reads, and writes there. +The single folder an ExApp uses for the current operation. Its path is relative to the User Files Root. Each ExApp defines what files inside that folder mean; this Library only opens, lists, reads, writes, and removes there. _Avoid_: catalog folder, zones directory, app-specific suffixes in Library code **Saved Default**: @@ -21,5 +21,5 @@ One inbound ExApp request bound to one Working Folder. App-icon launch uses the _Avoid_: silently switching folders, creating a visit path that was not saved as default **Files seam**: -The `Root` and `Folder` interfaces: open or ensure a relative path, then list, read, write, and test existence. Local disk and WebDAV are two implementations of the same seam. +The `Root` and `Folder` interfaces: open or ensure a relative path, then list, read, write, remove, and test existence. Local disk and WebDAV are two implementations of the same seam. _Avoid_: coupling the seam to a specific on-disk layout beyond basename rules diff --git a/files.go b/files.go index 3a80abc..bcdf1f9 100644 --- a/files.go +++ b/files.go @@ -32,6 +32,8 @@ type Folder interface { Write(name string, data []byte) error // Exists reports whether name is a non-directory file in the Working Folder. Exists(name string) (bool, error) + // Remove deletes a basename in the Working Folder. + Remove(name string) error } // Root is User Files Root–relative folder access. @@ -181,6 +183,18 @@ func (f localFolder) Exists(name string) (bool, error) { return !info.IsDir(), nil } +// Remove implements Folder for a local directory. +func (f localFolder) Remove(name string) error { + if err := RequireBasename(name); err != nil { + return err + } + err := os.Remove(filepath.Join(f.dir, name)) + if err != nil && os.IsNotExist(err) { + return fmt.Errorf("%w: %s", ErrNotExist, name) + } + return err +} + // RequireBasename rejects path segments that are not a single basename. func RequireBasename(name string) error { if name == "" || name != path.Base(name) || strings.ContainsAny(name, `/\`) || strings.Contains(name, "..") { diff --git a/webdav.go b/webdav.go index d8339e9..359d68f 100644 --- a/webdav.go +++ b/webdav.go @@ -335,6 +335,41 @@ func (f webdavFolder) Write(name string, data []byte) error { return fmt.Errorf("webdav PUT %s: %s", name, res.Status) } +// Remove implements Folder via WebDAV DELETE. +func (f webdavFolder) Remove(name string) error { + u, err := f.childURL(name) + if err != nil { + return err + } + req, err := http.NewRequest(http.MethodDelete, u, nil) + if err != nil { + return err + } + for k, vs := range f.root.Cred.AuthHeaders() { + for _, v := range vs { + req.Header.Set(k, v) + } + } + res, err := f.root.client().Do(req) + if err != nil { + return err + } + defer res.Body.Close() + if _, err := io.Copy(io.Discard, res.Body); err != nil { + return err + } + switch res.StatusCode { + case http.StatusOK, http.StatusNoContent, http.StatusAccepted: + return nil + case http.StatusNotFound: + return fmt.Errorf("%w: %s", ErrNotExist, name) + case http.StatusForbidden, http.StatusUnauthorized: + return fmt.Errorf("%w", ErrForbidden) + default: + return fmt.Errorf("webdav DELETE %s: %s", name, res.Status) + } +} + // Exists implements Folder via WebDAV HEAD (falls back to GET when needed). func (f webdavFolder) Exists(name string) (bool, error) { u, err := f.childURL(name) diff --git a/webdav_test.go b/webdav_test.go index 34a70de..92b00b6 100644 --- a/webdav_test.go +++ b/webdav_test.go @@ -56,6 +56,13 @@ func TestWebDAVReadWriteListExists(t *testing.T) { return } w.WriteHeader(http.StatusOK) + case http.MethodDelete: + if _, ok := store[path]; !ok { + http.NotFound(w, r) + return + } + delete(store, path) + w.WriteHeader(http.StatusNoContent) case "MKCOL": dirs[strings.TrimSuffix(path, "/")] = true w.WriteHeader(http.StatusCreated) @@ -102,6 +109,13 @@ func TestWebDAVReadWriteListExists(t *testing.T) { if !found { t.Fatalf("list: %+v", entries) } + if err := folder.Remove("note.txt"); err != nil { + t.Fatal(err) + } + ok, err = folder.Exists("note.txt") + if err != nil || ok { + t.Fatalf("after remove: ok=%v err=%v", ok, err) + } } func TestWebDAVOpenMissingIsNotExist(t *testing.T) {