|
package homecloud |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"crypto/md5" |
|
"crypto/sha1" |
|
"encoding/hex" |
|
"fmt" |
|
"io" |
|
"mime/multipart" |
|
"net/http" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"github.com/alist-org/alist/v3/drivers/base" |
|
"github.com/alist-org/alist/v3/internal/driver" |
|
"github.com/alist-org/alist/v3/internal/errs" |
|
"github.com/alist-org/alist/v3/internal/model" |
|
"github.com/alist-org/alist/v3/pkg/cron" |
|
"github.com/alist-org/alist/v3/pkg/utils" |
|
"github.com/alist-org/alist/v3/pkg/utils/random" |
|
) |
|
|
|
type HomeCloud struct { |
|
model.Storage |
|
Addition |
|
AccessToken string |
|
UserID string |
|
cron *cron.Cron |
|
Account string |
|
} |
|
|
|
func (d *HomeCloud) Config() driver.Config { |
|
return config |
|
} |
|
|
|
func (d *HomeCloud) GetAddition() driver.Additional { |
|
return &d.Addition |
|
} |
|
|
|
func (d *HomeCloud) Init(ctx context.Context) error { |
|
if d.RefreshToken == "" { |
|
return fmt.Errorf("RefreshToken is empty") |
|
} |
|
|
|
if len(d.Addition.RootFolderID) == 0 { |
|
d.RootFolderID = "0" |
|
} |
|
|
|
err := d.refreshToken() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
d.cron = cron.NewCron(time.Hour * 10) |
|
d.cron.Do(func() { |
|
err := d.refreshToken() |
|
if err != nil { |
|
return |
|
} |
|
}) |
|
|
|
return nil |
|
} |
|
|
|
func (d *HomeCloud) Drop(ctx context.Context) error { |
|
if d.cron != nil { |
|
d.cron.Stop() |
|
} |
|
return nil |
|
} |
|
|
|
func (d *HomeCloud) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { |
|
return d.familyGetFiles(dir.GetID()) |
|
} |
|
|
|
func (d *HomeCloud) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { |
|
var url string |
|
var err error |
|
url, err = d.getLink(file.GetID()) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
link := &model.Link{ |
|
URL: url, |
|
} |
|
|
|
|
|
header := make(http.Header) |
|
header.Add("Cookie", "H_TOKEN="+d.AccessToken) |
|
header.Add("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1.1 Mobile/15E148 Safari/604.1") |
|
link.Header = header |
|
|
|
return link, nil |
|
} |
|
|
|
func (d *HomeCloud) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { |
|
var err error |
|
data := base.Json{ |
|
"parentDirId": parentDir.GetID(), |
|
"dirName": dirName, |
|
"category": 0, |
|
"userId": d.UserID, |
|
"groupId": d.GroupID, |
|
} |
|
pathname := "/storage/addDirectory/v1" |
|
_, err = d.post(pathname, data, nil) |
|
return err |
|
} |
|
|
|
func (d *HomeCloud) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { |
|
data := base.Json{ |
|
"fileIds": []string{srcObj.GetID()}, |
|
"targetDirId": dstDir.GetID(), |
|
"userId": d.UserID, |
|
"groupId": d.GroupID, |
|
} |
|
pathname := "/storage/batchMoveFile/v1" |
|
_, err := d.post(pathname, data, nil) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return srcObj, nil |
|
} |
|
|
|
func (d *HomeCloud) Rename(ctx context.Context, srcObj model.Obj, newName string) error { |
|
var err error |
|
data := base.Json{ |
|
"fileId": srcObj.GetID(), |
|
"fileName": newName, |
|
"userId": d.UserID, |
|
"groupId": d.GroupID, |
|
} |
|
pathname := "/storage/updateFileName/v1" |
|
_, err = d.post(pathname, data, nil) |
|
|
|
return err |
|
} |
|
|
|
func (d *HomeCloud) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return errs.NotImplement |
|
} |
|
|
|
func (d *HomeCloud) Remove(ctx context.Context, obj model.Obj) error { |
|
data := base.Json{ |
|
"fileIds": []string{obj.GetID()}, |
|
"userId": d.UserID, |
|
"groupId": d.GroupID, |
|
} |
|
pathname := "/storage/batchDeleteFile/v1" |
|
if obj.IsDir() { |
|
data = base.Json{ |
|
"fileId": obj.GetID(), |
|
"userId": d.UserID, |
|
"groupId": d.GroupID, |
|
} |
|
pathname = "/storage/deleteDirectory/v1" |
|
} |
|
_, err := d.post(pathname, data, nil) |
|
return err |
|
} |
|
|
|
const ( |
|
_ = iota |
|
KB = 1 << (10 * iota) |
|
MB |
|
GB |
|
TB |
|
) |
|
|
|
func getPartSize(size int64) int64 { |
|
|
|
if size/GB > 30 { |
|
return 512 * MB |
|
} |
|
return 100 * MB |
|
} |
|
|
|
func (d *HomeCloud) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { |
|
var err error |
|
|
|
h := md5.New() |
|
|
|
tempFile, err := stream.CacheFullInTempFile() |
|
if err != nil { |
|
return err |
|
} |
|
defer func() { |
|
_ = tempFile.Close() |
|
}() |
|
if _, err = io.Copy(h, tempFile); err != nil { |
|
return err |
|
} |
|
_, err = tempFile.Seek(0, io.SeekStart) |
|
if err != nil { |
|
return err |
|
} |
|
etag := hex.EncodeToString(h.Sum(nil)) |
|
|
|
|
|
data := base.Json{ |
|
"userId": d.UserID, |
|
"groupId": d.GroupID, |
|
"dirId": dstDir.GetID(), |
|
"fileName": stream.GetName(), |
|
"fileMd5": etag, |
|
"fileSize": stream.GetSize(), |
|
"fileCategory": 99, |
|
} |
|
|
|
pathname := "/storage/addFileUploadTask/v1" |
|
var resp PersonalUploadResp |
|
_, err = d.post(pathname, data, &resp) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
|
|
p := driver.NewProgress(stream.GetSize(), up) |
|
|
|
var partSize = getPartSize(stream.GetSize()) |
|
part := (stream.GetSize() + partSize - 1) / partSize |
|
if part == 0 { |
|
part = 1 |
|
} |
|
for i := int64(0); i < part; i++ { |
|
if utils.IsCanceled(ctx) { |
|
return ctx.Err() |
|
} |
|
|
|
start := i * partSize |
|
byteSize := stream.GetSize() - start |
|
if byteSize > partSize { |
|
byteSize = partSize |
|
} |
|
|
|
limitReader := io.LimitReader(stream, byteSize) |
|
|
|
r := io.TeeReader(limitReader, p) |
|
|
|
|
|
|
|
body := &bytes.Buffer{} |
|
writer := multipart.NewWriter(body) |
|
filePart, err := writer.CreateFormFile("partFile", stream.GetName()) |
|
if err != nil { |
|
return err |
|
} |
|
_, err = io.Copy(filePart, r) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
isDone := false |
|
|
|
if i == (part - 1) { |
|
isDone = true |
|
} |
|
|
|
_ = writer.WriteField("uploadId", resp.Data.UploadId) |
|
_ = writer.WriteField("isComplete", strconv.FormatBool(isDone)) |
|
_ = writer.WriteField("rangeStart", strconv.Itoa(int(start))) |
|
|
|
err = writer.Close() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
req, err := http.NewRequest("POST", resp.Data.UploadUrl, body) |
|
if err != nil { |
|
return err |
|
} |
|
requestID := random.String(12) |
|
pbody, err := utils.Json.Marshal(body) |
|
|
|
if err != nil { |
|
return err |
|
} |
|
|
|
timestamp := fmt.Sprintf("%.3f", float64(time.Now().UnixNano())/1e6) |
|
h := sha1.New() |
|
var sha1Hash string |
|
|
|
if pbody == nil { |
|
h.Write([]byte("{}")) |
|
sha1Hash = strings.ToUpper(hex.EncodeToString(h.Sum(nil))) |
|
} else { |
|
h.Write(pbody) |
|
sha1Hash = strings.ToUpper(hex.EncodeToString(h.Sum(nil))) |
|
} |
|
|
|
uppathname := "/upload/upload/uploadFilePart/v1" |
|
encStr := fmt.Sprintf("%s;%s;%s;Bearer %s;%s", uppathname, sha1Hash, requestID, d.AccessToken, timestamp) |
|
signature := strings.ToUpper(fmt.Sprintf("%x", md5.Sum([]byte(encStr)))) |
|
|
|
req = req.WithContext(ctx) |
|
req.Header.Add("Accept", "*/*") |
|
req.Header.Add("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") |
|
req.Header.Add("Authorization", "Bearer "+d.AccessToken) |
|
req.Header.Add("Origin", "https://homecloud.komect.com") |
|
req.Header.Add("Referer", "https://homecloud.komect.com/disk/main/familyspace") |
|
req.Header.Add("Request-Id", requestID) |
|
req.Header.Add("Signature", signature) |
|
req.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36") |
|
req.Header.Add("X-Requested-With", "XMLHttpRequest") |
|
req.Header.Add("X-User-Agent", "Web|Chrome 127.0.0.0||OS X|homecloudWebDisk_1.1.1||yunpan 1.1.1|unknown") |
|
req.Header.Add("sec-ch-ua", "\"Not)A;Brand\";v=\"99\", \"Google Chrome\";v=\"127\", \"Chromium\";v=\"127\"") |
|
req.Header.Add("sec-ch-ua-mobile", "?0") |
|
req.Header.Add("sec-ch-ua-platform", "\"macOS\"") |
|
req.Header.Add("userId", d.UserID) |
|
req.Header.Set("Content-Type", writer.FormDataContentType()) |
|
|
|
res, err := base.HttpClient.Do(req) |
|
if err != nil { |
|
return err |
|
} |
|
_ = res.Body.Close() |
|
|
|
if res.StatusCode != http.StatusOK { |
|
return fmt.Errorf("unexpected status code: %d", res.StatusCode) |
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil |
|
} |
|
|
|
func (d *HomeCloud) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { |
|
return nil, errs.NotImplement |
|
} |
|
|
|
var _ driver.Driver = (*HomeCloud)(nil) |
|
|