Skip to content

Commit

Permalink
Manage the Nextcloud trash (#4432)
Browse files Browse the repository at this point in the history
  • Loading branch information
nono authored Jul 2, 2024
2 parents 8797be3 + 1083643 commit 8a145e0
Show file tree
Hide file tree
Showing 4 changed files with 302 additions and 12 deletions.
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",
"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"
}
}
]
}
```

#### 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)
}

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
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

0 comments on commit 8a145e0

Please sign in to comment.