mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 21:13:10 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 525d004887 | |||
| af49100134 | |||
| 3338c018b0 | |||
| 5f12e75584 |
+4
-3
@@ -32,9 +32,10 @@ RUN apk update && \
|
||||
/opt/aria2/.aria2/tracker.sh ; \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY --chmod=755 --from=builder /app/bin/alist ./
|
||||
COPY --chmod=755 entrypoint.sh /entrypoint.sh
|
||||
RUN /entrypoint.sh version
|
||||
COPY --from=builder /app/bin/alist ./
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /opt/alist/alist && \
|
||||
chmod +x /entrypoint.sh && /entrypoint.sh version
|
||||
|
||||
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
|
||||
VOLUME /opt/alist/data/
|
||||
|
||||
+4
-3
@@ -24,9 +24,10 @@ RUN apk update && \
|
||||
/opt/aria2/.aria2/tracker.sh ; \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY --chmod=755 /build/${TARGETPLATFORM}/alist ./
|
||||
COPY --chmod=755 entrypoint.sh /entrypoint.sh
|
||||
RUN /entrypoint.sh version
|
||||
COPY /build/${TARGETPLATFORM}/alist ./
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /opt/alist/alist && \
|
||||
chmod +x /entrypoint.sh && /entrypoint.sh version
|
||||
|
||||
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
|
||||
VOLUME /opt/alist/data/
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
smb2 "github.com/KirCute/go-smb2-alist/server"
|
||||
"github.com/KirCute/sftpd-alist"
|
||||
"github.com/alist-org/alist/v3/cmd/flags"
|
||||
"github.com/alist-org/alist/v3/internal/bootstrap"
|
||||
@@ -157,6 +158,22 @@ the address is defined in config file`,
|
||||
}()
|
||||
}
|
||||
}
|
||||
var smbServer *smb2.Server
|
||||
if conf.Conf.SMB.Listen != "" && conf.Conf.SMB.Enable {
|
||||
var err error
|
||||
smbServer, err = server.NewSmbServer()
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to start smb server: %s", err.Error())
|
||||
} else {
|
||||
utils.Log.Infof("start smb server on %s", conf.Conf.SMB.Listen)
|
||||
go func() {
|
||||
err = smbServer.Serve(conf.Conf.SMB.Listen)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("problem smb server listening: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
// Wait for interrupt signal to gracefully shutdown the server with
|
||||
// a timeout of 1 second.
|
||||
quit := make(chan os.Signal, 1)
|
||||
@@ -217,6 +234,15 @@ the address is defined in config file`,
|
||||
}
|
||||
}()
|
||||
}
|
||||
if conf.Conf.SMB.Listen != "" && conf.Conf.SMB.Enable && smbServer != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := smbServer.Shutdown(); err != nil {
|
||||
utils.Log.Fatal("SMB server shutdown err: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
utils.Log.Println("Server exit")
|
||||
},
|
||||
|
||||
@@ -16,14 +16,12 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
sdk "github.com/xhofe/115-sdk-go"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type Open115 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
client *sdk.Client
|
||||
limiter *rate.Limiter
|
||||
client *sdk.Client
|
||||
}
|
||||
|
||||
func (d *Open115) Config() driver.Config {
|
||||
@@ -49,16 +47,6 @@ func (d *Open115) Init(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.Addition.LimitRate > 0 {
|
||||
d.limiter = rate.NewLimiter(rate.Limit(d.Addition.LimitRate), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open115) WaitLimit(ctx context.Context) error {
|
||||
if d.limiter != nil {
|
||||
return d.limiter.Wait(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,9 +59,6 @@ func (d *Open115) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
pageSize := int64(200)
|
||||
offset := int64(0)
|
||||
for {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := d.client.GetFiles(ctx, &sdk.GetFilesReq{
|
||||
CID: dir.GetID(),
|
||||
Limit: pageSize,
|
||||
@@ -99,9 +84,6 @@ func (d *Open115) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
}
|
||||
|
||||
func (d *Open115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ua string
|
||||
if args.Header != nil {
|
||||
ua = args.Header.Get("User-Agent")
|
||||
@@ -131,9 +113,6 @@ func (d *Open115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
}
|
||||
|
||||
func (d *Open115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := d.client.Mkdir(ctx, parentDir.GetID(), dirName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -150,9 +129,6 @@ func (d *Open115) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
|
||||
}
|
||||
|
||||
func (d *Open115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err := d.client.Move(ctx, &sdk.MoveReq{
|
||||
FileIDs: srcObj.GetID(),
|
||||
ToCid: dstDir.GetID(),
|
||||
@@ -164,9 +140,6 @@ func (d *Open115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj
|
||||
}
|
||||
|
||||
func (d *Open115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err := d.client.UpdateFile(ctx, &sdk.UpdateFileReq{
|
||||
FileID: srcObj.GetID(),
|
||||
FileNma: newName,
|
||||
@@ -182,9 +155,6 @@ func (d *Open115) Rename(ctx context.Context, srcObj model.Obj, newName string)
|
||||
}
|
||||
|
||||
func (d *Open115) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err := d.client.Copy(ctx, &sdk.CopyReq{
|
||||
PID: dstDir.GetID(),
|
||||
FileID: srcObj.GetID(),
|
||||
@@ -197,9 +167,6 @@ func (d *Open115) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj
|
||||
}
|
||||
|
||||
func (d *Open115) Remove(ctx context.Context, obj model.Obj) error {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
_obj, ok := obj.(*Obj)
|
||||
if !ok {
|
||||
return fmt.Errorf("can't convert obj")
|
||||
@@ -215,9 +182,6 @@ func (d *Open115) Remove(ctx context.Context, obj model.Obj) error {
|
||||
}
|
||||
|
||||
func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tempF, err := file.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -9,10 +9,9 @@ type Addition struct {
|
||||
// Usually one of two
|
||||
driver.RootID
|
||||
// define other
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"file_name,file_size,user_utime,file_type"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"`
|
||||
LimitRate float64 `json:"limit_rate" type:"float" default:"1" help:"limit all api request rate ([limit]r/1s)"`
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"file_name,file_size,user_utime,file_type"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"`
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
|
||||
+35
-37
@@ -24,10 +24,9 @@ import (
|
||||
type Yun139 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
cron *cron.Cron
|
||||
Account string
|
||||
ref *Yun139
|
||||
PersonalCloudHost string
|
||||
cron *cron.Cron
|
||||
Account string
|
||||
ref *Yun139
|
||||
}
|
||||
|
||||
func (d *Yun139) Config() driver.Config {
|
||||
@@ -40,36 +39,13 @@ func (d *Yun139) GetAddition() driver.Additional {
|
||||
|
||||
func (d *Yun139) Init(ctx context.Context) error {
|
||||
if d.ref == nil {
|
||||
if len(d.Authorization) == 0 {
|
||||
if d.Authorization == "" {
|
||||
return fmt.Errorf("authorization is empty")
|
||||
}
|
||||
err := d.refreshToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Query Route Policy
|
||||
var resp QueryRoutePolicyResp
|
||||
_, err = d.requestRoute(base.Json{
|
||||
"userInfo": base.Json{
|
||||
"userType": 1,
|
||||
"accountType": 1,
|
||||
"accountName": d.Account},
|
||||
"modAddrType": 1,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, policyItem := range resp.Data.RoutePolicyList {
|
||||
if policyItem.ModName == "personal" {
|
||||
d.PersonalCloudHost = policyItem.HttpsUrl
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(d.PersonalCloudHost) == 0 {
|
||||
return fmt.Errorf("PersonalCloudHost is empty")
|
||||
}
|
||||
|
||||
d.cron = cron.NewCron(time.Hour * 12)
|
||||
d.cron.Do(func() {
|
||||
err := d.refreshToken()
|
||||
@@ -95,6 +71,28 @@ func (d *Yun139) Init(ctx context.Context) error {
|
||||
default:
|
||||
return errs.NotImplement
|
||||
}
|
||||
// if d.ref != nil {
|
||||
// return nil
|
||||
// }
|
||||
// decode, err := base64.StdEncoding.DecodeString(d.Authorization)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// decodeStr := string(decode)
|
||||
// splits := strings.Split(decodeStr, ":")
|
||||
// if len(splits) < 2 {
|
||||
// return fmt.Errorf("authorization is invalid, splits < 2")
|
||||
// }
|
||||
// d.Account = splits[1]
|
||||
// _, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{
|
||||
// "qryUserExternInfoReq": base.Json{
|
||||
// "commonAccountInfo": base.Json{
|
||||
// "account": d.getAccount(),
|
||||
// "accountType": 1,
|
||||
// },
|
||||
// },
|
||||
// }, nil)
|
||||
// return err
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -162,7 +160,7 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
|
||||
"type": "folder",
|
||||
"fileRenameMode": "force_rename",
|
||||
}
|
||||
pathname := "/file/create"
|
||||
pathname := "/hcy/file/create"
|
||||
_, err = d.personalPost(pathname, data, nil)
|
||||
case MetaPersonal:
|
||||
data := base.Json{
|
||||
@@ -215,7 +213,7 @@ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj,
|
||||
"fileIds": []string{srcObj.GetID()},
|
||||
"toParentFileId": dstDir.GetID(),
|
||||
}
|
||||
pathname := "/file/batchMove"
|
||||
pathname := "/hcy/file/batchMove"
|
||||
_, err := d.personalPost(pathname, data, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -292,7 +290,7 @@ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) e
|
||||
"name": newName,
|
||||
"description": "",
|
||||
}
|
||||
pathname := "/file/update"
|
||||
pathname := "/hcy/file/update"
|
||||
_, err = d.personalPost(pathname, data, nil)
|
||||
case MetaPersonal:
|
||||
var data base.Json
|
||||
@@ -392,7 +390,7 @@ func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
"fileIds": []string{srcObj.GetID()},
|
||||
"toParentFileId": dstDir.GetID(),
|
||||
}
|
||||
pathname := "/file/batchCopy"
|
||||
pathname := "/hcy/file/batchCopy"
|
||||
_, err := d.personalPost(pathname, data, nil)
|
||||
return err
|
||||
case MetaPersonal:
|
||||
@@ -432,7 +430,7 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
|
||||
data := base.Json{
|
||||
"fileIds": []string{obj.GetID()},
|
||||
}
|
||||
pathname := "/recyclebin/batchTrash"
|
||||
pathname := "/hcy/recyclebin/batchTrash"
|
||||
_, err := d.personalPost(pathname, data, nil)
|
||||
return err
|
||||
case MetaGroup:
|
||||
@@ -576,7 +574,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
"type": "file",
|
||||
"fileRenameMode": "auto_rename",
|
||||
}
|
||||
pathname := "/file/create"
|
||||
pathname := "/hcy/file/create"
|
||||
var resp PersonalUploadResp
|
||||
_, err = d.personalPost(pathname, data, &resp)
|
||||
if err != nil {
|
||||
@@ -613,7 +611,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname := "/file/getUploadUrl"
|
||||
pathname := "/hcy/file/getUploadUrl"
|
||||
var moreresp PersonalUploadUrlResp
|
||||
_, err = d.personalPost(pathname, moredata, &moreresp)
|
||||
if err != nil {
|
||||
@@ -664,7 +662,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
"fileId": resp.Data.FileId,
|
||||
"uploadId": resp.Data.UploadId,
|
||||
}
|
||||
_, err = d.personalPost("/file/complete", data, nil)
|
||||
_, err = d.personalPost("/hcy/file/complete", data, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -856,7 +854,7 @@ func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{},
|
||||
}
|
||||
switch args.Method {
|
||||
case "video_preview":
|
||||
uri = "/videoPreview/getPreviewInfo"
|
||||
uri = "/hcy/videoPreview/getPreviewInfo"
|
||||
default:
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ type Addition struct {
|
||||
CloudID string `json:"cloud_id"`
|
||||
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
|
||||
ReportRealSize bool `json:"report_real_size" type:"bool" default:"true" help:"Enable to report the real file size during upload"`
|
||||
UseLargeThumbnail bool `json:"use_large_thumbnail" type:"bool" default:"false" help:"Enable to use large thumbnail for images"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
||||
+6
-25
@@ -285,30 +285,11 @@ type PersonalUploadUrlResp struct {
|
||||
}
|
||||
}
|
||||
|
||||
type QueryRoutePolicyResp struct {
|
||||
Success bool `json:"success"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
RoutePolicyList []struct {
|
||||
SiteID string `json:"siteID"`
|
||||
SiteCode string `json:"siteCode"`
|
||||
ModName string `json:"modName"`
|
||||
HttpUrl string `json:"httpUrl"`
|
||||
HttpsUrl string `json:"httpsUrl"`
|
||||
EnvID string `json:"envID"`
|
||||
ExtInfo string `json:"extInfo"`
|
||||
HashName string `json:"hashName"`
|
||||
ModAddrType int `json:"modAddrType"`
|
||||
} `json:"routePolicyList"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type RefreshTokenResp struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Return string `xml:"return"`
|
||||
Token string `xml:"token"`
|
||||
Expiretime int32 `xml:"expiretime"`
|
||||
AccessToken string `xml:"accessToken"`
|
||||
Desc string `xml:"desc"`
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Return string `xml:"return"`
|
||||
Token string `xml:"token"`
|
||||
Expiretime int32 `xml:"expiretime"`
|
||||
AccessToken string `xml:"accessToken"`
|
||||
Desc string `xml:"desc"`
|
||||
}
|
||||
|
||||
+6
-76
@@ -157,64 +157,6 @@ func (d *Yun139) request(pathname string, method string, callback base.ReqCallba
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *Yun139) requestRoute(data interface{}, resp interface{}) ([]byte, error) {
|
||||
url := "https://user-njs.yun.139.com/user/route/qryRoutePolicy"
|
||||
req := base.RestyClient.R()
|
||||
randStr := random.String(16)
|
||||
ts := time.Now().Format("2006-01-02 15:04:05")
|
||||
callback := func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
body, err := utils.Json.Marshal(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sign := calSign(string(body), ts, randStr)
|
||||
svcType := "1"
|
||||
if d.isFamily() {
|
||||
svcType = "2"
|
||||
}
|
||||
req.SetHeaders(map[string]string{
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"CMS-DEVICE": "default",
|
||||
"Authorization": "Basic " + d.getAuthorization(),
|
||||
"mcloud-channel": "1000101",
|
||||
"mcloud-client": "10701",
|
||||
//"mcloud-route": "001",
|
||||
"mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
|
||||
//"mcloud-skey":"",
|
||||
"mcloud-version": "7.14.0",
|
||||
"Origin": "https://yun.139.com",
|
||||
"Referer": "https://yun.139.com/w/",
|
||||
"x-DeviceInfo": "||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||",
|
||||
"x-huawei-channelSrc": "10000034",
|
||||
"x-inner-ntwk": "2",
|
||||
"x-m4c-caller": "PC",
|
||||
"x-m4c-src": "10002",
|
||||
"x-SvcType": svcType,
|
||||
"Inner-Hcy-Router-Https": "1",
|
||||
})
|
||||
|
||||
var e BaseResp
|
||||
req.SetResult(&e)
|
||||
res, err := req.Execute(http.MethodPost, url)
|
||||
log.Debugln(res.String())
|
||||
if !e.Success {
|
||||
return nil, errors.New(e.Message)
|
||||
}
|
||||
if resp != nil {
|
||||
err = utils.Json.Unmarshal(res.Body(), resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *Yun139) post(pathname string, data interface{}, resp interface{}) ([]byte, error) {
|
||||
return d.request(pathname, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
@@ -449,7 +391,7 @@ func unicode(str string) string {
|
||||
}
|
||||
|
||||
func (d *Yun139) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
url := d.getPersonalCloudHost() + pathname
|
||||
url := "https://personal-kd-njs.yun.139.com" + pathname
|
||||
req := base.RestyClient.R()
|
||||
randStr := random.String(16)
|
||||
ts := time.Now().Format("2006-01-02 15:04:05")
|
||||
@@ -475,6 +417,8 @@ func (d *Yun139) personalRequest(pathname string, method string, callback base.R
|
||||
"Mcloud-Route": "001",
|
||||
"Mcloud-Sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
|
||||
"Mcloud-Version": "7.14.0",
|
||||
"Origin": "https://yun.139.com",
|
||||
"Referer": "https://yun.139.com/w/",
|
||||
"x-DeviceInfo": "||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||",
|
||||
"x-huawei-channelSrc": "10000034",
|
||||
"x-inner-ntwk": "2",
|
||||
@@ -536,7 +480,7 @@ func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
|
||||
"parentFileId": fileId,
|
||||
}
|
||||
var resp PersonalListResp
|
||||
_, err := d.personalPost("/file/list", data, &resp)
|
||||
_, err := d.personalPost("/hcy/file/list", data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -556,15 +500,7 @@ func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
|
||||
} else {
|
||||
var Thumbnails = item.Thumbnails
|
||||
var ThumbnailUrl string
|
||||
if d.UseLargeThumbnail {
|
||||
for _, thumb := range Thumbnails {
|
||||
if strings.Contains(thumb.Style, "Large") {
|
||||
ThumbnailUrl = thumb.Url
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ThumbnailUrl == "" && len(Thumbnails) > 0 {
|
||||
if len(Thumbnails) > 0 {
|
||||
ThumbnailUrl = Thumbnails[len(Thumbnails)-1].Url
|
||||
}
|
||||
f = &model.ObjThumb{
|
||||
@@ -592,7 +528,7 @@ func (d *Yun139) personalGetLink(fileId string) (string, error) {
|
||||
data := base.Json{
|
||||
"fileId": fileId,
|
||||
}
|
||||
res, err := d.personalPost("/file/getDownloadUrl",
|
||||
res, err := d.personalPost("/hcy/file/getDownloadUrl",
|
||||
data, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -617,9 +553,3 @@ func (d *Yun139) getAccount() string {
|
||||
}
|
||||
return d.Account
|
||||
}
|
||||
func (d *Yun139) getPersonalCloudHost() string {
|
||||
if d.ref != nil {
|
||||
return d.ref.getPersonalCloudHost()
|
||||
}
|
||||
return d.PersonalCloudHost
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/rateg"
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AliyundriveOpen struct {
|
||||
@@ -74,18 +72,6 @@ func (d *AliyundriveOpen) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRoot implements the driver.GetRooter interface to properly set up the root object
|
||||
func (d *AliyundriveOpen) GetRoot(ctx context.Context) (model.Obj, error) {
|
||||
return &model.Object{
|
||||
ID: d.RootFolderID,
|
||||
Path: "/",
|
||||
Name: "root",
|
||||
Size: 0,
|
||||
Modified: d.Modified,
|
||||
IsFolder: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
if d.limitList == nil {
|
||||
return nil, fmt.Errorf("driver not init")
|
||||
@@ -94,17 +80,9 @@ func (d *AliyundriveOpen) List(ctx context.Context, dir model.Obj, args model.Li
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objs, err := utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||
obj := fileToObj(src)
|
||||
// Set the correct path for the object
|
||||
if dir.GetPath() != "" {
|
||||
obj.Path = filepath.Join(dir.GetPath(), obj.GetName())
|
||||
}
|
||||
return obj, nil
|
||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||
return fileToObj(src), nil
|
||||
})
|
||||
|
||||
return objs, err
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) link(ctx context.Context, file model.Obj) (*model.Link, error) {
|
||||
@@ -154,16 +132,7 @@ func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirN
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj := fileToObj(newDir)
|
||||
|
||||
// Set the correct Path for the returned directory object
|
||||
if parentDir.GetPath() != "" {
|
||||
obj.Path = filepath.Join(parentDir.GetPath(), dirName)
|
||||
} else {
|
||||
obj.Path = "/" + dirName
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
return fileToObj(newDir), nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
@@ -173,24 +142,20 @@ func (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) (m
|
||||
"drive_id": d.DriveId,
|
||||
"file_id": srcObj.GetID(),
|
||||
"to_parent_file_id": dstDir.GetID(),
|
||||
"check_name_mode": "ignore", // optional:ignore,auto_rename,refuse
|
||||
"check_name_mode": "refuse", // optional:ignore,auto_rename,refuse
|
||||
//"new_name": "newName", // The new name to use when a file of the same name exists
|
||||
}).SetResult(&resp)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Exist {
|
||||
return nil, errors.New("existence of files with the same name")
|
||||
}
|
||||
|
||||
if srcObj, ok := srcObj.(*model.ObjThumb); ok {
|
||||
srcObj.ID = resp.FileID
|
||||
srcObj.Modified = time.Now()
|
||||
srcObj.Path = filepath.Join(dstDir.GetPath(), srcObj.GetName())
|
||||
|
||||
// Check for duplicate files in the destination directory
|
||||
if err := d.removeDuplicateFiles(ctx, dstDir.GetPath(), srcObj.GetName(), srcObj.GetID()); err != nil {
|
||||
// Only log a warning instead of returning an error since the move operation has already completed successfully
|
||||
log.Warnf("Failed to remove duplicate files after move: %v", err)
|
||||
}
|
||||
return srcObj, nil
|
||||
}
|
||||
return nil, nil
|
||||
@@ -208,47 +173,19 @@ func (d *AliyundriveOpen) Rename(ctx context.Context, srcObj model.Obj, newName
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check for duplicate files in the parent directory
|
||||
parentPath := filepath.Dir(srcObj.GetPath())
|
||||
if err := d.removeDuplicateFiles(ctx, parentPath, newName, newFile.FileId); err != nil {
|
||||
// Only log a warning instead of returning an error since the rename operation has already completed successfully
|
||||
log.Warnf("Failed to remove duplicate files after rename: %v", err)
|
||||
}
|
||||
|
||||
obj := fileToObj(newFile)
|
||||
|
||||
// Set the correct Path for the renamed object
|
||||
if parentPath != "" && parentPath != "." {
|
||||
obj.Path = filepath.Join(parentPath, newName)
|
||||
} else {
|
||||
obj.Path = "/" + newName
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
return fileToObj(newFile), nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
var resp MoveOrCopyResp
|
||||
_, err := d.request("/adrive/v1.0/openFile/copy", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"file_id": srcObj.GetID(),
|
||||
"to_parent_file_id": dstDir.GetID(),
|
||||
"auto_rename": false,
|
||||
}).SetResult(&resp)
|
||||
"auto_rename": true,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check for duplicate files in the destination directory
|
||||
if err := d.removeDuplicateFiles(ctx, dstDir.GetPath(), srcObj.GetName(), resp.FileID); err != nil {
|
||||
// Only log a warning instead of returning an error since the copy operation has already completed successfully
|
||||
log.Warnf("Failed to remove duplicate files after copy: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Remove(ctx context.Context, obj model.Obj) error {
|
||||
@@ -266,18 +203,7 @@ func (d *AliyundriveOpen) Remove(ctx context.Context, obj model.Obj) error {
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
obj, err := d.upload(ctx, dstDir, stream, up)
|
||||
|
||||
// Set the correct Path for the returned file object
|
||||
if obj != nil && obj.GetPath() == "" {
|
||||
if dstDir.GetPath() != "" {
|
||||
if objWithPath, ok := obj.(model.SetPath); ok {
|
||||
objWithPath.SetPath(filepath.Join(dstDir.GetPath(), obj.GetName()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj, err
|
||||
return d.upload(ctx, dstDir, stream, up)
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
@@ -309,4 +235,3 @@ var _ driver.MkdirResult = (*AliyundriveOpen)(nil)
|
||||
var _ driver.MoveResult = (*AliyundriveOpen)(nil)
|
||||
var _ driver.RenameResult = (*AliyundriveOpen)(nil)
|
||||
var _ driver.PutResult = (*AliyundriveOpen)(nil)
|
||||
var _ driver.GetRooter = (*AliyundriveOpen)(nil)
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
@@ -187,36 +186,3 @@ func (d *AliyundriveOpen) getAccessToken() string {
|
||||
}
|
||||
return d.AccessToken
|
||||
}
|
||||
|
||||
// Remove duplicate files with the same name in the given directory path,
|
||||
// preserving the file with the given skipID if provided
|
||||
func (d *AliyundriveOpen) removeDuplicateFiles(ctx context.Context, parentPath string, fileName string, skipID string) error {
|
||||
// Handle empty path (root directory) case
|
||||
if parentPath == "" {
|
||||
parentPath = "/"
|
||||
}
|
||||
|
||||
// List all files in the parent directory
|
||||
files, err := op.List(ctx, d, parentPath, model.ListArgs{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find all files with the same name
|
||||
var duplicates []model.Obj
|
||||
for _, file := range files {
|
||||
if file.GetName() == fileName && file.GetID() != skipID {
|
||||
duplicates = append(duplicates, file)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all duplicates files, except the file with the given ID
|
||||
for _, file := range duplicates {
|
||||
err := d.Remove(ctx, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,10 +22,8 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/baidu_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/chaoxing"
|
||||
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
|
||||
_ "github.com/alist-org/alist/v3/drivers/cloudreve_v4"
|
||||
_ "github.com/alist-org/alist/v3/drivers/crypt"
|
||||
_ "github.com/alist-org/alist/v3/drivers/doubao"
|
||||
_ "github.com/alist-org/alist/v3/drivers/doubao_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/dropbox"
|
||||
_ "github.com/alist-org/alist/v3/drivers/febbox"
|
||||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
||||
|
||||
@@ -12,11 +12,6 @@ type Addition struct {
|
||||
SignURLExpire int `json:"sign_url_expire" type:"number" default:"4" help:"The expiration time for SAS URLs, in hours."`
|
||||
}
|
||||
|
||||
// implement GetRootId interface
|
||||
func (r Addition) GetRootId() string {
|
||||
return r.ContainerName
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Azure Blob Storage",
|
||||
LocalSort: true,
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
type Cloudreve struct {
|
||||
model.Storage
|
||||
Addition
|
||||
ref *Cloudreve
|
||||
}
|
||||
|
||||
func (d *Cloudreve) Config() driver.Config {
|
||||
@@ -38,18 +37,8 @@ func (d *Cloudreve) Init(ctx context.Context) error {
|
||||
return d.login()
|
||||
}
|
||||
|
||||
func (d *Cloudreve) InitReference(storage driver.Driver) error {
|
||||
refStorage, ok := storage.(*Cloudreve)
|
||||
if ok {
|
||||
d.ref = refStorage
|
||||
return nil
|
||||
}
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *Cloudreve) Drop(ctx context.Context) error {
|
||||
d.Cookie = ""
|
||||
d.ref = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+44
-113
@@ -4,14 +4,12 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
@@ -21,6 +19,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/pkg/cookie"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
json "github.com/json-iterator/go"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
@@ -36,9 +35,6 @@ func (d *Cloudreve) getUA() string {
|
||||
}
|
||||
|
||||
func (d *Cloudreve) request(method string, path string, callback base.ReqCallback, out interface{}) error {
|
||||
if d.ref != nil {
|
||||
return d.ref.request(method, path, callback, out)
|
||||
}
|
||||
u := d.Address + "/api/v3" + path
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
@@ -83,11 +79,11 @@ func (d *Cloudreve) request(method string, path string, callback base.ReqCallbac
|
||||
}
|
||||
if out != nil && r.Data != nil {
|
||||
var marshal []byte
|
||||
marshal, err = jsoniter.Marshal(r.Data)
|
||||
marshal, err = json.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = jsoniter.Unmarshal(marshal, out)
|
||||
err = json.Unmarshal(marshal, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -191,9 +187,12 @@ func (d *Cloudreve) upLocal(ctx context.Context, stream model.FileStreamer, u Up
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
utils.Log.Debugf("[Cloudreve-Local] upload: %d", finish)
|
||||
var byteSize = DEFAULT
|
||||
left := stream.GetSize() - finish
|
||||
byteSize := min(left, DEFAULT)
|
||||
utils.Log.Debugf("[Cloudreve-Local] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(stream, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
@@ -206,26 +205,9 @@ func (d *Cloudreve) upLocal(ctx context.Context, stream model.FileStreamer, u Up
|
||||
req.SetHeader("Content-Length", strconv.FormatInt(byteSize, 10))
|
||||
req.SetHeader("User-Agent", d.getUA())
|
||||
req.SetBody(driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
|
||||
req.AddRetryCondition(func(r *resty.Response, err error) bool {
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if r.IsError() {
|
||||
return true
|
||||
}
|
||||
var retryResp Resp
|
||||
jErr := base.RestyClient.JSONUnmarshal(r.Body(), &retryResp)
|
||||
if jErr != nil {
|
||||
return true
|
||||
}
|
||||
if retryResp.Code != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
break
|
||||
}
|
||||
finish += byteSize
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
@@ -240,15 +222,16 @@ func (d *Cloudreve) upRemote(ctx context.Context, stream model.FileStreamer, u U
|
||||
var finish int64 = 0
|
||||
var chunk int = 0
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
retryCount := 0
|
||||
maxRetries := 3
|
||||
for finish < stream.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
utils.Log.Debugf("[Cloudreve-Remote] upload: %d", finish)
|
||||
var byteSize = DEFAULT
|
||||
left := stream.GetSize() - finish
|
||||
byteSize := min(left, DEFAULT)
|
||||
utils.Log.Debugf("[Cloudreve-Remote] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(stream, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
@@ -265,43 +248,14 @@ func (d *Cloudreve) upRemote(ctx context.Context, stream model.FileStreamer, u U
|
||||
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Authorization", fmt.Sprint(credential))
|
||||
req.Header.Set("User-Agent", d.getUA())
|
||||
err = func() error {
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var up Resp
|
||||
err = json.Unmarshal(body, &up)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if up.Code != 0 {
|
||||
return errors.New(up.Msg)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err == nil {
|
||||
retryCount = 0
|
||||
finish += byteSize
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
chunk++
|
||||
} else {
|
||||
retryCount++
|
||||
if retryCount > maxRetries {
|
||||
return fmt.Errorf("upload failed after %d retries due to server errors, error: %s", maxRetries, err)
|
||||
}
|
||||
backoff := time.Duration(1<<retryCount) * time.Second
|
||||
utils.Log.Warnf("[Cloudreve-Remote] server errors while uploading, retrying after %v...", backoff)
|
||||
time.Sleep(backoff)
|
||||
finish += byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = res.Body.Close()
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
chunk++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -310,15 +264,16 @@ func (d *Cloudreve) upOneDrive(ctx context.Context, stream model.FileStreamer, u
|
||||
uploadUrl := u.UploadURLs[0]
|
||||
var finish int64 = 0
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
retryCount := 0
|
||||
maxRetries := 3
|
||||
for finish < stream.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
utils.Log.Debugf("[Cloudreve-OneDrive] upload: %d", finish)
|
||||
var byteSize = DEFAULT
|
||||
left := stream.GetSize() - finish
|
||||
byteSize := min(left, DEFAULT)
|
||||
utils.Log.Debugf("[Cloudreve-OneDrive] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(stream, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
@@ -340,31 +295,22 @@ func (d *Cloudreve) upOneDrive(ctx context.Context, stream model.FileStreamer, u
|
||||
return err
|
||||
}
|
||||
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
|
||||
switch {
|
||||
case res.StatusCode >= 500 && res.StatusCode <= 504:
|
||||
retryCount++
|
||||
if retryCount > maxRetries {
|
||||
res.Body.Close()
|
||||
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
|
||||
}
|
||||
backoff := time.Duration(1<<retryCount) * time.Second
|
||||
utils.Log.Warnf("[Cloudreve-OneDrive] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
|
||||
time.Sleep(backoff)
|
||||
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
|
||||
if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 {
|
||||
data, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
_ = res.Body.Close()
|
||||
return errors.New(string(data))
|
||||
default:
|
||||
res.Body.Close()
|
||||
retryCount = 0
|
||||
finish += byteSize
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
}
|
||||
_ = res.Body.Close()
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
}
|
||||
// 上传成功发送回调请求
|
||||
return d.request(http.MethodPost, "/callback/onedrive/finish/"+u.SessionID, func(req *resty.Request) {
|
||||
err := d.request(http.MethodPost, "/callback/onedrive/finish/"+u.SessionID, func(req *resty.Request) {
|
||||
req.SetBody("{}")
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {
|
||||
@@ -372,15 +318,16 @@ func (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u Uploa
|
||||
var chunk int = 0
|
||||
var etags []string
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
retryCount := 0
|
||||
maxRetries := 3
|
||||
for finish < stream.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
utils.Log.Debugf("[Cloudreve-S3] upload: %d", finish)
|
||||
var byteSize = DEFAULT
|
||||
left := stream.GetSize() - finish
|
||||
byteSize := min(left, DEFAULT)
|
||||
utils.Log.Debugf("[Cloudreve-S3] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(stream, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
@@ -399,26 +346,10 @@ func (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u Uploa
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
etag := res.Header.Get("ETag")
|
||||
res.Body.Close()
|
||||
switch {
|
||||
case res.StatusCode != 200:
|
||||
retryCount++
|
||||
if retryCount > maxRetries {
|
||||
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
|
||||
}
|
||||
backoff := time.Duration(1<<retryCount) * time.Second
|
||||
utils.Log.Warnf("[Cloudreve-S3] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
|
||||
time.Sleep(backoff)
|
||||
case etag == "":
|
||||
return errors.New("faild to get ETag from header")
|
||||
default:
|
||||
retryCount = 0
|
||||
etags = append(etags, etag)
|
||||
finish += byteSize
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
chunk++
|
||||
}
|
||||
_ = res.Body.Close()
|
||||
etags = append(etags, res.Header.Get("ETag"))
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
chunk++
|
||||
}
|
||||
|
||||
// s3LikeFinishUpload
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
package cloudreve_v4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type CloudreveV4 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
ref *CloudreveV4
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) Config() driver.Config {
|
||||
if d.ref != nil {
|
||||
return d.ref.Config()
|
||||
}
|
||||
if d.EnableVersionUpload {
|
||||
config.NoOverwriteUpload = false
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) Init(ctx context.Context) error {
|
||||
// removing trailing slash
|
||||
d.Address = strings.TrimSuffix(d.Address, "/")
|
||||
op.MustSaveDriverStorage(d)
|
||||
if d.ref != nil {
|
||||
return nil
|
||||
}
|
||||
if d.AccessToken == "" && d.RefreshToken != "" {
|
||||
return d.refreshToken()
|
||||
}
|
||||
if d.Username != "" {
|
||||
return d.login()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) InitReference(storage driver.Driver) error {
|
||||
refStorage, ok := storage.(*CloudreveV4)
|
||||
if ok {
|
||||
d.ref = refStorage
|
||||
return nil
|
||||
}
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) Drop(ctx context.Context) error {
|
||||
d.ref = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
const pageSize int = 100
|
||||
var f []File
|
||||
var r FileResp
|
||||
params := map[string]string{
|
||||
"page_size": strconv.Itoa(pageSize),
|
||||
"uri": dir.GetPath(),
|
||||
"order_by": d.OrderBy,
|
||||
"order_direction": d.OrderDirection,
|
||||
"page": "0",
|
||||
}
|
||||
|
||||
for {
|
||||
err := d.request(http.MethodGet, "/file", func(req *resty.Request) {
|
||||
req.SetQueryParams(params)
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f = append(f, r.Files...)
|
||||
if r.Pagination.NextToken == "" || len(r.Files) < pageSize {
|
||||
break
|
||||
}
|
||||
params["next_page_token"] = r.Pagination.NextToken
|
||||
}
|
||||
|
||||
return utils.SliceConvert(f, func(src File) (model.Obj, error) {
|
||||
if d.EnableFolderSize && src.Type == 1 {
|
||||
var ds FolderSummaryResp
|
||||
err := d.request(http.MethodGet, "/file/info", func(req *resty.Request) {
|
||||
req.SetQueryParam("uri", src.Path)
|
||||
req.SetQueryParam("folder_summary", "true")
|
||||
}, &ds)
|
||||
if err == nil && ds.FolderSummary.Size > 0 {
|
||||
src.Size = ds.FolderSummary.Size
|
||||
}
|
||||
}
|
||||
var thumb model.Thumbnail
|
||||
if d.EnableThumb && src.Type == 0 {
|
||||
var t FileThumbResp
|
||||
err := d.request(http.MethodGet, "/file/thumb", func(req *resty.Request) {
|
||||
req.SetQueryParam("uri", src.Path)
|
||||
}, &t)
|
||||
if err == nil && t.URL != "" {
|
||||
thumb = model.Thumbnail{
|
||||
Thumbnail: t.URL,
|
||||
}
|
||||
}
|
||||
}
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: src.ID,
|
||||
Path: src.Path,
|
||||
Name: src.Name,
|
||||
Size: src.Size,
|
||||
Modified: src.UpdatedAt,
|
||||
Ctime: src.CreatedAt,
|
||||
IsFolder: src.Type == 1,
|
||||
},
|
||||
Thumbnail: thumb,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var url FileUrlResp
|
||||
err := d.request(http.MethodPost, "/file/url", func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"uris": []string{file.GetPath()},
|
||||
"download": true,
|
||||
})
|
||||
}, &url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(url.Urls) == 0 {
|
||||
return nil, errors.New("server returns no url")
|
||||
}
|
||||
exp := time.Until(url.Expires)
|
||||
return &model.Link{
|
||||
URL: url.Urls[0].URL,
|
||||
Expiration: &exp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
return d.request(http.MethodPost, "/file/create", func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"type": "folder",
|
||||
"uri": parentDir.GetPath() + "/" + dirName,
|
||||
"error_on_conflict": true,
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
return d.request(http.MethodPost, "/file/move", func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"uris": []string{srcObj.GetPath()},
|
||||
"dst": dstDir.GetPath(),
|
||||
"copy": false,
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
return d.request(http.MethodPost, "/file/create", func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"new_name": newName,
|
||||
"uri": srcObj.GetPath(),
|
||||
})
|
||||
}, nil)
|
||||
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
return d.request(http.MethodPost, "/file/move", func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"uris": []string{srcObj.GetPath()},
|
||||
"dst": dstDir.GetPath(),
|
||||
"copy": true,
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) Remove(ctx context.Context, obj model.Obj) error {
|
||||
return d.request(http.MethodDelete, "/file", func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"uris": []string{obj.GetPath()},
|
||||
"unlink": false,
|
||||
"skip_soft_delete": true,
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
|
||||
if file.GetSize() == 0 {
|
||||
// 空文件使用新建文件方法,避免上传卡锁
|
||||
return d.request(http.MethodPost, "/file/create", func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"type": "file",
|
||||
"uri": dstDir.GetPath() + "/" + file.GetName(),
|
||||
"error_on_conflict": true,
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
var p StoragePolicy
|
||||
var r FileResp
|
||||
var u FileUploadResp
|
||||
var err error
|
||||
params := map[string]string{
|
||||
"page_size": "10",
|
||||
"uri": dstDir.GetPath(),
|
||||
"order_by": "created_at",
|
||||
"order_direction": "asc",
|
||||
"page": "0",
|
||||
}
|
||||
err = d.request(http.MethodGet, "/file", func(req *resty.Request) {
|
||||
req.SetQueryParams(params)
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p = r.StoragePolicy
|
||||
body := base.Json{
|
||||
"uri": dstDir.GetPath() + "/" + file.GetName(),
|
||||
"size": file.GetSize(),
|
||||
"policy_id": p.ID,
|
||||
"last_modified": file.ModTime().UnixMilli(),
|
||||
"mime_type": "",
|
||||
}
|
||||
if d.EnableVersionUpload {
|
||||
body["entity_type"] = "version"
|
||||
}
|
||||
err = d.request(http.MethodPut, "/file/upload", func(req *resty.Request) {
|
||||
req.SetBody(body)
|
||||
}, &u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.StoragePolicy.Relay {
|
||||
err = d.upLocal(ctx, file, u, up)
|
||||
} else {
|
||||
switch u.StoragePolicy.Type {
|
||||
case "local":
|
||||
err = d.upLocal(ctx, file, u, up)
|
||||
case "remote":
|
||||
err = d.upRemote(ctx, file, u, up)
|
||||
case "onedrive":
|
||||
err = d.upOneDrive(ctx, file, u, up)
|
||||
case "s3":
|
||||
err = d.upS3(ctx, file, u, up)
|
||||
default:
|
||||
return errs.NotImplement
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// 删除失败的会话
|
||||
_ = d.request(http.MethodDelete, "/file/upload", func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"id": u.SessionID,
|
||||
"uri": u.URI,
|
||||
})
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||
// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||
// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||
// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
|
||||
// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional
|
||||
// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir
|
||||
// return errs.NotImplement to use an internal archive tool
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
//func (d *CloudreveV4) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
var _ driver.Driver = (*CloudreveV4)(nil)
|
||||
@@ -1,44 +0,0 @@
|
||||
package cloudreve_v4
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
// Usually one of two
|
||||
driver.RootPath
|
||||
// driver.RootID
|
||||
// define other
|
||||
Address string `json:"address" required:"true"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
CustomUA string `json:"custom_ua"`
|
||||
EnableFolderSize bool `json:"enable_folder_size"`
|
||||
EnableThumb bool `json:"enable_thumb"`
|
||||
EnableVersionUpload bool `json:"enable_version_upload"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at" default:"name" required:"true"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc" required:"true"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Cloudreve V4",
|
||||
LocalSort: false,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "cloudreve://my",
|
||||
CheckStatus: true,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &CloudreveV4{}
|
||||
})
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package cloudreve_v4
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type Object struct {
|
||||
model.Object
|
||||
StoragePolicy StoragePolicy
|
||||
}
|
||||
|
||||
type Resp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type BasicConfigResp struct {
|
||||
InstanceID string `json:"instance_id"`
|
||||
// Title string `json:"title"`
|
||||
// Themes string `json:"themes"`
|
||||
// DefaultTheme string `json:"default_theme"`
|
||||
User struct {
|
||||
ID string `json:"id"`
|
||||
// Nickname string `json:"nickname"`
|
||||
// CreatedAt time.Time `json:"created_at"`
|
||||
// Anonymous bool `json:"anonymous"`
|
||||
Group struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Permission string `json:"permission"`
|
||||
} `json:"group"`
|
||||
} `json:"user"`
|
||||
// Logo string `json:"logo"`
|
||||
// LogoLight string `json:"logo_light"`
|
||||
// CaptchaReCaptchaKey string `json:"captcha_ReCaptchaKey"`
|
||||
CaptchaType string `json:"captcha_type"` // support 'normal' only
|
||||
// AppPromotion bool `json:"app_promotion"`
|
||||
}
|
||||
|
||||
type SiteLoginConfigResp struct {
|
||||
LoginCaptcha bool `json:"login_captcha"`
|
||||
Authn bool `json:"authn"`
|
||||
}
|
||||
|
||||
type PrepareLoginResp struct {
|
||||
WebauthnEnabled bool `json:"webauthn_enabled"`
|
||||
PasswordEnabled bool `json:"password_enabled"`
|
||||
}
|
||||
|
||||
type CaptchaResp struct {
|
||||
Image string `json:"image"`
|
||||
Ticket string `json:"ticket"`
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
AccessExpires time.Time `json:"access_expires"`
|
||||
RefreshExpires time.Time `json:"refresh_expires"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
User struct {
|
||||
ID string `json:"id"`
|
||||
// Email string `json:"email"`
|
||||
// Nickname string `json:"nickname"`
|
||||
Status string `json:"status"`
|
||||
// CreatedAt time.Time `json:"created_at"`
|
||||
Group struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Permission string `json:"permission"`
|
||||
// DirectLinkBatchSize int `json:"direct_link_batch_size"`
|
||||
// TrashRetention int `json:"trash_retention"`
|
||||
} `json:"group"`
|
||||
// Language string `json:"language"`
|
||||
} `json:"user"`
|
||||
Token Token `json:"token"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Type int `json:"type"` // 0: file, 1: folder
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Size int64 `json:"size"`
|
||||
Metadata interface{} `json:"metadata"`
|
||||
Path string `json:"path"`
|
||||
Capability string `json:"capability"`
|
||||
Owned bool `json:"owned"`
|
||||
PrimaryEntity string `json:"primary_entity"`
|
||||
}
|
||||
|
||||
type StoragePolicy struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
MaxSize int64 `json:"max_size"`
|
||||
Relay bool `json:"relay,omitempty"`
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
IsCursor bool `json:"is_cursor"`
|
||||
NextToken string `json:"next_token,omitempty"`
|
||||
}
|
||||
|
||||
type Props struct {
|
||||
Capability string `json:"capability"`
|
||||
MaxPageSize int `json:"max_page_size"`
|
||||
OrderByOptions []string `json:"order_by_options"`
|
||||
OrderDirectionOptions []string `json:"order_direction_options"`
|
||||
}
|
||||
|
||||
type FileResp struct {
|
||||
Files []File `json:"files"`
|
||||
Parent File `json:"parent"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
Props Props `json:"props"`
|
||||
ContextHint string `json:"context_hint"`
|
||||
MixedType bool `json:"mixed_type"`
|
||||
StoragePolicy StoragePolicy `json:"storage_policy"`
|
||||
}
|
||||
|
||||
type FileUrlResp struct {
|
||||
Urls []struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"urls"`
|
||||
Expires time.Time `json:"expires"`
|
||||
}
|
||||
|
||||
type FileUploadResp struct {
|
||||
// UploadID string `json:"upload_id"`
|
||||
SessionID string `json:"session_id"`
|
||||
ChunkSize int64 `json:"chunk_size"`
|
||||
Expires int64 `json:"expires"`
|
||||
StoragePolicy StoragePolicy `json:"storage_policy"`
|
||||
URI string `json:"uri"`
|
||||
CompleteURL string `json:"completeURL,omitempty"` // for S3-like
|
||||
CallbackSecret string `json:"callback_secret,omitempty"` // for S3-like, OneDrive
|
||||
UploadUrls []string `json:"upload_urls,omitempty"` // for not-local
|
||||
Credential string `json:"credential,omitempty"` // for local
|
||||
}
|
||||
|
||||
type FileThumbResp struct {
|
||||
URL string `json:"url"`
|
||||
Expires time.Time `json:"expires"`
|
||||
}
|
||||
|
||||
type FolderSummaryResp struct {
|
||||
File
|
||||
FolderSummary struct {
|
||||
Size int64 `json:"size"`
|
||||
Files int64 `json:"files"`
|
||||
Folders int64 `json:"folders"`
|
||||
Completed bool `json:"completed"`
|
||||
CalculatedAt time.Time `json:"calculated_at"`
|
||||
} `json:"folder_summary"`
|
||||
}
|
||||
@@ -1,476 +0,0 @@
|
||||
package cloudreve_v4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *CloudreveV4) getUA() string {
|
||||
if d.CustomUA != "" {
|
||||
return d.CustomUA
|
||||
}
|
||||
return base.UserAgent
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) request(method string, path string, callback base.ReqCallback, out any) error {
|
||||
if d.ref != nil {
|
||||
return d.ref.request(method, path, callback, out)
|
||||
}
|
||||
u := d.Address + "/api/v4" + path
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"User-Agent": d.getUA(),
|
||||
})
|
||||
if d.AccessToken != "" {
|
||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||
}
|
||||
|
||||
var r Resp
|
||||
req.SetResult(&r)
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
|
||||
resp, err := req.Execute(method, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.IsSuccess() {
|
||||
return errors.New(resp.String())
|
||||
}
|
||||
|
||||
if r.Code != 0 {
|
||||
if r.Code == 401 && d.RefreshToken != "" && path != "/session/token/refresh" {
|
||||
// try to refresh token
|
||||
err = d.refreshToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.request(method, path, callback, out)
|
||||
}
|
||||
return errors.New(r.Msg)
|
||||
}
|
||||
|
||||
if out != nil && r.Data != nil {
|
||||
var marshal []byte
|
||||
marshal, err = json.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(marshal, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) login() error {
|
||||
var siteConfig SiteLoginConfigResp
|
||||
err := d.request(http.MethodGet, "/site/config/login", nil, &siteConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !siteConfig.Authn {
|
||||
return errors.New("authn not support")
|
||||
}
|
||||
var prepareLogin PrepareLoginResp
|
||||
err = d.request(http.MethodGet, "/session/prepare?email="+d.Addition.Username, nil, &prepareLogin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !prepareLogin.PasswordEnabled {
|
||||
return errors.New("password not enabled")
|
||||
}
|
||||
if prepareLogin.WebauthnEnabled {
|
||||
return errors.New("webauthn not support")
|
||||
}
|
||||
for range 5 {
|
||||
err = d.doLogin(siteConfig.LoginCaptcha)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if err.Error() != "CAPTCHA not match." {
|
||||
break
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) doLogin(needCaptcha bool) error {
|
||||
var err error
|
||||
loginBody := base.Json{
|
||||
"email": d.Username,
|
||||
"password": d.Password,
|
||||
}
|
||||
if needCaptcha {
|
||||
var config BasicConfigResp
|
||||
err = d.request(http.MethodGet, "/site/config/basic", nil, &config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.CaptchaType != "normal" {
|
||||
return fmt.Errorf("captcha type %s not support", config.CaptchaType)
|
||||
}
|
||||
var captcha CaptchaResp
|
||||
err = d.request(http.MethodGet, "/site/captcha", nil, &captcha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasPrefix(captcha.Image, "data:image/png;base64,") {
|
||||
return errors.New("can not get captcha")
|
||||
}
|
||||
loginBody["ticket"] = captcha.Ticket
|
||||
i := strings.Index(captcha.Image, ",")
|
||||
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(captcha.Image[i+1:]))
|
||||
vRes, err := base.RestyClient.R().SetMultipartField(
|
||||
"image", "validateCode.png", "image/png", dec).
|
||||
Post(setting.GetStr(conf.OcrApi))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 {
|
||||
return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString())
|
||||
}
|
||||
captchaCode := jsoniter.Get(vRes.Body(), "result").ToString()
|
||||
if captchaCode == "" {
|
||||
return errors.New("ocr error: empty result")
|
||||
}
|
||||
loginBody["captcha"] = captchaCode
|
||||
}
|
||||
var token TokenResponse
|
||||
err = d.request(http.MethodPost, "/session/token", func(req *resty.Request) {
|
||||
req.SetBody(loginBody)
|
||||
}, &token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.AccessToken, d.RefreshToken = token.Token.AccessToken, token.Token.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) refreshToken() error {
|
||||
var token Token
|
||||
if token.RefreshToken == "" {
|
||||
if d.Username != "" {
|
||||
err := d.login()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot login to get refresh token, error: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err := d.request(http.MethodPost, "/session/token/refresh", func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"refresh_token": d.RefreshToken,
|
||||
})
|
||||
}, &token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.AccessToken, d.RefreshToken = token.AccessToken, token.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) upLocal(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
|
||||
var finish int64 = 0
|
||||
var chunk int = 0
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
if DEFAULT == 0 {
|
||||
// support relay
|
||||
DEFAULT = file.GetSize()
|
||||
}
|
||||
for finish < file.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
left := file.GetSize() - finish
|
||||
byteSize := min(left, DEFAULT)
|
||||
utils.Log.Debugf("[CloudreveV4-Local] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(file, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.request(http.MethodPost, "/file/upload/"+u.SessionID+"/"+strconv.Itoa(chunk), func(req *resty.Request) {
|
||||
req.SetHeader("Content-Type", "application/octet-stream")
|
||||
req.SetContentLength(true)
|
||||
req.SetHeader("Content-Length", strconv.FormatInt(byteSize, 10))
|
||||
req.SetBody(driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
|
||||
req.AddRetryCondition(func(r *resty.Response, err error) bool {
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if r.IsError() {
|
||||
return true
|
||||
}
|
||||
var retryResp Resp
|
||||
jErr := base.RestyClient.JSONUnmarshal(r.Body(), &retryResp)
|
||||
if jErr != nil {
|
||||
return true
|
||||
}
|
||||
if retryResp.Code != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
finish += byteSize
|
||||
up(float64(finish) * 100 / float64(file.GetSize()))
|
||||
chunk++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) upRemote(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
|
||||
uploadUrl := u.UploadUrls[0]
|
||||
credential := u.Credential
|
||||
var finish int64 = 0
|
||||
var chunk int = 0
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
retryCount := 0
|
||||
maxRetries := 3
|
||||
for finish < file.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
left := file.GetSize() - finish
|
||||
byteSize := min(left, DEFAULT)
|
||||
utils.Log.Debugf("[CloudreveV4-Remote] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(file, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("POST", uploadUrl+"?chunk="+strconv.Itoa(chunk),
|
||||
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.ContentLength = byteSize
|
||||
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Authorization", fmt.Sprint(credential))
|
||||
req.Header.Set("User-Agent", d.getUA())
|
||||
err = func() error {
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var up Resp
|
||||
err = json.Unmarshal(body, &up)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if up.Code != 0 {
|
||||
return errors.New(up.Msg)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err == nil {
|
||||
retryCount = 0
|
||||
finish += byteSize
|
||||
up(float64(finish) * 100 / float64(file.GetSize()))
|
||||
chunk++
|
||||
} else {
|
||||
retryCount++
|
||||
if retryCount > maxRetries {
|
||||
return fmt.Errorf("upload failed after %d retries due to server errors, error: %s", maxRetries, err)
|
||||
}
|
||||
backoff := time.Duration(1<<retryCount) * time.Second
|
||||
utils.Log.Warnf("[Cloudreve-Remote] server errors while uploading, retrying after %v...", backoff)
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) upOneDrive(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
|
||||
uploadUrl := u.UploadUrls[0]
|
||||
var finish int64 = 0
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
retryCount := 0
|
||||
maxRetries := 3
|
||||
for finish < file.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
left := file.GetSize() - finish
|
||||
byteSize := min(left, DEFAULT)
|
||||
utils.Log.Debugf("[CloudreveV4-OneDrive] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(file, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.ContentLength = byteSize
|
||||
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, file.GetSize()))
|
||||
req.Header.Set("User-Agent", d.getUA())
|
||||
finish += byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
|
||||
switch {
|
||||
case res.StatusCode >= 500 && res.StatusCode <= 504:
|
||||
retryCount++
|
||||
if retryCount > maxRetries {
|
||||
res.Body.Close()
|
||||
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
|
||||
}
|
||||
backoff := time.Duration(1<<retryCount) * time.Second
|
||||
utils.Log.Warnf("[CloudreveV4-OneDrive] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
|
||||
time.Sleep(backoff)
|
||||
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
|
||||
data, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(string(data))
|
||||
default:
|
||||
res.Body.Close()
|
||||
retryCount = 0
|
||||
finish += byteSize
|
||||
up(float64(finish) * 100 / float64(file.GetSize()))
|
||||
}
|
||||
}
|
||||
// 上传成功发送回调请求
|
||||
return d.request(http.MethodPost, "/callback/onedrive/"+u.SessionID+"/"+u.CallbackSecret, func(req *resty.Request) {
|
||||
req.SetBody("{}")
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (d *CloudreveV4) upS3(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
|
||||
var finish int64 = 0
|
||||
var chunk int = 0
|
||||
var etags []string
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
retryCount := 0
|
||||
maxRetries := 3
|
||||
for finish < file.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
left := file.GetSize() - finish
|
||||
byteSize := min(left, DEFAULT)
|
||||
utils.Log.Debugf("[CloudreveV4-S3] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(file, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPut, u.UploadUrls[chunk],
|
||||
driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.ContentLength = byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
etag := res.Header.Get("ETag")
|
||||
res.Body.Close()
|
||||
switch {
|
||||
case res.StatusCode != 200:
|
||||
retryCount++
|
||||
if retryCount > maxRetries {
|
||||
return fmt.Errorf("upload failed after %d retries due to server errors", maxRetries)
|
||||
}
|
||||
backoff := time.Duration(1<<retryCount) * time.Second
|
||||
utils.Log.Warnf("server error %d, retrying after %v...", res.StatusCode, backoff)
|
||||
time.Sleep(backoff)
|
||||
case etag == "":
|
||||
return errors.New("faild to get ETag from header")
|
||||
default:
|
||||
retryCount = 0
|
||||
etags = append(etags, etag)
|
||||
finish += byteSize
|
||||
up(float64(finish) * 100 / float64(file.GetSize()))
|
||||
chunk++
|
||||
}
|
||||
}
|
||||
|
||||
// s3LikeFinishUpload
|
||||
bodyBuilder := &strings.Builder{}
|
||||
bodyBuilder.WriteString("<CompleteMultipartUpload>")
|
||||
for i, etag := range etags {
|
||||
bodyBuilder.WriteString(fmt.Sprintf(
|
||||
`<Part><PartNumber>%d</PartNumber><ETag>%s</ETag></Part>`,
|
||||
i+1, // PartNumber 从 1 开始
|
||||
etag,
|
||||
))
|
||||
}
|
||||
bodyBuilder.WriteString("</CompleteMultipartUpload>")
|
||||
req, err := http.NewRequest(
|
||||
"POST",
|
||||
u.CompleteURL,
|
||||
strings.NewReader(bodyBuilder.String()),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/xml")
|
||||
req.Header.Set("User-Agent", d.getUA())
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
return fmt.Errorf("up status: %d, error: %s", res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 上传成功发送回调请求
|
||||
return d.request(http.MethodPost, "/callback/s3/"+u.SessionID+"/"+u.CallbackSecret, func(req *resty.Request) {
|
||||
req.SetBody("{}")
|
||||
}, nil)
|
||||
}
|
||||
+23
-41
@@ -3,11 +3,6 @@ package doubao
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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"
|
||||
@@ -15,6 +10,10 @@ import (
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/google/uuid"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Doubao struct {
|
||||
@@ -98,50 +97,33 @@ func (d *Doubao) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
var downloadUrl string
|
||||
|
||||
if u, ok := file.(*Object); ok {
|
||||
switch d.DownloadApi {
|
||||
case "get_download_info":
|
||||
var r GetDownloadInfoResp
|
||||
_, err := d.request("/samantha/aispace/get_download_info", http.MethodPost, func(req *resty.Request) {
|
||||
switch u.NodeType {
|
||||
case VideoType, AudioType:
|
||||
var r GetVideoFileUrlResp
|
||||
_, err := d.request("/samantha/media/get_play_info", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"requests": []base.Json{{"node_id": file.GetID()}},
|
||||
"key": u.Key,
|
||||
"node_id": file.GetID(),
|
||||
})
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadUrl = r.Data.DownloadInfos[0].MainURL
|
||||
case "get_file_url":
|
||||
switch u.NodeType {
|
||||
case VideoType, AudioType:
|
||||
var r GetVideoFileUrlResp
|
||||
_, err := d.request("/samantha/media/get_play_info", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"key": u.Key,
|
||||
"node_id": file.GetID(),
|
||||
})
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadUrl = r.Data.OriginalMediaInfo.MainURL
|
||||
default:
|
||||
var r GetFileUrlResp
|
||||
_, err := d.request("/alice/message/get_file_url", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"uris": []string{u.Key},
|
||||
"type": FileNodeType[u.NodeType],
|
||||
})
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadUrl = r.Data.FileUrls[0].MainURL
|
||||
}
|
||||
downloadUrl = r.Data.OriginalMediaInfo.MainURL
|
||||
default:
|
||||
return nil, errs.NotImplement
|
||||
var r GetFileUrlResp
|
||||
_, err := d.request("/alice/message/get_file_url", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"uris": []string{u.Key},
|
||||
"type": FileNodeType[u.NodeType],
|
||||
})
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadUrl = r.Data.FileUrls[0].MainURL
|
||||
}
|
||||
|
||||
// 生成标准的Content-Disposition
|
||||
|
||||
@@ -12,7 +12,6 @@ type Addition struct {
|
||||
// define other
|
||||
Cookie string `json:"cookie" type:"text"`
|
||||
UploadThread string `json:"upload_thread" default:"3"`
|
||||
DownloadApi string `json:"download_api" type:"select" options:"get_file_url,get_download_info" default:"get_file_url"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
||||
+1
-13
@@ -3,9 +3,8 @@ package doubao
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseResp struct {
|
||||
@@ -39,17 +38,6 @@ type File struct {
|
||||
UpdateTime int64 `json:"update_time"`
|
||||
}
|
||||
|
||||
type GetDownloadInfoResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
DownloadInfos []struct {
|
||||
NodeID string `json:"node_id"`
|
||||
MainURL string `json:"main_url"`
|
||||
BackupURL string `json:"backup_url"`
|
||||
} `json:"download_infos"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type GetFileUrlResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
package doubao_share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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/go-resty/resty/v2"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type DoubaoShare struct {
|
||||
model.Storage
|
||||
Addition
|
||||
RootFiles []RootFileList
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Init(ctx context.Context) error {
|
||||
// 初始化 虚拟分享列表
|
||||
if err := d.initShareList(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
// 检查是否为根目录
|
||||
if dir.GetID() == "" && dir.GetPath() == "/" {
|
||||
return d.listRootDirectory(ctx)
|
||||
}
|
||||
|
||||
// 非根目录,处理不同情况
|
||||
if fo, ok := dir.(*FileObject); ok {
|
||||
if fo.ShareID == "" {
|
||||
// 虚拟目录,需要列出子目录
|
||||
return d.listVirtualDirectoryContent(dir)
|
||||
} else {
|
||||
// 具有分享ID的目录,获取此分享下的文件
|
||||
shareId, relativePath, err := d._findShareAndPath(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.getFilesInPath(ctx, shareId, dir.GetID(), relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用通用方法
|
||||
shareId, relativePath, err := d._findShareAndPath(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取指定路径下的文件
|
||||
return d.getFilesInPath(ctx, shareId, dir.GetID(), relativePath)
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var downloadUrl string
|
||||
|
||||
if u, ok := file.(*FileObject); ok {
|
||||
switch u.NodeType {
|
||||
case VideoType, AudioType:
|
||||
var r GetVideoFileUrlResp
|
||||
_, err := d.request("/samantha/media/get_play_info", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"key": u.Key,
|
||||
"share_id": u.ShareID,
|
||||
"node_id": file.GetID(),
|
||||
})
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadUrl = r.Data.OriginalMediaInfo.MainURL
|
||||
default:
|
||||
var r GetFileUrlResp
|
||||
_, err := d.request("/alice/message/get_file_url", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"uris": []string{u.Key},
|
||||
"type": FileNodeType[u.NodeType],
|
||||
})
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadUrl = r.Data.FileUrls[0].MainURL
|
||||
}
|
||||
|
||||
// 生成标准的Content-Disposition
|
||||
contentDisposition := generateContentDisposition(u.Name)
|
||||
|
||||
return &model.Link{
|
||||
URL: downloadUrl,
|
||||
Header: http.Header{
|
||||
"User-Agent": []string{UserAgent},
|
||||
"Content-Disposition": []string{contentDisposition},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("can't convert obj to URL")
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
// TODO create folder, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
// TODO move obj, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
// TODO rename obj, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
// TODO copy obj, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Remove(ctx context.Context, obj model.Obj) error {
|
||||
// TODO remove obj, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
// TODO upload file, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||
// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||
// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||
// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
|
||||
// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional
|
||||
// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir
|
||||
// return errs.NotImplement to use an internal archive tool
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
//func (d *DoubaoShare) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
var _ driver.Driver = (*DoubaoShare)(nil)
|
||||
@@ -1,32 +0,0 @@
|
||||
package doubao_share
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootPath
|
||||
Cookie string `json:"cookie" type:"text"`
|
||||
ShareIds string `json:"share_ids" type:"text" required:"true"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "DoubaoShare",
|
||||
LocalSort: true,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: true,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &DoubaoShare{}
|
||||
})
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
package doubao_share
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type BaseResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type NodeInfoData struct {
|
||||
Share ShareInfo `json:"share,omitempty"`
|
||||
Creator CreatorInfo `json:"creator,omitempty"`
|
||||
NodeList []File `json:"node_list,omitempty"`
|
||||
NodeInfo File `json:"node_info,omitempty"`
|
||||
Children []File `json:"children,omitempty"`
|
||||
Path FilePath `json:"path,omitempty"`
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
HasMore bool `json:"has_more,omitempty"`
|
||||
}
|
||||
|
||||
type NodeInfoResp struct {
|
||||
BaseResp
|
||||
NodeInfoData `json:"data"`
|
||||
}
|
||||
|
||||
type RootFileList struct {
|
||||
ShareID string
|
||||
VirtualPath string
|
||||
NodeInfo NodeInfoData
|
||||
Child *[]RootFileList
|
||||
}
|
||||
|
||||
type File struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
NodeType int `json:"node_type"`
|
||||
Size int64 `json:"size"`
|
||||
Source int `json:"source"`
|
||||
NameReviewStatus int `json:"name_review_status"`
|
||||
ContentReviewStatus int `json:"content_review_status"`
|
||||
RiskReviewStatus int `json:"risk_review_status"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
ParentID string `json:"parent_id"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
UpdateTime int64 `json:"update_time"`
|
||||
}
|
||||
|
||||
type FileObject struct {
|
||||
model.Object
|
||||
ShareID string
|
||||
Key string
|
||||
NodeID string
|
||||
NodeType int
|
||||
}
|
||||
|
||||
type ShareInfo struct {
|
||||
ShareID string `json:"share_id"`
|
||||
FirstNode struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
NodeType int `json:"node_type"`
|
||||
Size int `json:"size"`
|
||||
Source int `json:"source"`
|
||||
Content struct {
|
||||
LinkFileType string `json:"link_file_type"`
|
||||
ImageWidth int `json:"image_width"`
|
||||
ImageHeight int `json:"image_height"`
|
||||
AiSkillStatus int `json:"ai_skill_status"`
|
||||
} `json:"content"`
|
||||
NameReviewStatus int `json:"name_review_status"`
|
||||
ContentReviewStatus int `json:"content_review_status"`
|
||||
RiskReviewStatus int `json:"risk_review_status"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
ParentID string `json:"parent_id"`
|
||||
CreateTime int `json:"create_time"`
|
||||
UpdateTime int `json:"update_time"`
|
||||
} `json:"first_node"`
|
||||
NodeCount int `json:"node_count"`
|
||||
CreateTime int `json:"create_time"`
|
||||
Channel string `json:"channel"`
|
||||
InfluencerType int `json:"influencer_type"`
|
||||
}
|
||||
|
||||
type CreatorInfo struct {
|
||||
EntityID string `json:"entity_id"`
|
||||
UserName string `json:"user_name"`
|
||||
NickName string `json:"nick_name"`
|
||||
Avatar struct {
|
||||
OriginURL string `json:"origin_url"`
|
||||
TinyURL string `json:"tiny_url"`
|
||||
URI string `json:"uri"`
|
||||
} `json:"avatar"`
|
||||
}
|
||||
|
||||
type FilePath []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
NodeType int `json:"node_type"`
|
||||
Size int `json:"size"`
|
||||
Source int `json:"source"`
|
||||
NameReviewStatus int `json:"name_review_status"`
|
||||
ContentReviewStatus int `json:"content_review_status"`
|
||||
RiskReviewStatus int `json:"risk_review_status"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
ParentID string `json:"parent_id"`
|
||||
CreateTime int `json:"create_time"`
|
||||
UpdateTime int `json:"update_time"`
|
||||
}
|
||||
|
||||
type GetFileUrlResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
FileUrls []struct {
|
||||
URI string `json:"uri"`
|
||||
MainURL string `json:"main_url"`
|
||||
BackURL string `json:"back_url"`
|
||||
} `json:"file_urls"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type GetVideoFileUrlResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
MediaType string `json:"media_type"`
|
||||
MediaInfo []struct {
|
||||
Meta struct {
|
||||
Height string `json:"height"`
|
||||
Width string `json:"width"`
|
||||
Format string `json:"format"`
|
||||
Duration float64 `json:"duration"`
|
||||
CodecType string `json:"codec_type"`
|
||||
Definition string `json:"definition"`
|
||||
} `json:"meta"`
|
||||
MainURL string `json:"main_url"`
|
||||
BackupURL string `json:"backup_url"`
|
||||
} `json:"media_info"`
|
||||
OriginalMediaInfo struct {
|
||||
Meta struct {
|
||||
Height string `json:"height"`
|
||||
Width string `json:"width"`
|
||||
Format string `json:"format"`
|
||||
Duration float64 `json:"duration"`
|
||||
CodecType string `json:"codec_type"`
|
||||
Definition string `json:"definition"`
|
||||
} `json:"meta"`
|
||||
MainURL string `json:"main_url"`
|
||||
BackupURL string `json:"backup_url"`
|
||||
} `json:"original_media_info"`
|
||||
PosterURL string `json:"poster_url"`
|
||||
PlayableStatus int `json:"playable_status"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type CommonResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Message string `json:"message,omitempty"` // 错误情况下的消息
|
||||
Data json.RawMessage `json:"data,omitempty"` // 原始数据,稍后解析
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Locale string `json:"locale"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// IsSuccess 判断响应是否成功
|
||||
func (r *CommonResp) IsSuccess() bool {
|
||||
return r.Code == 0
|
||||
}
|
||||
|
||||
// GetError 获取错误信息
|
||||
func (r *CommonResp) GetError() error {
|
||||
if r.IsSuccess() {
|
||||
return nil
|
||||
}
|
||||
// 优先使用message字段
|
||||
errMsg := r.Message
|
||||
if errMsg == "" {
|
||||
errMsg = r.Msg
|
||||
}
|
||||
// 如果error对象存在且有详细消息,则使用error中的信息
|
||||
if r.Error != nil && r.Error.Message != "" {
|
||||
errMsg = r.Error.Message
|
||||
}
|
||||
|
||||
return fmt.Errorf("[doubao] API error (code: %d): %s", r.Code, errMsg)
|
||||
}
|
||||
|
||||
// UnmarshalData 将data字段解析为指定类型
|
||||
func (r *CommonResp) UnmarshalData(v interface{}) error {
|
||||
if !r.IsSuccess() {
|
||||
return r.GetError()
|
||||
}
|
||||
|
||||
if len(r.Data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(r.Data, v)
|
||||
}
|
||||
@@ -1,744 +0,0 @@
|
||||
package doubao_share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DirectoryType = 1
|
||||
FileType = 2
|
||||
LinkType = 3
|
||||
ImageType = 4
|
||||
PagesType = 5
|
||||
VideoType = 6
|
||||
AudioType = 7
|
||||
MeetingMinutesType = 8
|
||||
)
|
||||
|
||||
var FileNodeType = map[int]string{
|
||||
1: "directory",
|
||||
2: "file",
|
||||
3: "link",
|
||||
4: "image",
|
||||
5: "pages",
|
||||
6: "video",
|
||||
7: "audio",
|
||||
8: "meeting_minutes",
|
||||
}
|
||||
|
||||
const (
|
||||
BaseURL = "https://www.doubao.com"
|
||||
FileDataType = "file"
|
||||
ImgDataType = "image"
|
||||
VideoDataType = "video"
|
||||
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
func (d *DoubaoShare) request(path string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
reqUrl := BaseURL + path
|
||||
req := base.RestyClient.R()
|
||||
|
||||
req.SetHeaders(map[string]string{
|
||||
"Cookie": d.Cookie,
|
||||
"User-Agent": UserAgent,
|
||||
})
|
||||
|
||||
req.SetQueryParams(map[string]string{
|
||||
"version_code": "20800",
|
||||
"device_platform": "web",
|
||||
})
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
|
||||
var commonResp CommonResp
|
||||
|
||||
res, err := req.Execute(method, reqUrl)
|
||||
log.Debugln(res.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body := res.Body()
|
||||
// 先解析为通用响应
|
||||
if err = json.Unmarshal(body, &commonResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 检查响应是否成功
|
||||
if !commonResp.IsSuccess() {
|
||||
return body, commonResp.GetError()
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
if err = json.Unmarshal(body, resp); err != nil {
|
||||
return body, err
|
||||
}
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) getFiles(dirId, nodeId, cursor string) (resp []File, err error) {
|
||||
var r NodeInfoResp
|
||||
|
||||
var body = base.Json{
|
||||
"share_id": dirId,
|
||||
"node_id": nodeId,
|
||||
}
|
||||
// 如果有游标,则设置游标和大小
|
||||
if cursor != "" {
|
||||
body["cursor"] = cursor
|
||||
body["size"] = 50
|
||||
} else {
|
||||
body["need_full_path"] = false
|
||||
}
|
||||
|
||||
_, err = d.request("/samantha/aispace/share/node_info", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(body)
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.NodeInfoData.Children != nil {
|
||||
resp = r.NodeInfoData.Children
|
||||
}
|
||||
|
||||
if r.NodeInfoData.NextCursor != "-1" {
|
||||
// 递归获取下一页
|
||||
nextFiles, err := d.getFiles(dirId, nodeId, r.NodeInfoData.NextCursor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = append(r.NodeInfoData.Children, nextFiles...)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) getShareOverview(shareId, cursor string) (resp []File, err error) {
|
||||
return d.getShareOverviewWithHistory(shareId, cursor, make(map[string]bool))
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) getShareOverviewWithHistory(shareId, cursor string, cursorHistory map[string]bool) (resp []File, err error) {
|
||||
var r NodeInfoResp
|
||||
|
||||
var body = base.Json{
|
||||
"share_id": shareId,
|
||||
}
|
||||
// 如果有游标,则设置游标和大小
|
||||
if cursor != "" {
|
||||
body["cursor"] = cursor
|
||||
body["size"] = 50
|
||||
} else {
|
||||
body["need_full_path"] = false
|
||||
}
|
||||
|
||||
_, err = d.request("/samantha/aispace/share/overview", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(body)
|
||||
}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.NodeInfoData.NodeList != nil {
|
||||
resp = r.NodeInfoData.NodeList
|
||||
}
|
||||
|
||||
if r.NodeInfoData.NextCursor != "-1" {
|
||||
// 检查游标是否重复出现,防止无限循环
|
||||
if cursorHistory[r.NodeInfoData.NextCursor] {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 记录当前游标
|
||||
cursorHistory[r.NodeInfoData.NextCursor] = true
|
||||
|
||||
// 递归获取下一页
|
||||
nextFiles, err := d.getShareOverviewWithHistory(shareId, r.NodeInfoData.NextCursor, cursorHistory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = append(resp, nextFiles...)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (d *DoubaoShare) initShareList() error {
|
||||
if d.Addition.ShareIds == "" {
|
||||
return fmt.Errorf("share_ids is empty")
|
||||
}
|
||||
|
||||
// 解析分享配置
|
||||
shareConfigs, rootShares, err := d._parseShareConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查路径冲突
|
||||
if err := d._detectPathConflicts(shareConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
rootMap := d._buildTreeStructure(shareConfigs, rootShares)
|
||||
|
||||
// 提取顶级节点
|
||||
topLevelNodes := d._extractTopLevelNodes(rootMap, rootShares)
|
||||
if len(topLevelNodes) == 0 {
|
||||
return fmt.Errorf("no valid share_ids found")
|
||||
}
|
||||
|
||||
// 存储结果
|
||||
d.RootFiles = topLevelNodes
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 从配置中解析分享ID和路径
|
||||
func (d *DoubaoShare) _parseShareConfigs() (map[string]string, []string, error) {
|
||||
shareConfigs := make(map[string]string) // 路径 -> 分享ID
|
||||
rootShares := make([]string, 0) // 根目录显示的分享ID
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(d.Addition.ShareIds), "\n")
|
||||
if len(lines) == 0 {
|
||||
return nil, nil, fmt.Errorf("no share_ids found")
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析分享ID和路径
|
||||
parts := strings.Split(line, "|")
|
||||
var shareId, sharePath string
|
||||
|
||||
if len(parts) == 1 {
|
||||
// 无路径分享,直接在根目录显示
|
||||
shareId = _extractShareId(parts[0])
|
||||
if shareId != "" {
|
||||
rootShares = append(rootShares, shareId)
|
||||
}
|
||||
continue
|
||||
} else if len(parts) >= 2 {
|
||||
shareId = _extractShareId(parts[0])
|
||||
sharePath = strings.Trim(parts[1], "/")
|
||||
}
|
||||
|
||||
if shareId == "" {
|
||||
log.Warnf("[doubao_share] Invalid Share_id Format: %s", line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 空路径也加入根目录显示
|
||||
if sharePath == "" {
|
||||
rootShares = append(rootShares, shareId)
|
||||
continue
|
||||
}
|
||||
|
||||
// 添加到路径映射
|
||||
shareConfigs[sharePath] = shareId
|
||||
}
|
||||
|
||||
return shareConfigs, rootShares, nil
|
||||
}
|
||||
|
||||
// 检测路径冲突
|
||||
func (d *DoubaoShare) _detectPathConflicts(shareConfigs map[string]string) error {
|
||||
// 检查直接路径冲突
|
||||
pathToShareIds := make(map[string][]string)
|
||||
for sharePath, id := range shareConfigs {
|
||||
pathToShareIds[sharePath] = append(pathToShareIds[sharePath], id)
|
||||
}
|
||||
|
||||
for sharePath, ids := range pathToShareIds {
|
||||
if len(ids) > 1 {
|
||||
return fmt.Errorf("路径冲突: 路径 '%s' 被多个不同的分享ID使用: %s",
|
||||
sharePath, strings.Join(ids, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// 检查层次冲突
|
||||
for path1, id1 := range shareConfigs {
|
||||
for path2, id2 := range shareConfigs {
|
||||
if path1 == path2 || id1 == id2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查前缀冲突
|
||||
if strings.HasPrefix(path2, path1+"/") || strings.HasPrefix(path1, path2+"/") {
|
||||
return fmt.Errorf("路径冲突: 路径 '%s' (ID: %s) 与路径 '%s' (ID: %s) 存在层次冲突",
|
||||
path1, id1, path2, id2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
func (d *DoubaoShare) _buildTreeStructure(shareConfigs map[string]string, rootShares []string) map[string]*RootFileList {
|
||||
rootMap := make(map[string]*RootFileList)
|
||||
|
||||
// 添加所有分享节点
|
||||
for sharePath, shareId := range shareConfigs {
|
||||
children := make([]RootFileList, 0)
|
||||
rootMap[sharePath] = &RootFileList{
|
||||
ShareID: shareId,
|
||||
VirtualPath: sharePath,
|
||||
NodeInfo: NodeInfoData{},
|
||||
Child: &children,
|
||||
}
|
||||
}
|
||||
|
||||
// 构建父子关系
|
||||
for sharePath, node := range rootMap {
|
||||
if sharePath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
pathParts := strings.Split(sharePath, "/")
|
||||
if len(pathParts) > 1 {
|
||||
parentPath := strings.Join(pathParts[:len(pathParts)-1], "/")
|
||||
|
||||
// 确保所有父级路径都已创建
|
||||
_ensurePathExists(rootMap, parentPath)
|
||||
|
||||
// 添加当前节点到父节点
|
||||
if parent, exists := rootMap[parentPath]; exists {
|
||||
*parent.Child = append(*parent.Child, *node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootMap
|
||||
}
|
||||
|
||||
// 提取顶级节点
|
||||
func (d *DoubaoShare) _extractTopLevelNodes(rootMap map[string]*RootFileList, rootShares []string) []RootFileList {
|
||||
var topLevelNodes []RootFileList
|
||||
|
||||
// 添加根目录分享
|
||||
for _, shareId := range rootShares {
|
||||
children := make([]RootFileList, 0)
|
||||
topLevelNodes = append(topLevelNodes, RootFileList{
|
||||
ShareID: shareId,
|
||||
VirtualPath: "",
|
||||
NodeInfo: NodeInfoData{},
|
||||
Child: &children,
|
||||
})
|
||||
}
|
||||
|
||||
// 添加顶级目录
|
||||
for rootPath, node := range rootMap {
|
||||
if rootPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
isTopLevel := true
|
||||
pathParts := strings.Split(rootPath, "/")
|
||||
|
||||
if len(pathParts) > 1 {
|
||||
parentPath := strings.Join(pathParts[:len(pathParts)-1], "/")
|
||||
if _, exists := rootMap[parentPath]; exists {
|
||||
isTopLevel = false
|
||||
}
|
||||
}
|
||||
|
||||
if isTopLevel {
|
||||
topLevelNodes = append(topLevelNodes, *node)
|
||||
}
|
||||
}
|
||||
|
||||
return topLevelNodes
|
||||
}
|
||||
|
||||
// 确保路径存在,创建所有必要的中间节点
|
||||
func _ensurePathExists(rootMap map[string]*RootFileList, path string) {
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果路径已存在,不需要再处理
|
||||
if _, exists := rootMap[path]; exists {
|
||||
return
|
||||
}
|
||||
|
||||
// 创建当前路径节点
|
||||
children := make([]RootFileList, 0)
|
||||
rootMap[path] = &RootFileList{
|
||||
ShareID: "",
|
||||
VirtualPath: path,
|
||||
NodeInfo: NodeInfoData{},
|
||||
Child: &children,
|
||||
}
|
||||
|
||||
// 处理父路径
|
||||
pathParts := strings.Split(path, "/")
|
||||
if len(pathParts) > 1 {
|
||||
parentPath := strings.Join(pathParts[:len(pathParts)-1], "/")
|
||||
|
||||
// 确保父路径存在
|
||||
_ensurePathExists(rootMap, parentPath)
|
||||
|
||||
// 将当前节点添加为父节点的子节点
|
||||
if parent, exists := rootMap[parentPath]; exists {
|
||||
*parent.Child = append(*parent.Child, *rootMap[path])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// _extractShareId 从URL或直接ID中提取分享ID
|
||||
func _extractShareId(input string) string {
|
||||
input = strings.TrimSpace(input)
|
||||
if strings.HasPrefix(input, "http") {
|
||||
regex := regexp.MustCompile(`/drive/s/([a-zA-Z0-9]+)`)
|
||||
if matches := regex.FindStringSubmatch(input); len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return input // 直接返回ID
|
||||
}
|
||||
|
||||
// _findRootFileByShareID 查找指定ShareID的配置
|
||||
func _findRootFileByShareID(rootFiles []RootFileList, shareID string) *RootFileList {
|
||||
for i, rf := range rootFiles {
|
||||
if rf.ShareID == shareID {
|
||||
return &rootFiles[i]
|
||||
}
|
||||
if rf.Child != nil && len(*rf.Child) > 0 {
|
||||
if found := _findRootFileByShareID(*rf.Child, shareID); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// _findNodeByPath 查找指定路径的节点
|
||||
func _findNodeByPath(rootFiles []RootFileList, path string) *RootFileList {
|
||||
for i, rf := range rootFiles {
|
||||
if rf.VirtualPath == path {
|
||||
return &rootFiles[i]
|
||||
}
|
||||
if rf.Child != nil && len(*rf.Child) > 0 {
|
||||
if found := _findNodeByPath(*rf.Child, path); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// _findShareByPath 根据路径查找分享和相对路径
|
||||
func _findShareByPath(rootFiles []RootFileList, path string) (*RootFileList, string) {
|
||||
// 完全匹配或子路径匹配
|
||||
for i, rf := range rootFiles {
|
||||
if rf.VirtualPath == path {
|
||||
return &rootFiles[i], ""
|
||||
}
|
||||
|
||||
if rf.VirtualPath != "" && strings.HasPrefix(path, rf.VirtualPath+"/") {
|
||||
relPath := strings.TrimPrefix(path, rf.VirtualPath+"/")
|
||||
|
||||
// 先检查子节点
|
||||
if rf.Child != nil && len(*rf.Child) > 0 {
|
||||
if child, childPath := _findShareByPath(*rf.Child, path); child != nil {
|
||||
return child, childPath
|
||||
}
|
||||
}
|
||||
|
||||
return &rootFiles[i], relPath
|
||||
}
|
||||
|
||||
// 递归检查子节点
|
||||
if rf.Child != nil && len(*rf.Child) > 0 {
|
||||
if child, childPath := _findShareByPath(*rf.Child, path); child != nil {
|
||||
return child, childPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查根目录分享
|
||||
for i, rf := range rootFiles {
|
||||
if rf.VirtualPath == "" && rf.ShareID != "" {
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
if len(parts) > 0 && parts[0] == rf.ShareID {
|
||||
if len(parts) > 1 {
|
||||
return &rootFiles[i], parts[1]
|
||||
}
|
||||
return &rootFiles[i], ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// _findShareAndPath 根据给定路径查找对应的ShareID和相对路径
|
||||
func (d *DoubaoShare) _findShareAndPath(dir model.Obj) (string, string, error) {
|
||||
dirPath := dir.GetPath()
|
||||
|
||||
// 如果是根目录,返回空值表示需要列出所有分享
|
||||
if dirPath == "/" || dirPath == "" {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// 检查是否是 FileObject 类型,并获取 ShareID
|
||||
if fo, ok := dir.(*FileObject); ok && fo.ShareID != "" {
|
||||
// 直接使用对象中存储的 ShareID
|
||||
// 计算相对路径(移除前导斜杠)
|
||||
relativePath := strings.TrimPrefix(dirPath, "/")
|
||||
|
||||
// 递归查找对应的 RootFile
|
||||
found := _findRootFileByShareID(d.RootFiles, fo.ShareID)
|
||||
if found != nil {
|
||||
if found.VirtualPath != "" {
|
||||
// 如果此分享配置了路径前缀,需要考虑相对路径的计算
|
||||
if strings.HasPrefix(relativePath, found.VirtualPath) {
|
||||
return fo.ShareID, strings.TrimPrefix(relativePath, found.VirtualPath+"/"), nil
|
||||
}
|
||||
}
|
||||
return fo.ShareID, relativePath, nil
|
||||
}
|
||||
|
||||
// 如果找不到对应的 RootFile 配置,仍然使用对象中的 ShareID
|
||||
return fo.ShareID, relativePath, nil
|
||||
}
|
||||
|
||||
// 移除开头的斜杠
|
||||
cleanPath := strings.TrimPrefix(dirPath, "/")
|
||||
|
||||
// 先检查是否有直接匹配的根目录分享
|
||||
for _, rootFile := range d.RootFiles {
|
||||
if rootFile.VirtualPath == "" && rootFile.ShareID != "" {
|
||||
// 检查是否匹配当前路径的第一部分
|
||||
parts := strings.SplitN(cleanPath, "/", 2)
|
||||
if len(parts) > 0 && parts[0] == rootFile.ShareID {
|
||||
if len(parts) > 1 {
|
||||
return rootFile.ShareID, parts[1], nil
|
||||
}
|
||||
return rootFile.ShareID, "", nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找匹配此路径的分享或虚拟目录
|
||||
share, relPath := _findShareByPath(d.RootFiles, cleanPath)
|
||||
if share != nil {
|
||||
return share.ShareID, relPath, nil
|
||||
}
|
||||
|
||||
log.Warnf("[doubao_share] No matching share path found: %s", dirPath)
|
||||
return "", "", fmt.Errorf("no matching share path found: %s", dirPath)
|
||||
}
|
||||
|
||||
// convertToFileObject 将File转换为FileObject
|
||||
func (d *DoubaoShare) convertToFileObject(file File, shareId string, relativePath string) *FileObject {
|
||||
// 构建文件对象
|
||||
obj := &FileObject{
|
||||
Object: model.Object{
|
||||
ID: file.ID,
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
Modified: time.Unix(file.UpdateTime, 0),
|
||||
Ctime: time.Unix(file.CreateTime, 0),
|
||||
IsFolder: file.NodeType == DirectoryType,
|
||||
Path: path.Join(relativePath, file.Name),
|
||||
},
|
||||
ShareID: shareId,
|
||||
Key: file.Key,
|
||||
NodeID: file.ID,
|
||||
NodeType: file.NodeType,
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// getFilesInPath 获取指定分享和路径下的文件
|
||||
func (d *DoubaoShare) getFilesInPath(ctx context.Context, shareId, nodeId, relativePath string) ([]model.Obj, error) {
|
||||
var (
|
||||
files []File
|
||||
err error
|
||||
)
|
||||
|
||||
// 调用overview接口获取分享链接信息 nodeId
|
||||
if nodeId == "" {
|
||||
files, err = d.getShareOverview(shareId, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get share link information: %w", err)
|
||||
}
|
||||
|
||||
result := make([]model.Obj, 0, len(files))
|
||||
for _, file := range files {
|
||||
result = append(result, d.convertToFileObject(file, shareId, "/"))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
} else {
|
||||
files, err = d.getFiles(shareId, nodeId, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get share file: %w", err)
|
||||
}
|
||||
|
||||
result := make([]model.Obj, 0, len(files))
|
||||
for _, file := range files {
|
||||
result = append(result, d.convertToFileObject(file, shareId, path.Join("/", relativePath)))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// listRootDirectory 处理根目录的内容展示
|
||||
func (d *DoubaoShare) listRootDirectory(ctx context.Context) ([]model.Obj, error) {
|
||||
objects := make([]model.Obj, 0)
|
||||
|
||||
// 分组处理:直接显示的分享内容 vs 虚拟目录
|
||||
var directShareIDs []string
|
||||
addedDirs := make(map[string]bool)
|
||||
|
||||
// 处理所有根节点
|
||||
for _, rootFile := range d.RootFiles {
|
||||
if rootFile.VirtualPath == "" && rootFile.ShareID != "" {
|
||||
// 无路径分享,记录ShareID以便后续获取内容
|
||||
directShareIDs = append(directShareIDs, rootFile.ShareID)
|
||||
} else {
|
||||
// 有路径的分享,显示第一级目录
|
||||
parts := strings.SplitN(rootFile.VirtualPath, "/", 2)
|
||||
firstLevel := parts[0]
|
||||
|
||||
// 避免重复添加同名目录
|
||||
if _, exists := addedDirs[firstLevel]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// 创建虚拟目录对象
|
||||
obj := &FileObject{
|
||||
Object: model.Object{
|
||||
ID: "",
|
||||
Name: firstLevel,
|
||||
Modified: time.Now(),
|
||||
Ctime: time.Now(),
|
||||
IsFolder: true,
|
||||
Path: path.Join("/", firstLevel),
|
||||
},
|
||||
ShareID: rootFile.ShareID,
|
||||
Key: "",
|
||||
NodeID: "",
|
||||
NodeType: DirectoryType,
|
||||
}
|
||||
objects = append(objects, obj)
|
||||
addedDirs[firstLevel] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 处理直接显示的分享内容
|
||||
for _, shareID := range directShareIDs {
|
||||
shareFiles, err := d.getFilesInPath(ctx, shareID, "", "")
|
||||
if err != nil {
|
||||
log.Warnf("[doubao_share] Failed to get list of files in share %s: %s", shareID, err)
|
||||
continue
|
||||
}
|
||||
objects = append(objects, shareFiles...)
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
// listVirtualDirectoryContent 列出虚拟目录的内容
|
||||
func (d *DoubaoShare) listVirtualDirectoryContent(dir model.Obj) ([]model.Obj, error) {
|
||||
dirPath := strings.TrimPrefix(dir.GetPath(), "/")
|
||||
objects := make([]model.Obj, 0)
|
||||
|
||||
// 递归查找此路径的节点
|
||||
node := _findNodeByPath(d.RootFiles, dirPath)
|
||||
|
||||
if node != nil && node.Child != nil {
|
||||
// 显示此节点的所有子节点
|
||||
for _, child := range *node.Child {
|
||||
// 计算显示名称(取路径的最后一部分)
|
||||
displayName := child.VirtualPath
|
||||
if child.VirtualPath != "" {
|
||||
parts := strings.Split(child.VirtualPath, "/")
|
||||
displayName = parts[len(parts)-1]
|
||||
} else if child.ShareID != "" {
|
||||
displayName = child.ShareID
|
||||
}
|
||||
|
||||
obj := &FileObject{
|
||||
Object: model.Object{
|
||||
ID: "",
|
||||
Name: displayName,
|
||||
Modified: time.Now(),
|
||||
Ctime: time.Now(),
|
||||
IsFolder: true,
|
||||
Path: path.Join("/", child.VirtualPath),
|
||||
},
|
||||
ShareID: child.ShareID,
|
||||
Key: "",
|
||||
NodeID: "",
|
||||
NodeType: DirectoryType,
|
||||
}
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
// generateContentDisposition 生成符合RFC 5987标准的Content-Disposition头部
|
||||
func generateContentDisposition(filename string) string {
|
||||
// 按照RFC 2047进行编码,用于filename部分
|
||||
encodedName := urlEncode(filename)
|
||||
|
||||
// 按照RFC 5987进行编码,用于filename*部分
|
||||
encodedNameRFC5987 := encodeRFC5987(filename)
|
||||
|
||||
return fmt.Sprintf("attachment; filename=\"%s\"; filename*=utf-8''%s",
|
||||
encodedName, encodedNameRFC5987)
|
||||
}
|
||||
|
||||
// encodeRFC5987 按照RFC 5987规范编码字符串,适用于HTTP头部参数中的非ASCII字符
|
||||
func encodeRFC5987(s string) string {
|
||||
var buf strings.Builder
|
||||
for _, r := range []byte(s) {
|
||||
// 根据RFC 5987,只有字母、数字和部分特殊符号可以不编码
|
||||
if (r >= 'a' && r <= 'z') ||
|
||||
(r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9') ||
|
||||
r == '-' || r == '.' || r == '_' || r == '~' {
|
||||
buf.WriteByte(r)
|
||||
} else {
|
||||
// 其他字符都需要百分号编码
|
||||
fmt.Fprintf(&buf, "%%%02X", r)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func urlEncode(s string) string {
|
||||
s = url.QueryEscape(s)
|
||||
s = strings.ReplaceAll(s, "+", "%20")
|
||||
return s
|
||||
}
|
||||
@@ -78,42 +78,6 @@ func RemoveNotes(html string) string {
|
||||
})
|
||||
}
|
||||
|
||||
// 清理JS注释
|
||||
func RemoveJSComment(data string) string {
|
||||
var result strings.Builder
|
||||
inComment := false
|
||||
inSingleLineComment := false
|
||||
|
||||
for i := 0; i < len(data); i++ {
|
||||
v := data[i]
|
||||
|
||||
if inSingleLineComment && (v == '\n' || v == '\r') {
|
||||
inSingleLineComment = false
|
||||
result.WriteByte(v)
|
||||
continue
|
||||
}
|
||||
if inComment && v == '*' && i+1 < len(data) && data[i+1] == '/' {
|
||||
inComment = false
|
||||
continue
|
||||
}
|
||||
if v == '/' && i+1 < len(data) {
|
||||
nextChar := data[i+1]
|
||||
if nextChar == '*' {
|
||||
inComment = true
|
||||
i++
|
||||
continue
|
||||
} else if nextChar == '/' {
|
||||
inSingleLineComment = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
result.WriteByte(v)
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
var findAcwScV2Reg = regexp.MustCompile(`arg1='([0-9A-Z]+)'`)
|
||||
|
||||
// 在页面被过多访问或其他情况下,有时候会先返回一个加密的页面,其执行计算出一个acw_sc__v2后放入页面后再重新访问页面才能获得正常页面
|
||||
|
||||
@@ -348,10 +348,6 @@ func (d *LanZou) getFilesByShareUrl(shareID, pwd string, sharePageData string) (
|
||||
file FileOrFolderByShareUrl
|
||||
)
|
||||
|
||||
// 删除注释
|
||||
sharePageData = RemoveNotes(sharePageData)
|
||||
sharePageData = RemoveJSComment(sharePageData)
|
||||
|
||||
// 需要密码
|
||||
if strings.Contains(sharePageData, "pwdload") || strings.Contains(sharePageData, "passwddiv") {
|
||||
sharePageData, err := getJSFunctionByName(sharePageData, "down_p")
|
||||
|
||||
@@ -35,10 +35,6 @@ type Local struct {
|
||||
// zero means no limit
|
||||
thumbConcurrency int
|
||||
thumbTokenBucket TokenBucket
|
||||
|
||||
// video thumb position
|
||||
videoThumbPos float64
|
||||
videoThumbPosIsPercentage bool
|
||||
}
|
||||
|
||||
func (d *Local) Config() driver.Config {
|
||||
@@ -96,8 +92,6 @@ func (d *Local) Init(ctx context.Context) error {
|
||||
if val < 0 || val > 100 {
|
||||
return fmt.Errorf("invalid video_thumb_pos value: %s, the precentage must be a number between 0 and 100", d.VideoThumbPos)
|
||||
}
|
||||
d.videoThumbPosIsPercentage = true
|
||||
d.videoThumbPos = val / 100
|
||||
} else {
|
||||
val, err := strconv.ParseFloat(d.VideoThumbPos, 64)
|
||||
if err != nil {
|
||||
@@ -106,8 +100,6 @@ func (d *Local) Init(ctx context.Context) error {
|
||||
if val < 0 {
|
||||
return fmt.Errorf("invalid video_thumb_pos value: %s, the time must be a positive number", d.VideoThumbPos)
|
||||
}
|
||||
d.videoThumbPosIsPercentage = false
|
||||
d.videoThumbPos = val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+12
-4
@@ -61,14 +61,22 @@ func (d *Local) GetSnapshot(videoPath string) (imgData *bytes.Buffer, err error)
|
||||
}
|
||||
|
||||
var ss string
|
||||
if d.videoThumbPosIsPercentage {
|
||||
ss = fmt.Sprintf("%f", totalDuration*d.videoThumbPos)
|
||||
if strings.HasSuffix(d.VideoThumbPos, "%") {
|
||||
percentage, err := strconv.ParseFloat(strings.TrimSuffix(d.VideoThumbPos, "%"), 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ss = fmt.Sprintf("%f", totalDuration*percentage/100)
|
||||
} else {
|
||||
val, err := strconv.ParseFloat(d.VideoThumbPos, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If the value is greater than the total duration, use the total duration
|
||||
if d.videoThumbPos > totalDuration {
|
||||
if val > totalDuration {
|
||||
ss = fmt.Sprintf("%f", totalDuration)
|
||||
} else {
|
||||
ss = fmt.Sprintf("%f", d.videoThumbPos)
|
||||
ss = d.VideoThumbPos
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-12
@@ -56,21 +56,12 @@ func (d *Mega) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fn := make(map[string]model.Obj)
|
||||
res := make([]model.Obj, 0)
|
||||
for i := range nodes {
|
||||
n := nodes[i]
|
||||
if n.GetType() != mega.FILE && n.GetType() != mega.FOLDER {
|
||||
continue
|
||||
if n.GetType() == mega.FILE || n.GetType() == mega.FOLDER {
|
||||
res = append(res, &MegaNode{n})
|
||||
}
|
||||
if _, ok := fn[n.GetName()]; !ok {
|
||||
fn[n.GetName()] = &MegaNode{n}
|
||||
} else if sameNameObj := fn[n.GetName()]; (&MegaNode{n}).ModTime().After(sameNameObj.ModTime()) {
|
||||
fn[n.GetName()] = &MegaNode{n}
|
||||
}
|
||||
}
|
||||
res := make([]model.Obj, 0)
|
||||
for _, v := range fn {
|
||||
res = append(res, v)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ package netease_music
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/sign"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
@@ -28,8 +28,8 @@ type SongResp struct {
|
||||
}
|
||||
|
||||
type ListResp struct {
|
||||
Size int64 `json:"size"`
|
||||
MaxSize int64 `json:"maxSize"`
|
||||
Size string `json:"size"`
|
||||
MaxSize string `json:"maxSize"`
|
||||
Data []struct {
|
||||
AddTime int64 `json:"addTime"`
|
||||
FileName string `json:"fileName"`
|
||||
|
||||
+11
-22
@@ -8,7 +8,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
stdpath "path"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var onedriveHostMap = map[string]Host{
|
||||
@@ -204,18 +204,19 @@ func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.Fil
|
||||
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
|
||||
var finish int64 = 0
|
||||
DEFAULT := d.ChunkSize * 1024 * 1024
|
||||
retryCount := 0
|
||||
maxRetries := 3
|
||||
for finish < stream.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
log.Debugf("upload: %d", finish)
|
||||
var byteSize int64 = DEFAULT
|
||||
left := stream.GetSize() - finish
|
||||
byteSize := min(left, DEFAULT)
|
||||
utils.Log.Debugf("[Onedrive] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(stream, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -227,31 +228,19 @@ func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.Fil
|
||||
req.ContentLength = byteSize
|
||||
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
|
||||
finish += byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
|
||||
switch {
|
||||
case res.StatusCode >= 500 && res.StatusCode <= 504:
|
||||
retryCount++
|
||||
if retryCount > maxRetries {
|
||||
res.Body.Close()
|
||||
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
|
||||
}
|
||||
backoff := time.Duration(1<<retryCount) * time.Second
|
||||
utils.Log.Warnf("[Onedrive] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
|
||||
time.Sleep(backoff)
|
||||
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
|
||||
if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 {
|
||||
data, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(string(data))
|
||||
default:
|
||||
res.Body.Close()
|
||||
retryCount = 0
|
||||
finish += byteSize
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
}
|
||||
res.Body.Close()
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
stdpath "path"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var onedriveHostMap = map[string]Host{
|
||||
@@ -154,18 +154,19 @@ func (d *OnedriveAPP) upBig(ctx context.Context, dstDir model.Obj, stream model.
|
||||
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
|
||||
var finish int64 = 0
|
||||
DEFAULT := d.ChunkSize * 1024 * 1024
|
||||
retryCount := 0
|
||||
maxRetries := 3
|
||||
for finish < stream.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
log.Debugf("upload: %d", finish)
|
||||
var byteSize int64 = DEFAULT
|
||||
left := stream.GetSize() - finish
|
||||
byteSize := min(left, DEFAULT)
|
||||
utils.Log.Debugf("[OnedriveAPP] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(stream, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -177,31 +178,19 @@ func (d *OnedriveAPP) upBig(ctx context.Context, dstDir model.Obj, stream model.
|
||||
req.ContentLength = byteSize
|
||||
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
|
||||
finish += byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
|
||||
switch {
|
||||
case res.StatusCode >= 500 && res.StatusCode <= 504:
|
||||
retryCount++
|
||||
if retryCount > maxRetries {
|
||||
res.Body.Close()
|
||||
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
|
||||
}
|
||||
backoff := time.Duration(1<<retryCount) * time.Second
|
||||
utils.Log.Warnf("[OnedriveAPP] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
|
||||
time.Sleep(backoff)
|
||||
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
|
||||
if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 {
|
||||
data, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(string(data))
|
||||
default:
|
||||
res.Body.Close()
|
||||
retryCount = 0
|
||||
finish += byteSize
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
}
|
||||
res.Body.Close()
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
umask ${UMASK}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ go 1.23.4
|
||||
|
||||
require (
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0
|
||||
github.com/KirCute/go-smb2-alist v0.0.3
|
||||
github.com/KirCute/sftpd-alist v0.0.12
|
||||
github.com/ProtonMail/go-crypto v1.0.0
|
||||
github.com/SheltonZhu/115driver v1.0.34
|
||||
|
||||
@@ -30,6 +30,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0 h1:ikwCzeqoqN6wvBHOB9OI6dde/jbV7EoTMpUcxtYl5Po=
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0/go.mod h1:v0NgMtKDDi/6CM6r4P+daCljCW3eO9yS+Z+pZDTKo1E=
|
||||
github.com/KirCute/go-smb2-alist v0.0.3 h1:NgL2F6cb0iW4GWa+rR75Bh8Z4TjDoDnt4mBCnhiSqck=
|
||||
github.com/KirCute/go-smb2-alist v0.0.3/go.mod h1:BzUuyzAaJbS4Eb733i9SBxIllZQYDZe5UBsGpkoP6U4=
|
||||
github.com/KirCute/sftpd-alist v0.0.12 h1:GNVM5QLbQLAfXP4wGUlXFA2IO6fVek0n0IsGnOuISdg=
|
||||
github.com/KirCute/sftpd-alist v0.0.12/go.mod h1:2wNK7yyW2XfjyJq10OY6xB4COLac64hOwfV6clDJn6s=
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=
|
||||
|
||||
@@ -93,6 +93,19 @@ type SFTP struct {
|
||||
Listen string `json:"listen" env:"LISTEN"`
|
||||
}
|
||||
|
||||
type SMB struct {
|
||||
Enable bool `json:"enable" env:"ENABLE"`
|
||||
Listen string `json:"listen" env:"LISTEN"`
|
||||
MaxIOReads int `json:"max_io_reads" env:"MAX_IO_READS"`
|
||||
MaxIOWrites int `json:"max_io_writes" env:"MAX_IO_WRITES"`
|
||||
TargetSPN string `json:"target_spn" env:"TARGET_SPN"`
|
||||
NbDomain string `json:"nb_domain" env:"NB_DOMAIN"`
|
||||
NbName string `json:"nb_name" env:"NB_NAME"`
|
||||
DnsName string `json:"dns_name" env:"DNS_NAME"`
|
||||
DnsDomain string `json:"dns_domain" env:"DNS_DOMAIN"`
|
||||
ShareName string `json:"share_name" env:"SHARE_NAME"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Force bool `json:"force" env:"FORCE"`
|
||||
SiteURL string `json:"site_url" env:"SITE_URL"`
|
||||
@@ -115,6 +128,7 @@ type Config struct {
|
||||
S3 S3 `json:"s3" envPrefix:"S3_"`
|
||||
FTP FTP `json:"ftp" envPrefix:"FTP_"`
|
||||
SFTP SFTP `json:"sftp" envPrefix:"SFTP_"`
|
||||
SMB SMB `json:"smb" envPrefix:"SMB_"`
|
||||
LastLaunchedVersion string `json:"last_launched_version"`
|
||||
}
|
||||
|
||||
@@ -212,6 +226,18 @@ func DefaultConfig() *Config {
|
||||
Enable: false,
|
||||
Listen: ":5222",
|
||||
},
|
||||
SMB: SMB{
|
||||
Enable: false,
|
||||
Listen: ":5445",
|
||||
MaxIOReads: 4,
|
||||
MaxIOWrites: 4,
|
||||
TargetSPN: "",
|
||||
NbDomain: "",
|
||||
NbName: "",
|
||||
DnsName: "",
|
||||
DnsDomain: "",
|
||||
ShareName: "AList",
|
||||
},
|
||||
LastLaunchedVersion: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ type User struct {
|
||||
// 11: ftp/sftp write
|
||||
// 12: can read archives
|
||||
// 13: can decompress archives
|
||||
// 14: smb read
|
||||
// 15: smb write
|
||||
Permission int32 `json:"permission"`
|
||||
OtpSecret string `json:"-"`
|
||||
SsoID string `json:"sso_id"` // unique by sso platform
|
||||
@@ -137,6 +139,14 @@ func (u *User) CanDecompress() bool {
|
||||
return (u.Permission>>13)&1 == 1
|
||||
}
|
||||
|
||||
func (u *User) CanSMBAccess() bool {
|
||||
return (u.Permission>>14)&1 == 1
|
||||
}
|
||||
|
||||
func (u *User) CanSMBManage() bool {
|
||||
return (u.Permission>>15)&1 == 1
|
||||
}
|
||||
|
||||
func (u *User) JoinPath(reqPath string) (string, error) {
|
||||
return utils.JoinBasePath(u.BasePath, reqPath)
|
||||
}
|
||||
|
||||
@@ -113,10 +113,6 @@ func UpdateCurrent(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if user.IsGuest() {
|
||||
common.ErrorStrResp(c, "Guest user can not update profile", 403)
|
||||
return
|
||||
}
|
||||
user.Username = req.Username
|
||||
if req.Password != "" {
|
||||
user.SetPassword(req.Password)
|
||||
|
||||
+2
-2
@@ -77,10 +77,10 @@ func Init(e *gin.Engine) {
|
||||
api.GET("/auth/sso_get_token", handles.SSOLoginCallback)
|
||||
|
||||
// webauthn
|
||||
api.GET("/authn/webauthn_begin_login", handles.BeginAuthnLogin)
|
||||
api.POST("/authn/webauthn_finish_login", handles.FinishAuthnLogin)
|
||||
webauthn.GET("/webauthn_begin_registration", handles.BeginAuthnRegistration)
|
||||
webauthn.POST("/webauthn_finish_registration", handles.FinishAuthnRegistration)
|
||||
webauthn.GET("/webauthn_begin_login", handles.BeginAuthnLogin)
|
||||
webauthn.POST("/webauthn_finish_login", handles.FinishAuthnLogin)
|
||||
webauthn.POST("/delete_authn", handles.DeleteAuthnLogin)
|
||||
webauthn.GET("/getcredentials", handles.GetAuthnCredentials)
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
|
||||
smb2 "github.com/KirCute/go-smb2-alist/server"
|
||||
"github.com/KirCute/go-smb2-alist/vfs"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/server/smb"
|
||||
)
|
||||
|
||||
func NewSmbServer() (*smb2.Server, error) {
|
||||
srv := smb2.NewServer(
|
||||
&smb2.ServerConfig{
|
||||
MaxIOReads: conf.Conf.SMB.MaxIOReads,
|
||||
MaxIOWrites: conf.Conf.SMB.MaxIOWrites,
|
||||
Xatrrs: false,
|
||||
},
|
||||
&smb2.NTLMAuthenticator{
|
||||
TargetSPN: conf.Conf.SMB.TargetSPN,
|
||||
NbDomain: conf.Conf.SMB.NbDomain,
|
||||
NbName: conf.Conf.SMB.NbName,
|
||||
DnsName: conf.Conf.SMB.DnsName,
|
||||
DnsDomain: conf.Conf.SMB.DnsDomain,
|
||||
UserPassword: GetUserPassword,
|
||||
AllowGuest: AllowGuest,
|
||||
},
|
||||
GetUserFileSystem,
|
||||
)
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
func AllowGuest() bool {
|
||||
guest, err := op.GetGuest()
|
||||
return err == nil && !guest.Disabled
|
||||
}
|
||||
|
||||
func GetUserPassword(user string) (string, bool) {
|
||||
u, err := op.GetUserByName(user)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
if u.IsGuest() || !u.CanSMBAccess() {
|
||||
return "", false
|
||||
}
|
||||
return u.PwdHash[:16], true
|
||||
}
|
||||
|
||||
func GetUserFileSystem(user string) (map[string]vfs.VFSFileSystem, error) {
|
||||
var userObj *model.User
|
||||
var err error
|
||||
if user == "" {
|
||||
userObj, err = op.GetGuest()
|
||||
} else {
|
||||
userObj, err = op.GetUserByName(user)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !userObj.CanSMBAccess() { // For allow guest case
|
||||
return nil, errs.PermissionDenied
|
||||
}
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "user", userObj)
|
||||
fs, err := smb.NewVFS(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]vfs.VFSFileSystem{
|
||||
conf.Conf.SMB.ShareName: fs,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package smb
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
var (
|
||||
ErrBadHandle = errors.New("bad handle")
|
||||
TestError = errors.New("test error")
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdpath "path"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func Mkdir(ctx context.Context, path string) (model.Obj, error) {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
reqPath, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !user.CanWrite() || !user.CanSMBManage() {
|
||||
meta, err := op.GetNearestMeta(stdpath.Dir(reqPath))
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !common.CanWrite(meta, reqPath) {
|
||||
return nil, errs.PermissionDenied
|
||||
}
|
||||
}
|
||||
if err = fs.MakeDir(ctx, reqPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fs.Get(ctx, reqPath, &fs.GetArgs{})
|
||||
}
|
||||
|
||||
func Remove(ctx context.Context, path string) error {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
if !user.CanRemove() || !user.CanSMBManage() {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
reqPath, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.Remove(ctx, reqPath)
|
||||
}
|
||||
|
||||
func Rename(ctx context.Context, oldPath, newPath string) error {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
srcPath, err := user.JoinPath(oldPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstPath, err := user.JoinPath(newPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcDir, srcBase := stdpath.Split(srcPath)
|
||||
dstDir, dstBase := stdpath.Split(dstPath)
|
||||
if srcDir == dstDir {
|
||||
if !user.CanRename() || !user.CanSMBManage() {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
return fs.Rename(ctx, srcPath, dstBase)
|
||||
} else {
|
||||
if !user.CanSMBManage() || !user.CanMove() || (srcBase != dstBase && !user.CanRename()) {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
if err = fs.Move(ctx, srcPath, dstDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if srcBase != dstBase {
|
||||
return fs.Rename(ctx, stdpath.Join(dstDir, srcBase), dstBase)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hash/fnv"
|
||||
"os"
|
||||
|
||||
"github.com/KirCute/go-smb2-alist/vfs"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type readingFile struct {
|
||||
path string
|
||||
obj model.Obj
|
||||
s stream.SStreamReadAtSeeker
|
||||
dirRead bool
|
||||
}
|
||||
|
||||
func newRead(path string, obj model.Obj) *readingFile {
|
||||
return &readingFile{path: utils.FixAndCleanPath(path), obj: obj, s: nil}
|
||||
}
|
||||
|
||||
func (f *readingFile) initDownload(ctx context.Context) error {
|
||||
if f.s != nil {
|
||||
return nil
|
||||
}
|
||||
if f.obj.IsDir() {
|
||||
return errs.NotFile
|
||||
}
|
||||
user := ctx.Value("user").(*model.User)
|
||||
reqPath, err := user.JoinPath(f.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
meta, err := op.GetNearestMeta(reqPath)
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, "meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, "") {
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
|
||||
link, obj, err := fs.Link(ctx, reqPath, model.LinkArgs{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.obj = obj
|
||||
fileStream := stream.FileStream{
|
||||
Obj: obj,
|
||||
Ctx: ctx,
|
||||
}
|
||||
ss, err := stream.NewSeekableStream(fileStream, link)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, err := stream.NewReadAtSeeker(ss, 0)
|
||||
if err != nil {
|
||||
_ = ss.Close()
|
||||
return err
|
||||
}
|
||||
f.s = reader
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *readingFile) Close() error {
|
||||
if f.s == nil {
|
||||
return nil
|
||||
}
|
||||
return f.s.Close()
|
||||
}
|
||||
|
||||
func FsGet(ctx context.Context, path string) (model.Obj, error) {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
reqPath, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta, err := op.GetNearestMeta(reqPath)
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, "meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, "") {
|
||||
return nil, errs.PermissionDenied
|
||||
}
|
||||
return fs.Get(ctx, reqPath, &fs.GetArgs{})
|
||||
}
|
||||
|
||||
func List(ctx context.Context, path string) ([]vfs.DirInfo, error) {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
reqPath, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta, err := op.GetNearestMeta(reqPath)
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, "meta", meta)
|
||||
if !common.CanAccess(user, meta, reqPath, "") {
|
||||
return nil, errs.PermissionDenied
|
||||
}
|
||||
self, err := fs.Get(ctx, reqPath, &fs.GetArgs{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attr, err := MakeObjAttribute(self)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objs, err := fs.List(ctx, reqPath, &fs.ListArgs{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]vfs.DirInfo, 0, len(objs)+2)
|
||||
ret = append(ret, vfs.DirInfo{Name: ".", Attributes: *attr})
|
||||
if utils.FixAndCleanPath(user.BasePath) != reqPath {
|
||||
ret = append(ret, vfs.DirInfo{Name: "..", Attributes: *attr})
|
||||
}
|
||||
for _, obj := range objs {
|
||||
a, e := MakeObjAttribute(obj)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, vfs.DirInfo{Name: obj.GetName(), Attributes: *a})
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func MakeFileAttribute(file *os.File) (*vfs.Attributes, error) {
|
||||
a := &vfs.Attributes{}
|
||||
a.SetInodeNumber(uint64(file.Fd())) // 用fd没什么依据,纯随便给了个不会冲突的整数
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
a.SetSizeBytes(uint64(stat.Size()))
|
||||
a.SetUnixMode(uint32(stat.Mode()))
|
||||
a.SetPermissions(vfs.NewPermissionsFromMode(uint32(stat.Mode().Perm())))
|
||||
a.SetLastDataModificationTime(stat.ModTime())
|
||||
if stat.IsDir() {
|
||||
a.SetFileType(vfs.FileTypeDirectory)
|
||||
} else {
|
||||
a.SetFileType(vfs.FileTypeRegularFile)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func MakeObjAttribute(obj model.Obj) (*vfs.Attributes, error) {
|
||||
a := &vfs.Attributes{}
|
||||
h := fnv.New64()
|
||||
_, err := h.Write([]byte(obj.GetPath()))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
a.SetInodeNumber(h.Sum64())
|
||||
a.SetSizeBytes(uint64(obj.GetSize()))
|
||||
a.SetLastDataModificationTime(obj.ModTime())
|
||||
if obj.IsDir() {
|
||||
a.SetFileType(vfs.FileTypeDirectory)
|
||||
a.SetUnixMode(0755)
|
||||
a.SetPermissions(vfs.NewPermissionsFromMode(0755))
|
||||
} else {
|
||||
a.SetFileType(vfs.FileTypeRegularFile)
|
||||
a.SetUnixMode(0644)
|
||||
a.SetPermissions(vfs.NewPermissionsFromMode(0644))
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
stdpath "path"
|
||||
"time"
|
||||
|
||||
"github.com/KirCute/go-smb2-alist/vfs"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/internal/task"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type writingFile struct {
|
||||
path string
|
||||
f *os.File
|
||||
}
|
||||
|
||||
func newUpload(ctx context.Context, path string) (*writingFile, error) {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
reqPath, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
err = uploadAuth(ctx, reqPath)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
tmpFile, err := os.CreateTemp(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return &writingFile{f: tmpFile, path: utils.FixAndCleanPath(path)}, nil
|
||||
}
|
||||
|
||||
func uploadAuth(ctx context.Context, path string) error {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
meta, err := op.GetNearestMeta(stdpath.Dir(path))
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
if !(common.CanAccess(user, meta, path, "") &&
|
||||
((user.CanSMBManage() && user.CanWrite()) || common.CanWrite(meta, stdpath.Dir(path)))) {
|
||||
return errors.WithStack(errs.PermissionDenied)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *writingFile) close(ctx context.Context) (task.TaskExtensionInfo, error) {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
reqPath, err := user.JoinPath(f.path)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
dir, name := stdpath.Split(reqPath)
|
||||
size, err := f.f.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
if _, err := f.f.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
arr := make([]byte, 512)
|
||||
if _, err := f.f.Read(arr); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
contentType := http.DetectContentType(arr)
|
||||
if _, err := f.f.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
_ = fs.Remove(ctx, reqPath)
|
||||
s := &stream.FileStream{
|
||||
Obj: &model.Object{
|
||||
Name: name,
|
||||
Size: size,
|
||||
Modified: time.Now(),
|
||||
},
|
||||
Mimetype: contentType,
|
||||
WebPutAsTask: true,
|
||||
}
|
||||
s.SetTmpFile(f.f)
|
||||
return fs.PutAsTask(ctx, dir, s)
|
||||
}
|
||||
|
||||
func MakeTaskAttribute(tsk task.TaskExtensionInfo) (*vfs.Attributes, error) {
|
||||
a := &vfs.Attributes{}
|
||||
h := fnv.New64()
|
||||
_, err := h.Write([]byte(tsk.GetID()))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
a.SetInodeNumber(h.Sum64())
|
||||
a.SetSizeBytes(uint64(tsk.GetTotalBytes()))
|
||||
a.SetLastDataModificationTime(*tsk.GetStartTime())
|
||||
a.SetFileType(vfs.FileTypeRegularFile)
|
||||
a.SetUnixMode(0644)
|
||||
a.SetPermissions(vfs.NewPermissionsFromMode(0644))
|
||||
return a, nil
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
stdpath "path"
|
||||
"sync"
|
||||
|
||||
"github.com/KirCute/go-smb2-alist/vfs"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/internal/task"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/xhofe/tache"
|
||||
)
|
||||
|
||||
type VFS struct {
|
||||
ctx context.Context
|
||||
openedPath map[vfs.VfsHandle]*readingFile
|
||||
openedTmpFile map[vfs.VfsHandle]*writingFile
|
||||
uploadingFile map[string]task.TaskExtensionInfo
|
||||
nextHandle vfs.VfsHandle
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewVFS(ctx context.Context) (vfs.VFSFileSystem, error) {
|
||||
fs := &VFS{
|
||||
ctx: ctx,
|
||||
openedPath: make(map[vfs.VfsHandle]*readingFile),
|
||||
openedTmpFile: make(map[vfs.VfsHandle]*writingFile),
|
||||
uploadingFile: make(map[string]task.TaskExtensionInfo),
|
||||
nextHandle: vfs.VfsHandle(1),
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
root, err := FsGet(ctx, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fs.openedPath[vfs.VfsHandle(0)] = newRead(".", root)
|
||||
user := ctx.Value("user").(*model.User)
|
||||
log.Infof("User %s logged in the SMB endpoint", user.Username)
|
||||
return &VFSWrapper{fs: fs, user: user.Username}, nil
|
||||
}
|
||||
|
||||
func (fs *VFS) GetAttr(handle vfs.VfsHandle) (*vfs.Attributes, error) {
|
||||
fs.mutex.RLock()
|
||||
defer fs.mutex.RUnlock()
|
||||
if tmp, ok := fs.openedTmpFile[handle]; ok {
|
||||
return MakeFileAttribute(tmp.f)
|
||||
}
|
||||
if r, ok := fs.openedPath[handle]; ok {
|
||||
return MakeObjAttribute(r.obj)
|
||||
}
|
||||
return nil, ErrBadHandle
|
||||
}
|
||||
|
||||
func (fs *VFS) Flush(handle vfs.VfsHandle) error {
|
||||
fs.mutex.RLock()
|
||||
tmp, ok := fs.openedTmpFile[handle]
|
||||
fs.mutex.RUnlock()
|
||||
if ok {
|
||||
return tmp.f.Sync()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *VFS) newUpload(path string) (vfs.VfsHandle, error) {
|
||||
f, err := newUpload(fs.ctx, path)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
fs.mutex.Lock()
|
||||
defer fs.mutex.Unlock()
|
||||
handle := fs.nextHandle
|
||||
fs.nextHandle++
|
||||
fs.openedTmpFile[handle] = f
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
func (fs *VFS) Open(path string, flags int, _ int) (vfs.VfsHandle, error) {
|
||||
if (flags & os.O_APPEND) != 0 {
|
||||
return 0, errors.WithStack(errs.NotSupport)
|
||||
}
|
||||
path = utils.FixAndCleanPath(path)
|
||||
fs.mutex.RLock()
|
||||
for h, tmp := range fs.openedTmpFile {
|
||||
if tmp.path == path {
|
||||
fs.mutex.RUnlock()
|
||||
return h, nil
|
||||
}
|
||||
}
|
||||
t, ok := fs.uploadingFile[path]
|
||||
fs.mutex.RUnlock()
|
||||
if ok {
|
||||
if t.GetState() == tache.StateSucceeded {
|
||||
fs.mutex.Lock()
|
||||
delete(fs.uploadingFile, path)
|
||||
fs.mutex.Unlock()
|
||||
} else if t.GetState() == tache.StateFailed || t.GetState() == tache.StateCanceled {
|
||||
fs.mutex.Lock()
|
||||
delete(fs.uploadingFile, path)
|
||||
fs.mutex.Unlock()
|
||||
return 0, errors.WithStack(os.ErrNotExist)
|
||||
} else {
|
||||
return 0, errors.WithStack(os.ErrPermission)
|
||||
}
|
||||
}
|
||||
obj, err := FsGet(fs.ctx, path)
|
||||
if errors.Is(err, errs.ObjectNotFound) {
|
||||
if (flags&os.O_RDWR) == 0 || (flags&os.O_CREATE) == 0 {
|
||||
return 0, errors.WithStack(os.ErrNotExist)
|
||||
}
|
||||
return fs.newUpload(path)
|
||||
} else if err == nil {
|
||||
if (flags&os.O_CREATE) != 0 && (flags&os.O_EXCL) != 0 {
|
||||
return 0, errors.WithStack(os.ErrExist)
|
||||
}
|
||||
if (flags & os.O_RDWR) != 0 {
|
||||
if (flags&os.O_CREATE) != 0 && (flags&os.O_TRUNC) != 0 {
|
||||
return fs.newUpload(path)
|
||||
}
|
||||
return 0, errors.WithStack(errs.NotSupport)
|
||||
}
|
||||
fs.mutex.Lock()
|
||||
defer fs.mutex.Unlock()
|
||||
handle := fs.nextHandle
|
||||
fs.nextHandle++
|
||||
fs.openedPath[handle] = newRead(path, obj)
|
||||
return handle, nil
|
||||
}
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
func (fs *VFS) Close(handle vfs.VfsHandle) error {
|
||||
if handle == 0 {
|
||||
return ErrBadHandle
|
||||
}
|
||||
fs.mutex.RLock()
|
||||
tmp, ok := fs.openedTmpFile[handle]
|
||||
fs.mutex.RUnlock()
|
||||
if ok {
|
||||
tsk, err := tmp.close(fs.ctx)
|
||||
fs.mutex.Lock()
|
||||
delete(fs.openedTmpFile, handle)
|
||||
if tsk != nil {
|
||||
fs.uploadingFile[tmp.path] = tsk
|
||||
}
|
||||
fs.mutex.Unlock()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
fs.mutex.RLock()
|
||||
r, ok := fs.openedPath[handle]
|
||||
fs.mutex.RUnlock()
|
||||
if ok {
|
||||
err := r.Close()
|
||||
fs.mutex.Lock()
|
||||
delete(fs.openedPath, handle)
|
||||
fs.mutex.Unlock()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return ErrBadHandle
|
||||
}
|
||||
|
||||
func (fs *VFS) Lookup(handle vfs.VfsHandle, name string) (*vfs.Attributes, error) {
|
||||
fs.mutex.RLock()
|
||||
var p *readingFile
|
||||
var ok bool
|
||||
if p, ok = fs.openedPath[handle]; !ok {
|
||||
fs.mutex.RUnlock()
|
||||
return nil, ErrBadHandle
|
||||
}
|
||||
if name == "/" {
|
||||
fs.mutex.RUnlock()
|
||||
return MakeObjAttribute(p.obj)
|
||||
}
|
||||
path := utils.FixAndCleanPath(stdpath.Join(p.path, name))
|
||||
for _, tmp := range fs.openedTmpFile {
|
||||
if tmp.path == path {
|
||||
fs.mutex.RUnlock()
|
||||
return MakeFileAttribute(tmp.f)
|
||||
}
|
||||
}
|
||||
t, ok := fs.uploadingFile[path]
|
||||
fs.mutex.RUnlock()
|
||||
if ok {
|
||||
if t.GetState() == tache.StateSucceeded {
|
||||
fs.mutex.Lock()
|
||||
delete(fs.uploadingFile, path)
|
||||
fs.mutex.Unlock()
|
||||
} else if t.GetState() == tache.StateFailed || t.GetState() == tache.StateCanceled {
|
||||
fs.mutex.Lock()
|
||||
delete(fs.uploadingFile, path)
|
||||
fs.mutex.Unlock()
|
||||
return nil, errors.WithStack(os.ErrNotExist)
|
||||
} else {
|
||||
return MakeTaskAttribute(t)
|
||||
}
|
||||
}
|
||||
obj, err := FsGet(fs.ctx, path)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return MakeObjAttribute(obj)
|
||||
}
|
||||
|
||||
func (fs *VFS) Mkdir(path string, _ int) (*vfs.Attributes, error) {
|
||||
obj, err := Mkdir(fs.ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return MakeObjAttribute(obj)
|
||||
}
|
||||
|
||||
func (fs *VFS) Read(handle vfs.VfsHandle, buf []byte, offset uint64, _ int) (n int, err error) {
|
||||
fs.mutex.RLock()
|
||||
if tmp, ok := fs.openedTmpFile[handle]; ok {
|
||||
fs.mutex.RUnlock()
|
||||
n, err = tmp.f.ReadAt(buf, int64(offset))
|
||||
} else if r, ok := fs.openedPath[handle]; ok {
|
||||
fs.mutex.RUnlock()
|
||||
err = r.initDownload(fs.ctx)
|
||||
if err == nil {
|
||||
n, err = r.s.ReadAt(buf, int64(offset))
|
||||
}
|
||||
} else {
|
||||
fs.mutex.RUnlock()
|
||||
err = ErrBadHandle
|
||||
}
|
||||
_ = stream.ClientDownloadLimit.WaitN(fs.ctx, n)
|
||||
return
|
||||
}
|
||||
|
||||
func (fs *VFS) Write(handle vfs.VfsHandle, buf []byte, offset uint64, _ int) (n int, err error) {
|
||||
fs.mutex.RLock()
|
||||
tmp, ok := fs.openedTmpFile[handle]
|
||||
fs.mutex.RUnlock()
|
||||
if ok {
|
||||
n, err = tmp.f.WriteAt(buf, int64(offset))
|
||||
} else {
|
||||
err = ErrBadHandle
|
||||
}
|
||||
_ = stream.ClientUploadLimit.WaitN(fs.ctx, n)
|
||||
return
|
||||
}
|
||||
|
||||
func (fs *VFS) OpenDir(path string) (vfs.VfsHandle, error) {
|
||||
obj, err := FsGet(fs.ctx, path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fs.mutex.Lock()
|
||||
defer fs.mutex.Unlock()
|
||||
handle := fs.nextHandle
|
||||
fs.nextHandle++
|
||||
fs.openedPath[handle] = newRead(path, obj)
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
func (fs *VFS) ReadDir(handle vfs.VfsHandle, pos int, _ int) ([]vfs.DirInfo, error) {
|
||||
fs.mutex.RLock()
|
||||
p, ok := fs.openedPath[handle]
|
||||
fs.mutex.RUnlock()
|
||||
if !ok {
|
||||
return nil, ErrBadHandle
|
||||
}
|
||||
if pos == 0 && p.dirRead {
|
||||
return nil, io.EOF
|
||||
}
|
||||
p.dirRead = true
|
||||
return List(fs.ctx, p.path)
|
||||
}
|
||||
|
||||
func (fs *VFS) Unlink(handle vfs.VfsHandle) error {
|
||||
if handle == 0 {
|
||||
return ErrBadHandle
|
||||
}
|
||||
fs.mutex.RLock()
|
||||
r, ok := fs.openedPath[handle]
|
||||
fs.mutex.RUnlock()
|
||||
if ok {
|
||||
_ = r.Close()
|
||||
fs.mutex.Lock()
|
||||
delete(fs.openedPath, handle)
|
||||
fs.mutex.Unlock()
|
||||
return Remove(fs.ctx, r.path)
|
||||
}
|
||||
return ErrBadHandle
|
||||
}
|
||||
|
||||
func (fs *VFS) Rename(handle vfs.VfsHandle, to string, _ int) error {
|
||||
if handle == 0 {
|
||||
return ErrBadHandle
|
||||
}
|
||||
fs.mutex.RLock()
|
||||
r, ok := fs.openedPath[handle]
|
||||
fs.mutex.RUnlock()
|
||||
if ok {
|
||||
_ = r.Close()
|
||||
fs.mutex.Lock()
|
||||
delete(fs.openedPath, handle)
|
||||
fs.mutex.Unlock()
|
||||
return Rename(fs.ctx, r.path, to)
|
||||
}
|
||||
return ErrBadHandle
|
||||
}
|
||||
|
||||
func (fs *VFS) StatFS(vfs.VfsHandle) (*vfs.FSAttributes, error) {
|
||||
return &vfs.FSAttributes{}, nil
|
||||
}
|
||||
|
||||
func (fs *VFS) SetAttr(vfs.VfsHandle, *vfs.Attributes) (*vfs.Attributes, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (fs *VFS) FSync(vfs.VfsHandle) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *VFS) Readlink(vfs.VfsHandle) (string, error) {
|
||||
return "", errs.NotSupport
|
||||
}
|
||||
|
||||
func (fs *VFS) Truncate(vfs.VfsHandle, uint64) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (fs *VFS) Symlink(vfs.VfsHandle, string, int) (*vfs.Attributes, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
func (fs *VFS) Link(vfs.VfsNode, vfs.VfsNode, string) (*vfs.Attributes, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (fs *VFS) Listxattr(vfs.VfsHandle) ([]string, error) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
func (fs *VFS) Getxattr(vfs.VfsHandle, string, []byte) (int, error) {
|
||||
return 0, errs.NotSupport
|
||||
}
|
||||
|
||||
func (fs *VFS) Setxattr(vfs.VfsHandle, string, []byte) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (fs *VFS) Removexattr(vfs.VfsHandle, string) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/KirCute/go-smb2-alist/vfs"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type VFSWrapper struct {
|
||||
fs *VFS
|
||||
user string
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) GetAttr(handle vfs.VfsHandle) (*vfs.Attributes, error) {
|
||||
a, e := v.fs.GetAttr(handle)
|
||||
if e != nil {
|
||||
log.Errorf("SMB: %s called GetAttr(%d) and got error: %+v", v.user, handle, e)
|
||||
}
|
||||
return a, e
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) SetAttr(handle vfs.VfsHandle, attributes *vfs.Attributes) (*vfs.Attributes, error) {
|
||||
return v.fs.SetAttr(handle, attributes)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) StatFS(handle vfs.VfsHandle) (*vfs.FSAttributes, error) {
|
||||
return v.fs.StatFS(handle)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) FSync(handle vfs.VfsHandle) error {
|
||||
return v.fs.FSync(handle)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Flush(handle vfs.VfsHandle) error {
|
||||
return v.fs.Flush(handle)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Open(s string, i int, i2 int) (vfs.VfsHandle, error) {
|
||||
h, e := v.fs.Open(s, i, i2)
|
||||
if e == nil {
|
||||
log.Infof("SMB: %s called Open(%s, %o) and got result: %d", v.user, s, i, h)
|
||||
} else {
|
||||
log.Errorf("SMB: %s called Open(%s, %o) and got error: %+v", v.user, s, i, e)
|
||||
}
|
||||
return h, e
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Close(handle vfs.VfsHandle) error {
|
||||
e := v.fs.Close(handle)
|
||||
if e == nil {
|
||||
log.Infof("SMB: %s called Close(%d)", v.user, handle)
|
||||
} else if errors.Is(e, ErrBadHandle) {
|
||||
log.Warnf("SMB: %s called Close(%d) but duplicate", v.user, handle)
|
||||
} else {
|
||||
log.Errorf("SMB: %s called Close(%d) and got error: %+v", v.user, handle, e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Lookup(handle vfs.VfsHandle, s string) (*vfs.Attributes, error) {
|
||||
a, e := v.fs.Lookup(handle, s)
|
||||
if errors.Is(e, errs.ObjectNotFound) {
|
||||
log.Warnf("SMB: %s called Lookup(%d, %s) but not found", v.user, handle, s)
|
||||
} else if e != nil {
|
||||
log.Errorf("SMB: %s called Lookup(%d, %s) and got error: %+v", v.user, handle, s, e)
|
||||
}
|
||||
return a, e
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Mkdir(s string, i int) (*vfs.Attributes, error) {
|
||||
a, e := v.fs.Mkdir(s, i)
|
||||
if e == nil {
|
||||
log.Infof("SMB: %s called Mkdir(%s, %d)", v.user, s, i)
|
||||
} else {
|
||||
log.Errorf("SMB: %s called Mkdir(%s, %d) and got error: %+v", v.user, s, i, e)
|
||||
}
|
||||
return a, e
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Read(handle vfs.VfsHandle, bytes []byte, u uint64, i int) (int, error) {
|
||||
n, err := v.fs.Read(handle, bytes, u, i)
|
||||
if err != nil {
|
||||
log.Errorf("SMB: %s called Read(%d, len=%d, offset=%d), read %d bytes and got error %+v", v.user, handle, len(bytes), u, n, err)
|
||||
} else {
|
||||
log.Infof("SMB: %s called Read(%d, len=%d, offset=%d), read %d bytes", v.user, handle, len(bytes), u, n)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Write(handle vfs.VfsHandle, bytes []byte, u uint64, i int) (int, error) {
|
||||
n, err := v.fs.Write(handle, bytes, u, i)
|
||||
if err != nil {
|
||||
log.Errorf("SMB: %s called Write(%d, len=%d, offset=%d, mode=%d), write %d bytes and got error %+v", v.user, handle, len(bytes), u, i, n, err)
|
||||
} else {
|
||||
log.Infof("SMB: %s called Write(%d, len=%d, offset=%d, mode=%d), write %d bytes", v.user, handle, len(bytes), u, i, n)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) OpenDir(s string) (vfs.VfsHandle, error) {
|
||||
h, e := v.fs.OpenDir(s)
|
||||
if e == nil {
|
||||
log.Infof("SMB: %s called OpenDir(%s) and got result: %d", v.user, s, h)
|
||||
} else {
|
||||
log.Errorf("SMB: %s called OpenDir(%s) and got error: %+v", v.user, s, e)
|
||||
}
|
||||
return h, e
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) ReadDir(handle vfs.VfsHandle, i int, i2 int) ([]vfs.DirInfo, error) {
|
||||
info, e := v.fs.ReadDir(handle, i, i2)
|
||||
if e != nil && !errors.Is(e, io.EOF) {
|
||||
log.Errorf("SMB: %s called ReadDir(%d, %d, %d) and got error: %+v", v.user, handle, i, i2, e)
|
||||
}
|
||||
return info, e
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Readlink(handle vfs.VfsHandle) (string, error) {
|
||||
return v.fs.Readlink(handle)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Unlink(handle vfs.VfsHandle) error {
|
||||
e := v.fs.Unlink(handle)
|
||||
if e == nil {
|
||||
log.Infof("SMB: %s called Unlink(%d)", v.user, handle)
|
||||
} else {
|
||||
log.Errorf("SMB: %s called Unlink(%d) and got error: %+v", v.user, handle, e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Truncate(handle vfs.VfsHandle, u uint64) error {
|
||||
return v.fs.Truncate(handle, u)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Rename(handle vfs.VfsHandle, s string, i int) error {
|
||||
e := v.fs.Rename(handle, s, i)
|
||||
if e == nil {
|
||||
log.Infof("SMB: %s called Rename(%d, %s, %d)", v.user, handle, s, i)
|
||||
} else {
|
||||
log.Errorf("SMB: %s called Rename(%d, %s, %d) and got error: %+v", v.user, handle, s, i, e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Symlink(handle vfs.VfsHandle, s string, i int) (*vfs.Attributes, error) {
|
||||
return v.fs.Symlink(handle, s, i)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Link(node vfs.VfsNode, node2 vfs.VfsNode, s string) (*vfs.Attributes, error) {
|
||||
return v.fs.Link(node, node2, s)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Listxattr(handle vfs.VfsHandle) ([]string, error) {
|
||||
return v.fs.Listxattr(handle)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Getxattr(handle vfs.VfsHandle, s string, bytes []byte) (int, error) {
|
||||
return v.fs.Getxattr(handle, s, bytes)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Setxattr(handle vfs.VfsHandle, s string, bytes []byte) error {
|
||||
return v.fs.Setxattr(handle, s, bytes)
|
||||
}
|
||||
|
||||
func (v *VFSWrapper) Removexattr(handle vfs.VfsHandle, s string) error {
|
||||
return v.fs.Removexattr(handle, s)
|
||||
}
|
||||
Reference in New Issue
Block a user