Files
OpenList/drivers/wps/util.go
T
KirCute 744dbd5e26 feat(drivers): support getting disk usage of some drivers (#1905)
* feat(drivers): support getting disk usage of some drivers

* feat(drivers/degoo): implement GetDetails

* fix(fs/storage-details): fill used space rather than free space

* fix mega

* fix bsize type
2026-01-08 10:47:22 +08:00

1063 lines
25 KiB
Go

package wps
import (
"bytes"
"context"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/go-resty/resty/v2"
)
const endpoint = "https://365.kdocs.cn"
const personalEndpoint = "https://drive.wps.cn"
type resolvedNode struct {
kind string
group Group
file *FileInfo
}
type resolveCacheEntry struct {
node *resolvedNode
expire time.Time
}
type resolveCacheStore struct {
mu sync.RWMutex
m map[string]resolveCacheEntry
}
var resolveCaches sync.Map
type apiResult struct {
Result string `json:"result"`
Msg string `json:"msg"`
}
type uploadCreateUpdateResp struct {
apiResult
Method string `json:"method"`
URL string `json:"url"`
Store string `json:"store"`
Request struct {
Headers map[string]string `json:"headers"`
FormData map[string]string `json:"formData"`
} `json:"request"`
Response struct {
ExpectCode []int `json:"expect_code"`
ArgsETag string `json:"args_etag"`
ArgsKey string `json:"args_key"`
} `json:"response"`
}
type uploadPutResp struct {
NewFilename string `json:"newfilename"`
Sha1 string `json:"sha1"`
MD5 string `json:"md5"`
}
type personalGroupsResp struct {
apiResult
Groups []struct {
ID int64 `json:"id"`
Name string `json:"name"`
} `json:"groups"`
}
type countingWriter struct {
n *int64
}
func (w countingWriter) Write(p []byte) (int, error) {
*w.n += int64(len(p))
return len(p), nil
}
func (d *Wps) isPersonal() bool {
return strings.TrimSpace(d.Mode) == "Personal"
}
func (d *Wps) driveHost() string {
if d.isPersonal() {
return personalEndpoint
}
return endpoint
}
func (d *Wps) drivePrefix() string {
if d.isPersonal() {
return ""
}
return "/3rd/drive"
}
func (d *Wps) driveURL(path string) string {
return d.driveHost() + d.drivePrefix() + path
}
func (d *Wps) origin() string {
return d.driveHost()
}
func (d *Wps) canDownload(f *FileInfo) bool {
if f == nil || f.Type == "folder" {
return false
}
if f.FilePerms.Download != 0 {
return true
}
return d.isPersonal()
}
func (d *Wps) request(ctx context.Context) *resty.Request {
return base.RestyClient.R().
SetHeader("Cookie", d.Cookie).
SetHeader("Accept", "application/json").
SetContext(ctx)
}
func (d *Wps) jsonRequest(ctx context.Context) *resty.Request {
return d.request(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("Origin", d.origin())
}
func statusOK(code int, expect []int) bool {
if len(expect) == 0 {
return code >= 200 && code < 300
}
for _, v := range expect {
if v == code {
return true
}
}
return false
}
func respArg(arg string, resp *http.Response, body []byte) string {
arg = strings.TrimSpace(arg)
if arg == "" {
return ""
}
l := strings.ToLower(arg)
if strings.HasPrefix(l, "header.") {
h := strings.TrimSpace(arg[len("header."):])
if h == "" {
return ""
}
return strings.TrimSpace(resp.Header.Get(h))
}
if strings.HasPrefix(l, "body.") {
k := strings.TrimSpace(arg[len("body."):])
if k == "" {
return ""
}
var m map[string]interface{}
if err := json.Unmarshal(body, &m); err != nil {
return ""
}
if v, ok := m[k]; ok {
if s, ok := v.(string); ok {
return strings.TrimSpace(s)
}
}
}
return ""
}
func extractXMLTag(v, tag string) string {
s := strings.TrimSpace(v)
if s == "" {
return ""
}
lt := strings.ToLower(tag)
open := "<" + lt + ">"
clos := "</" + lt + ">"
ls := strings.ToLower(s)
i := strings.Index(ls, open)
if i < 0 {
return ""
}
i += len(open)
j := strings.Index(ls[i:], clos)
if j < 0 {
return ""
}
r := strings.TrimSpace(s[i : i+j])
r = strings.ReplaceAll(r, "&quot;", "")
return strings.Trim(r, `"'`)
}
func checkAPI(resp *resty.Response, result apiResult) error {
if result.Result != "" && result.Result != "ok" {
if result.Msg == "" {
result.Msg = "unknown error"
}
return fmt.Errorf("%s: %s", result.Result, result.Msg)
}
if resp != nil && resp.IsError() {
if result.Msg != "" {
return fmt.Errorf("%s", result.Msg)
}
return fmt.Errorf("http error: %d", resp.StatusCode())
}
return nil
}
func (d *Wps) ensureCompanyID(ctx context.Context) error {
if d.isPersonal() {
return nil
}
if d.companyID != "" {
return nil
}
var resp workspaceResp
r, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(endpoint + "/3rd/plussvr/compose/v1/users/self/workspaces?fields=name&comp_status=active")
if err != nil {
return err
}
if r != nil && r.IsError() {
return fmt.Errorf("http error: %d", r.StatusCode())
}
if len(resp.Companies) == 0 {
return fmt.Errorf("no company id")
}
d.companyID = strconv.FormatInt(resp.Companies[0].ID, 10)
return nil
}
func (d *Wps) getGroups(ctx context.Context) ([]Group, error) {
if d.isPersonal() {
var resp personalGroupsResp
r, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(d.driveURL("/api/v3/groups"))
if err != nil {
return nil, err
}
if err := checkAPI(r, resp.apiResult); err != nil {
return nil, err
}
res := make([]Group, 0, len(resp.Groups))
for _, g := range resp.Groups {
res = append(res, Group{GroupID: g.ID, Name: g.Name})
}
return res, nil
}
if err := d.ensureCompanyID(ctx); err != nil {
return nil, err
}
var resp groupsResp
url := fmt.Sprintf("%s/3rd/plus/groups/v1/companies/%s/users/self/groups/private", endpoint, d.companyID)
r, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(url)
if err != nil {
return nil, err
}
if r != nil && r.IsError() {
return nil, fmt.Errorf("http error: %d", r.StatusCode())
}
return resp.Groups, nil
}
func (d *Wps) getFiles(ctx context.Context, groupID, parentID int64) ([]FileInfo, error) {
var resp filesResp
url := fmt.Sprintf("%s/api/v5/groups/%d/files", d.driveHost()+d.drivePrefix(), groupID)
r, err := d.request(ctx).
SetQueryParam("parentid", strconv.FormatInt(parentID, 10)).
SetResult(&resp).
SetError(&resp).
Get(url)
if err != nil {
return nil, err
}
if r != nil && r.IsError() {
return nil, fmt.Errorf("http error: %d", r.StatusCode())
}
return resp.Files, nil
}
func parseTime(v int64) time.Time {
if v <= 0 {
return time.Time{}
}
return time.Unix(v, 0)
}
func joinPath(basePath, name string) string {
if basePath == "" || basePath == "/" {
return "/" + name
}
return strings.TrimRight(basePath, "/") + "/" + name
}
func normalizePath(path string) string {
clean := strings.TrimSpace(path)
if clean == "" || clean == "/" {
return "/"
}
return "/" + strings.Trim(clean, "/")
}
func (d *Wps) resolveCacheStore() *resolveCacheStore {
if d == nil {
return nil
}
if v, ok := resolveCaches.Load(d); ok {
if s, ok := v.(*resolveCacheStore); ok {
return s
}
}
s := &resolveCacheStore{m: make(map[string]resolveCacheEntry)}
if v, loaded := resolveCaches.LoadOrStore(d, s); loaded {
if s2, ok := v.(*resolveCacheStore); ok {
return s2
}
}
return s
}
func (d *Wps) getResolveCache(path string) (*resolvedNode, bool) {
s := d.resolveCacheStore()
if s == nil {
return nil, false
}
s.mu.RLock()
e, ok := s.m[path]
s.mu.RUnlock()
if !ok || e.node == nil {
return nil, false
}
if !e.expire.IsZero() && time.Now().After(e.expire) {
s.mu.Lock()
delete(s.m, path)
s.mu.Unlock()
return nil, false
}
return e.node, true
}
func (d *Wps) setResolveCache(path string, node *resolvedNode) {
s := d.resolveCacheStore()
if s == nil || node == nil {
return
}
s.mu.Lock()
s.m[path] = resolveCacheEntry{node: node, expire: time.Now().Add(10 * time.Minute)}
s.mu.Unlock()
}
func (d *Wps) clearResolveCache() {
s := d.resolveCacheStore()
if s == nil {
return
}
s.mu.Lock()
if len(s.m) != 0 {
s.m = make(map[string]resolveCacheEntry)
}
s.mu.Unlock()
}
func (d *Wps) resolvePath(ctx context.Context, path string) (*resolvedNode, error) {
cacheKey := normalizePath(path)
if n, ok := d.getResolveCache(cacheKey); ok {
return n, nil
}
clean := strings.TrimSpace(path)
if clean == "" {
clean = "/"
}
clean = strings.Trim(clean, "/")
if clean == "" {
n := &resolvedNode{kind: "root"}
d.setResolveCache("/", n)
return n, nil
}
seg := strings.Split(clean, "/")
groups, err := d.getGroups(ctx)
if err != nil {
return nil, err
}
var grp *Group
for i := range groups {
if groups[i].Name == seg[0] {
grp = &groups[i]
break
}
}
if grp == nil {
return nil, fmt.Errorf("group not found")
}
cur := "/" + seg[0]
gn := &resolvedNode{kind: "group", group: *grp}
d.setResolveCache(cur, gn)
if len(seg) == 1 {
return gn, nil
}
parentID := int64(0)
var lastNode *resolvedNode
for i := 1; i < len(seg); i++ {
files, err := d.getFiles(ctx, grp.GroupID, parentID)
if err != nil {
return nil, err
}
var found *FileInfo
for j := range files {
if files[j].Name == seg[i] {
found = &files[j]
break
}
}
if found == nil {
return nil, fmt.Errorf("path not found")
}
if i < len(seg)-1 && found.Type != "folder" {
return nil, fmt.Errorf("path not found")
}
fi := *found
parentID = fi.ID
cur = cur + "/" + seg[i]
kind := "file"
if fi.Type == "folder" {
kind = "folder"
}
n := &resolvedNode{kind: kind, group: *grp, file: &fi}
d.setResolveCache(cur, n)
lastNode = n
}
if lastNode == nil {
return nil, fmt.Errorf("path not found")
}
return lastNode, nil
}
func (d *Wps) fileToObj(basePath string, f FileInfo) *Obj {
name := f.Name
path := joinPath(basePath, name)
obj := &Obj{
id: path,
name: name,
size: f.Size,
ctime: parseTime(f.Ctime),
mtime: parseTime(f.Mtime),
isDir: f.Type == "folder",
path: path,
}
if !obj.isDir {
obj.canDownload = d.canDownload(&f)
}
return obj
}
func (d *Wps) doJSON(ctx context.Context, method, url string, body interface{}) error {
var result apiResult
req := d.jsonRequest(ctx).SetBody(body).SetResult(&result).SetError(&result)
var (
resp *resty.Response
err error
)
switch method {
case http.MethodPost:
resp, err = req.Post(url)
case http.MethodPut:
resp, err = req.Put(url)
default:
return errs.NotSupport
}
if err != nil {
return err
}
return checkAPI(resp, result)
}
func (d *Wps) list(ctx context.Context, basePath string) ([]model.Obj, error) {
if strings.TrimSpace(basePath) == "" {
basePath = "/"
}
node, err := d.resolvePath(ctx, basePath)
if err != nil {
return nil, err
}
if node.kind == "root" {
groups, err := d.getGroups(ctx)
if err != nil {
return nil, err
}
res := make([]model.Obj, 0, len(groups))
for _, g := range groups {
path := joinPath(basePath, g.Name)
obj := &Obj{
id: path,
name: g.Name,
ctime: parseTime(0),
mtime: parseTime(0),
isDir: true,
path: path,
}
res = append(res, obj)
d.setResolveCache(normalizePath(path), &resolvedNode{kind: "group", group: g})
}
d.setResolveCache("/", &resolvedNode{kind: "root"})
return res, nil
}
if node.kind != "group" && node.kind != "folder" {
return nil, nil
}
parentID := int64(0)
if node.file != nil && node.kind == "folder" {
parentID = node.file.ID
}
files, err := d.getFiles(ctx, node.group.GroupID, parentID)
if err != nil {
return nil, err
}
res := make([]model.Obj, 0, len(files))
for _, f := range files {
res = append(res, d.fileToObj(basePath, f))
path := normalizePath(joinPath(basePath, f.Name))
fi := f
kind := "file"
if fi.Type == "folder" {
kind = "folder"
}
d.setResolveCache(path, &resolvedNode{kind: kind, group: node.group, file: &fi})
}
return res, nil
}
func (d *Wps) link(ctx context.Context, path string) (*model.Link, error) {
node, err := d.resolvePath(ctx, path)
if err != nil {
return nil, err
}
if node.kind != "file" || node.file == nil {
return nil, errs.NotSupport
}
if !d.canDownload(node.file) {
return nil, fmt.Errorf("no download permission")
}
url := fmt.Sprintf("%s/api/v5/groups/%d/files/%d/download?support_checksums=sha1", d.driveHost()+d.drivePrefix(), node.group.GroupID, node.file.ID)
var resp downloadResp
r, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(url)
if err != nil {
return nil, err
}
if r != nil && r.IsError() {
return nil, fmt.Errorf("http error: %d", r.StatusCode())
}
if resp.URL == "" {
return nil, fmt.Errorf("empty download url")
}
return &model.Link{URL: resp.URL, Header: http.Header{}}, nil
}
func (d *Wps) makeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if parentDir == nil {
return errs.NotSupport
}
node, err := d.resolvePath(ctx, parentDir.GetPath())
if err != nil {
return err
}
if node.kind != "group" && node.kind != "folder" {
return errs.NotSupport
}
parentID := int64(0)
if node.file != nil && node.kind == "folder" {
parentID = node.file.ID
}
body := map[string]interface{}{
"groupid": node.group.GroupID,
"name": dirName,
"parentid": parentID,
}
if err := d.doJSON(ctx, http.MethodPost, d.driveURL("/api/v5/files/folder"), body); err != nil {
return err
}
d.clearResolveCache()
return nil
}
func (d *Wps) move(ctx context.Context, srcObj, dstDir model.Obj) error {
if srcObj == nil || dstDir == nil {
return errs.NotSupport
}
nodeSrc, err := d.resolvePath(ctx, srcObj.GetPath())
if err != nil {
return err
}
nodeDst, err := d.resolvePath(ctx, dstDir.GetPath())
if err != nil {
return err
}
if nodeSrc.kind != "file" && nodeSrc.kind != "folder" {
return errs.NotSupport
}
if nodeDst.kind != "group" && nodeDst.kind != "folder" {
return errs.NotSupport
}
targetParentID := int64(0)
if nodeDst.file != nil && nodeDst.kind == "folder" {
targetParentID = nodeDst.file.ID
}
body := map[string]interface{}{
"fileids": []int64{nodeSrc.file.ID},
"target_groupid": nodeDst.group.GroupID,
"target_parentid": targetParentID,
}
url := fmt.Sprintf("/api/v3/groups/%d/files/batch/move", nodeSrc.group.GroupID)
for {
var res apiResult
resp, err := d.jsonRequest(ctx).
SetBody(body).
SetResult(&res).
SetError(&res).
Post(d.driveURL(url))
if err != nil {
return err
}
if resp.StatusCode() == 403 && res.Result == "fileTaskDuplicated" {
time.Sleep(500 * time.Millisecond)
continue
}
if err := checkAPI(resp, res); err != nil {
return err
}
break
}
d.clearResolveCache()
return nil
}
func (d *Wps) rename(ctx context.Context, srcObj model.Obj, newName string) error {
if srcObj == nil {
return errs.NotSupport
}
node, err := d.resolvePath(ctx, srcObj.GetPath())
if err != nil {
return err
}
if node.kind != "file" && node.kind != "folder" {
return errs.NotSupport
}
url := fmt.Sprintf("/api/v3/groups/%d/files/%d", node.group.GroupID, node.file.ID)
body := map[string]string{"fname": newName}
if err := d.doJSON(ctx, http.MethodPut, d.driveURL(url), body); err != nil {
return err
}
d.clearResolveCache()
return nil
}
func (d *Wps) copy(ctx context.Context, srcObj, dstDir model.Obj) error {
if srcObj == nil || dstDir == nil {
return errs.NotSupport
}
nodeSrc, err := d.resolvePath(ctx, srcObj.GetPath())
if err != nil {
return err
}
nodeDst, err := d.resolvePath(ctx, dstDir.GetPath())
if err != nil {
return err
}
if nodeSrc.kind != "file" && nodeSrc.kind != "folder" {
return errs.NotSupport
}
if nodeDst.kind != "group" && nodeDst.kind != "folder" {
return errs.NotSupport
}
targetParentID := int64(0)
if nodeDst.file != nil && nodeDst.kind == "folder" {
targetParentID = nodeDst.file.ID
}
body := map[string]interface{}{
"fileids": []int64{nodeSrc.file.ID},
"groupid": nodeSrc.group.GroupID,
"target_groupid": nodeDst.group.GroupID,
"target_parentid": targetParentID,
"duplicated_name_model": 1,
}
url := fmt.Sprintf("/api/v3/groups/%d/files/batch/copy", nodeSrc.group.GroupID)
for {
var res apiResult
resp, err := d.jsonRequest(ctx).
SetBody(body).
SetResult(&res).
SetError(&res).
Post(d.driveURL(url))
if err != nil {
return err
}
if resp.StatusCode() == 403 && res.Result == "fileTaskDuplicated" {
time.Sleep(500 * time.Millisecond)
continue
}
if err := checkAPI(resp, res); err != nil {
return err
}
break
}
d.clearResolveCache()
return nil
}
func (d *Wps) remove(ctx context.Context, obj model.Obj) error {
if obj == nil {
return errs.NotSupport
}
node, err := d.resolvePath(ctx, obj.GetPath())
if err != nil {
return err
}
if node.kind != "file" && node.kind != "folder" {
return errs.NotSupport
}
body := map[string]interface{}{
"fileids": []int64{node.file.ID},
}
url := fmt.Sprintf("/api/v3/groups/%d/files/batch/delete", node.group.GroupID)
for {
var res apiResult
resp, err := d.jsonRequest(ctx).
SetBody(body).
SetResult(&res).
SetError(&res).
Post(d.driveURL(url))
if err != nil {
return err
}
// 无法连续创建文件夹删除。如果一定要删除,每0.5s 尝试一次创建下一个删除请求,应当避免递归删除文件夹
if resp.StatusCode() == 403 && res.Result == "fileTaskDuplicated" {
time.Sleep(500 * time.Millisecond)
continue
}
if err := checkAPI(resp, res); err != nil {
return err
}
break
}
d.clearResolveCache()
return nil
}
func cacheAndHash(file model.FileStreamer, up driver.UpdateProgress) (model.File, int64, string, string, error) {
h1 := sha1.New()
h256 := sha256.New()
size := file.GetSize()
var counted int64
ws := []io.Writer{h1, h256}
if size <= 0 {
ws = append(ws, countingWriter{n: &counted})
}
p := up
f, err := file.CacheFullAndWriter(&p, io.MultiWriter(ws...))
if err != nil {
return nil, 0, "", "", err
}
if size <= 0 {
size = counted
}
return f, size, hex.EncodeToString(h1.Sum(nil)), hex.EncodeToString(h256.Sum(nil)), nil
}
func (d *Wps) createUpload(ctx context.Context, groupID, parentID int64, name string, size int64, sha1Hex, sha256Hex string) (*uploadCreateUpdateResp, error) {
body := map[string]string{
"group_id": strconv.FormatInt(groupID, 10),
"name": name,
"parent_id": strconv.FormatInt(parentID, 10),
"sha1": sha1Hex,
"sha256": sha256Hex,
"size": strconv.FormatInt(size, 10),
}
var resp uploadCreateUpdateResp
r, err := d.jsonRequest(ctx).
SetBody(body).
SetResult(&resp).
SetError(&resp).
Put(d.driveURL("/api/v5/files/upload/create_update"))
if err != nil {
return nil, err
}
if err := checkAPI(r, resp.apiResult); err != nil {
return nil, err
}
if resp.URL == "" {
return nil, fmt.Errorf("empty upload url")
}
return &resp, nil
}
func normalizeETag(v string) string {
v = strings.TrimSpace(v)
if strings.HasPrefix(v, "W/") {
v = strings.TrimSpace(strings.TrimPrefix(v, "W/"))
}
return strings.Trim(v, `"`)
}
func (d *Wps) commitUpload(ctx context.Context, etag, key string, groupID, parentID int64, name, sha1Hex string, size int64, store string) error {
store = strings.TrimSpace(store)
if store == "" {
store = "ks3"
}
storeKey := ""
if key != "" {
storeKey = key
}
body := map[string]interface{}{
"etag": etag,
"groupid": groupID,
"key": key,
"name": name,
"parentid": parentID,
"sha1": sha1Hex,
"size": size,
"store": store,
"storekey": storeKey,
}
return d.doJSON(ctx, http.MethodPost, d.driveURL("/api/v5/files/file"), body)
}
func (d *Wps) put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
if dstDir == nil || file == nil {
return errs.NotSupport
}
if up == nil {
up = func(float64) {}
}
node, err := d.resolvePath(ctx, dstDir.GetPath())
if err != nil {
return err
}
if node.kind != "group" && node.kind != "folder" {
return errs.NotSupport
}
parentID := int64(0)
if node.file != nil && node.kind == "folder" {
parentID = node.file.ID
}
f, size, sha1Hex, sha256Hex, err := cacheAndHash(file, func(float64) {})
if err != nil {
return err
}
if c, ok := f.(io.Closer); ok {
defer c.Close()
}
// 在隐藏文件名前加_上传,这是WPS的限制,无法上传隐藏文件,也无法将任何文件重命名为隐藏文件,所有隐藏文件会被自动加上_ 上传
// 甚至可以上传前缀是..的文件,但是单个点就是不行
realName := file.GetName()
uploadName := realName
if strings.HasPrefix(realName, ".") {
uploadName = "_" + realName
}
info, err := d.createUpload(ctx, node.group.GroupID, parentID, uploadName, size, sha1Hex, sha256Hex)
if err != nil {
return err
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
return err
}
rf := driver.NewLimitedUploadFile(ctx, f)
prog := driver.NewProgress(size, model.UpdateProgressWithRange(up, 0, 1))
method := strings.ToUpper(strings.TrimSpace(info.Method))
if method == "" {
method = http.MethodPut
}
var req *http.Request
if method == http.MethodPost && len(info.Request.FormData) > 0 {
if size == 0 {
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
for k, v := range info.Request.FormData {
if err := mw.WriteField(k, v); err != nil {
return err
}
}
part, err := mw.CreateFormFile("file", uploadName)
if err != nil {
return err
}
if _, err := io.Copy(part, io.TeeReader(rf, prog)); err != nil {
return err
}
if err := mw.Close(); err != nil {
return err
}
req, err = http.NewRequestWithContext(ctx, method, info.URL, bytes.NewReader(buf.Bytes()))
if err != nil {
return err
}
for k, v := range info.Request.Headers {
req.Header.Set(k, v)
}
req.Header.Set("Content-Type", mw.FormDataContentType())
req.ContentLength = int64(buf.Len())
req.Header.Set("Content-Length", strconv.FormatInt(req.ContentLength, 10))
} else {
pr, pw := io.Pipe()
mw := multipart.NewWriter(pw)
req, err = http.NewRequestWithContext(ctx, method, info.URL, pr)
if err != nil {
return err
}
for k, v := range info.Request.Headers {
req.Header.Set(k, v)
}
req.Header.Set("Content-Type", mw.FormDataContentType())
go func() {
for k, v := range info.Request.FormData {
if err := mw.WriteField(k, v); err != nil {
pw.CloseWithError(err)
return
}
}
part, err := mw.CreateFormFile("file", uploadName)
if err != nil {
pw.CloseWithError(err)
return
}
if _, err := io.Copy(part, io.TeeReader(rf, prog)); err != nil {
pw.CloseWithError(err)
return
}
if err := mw.Close(); err != nil {
pw.CloseWithError(err)
return
}
pw.Close()
}()
}
} else {
var body = io.TeeReader(rf, prog)
if size == 0 {
body = bytes.NewReader(nil)
}
req, err = http.NewRequestWithContext(ctx, method, info.URL, body)
if err != nil {
return err
}
for k, v := range info.Request.Headers {
req.Header.Set(k, v)
}
req.ContentLength = size
req.Header.Set("Content-Length", strconv.FormatInt(size, 10))
}
c := *base.RestyClient.GetClient()
c.Timeout = 0
resp, err := (&c).Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if !statusOK(resp.StatusCode, info.Response.ExpectCode) {
io.Copy(io.Discard, resp.Body)
return fmt.Errorf("http error: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
etag := normalizeETag(respArg(info.Response.ArgsETag, resp, body))
if etag == "" {
etag = normalizeETag(resp.Header.Get("ETag"))
}
key := strings.TrimSpace(respArg(info.Response.ArgsKey, resp, body))
if key == "" {
key = strings.TrimSpace(resp.Header.Get("x-obs-save-key"))
}
var pr uploadPutResp
sha1FromServer := ""
if err := json.Unmarshal(body, &pr); err == nil {
sha1FromServer = strings.TrimSpace(pr.NewFilename)
if sha1FromServer == "" {
sha1FromServer = strings.TrimSpace(pr.Sha1)
}
if etag == "" && pr.MD5 != "" {
etag = strings.TrimSpace(pr.MD5)
}
}
if sha1FromServer == "" {
if v := extractXMLTag(string(body), "ETag"); v != "" {
sha1FromServer = v
if etag == "" {
etag = v
}
}
}
if sha1FromServer == "" && key != "" && len(key) == 40 {
sha1FromServer = key
}
if sha1FromServer == "" {
sha1FromServer = sha1Hex
}
if etag == "" {
return fmt.Errorf("empty etag")
}
if sha1FromServer == "" {
return fmt.Errorf("empty sha1")
}
store := strings.TrimSpace(info.Store)
commitKey := ""
if strings.TrimSpace(info.Response.ArgsKey) != "" {
commitKey = key
if commitKey == "" {
commitKey = sha1FromServer
}
}
if err := d.commitUpload(ctx, etag, commitKey, node.group.GroupID, parentID, uploadName, sha1FromServer, size, store); err != nil {
return err
}
up(1)
return nil
}
func (d *Wps) spaces(ctx context.Context) (*spacesResp, error) {
url := fmt.Sprintf("%s/api/v3/spaces", d.driveHost()+d.drivePrefix())
var resp spacesResp
r, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(url)
if err != nil {
return nil, err
}
if r != nil && r.IsError() {
return nil, fmt.Errorf("http error: %d", r.StatusCode())
}
return &resp, nil
}