Skip to content

Commit fcbe1f3

Browse files
committed
PATCH /api/resource reconstruction
1 parent 6fd3934 commit fcbe1f3

File tree

7 files changed

+277
-226
lines changed

7 files changed

+277
-226
lines changed

pkg/drives/base.go

+37-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type ResourceService interface {
2525
DeleteHandler(fileCache fileutils.FileCache) handleFunc
2626
PostHandler(w http.ResponseWriter, r *http.Request, d *common.Data) (int, error)
2727
PutHandler(w http.ResponseWriter, r *http.Request, d *common.Data) (int, error)
28-
//PatchHandle(w http.ResponseWriter, r *http.Request, d *common.Data) (int, error)
28+
PatchHandler(fileCache fileutils.FileCache) handleFunc
2929
}
3030

3131
var (
@@ -324,3 +324,39 @@ func (rs *BaseResourceService) PutHandler(w http.ResponseWriter, r *http.Request
324324

325325
return common.ErrToStatus(err), err
326326
}
327+
328+
func (rs *BaseResourceService) PatchHandler(fileCache fileutils.FileCache) handleFunc {
329+
return func(w http.ResponseWriter, r *http.Request, d *common.Data) (int, error) {
330+
src := r.URL.Path
331+
dst := r.URL.Query().Get("destination")
332+
action := r.URL.Query().Get("action")
333+
dst, err := common.UnescapeURLIfEscaped(dst)
334+
335+
if err != nil {
336+
return common.ErrToStatus(err), err
337+
}
338+
if dst == "/" || src == "/" {
339+
return http.StatusForbidden, nil
340+
}
341+
342+
err = fileutils.CheckParent(src, dst)
343+
if err != nil {
344+
return http.StatusBadRequest, err
345+
}
346+
347+
rename := r.URL.Query().Get("rename") == "true"
348+
if !rename {
349+
if _, err = files.DefaultFs.Stat(dst); err == nil {
350+
return http.StatusConflict, nil
351+
}
352+
}
353+
if rename {
354+
dst = fileutils.AddVersionSuffix(dst, files.DefaultFs, strings.HasSuffix(src, "/"))
355+
}
356+
357+
klog.Infoln("Before patch action:", src, dst, action, rename)
358+
err = fileutils.PatchAction(r.Context(), action, src, dst, d, fileCache)
359+
360+
return common.ErrToStatus(err), err
361+
}
362+
}

pkg/drives/cloud_drive.go

+6
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,12 @@ func (rc *CloudDriveResourceService) PutHandler(w http.ResponseWriter, r *http.R
10001000
return http.StatusNotImplemented, fmt.Errorf("cloud drive does not supoort editing files")
10011001
}
10021002

1003+
func (rc *CloudDriveResourceService) PatchHandler(fileCache fileutils.FileCache) handleFunc {
1004+
return func(w http.ResponseWriter, r *http.Request, d *common.Data) (int, error) {
1005+
return ResourcePatchCloudDrive(fileCache, w, r)
1006+
}
1007+
}
1008+
10031009
func ResourceDeleteCloudDrive(fileCache fileutils.FileCache, src string, w http.ResponseWriter, r *http.Request, returnResp bool) ([]byte, int, error) {
10041010
if src == "" {
10051011
src = r.URL.Path

pkg/drives/google_drive.go

+6
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,12 @@ func (rc *GoogleDriveResourceService) PutHandler(w http.ResponseWriter, r *http.
11791179
return http.StatusNotImplemented, fmt.Errorf("google drive does not supoort editing files")
11801180
}
11811181

1182+
func (rc *GoogleDriveResourceService) PatchHandler(fileCache fileutils.FileCache) handleFunc {
1183+
return func(w http.ResponseWriter, r *http.Request, d *common.Data) (int, error) {
1184+
return ResourcePatchGoogle(fileCache, w, r)
1185+
}
1186+
}
1187+
11821188
func ResourceDeleteGoogle(fileCache fileutils.FileCache, src string, w http.ResponseWriter, r *http.Request, returnResp bool) ([]byte, int, error) {
11831189
if src == "" {
11841190
src = r.URL.Path

pkg/drives/sync.go

+134
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
e "errors"
99
"files/pkg/common"
10+
"files/pkg/errors"
1011
"files/pkg/fileutils"
1112
"fmt"
1213
"io"
@@ -156,6 +157,25 @@ func (rc *SyncResourceService) PutHandler(w http.ResponseWriter, r *http.Request
156157
return http.StatusNotImplemented, fmt.Errorf("sync drive does not supoort editing files for the time being")
157158
}
158159

160+
func (rc *SyncResourceService) PatchHandler(w http.ResponseWriter, r *http.Request, d *common.Data) (int, error) {
161+
src := r.URL.Path
162+
dst := r.URL.Query().Get("destination")
163+
164+
action := r.URL.Query().Get("action")
165+
var err error
166+
src, err = common.UnescapeURLIfEscaped(src)
167+
if err != nil {
168+
return common.ErrToStatus(err), err
169+
}
170+
dst, err = common.UnescapeURLIfEscaped(dst)
171+
if err != nil {
172+
return common.ErrToStatus(err), err
173+
}
174+
175+
err = ResourceSyncPatch(action, src, dst, r)
176+
return common.ErrToStatus(err), err
177+
}
178+
159179
type Dirent struct {
160180
Type string `json:"type"`
161181
ID string `json:"id"`
@@ -760,3 +780,117 @@ func SyncPermToMode(permStr string) os.FileMode {
760780

761781
return perm
762782
}
783+
784+
func ResourceSyncPatch(action, src, dst string, r *http.Request) error {
785+
var apiName string
786+
switch action {
787+
case "copy":
788+
apiName = "sync-batch-copy-item"
789+
case "rename":
790+
apiName = "sync-batch-move-item"
791+
default:
792+
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
793+
}
794+
795+
// It seems that we can't mkdir althrough when using sync-bacth-copy/move-item, so we must use false for isDir here.
796+
if err := SyncMkdirAll(dst, 0, false, r); err != nil {
797+
return err
798+
}
799+
800+
src = strings.Trim(src, "/")
801+
if !strings.Contains(src, "/") {
802+
err := e.New("invalid path format: path must contain at least one '/'")
803+
klog.Errorln("Error:", err)
804+
return err
805+
}
806+
807+
srcFirstSlashIdx := strings.Index(src, "/")
808+
809+
srcRepoID := src[:srcFirstSlashIdx]
810+
811+
srcLastSlashIdx := strings.LastIndex(src, "/")
812+
813+
srcFilename := src[srcLastSlashIdx+1:]
814+
815+
srcPrefix := ""
816+
if srcFirstSlashIdx != srcLastSlashIdx {
817+
srcPrefix = src[srcFirstSlashIdx+1 : srcLastSlashIdx+1]
818+
}
819+
820+
if srcPrefix != "" {
821+
srcPrefix = "/" + srcPrefix
822+
} else {
823+
srcPrefix = "/"
824+
}
825+
826+
dst = strings.Trim(dst, "/")
827+
if !strings.Contains(dst, "/") {
828+
err := e.New("invalid path format: path must contain at least one '/'")
829+
klog.Errorln("Error:", err)
830+
return err
831+
}
832+
833+
dstFirstSlashIdx := strings.Index(dst, "/")
834+
835+
dstRepoID := dst[:dstFirstSlashIdx]
836+
837+
dstLastSlashIdx := strings.LastIndex(dst, "/")
838+
839+
dstPrefix := ""
840+
if dstFirstSlashIdx != dstLastSlashIdx {
841+
dstPrefix = dst[dstFirstSlashIdx+1 : dstLastSlashIdx+1]
842+
}
843+
844+
if dstPrefix != "" {
845+
dstPrefix = "/" + dstPrefix
846+
} else {
847+
dstPrefix = "/"
848+
}
849+
850+
targetURL := "http://127.0.0.1:80/seahub/api/v2.1/repos/" + apiName + "/"
851+
requestBody := map[string]interface{}{
852+
"dst_parent_dir": dstPrefix,
853+
"dst_repo_id": dstRepoID,
854+
"src_dirents": []string{srcFilename},
855+
"src_parent_dir": srcPrefix,
856+
"src_repo_id": srcRepoID,
857+
}
858+
klog.Infoln(requestBody)
859+
jsonBody, err := json.Marshal(requestBody)
860+
if err != nil {
861+
return err
862+
}
863+
864+
request, err := http.NewRequest("POST", targetURL, bytes.NewBuffer(jsonBody))
865+
if err != nil {
866+
return err
867+
}
868+
869+
request.Header = r.Header
870+
request.Header.Set("Content-Type", "application/json")
871+
872+
client := &http.Client{
873+
Timeout: 10 * time.Second,
874+
}
875+
876+
response, err := client.Do(request)
877+
if err != nil {
878+
return err
879+
}
880+
defer response.Body.Close()
881+
882+
// Read the response body as a string
883+
postBody, err := io.ReadAll(response.Body)
884+
klog.Infoln("ReadAll")
885+
if err != nil {
886+
klog.Errorln("ReadAll error: ", err)
887+
return err
888+
}
889+
890+
if response.StatusCode != http.StatusOK {
891+
klog.Infoln(string(postBody))
892+
return fmt.Errorf("file paste failed with status: %d", response.StatusCode)
893+
}
894+
895+
return nil
896+
}

pkg/fileutils/patch.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package fileutils
2+
3+
import (
4+
"context"
5+
"files/pkg/common"
6+
"files/pkg/errors"
7+
"files/pkg/files"
8+
"files/pkg/preview"
9+
"fmt"
10+
"github.com/spf13/afero"
11+
"path"
12+
"path/filepath"
13+
"strings"
14+
)
15+
16+
func CheckParent(src, dst string) error {
17+
rel, err := filepath.Rel(src, dst)
18+
if err != nil {
19+
return err
20+
}
21+
22+
rel = filepath.ToSlash(rel)
23+
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
24+
return errors.ErrSourceIsParent
25+
}
26+
27+
return nil
28+
}
29+
30+
func AddVersionSuffix(source string, fs afero.Fs, isDir bool) string {
31+
counter := 1
32+
dir, name := path.Split(source)
33+
ext := ""
34+
base := name
35+
if !isDir {
36+
ext = filepath.Ext(name)
37+
base = strings.TrimSuffix(name, ext)
38+
}
39+
40+
for {
41+
if _, err := fs.Stat(source); err != nil {
42+
break
43+
}
44+
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
45+
source = path.Join(dir, renamed)
46+
counter++
47+
}
48+
49+
return source
50+
}
51+
52+
func PatchAction(ctx context.Context, action, src, dst string, d *common.Data, fileCache FileCache) error {
53+
switch action {
54+
case "copy":
55+
return Copy(files.DefaultFs, src, dst)
56+
case "rename":
57+
src = path.Clean("/" + src)
58+
dst = path.Clean("/" + dst)
59+
60+
file, err := files.NewFileInfo(files.FileOptions{
61+
Fs: files.DefaultFs,
62+
Path: src,
63+
Modify: true,
64+
Expand: false,
65+
ReadHeader: false,
66+
})
67+
if err != nil {
68+
return err
69+
}
70+
71+
// delete thumbnails
72+
err = preview.DelThumbs(ctx, fileCache, file)
73+
if err != nil {
74+
return err
75+
}
76+
77+
return MoveFile(files.DefaultFs, src, dst)
78+
default:
79+
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
80+
}
81+
}

0 commit comments

Comments
 (0)