From 3f6ed1040c89af00594aad82d54d50e0cd7a549f Mon Sep 17 00:00:00 2001 From: nostalume Date: Thu, 24 Sep 2026 20:44:01 +0800 Subject: [PATCH 1/3] refactor(drivers): make optional capabilities truthful - Remove unconditional unsupported mutation and archive-decompress methods from 16 drivers. - Let operation dispatch represent unavailable capabilities through interface absence without adding a compatibility layer. Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- drivers/chaoxing/driver.go | 6 ----- drivers/cloudreve_v4/driver.go | 7 ------ drivers/cnb_releases/driver.go | 8 ------ drivers/degoo/driver.go | 6 ----- drivers/doubao/driver.go | 12 --------- drivers/doubao_new/driver.go | 5 ---- drivers/doubao_share/driver.go | 37 ---------------------------- drivers/febbox/driver.go | 5 ---- drivers/github_releases/driver.go | 26 ------------------- drivers/ilanzou/driver.go | 7 ------ drivers/lenovonas_share/driver.go | 25 ------------------- drivers/mega/driver.go | 5 ---- drivers/onedrive_sharelink/driver.go | 20 --------------- drivers/quark_uc_tv/driver.go | 25 ------------------- drivers/teldrive/driver.go | 7 ------ drivers/weiyun/driver.go | 4 --- 16 files changed, 205 deletions(-) diff --git a/drivers/chaoxing/driver.go b/drivers/chaoxing/driver.go index dfd25d1957..52f4293e81 100644 --- a/drivers/chaoxing/driver.go +++ b/drivers/chaoxing/driver.go @@ -15,7 +15,6 @@ import ( "time" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/pkg/cron" @@ -184,11 +183,6 @@ func (d *ChaoXing) Rename(ctx context.Context, srcObj model.Obj, newName string) return nil } -func (d *ChaoXing) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { - // TODO copy obj, optional - return errs.NotImplement -} - func (d *ChaoXing) Remove(ctx context.Context, obj model.Obj) error { query := map[string]string{ "bbsid": d.Addition.Bbsid, diff --git a/drivers/cloudreve_v4/driver.go b/drivers/cloudreve_v4/driver.go index afd64d3d5e..4d253bbfe3 100644 --- a/drivers/cloudreve_v4/driver.go +++ b/drivers/cloudreve_v4/driver.go @@ -344,13 +344,6 @@ func (d *CloudreveV4) Extract(ctx context.Context, obj model.Obj, args model.Arc 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) GetDetails(ctx context.Context) (*model.StorageDetails, error) { // TODO return storage details (total space, free space, etc.) var r CapacityResp diff --git a/drivers/cnb_releases/driver.go b/drivers/cnb_releases/driver.go index 09ec153c8c..7464e44199 100644 --- a/drivers/cnb_releases/driver.go +++ b/drivers/cnb_releases/driver.go @@ -129,10 +129,6 @@ func (d *CnbReleases) MakeDir(ctx context.Context, parentDir model.Obj, dirName return errs.NotImplement } -func (d *CnbReleases) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - return nil, errs.NotImplement -} - func (d *CnbReleases) Rename(ctx context.Context, srcObj model.Obj, newName string) error { if srcObj.IsDir() && !d.UseTagName { return d.Request(http.MethodPatch, "/{repo}/-/releases/{release_id}", func(req *resty.Request) { @@ -146,10 +142,6 @@ func (d *CnbReleases) Rename(ctx context.Context, srcObj model.Obj, newName stri return errs.NotImplement } -func (d *CnbReleases) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - return nil, errs.NotImplement -} - func (d *CnbReleases) Remove(ctx context.Context, obj model.Obj) error { if obj.IsDir() { return d.Request(http.MethodDelete, "/{repo}/-/releases/{release_id}", func(req *resty.Request) { diff --git a/drivers/degoo/driver.go b/drivers/degoo/driver.go index 4cbc8be63c..a88ec7e923 100644 --- a/drivers/degoo/driver.go +++ b/drivers/degoo/driver.go @@ -9,7 +9,6 @@ import ( "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/pkg/utils" ) @@ -143,11 +142,6 @@ func (d *Degoo) Rename(ctx context.Context, srcObj model.Obj, newName string) er return nil } -func (d *Degoo) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - // Copy is not implemented, Degoo API does not support direct copy. - return nil, errs.NotImplement -} - func (d *Degoo) Remove(ctx context.Context, obj model.Obj) error { // Remove deletes a file or folder (moves to trash). const query = `mutation SetDeleteFile5($Token: String!, $IsInRecycleBin: Boolean!, $IDs: [IDType]!) { setDeleteFile5(Token: $Token, IsInRecycleBin: $IsInRecycleBin, IDs: $IDs) }` diff --git a/drivers/doubao/driver.go b/drivers/doubao/driver.go index 6d4a4fc6e0..2acde7ceea 100644 --- a/drivers/doubao/driver.go +++ b/drivers/doubao/driver.go @@ -234,11 +234,6 @@ func (d *Doubao) Rename(ctx context.Context, srcObj model.Obj, newName string) e return err } -func (d *Doubao) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - // TODO copy obj, optional - return nil, errs.NotImplement -} - func (d *Doubao) Remove(ctx context.Context, obj model.Obj) error { if err := d.WaitLimit(ctx); err != nil { return err @@ -298,13 +293,6 @@ func (d *Doubao) Extract(ctx context.Context, obj model.Obj, args model.ArchiveI return nil, errs.NotImplement } -func (d *Doubao) 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 *Doubao) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { // return nil, errs.NotSupport //} diff --git a/drivers/doubao_new/driver.go b/drivers/doubao_new/driver.go index f84828f0e1..3cbfbd74f1 100644 --- a/drivers/doubao_new/driver.go +++ b/drivers/doubao_new/driver.go @@ -260,11 +260,6 @@ func (d *DoubaoNew) Rename(ctx context.Context, srcObj model.Obj, newName string return srcObj, nil } -func (d *DoubaoNew) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - // TODO copy obj, optional - return nil, errs.NotImplement -} - func (d *DoubaoNew) Remove(ctx context.Context, obj model.Obj) error { if obj == nil { return errors.New("nil object") diff --git a/drivers/doubao_share/driver.go b/drivers/doubao_share/driver.go index 4d23d7022b..3c9054dabf 100644 --- a/drivers/doubao_share/driver.go +++ b/drivers/doubao_share/driver.go @@ -120,36 +120,6 @@ func (d *DoubaoShare) Link(ctx context.Context, file model.Obj, args model.LinkA 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 @@ -165,13 +135,6 @@ func (d *DoubaoShare) Extract(ctx context.Context, obj model.Obj, args model.Arc 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 //} diff --git a/drivers/febbox/driver.go b/drivers/febbox/driver.go index 2089cb05d7..7cecb891d1 100644 --- a/drivers/febbox/driver.go +++ b/drivers/febbox/driver.go @@ -9,7 +9,6 @@ import ( "golang.org/x/oauth2/clientcredentials" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" ) @@ -126,8 +125,4 @@ func (d *FebBox) Remove(ctx context.Context, obj model.Obj) error { return nil } -func (d *FebBox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { - return nil, errs.NotImplement -} - var _ driver.Driver = (*FebBox)(nil) diff --git a/drivers/github_releases/driver.go b/drivers/github_releases/driver.go index e19f7998ae..49fd4d2b04 100644 --- a/drivers/github_releases/driver.go +++ b/drivers/github_releases/driver.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/pkg/utils" log "github.com/sirupsen/logrus" @@ -189,28 +188,3 @@ func (d *GithubReleases) Link(ctx context.Context, file model.Obj, args model.Li } return &link, nil } - -func (d *GithubReleases) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { - // TODO create folder, optional - return nil, errs.NotImplement -} - -func (d *GithubReleases) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - // TODO move obj, optional - return nil, errs.NotImplement -} - -func (d *GithubReleases) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { - // TODO rename obj, optional - return nil, errs.NotImplement -} - -func (d *GithubReleases) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - // TODO copy obj, optional - return nil, errs.NotImplement -} - -func (d *GithubReleases) Remove(ctx context.Context, obj model.Obj) error { - // TODO remove obj, optional - return errs.NotImplement -} diff --git a/drivers/ilanzou/driver.go b/drivers/ilanzou/driver.go index a640d8669a..80acbe7923 100644 --- a/drivers/ilanzou/driver.go +++ b/drivers/ilanzou/driver.go @@ -15,7 +15,6 @@ import ( "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/pkg/utils" @@ -248,12 +247,6 @@ func (d *ILanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) return &model.Object{ID: srcObj.GetID(), Name: newName, Size: srcObj.GetSize(), Modified: time.Now(), Ctime: srcObj.CreateTime(), IsFolder: srcObj.IsDir()}, nil } -// iLanzou has no server-side copy primitive. Returning NotImplement delegates -// to OpenList's persistent CopyTaskManager instead of blocking the HTTP request. -func (d *ILanZou) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - return nil, errs.NotImplement -} - func (d *ILanZou) Remove(ctx context.Context, obj model.Obj) error { var fileIDs, folderIDs []string if obj.IsDir() { diff --git a/drivers/lenovonas_share/driver.go b/drivers/lenovonas_share/driver.go index 526eb1f4e6..618df80070 100644 --- a/drivers/lenovonas_share/driver.go +++ b/drivers/lenovonas_share/driver.go @@ -12,7 +12,6 @@ import ( "github.com/go-resty/resty/v2" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/pkg/utils" ) @@ -139,28 +138,4 @@ func (d *LenovoNasShare) Link(ctx context.Context, file model.Obj, args model.Li return &link, nil } -func (d *LenovoNasShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { - return nil, errs.NotImplement -} - -func (d *LenovoNasShare) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - return nil, errs.NotImplement -} - -func (d *LenovoNasShare) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { - return nil, errs.NotImplement -} - -func (d *LenovoNasShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - return nil, errs.NotImplement -} - -func (d *LenovoNasShare) Remove(ctx context.Context, obj model.Obj) error { - return errs.NotImplement -} - -func (d *LenovoNasShare) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { - return nil, errs.NotImplement -} - var _ driver.Driver = (*LenovoNasShare)(nil) diff --git a/drivers/mega/driver.go b/drivers/mega/driver.go index f06b3fa98a..246896e696 100644 --- a/drivers/mega/driver.go +++ b/drivers/mega/driver.go @@ -12,7 +12,6 @@ import ( "github.com/rclone/rclone/lib/readers" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/pkg/utils" @@ -146,10 +145,6 @@ func (d *Mega) Rename(ctx context.Context, srcObj model.Obj, newName string) err return fmt.Errorf("unable to convert dir to mega n") } -func (d *Mega) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { - return errs.NotImplement -} - func (d *Mega) Remove(ctx context.Context, obj model.Obj) error { if node, ok := obj.(*MegaNode); ok { return d.c.Delete(node.n, !d.MoveToTrash) diff --git a/drivers/onedrive_sharelink/driver.go b/drivers/onedrive_sharelink/driver.go index a975772ac0..fa939bbef7 100644 --- a/drivers/onedrive_sharelink/driver.go +++ b/drivers/onedrive_sharelink/driver.go @@ -168,26 +168,6 @@ func (d *OnedriveSharelink) MakeDir(ctx context.Context, parentDir model.Obj, di } } -func (d *OnedriveSharelink) Move(ctx context.Context, srcObj, dstDir model.Obj) error { - // TODO move obj, optional - return errs.NotImplement -} - -func (d *OnedriveSharelink) Rename(ctx context.Context, srcObj model.Obj, newName string) error { - // TODO rename obj, optional - return errs.NotImplement -} - -func (d *OnedriveSharelink) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { - // TODO copy obj, optional - return errs.NotImplement -} - -func (d *OnedriveSharelink) Remove(ctx context.Context, obj model.Obj) error { - // TODO remove obj, optional - return errs.NotImplement -} - func (d *OnedriveSharelink) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { info, err := d.createUploadInfo(ctx, stdpath.Join(dstDir.GetPath(), stream.GetName()), stream.GetSize()) if err != nil { diff --git a/drivers/quark_uc_tv/driver.go b/drivers/quark_uc_tv/driver.go index dc062d9ffb..9f64a90634 100644 --- a/drivers/quark_uc_tv/driver.go +++ b/drivers/quark_uc_tv/driver.go @@ -12,7 +12,6 @@ import ( "github.com/go-resty/resty/v2" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" ) @@ -144,30 +143,6 @@ func (d *QuarkUCTV) Link(ctx context.Context, file model.Obj, args model.LinkArg return d.getDownloadLink(ctx, file) } -func (d *QuarkUCTV) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { - return nil, errs.NotImplement -} - -func (d *QuarkUCTV) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - return nil, errs.NotImplement -} - -func (d *QuarkUCTV) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { - return nil, errs.NotImplement -} - -func (d *QuarkUCTV) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - return nil, errs.NotImplement -} - -func (d *QuarkUCTV) Remove(ctx context.Context, obj model.Obj) error { - return errs.NotImplement -} - -func (d *QuarkUCTV) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { - return nil, errs.NotImplement -} - type QuarkUCTVCommon struct { AccessToken string } diff --git a/drivers/teldrive/driver.go b/drivers/teldrive/driver.go index 849d9170e2..eefae6967e 100644 --- a/drivers/teldrive/driver.go +++ b/drivers/teldrive/driver.go @@ -261,13 +261,6 @@ func (d *Teldrive) Extract(ctx context.Context, obj model.Obj, args model.Archiv return nil, errs.NotImplement } -func (d *Teldrive) 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 *Teldrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { // return nil, errs.NotSupport //} diff --git a/drivers/weiyun/driver.go b/drivers/weiyun/driver.go index 5178c95290..21d880e670 100644 --- a/drivers/weiyun/driver.go +++ b/drivers/weiyun/driver.go @@ -285,10 +285,6 @@ func (d *WeiYun) Rename(ctx context.Context, srcObj model.Obj, newName string) ( return nil, errs.NotSupport } -func (d *WeiYun) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { - return errs.NotImplement -} - func (d *WeiYun) Remove(ctx context.Context, obj model.Obj) error { switch obj := obj.(type) { case *File: From e248566be1b8d998c19eb214eb5e812cb8069916 Mon Sep 17 00:00:00 2001 From: nostalume Date: Thu, 24 Sep 2026 20:44:01 +0800 Subject: [PATCH 2/3] refactor(drivers): unify OpenList protocol family - Share the AList V3 and OpenList protocol mechanism while retaining product shells and dialects. - Replace duplicated implementation with focused family behavior and dialect tests. Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- drivers/alist_v3/driver.go | 362 +++------------------------ drivers/alist_v3/driver_test.go | 182 ++++++++++++-- drivers/alist_v3/types.go | 169 ++----------- drivers/alist_v3/util.go | 65 ----- drivers/internal/openlist/driver.go | 330 +++++++++++++++++++++++++ drivers/internal/openlist/state.go | 24 ++ drivers/internal/openlist/types.go | 158 ++++++++++++ drivers/internal/openlist/util.go | 97 ++++++++ drivers/openlist/driver.go | 365 ++++------------------------ drivers/openlist/types.go | 163 ++----------- drivers/openlist/util.go | 65 ----- 11 files changed, 879 insertions(+), 1101 deletions(-) delete mode 100644 drivers/alist_v3/util.go create mode 100644 drivers/internal/openlist/driver.go create mode 100644 drivers/internal/openlist/state.go create mode 100644 drivers/internal/openlist/types.go create mode 100644 drivers/internal/openlist/util.go delete mode 100644 drivers/openlist/util.go diff --git a/drivers/alist_v3/driver.go b/drivers/alist_v3/driver.go index ddfb0a9626..680587d6de 100644 --- a/drivers/alist_v3/driver.go +++ b/drivers/alist_v3/driver.go @@ -2,22 +2,14 @@ package alist_v3 import ( "context" - "fmt" - "io" "net/http" - "net/url" - "path" - "strings" - "github.com/OpenListTeam/OpenList/v4/drivers/base" - "github.com/OpenListTeam/OpenList/v4/internal/conf" + family "github.com/OpenListTeam/OpenList/v4/drivers/internal/openlist" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/server/common" "github.com/go-resty/resty/v2" - log "github.com/sirupsen/logrus" ) type AListV3 struct { @@ -25,356 +17,82 @@ type AListV3 struct { Addition } -func (d *AListV3) Config() driver.Config { - return config -} +func (d *AListV3) Config() driver.Config { return config } -func (d *AListV3) GetAddition() driver.Additional { - return &d.Addition -} +func (d *AListV3) GetAddition() driver.Additional { return &d.Addition } -func (d *AListV3) Init(ctx context.Context) error { - d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/") - var resp common.Resp[MeResp] - _, _, err := d.request("/me", http.MethodGet, func(req *resty.Request) { - req.SetResult(&resp) - }) - if err != nil { - return err - } - // if the username is not empty and the username is not the same as the current username, then login again - if d.Username != resp.Data.Username { - err = d.login() - if err != nil { - return err - } - } - // re-get the user info - _, _, err = d.request("/me", http.MethodGet, func(req *resty.Request) { - req.SetResult(&resp) - }) - if err != nil { - return err - } - if utils.SliceContains(resp.Data.Role, model.GUEST) { - u := d.Address + "/api/public/settings" - res, err := base.RestyClient.R().Get(u) - if err != nil { - return err - } - allowMounted := utils.Json.Get(res.Body(), "data", conf.AllowMounted).ToString() == "true" - if !allowMounted { - return fmt.Errorf("the site does not allow mounted") - } +func (d *AListV3) state() family.State { + return family.State{ + Driver: d, Address: &d.Address, MetaPassword: &d.MetaPassword, + Username: &d.Username, Password: &d.Password, Token: &d.Token, + PassIP: d.PassIPToUpsteam, PassUserAgent: d.PassUAToUpsteam, + ForwardArchives: d.ForwardArchiveReq, } - return err } -func (d *AListV3) Drop(ctx context.Context) error { - return nil +func (d *AListV3) Init(context.Context) error { + return family.Init(d.state(), func(s family.State) (family.Identity, error) { + var resp common.Resp[MeResp] + _, _, err := s.Request("/me", http.MethodGet, func(req *resty.Request) { req.SetResult(&resp) }) + return family.Identity{ + Username: resp.Data.Username, + Guest: utils.SliceContains(resp.Data.Role, model.GUEST), + }, err + }) } +func (d *AListV3) Drop(context.Context) error { return nil } + func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { - var resp common.Resp[FsListResp] - _, _, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(ListReq{ - PageReq: model.PageReq{ - Page: 1, - PerPage: 0, - }, - Path: dir.GetPath(), - Password: d.MetaPassword, - Refresh: false, - }) - }) - if err != nil { - return nil, err - } - var files []model.Obj - for _, f := range resp.Data.Content { - file := model.ObjThumb{ - Object: model.Object{ - Name: f.Name, - Path: path.Join(dir.GetPath(), f.Name), - Modified: f.Modified, - Ctime: f.Created, - Size: f.Size, - IsFolder: f.IsDir, - HashInfo: utils.FromString(f.HashInfo), - }, - Thumbnail: model.Thumbnail{Thumbnail: f.Thumb}, - } - files = append(files, &file) - } - return files, nil + return family.List(d.state(), ctx, dir, args) } func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { - var resp common.Resp[FsGetResp] - headers := map[string]string{ - "User-Agent": base.UserAgent, - } - // if PassUAToUpsteam is true, then pass the user-agent to the upstream - if d.PassUAToUpsteam { - userAgent := args.Header.Get("user-agent") - if userAgent != "" { - headers["User-Agent"] = userAgent - } - } - // if PassIPToUpsteam is true, then pass the ip address to the upstream - if d.PassIPToUpsteam { - ip := args.IP - if ip != "" { - headers["X-Forwarded-For"] = ip - headers["X-Real-Ip"] = ip - } - } - _, _, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(FsGetReq{ - Path: file.GetPath(), - Password: d.MetaPassword, - }).SetHeaders(headers) - }) - if err != nil { - return nil, err - } - return &model.Link{ - URL: resp.Data.RawURL, - }, nil + return family.Link(d.state(), ctx, file, args) } -func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { - _, _, err := d.request("/fs/mkdir", http.MethodPost, func(req *resty.Request) { - req.SetBody(MkdirOrLinkReq{ - Path: path.Join(parentDir.GetPath(), dirName), - }) - }) - return err +func (d *AListV3) MakeDir(ctx context.Context, parent model.Obj, name string) error { + return family.MakeDir(d.state(), ctx, parent, name) } -func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error { - _, _, err := d.request("/fs/move", http.MethodPost, func(req *resty.Request) { - req.SetBody(MoveCopyReq{ - SrcDir: path.Dir(srcObj.GetPath()), - DstDir: dstDir.GetPath(), - Names: []string{srcObj.GetName()}, - }) - }) - return err +func (d *AListV3) Move(ctx context.Context, src, dst model.Obj) error { + return family.Move(d.state(), ctx, src, dst) } -func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error { - _, _, err := d.request("/fs/rename", http.MethodPost, func(req *resty.Request) { - req.SetBody(RenameReq{ - Path: srcObj.GetPath(), - Name: newName, - }) - }) - return err +func (d *AListV3) Rename(ctx context.Context, src model.Obj, name string) error { + return family.Rename(d.state(), ctx, src, name) } -func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { - _, _, err := d.request("/fs/copy", http.MethodPost, func(req *resty.Request) { - req.SetBody(MoveCopyReq{ - SrcDir: path.Dir(srcObj.GetPath()), - DstDir: dstDir.GetPath(), - Names: []string{srcObj.GetName()}, - }) - }) - return err +func (d *AListV3) Copy(ctx context.Context, src, dst model.Obj) error { + return family.Copy(d.state(), ctx, src, dst) } func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error { - _, _, err := d.request("/fs/remove", http.MethodPost, func(req *resty.Request) { - req.SetBody(RemoveReq{ - Dir: path.Dir(obj.GetPath()), - Names: []string{obj.GetName()}, - }) - }) - return err + return family.Remove(d.state(), ctx, obj) } -func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error { - reader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{ - Reader: s, - UpdateProgress: up, - }) - req, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+"/api/fs/put", reader) - if err != nil { - return err - } - req.Header.Set("Authorization", d.Token) - req.Header.Set("File-Path", path.Join(dstDir.GetPath(), s.GetName())) - req.Header.Set("Password", d.MetaPassword) - if md5 := s.GetHash().GetHash(utils.MD5); len(md5) > 0 { - req.Header.Set("X-File-Md5", md5) - } - if sha1 := s.GetHash().GetHash(utils.SHA1); len(sha1) > 0 { - req.Header.Set("X-File-Sha1", sha1) - } - if sha256 := s.GetHash().GetHash(utils.SHA256); len(sha256) > 0 { - req.Header.Set("X-File-Sha256", sha256) - } - - req.ContentLength = s.GetSize() - // client := base.NewHttpClient() - // client.Timeout = time.Hour * 6 - res, err := base.HttpClient.Do(req) - if err != nil { - return err - } - - bytes, err := io.ReadAll(res.Body) - if err != nil { - return err - } - log.Debugf("[openlist] response body: %s", string(bytes)) - if res.StatusCode >= 400 { - return fmt.Errorf("request failed, status: %s", res.Status) - } - code := utils.Json.Get(bytes, "code").ToInt() - if code != 200 { - if code == 401 || code == 403 { - err = d.login() - if err != nil { - return err - } - } - return fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(bytes, "message").ToString()) - } - return nil +func (d *AListV3) Put(ctx context.Context, dst model.Obj, src model.FileStreamer, up driver.UpdateProgress) error { + return family.Put(d.state(), ctx, dst, src, up) } func (d *AListV3) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) { - if !d.ForwardArchiveReq { - return nil, errs.NotImplement - } - var resp common.Resp[ArchiveMetaResp] - _, code, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(ArchiveMetaReq{ - ArchivePass: args.Password, - Password: d.MetaPassword, - Path: obj.GetPath(), - Refresh: false, - }) - }) - if code == 202 { - return nil, errs.WrongArchivePassword - } - if err != nil { - return nil, err - } - var tree []model.ObjTree - if resp.Data.Content != nil { - tree = make([]model.ObjTree, 0, len(resp.Data.Content)) - for _, content := range resp.Data.Content { - tree = append(tree, &content) - } - } - return &model.ArchiveMetaInfo{ - Comment: resp.Data.Comment, - Encrypted: resp.Data.Encrypted, - Tree: tree, - }, nil + return family.GetArchiveMeta(d.state(), ctx, obj, args) } func (d *AListV3) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) { - if !d.ForwardArchiveReq { - return nil, errs.NotImplement - } - var resp common.Resp[ArchiveListResp] - _, code, err := d.request("/fs/archive/list", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(ArchiveListReq{ - ArchiveMetaReq: ArchiveMetaReq{ - ArchivePass: args.Password, - Password: d.MetaPassword, - Path: obj.GetPath(), - Refresh: false, - }, - PageReq: model.PageReq{ - Page: 1, - PerPage: 0, - }, - InnerPath: args.InnerPath, - }) - }) - if code == 202 { - return nil, errs.WrongArchivePassword - } - if err != nil { - return nil, err - } - var files []model.Obj - for _, f := range resp.Data.Content { - file := model.ObjThumb{ - Object: model.Object{ - Name: f.Name, - Modified: f.Modified, - Ctime: f.Created, - Size: f.Size, - IsFolder: f.IsDir, - HashInfo: utils.FromString(f.HashInfo), - }, - Thumbnail: model.Thumbnail{Thumbnail: f.Thumb}, - } - files = append(files, &file) - } - return files, nil + return family.ListArchive(d.state(), ctx, obj, args) } func (d *AListV3) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) { - if !d.ForwardArchiveReq { - return nil, errs.NotSupport - } - var resp common.Resp[ArchiveMetaResp] - _, _, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(ArchiveMetaReq{ - ArchivePass: args.Password, - Password: d.MetaPassword, - Path: obj.GetPath(), - Refresh: false, - }) - }) - if err != nil { - return nil, err - } - return &model.Link{ - URL: fmt.Sprintf("%s?inner=%s&pass=%s&sign=%s", - resp.Data.RawURL, - utils.EncodePath(args.InnerPath, true), - url.QueryEscape(args.Password), - resp.Data.Sign), - }, nil + return family.Extract(d.state(), ctx, obj, args) } -func (d *AListV3) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error { - if !d.ForwardArchiveReq { - return errs.NotImplement - } - dir, name := path.Split(srcObj.GetPath()) - _, _, err := d.request("/fs/archive/decompress", http.MethodPost, func(req *resty.Request) { - req.SetBody(DecompressReq{ - ArchivePass: args.Password, - CacheFull: args.CacheFull, - DstDir: dstDir.GetPath(), - InnerPath: args.InnerPath, - Name: []string{name}, - PutIntoNewDir: args.PutIntoNewDir, - SrcDir: dir, - }) - }) - return err +func (d *AListV3) ArchiveDecompress(ctx context.Context, src, dst model.Obj, args model.ArchiveDecompressArgs) error { + return family.ArchiveDecompress(d.state(), ctx, src, dst, args) } -func (d *AListV3) ResolveLinkCacheMode(_ string) driver.LinkCacheMode { - var mode driver.LinkCacheMode - if d.PassIPToUpsteam { - mode |= driver.LinkCacheIP - } - if d.PassUAToUpsteam { - mode |= driver.LinkCacheUA - } - return mode +func (d *AListV3) ResolveLinkCacheMode(path string) driver.LinkCacheMode { + return family.ResolveLinkCacheMode(d.state(), path) } var _ driver.Driver = (*AListV3)(nil) diff --git a/drivers/alist_v3/driver_test.go b/drivers/alist_v3/driver_test.go index bb8e009eab..cbc5a4066d 100644 --- a/drivers/alist_v3/driver_test.go +++ b/drivers/alist_v3/driver_test.go @@ -9,16 +9,27 @@ import ( "time" "github.com/OpenListTeam/OpenList/v4/drivers/base" + openlistdriver "github.com/OpenListTeam/OpenList/v4/drivers/openlist" "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/go-resty/resty/v2" ) -// TestListSetsChildPaths descends two levels the way op.Get does, feeding an -// object from one listing back into List as dir. Without a Path on that object -// the driver asks upstream for "", which a real server answers with its own -// root -- hence the fake upstream's fallback, and the endless self-similar tree. -func TestListSetsChildPaths(t *testing.T) { +type familyDriver interface { + List(context.Context, model.Obj, model.ListArgs) ([]model.Obj, error) + ArchiveDecompress(context.Context, model.Obj, model.Obj, model.ArchiveDecompressArgs) error +} + +func useTestClient(t *testing.T) { + t.Helper() + prev := base.RestyClient + base.RestyClient = resty.New().SetTimeout(5 * time.Second) + t.Cleanup(func() { base.RestyClient = prev }) +} + +// This descends two levels the way op.Get does, feeding an object from one +// listing back into List. It is one behavioral contract for both products. +func TestFamilyListSetsChildPaths(t *testing.T) { tree := map[string][]ObjResp{ "/": {{Name: "root-marker", IsDir: true}}, "/drive": {{Name: "concerts", IsDir: true}}, @@ -31,35 +42,152 @@ func TestListSetsChildPaths(t *testing.T) { if !ok { content = tree["/"] } - w.Header().Set("Content-Type", "application/json") // resty only unmarshals JSON + w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "code": 200, "message": "success", "data": map[string]any{"content": content, "total": len(content)}, }) })) t.Cleanup(srv.Close) + useTestClient(t) - // conf.Conf is nil outside a booted server, so base.InitClient() is unusable. - prev := base.RestyClient - base.RestyClient = resty.New().SetTimeout(5 * time.Second) - t.Cleanup(func() { base.RestyClient = prev }) + drivers := map[string]familyDriver{ + "alist-v3": &AListV3{Addition: Addition{ + RootPath: driver.RootPath{RootFolderPath: "/drive"}, + Address: srv.URL, + }}, + "openlist": &openlistdriver.OpenList{Addition: openlistdriver.Addition{ + RootPath: driver.RootPath{RootFolderPath: "/drive"}, + Address: srv.URL, + }}, + } + for name, d := range drivers { + t.Run(name, func(t *testing.T) { + dir := model.Obj(&model.Object{Path: "/drive", IsFolder: true}) + for _, want := range []string{"/drive/concerts", "/drive/concerts/show.mkv"} { + objs, err := d.List(context.Background(), dir, model.ListArgs{}) + if err != nil { + t.Fatalf("List(%q): %v", dir.GetPath(), err) + } + if len(objs) != 1 { + t.Fatalf("List(%q) returned %d objects, want 1", dir.GetPath(), len(objs)) + } + if got := objs[0].GetPath(); got != want { + t.Fatalf("child of %q has path %q, want %q", dir.GetPath(), got, want) + } + dir = objs[0] + } + }) + } +} - d := &AListV3{Addition: Addition{ - RootPath: driver.RootPath{RootFolderPath: "/drive"}, - Address: srv.URL, - }} - dir := model.Obj(&model.Object{Path: "/drive", IsFolder: true}) - for _, want := range []string{"/drive/concerts", "/drive/concerts/show.mkv"} { - objs, err := d.List(context.Background(), dir, model.ListArgs{}) - if err != nil { - t.Fatalf("List(%q): %v", dir.GetPath(), err) - } - if len(objs) != 1 { - t.Fatalf("List(%q) returned %d objects, want 1", dir.GetPath(), len(objs)) - } - if got := objs[0].GetPath(); got != want { - t.Fatalf("child of %q has path %q, want %q", dir.GetPath(), got, want) - } - dir = objs[0] +func TestFamilyDialectRequests(t *testing.T) { + tests := []struct { + name string + newDriver func(string) familyDriver + wantRefresh bool + wantOverwrite bool + wantOverwriteKey bool + }{ + { + name: "alist-v3 keeps legacy request shape", + newDriver: func(address string) familyDriver { + return &AListV3{Addition: Addition{ + Address: address, ForwardArchiveReq: true, + }} + }, + }, + { + name: "openlist suppresses refresh unless enabled", + newDriver: func(address string) familyDriver { + return &openlistdriver.OpenList{Addition: openlistdriver.Addition{ + Address: address, ForwardArchiveReq: true, + }} + }, + wantOverwrite: true, wantOverwriteKey: true, + }, + { + name: "openlist forwards enabled flags", + newDriver: func(address string) familyDriver { + return &openlistdriver.OpenList{Addition: openlistdriver.Addition{ + Address: address, ForwardArchiveReq: true, PassRefreshFlagToUpsteam: true, + }} + }, + wantRefresh: true, wantOverwrite: true, wantOverwriteKey: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var listBody, decompressBody map[string]json.RawMessage + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/fs/list": + _ = json.NewDecoder(r.Body).Decode(&listBody) + case "/api/fs/archive/decompress": + _ = json.NewDecoder(r.Body).Decode(&decompressBody) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"code":200,"message":"success","data":{"content":[]}}`)) + })) + t.Cleanup(srv.Close) + useTestClient(t) + + d := tt.newDriver(srv.URL) + dir := &model.Object{Path: "/dst", IsFolder: true} + if _, err := d.List(context.Background(), dir, model.ListArgs{Refresh: true}); err != nil { + t.Fatal(err) + } + if err := d.ArchiveDecompress( + context.Background(), + &model.Object{Path: "/src/a.zip"}, + dir, + model.ArchiveDecompressArgs{Overwrite: true}, + ); err != nil { + t.Fatal(err) + } + + var gotRefresh bool + if err := json.Unmarshal(listBody["refresh"], &gotRefresh); err != nil { + t.Fatal(err) + } + if gotRefresh != tt.wantRefresh { + t.Fatalf("refresh = %v, want %v", gotRefresh, tt.wantRefresh) + } + raw, hasOverwrite := decompressBody["overwrite"] + if hasOverwrite != tt.wantOverwriteKey { + t.Fatalf("overwrite presence = %v, want %v", hasOverwrite, tt.wantOverwriteKey) + } + if hasOverwrite { + var gotOverwrite bool + if err := json.Unmarshal(raw, &gotOverwrite); err != nil { + t.Fatal(err) + } + if gotOverwrite != tt.wantOverwrite { + t.Fatalf("overwrite = %v, want %v", gotOverwrite, tt.wantOverwrite) + } + } + }) + } +} + +func TestRoleDialectDecoding(t *testing.T) { + var legacy MeResp + if err := json.Unmarshal([]byte(`{"role":[2]}`), &legacy); err != nil { + t.Fatalf("AList V3 array role: %v", err) + } + if len(legacy.Role) != 1 || legacy.Role[0] != 2 { + t.Fatalf("AList V3 role = %v, want [2]", legacy.Role) + } + if err := json.Unmarshal([]byte(`{"role":2}`), &legacy); err != nil { + t.Fatalf("AList V3 scalar role: %v", err) + } + + var current openlistdriver.MeResp + if err := json.Unmarshal([]byte(`{"role":2}`), ¤t); err != nil { + t.Fatalf("OpenList scalar role: %v", err) + } + if err := json.Unmarshal([]byte(`{"role":[2]}`), ¤t); err == nil { + t.Fatal("OpenList unexpectedly accepted an array role") } } diff --git a/drivers/alist_v3/types.go b/drivers/alist_v3/types.go index 527de77993..dc93752190 100644 --- a/drivers/alist_v3/types.go +++ b/drivers/alist_v3/types.go @@ -2,75 +2,26 @@ package alist_v3 import ( "encoding/json" - "time" - "github.com/OpenListTeam/OpenList/v4/internal/model" - "github.com/OpenListTeam/OpenList/v4/pkg/utils" + family "github.com/OpenListTeam/OpenList/v4/drivers/internal/openlist" ) -type ListReq struct { - model.PageReq - Path string `json:"path" form:"path"` - Password string `json:"password" form:"password"` - Refresh bool `json:"refresh"` -} - -type ObjResp struct { - Name string `json:"name"` - Size int64 `json:"size"` - IsDir bool `json:"is_dir"` - Modified time.Time `json:"modified"` - Created time.Time `json:"created"` - Sign string `json:"sign"` - Thumb string `json:"thumb"` - Type int `json:"type"` - HashInfo string `json:"hashinfo"` -} - -type FsListResp struct { - Content []ObjResp `json:"content"` - Total int64 `json:"total"` - Readme string `json:"readme"` - Write bool `json:"write"` - Provider string `json:"provider"` -} - -type FsGetReq struct { - Path string `json:"path" form:"path"` - Password string `json:"password" form:"password"` -} - -type FsGetResp struct { - ObjResp - RawURL string `json:"raw_url"` - Readme string `json:"readme"` - Provider string `json:"provider"` - Related []ObjResp `json:"related"` -} - -type MkdirOrLinkReq struct { - Path string `json:"path" form:"path"` -} - -type MoveCopyReq struct { - SrcDir string `json:"src_dir"` - DstDir string `json:"dst_dir"` - Names []string `json:"names"` -} - -type RenameReq struct { - Path string `json:"path"` - Name string `json:"name"` -} - -type RemoveReq struct { - Dir string `json:"dir"` - Names []string `json:"names"` -} - -type LoginResp struct { - Token string `json:"token"` -} +type ListReq = family.ListReq +type ObjResp = family.ObjResp +type FsListResp = family.FsListResp +type FsGetReq = family.FsGetReq +type FsGetResp = family.FsGetResp +type MkdirOrLinkReq = family.MkdirOrLinkReq +type MoveCopyReq = family.MoveCopyReq +type RenameReq = family.RenameReq +type RemoveReq = family.RemoveReq +type LoginResp = family.LoginResp +type ArchiveMetaReq = family.ArchiveMetaReq +type TreeResp = family.TreeResp +type ArchiveMetaResp = family.ArchiveMetaResp +type ArchiveListReq = family.ArchiveListReq +type ArchiveListResp = family.ArchiveListResp +type DecompressReq = family.DecompressReq type MeResp struct { Id int `json:"id"` @@ -94,89 +45,3 @@ func (s *IntSlice) UnmarshalJSON(b []byte) error { } return json.Unmarshal(b, (*[]int)(s)) } - -type ArchiveMetaReq struct { - ArchivePass string `json:"archive_pass"` - Password string `json:"password"` - Path string `json:"path"` - Refresh bool `json:"refresh"` -} - -type TreeResp struct { - ObjResp - Children []TreeResp `json:"children"` - hashCache *utils.HashInfo -} - -func (t *TreeResp) GetSize() int64 { - return t.Size -} - -func (t *TreeResp) GetName() string { - return t.Name -} - -func (t *TreeResp) ModTime() time.Time { - return t.Modified -} - -func (t *TreeResp) CreateTime() time.Time { - return t.Created -} - -func (t *TreeResp) IsDir() bool { - return t.ObjResp.IsDir -} - -func (t *TreeResp) GetHash() utils.HashInfo { - return utils.FromString(t.HashInfo) -} - -func (t *TreeResp) GetID() string { - return "" -} - -func (t *TreeResp) GetPath() string { - return "" -} - -func (t *TreeResp) GetChildren() []model.ObjTree { - ret := make([]model.ObjTree, 0, len(t.Children)) - for _, child := range t.Children { - ret = append(ret, &child) - } - return ret -} - -func (t *TreeResp) Thumb() string { - return t.ObjResp.Thumb -} - -type ArchiveMetaResp struct { - Comment string `json:"comment"` - Encrypted bool `json:"encrypted"` - Content []TreeResp `json:"content"` - RawURL string `json:"raw_url"` - Sign string `json:"sign"` -} - -type ArchiveListReq struct { - model.PageReq - ArchiveMetaReq - InnerPath string `json:"inner_path"` -} - -type ArchiveListResp struct { - Content []ObjResp `json:"content"` - Total int64 `json:"total"` -} - -type DecompressReq struct { - ArchivePass string `json:"archive_pass"` - CacheFull bool `json:"cache_full"` - DstDir string `json:"dst_dir"` - InnerPath string `json:"inner_path"` - Name []string `json:"name"` - PutIntoNewDir bool `json:"put_into_new_dir"` - SrcDir string `json:"src_dir"` -} diff --git a/drivers/alist_v3/util.go b/drivers/alist_v3/util.go deleted file mode 100644 index 821fe5465c..0000000000 --- a/drivers/alist_v3/util.go +++ /dev/null @@ -1,65 +0,0 @@ -package alist_v3 - -import ( - "fmt" - "net/http" - - "github.com/OpenListTeam/OpenList/v4/drivers/base" - "github.com/OpenListTeam/OpenList/v4/internal/op" - "github.com/OpenListTeam/OpenList/v4/pkg/utils" - "github.com/OpenListTeam/OpenList/v4/server/common" - "github.com/go-resty/resty/v2" - log "github.com/sirupsen/logrus" -) - -func (d *AListV3) login() error { - if d.Username == "" { - return nil - } - var resp common.Resp[LoginResp] - _, _, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(base.Json{ - "username": d.Username, - "password": d.Password, - }) - }) - if err != nil { - return err - } - d.Token = resp.Data.Token - op.MustSaveDriverStorage(d) - return nil -} - -func (d *AListV3) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, int, error) { - url := d.Address + "/api" + api - req := base.RestyClient.R() - req.SetHeader("Authorization", d.Token) - if callback != nil { - callback(req) - } - res, err := req.Execute(method, url) - if err != nil { - code := 0 - if res != nil { - code = res.StatusCode() - } - return nil, code, err - } - log.Debugf("[openlist] response body: %s", res.String()) - if res.StatusCode() >= 400 { - return nil, res.StatusCode(), fmt.Errorf("request failed, status: %s", res.Status()) - } - code := utils.Json.Get(res.Body(), "code").ToInt() - if code != 200 { - if (code == 401 || code == 403) && !utils.IsBool(retry...) { - err = d.login() - if err != nil { - return nil, code, err - } - return d.request(api, method, callback, true) - } - return nil, code, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString()) - } - return res.Body(), 200, nil -} diff --git a/drivers/internal/openlist/driver.go b/drivers/internal/openlist/driver.go new file mode 100644 index 0000000000..bb1a02d085 --- /dev/null +++ b/drivers/internal/openlist/driver.go @@ -0,0 +1,330 @@ +package openlist + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "path" + + "github.com/OpenListTeam/OpenList/v4/drivers/base" + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/OpenListTeam/OpenList/v4/server/common" + "github.com/go-resty/resty/v2" + log "github.com/sirupsen/logrus" +) + +func List(d State, ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + var resp common.Resp[FsListResp] + _, _, err := d.Request("/fs/list", http.MethodPost, func(req *resty.Request) { + req.SetResult(&resp).SetBody(ListReq{ + PageReq: model.PageReq{ + Page: 1, + PerPage: 0, + }, + Path: dir.GetPath(), + Password: *d.MetaPassword, + Refresh: d.ForwardRefresh && args.Refresh, + }) + }) + if err != nil { + return nil, err + } + var files []model.Obj + for _, f := range resp.Data.Content { + file := model.ObjThumb{ + Object: model.Object{ + Name: f.Name, + Path: path.Join(dir.GetPath(), f.Name), + Modified: f.Modified, + Ctime: f.Created, + Size: f.Size, + IsFolder: f.IsDir, + HashInfo: utils.FromString(f.HashInfo), + }, + Thumbnail: model.Thumbnail{Thumbnail: f.Thumb}, + } + files = append(files, &file) + } + return files, nil +} + +func Link(d State, ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + var resp common.Resp[FsGetResp] + headers := map[string]string{ + "User-Agent": base.UserAgent, + } + // if PassUAToUpsteam is true, then pass the user-agent to the upstream + if d.PassUserAgent { + userAgent := args.Header.Get("user-agent") + if userAgent != "" { + headers["User-Agent"] = userAgent + } + } + // if PassIPToUpsteam is true, then pass the ip address to the upstream + if d.PassIP { + ip := args.IP + if ip != "" { + headers["X-Forwarded-For"] = ip + headers["X-Real-Ip"] = ip + } + } + _, _, err := d.Request("/fs/get", http.MethodPost, func(req *resty.Request) { + req.SetResult(&resp).SetBody(FsGetReq{ + Path: file.GetPath(), + Password: *d.MetaPassword, + }).SetHeaders(headers) + }) + if err != nil { + return nil, err + } + return &model.Link{ + URL: resp.Data.RawURL, + }, nil +} + +func MakeDir(d State, ctx context.Context, parentDir model.Obj, dirName string) error { + _, _, err := d.Request("/fs/mkdir", http.MethodPost, func(req *resty.Request) { + req.SetBody(MkdirOrLinkReq{ + Path: path.Join(parentDir.GetPath(), dirName), + }) + }) + return err +} + +func Move(d State, ctx context.Context, srcObj, dstDir model.Obj) error { + _, _, err := d.Request("/fs/move", http.MethodPost, func(req *resty.Request) { + req.SetBody(MoveCopyReq{ + SrcDir: path.Dir(srcObj.GetPath()), + DstDir: dstDir.GetPath(), + Names: []string{srcObj.GetName()}, + }) + }) + return err +} + +func Rename(d State, ctx context.Context, srcObj model.Obj, newName string) error { + _, _, err := d.Request("/fs/rename", http.MethodPost, func(req *resty.Request) { + req.SetBody(RenameReq{ + Path: srcObj.GetPath(), + Name: newName, + }) + }) + return err +} + +func Copy(d State, ctx context.Context, srcObj, dstDir model.Obj) error { + _, _, err := d.Request("/fs/copy", http.MethodPost, func(req *resty.Request) { + req.SetBody(MoveCopyReq{ + SrcDir: path.Dir(srcObj.GetPath()), + DstDir: dstDir.GetPath(), + Names: []string{srcObj.GetName()}, + }) + }) + return err +} + +func Remove(d State, ctx context.Context, obj model.Obj) error { + _, _, err := d.Request("/fs/remove", http.MethodPost, func(req *resty.Request) { + req.SetBody(RemoveReq{ + Dir: path.Dir(obj.GetPath()), + Names: []string{obj.GetName()}, + }) + }) + return err +} + +func Put(d State, ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error { + reader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{ + Reader: s, + UpdateProgress: up, + }) + req, err := http.NewRequestWithContext(ctx, http.MethodPut, *d.Address+"/api/fs/put", reader) + if err != nil { + return err + } + req.Header.Set("Authorization", *d.Token) + req.Header.Set("File-Path", path.Join(dstDir.GetPath(), s.GetName())) + req.Header.Set("Password", *d.MetaPassword) + if md5 := s.GetHash().GetHash(utils.MD5); len(md5) > 0 { + req.Header.Set("X-File-Md5", md5) + } + if sha1 := s.GetHash().GetHash(utils.SHA1); len(sha1) > 0 { + req.Header.Set("X-File-Sha1", sha1) + } + if sha256 := s.GetHash().GetHash(utils.SHA256); len(sha256) > 0 { + req.Header.Set("X-File-Sha256", sha256) + } + + req.ContentLength = s.GetSize() + // client := base.NewHttpClient() + // client.Timeout = time.Hour * 6 + res, err := base.HttpClient.Do(req) + if err != nil { + return err + } + + bytes, err := io.ReadAll(res.Body) + if err != nil { + return err + } + log.Debugf("[openlist] response body: %s", string(bytes)) + if res.StatusCode >= 400 { + return fmt.Errorf("request failed, status: %s", res.Status) + } + code := utils.Json.Get(bytes, "code").ToInt() + if code != 200 { + if code == 401 || code == 403 { + err = d.Login() + if err != nil { + return err + } + } + return fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(bytes, "message").ToString()) + } + return nil +} + +func GetArchiveMeta(d State, ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) { + if !d.ForwardArchives { + return nil, errs.NotImplement + } + var resp common.Resp[ArchiveMetaResp] + _, code, err := d.Request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) { + req.SetResult(&resp).SetBody(ArchiveMetaReq{ + ArchivePass: args.Password, + Password: *d.MetaPassword, + Path: obj.GetPath(), + Refresh: false, + }) + }) + if code == 202 { + return nil, errs.WrongArchivePassword + } + if err != nil { + return nil, err + } + var tree []model.ObjTree + if resp.Data.Content != nil { + tree = make([]model.ObjTree, 0, len(resp.Data.Content)) + for _, content := range resp.Data.Content { + tree = append(tree, &content) + } + } + return &model.ArchiveMetaInfo{ + Comment: resp.Data.Comment, + Encrypted: resp.Data.Encrypted, + Tree: tree, + }, nil +} + +func ListArchive(d State, ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) { + if !d.ForwardArchives { + return nil, errs.NotImplement + } + var resp common.Resp[ArchiveListResp] + _, code, err := d.Request("/fs/archive/list", http.MethodPost, func(req *resty.Request) { + req.SetResult(&resp).SetBody(ArchiveListReq{ + ArchiveMetaReq: ArchiveMetaReq{ + ArchivePass: args.Password, + Password: *d.MetaPassword, + Path: obj.GetPath(), + Refresh: false, + }, + PageReq: model.PageReq{ + Page: 1, + PerPage: 0, + }, + InnerPath: args.InnerPath, + }) + }) + if code == 202 { + return nil, errs.WrongArchivePassword + } + if err != nil { + return nil, err + } + var files []model.Obj + for _, f := range resp.Data.Content { + file := model.ObjThumb{ + Object: model.Object{ + Name: f.Name, + Modified: f.Modified, + Ctime: f.Created, + Size: f.Size, + IsFolder: f.IsDir, + HashInfo: utils.FromString(f.HashInfo), + }, + Thumbnail: model.Thumbnail{Thumbnail: f.Thumb}, + } + files = append(files, &file) + } + return files, nil +} + +func Extract(d State, ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) { + if !d.ForwardArchives { + return nil, errs.NotSupport + } + var resp common.Resp[ArchiveMetaResp] + _, _, err := d.Request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) { + req.SetResult(&resp).SetBody(ArchiveMetaReq{ + ArchivePass: args.Password, + Password: *d.MetaPassword, + Path: obj.GetPath(), + Refresh: false, + }) + }) + if err != nil { + return nil, err + } + return &model.Link{ + URL: fmt.Sprintf("%s?inner=%s&pass=%s&sign=%s", + resp.Data.RawURL, + utils.EncodePath(args.InnerPath, true), + url.QueryEscape(args.Password), + resp.Data.Sign), + }, nil +} + +func ArchiveDecompress(d State, ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error { + if !d.ForwardArchives { + return errs.NotImplement + } + dir, name := path.Split(srcObj.GetPath()) + _, _, err := d.Request("/fs/archive/decompress", http.MethodPost, func(req *resty.Request) { + body := DecompressReq{ + ArchivePass: args.Password, + CacheFull: args.CacheFull, + DstDir: dstDir.GetPath(), + InnerPath: args.InnerPath, + Name: []string{name}, + PutIntoNewDir: args.PutIntoNewDir, + SrcDir: dir, + } + if d.SendOverwrite { + req.SetBody(struct { + DecompressReq + Overwrite bool `json:"overwrite"` + }{body, args.Overwrite}) + return + } + req.SetBody(body) + }) + return err +} + +func ResolveLinkCacheMode(d State, _ string) driver.LinkCacheMode { + var mode driver.LinkCacheMode + if d.PassIP { + mode |= driver.LinkCacheIP + } + if d.PassUserAgent { + mode |= driver.LinkCacheUA + } + return mode +} diff --git a/drivers/internal/openlist/state.go b/drivers/internal/openlist/state.go new file mode 100644 index 0000000000..301407f4b7 --- /dev/null +++ b/drivers/internal/openlist/state.go @@ -0,0 +1,24 @@ +package openlist + +import ( + "github.com/OpenListTeam/OpenList/v4/internal/driver" +) + +// State is the product-specific configuration used by the shared AList/OpenList +// protocol implementation. Pointers keep login and address normalization updates +// on the registered driver value rather than on a copied adapter. +type State struct { + Driver driver.Driver + + Address *string + MetaPassword *string + Username *string + Password *string + Token *string + + PassIP bool + PassUserAgent bool + ForwardArchives bool + ForwardRefresh bool + SendOverwrite bool +} diff --git a/drivers/internal/openlist/types.go b/drivers/internal/openlist/types.go new file mode 100644 index 0000000000..f5c6a2ff53 --- /dev/null +++ b/drivers/internal/openlist/types.go @@ -0,0 +1,158 @@ +package openlist + +import ( + "time" + + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" +) + +type ListReq struct { + model.PageReq + Path string `json:"path" form:"path"` + Password string `json:"password" form:"password"` + Refresh bool `json:"refresh"` +} + +type ObjResp struct { + Name string `json:"name"` + Size int64 `json:"size"` + IsDir bool `json:"is_dir"` + Modified time.Time `json:"modified"` + Created time.Time `json:"created"` + Sign string `json:"sign"` + Thumb string `json:"thumb"` + Type int `json:"type"` + HashInfo string `json:"hashinfo"` +} + +type FsListResp struct { + Content []ObjResp `json:"content"` + Total int64 `json:"total"` + Readme string `json:"readme"` + Write bool `json:"write"` + Provider string `json:"provider"` +} + +type FsGetReq struct { + Path string `json:"path" form:"path"` + Password string `json:"password" form:"password"` +} + +type FsGetResp struct { + ObjResp + RawURL string `json:"raw_url"` + Readme string `json:"readme"` + Provider string `json:"provider"` + Related []ObjResp `json:"related"` +} + +type MkdirOrLinkReq struct { + Path string `json:"path" form:"path"` +} + +type MoveCopyReq struct { + SrcDir string `json:"src_dir"` + DstDir string `json:"dst_dir"` + Names []string `json:"names"` +} + +type RenameReq struct { + Path string `json:"path"` + Name string `json:"name"` +} + +type RemoveReq struct { + Dir string `json:"dir"` + Names []string `json:"names"` +} + +type LoginResp struct { + Token string `json:"token"` +} + +type ArchiveMetaReq struct { + ArchivePass string `json:"archive_pass"` + Password string `json:"password"` + Path string `json:"path"` + Refresh bool `json:"refresh"` +} + +type TreeResp struct { + ObjResp + Children []TreeResp `json:"children"` + hashCache *utils.HashInfo +} + +func (t *TreeResp) GetSize() int64 { + return t.Size +} + +func (t *TreeResp) GetName() string { + return t.Name +} + +func (t *TreeResp) ModTime() time.Time { + return t.Modified +} + +func (t *TreeResp) CreateTime() time.Time { + return t.Created +} + +func (t *TreeResp) IsDir() bool { + return t.ObjResp.IsDir +} + +func (t *TreeResp) GetHash() utils.HashInfo { + return utils.FromString(t.HashInfo) +} + +func (t *TreeResp) GetID() string { + return "" +} + +func (t *TreeResp) GetPath() string { + return "" +} + +func (t *TreeResp) GetChildren() []model.ObjTree { + ret := make([]model.ObjTree, 0, len(t.Children)) + for _, child := range t.Children { + ret = append(ret, &child) + } + return ret +} + +func (t *TreeResp) Thumb() string { + return t.ObjResp.Thumb +} + +type ArchiveMetaResp struct { + Comment string `json:"comment"` + Encrypted bool `json:"encrypted"` + Content []TreeResp `json:"content"` + RawURL string `json:"raw_url"` + Sign string `json:"sign"` +} + +type ArchiveListReq struct { + model.PageReq + ArchiveMetaReq + InnerPath string `json:"inner_path"` +} + +type ArchiveListResp struct { + Content []ObjResp `json:"content"` + Total int64 `json:"total"` +} + +type DecompressReq struct { + ArchivePass string `json:"archive_pass"` + CacheFull bool `json:"cache_full"` + DstDir string `json:"dst_dir"` + InnerPath string `json:"inner_path"` + Name []string `json:"name"` + PutIntoNewDir bool `json:"put_into_new_dir"` + SrcDir string `json:"src_dir"` +} diff --git a/drivers/internal/openlist/util.go b/drivers/internal/openlist/util.go new file mode 100644 index 0000000000..3649713179 --- /dev/null +++ b/drivers/internal/openlist/util.go @@ -0,0 +1,97 @@ +package openlist + +import ( + "fmt" + "net/http" + "strings" + + "github.com/OpenListTeam/OpenList/v4/drivers/base" + "github.com/OpenListTeam/OpenList/v4/internal/conf" + "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/OpenListTeam/OpenList/v4/server/common" + "github.com/go-resty/resty/v2" + log "github.com/sirupsen/logrus" +) + +type Identity struct { + Username string + Guest bool +} + +func Init(d State, getIdentity func(State) (Identity, error)) error { + *d.Address = strings.TrimSuffix(*d.Address, "/") + identity, err := getIdentity(d) + if err != nil { + return err + } + if *d.Username != identity.Username { + if err := d.Login(); err != nil { + return err + } + } + identity, err = getIdentity(d) + if err != nil || !identity.Guest { + return err + } + res, err := base.RestyClient.R().Get(*d.Address + "/api/public/settings") + if err != nil { + return err + } + if utils.Json.Get(res.Body(), "data", conf.AllowMounted).ToString() != "true" { + return fmt.Errorf("the site does not allow mounted") + } + return nil +} + +func (d State) Login() error { + if *d.Username == "" { + return nil + } + var resp common.Resp[LoginResp] + _, _, err := d.Request("/auth/login", http.MethodPost, func(req *resty.Request) { + req.SetResult(&resp).SetBody(base.Json{ + "username": *d.Username, + "password": *d.Password, + }) + }) + if err != nil { + return err + } + *d.Token = resp.Data.Token + op.MustSaveDriverStorage(d.Driver) + return nil +} + +func (d State) Request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, int, error) { + url := *d.Address + "/api" + api + req := base.RestyClient.R() + req.SetHeader("Authorization", *d.Token) + if callback != nil { + callback(req) + } + res, err := req.Execute(method, url) + if err != nil { + code := 0 + if res != nil { + code = res.StatusCode() + } + return nil, code, err + } + log.Debugf("[openlist] response body: %s", res.String()) + if res.StatusCode() >= 400 { + return nil, res.StatusCode(), fmt.Errorf("request failed, status: %s", res.Status()) + } + code := utils.Json.Get(res.Body(), "code").ToInt() + if code != 200 { + if (code == 401 || code == 403) && !utils.IsBool(retry...) { + err = d.Login() + if err != nil { + return nil, code, err + } + return d.Request(api, method, callback, true) + } + return nil, code, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString()) + } + return res.Body(), 200, nil +} diff --git a/drivers/openlist/driver.go b/drivers/openlist/driver.go index 79fc511854..20833688ee 100644 --- a/drivers/openlist/driver.go +++ b/drivers/openlist/driver.go @@ -2,22 +2,13 @@ package openlist import ( "context" - "fmt" - "io" "net/http" - "net/url" - "path" - "strings" - "github.com/OpenListTeam/OpenList/v4/drivers/base" - "github.com/OpenListTeam/OpenList/v4/internal/conf" + family "github.com/OpenListTeam/OpenList/v4/drivers/internal/openlist" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" - "github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/server/common" "github.com/go-resty/resty/v2" - log "github.com/sirupsen/logrus" ) type OpenList struct { @@ -25,357 +16,83 @@ type OpenList struct { Addition } -func (d *OpenList) Config() driver.Config { - return config -} +func (d *OpenList) Config() driver.Config { return config } -func (d *OpenList) GetAddition() driver.Additional { - return &d.Addition -} +func (d *OpenList) GetAddition() driver.Additional { return &d.Addition } -func (d *OpenList) Init(ctx context.Context) error { - d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/") - var resp common.Resp[MeResp] - _, _, err := d.request("/me", http.MethodGet, func(req *resty.Request) { - req.SetResult(&resp) - }) - if err != nil { - return err - } - // if the username is not empty and the username is not the same as the current username, then login again - if d.Username != resp.Data.Username { - err = d.login() - if err != nil { - return err - } - } - // re-get the user info - _, _, err = d.request("/me", http.MethodGet, func(req *resty.Request) { - req.SetResult(&resp) - }) - if err != nil { - return err - } - if resp.Data.Role == model.GUEST { - u := d.Address + "/api/public/settings" - res, err := base.RestyClient.R().Get(u) - if err != nil { - return err - } - allowMounted := utils.Json.Get(res.Body(), "data", conf.AllowMounted).ToString() == "true" - if !allowMounted { - return fmt.Errorf("the site does not allow mounted") - } +func (d *OpenList) state() family.State { + return family.State{ + Driver: d, Address: &d.Address, MetaPassword: &d.MetaPassword, + Username: &d.Username, Password: &d.Password, Token: &d.Token, + PassIP: d.PassIPToUpsteam, PassUserAgent: d.PassUAToUpsteam, + ForwardArchives: d.ForwardArchiveReq, + ForwardRefresh: d.PassRefreshFlagToUpsteam, SendOverwrite: true, } - return err } -func (d *OpenList) Drop(ctx context.Context) error { - return nil +func (d *OpenList) Init(context.Context) error { + return family.Init(d.state(), func(s family.State) (family.Identity, error) { + var resp common.Resp[MeResp] + _, _, err := s.Request("/me", http.MethodGet, func(req *resty.Request) { req.SetResult(&resp) }) + return family.Identity{ + Username: resp.Data.Username, + Guest: resp.Data.Role == model.GUEST, + }, err + }) } +func (d *OpenList) Drop(context.Context) error { return nil } + func (d *OpenList) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { - var resp common.Resp[FsListResp] - _, _, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(ListReq{ - PageReq: model.PageReq{ - Page: 1, - PerPage: 0, - }, - Path: dir.GetPath(), - Password: d.MetaPassword, - Refresh: d.PassRefreshFlagToUpsteam && args.Refresh, - }) - }) - if err != nil { - return nil, err - } - var files []model.Obj - for _, f := range resp.Data.Content { - file := model.ObjThumb{ - Object: model.Object{ - Name: f.Name, - Path: path.Join(dir.GetPath(), f.Name), - Modified: f.Modified, - Ctime: f.Created, - Size: f.Size, - IsFolder: f.IsDir, - HashInfo: utils.FromString(f.HashInfo), - }, - Thumbnail: model.Thumbnail{Thumbnail: f.Thumb}, - } - files = append(files, &file) - } - return files, nil + return family.List(d.state(), ctx, dir, args) } func (d *OpenList) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { - var resp common.Resp[FsGetResp] - headers := map[string]string{ - "User-Agent": base.UserAgent, - } - // if PassUAToUpsteam is true, then pass the user-agent to the upstream - if d.PassUAToUpsteam { - userAgent := args.Header.Get("user-agent") - if userAgent != "" { - headers["User-Agent"] = userAgent - } - } - // if PassIPToUpsteam is true, then pass the ip address to the upstream - if d.PassIPToUpsteam { - ip := args.IP - if ip != "" { - headers["X-Forwarded-For"] = ip - headers["X-Real-Ip"] = ip - } - } - _, _, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(FsGetReq{ - Path: file.GetPath(), - Password: d.MetaPassword, - }).SetHeaders(headers) - }) - if err != nil { - return nil, err - } - return &model.Link{ - URL: resp.Data.RawURL, - }, nil + return family.Link(d.state(), ctx, file, args) } -func (d *OpenList) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { - _, _, err := d.request("/fs/mkdir", http.MethodPost, func(req *resty.Request) { - req.SetBody(MkdirOrLinkReq{ - Path: path.Join(parentDir.GetPath(), dirName), - }) - }) - return err +func (d *OpenList) MakeDir(ctx context.Context, parent model.Obj, name string) error { + return family.MakeDir(d.state(), ctx, parent, name) } -func (d *OpenList) Move(ctx context.Context, srcObj, dstDir model.Obj) error { - _, _, err := d.request("/fs/move", http.MethodPost, func(req *resty.Request) { - req.SetBody(MoveCopyReq{ - SrcDir: path.Dir(srcObj.GetPath()), - DstDir: dstDir.GetPath(), - Names: []string{srcObj.GetName()}, - }) - }) - return err +func (d *OpenList) Move(ctx context.Context, src, dst model.Obj) error { + return family.Move(d.state(), ctx, src, dst) } -func (d *OpenList) Rename(ctx context.Context, srcObj model.Obj, newName string) error { - _, _, err := d.request("/fs/rename", http.MethodPost, func(req *resty.Request) { - req.SetBody(RenameReq{ - Path: srcObj.GetPath(), - Name: newName, - }) - }) - return err +func (d *OpenList) Rename(ctx context.Context, src model.Obj, name string) error { + return family.Rename(d.state(), ctx, src, name) } -func (d *OpenList) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { - _, _, err := d.request("/fs/copy", http.MethodPost, func(req *resty.Request) { - req.SetBody(MoveCopyReq{ - SrcDir: path.Dir(srcObj.GetPath()), - DstDir: dstDir.GetPath(), - Names: []string{srcObj.GetName()}, - }) - }) - return err +func (d *OpenList) Copy(ctx context.Context, src, dst model.Obj) error { + return family.Copy(d.state(), ctx, src, dst) } func (d *OpenList) Remove(ctx context.Context, obj model.Obj) error { - _, _, err := d.request("/fs/remove", http.MethodPost, func(req *resty.Request) { - req.SetBody(RemoveReq{ - Dir: path.Dir(obj.GetPath()), - Names: []string{obj.GetName()}, - }) - }) - return err + return family.Remove(d.state(), ctx, obj) } -func (d *OpenList) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error { - reader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{ - Reader: s, - UpdateProgress: up, - }) - req, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+"/api/fs/put", reader) - if err != nil { - return err - } - req.Header.Set("Authorization", d.Token) - req.Header.Set("File-Path", path.Join(dstDir.GetPath(), s.GetName())) - req.Header.Set("Password", d.MetaPassword) - if md5 := s.GetHash().GetHash(utils.MD5); len(md5) > 0 { - req.Header.Set("X-File-Md5", md5) - } - if sha1 := s.GetHash().GetHash(utils.SHA1); len(sha1) > 0 { - req.Header.Set("X-File-Sha1", sha1) - } - if sha256 := s.GetHash().GetHash(utils.SHA256); len(sha256) > 0 { - req.Header.Set("X-File-Sha256", sha256) - } - - req.ContentLength = s.GetSize() - // client := base.NewHttpClient() - // client.Timeout = time.Hour * 6 - res, err := base.HttpClient.Do(req) - if err != nil { - return err - } - - bytes, err := io.ReadAll(res.Body) - if err != nil { - return err - } - log.Debugf("[openlist] response body: %s", string(bytes)) - if res.StatusCode >= 400 { - return fmt.Errorf("request failed, status: %s", res.Status) - } - code := utils.Json.Get(bytes, "code").ToInt() - if code != 200 { - if code == 401 || code == 403 { - err = d.login() - if err != nil { - return err - } - } - return fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(bytes, "message").ToString()) - } - return nil +func (d *OpenList) Put(ctx context.Context, dst model.Obj, src model.FileStreamer, up driver.UpdateProgress) error { + return family.Put(d.state(), ctx, dst, src, up) } func (d *OpenList) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) { - if !d.ForwardArchiveReq { - return nil, errs.NotImplement - } - var resp common.Resp[ArchiveMetaResp] - _, code, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(ArchiveMetaReq{ - ArchivePass: args.Password, - Password: d.MetaPassword, - Path: obj.GetPath(), - Refresh: false, - }) - }) - if code == 202 { - return nil, errs.WrongArchivePassword - } - if err != nil { - return nil, err - } - var tree []model.ObjTree - if resp.Data.Content != nil { - tree = make([]model.ObjTree, 0, len(resp.Data.Content)) - for _, content := range resp.Data.Content { - tree = append(tree, &content) - } - } - return &model.ArchiveMetaInfo{ - Comment: resp.Data.Comment, - Encrypted: resp.Data.Encrypted, - Tree: tree, - }, nil + return family.GetArchiveMeta(d.state(), ctx, obj, args) } func (d *OpenList) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) { - if !d.ForwardArchiveReq { - return nil, errs.NotImplement - } - var resp common.Resp[ArchiveListResp] - _, code, err := d.request("/fs/archive/list", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(ArchiveListReq{ - ArchiveMetaReq: ArchiveMetaReq{ - ArchivePass: args.Password, - Password: d.MetaPassword, - Path: obj.GetPath(), - Refresh: false, - }, - PageReq: model.PageReq{ - Page: 1, - PerPage: 0, - }, - InnerPath: args.InnerPath, - }) - }) - if code == 202 { - return nil, errs.WrongArchivePassword - } - if err != nil { - return nil, err - } - var files []model.Obj - for _, f := range resp.Data.Content { - file := model.ObjThumb{ - Object: model.Object{ - Name: f.Name, - Modified: f.Modified, - Ctime: f.Created, - Size: f.Size, - IsFolder: f.IsDir, - HashInfo: utils.FromString(f.HashInfo), - }, - Thumbnail: model.Thumbnail{Thumbnail: f.Thumb}, - } - files = append(files, &file) - } - return files, nil + return family.ListArchive(d.state(), ctx, obj, args) } func (d *OpenList) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) { - if !d.ForwardArchiveReq { - return nil, errs.NotSupport - } - var resp common.Resp[ArchiveMetaResp] - _, _, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(ArchiveMetaReq{ - ArchivePass: args.Password, - Password: d.MetaPassword, - Path: obj.GetPath(), - Refresh: false, - }) - }) - if err != nil { - return nil, err - } - return &model.Link{ - URL: fmt.Sprintf("%s?inner=%s&pass=%s&sign=%s", - resp.Data.RawURL, - utils.EncodePath(args.InnerPath, true), - url.QueryEscape(args.Password), - resp.Data.Sign), - }, nil + return family.Extract(d.state(), ctx, obj, args) } -func (d *OpenList) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error { - if !d.ForwardArchiveReq { - return errs.NotImplement - } - dir, name := path.Split(srcObj.GetPath()) - _, _, err := d.request("/fs/archive/decompress", http.MethodPost, func(req *resty.Request) { - req.SetBody(DecompressReq{ - ArchivePass: args.Password, - CacheFull: args.CacheFull, - DstDir: dstDir.GetPath(), - InnerPath: args.InnerPath, - Name: []string{name}, - PutIntoNewDir: args.PutIntoNewDir, - SrcDir: dir, - Overwrite: args.Overwrite, - }) - }) - return err +func (d *OpenList) ArchiveDecompress(ctx context.Context, src, dst model.Obj, args model.ArchiveDecompressArgs) error { + return family.ArchiveDecompress(d.state(), ctx, src, dst, args) } -func (d *OpenList) ResolveLinkCacheMode(_ string) driver.LinkCacheMode { - var mode driver.LinkCacheMode - if d.PassIPToUpsteam { - mode |= driver.LinkCacheIP - } - if d.PassUAToUpsteam { - mode |= driver.LinkCacheUA - } - return mode +func (d *OpenList) ResolveLinkCacheMode(path string) driver.LinkCacheMode { + return family.ResolveLinkCacheMode(d.state(), path) } var _ driver.Driver = (*OpenList)(nil) diff --git a/drivers/openlist/types.go b/drivers/openlist/types.go index ffc3e350a4..b3405e010c 100644 --- a/drivers/openlist/types.go +++ b/drivers/openlist/types.go @@ -1,75 +1,22 @@ package openlist -import ( - "time" - - "github.com/OpenListTeam/OpenList/v4/internal/model" - "github.com/OpenListTeam/OpenList/v4/pkg/utils" -) - -type ListReq struct { - model.PageReq - Path string `json:"path" form:"path"` - Password string `json:"password" form:"password"` - Refresh bool `json:"refresh"` -} - -type ObjResp struct { - Name string `json:"name"` - Size int64 `json:"size"` - IsDir bool `json:"is_dir"` - Modified time.Time `json:"modified"` - Created time.Time `json:"created"` - Sign string `json:"sign"` - Thumb string `json:"thumb"` - Type int `json:"type"` - HashInfo string `json:"hashinfo"` -} - -type FsListResp struct { - Content []ObjResp `json:"content"` - Total int64 `json:"total"` - Readme string `json:"readme"` - Write bool `json:"write"` - Provider string `json:"provider"` -} - -type FsGetReq struct { - Path string `json:"path" form:"path"` - Password string `json:"password" form:"password"` -} - -type FsGetResp struct { - ObjResp - RawURL string `json:"raw_url"` - Readme string `json:"readme"` - Provider string `json:"provider"` - Related []ObjResp `json:"related"` -} - -type MkdirOrLinkReq struct { - Path string `json:"path" form:"path"` -} - -type MoveCopyReq struct { - SrcDir string `json:"src_dir"` - DstDir string `json:"dst_dir"` - Names []string `json:"names"` -} - -type RenameReq struct { - Path string `json:"path"` - Name string `json:"name"` -} - -type RemoveReq struct { - Dir string `json:"dir"` - Names []string `json:"names"` -} - -type LoginResp struct { - Token string `json:"token"` -} +import family "github.com/OpenListTeam/OpenList/v4/drivers/internal/openlist" + +type ListReq = family.ListReq +type ObjResp = family.ObjResp +type FsListResp = family.FsListResp +type FsGetReq = family.FsGetReq +type FsGetResp = family.FsGetResp +type MkdirOrLinkReq = family.MkdirOrLinkReq +type MoveCopyReq = family.MoveCopyReq +type RenameReq = family.RenameReq +type RemoveReq = family.RemoveReq +type LoginResp = family.LoginResp +type ArchiveMetaReq = family.ArchiveMetaReq +type TreeResp = family.TreeResp +type ArchiveMetaResp = family.ArchiveMetaResp +type ArchiveListReq = family.ArchiveListReq +type ArchiveListResp = family.ArchiveListResp type MeResp struct { Id int `json:"id"` @@ -83,82 +30,6 @@ type MeResp struct { Otp bool `json:"otp"` } -type ArchiveMetaReq struct { - ArchivePass string `json:"archive_pass"` - Password string `json:"password"` - Path string `json:"path"` - Refresh bool `json:"refresh"` -} - -type TreeResp struct { - ObjResp - Children []TreeResp `json:"children"` - hashCache *utils.HashInfo -} - -func (t *TreeResp) GetSize() int64 { - return t.Size -} - -func (t *TreeResp) GetName() string { - return t.Name -} - -func (t *TreeResp) ModTime() time.Time { - return t.Modified -} - -func (t *TreeResp) CreateTime() time.Time { - return t.Created -} - -func (t *TreeResp) IsDir() bool { - return t.ObjResp.IsDir -} - -func (t *TreeResp) GetHash() utils.HashInfo { - return utils.FromString(t.HashInfo) -} - -func (t *TreeResp) GetID() string { - return "" -} - -func (t *TreeResp) GetPath() string { - return "" -} - -func (t *TreeResp) GetChildren() []model.ObjTree { - ret := make([]model.ObjTree, 0, len(t.Children)) - for _, child := range t.Children { - ret = append(ret, &child) - } - return ret -} - -func (t *TreeResp) Thumb() string { - return t.ObjResp.Thumb -} - -type ArchiveMetaResp struct { - Comment string `json:"comment"` - Encrypted bool `json:"encrypted"` - Content []TreeResp `json:"content"` - RawURL string `json:"raw_url"` - Sign string `json:"sign"` -} - -type ArchiveListReq struct { - model.PageReq - ArchiveMetaReq - InnerPath string `json:"inner_path"` -} - -type ArchiveListResp struct { - Content []ObjResp `json:"content"` - Total int64 `json:"total"` -} - type DecompressReq struct { ArchivePass string `json:"archive_pass"` CacheFull bool `json:"cache_full"` diff --git a/drivers/openlist/util.go b/drivers/openlist/util.go deleted file mode 100644 index 743ba266be..0000000000 --- a/drivers/openlist/util.go +++ /dev/null @@ -1,65 +0,0 @@ -package openlist - -import ( - "fmt" - "net/http" - - "github.com/OpenListTeam/OpenList/v4/drivers/base" - "github.com/OpenListTeam/OpenList/v4/internal/op" - "github.com/OpenListTeam/OpenList/v4/pkg/utils" - "github.com/OpenListTeam/OpenList/v4/server/common" - "github.com/go-resty/resty/v2" - log "github.com/sirupsen/logrus" -) - -func (d *OpenList) login() error { - if d.Username == "" { - return nil - } - var resp common.Resp[LoginResp] - _, _, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) { - req.SetResult(&resp).SetBody(base.Json{ - "username": d.Username, - "password": d.Password, - }) - }) - if err != nil { - return err - } - d.Token = resp.Data.Token - op.MustSaveDriverStorage(d) - return nil -} - -func (d *OpenList) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, int, error) { - url := d.Address + "/api" + api - req := base.RestyClient.R() - req.SetHeader("Authorization", d.Token) - if callback != nil { - callback(req) - } - res, err := req.Execute(method, url) - if err != nil { - code := 0 - if res != nil { - code = res.StatusCode() - } - return nil, code, err - } - log.Debugf("[openlist] response body: %s", res.String()) - if res.StatusCode() >= 400 { - return nil, res.StatusCode(), fmt.Errorf("request failed, status: %s", res.Status()) - } - code := utils.Json.Get(res.Body(), "code").ToInt() - if code != 200 { - if (code == 401 || code == 403) && !utils.IsBool(retry...) { - err = d.login() - if err != nil { - return nil, code, err - } - return d.request(api, method, callback, true) - } - return nil, code, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString()) - } - return res.Body(), 200, nil -} From ff255c38c9cae803c11b5a09e8836e00318b5904 Mon Sep 17 00:00:00 2001 From: nostalume Date: Thu, 24 Sep 2026 21:00:22 +0800 Subject: [PATCH 3/3] test(drivers): verify capability absence and link context - Check operation-level NotImplement behavior for unavailable copy and archive capabilities. - Exercise shared AList/OpenList link forwarding and cache-key context through both product shells. Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- drivers/alist_v3/driver_test.go | 51 +++++++++++++++++++++++++++++ internal/op/capability_test.go | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 internal/op/capability_test.go diff --git a/drivers/alist_v3/driver_test.go b/drivers/alist_v3/driver_test.go index cbc5a4066d..dc1accc803 100644 --- a/drivers/alist_v3/driver_test.go +++ b/drivers/alist_v3/driver_test.go @@ -191,3 +191,54 @@ func TestRoleDialectDecoding(t *testing.T) { t.Fatal("OpenList unexpectedly accepted an array role") } } + +func TestFamilyLinkPreservesForwardedContext(t *testing.T) { + type linkDriver interface { + Link(context.Context, model.Obj, model.LinkArgs) (*model.Link, error) + ResolveLinkCacheMode(string) driver.LinkCacheMode + } + for _, tt := range []struct { + name string + new func(string) linkDriver + }{ + {"alist-v3", func(address string) linkDriver { + return &AListV3{Addition: Addition{ + Address: address, MetaPassword: "secret", PassIPToUpsteam: true, PassUAToUpsteam: true, + }} + }}, + {"openlist", func(address string) linkDriver { + return &openlistdriver.OpenList{Addition: openlistdriver.Addition{ + Address: address, MetaPassword: "secret", PassIPToUpsteam: true, PassUAToUpsteam: true, + }} + }}, + } { + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/fs/get" || r.Method != http.MethodPost { + t.Errorf("request = %s %s", r.Method, r.URL.Path) + } + if r.Header.Get("User-Agent") != "client" || r.Header.Get("X-Forwarded-For") != "192.0.2.1" || r.Header.Get("X-Real-Ip") != "192.0.2.1" { + t.Errorf("forwarded headers = %v", r.Header) + } + var body FsGetReq + if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Path != "/movie" || body.Password != "secret" { + t.Errorf("link request = %+v, err = %v", body, err) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"code":200,"message":"success","data":{"raw_url":"https://example.invalid/movie"}}`)) + })) + t.Cleanup(srv.Close) + useTestClient(t) + d := tt.new(srv.URL) + link, err := d.Link(context.Background(), &model.Object{Path: "/movie"}, model.LinkArgs{ + IP: "192.0.2.1", Header: http.Header{"User-Agent": []string{"client"}}, + }) + if err != nil || link == nil || link.URL != "https://example.invalid/movie" { + t.Fatalf("Link = %+v, err = %v", link, err) + } + if got, want := d.ResolveLinkCacheMode("/movie"), driver.LinkCacheIP|driver.LinkCacheUA; got != want { + t.Fatalf("cache mode = %v, want %v", got, want) + } + }) + } +} diff --git a/internal/op/capability_test.go b/internal/op/capability_test.go new file mode 100644 index 0000000000..4827bcf82e --- /dev/null +++ b/internal/op/capability_test.go @@ -0,0 +1,58 @@ +package op_test + +import ( + "context" + "errors" + "testing" + + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" +) + +// A driver with only the required read behavior has no mutation or archive +// capability. The operation boundary must retain the old NotImplement result. +type readOnlyCapabilityDriver struct{ model.Storage } + +func (*readOnlyCapabilityDriver) Config() driver.Config { return driver.Config{NoCache: true} } +func (*readOnlyCapabilityDriver) GetAddition() driver.Additional { return nil } +func (*readOnlyCapabilityDriver) Init(context.Context) error { return nil } +func (*readOnlyCapabilityDriver) Drop(context.Context) error { return nil } +func (*readOnlyCapabilityDriver) List(context.Context, model.Obj, model.ListArgs) ([]model.Obj, error) { + return nil, nil +} +func (*readOnlyCapabilityDriver) Link(context.Context, model.Obj, model.LinkArgs) (*model.Link, error) { + return nil, nil +} +func (*readOnlyCapabilityDriver) Get(_ context.Context, path string) (model.Obj, error) { + switch path { + case "/src": + return &model.Object{Name: "src", Path: path}, nil + case "/dst": + return &model.Object{Name: "dst", Path: path, IsFolder: true}, nil + default: + return nil, errs.ObjectNotFound + } +} + +func TestAbsentDriverCapabilitiesReturnNotImplement(t *testing.T) { + storage := &readOnlyCapabilityDriver{Storage: model.Storage{MountPath: "/capability-test"}} + ctx := context.Background() + tests := []struct { + name string + run func() error + }{ + {"copy", func() error { return op.Copy(ctx, storage, "/src", "/dst") }}, + {"archive decompress", func() error { + return op.ArchiveDecompress(ctx, storage, "/src", "/dst", model.ArchiveDecompressArgs{}) + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.run(); !errors.Is(err, errs.NotImplement) { + t.Fatalf("got %v, want NotImplement", err) + } + }) + } +}