From 70e5029bf4e0cb8f8b1510a1017ac631e929ed5b Mon Sep 17 00:00:00 2001 From: Bruno Michel Date: Thu, 6 Jun 2024 16:55:27 +0200 Subject: [PATCH 1/5] Fix uploading files to NextCloud via WebDAV The HTTP request to NextCloud was made with Transfer-Encoding: chunked, because of the way the Content-Length was set. But it looks like NextCloud doesn't support that, and files were created empty. We can avoid that by setting request.ContentLength. --- model/nextcloud/nextcloud.go | 9 ++++----- pkg/webdav/webdav.go | 31 ++++++++++++++++++++++--------- web/remote/nextcloud.go | 2 +- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/model/nextcloud/nextcloud.go b/model/nextcloud/nextcloud.go index 1cd95a6a7c8..6a4ff882956 100644 --- a/model/nextcloud/nextcloud.go +++ b/model/nextcloud/nextcloud.go @@ -109,11 +109,11 @@ func (nc *NextCloud) Download(path string) (*webdav.Download, error) { return nc.webdav.Get(path) } -func (nc *NextCloud) Upload(path, mime string, body io.Reader) error { +func (nc *NextCloud) Upload(path, mime string, contentLength int64, body io.Reader) error { headers := map[string]string{ echo.HeaderContentType: mime, } - return nc.webdav.Put(path, headers, body) + return nc.webdav.Put(path, contentLength, headers, body) } func (nc *NextCloud) Mkdir(path string) error { @@ -218,10 +218,9 @@ func (nc *NextCloud) Upstream(path, from string) error { defer f.Close() headers := map[string]string{ - echo.HeaderContentType: doc.Mime, - echo.HeaderContentLength: strconv.Itoa(int(doc.ByteSize)), + echo.HeaderContentType: doc.Mime, } - if err := nc.webdav.Put(path, headers, f); err != nil { + if err := nc.webdav.Put(path, doc.ByteSize, headers, f); err != nil { return err } _ = fs.DestroyFile(doc) diff --git a/pkg/webdav/webdav.go b/pkg/webdav/webdav.go index 3ef346909cb..5b14573b073 100644 --- a/pkg/webdav/webdav.go +++ b/pkg/webdav/webdav.go @@ -27,7 +27,7 @@ type Client struct { } func (c *Client) Mkcol(path string) error { - res, err := c.req("MKCOL", path, nil, nil) + res, err := c.req("MKCOL", path, 0, nil, nil) if err != nil { return err } @@ -47,7 +47,7 @@ func (c *Client) Mkcol(path string) error { } func (c *Client) Delete(path string) error { - res, err := c.req("DELETE", path, nil, nil) + res, err := c.req("DELETE", path, 0, nil, nil) if err != nil { return err } @@ -75,7 +75,7 @@ func (c *Client) Move(oldPath, newPath string) error { "Destination": u.String(), "Overwrite": "F", } - res, err := c.req("MOVE", oldPath, headers, nil) + res, err := c.req("MOVE", oldPath, 0, headers, nil) if err != nil { return err } @@ -105,7 +105,7 @@ func (c *Client) Copy(oldPath, newPath string) error { "Destination": u.String(), "Overwrite": "F", } - res, err := c.req("COPY", oldPath, headers, nil) + res, err := c.req("COPY", oldPath, 0, headers, nil) if err != nil { return err } @@ -124,8 +124,13 @@ func (c *Client) Copy(oldPath, newPath string) error { } } -func (c *Client) Put(path string, headers map[string]string, body io.Reader) error { - res, err := c.req("PUT", path, headers, body) +func (c *Client) Put( + path string, + contentLength int64, + headers map[string]string, + body io.Reader, +) error { + res, err := c.req("PUT", path, contentLength, headers, body) if err != nil { return err } @@ -145,7 +150,7 @@ func (c *Client) Put(path string, headers map[string]string, body io.Reader) err } func (c *Client) Get(path string) (*Download, error) { - res, err := c.req("GET", path, nil, nil) + res, err := c.req("GET", path, 0, nil, nil) if err != nil { return nil, err } @@ -187,7 +192,7 @@ func (c *Client) List(path string) ([]Item, error) { "Depth": "1", } payload := strings.NewReader(ListFilesPayload) - res, err := c.req("PROPFIND", path, headers, payload) + res, err := c.req("PROPFIND", path, 0, headers, payload) if err != nil { return nil, err } @@ -300,7 +305,12 @@ const ListFilesPayload = ` ` -func (c *Client) req(method, path string, headers map[string]string, body io.Reader) (*http.Response, error) { +func (c *Client) req( + method, path string, + contentLength int64, + headers map[string]string, + body io.Reader, +) (*http.Response, error) { path = c.BasePath + fixSlashes(path) u := url.URL{ Scheme: c.Scheme, @@ -316,6 +326,9 @@ func (c *Client) req(method, path string, headers map[string]string, body io.Rea for k, v := range headers { req.Header.Set(k, v) } + if contentLength > 0 { + req.ContentLength = contentLength + } start := time.Now() res, err := safehttp.ClientWithKeepAlive.Do(req) elapsed := time.Since(start) diff --git a/web/remote/nextcloud.go b/web/remote/nextcloud.go index ef8dfdc68b3..a08ed53d8dd 100644 --- a/web/remote/nextcloud.go +++ b/web/remote/nextcloud.go @@ -103,7 +103,7 @@ func nextcloudPut(c echo.Context) error { func nextcloudUpload(c echo.Context, nc *nextcloud.NextCloud, path string) error { req := c.Request() mime := req.Header.Get(echo.HeaderContentType) - if err := nc.Upload(path, mime, req.Body); err != nil { + if err := nc.Upload(path, mime, req.ContentLength, req.Body); err != nil { return wrapNextcloudErrors(err) } return c.JSON(http.StatusCreated, echo.Map{"ok": true}) From fd491aed6014459c608c30e7d4a725fffd5d5824 Mon Sep 17 00:00:00 2001 From: Bruno Michel Date: Thu, 6 Jun 2024 17:26:47 +0200 Subject: [PATCH 2/5] Allow to copy a file from Cozy to NextCloud --- docs/nextcloud.md | 5 ++++- model/nextcloud/nextcloud.go | 13 +++++++++++-- web/remote/nextcloud.go | 7 ++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/nextcloud.md b/docs/nextcloud.md index 11603fc1690..b7609ad15de 100644 --- a/docs/nextcloud.md +++ b/docs/nextcloud.md @@ -392,7 +392,7 @@ Content-Type: application/vnd.api+json ## POST /remote/nextcloud/:account/upstream/*path -This route can be used to move a file from the Cozy to the NextCloud. +This route can be used to move/copy a file from the Cozy to the NextCloud. The `:account` parameter is the identifier of the NextCloud `io.cozy.account`. @@ -401,6 +401,9 @@ The `*path` parameter is the path of the file on the NextCloud. The `From` parameter in the query-string must be given, as the ID of the file on the Cozy that will be moved. +By default, the file will be moved, but using `Copy=true` in the query-string +will makes a copy. + **Note:** a permission on `POST io.cozy.files` is required to use this route. ### Request diff --git a/model/nextcloud/nextcloud.go b/model/nextcloud/nextcloud.go index 6a4ff882956..624f4f257e8 100644 --- a/model/nextcloud/nextcloud.go +++ b/model/nextcloud/nextcloud.go @@ -24,6 +24,13 @@ import ( "github.com/labstack/echo/v4" ) +type OperationKind int + +const ( + MoveOperation OperationKind = iota + CopyOperation +) + type File struct { DocID string `json:"id,omitempty"` Type string `json:"type"` @@ -205,7 +212,7 @@ func (nc *NextCloud) Downstream(path, dirID string, cozyMetadata *vfs.FilesCozyM return doc, nil } -func (nc *NextCloud) Upstream(path, from string) error { +func (nc *NextCloud) Upstream(path, from string, kind OperationKind) error { fs := nc.inst.VFS() doc, err := fs.FileByID(from) if err != nil { @@ -223,7 +230,9 @@ func (nc *NextCloud) Upstream(path, from string) error { if err := nc.webdav.Put(path, doc.ByteSize, headers, f); err != nil { return err } - _ = fs.DestroyFile(doc) + if kind == MoveOperation { + _ = fs.DestroyFile(doc) + } return nil } diff --git a/web/remote/nextcloud.go b/web/remote/nextcloud.go index a08ed53d8dd..2ae9ab37724 100644 --- a/web/remote/nextcloud.go +++ b/web/remote/nextcloud.go @@ -226,7 +226,12 @@ func nextcloudUpstream(c echo.Context) error { return jsonapi.BadRequest(errors.New("missing From parameter")) } - if err := nc.Upstream(path, from); err != nil { + kind := nextcloud.MoveOperation + if c.QueryParam("Copy") != "" { + kind = nextcloud.CopyOperation + } + + if err := nc.Upstream(path, from, kind); err != nil { return wrapNextcloudErrors(err) } return c.NoContent(http.StatusNoContent) From eefb2ad068bbd8fdae5a183649a92959ea710a71 Mon Sep 17 00:00:00 2001 From: Bruno Michel Date: Thu, 6 Jun 2024 17:32:30 +0200 Subject: [PATCH 3/5] Allow to copy a file from NextCloud to Cozy --- docs/nextcloud.md | 5 ++++- model/nextcloud/nextcloud.go | 6 ++++-- web/remote/nextcloud.go | 7 ++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/nextcloud.md b/docs/nextcloud.md index b7609ad15de..07f105a89ed 100644 --- a/docs/nextcloud.md +++ b/docs/nextcloud.md @@ -303,7 +303,7 @@ Content-Type: application/json ## POST /remote/nextcloud/:account/downstream/*path -This route can be used to move a file from the NextCloud to the Cozy. +This route can be used to move/copy a file from the NextCloud to the Cozy. The `:account` parameter is the identifier of the NextCloud `io.cozy.account`. @@ -312,6 +312,9 @@ The `*path` parameter is the path of the file on the NextCloud. The `To` parameter in the query-string must be given, as the ID of the directory on the Cozy where the file will be put. +By default, the file will be moved, but using `Copy=true` in the query-string +will makes a copy. + **Note:** a permission on `POST io.cozy.files` is required to use this route. ### Request diff --git a/model/nextcloud/nextcloud.go b/model/nextcloud/nextcloud.go index 624f4f257e8..f3446b42c0c 100644 --- a/model/nextcloud/nextcloud.go +++ b/model/nextcloud/nextcloud.go @@ -167,7 +167,7 @@ func (nc *NextCloud) ListFiles(path string) ([]jsonapi.Object, error) { return files, nil } -func (nc *NextCloud) Downstream(path, dirID string, cozyMetadata *vfs.FilesCozyMetadata) (*vfs.FileDoc, error) { +func (nc *NextCloud) Downstream(path, dirID string, kind OperationKind, cozyMetadata *vfs.FilesCozyMetadata) (*vfs.FileDoc, error) { dl, err := nc.webdav.Get(path) if err != nil { return nil, err @@ -208,7 +208,9 @@ func (nc *NextCloud) Downstream(path, dirID string, cozyMetadata *vfs.FilesCozyM return nil, err } - _ = nc.webdav.Delete(path) + if kind == MoveOperation { + _ = nc.webdav.Delete(path) + } return doc, nil } diff --git a/web/remote/nextcloud.go b/web/remote/nextcloud.go index 2ae9ab37724..2b54f7b9698 100644 --- a/web/remote/nextcloud.go +++ b/web/remote/nextcloud.go @@ -199,8 +199,13 @@ func nextcloudDownstream(c echo.Context) error { return jsonapi.BadRequest(errors.New("missing To parameter")) } + kind := nextcloud.MoveOperation + if c.QueryParam("Copy") != "" { + kind = nextcloud.CopyOperation + } + cozyMetadata, _ := files.CozyMetadataFromClaims(c, true) - f, err := nc.Downstream(path, to, cozyMetadata) + f, err := nc.Downstream(path, to, kind, cozyMetadata) if err != nil { return wrapNextcloudErrors(err) } From f54a4fd7a8a322d88c02c822c1c7e6f935433e46 Mon Sep 17 00:00:00 2001 From: Bruno Michel Date: Thu, 6 Jun 2024 17:35:40 +0200 Subject: [PATCH 4/5] Allow to copy a file on NextCloud to a new path --- docs/nextcloud.md | 5 +++-- web/remote/nextcloud.go | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/nextcloud.md b/docs/nextcloud.md index 07f105a89ed..95bc9cbdabd 100644 --- a/docs/nextcloud.md +++ b/docs/nextcloud.md @@ -264,7 +264,8 @@ HTTP/1.1 204 No Content This route can be used to create a copy of a file in the same directory, with a copy suffix in its name. The new name can be optionaly given with the `Name` -parameter in the query-string. +parameter in the query-string, or the full path can be given with `Path` +parameter. The `:account` parameter is the identifier of the NextCloud `io.cozy.account`. @@ -275,7 +276,7 @@ The `*path` parameter is the path of the file on the NextCloud. ### Request ```http -POST /remote/nextcloud/4ab2155707bb6613a8b9463daf00381b/copy/Documents/wallpaper.jpg HTTP/1.1 +POST /remote/nextcloud/4ab2155707bb6613a8b9463daf00381b/copy/Documents/wallpaper.jpg?Path=/Images/beach.jpg HTTP/1.1 Host: cozy.example.net Authorization: Bearer eyJhbG... ``` diff --git a/web/remote/nextcloud.go b/web/remote/nextcloud.go index 2b54f7b9698..5522fcacc85 100644 --- a/web/remote/nextcloud.go +++ b/web/remote/nextcloud.go @@ -166,7 +166,9 @@ func nextcloudCopy(c echo.Context) error { oldPath := c.Param("*") newPath := oldPath - if newName := c.QueryParam("Name"); newName != "" { + if p := c.QueryParam("Path"); p != "" { + newPath = p + } else if newName := c.QueryParam("Name"); newName != "" { newPath = filepath.Join(filepath.Dir(oldPath), newName) } else { ext := filepath.Ext(oldPath) From c6d7c4f61c1d8b1757dc674442c1b0ab231bf97a Mon Sep 17 00:00:00 2001 From: Bruno Michel Date: Thu, 13 Jun 2024 09:14:38 +0200 Subject: [PATCH 5/5] Use ParseBool for parsing Copy parameter in query-string --- web/remote/nextcloud.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/remote/nextcloud.go b/web/remote/nextcloud.go index 5522fcacc85..64a6013f402 100644 --- a/web/remote/nextcloud.go +++ b/web/remote/nextcloud.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "path/filepath" + "strconv" "strings" "github.com/cozy/cozy-stack/model/nextcloud" @@ -202,7 +203,7 @@ func nextcloudDownstream(c echo.Context) error { } kind := nextcloud.MoveOperation - if c.QueryParam("Copy") != "" { + if isCopy, _ := strconv.ParseBool(c.QueryParam("Copy")); isCopy { kind = nextcloud.CopyOperation } @@ -234,7 +235,7 @@ func nextcloudUpstream(c echo.Context) error { } kind := nextcloud.MoveOperation - if c.QueryParam("Copy") != "" { + if isCopy, _ := strconv.ParseBool(c.QueryParam("Copy")); isCopy { kind = nextcloud.CopyOperation }