mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
234 lines
7.2 KiB
Go
234 lines
7.2 KiB
Go
package cloud
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"path"
|
||
"strings"
|
||
)
|
||
|
||
func (p *cloudDrive2Provider) Mkdir(ctx context.Context, parentDir, name string) (*FileEntry, error) {
|
||
cleanName, err := cleanCloudEntryName(name)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
parent := normalizeCloudDAVPath(parentDir)
|
||
target := joinOpenListAPIPath(parent, cleanName)
|
||
if p.typ == TypeOpenList && p.apiBase != nil && p.hasOpenListAPICredentials() {
|
||
if err := p.openListAPIMkdir(ctx, target); err != nil {
|
||
return nil, err
|
||
}
|
||
return &FileEntry{ID: target, Name: cleanName, IsDir: true}, nil
|
||
}
|
||
if err := p.webDAVMkdir(ctx, target); err != nil {
|
||
return nil, err
|
||
}
|
||
return &FileEntry{ID: target, Name: cleanName, IsDir: true}, nil
|
||
}
|
||
|
||
func (p *cloudDrive2Provider) Rename(ctx context.Context, ref, name string) (*FileEntry, error) {
|
||
cleanName, err := cleanCloudEntryName(name)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
source := normalizeCloudDAVPath(ref)
|
||
if source == "/" {
|
||
return nil, fmt.Errorf("%s: cannot rename root directory", p.name)
|
||
}
|
||
target := joinOpenListAPIPath(path.Dir(source), cleanName)
|
||
if p.typ == TypeOpenList && p.apiBase != nil && p.hasOpenListAPICredentials() {
|
||
if err := p.openListAPIRename(ctx, source, cleanName); err != nil {
|
||
return nil, err
|
||
}
|
||
return &FileEntry{ID: target, Name: cleanName, IsDir: true}, nil
|
||
}
|
||
if err := p.webDAVRename(ctx, source, target); err != nil {
|
||
return nil, err
|
||
}
|
||
return &FileEntry{ID: target, Name: cleanName, IsDir: true}, nil
|
||
}
|
||
|
||
func (p *cloudDrive2Provider) Move(ctx context.Context, ref, targetDir, name string) (*FileEntry, error) {
|
||
source := normalizeCloudDAVPath(ref)
|
||
if source == "/" {
|
||
return nil, fmt.Errorf("%s: cannot move root directory", p.name)
|
||
}
|
||
cleanName := strings.TrimSpace(name)
|
||
if cleanName == "" {
|
||
cleanName = path.Base(source)
|
||
}
|
||
var err error
|
||
cleanName, err = cleanCloudEntryName(cleanName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
targetDir = normalizeCloudDAVPath(targetDir)
|
||
target := joinOpenListAPIPath(targetDir, cleanName)
|
||
if sameCloudDAVPath(source, target) {
|
||
return &FileEntry{ID: target, Name: cleanName}, nil
|
||
}
|
||
if p.typ == TypeOpenList && p.apiBase != nil && p.hasOpenListAPICredentials() {
|
||
if err := p.openListAPIMove(ctx, source, targetDir, cleanName); err != nil {
|
||
return nil, err
|
||
}
|
||
return &FileEntry{ID: target, Name: cleanName}, nil
|
||
}
|
||
if err := p.webDAVRename(ctx, source, target); err != nil {
|
||
return nil, err
|
||
}
|
||
return &FileEntry{ID: target, Name: cleanName}, nil
|
||
}
|
||
|
||
func cleanCloudEntryName(name string) (string, error) {
|
||
name = strings.TrimSpace(name)
|
||
if name == "" || name == "." || name == ".." {
|
||
return "", fmt.Errorf("entry name is required")
|
||
}
|
||
if strings.ContainsAny(name, `/\`) {
|
||
return "", fmt.Errorf("entry name cannot contain path separators")
|
||
}
|
||
return name, nil
|
||
}
|
||
|
||
func (p *cloudDrive2Provider) openListAPIMkdir(ctx context.Context, target string) error {
|
||
return p.openListAPIPost(ctx, "/api/fs/mkdir", map[string]string{"path": normalizeCloudDAVPath(target)}, "mkdir")
|
||
}
|
||
|
||
func (p *cloudDrive2Provider) openListAPIRename(ctx context.Context, source, name string) error {
|
||
return p.openListAPIPost(ctx, "/api/fs/rename", map[string]string{
|
||
"path": normalizeCloudDAVPath(source),
|
||
"name": name,
|
||
}, "rename")
|
||
}
|
||
|
||
func (p *cloudDrive2Provider) openListAPIMove(ctx context.Context, source, targetDir, targetName string) error {
|
||
targetDir = normalizeCloudDAVPath(targetDir)
|
||
sourceName := path.Base(normalizeCloudDAVPath(source))
|
||
if sameCloudDAVPath(path.Dir(source), targetDir) {
|
||
if sourceName == targetName {
|
||
return nil
|
||
}
|
||
return p.openListAPIRename(ctx, source, targetName)
|
||
}
|
||
if err := p.openListAPIPost(ctx, "/api/fs/move", map[string]any{
|
||
"src_dir": normalizeCloudDAVPath(path.Dir(source)),
|
||
"dst_dir": targetDir,
|
||
"names": []string{sourceName},
|
||
}, "move"); err != nil {
|
||
return err
|
||
}
|
||
if sourceName != targetName {
|
||
moved := joinOpenListAPIPath(targetDir, sourceName)
|
||
return p.openListAPIRename(ctx, moved, targetName)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (p *cloudDrive2Provider) openListAPIPost(ctx context.Context, apiPath string, payload any, action string) error {
|
||
token, err := p.openListAPIToken(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
body, _ := json.Marshal(payload)
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.openListAPIURL(apiPath), bytes.NewReader(body))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Accept", "application/json")
|
||
req.Header.Set("User-Agent", p.ua)
|
||
if token != "" {
|
||
req.Header.Set("Authorization", token)
|
||
}
|
||
resp, err := p.client.Do(req)
|
||
if err != nil {
|
||
return decorateDAVTransportError(p.name, p.openListAPIURL(apiPath), err)
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
return fmt.Errorf("%s: api %s returned http %d", p.name, action, resp.StatusCode)
|
||
}
|
||
var decoded struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
}
|
||
if err := json.NewDecoder(io.LimitReader(resp.Body, 4<<20)).Decode(&decoded); err != nil {
|
||
return fmt.Errorf("%s: decode api %s: %w", p.name, action, err)
|
||
}
|
||
if decoded.Code != 0 && decoded.Code != 200 {
|
||
msg := strings.TrimSpace(decoded.Message)
|
||
if msg == "" {
|
||
msg = fmt.Sprintf("code %d", decoded.Code)
|
||
}
|
||
return fmt.Errorf("%s: api %s failed: %s", p.name, action, msg)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (p *cloudDrive2Provider) webDAVMkdir(ctx context.Context, target string) error {
|
||
req, err := http.NewRequestWithContext(ctx, "MKCOL", p.urlFor(target), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
p.auth(req)
|
||
resp, err := p.client.Do(req)
|
||
if err != nil {
|
||
return decorateDAVTransportError(p.name, p.urlFor(target), err)
|
||
}
|
||
defer resp.Body.Close()
|
||
switch resp.StatusCode {
|
||
case http.StatusCreated, http.StatusOK, http.StatusNoContent:
|
||
return nil
|
||
case http.StatusMethodNotAllowed:
|
||
return fmt.Errorf("%s: mkdir %s returned http %d; the folder may already exist or this WebDAV backend is read-only", p.name, target, resp.StatusCode)
|
||
default:
|
||
return p.decorateDAVMutationStatusError(resp, "mkdir", target)
|
||
}
|
||
}
|
||
|
||
func (p *cloudDrive2Provider) webDAVRename(ctx context.Context, source, target string) error {
|
||
req, err := http.NewRequestWithContext(ctx, "MOVE", p.urlFor(source), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
p.auth(req)
|
||
req.Header.Set("Destination", p.webDAVDestination(target))
|
||
req.Header.Set("Overwrite", "F")
|
||
resp, err := p.client.Do(req)
|
||
if err != nil {
|
||
return decorateDAVTransportError(p.name, p.urlFor(source), err)
|
||
}
|
||
defer resp.Body.Close()
|
||
switch resp.StatusCode {
|
||
case http.StatusCreated, http.StatusOK, http.StatusNoContent:
|
||
return nil
|
||
default:
|
||
return p.decorateDAVMutationStatusError(resp, "rename", source)
|
||
}
|
||
}
|
||
|
||
func (p *cloudDrive2Provider) webDAVDestination(target string) string {
|
||
raw := p.urlFor(target)
|
||
u, err := url.Parse(raw)
|
||
if err != nil {
|
||
return raw
|
||
}
|
||
u.RawQuery = ""
|
||
u.Fragment = ""
|
||
return u.String()
|
||
}
|
||
|
||
func (p *cloudDrive2Provider) decorateDAVMutationStatusError(resp *http.Response, action, target string) error {
|
||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||
detail := compactDAVErrorBody(string(body))
|
||
if detail == "" {
|
||
return fmt.Errorf("%s: %s %s returned http %d", p.name, action, target, resp.StatusCode)
|
||
}
|
||
return fmt.Errorf("%s: %s %s returned http %d:%s", p.name, action, target, resp.StatusCode, detail)
|
||
}
|