Skip to content

Commit 8a145e0

Browse files
authored
Manage the Nextcloud trash (#4432)
2 parents 8797be3 + 1083643 commit 8a145e0

File tree

4 files changed

+302
-12
lines changed

4 files changed

+302
-12
lines changed

docs/nextcloud.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Content-Type: application/vnd.api+json
4545
"attributes": {
4646
"type": "directory",
4747
"name": "Images",
48+
"path": "/Documents/Images",
4849
"updated_at": "Thu, 02 May 2024 09:29:53 GMT",
4950
"etag": "\"66335d11c4b91\""
5051
},
@@ -59,6 +60,7 @@ Content-Type: application/vnd.api+json
5960
"attributes": {
6061
"type": "file",
6162
"name": "BugBounty.pdf",
63+
"path": "/Documents/BugBounty.pdf",
6264
"size": 2947,
6365
"mime": "application/pdf",
6466
"class": "pdf",
@@ -76,6 +78,7 @@ Content-Type: application/vnd.api+json
7678
"attributes": {
7779
"type": "directory",
7880
"name": "Music",
81+
"name": "/Documents/Music",
7982
"updated_at": "Thu, 02 May 2024 09:28:37 GMT",
8083
"etag": "\"66335cc55204b\""
8184
},
@@ -90,6 +93,7 @@ Content-Type: application/vnd.api+json
9093
"attributes": {
9194
"type": "directory",
9295
"name": "Video",
96+
"path": "/Documents/Video",
9397
"updated_at": "Thu, 02 May 2024 09:29:53 GMT",
9498
"etag": "\"66335d11c2318\""
9599
},
@@ -430,3 +434,127 @@ HTTP/1.1 204 No Content
430434
- 400 Bad Request, when the account is not configured for NextCloud
431435
- 401 Unauthorized, when authentication to the NextCloud fails
432436
- 404 Not Found, when the account is not found or the file is not found on the Cozy
437+
438+
## GET /remote/nextcloud/:account/trash/*
439+
440+
This route can be used to list the files and directories inside the trashbin
441+
of NextCloud.
442+
443+
### Request (list)
444+
445+
```http
446+
GET /remote/nextcloud/4ab2155707bb6613a8b9463daf00381b/trash/ HTTP/1.1
447+
Host: cozy.example.net
448+
Authorization: Bearer eyJhbG...
449+
```
450+
451+
### Response (list)
452+
453+
```http
454+
HTTP/1.1 200 OK
455+
Content-Type: application/vnd.api+json
456+
```
457+
458+
```json
459+
{
460+
"data": [
461+
{
462+
"type": "io.cozy.remote.nextcloud.files",
463+
"id": "613281",
464+
"attributes": {
465+
"type": "directory",
466+
"name": "Old",
467+
"path": "/trash/Old.d93571568",
468+
"updated_at": "Tue, 25 Jun 2024 14:31:44 GMT",
469+
"etag": "1719326384"
470+
},
471+
"meta": {},
472+
"links": {
473+
"self": "https://nextcloud.example.net/apps/files/trashbin/613281?dir=/Old"
474+
}
475+
}
476+
]
477+
}
478+
```
479+
480+
#### Status codes
481+
482+
- 200 OK, for a success
483+
- 401 Unauthorized, when authentication to the NextCloud fails
484+
- 404 Not Found, when the account is not found or the directory is not found on the NextCloud
485+
486+
## POST /remote/nextcloud/:account/restore/*path
487+
488+
This route can be used to restore a file/directory from the trashbin on the
489+
NextCloud.
490+
491+
The `:account` parameter is the identifier of the NextCloud `io.cozy.account`.
492+
493+
The `*path` parameter is the path of the file on the NextCloud.
494+
495+
**Note:** a permission on `POST io.cozy.files` is required to use this route.
496+
497+
### Request
498+
499+
```http
500+
POST /remote/nextcloud/4ab2155707bb6613a8b9463daf00381b/restore/trash/Old.d93571568 HTTP/1.1
501+
Host: cozy.example.net
502+
Authorization: Bearer eyJhbG...
503+
```
504+
505+
### Response
506+
507+
```http
508+
HTTP/1.1 204 No Content
509+
```
510+
511+
#### Status codes
512+
513+
- 204 No Content, when the file/directory has been restored
514+
- 400 Bad Request, when the account is not configured for NextCloud
515+
- 401 Unauthorized, when authentication to the NextCloud fails
516+
- 404 Not Found, when the account is not found or the file/directory is not found on the NextCloud
517+
- 409 Conflict, when a directory or file already exists where the file/directory should be restored on the NextCloud.
518+
519+
## DELETE /remote/nextcloud/:account/trash/*
520+
521+
This route can be used to delete a file in the trash.
522+
523+
### Request
524+
525+
```http
526+
DELETE /remote/nextcloud/4ab2155707bb6613a8b9463daf00381b/trash/document-v1.docx.d64283654 HTTP/1.1
527+
Host: cozy.example.net
528+
Authorization: Bearer eyJhbG...
529+
```
530+
531+
### Response
532+
533+
```http
534+
HTTP/1.1 204 No Content
535+
```
536+
537+
#### Status codes
538+
539+
- 204 No Content, when the file/directory has been put in the trash
540+
- 400 Bad Request, when the account is not configured for NextCloud, or the `To` parameter is missing
541+
- 401 Unauthorized, when authentication to the NextCloud fails
542+
- 404 Not Found, when the account is not found or the file/directory is not found on the NextCloud
543+
544+
## DELETE /remote/nextcloud/:account/trash
545+
546+
This route can be used to empty the trash bin on NextCloud.
547+
548+
### Request
549+
550+
```http
551+
DELETE /remote/nextcloud/4ab2155707bb6613a8b9463daf00381b/trash HTTP/1.1
552+
Host: cozy.example.net
553+
Authorization: Bearer eyJhbG...
554+
```
555+
556+
### Response
557+
558+
```http
559+
HTTP/1.1 204 No Content
560+
```

model/nextcloud/nextcloud.go

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path/filepath"
1111
"runtime"
1212
"strconv"
13+
"strings"
1314
"time"
1415

1516
"github.com/cozy/cozy-stack/model/account"
@@ -35,6 +36,7 @@ type File struct {
3536
DocID string `json:"id,omitempty"`
3637
Type string `json:"type"`
3738
Name string `json:"name"`
39+
Path string `json:"path"`
3840
Size uint64 `json:"size,omitempty"`
3941
Mime string `json:"mime,omitempty"`
4042
Class string `json:"class,omitempty"`
@@ -62,6 +64,7 @@ var _ jsonapi.Object = (*File)(nil)
6264
type NextCloud struct {
6365
inst *instance.Instance
6466
accountID string
67+
userID string
6568
webdav *webdav.Client
6669
}
6770

@@ -99,48 +102,68 @@ func New(inst *instance.Instance, accountID string) (*NextCloud, error) {
99102
Host: u.Host,
100103
Username: username,
101104
Password: password,
105+
BasePath: "/remote.php/dav",
102106
Logger: logger,
103107
}
104108
nc := &NextCloud{
105109
inst: inst,
106110
accountID: accountID,
107111
webdav: webdav,
108112
}
109-
if err := nc.fillBasePath(&doc); err != nil {
113+
if err := nc.fillUserID(&doc); err != nil {
110114
return nil, err
111115
}
112116
return nc, nil
113117
}
114118

115119
func (nc *NextCloud) Download(path string) (*webdav.Download, error) {
116-
return nc.webdav.Get(path)
120+
return nc.webdav.Get("/files/" + nc.userID + "/" + path)
117121
}
118122

119123
func (nc *NextCloud) Upload(path, mime string, contentLength int64, body io.Reader) error {
120124
headers := map[string]string{
121125
echo.HeaderContentType: mime,
122126
}
127+
path = "/files/" + nc.userID + "/" + path
123128
return nc.webdav.Put(path, contentLength, headers, body)
124129
}
125130

126131
func (nc *NextCloud) Mkdir(path string) error {
127-
return nc.webdav.Mkcol(path)
132+
return nc.webdav.Mkcol("/files/" + nc.userID + "/" + path)
128133
}
129134

130135
func (nc *NextCloud) Delete(path string) error {
131-
return nc.webdav.Delete(path)
136+
return nc.webdav.Delete("/files/" + nc.userID + "/" + path)
132137
}
133138

134139
func (nc *NextCloud) Move(oldPath, newPath string) error {
140+
oldPath = "/files/" + nc.userID + "/" + oldPath
141+
newPath = "/files/" + nc.userID + "/" + newPath
135142
return nc.webdav.Move(oldPath, newPath)
136143
}
137144

138145
func (nc *NextCloud) Copy(oldPath, newPath string) error {
146+
oldPath = "/files/" + nc.userID + "/" + oldPath
147+
newPath = "/files/" + nc.userID + "/" + newPath
139148
return nc.webdav.Copy(oldPath, newPath)
140149
}
141150

151+
func (nc *NextCloud) Restore(path string) error {
152+
path = "/trashbin/" + nc.userID + "/" + path
153+
dst := "/trashbin/" + nc.userID + "/restore/" + filepath.Base(path)
154+
return nc.webdav.Move(path, dst)
155+
}
156+
157+
func (nc *NextCloud) DeleteTrash(path string) error {
158+
return nc.webdav.Delete("/trashbin/" + nc.userID + "/" + path)
159+
}
160+
161+
func (nc *NextCloud) EmptyTrash() error {
162+
return nc.webdav.Delete("/trashbin/" + nc.userID + "/trash")
163+
}
164+
142165
func (nc *NextCloud) ListFiles(path string) ([]jsonapi.Object, error) {
143-
items, err := nc.webdav.List(path)
166+
items, err := nc.webdav.List("/files/" + nc.userID + "/" + path)
144167
if err != nil {
145168
return nil, err
146169
}
@@ -155,6 +178,7 @@ func (nc *NextCloud) ListFiles(path string) ([]jsonapi.Object, error) {
155178
DocID: item.ID,
156179
Type: item.Type,
157180
Name: item.Name,
181+
Path: "/" + filepath.Join(path, filepath.Base(item.Href)),
158182
Size: item.Size,
159183
Mime: mime,
160184
Class: class,
@@ -167,7 +191,38 @@ func (nc *NextCloud) ListFiles(path string) ([]jsonapi.Object, error) {
167191
return files, nil
168192
}
169193

194+
func (nc *NextCloud) ListTrashed(path string) ([]jsonapi.Object, error) {
195+
path = "/trash/" + path
196+
items, err := nc.webdav.List("/trashbin/" + nc.userID + path)
197+
if err != nil {
198+
return nil, err
199+
}
200+
201+
var files []jsonapi.Object
202+
for _, item := range items {
203+
var mime, class string
204+
if item.Type == "file" {
205+
mime, class = vfs.ExtractMimeAndClassFromFilename(item.TrashedName)
206+
}
207+
file := &File{
208+
DocID: item.ID,
209+
Type: item.Type,
210+
Name: item.TrashedName,
211+
Path: filepath.Join(path, filepath.Base(item.Href)),
212+
Size: item.Size,
213+
Mime: mime,
214+
Class: class,
215+
UpdatedAt: item.LastModified,
216+
ETag: item.ETag,
217+
url: nc.buildTrashedURL(item, path),
218+
}
219+
files = append(files, file)
220+
}
221+
return files, nil
222+
}
223+
170224
func (nc *NextCloud) Downstream(path, dirID string, kind OperationKind, cozyMetadata *vfs.FilesCozyMetadata) (*vfs.FileDoc, error) {
225+
path = "/files/" + nc.userID + "/" + path
171226
dl, err := nc.webdav.Get(path)
172227
if err != nil {
173228
return nil, err
@@ -215,6 +270,7 @@ func (nc *NextCloud) Downstream(path, dirID string, kind OperationKind, cozyMeta
215270
}
216271

217272
func (nc *NextCloud) Upstream(path, from string, kind OperationKind) error {
273+
path = "/files/" + nc.userID + "/" + path
218274
fs := nc.inst.VFS()
219275
doc, err := fs.FileByID(from)
220276
if err != nil {
@@ -238,18 +294,18 @@ func (nc *NextCloud) Upstream(path, from string, kind OperationKind) error {
238294
return nil
239295
}
240296

241-
func (nc *NextCloud) fillBasePath(accountDoc *couchdb.JSONDoc) error {
297+
func (nc *NextCloud) fillUserID(accountDoc *couchdb.JSONDoc) error {
242298
userID, _ := accountDoc.M["webdav_user_id"].(string)
243299
if userID != "" {
244-
nc.webdav.BasePath = "/remote.php/dav/files/" + userID
300+
nc.userID = userID
245301
return nil
246302
}
247303

248304
userID, err := nc.fetchUserID()
249305
if err != nil {
250306
return err
251307
}
252-
nc.webdav.BasePath = "/remote.php/dav/files/" + userID
308+
nc.userID = userID
253309

254310
// Try to persist the userID to avoid fetching it for every WebDAV request
255311
accountDoc.M["webdav_user_id"] = userID
@@ -266,6 +322,28 @@ func (nc *NextCloud) buildURL(item webdav.Item, path string) string {
266322
Path: "/apps/files/files/" + item.ID,
267323
RawQuery: "dir=/" + path,
268324
}
325+
if item.Type == "directory" {
326+
if !strings.HasSuffix(u.RawQuery, "/") {
327+
u.RawQuery += "/"
328+
}
329+
u.RawQuery += item.Name
330+
}
331+
return u.String()
332+
}
333+
334+
func (nc *NextCloud) buildTrashedURL(item webdav.Item, path string) string {
335+
u := &url.URL{
336+
Scheme: nc.webdav.Scheme,
337+
Host: nc.webdav.Host,
338+
Path: "/apps/files/trashbin/" + item.ID,
339+
RawQuery: "dir=" + strings.TrimPrefix(path, "/trash"),
340+
}
341+
if item.Type == "directory" {
342+
if !strings.HasSuffix(u.RawQuery, "/") {
343+
u.RawQuery += "/"
344+
}
345+
u.RawQuery += item.TrashedName
346+
}
269347
return u.String()
270348
}
271349

0 commit comments

Comments
 (0)