Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage the Nextcloud trash #4432

Merged
merged 7 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions docs/nextcloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Content-Type: application/vnd.api+json
"attributes": {
"type": "directory",
"name": "Images",
"path": "/Documents/Images",
"updated_at": "Thu, 02 May 2024 09:29:53 GMT",
"etag": "\"66335d11c4b91\""
},
Expand All @@ -59,6 +60,7 @@ Content-Type: application/vnd.api+json
"attributes": {
"type": "file",
"name": "BugBounty.pdf",
"path": "/Documents/BugBounty.pdf",
"size": 2947,
"mime": "application/pdf",
"class": "pdf",
Expand All @@ -76,6 +78,7 @@ Content-Type: application/vnd.api+json
"attributes": {
"type": "directory",
"name": "Music",
"name": "/Documents/Music",
"updated_at": "Thu, 02 May 2024 09:28:37 GMT",
"etag": "\"66335cc55204b\""
},
Expand All @@ -90,6 +93,7 @@ Content-Type: application/vnd.api+json
"attributes": {
"type": "directory",
"name": "Video",
"path": "/Documents/Video",
"updated_at": "Thu, 02 May 2024 09:29:53 GMT",
"etag": "\"66335d11c2318\""
},
Expand Down Expand Up @@ -430,3 +434,127 @@ HTTP/1.1 204 No Content
- 400 Bad Request, when the account is not configured for NextCloud
- 401 Unauthorized, when authentication to the NextCloud fails
- 404 Not Found, when the account is not found or the file is not found on the Cozy

## GET /remote/nextcloud/:account/trash/*

This route can be used to list the files and directories inside the trashbin
of NextCloud.

### Request (list)

```http
GET /remote/nextcloud/4ab2155707bb6613a8b9463daf00381b/trash/ HTTP/1.1
Host: cozy.example.net
Authorization: Bearer eyJhbG...
```

### Response (list)

```http
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
```

```json
{
"data": [
{
"type": "io.cozy.remote.nextcloud.files",
"id": "613281",
"attributes": {
"type": "directory",
"name": "Old",
"path": "/trash/Old.d93571568",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cballevre be careful, the path for things inside the trash is a bit weird and cannot be computed with just the parent and the name, so it was added to the response

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting clarification. I saw that you added the path everywhere. In cozy-client, I add a second attribute named parentPath. It corresponds to the folder path. This is to make it easier to replay requests with sift.js (commit with some explanation). Wouldn't it make sense to add it to the back as well, so we wouldn't have to calculate anything on the front?

"updated_at": "Tue, 25 Jun 2024 14:31:44 GMT",
"etag": "1719326384"
},
"meta": {},
"links": {
"self": "https://nextcloud.example.net/apps/files/trashbin/613281?dir=/Old"
taratatach marked this conversation as resolved.
Show resolved Hide resolved
}
}
]
}
```

#### Status codes

- 200 OK, for a success
- 401 Unauthorized, when authentication to the NextCloud fails
- 404 Not Found, when the account is not found or the directory is not found on the NextCloud

## POST /remote/nextcloud/:account/restore/*path

This route can be used to restore a file/directory from the trashbin on the
NextCloud.

The `:account` parameter is the identifier of the NextCloud `io.cozy.account`.

The `*path` parameter is the path of the file on the NextCloud.

**Note:** a permission on `POST io.cozy.files` is required to use this route.

### Request

```http
POST /remote/nextcloud/4ab2155707bb6613a8b9463daf00381b/restore/trash/Old.d93571568 HTTP/1.1
Host: cozy.example.net
Authorization: Bearer eyJhbG...
```

### Response

```http
HTTP/1.1 204 No Content
```

#### Status codes

- 204 No Content, when the file/directory has been restored
- 400 Bad Request, when the account is not configured for NextCloud
- 401 Unauthorized, when authentication to the NextCloud fails
- 404 Not Found, when the account is not found or the file/directory is not found on the NextCloud
- 409 Conflict, when a directory or file already exists where the file/directory should be restored on the NextCloud.

## DELETE /remote/nextcloud/:account/trash/*

This route can be used to delete a file in the trash.

### Request

```http
DELETE /remote/nextcloud/4ab2155707bb6613a8b9463daf00381b/trash/document-v1.docx.d64283654 HTTP/1.1
Host: cozy.example.net
Authorization: Bearer eyJhbG...
```

### Response

```http
HTTP/1.1 204 No Content
```

#### Status codes

- 204 No Content, when the file/directory has been put in the trash
- 400 Bad Request, when the account is not configured for NextCloud, or the `To` parameter is missing
- 401 Unauthorized, when authentication to the NextCloud fails
- 404 Not Found, when the account is not found or the file/directory is not found on the NextCloud

## DELETE /remote/nextcloud/:account/trash

This route can be used to empty the trash bin on NextCloud.

### Request

```http
DELETE /remote/nextcloud/4ab2155707bb6613a8b9463daf00381b/trash HTTP/1.1
Host: cozy.example.net
Authorization: Bearer eyJhbG...
```

### Response

```http
HTTP/1.1 204 No Content
```
94 changes: 86 additions & 8 deletions model/nextcloud/nextcloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"path/filepath"
"runtime"
"strconv"
"strings"
"time"

"github.com/cozy/cozy-stack/model/account"
Expand All @@ -35,6 +36,7 @@ type File struct {
DocID string `json:"id,omitempty"`
Type string `json:"type"`
Name string `json:"name"`
Path string `json:"path"`
Size uint64 `json:"size,omitempty"`
Mime string `json:"mime,omitempty"`
Class string `json:"class,omitempty"`
Expand Down Expand Up @@ -62,6 +64,7 @@ var _ jsonapi.Object = (*File)(nil)
type NextCloud struct {
inst *instance.Instance
accountID string
userID string
webdav *webdav.Client
}

Expand Down Expand Up @@ -99,48 +102,68 @@ func New(inst *instance.Instance, accountID string) (*NextCloud, error) {
Host: u.Host,
Username: username,
Password: password,
BasePath: "/remote.php/dav",
Logger: logger,
}
nc := &NextCloud{
inst: inst,
accountID: accountID,
webdav: webdav,
}
if err := nc.fillBasePath(&doc); err != nil {
if err := nc.fillUserID(&doc); err != nil {
return nil, err
}
return nc, nil
}

func (nc *NextCloud) Download(path string) (*webdav.Download, error) {
return nc.webdav.Get(path)
return nc.webdav.Get("/files/" + nc.userID + "/" + path)
nono marked this conversation as resolved.
Show resolved Hide resolved
}

func (nc *NextCloud) Upload(path, mime string, contentLength int64, body io.Reader) error {
headers := map[string]string{
echo.HeaderContentType: mime,
}
path = "/files/" + nc.userID + "/" + path
nono marked this conversation as resolved.
Show resolved Hide resolved
return nc.webdav.Put(path, contentLength, headers, body)
}

func (nc *NextCloud) Mkdir(path string) error {
return nc.webdav.Mkcol(path)
return nc.webdav.Mkcol("/files/" + nc.userID + "/" + path)
}

func (nc *NextCloud) Delete(path string) error {
return nc.webdav.Delete(path)
return nc.webdav.Delete("/files/" + nc.userID + "/" + path)
}

func (nc *NextCloud) Move(oldPath, newPath string) error {
oldPath = "/files/" + nc.userID + "/" + oldPath
newPath = "/files/" + nc.userID + "/" + newPath
return nc.webdav.Move(oldPath, newPath)
}

func (nc *NextCloud) Copy(oldPath, newPath string) error {
oldPath = "/files/" + nc.userID + "/" + oldPath
newPath = "/files/" + nc.userID + "/" + newPath
return nc.webdav.Copy(oldPath, newPath)
}

func (nc *NextCloud) Restore(path string) error {
path = "/trashbin/" + nc.userID + "/" + path
dst := "/trashbin/" + nc.userID + "/restore/" + filepath.Base(path)
return nc.webdav.Move(path, dst)
}

func (nc *NextCloud) DeleteTrash(path string) error {
return nc.webdav.Delete("/trashbin/" + nc.userID + "/" + path)
}

func (nc *NextCloud) EmptyTrash() error {
return nc.webdav.Delete("/trashbin/" + nc.userID + "/trash")
}

func (nc *NextCloud) ListFiles(path string) ([]jsonapi.Object, error) {
items, err := nc.webdav.List(path)
items, err := nc.webdav.List("/files/" + nc.userID + "/" + path)
if err != nil {
return nil, err
}
Expand All @@ -155,6 +178,7 @@ func (nc *NextCloud) ListFiles(path string) ([]jsonapi.Object, error) {
DocID: item.ID,
Type: item.Type,
Name: item.Name,
Path: "/" + filepath.Join(path, filepath.Base(item.Href)),
Size: item.Size,
Mime: mime,
Class: class,
Expand All @@ -167,7 +191,38 @@ func (nc *NextCloud) ListFiles(path string) ([]jsonapi.Object, error) {
return files, nil
}

func (nc *NextCloud) ListTrashed(path string) ([]jsonapi.Object, error) {
path = "/trash/" + path
items, err := nc.webdav.List("/trashbin/" + nc.userID + path)
if err != nil {
return nil, err
}

var files []jsonapi.Object
for _, item := range items {
var mime, class string
if item.Type == "file" {
mime, class = vfs.ExtractMimeAndClassFromFilename(item.TrashedName)
}
file := &File{
DocID: item.ID,
Type: item.Type,
Name: item.TrashedName,
Path: filepath.Join(path, filepath.Base(item.Href)),
Size: item.Size,
Mime: mime,
Class: class,
UpdatedAt: item.LastModified,
ETag: item.ETag,
url: nc.buildTrashedURL(item, path),
}
files = append(files, file)
}
return files, nil
}

func (nc *NextCloud) Downstream(path, dirID string, kind OperationKind, cozyMetadata *vfs.FilesCozyMetadata) (*vfs.FileDoc, error) {
path = "/files/" + nc.userID + "/" + path
dl, err := nc.webdav.Get(path)
if err != nil {
return nil, err
Expand Down Expand Up @@ -215,6 +270,7 @@ func (nc *NextCloud) Downstream(path, dirID string, kind OperationKind, cozyMeta
}

func (nc *NextCloud) Upstream(path, from string, kind OperationKind) error {
path = "/files/" + nc.userID + "/" + path
fs := nc.inst.VFS()
doc, err := fs.FileByID(from)
if err != nil {
Expand All @@ -238,18 +294,18 @@ func (nc *NextCloud) Upstream(path, from string, kind OperationKind) error {
return nil
}

func (nc *NextCloud) fillBasePath(accountDoc *couchdb.JSONDoc) error {
func (nc *NextCloud) fillUserID(accountDoc *couchdb.JSONDoc) error {
userID, _ := accountDoc.M["webdav_user_id"].(string)
if userID != "" {
nc.webdav.BasePath = "/remote.php/dav/files/" + userID
nc.userID = userID
return nil
}

userID, err := nc.fetchUserID()
if err != nil {
return err
}
nc.webdav.BasePath = "/remote.php/dav/files/" + userID
nc.userID = userID

// Try to persist the userID to avoid fetching it for every WebDAV request
accountDoc.M["webdav_user_id"] = userID
Expand All @@ -266,6 +322,28 @@ func (nc *NextCloud) buildURL(item webdav.Item, path string) string {
Path: "/apps/files/files/" + item.ID,
RawQuery: "dir=/" + path,
}
if item.Type == "directory" {
if !strings.HasSuffix(u.RawQuery, "/") {
u.RawQuery += "/"
}
u.RawQuery += item.Name
}
return u.String()
}

func (nc *NextCloud) buildTrashedURL(item webdav.Item, path string) string {
u := &url.URL{
Scheme: nc.webdav.Scheme,
Host: nc.webdav.Host,
Path: "/apps/files/trashbin/" + item.ID,
RawQuery: "dir=" + strings.TrimPrefix(path, "/trash"),
}
if item.Type == "directory" {
if !strings.HasSuffix(u.RawQuery, "/") {
u.RawQuery += "/"
}
u.RawQuery += item.TrashedName
}
return u.String()
}

Expand Down
Loading
Loading