Compare commits

..

11 Commits

Author SHA1 Message Date
3b90f591b5 Squashed commit of the following:
commit 65c5ec0c34
Author: itsHenry <2671230065@qq.com>
Date:   Sat Nov 4 13:35:09 2023 +0800

    feat(cloudreve): folder size count and switch (#5457 close #5395)

commit a6325967d0
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Mon Oct 30 15:11:20 2023 +0800

    fix(deps): update module github.com/charmbracelet/lipgloss to v0.9.1 (#5234)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit 4dff49470a
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Mon Oct 30 15:10:36 2023 +0800

    fix(deps): update golang.org/x/exp digest to 7918f67 (#5366)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit cc86d6f3d1
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Sun Oct 29 14:45:55 2023 +0800

    fix(deps): update module golang.org/x/net to v0.17.0 [security] (#5370)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit c0f9c8ebaf
Author: Andy Hsu <i@nn.ci>
Date:   Thu Oct 26 19:21:09 2023 +0800

    feat: add ignore direct link params (close #5434)
2023-11-05 22:28:31 +08:00
5657b12b20 Merge branch 'main' into refactor/offline-download 2023-10-20 21:17:32 +08:00
aba8bc0ec2 fix: adapt update progress type 2023-10-20 21:15:37 +08:00
ce6e486666 Squashed commit of the following:
commit 4fc0a77565
Author: Andy Hsu <i@nn.ci>
Date:   Fri Oct 20 21:06:25 2023 +0800

    fix(baidu_netdisk): upload file > 4GB (close #5392)

commit aaffaee2b5
Author: gmugu <94156510@qq.com>
Date:   Thu Oct 19 19:17:53 2023 +0800

    perf(webdav): support request with cookies (#5391)

commit 8ef8023c20
Author: NewbieOrange <NewbieOrange@users.noreply.github.com>
Date:   Thu Oct 19 19:17:09 2023 +0800

    fix(aliyundrive_open): upload progress for normal upload (#5398)

commit cdfbe6dcf2
Author: foxxorcat <95907542+foxxorcat@users.noreply.github.com>
Date:   Wed Oct 18 16:27:07 2023 +0800

    fix: hash gcid empty file (#5394)

commit 94d028743a
Author: Andy Hsu <i@nn.ci>
Date:   Sat Oct 14 13:17:51 2023 +0800

    ci: remove `pr-welcome` label when close issue [skip ci]

commit 7f7335435c
Author: itsHenry <2671230065@qq.com>
Date:   Sat Oct 14 13:12:46 2023 +0800

    feat(cloudreve): support thumbnail (#5373 close #5348)

    * feat(cloudreve): support thumbnail

    * chore: remove unnecessary code

commit b9e192b29c
Author: foxxorcat <95907542+foxxorcat@users.noreply.github.com>
Date:   Thu Oct 12 20:57:12 2023 +0800

    fix(115): limit request rate (#5367 close #5275)

    * fix(115):limit request rate

    * chore(115): fix unit of `limit_rate`

    ---------

    Co-authored-by: Andy Hsu <i@nn.ci>

commit 69a98eaef6
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Wed Oct 11 22:01:55 2023 +0800

    fix(deps): update module github.com/aliyun/aliyun-oss-go-sdk to v2.2.9+incompatible (#5141)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit 1ebc96a4e5
Author: Andy Hsu <i@nn.ci>
Date:   Tue Oct 10 18:32:00 2023 +0800

    fix(wopan): fatal error concurrent map writes (close #5352)

commit 66e2324cac
Author: Andy Hsu <i@nn.ci>
Date:   Tue Oct 10 18:23:11 2023 +0800

    chore(deps): upgrade dependencies

commit 7600dc28df
Author: Andy Hsu <i@nn.ci>
Date:   Tue Oct 10 18:13:58 2023 +0800

    fix(aliyundrive_open): change default api to raw server (close #5358)

commit 8ef89ad0a4
Author: foxxorcat <95907542+foxxorcat@users.noreply.github.com>
Date:   Tue Oct 10 18:08:27 2023 +0800

    fix(baidu_netdisk): hash and `error 2` (#5356)

    * fix(baidu):hash and error:2

    * fix:invalid memory address

commit 35d672217d
Author: jeffmingup <1960588251@qq.com>
Date:   Sun Oct 8 19:29:45 2023 +0800

    fix(onedrive_app): incorrect api on `_accessToken` (#5346)

commit 1a283bb272
Author: foxxorcat <95907542+foxxorcat@users.noreply.github.com>
Date:   Fri Oct 6 16:04:39 2023 +0800

    feat(google_drive): add `hash_info`, `ctime`, `thumbnail` (#5334)

commit a008f54f4d
Author: nkh0472 <67589323+nkh0472@users.noreply.github.com>
Date:   Thu Oct 5 13:10:51 2023 +0800

    docs: minor language improvements (#5329) [skip ci]
2023-10-20 21:14:15 +08:00
9fb9efb704 chore: fix typo 2023-10-06 22:32:05 +08:00
1490da8b53 wip: adapt qBittorrent 2023-10-06 16:02:29 +08:00
12dfb60a66 wip: use tool manager 2023-10-05 22:13:02 +08:00
0380d7fff9 wip: use items in offline_download 2023-10-05 13:38:35 +08:00
0acb2d6073 wip: adapt aria2 2023-10-04 22:23:45 +08:00
ea9a3432ab refactor: change type of percentage to float64 2023-10-04 20:59:11 +08:00
7db3975b18 wip: refactor offline download (#5331)
* base tool

* working: aria2
2023-10-04 16:27:08 +08:00
69 changed files with 1794 additions and 2650 deletions

View File

@ -27,9 +27,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.2
id: short-sha
- name: Install dependencies
run: |
sudo snap install zig --classic --beta
@ -44,5 +41,5 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: alist_${{ env.SHA }}
name: alist
path: dist

View File

@ -112,10 +112,9 @@ https://alist.nn.ci/guide/sponsor.html
### Special sponsors
- [VidHub](https://okaapps.com/product/1659622164?ref=alist) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
- [JetBrains](https://www.jetbrains.com/) - Essential tools for software developers and teams
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (sponsored Chinese API server)
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.pw/)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
## Contributors

View File

@ -110,10 +110,9 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
### 特别赞助
- [VidHub](https://zh.okaapps.com/product/1659622164?ref=alist) - 苹果生态下优雅的网盘视频播放器iPhoneiPadMacApple TV全平台支持。
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
- [JetBrains](https://www.jetbrains.com/) - Essential tools for software developers and teams
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (国内API服务器赞助)
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.pw/)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
## 贡献者

View File

@ -112,10 +112,9 @@ https://alist.nn.ci/guide/sponsor.html
### スペシャルスポンサー
- [VidHub](https://okaapps.com/product/1659622164?ref=alist) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
- [JetBrains](https://www.jetbrains.com/) - Essential tools for software developers and teams
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (sponsored Chinese API server)
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.pw/)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
## コントリビューター

View File

@ -2,7 +2,6 @@ package cmd
import (
"context"
"errors"
"fmt"
"net"
"net/http"
@ -37,7 +36,6 @@ the address is defined in config file`,
}
bootstrap.InitOfflineDownloadTools()
bootstrap.LoadStorages()
bootstrap.InitTaskManager()
if !flags.Debug && !flags.Dev {
gin.SetMode(gin.ReleaseMode)
}
@ -51,7 +49,7 @@ the address is defined in config file`,
httpSrv = &http.Server{Addr: httpBase, Handler: r}
go func() {
err := httpSrv.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
if err != nil && err != http.ErrServerClosed {
utils.Log.Fatalf("failed to start http: %s", err.Error())
}
}()
@ -62,7 +60,7 @@ the address is defined in config file`,
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
go func() {
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
if err != nil && err != http.ErrServerClosed {
utils.Log.Fatalf("failed to start https: %s", err.Error())
}
}()
@ -86,7 +84,7 @@ the address is defined in config file`,
}
}
err = unixSrv.Serve(listener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
if err != nil && err != http.ErrServerClosed {
utils.Log.Fatalf("failed to start unix: %s", err.Error())
}
}()

View File

@ -18,25 +18,26 @@ import (
"sync"
"time"
"github.com/SheltonZhu/115driver/pkg/driver"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/pkg/errors"
)
var UserAgent = driver115.UA115Desktop
var UserAgent = driver.UA115Desktop
func (d *Pan115) login() error {
var err error
opts := []driver115.Option{
driver115.UA(UserAgent),
func(c *driver115.Pan115Client) {
opts := []driver.Option{
driver.UA(UserAgent),
func(c *driver.Pan115Client) {
c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
},
}
d.client = driver115.New(opts...)
cr := &driver115.Credential{}
d.client = driver.New(opts...)
cr := &driver.Credential{}
if d.Addition.QRCodeToken != "" {
s := &driver115.QRCodeSession{
s := &driver.QRCodeSession{
UID: d.Addition.QRCodeToken,
}
if cr, err = d.client.QRCodeLogin(s); err != nil {
@ -58,7 +59,7 @@ func (d *Pan115) login() error {
func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
res := make([]FileObj, 0)
if d.PageSize <= 0 {
d.PageSize = driver115.FileListLimit
d.PageSize = driver.FileListLimit
}
files, err := d.client.ListWithLimit(fileId, d.PageSize)
if err != nil {
@ -248,7 +249,7 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
go func(threadId int) {
defer func() {
if r := recover(); r != nil {
errCh <- fmt.Errorf("recovered in %v", r)
errCh <- fmt.Errorf("Recovered in %v", r)
}
}()
for chunk := range chunksCh {

View File

@ -1,112 +0,0 @@
package _115_share
import (
"context"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"golang.org/x/time/rate"
)
type Pan115Share struct {
model.Storage
Addition
client *driver115.Pan115Client
limiter *rate.Limiter
}
func (d *Pan115Share) Config() driver.Config {
return config
}
func (d *Pan115Share) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Pan115Share) Init(ctx context.Context) error {
if d.LimitRate > 0 {
d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
}
return d.login()
}
func (d *Pan115Share) WaitLimit(ctx context.Context) error {
if d.limiter != nil {
return d.limiter.Wait(ctx)
}
return nil
}
func (d *Pan115Share) Drop(ctx context.Context) error {
return nil
}
func (d *Pan115Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
files := make([]driver115.ShareFile, 0)
fileResp, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, dir.GetID(), driver115.QueryLimit(int(d.PageSize)))
if err != nil {
return nil, err
}
files = append(files, fileResp.Data.List...)
total := fileResp.Data.Count
count := len(fileResp.Data.List)
for total > count {
fileResp, err := d.client.GetShareSnap(
d.ShareCode, d.ReceiveCode, dir.GetID(),
driver115.QueryLimit(int(d.PageSize)), driver115.QueryOffset(count),
)
if err != nil {
return nil, err
}
files = append(files, fileResp.Data.List...)
count += len(fileResp.Data.List)
}
return utils.SliceConvert(files, transFunc)
}
func (d *Pan115Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
downloadInfo, err := d.client.DownloadByShareCode(d.ShareCode, d.ReceiveCode, file.GetID())
if err != nil {
return nil, err
}
return &model.Link{URL: downloadInfo.URL.URL}, nil
}
func (d *Pan115Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotSupport
}
func (d *Pan115Share) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *Pan115Share) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return errs.NotSupport
}
func (d *Pan115Share) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *Pan115Share) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotSupport
}
func (d *Pan115Share) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
return errs.NotSupport
}
var _ driver.Driver = (*Pan115Share)(nil)

View File

@ -1,33 +0,0 @@
package _115_share
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
PageSize int64 `json:"page_size" type:"number" default:"20" help:"list api per page size of 115 driver"`
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
ShareCode string `json:"share_code" type:"text" required:"true" help:"share code of 115 share link"`
ReceiveCode string `json:"receive_code" type:"text" required:"true" help:"receive code of 115 share link"`
driver.RootID
}
var config = driver.Config{
Name: "115 Share",
DefaultRoot: "",
// OnlyProxy: true,
// OnlyLocal: true,
CheckStatus: false,
Alert: "",
NoOverwriteUpload: true,
NoUpload: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Pan115Share{}
})
}

View File

@ -1,111 +0,0 @@
package _115_share
import (
"fmt"
"strconv"
"time"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
)
var _ model.Obj = (*FileObj)(nil)
type FileObj struct {
Size int64
Sha1 string
Utm time.Time
FileName string
isDir bool
FileID string
}
func (f *FileObj) CreateTime() time.Time {
return f.Utm
}
func (f *FileObj) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.SHA1, f.Sha1)
}
func (f *FileObj) GetSize() int64 {
return f.Size
}
func (f *FileObj) GetName() string {
return f.FileName
}
func (f *FileObj) ModTime() time.Time {
return f.Utm
}
func (f *FileObj) IsDir() bool {
return f.isDir
}
func (f *FileObj) GetID() string {
return f.FileID
}
func (f *FileObj) GetPath() string {
return ""
}
func transFunc(sf driver115.ShareFile) (model.Obj, error) {
timeInt, err := strconv.ParseInt(sf.UpdateTime, 10, 64)
if err != nil {
return nil, err
}
var (
utm = time.Unix(timeInt, 0)
isDir = (sf.IsFile == 0)
fileID = string(sf.FileID)
)
if isDir {
fileID = string(sf.CategoryID)
}
return &FileObj{
Size: int64(sf.Size),
Sha1: sf.Sha1,
Utm: utm,
FileName: string(sf.FileName),
isDir: isDir,
FileID: fileID,
}, nil
}
var UserAgent = driver115.UA115Browser
func (d *Pan115Share) login() error {
var err error
opts := []driver115.Option{
driver115.UA(UserAgent),
}
d.client = driver115.New(opts...)
if _, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, ""); err != nil {
return errors.Wrap(err, "failed to get share snap")
}
cr := &driver115.Credential{}
if d.QRCodeToken != "" {
s := &driver115.QRCodeSession{
UID: d.QRCodeToken,
}
if cr, err = d.client.QRCodeLogin(s); err != nil {
return errors.Wrap(err, "failed to login by qrcode")
}
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
d.QRCodeToken = ""
} else if d.Cookie != "" {
if err = cr.FromCookie(d.Cookie); err != nil {
return errors.Wrap(err, "failed to login by cookies")
}
d.client.ImportCredential(cr)
} else {
return errors.New("missing cookie or qrcode account")
}
return d.client.LoginCheck()
}

View File

@ -80,7 +80,7 @@ func (d *AliyundriveOpen) link(ctx context.Context, file model.Obj) (*model.Link
req.SetBody(base.Json{
"drive_id": d.DriveId,
"file_id": file.GetID(),
"expire_sec": 900,
"expire_sec": 14400,
})
})
if err != nil {
@ -93,7 +93,7 @@ func (d *AliyundriveOpen) link(ctx context.Context, file model.Obj) (*model.Link
}
url = utils.Json.Get(res, "streamsUrl", d.LIVPDownloadFormat).ToString()
}
exp := time.Minute
exp := time.Hour
return &model.Link{
URL: url,
Expiration: &exp,
@ -207,7 +207,7 @@ func (d *AliyundriveOpen) Other(ctx context.Context, args model.OtherArgs) (inte
case "video_preview":
uri = "/adrive/v1.0/openFile/getVideoPreviewPlayInfo"
data["category"] = "live_transcoding"
data["url_expire_sec"] = 900
data["url_expire_sec"] = 14400
default:
return nil, errs.NotSupport
}

View File

@ -2,7 +2,6 @@ package drivers
import (
_ "github.com/alist-org/alist/v3/drivers/115"
_ "github.com/alist-org/alist/v3/drivers/115_share"
_ "github.com/alist-org/alist/v3/drivers/123"
_ "github.com/alist-org/alist/v3/drivers/123_link"
_ "github.com/alist-org/alist/v3/drivers/123_share"
@ -18,7 +17,6 @@ import (
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
_ "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/crypt"
_ "github.com/alist-org/alist/v3/drivers/dropbox"
@ -47,7 +45,6 @@ import (
_ "github.com/alist-org/alist/v3/drivers/url_tree"
_ "github.com/alist-org/alist/v3/drivers/uss"
_ "github.com/alist-org/alist/v3/drivers/virtual"
_ "github.com/alist-org/alist/v3/drivers/vtencent"
_ "github.com/alist-org/alist/v3/drivers/webdav"
_ "github.com/alist-org/alist/v3/drivers/weiyun"
_ "github.com/alist-org/alist/v3/drivers/wopan"

View File

@ -1,297 +0,0 @@
package chaoxing
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"strings"
"time"
"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/cron"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
"google.golang.org/appengine/log"
)
type ChaoXing struct {
model.Storage
Addition
cron *cron.Cron
config driver.Config
conf Conf
}
func (d *ChaoXing) Config() driver.Config {
return d.config
}
func (d *ChaoXing) GetAddition() driver.Additional {
return &d.Addition
}
func (d *ChaoXing) refreshCookie() error {
cookie, err := d.Login()
if err != nil {
d.Status = err.Error()
op.MustSaveDriverStorage(d)
return nil
}
d.Addition.Cookie = cookie
op.MustSaveDriverStorage(d)
return nil
}
func (d *ChaoXing) Init(ctx context.Context) error {
err := d.refreshCookie()
if err != nil {
log.Errorf(ctx, err.Error())
}
d.cron = cron.NewCron(time.Hour * 12)
d.cron.Do(func() {
err = d.refreshCookie()
if err != nil {
log.Errorf(ctx, err.Error())
}
})
return nil
}
func (d *ChaoXing) Drop(ctx context.Context) error {
d.cron.Stop()
return nil
}
func (d *ChaoXing) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.GetFiles(dir.GetID())
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
return fileToObj(src), nil
})
}
func (d *ChaoXing) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var resp DownResp
ua := d.conf.ua
fileId := strings.Split(file.GetID(), "$")[1]
_, err := d.requestDownload("/screen/note_note/files/status/"+fileId, http.MethodPost, func(req *resty.Request) {
req.SetHeader("User-Agent", ua)
}, &resp)
if err != nil {
return nil, err
}
u := resp.Download
return &model.Link{
URL: u,
Header: http.Header{
"Cookie": []string{d.Cookie},
"Referer": []string{d.conf.referer},
"User-Agent": []string{ua},
},
Concurrency: 2,
PartSize: 10 * utils.MB,
}, nil
}
func (d *ChaoXing) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"name": dirName,
"pid": parentDir.GetID(),
}
var resp ListFileResp
_, err := d.request("/pc/resource/addResourceFolder", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return err
}
if resp.Result != 1 {
msg := fmt.Sprintf("error:%s", resp.Msg)
return errors.New(msg)
}
return nil
}
func (d *ChaoXing) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"folderIds": srcObj.GetID(),
"targetId": dstDir.GetID(),
}
if !srcObj.IsDir() {
query = map[string]string{
"bbsid": d.Addition.Bbsid,
"recIds": strings.Split(srcObj.GetID(), "$")[0],
"targetId": dstDir.GetID(),
}
}
var resp ListFileResp
_, err := d.request("/pc/resource/moveResource", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return err
}
if !resp.Status {
msg := fmt.Sprintf("error:%s", resp.Msg)
return errors.New(msg)
}
return nil
}
func (d *ChaoXing) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"folderId": srcObj.GetID(),
"name": newName,
}
path := "/pc/resource/updateResourceFolderName"
if !srcObj.IsDir() {
// path = "/pc/resource/updateResourceFileName"
// query = map[string]string{
// "bbsid": d.Addition.Bbsid,
// "recIds": strings.Split(srcObj.GetID(), "$")[0],
// "name": newName,
// }
return errors.New("此网盘不支持修改文件名")
}
var resp ListFileResp
_, err := d.request(path, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return err
}
if resp.Result != 1 {
msg := fmt.Sprintf("error:%s", resp.Msg)
return errors.New(msg)
}
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,
"folderIds": obj.GetID(),
}
path := "/pc/resource/deleteResourceFolder"
var resp ListFileResp
if !obj.IsDir() {
path = "/pc/resource/deleteResourceFile"
query = map[string]string{
"bbsid": d.Addition.Bbsid,
"recIds": strings.Split(obj.GetID(), "$")[0],
}
}
_, err := d.request(path, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return err
}
if resp.Result != 1 {
msg := fmt.Sprintf("error:%s", resp.Msg)
return errors.New(msg)
}
return nil
}
func (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
var resp UploadDataRsp
_, err := d.request("https://noteyd.chaoxing.com/pc/files/getUploadConfig", http.MethodGet, func(req *resty.Request) {
}, &resp)
if err != nil {
return err
}
if resp.Result != 1 {
return errors.New("get upload data error")
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
filePart, err := writer.CreateFormFile("file", stream.GetName())
if err != nil {
return err
}
_, err = io.Copy(filePart, stream)
if err != nil {
return err
}
err = writer.WriteField("_token", resp.Msg.Token)
if err != nil {
return err
}
err = writer.WriteField("puid", fmt.Sprintf("%d", resp.Msg.Puid))
if err != nil {
fmt.Println("Error writing param2 to request body:", err)
return err
}
err = writer.Close()
if err != nil {
return err
}
req, err := http.NewRequest("POST", "https://pan-yz.chaoxing.com/upload", body)
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Content-Length", fmt.Sprintf("%d", body.Len()))
resps, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resps.Body.Close()
bodys, err := io.ReadAll(resps.Body)
if err != nil {
return err
}
var fileRsp UploadFileDataRsp
err = json.Unmarshal(bodys, &fileRsp)
if err != nil {
return err
}
if fileRsp.Msg != "success" {
return errors.New(fileRsp.Msg)
}
uploadDoneParam := UploadDoneParam{Key: fileRsp.ObjectID, Cataid: "100000019", Param: fileRsp.Data}
params, err := json.Marshal(uploadDoneParam)
if err != nil {
return err
}
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"pid": dstDir.GetID(),
"type": "yunpan",
"params": url.QueryEscape("[" + string(params) + "]"),
}
var respd ListFileResp
_, err = d.request("/pc/resource/addResource", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &respd)
if err != nil {
return err
}
if respd.Result != 1 {
msg := fmt.Sprintf("error:%v", resp.Msg)
return errors.New(msg)
}
return nil
}
var _ driver.Driver = (*ChaoXing)(nil)

View File

@ -1,47 +0,0 @@
package chaoxing
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
// 此程序挂载的是超星小组网盘,需要代理才能使用;
// 登录超星后进入个人空间,进入小组,新建小组,点击进去。
// url中就有bbsid的参数系统限制单文件大小2G没有总容量限制
type Addition struct {
// 超星用户名及密码
UserName string `json:"user_name" required:"true"`
Password string `json:"password" required:"true"`
// 从自己新建的小组url里获取
Bbsid string `json:"bbsid" required:"true"`
driver.RootID
// 可不填,程序会自动登录获取
Cookie string `json:"cookie"`
}
type Conf struct {
ua string
referer string
api string
DowloadApi string
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &ChaoXing{
config: driver.Config{
Name: "ChaoXingGroupDrive",
OnlyProxy: true,
OnlyLocal: false,
DefaultRoot: "-1",
NoOverwriteUpload: true,
},
conf: Conf{
ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch",
referer: "https://chaoxing.com/",
api: "https://groupweb.chaoxing.com",
DowloadApi: "https://noteyd.chaoxing.com",
},
}
})
}

View File

@ -1,263 +0,0 @@
package chaoxing
import (
"fmt"
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type Resp struct {
Result int `json:"result"`
}
type UserAuth struct {
GroupAuth struct {
AddData int `json:"addData"`
AddDataFolder int `json:"addDataFolder"`
AddLebel int `json:"addLebel"`
AddManager int `json:"addManager"`
AddMem int `json:"addMem"`
AddTopicFolder int `json:"addTopicFolder"`
AnonymousAddReply int `json:"anonymousAddReply"`
AnonymousAddTopic int `json:"anonymousAddTopic"`
BatchOperation int `json:"batchOperation"`
DelData int `json:"delData"`
DelDataFolder int `json:"delDataFolder"`
DelMem int `json:"delMem"`
DelTopicFolder int `json:"delTopicFolder"`
Dismiss int `json:"dismiss"`
ExamEnc string `json:"examEnc"`
GroupChat int `json:"groupChat"`
IsShowCircleChatButton int `json:"isShowCircleChatButton"`
IsShowCircleCloudButton int `json:"isShowCircleCloudButton"`
IsShowCompanyButton int `json:"isShowCompanyButton"`
Join int `json:"join"`
MemberShowRankSet int `json:"memberShowRankSet"`
ModifyDataFolder int `json:"modifyDataFolder"`
ModifyExpose int `json:"modifyExpose"`
ModifyName int `json:"modifyName"`
ModifyShowPic int `json:"modifyShowPic"`
ModifyTopicFolder int `json:"modifyTopicFolder"`
ModifyVisibleState int `json:"modifyVisibleState"`
OnlyMgrScoreSet int `json:"onlyMgrScoreSet"`
Quit int `json:"quit"`
SendNotice int `json:"sendNotice"`
ShowActivityManage int `json:"showActivityManage"`
ShowActivitySet int `json:"showActivitySet"`
ShowAttentionSet int `json:"showAttentionSet"`
ShowAutoClearStatus int `json:"showAutoClearStatus"`
ShowBarcode int `json:"showBarcode"`
ShowChatRoomSet int `json:"showChatRoomSet"`
ShowCircleActivitySet int `json:"showCircleActivitySet"`
ShowCircleSet int `json:"showCircleSet"`
ShowCmem int `json:"showCmem"`
ShowDataFolder int `json:"showDataFolder"`
ShowDelReason int `json:"showDelReason"`
ShowForward int `json:"showForward"`
ShowGroupChat int `json:"showGroupChat"`
ShowGroupChatSet int `json:"showGroupChatSet"`
ShowGroupSquareSet int `json:"showGroupSquareSet"`
ShowLockAddSet int `json:"showLockAddSet"`
ShowManager int `json:"showManager"`
ShowManagerIdentitySet int `json:"showManagerIdentitySet"`
ShowNeedDelReasonSet int `json:"showNeedDelReasonSet"`
ShowNotice int `json:"showNotice"`
ShowOnlyManagerReplySet int `json:"showOnlyManagerReplySet"`
ShowRank int `json:"showRank"`
ShowRank2 int `json:"showRank2"`
ShowRecycleBin int `json:"showRecycleBin"`
ShowReplyByClass int `json:"showReplyByClass"`
ShowReplyNeedCheck int `json:"showReplyNeedCheck"`
ShowSignbanSet int `json:"showSignbanSet"`
ShowSpeechSet int `json:"showSpeechSet"`
ShowTopicCheck int `json:"showTopicCheck"`
ShowTopicNeedCheck int `json:"showTopicNeedCheck"`
ShowTransferSet int `json:"showTransferSet"`
} `json:"groupAuth"`
OperationAuth struct {
Add int `json:"add"`
AddTopicToFolder int `json:"addTopicToFolder"`
ChoiceSet int `json:"choiceSet"`
DelTopicFromFolder int `json:"delTopicFromFolder"`
Delete int `json:"delete"`
Reply int `json:"reply"`
ScoreSet int `json:"scoreSet"`
TopSet int `json:"topSet"`
Update int `json:"update"`
} `json:"operationAuth"`
}
type File struct {
Cataid int `json:"cataid"`
Cfid int `json:"cfid"`
Content struct {
Cfid int `json:"cfid"`
Pid int `json:"pid"`
FolderName string `json:"folderName"`
ShareType int `json:"shareType"`
Preview string `json:"preview"`
Filetype string `json:"filetype"`
PreviewURL string `json:"previewUrl"`
IsImg bool `json:"isImg"`
ParentPath string `json:"parentPath"`
Icon string `json:"icon"`
Suffix string `json:"suffix"`
Duration int `json:"duration"`
Pantype string `json:"pantype"`
Puid int `json:"puid"`
Filepath string `json:"filepath"`
Crc string `json:"crc"`
Isfile bool `json:"isfile"`
Residstr string `json:"residstr"`
ObjectID string `json:"objectId"`
Extinfo string `json:"extinfo"`
Thumbnail string `json:"thumbnail"`
Creator int `json:"creator"`
ResTypeValue int `json:"resTypeValue"`
UploadDateFormat string `json:"uploadDateFormat"`
DisableOpt bool `json:"disableOpt"`
DownPath string `json:"downPath"`
Sort int `json:"sort"`
Topsort int `json:"topsort"`
Restype string `json:"restype"`
Size int `json:"size"`
UploadDate string `json:"uploadDate"`
FileSize string `json:"fileSize"`
Name string `json:"name"`
FileID string `json:"fileId"`
} `json:"content"`
CreatorID int `json:"creatorId"`
DesID string `json:"des_id"`
ID int `json:"id"`
Inserttime int64 `json:"inserttime"`
Key string `json:"key"`
Norder int `json:"norder"`
OwnerID int `json:"ownerId"`
OwnerType int `json:"ownerType"`
Path string `json:"path"`
Rid int `json:"rid"`
Status int `json:"status"`
Topsign int `json:"topsign"`
}
type ListFileResp struct {
Msg string `json:"msg"`
Result int `json:"result"`
Status bool `json:"status"`
UserAuth UserAuth `json:"userAuth"`
List []File `json:"list"`
}
type DownResp struct {
Msg string `json:"msg"`
Duration int `json:"duration"`
Download string `json:"download"`
FileStatus string `json:"fileStatus"`
URL string `json:"url"`
Status bool `json:"status"`
}
type UploadDataRsp struct {
Result int `json:"result"`
Msg struct {
Puid int `json:"puid"`
Token string `json:"token"`
} `json:"msg"`
}
type UploadFileDataRsp struct {
Result bool `json:"result"`
Msg string `json:"msg"`
Crc string `json:"crc"`
ObjectID string `json:"objectId"`
Resid int64 `json:"resid"`
Puid int `json:"puid"`
Data struct {
DisableOpt bool `json:"disableOpt"`
Resid int64 `json:"resid"`
Crc string `json:"crc"`
Puid int `json:"puid"`
Isfile bool `json:"isfile"`
Pantype string `json:"pantype"`
Size int `json:"size"`
Name string `json:"name"`
ObjectID string `json:"objectId"`
Restype string `json:"restype"`
UploadDate time.Time `json:"uploadDate"`
ModifyDate time.Time `json:"modifyDate"`
UploadDateFormat string `json:"uploadDateFormat"`
Residstr string `json:"residstr"`
Suffix string `json:"suffix"`
Preview string `json:"preview"`
Thumbnail string `json:"thumbnail"`
Creator int `json:"creator"`
Duration int `json:"duration"`
IsImg bool `json:"isImg"`
PreviewURL string `json:"previewUrl"`
Filetype string `json:"filetype"`
Filepath string `json:"filepath"`
Sort int `json:"sort"`
Topsort int `json:"topsort"`
ResTypeValue int `json:"resTypeValue"`
Extinfo string `json:"extinfo"`
} `json:"data"`
}
type UploadDoneParam struct {
Cataid string `json:"cataid"`
Key string `json:"key"`
Param struct {
DisableOpt bool `json:"disableOpt"`
Resid int64 `json:"resid"`
Crc string `json:"crc"`
Puid int `json:"puid"`
Isfile bool `json:"isfile"`
Pantype string `json:"pantype"`
Size int `json:"size"`
Name string `json:"name"`
ObjectID string `json:"objectId"`
Restype string `json:"restype"`
UploadDate time.Time `json:"uploadDate"`
ModifyDate time.Time `json:"modifyDate"`
UploadDateFormat string `json:"uploadDateFormat"`
Residstr string `json:"residstr"`
Suffix string `json:"suffix"`
Preview string `json:"preview"`
Thumbnail string `json:"thumbnail"`
Creator int `json:"creator"`
Duration int `json:"duration"`
IsImg bool `json:"isImg"`
PreviewURL string `json:"previewUrl"`
Filetype string `json:"filetype"`
Filepath string `json:"filepath"`
Sort int `json:"sort"`
Topsort int `json:"topsort"`
ResTypeValue int `json:"resTypeValue"`
Extinfo string `json:"extinfo"`
} `json:"param"`
}
func fileToObj(f File) *model.Object {
if len(f.Content.FolderName) > 0 {
return &model.Object{
ID: fmt.Sprintf("%d", f.ID),
Name: f.Content.FolderName,
Size: 0,
Modified: time.UnixMilli(f.Inserttime),
IsFolder: true,
}
}
paserTime, err := time.Parse("2006-01-02 15:04", f.Content.UploadDate)
if err != nil {
paserTime = time.Now()
}
return &model.Object{
ID: fmt.Sprintf("%d$%s", f.ID, f.Content.FileID),
Name: f.Content.Name,
Size: int64(f.Content.Size),
Modified: paserTime,
IsFolder: false,
}
}

View File

@ -1,179 +0,0 @@
package chaoxing
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"fmt"
"mime/multipart"
"net/http"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/go-resty/resty/v2"
)
func (d *ChaoXing) requestDownload(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
u := d.conf.DowloadApi + pathname
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"Cookie": d.Cookie,
"Accept": "application/json, text/plain, */*",
"Referer": d.conf.referer,
})
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
var e Resp
req.SetError(&e)
res, err := req.Execute(method, u)
if err != nil {
return nil, err
}
return res.Body(), nil
}
func (d *ChaoXing) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
u := d.conf.api + pathname
if strings.Contains(pathname, "getUploadConfig") {
u = pathname
}
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"Cookie": d.Cookie,
"Accept": "application/json, text/plain, */*",
"Referer": d.conf.referer,
})
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
var e Resp
req.SetError(&e)
res, err := req.Execute(method, u)
if err != nil {
return nil, err
}
return res.Body(), nil
}
func (d *ChaoXing) GetFiles(parent string) ([]File, error) {
files := make([]File, 0)
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"folderId": parent,
"recType": "1",
}
var resp ListFileResp
_, err := d.request("/pc/resource/getResourceList", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return nil, err
}
if resp.Result != 1 {
msg:=fmt.Sprintf("error code is:%d", resp.Result)
return nil, errors.New(msg)
}
if len(resp.List) > 0 {
files = append(files, resp.List...)
}
querys := map[string]string{
"bbsid": d.Addition.Bbsid,
"folderId": parent,
"recType": "2",
}
var resps ListFileResp
_, err = d.request("/pc/resource/getResourceList", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(querys)
}, &resps)
if err != nil {
return nil, err
}
if len(resps.List) > 0 {
files = append(files, resps.List...)
}
return files, nil
}
func EncryptByAES(message, key string) (string, error) {
aesKey := []byte(key)
plainText := []byte(message)
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}
iv := aesKey[:aes.BlockSize]
mode := cipher.NewCBCEncrypter(block, iv)
padding := aes.BlockSize - len(plainText)%aes.BlockSize
paddedText := append(plainText, byte(padding))
for i := 0; i < padding-1; i++ {
paddedText = append(paddedText, byte(padding))
}
ciphertext := make([]byte, len(paddedText))
mode.CryptBlocks(ciphertext, paddedText)
encrypted := base64.StdEncoding.EncodeToString(ciphertext)
return encrypted, nil
}
func CookiesToString(cookies []*http.Cookie) string {
var cookieStr string
for _, cookie := range cookies {
cookieStr += cookie.Name + "=" + cookie.Value + "; "
}
if len(cookieStr) > 2 {
cookieStr = cookieStr[:len(cookieStr)-2]
}
return cookieStr
}
func (d *ChaoXing) Login() (string, error) {
transferKey := "u2oh6Vu^HWe4_AES"
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
uname, err := EncryptByAES(d.Addition.UserName, transferKey)
if err != nil {
return "", err
}
password, err := EncryptByAES(d.Addition.Password, transferKey)
if err != nil {
return "", err
}
err = writer.WriteField("uname", uname)
if err != nil {
return "", err
}
err = writer.WriteField("password", password)
if err != nil {
return "", err
}
err = writer.WriteField("t", "true")
if err != nil {
return "", err
}
err = writer.Close()
if err != nil {
return "", err
}
// Create the request
req, err := http.NewRequest("POST", "https://passport2.chaoxing.com/fanyalogin", body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Content-Length", fmt.Sprintf("%d", body.Len()))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
return CookiesToString(resp.Cookies()), nil
}

View File

@ -124,9 +124,6 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
//filter illegal files
continue
}
if !d.ShowHidden && strings.HasPrefix(name, ".") {
continue
}
objRes := model.Object{
Name: name,
Size: 0,
@ -148,9 +145,6 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
//filter illegal files
continue
}
if !d.ShowHidden && strings.HasPrefix(name, ".") {
continue
}
objRes := model.Object{
Name: name,
Size: size,

View File

@ -21,8 +21,6 @@ type Addition struct {
FileNameEncoding string `json:"filename_encoding" type:"select" required:"true" options:"base64,base32,base32768" default:"base64" help:"for advanced user only!"`
Thumbnail bool `json:"thumbnail" required:"true" default:"false" help:"enable thumbnail which pre-generated under .thumbnails folder"`
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
}
var config = driver.Config{

View File

@ -5,6 +5,7 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
@ -43,7 +44,7 @@ func (d *GoogleDrive) refreshToken() error {
gdsaFileThis := d.RefreshToken
if gdsaFile.IsDir() {
if len(d.ServiceAccountFileList) <= 0 {
gdsaReadDir, gdsaDirErr := os.ReadDir(d.RefreshToken)
gdsaReadDir, gdsaDirErr := ioutil.ReadDir(d.RefreshToken)
if gdsaDirErr != nil {
log.Error("read dir fail")
return gdsaDirErr
@ -75,7 +76,7 @@ func (d *GoogleDrive) refreshToken() error {
}
}
gdsaFileThisContent, err := os.ReadFile(gdsaFileThis)
gdsaFileThisContent, err := ioutil.ReadFile(gdsaFileThis)
if err != nil {
return err
}

View File

@ -119,13 +119,10 @@ func (d *MoPan) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
}
data.DownloadUrl = strings.Replace(strings.ReplaceAll(data.DownloadUrl, "&amp;", "&"), "http://", "https://", 1)
res, err := base.NoRedirectClient.R().SetDoNotParseResponse(true).SetContext(ctx).Get(data.DownloadUrl)
res, err := base.NoRedirectClient.R().SetContext(ctx).Head(data.DownloadUrl)
if err != nil {
return nil, err
}
defer func() {
_ = res.RawBody().Close()
}()
if res.StatusCode() == 302 {
data.DownloadUrl = res.Header().Get("location")
}

View File

@ -1,208 +0,0 @@
package vtencent
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"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/cron"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
type Vtencent struct {
model.Storage
Addition
cron *cron.Cron
config driver.Config
conf Conf
}
func (d *Vtencent) Config() driver.Config {
return d.config
}
func (d *Vtencent) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Vtencent) Init(ctx context.Context) error {
tfUid, err := d.LoadUser()
if err != nil {
d.Status = err.Error()
op.MustSaveDriverStorage(d)
return nil
}
d.Addition.TfUid = tfUid
op.MustSaveDriverStorage(d)
d.cron = cron.NewCron(time.Hour * 12)
d.cron.Do(func() {
_, err := d.LoadUser()
if err != nil {
d.Status = err.Error()
op.MustSaveDriverStorage(d)
}
})
return nil
}
func (d *Vtencent) Drop(ctx context.Context) error {
d.cron.Stop()
return nil
}
func (d *Vtencent) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.GetFiles(dir.GetID())
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
return fileToObj(src), nil
})
}
func (d *Vtencent) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
form := fmt.Sprintf(`{"MaterialIds":["%s"]}`, file.GetID())
var dat map[string]interface{}
if err := json.Unmarshal([]byte(form), &dat); err != nil {
return nil, err
}
var resps RspDown
api := "https://api.vs.tencent.com/SaaS/Material/DescribeMaterialDownloadUrl"
rsp, err := d.request(api, http.MethodPost, func(req *resty.Request) {
req.SetBody(dat)
}, &resps)
if err != nil {
return nil, err
}
if err := json.Unmarshal(rsp, &resps); err != nil {
return nil, err
}
if len(resps.Data.DownloadURLInfoSet) == 0 {
return nil, err
}
u := resps.Data.DownloadURLInfoSet[0].DownloadURL
link := &model.Link{
URL: u,
Header: http.Header{
"Referer": []string{d.conf.referer},
"User-Agent": []string{d.conf.ua},
},
Concurrency: 2,
PartSize: 10 * utils.MB,
}
if file.GetSize() == 0 {
link.Concurrency = 0
link.PartSize = 0
}
return link, nil
}
func (d *Vtencent) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
classId, err := strconv.Atoi(parentDir.GetID())
if err != nil {
return err
}
_, err = d.request("https://api.vs.tencent.com/PaaS/Material/CreateClass", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"Owner": base.Json{
"Type": "PERSON",
"Id": d.TfUid,
},
"ParentClassId": classId,
"Name": dirName,
"VerifySign": ""})
}, nil)
return err
}
func (d *Vtencent) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
srcType := "MATERIAL"
if srcObj.IsDir() {
srcType = "CLASS"
}
form := fmt.Sprintf(`{"SourceInfos":[
{"Owner":{"Id":"%s","Type":"PERSON"},
"Resource":{"Type":"%s","Id":"%s"}}
],
"Destination":{"Owner":{"Id":"%s","Type":"PERSON"},
"Resource":{"Type":"CLASS","Id":"%s"}}
}`, d.TfUid, srcType, srcObj.GetID(), d.TfUid, dstDir.GetID())
var dat map[string]interface{}
if err := json.Unmarshal([]byte(form), &dat); err != nil {
return err
}
_, err := d.request("https://api.vs.tencent.com/PaaS/Material/MoveResource", http.MethodPost, func(req *resty.Request) {
req.SetBody(dat)
}, nil)
return err
}
func (d *Vtencent) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
api := "https://api.vs.tencent.com/PaaS/Material/ModifyMaterial"
form := fmt.Sprintf(`{
"Owner":{"Type":"PERSON","Id":"%s"},
"MaterialId":"%s","Name":"%s"}`, d.TfUid, srcObj.GetID(), newName)
if srcObj.IsDir() {
classId, err := strconv.Atoi(srcObj.GetID())
if err != nil {
return err
}
api = "https://api.vs.tencent.com/PaaS/Material/ModifyClass"
form = fmt.Sprintf(`{"Owner":{"Type":"PERSON","Id":"%s"},
"ClassId":%d,"Name":"%s"}`, d.TfUid, classId, newName)
}
var dat map[string]interface{}
if err := json.Unmarshal([]byte(form), &dat); err != nil {
return err
}
_, err := d.request(api, http.MethodPost, func(req *resty.Request) {
req.SetBody(dat)
}, nil)
return err
}
func (d *Vtencent) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
// TODO copy obj, optional
return errs.NotImplement
}
func (d *Vtencent) Remove(ctx context.Context, obj model.Obj) error {
srcType := "MATERIAL"
if obj.IsDir() {
srcType = "CLASS"
}
form := fmt.Sprintf(`{
"SourceInfos":[
{"Owner":{"Type":"PERSON","Id":"%s"},
"Resource":{"Type":"%s","Id":"%s"}}
]
}`, d.TfUid, srcType, obj.GetID())
var dat map[string]interface{}
if err := json.Unmarshal([]byte(form), &dat); err != nil {
return err
}
_, err := d.request("https://api.vs.tencent.com/PaaS/Material/DeleteResource", http.MethodPost, func(req *resty.Request) {
req.SetBody(dat)
}, nil)
return err
}
func (d *Vtencent) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
err := d.FileUpload(ctx, dstDir, stream, up)
return err
}
//func (d *Vtencent) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*Vtencent)(nil)

View File

@ -1,39 +0,0 @@
package vtencent
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootID
Cookie string `json:"cookie" required:"true"`
TfUid string `json:"tf_uid"`
OrderBy string `json:"order_by" type:"select" options:"Name,Size,UpdateTime,CreatTime"`
OrderDirection string `json:"order_direction" type:"select" options:"Asc,Desc"`
}
type Conf struct {
ua string
referer string
origin string
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Vtencent{
config: driver.Config{
Name: "VTencent",
OnlyProxy: true,
OnlyLocal: false,
DefaultRoot: "9",
NoOverwriteUpload: true,
},
conf: Conf{
ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch",
referer: "https://app.v.tencent.com/",
origin: "https://app.v.tencent.com",
},
}
})
}

View File

@ -1,33 +0,0 @@
package vtencent
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
)
func QSignatureKey(timeKey string, signPath string, key string) string {
signKey := hmac.New(sha1.New, []byte(key))
signKey.Write([]byte(timeKey))
signKeyBytes := signKey.Sum(nil)
signKeyHex := hex.EncodeToString(signKeyBytes)
sha := sha1.New()
sha.Write([]byte(signPath))
shaBytes := sha.Sum(nil)
shaHex := hex.EncodeToString(shaBytes)
O := "sha1\n" + timeKey + "\n" + shaHex + "\n"
dataSignKey := hmac.New(sha1.New, []byte(signKeyHex))
dataSignKey.Write([]byte(O))
dataSignKeyBytes := dataSignKey.Sum(nil)
dataSignKeyHex := hex.EncodeToString(dataSignKeyBytes)
return dataSignKeyHex
}
func QTwoSignatureKey(timeKey string, key string) string {
signKey := hmac.New(sha1.New, []byte(key))
signKey.Write([]byte(timeKey))
signKeyBytes := signKey.Sum(nil)
signKeyHex := hex.EncodeToString(signKeyBytes)
return signKeyHex
}

View File

@ -1,252 +0,0 @@
package vtencent
import (
"strconv"
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type RespErr struct {
Code string `json:"Code"`
Message string `json:"Message"`
}
type Reqfiles struct {
ScrollToken string `json:"ScrollToken"`
Text string `json:"Text"`
Offset int `json:"Offset"`
Limit int `json:"Limit"`
Sort struct {
Field string `json:"Field"`
Order string `json:"Order"`
} `json:"Sort"`
CreateTimeRanges []any `json:"CreateTimeRanges"`
MaterialTypes []any `json:"MaterialTypes"`
ReviewStatuses []any `json:"ReviewStatuses"`
Tags []any `json:"Tags"`
SearchScopes []struct {
Owner struct {
Type string `json:"Type"`
ID string `json:"Id"`
} `json:"Owner"`
ClassID int `json:"ClassId"`
SearchOneDepth bool `json:"SearchOneDepth"`
} `json:"SearchScopes"`
}
type File struct {
Type string `json:"Type"`
ClassInfo struct {
ClassID int `json:"ClassId"`
Name string `json:"Name"`
UpdateTime time.Time `json:"UpdateTime"`
CreateTime time.Time `json:"CreateTime"`
FileInboxID string `json:"FileInboxId"`
Owner struct {
Type string `json:"Type"`
ID string `json:"Id"`
} `json:"Owner"`
ClassPath string `json:"ClassPath"`
ParentClassID int `json:"ParentClassId"`
AttachmentInfo struct {
SubClassCount int `json:"SubClassCount"`
MaterialCount int `json:"MaterialCount"`
Size int64 `json:"Size"`
} `json:"AttachmentInfo"`
ClassPreviewURLSet []string `json:"ClassPreviewUrlSet"`
} `json:"ClassInfo"`
MaterialInfo struct {
BasicInfo struct {
MaterialID string `json:"MaterialId"`
MaterialType string `json:"MaterialType"`
Name string `json:"Name"`
CreateTime time.Time `json:"CreateTime"`
UpdateTime time.Time `json:"UpdateTime"`
ClassPath string `json:"ClassPath"`
ClassID int `json:"ClassId"`
TagInfoSet []any `json:"TagInfoSet"`
TagSet []any `json:"TagSet"`
PreviewURL string `json:"PreviewUrl"`
MediaURL string `json:"MediaUrl"`
UnifiedMediaPreviewURL string `json:"UnifiedMediaPreviewUrl"`
Owner struct {
Type string `json:"Type"`
ID string `json:"Id"`
} `json:"Owner"`
PermissionSet any `json:"PermissionSet"`
PermissionInfoSet []any `json:"PermissionInfoSet"`
TfUID string `json:"TfUid"`
GroupID string `json:"GroupId"`
VersionMaterialIDSet []any `json:"VersionMaterialIdSet"`
FileType string `json:"FileType"`
CmeMaterialPlayList []any `json:"CmeMaterialPlayList"`
Status string `json:"Status"`
DownloadSwitch string `json:"DownloadSwitch"`
} `json:"BasicInfo"`
MediaInfo struct {
Width int `json:"Width"`
Height int `json:"Height"`
Size int `json:"Size"`
Duration float64 `json:"Duration"`
Fps int `json:"Fps"`
BitRate int `json:"BitRate"`
Codec string `json:"Codec"`
MediaType string `json:"MediaType"`
FavoriteStatus string `json:"FavoriteStatus"`
} `json:"MediaInfo"`
MaterialStatus struct {
ContentReviewStatus string `json:"ContentReviewStatus"`
EditorUsableStatus string `json:"EditorUsableStatus"`
UnifiedPreviewStatus string `json:"UnifiedPreviewStatus"`
EditPreviewImageSpiritStatus string `json:"EditPreviewImageSpiritStatus"`
TranscodeStatus string `json:"TranscodeStatus"`
AdaptiveStreamingStatus string `json:"AdaptiveStreamingStatus"`
StreamConnectable string `json:"StreamConnectable"`
AiAnalysisStatus string `json:"AiAnalysisStatus"`
AiRecognitionStatus string `json:"AiRecognitionStatus"`
} `json:"MaterialStatus"`
ImageMaterial struct {
Height int `json:"Height"`
Width int `json:"Width"`
Size int `json:"Size"`
MaterialURL string `json:"MaterialUrl"`
Resolution string `json:"Resolution"`
VodFileID string `json:"VodFileId"`
OriginalURL string `json:"OriginalUrl"`
} `json:"ImageMaterial"`
VideoMaterial struct {
MetaData struct {
Size int `json:"Size"`
Container string `json:"Container"`
Bitrate int `json:"Bitrate"`
Height int `json:"Height"`
Width int `json:"Width"`
Duration float64 `json:"Duration"`
Rotate int `json:"Rotate"`
VideoStreamInfoSet []struct {
Bitrate int `json:"Bitrate"`
Height int `json:"Height"`
Width int `json:"Width"`
Codec string `json:"Codec"`
Fps int `json:"Fps"`
} `json:"VideoStreamInfoSet"`
AudioStreamInfoSet []struct {
Bitrate int `json:"Bitrate"`
SamplingRate int `json:"SamplingRate"`
Codec string `json:"Codec"`
} `json:"AudioStreamInfoSet"`
} `json:"MetaData"`
ImageSpriteInfo any `json:"ImageSpriteInfo"`
MaterialURL string `json:"MaterialUrl"`
CoverURL string `json:"CoverUrl"`
Resolution string `json:"Resolution"`
VodFileID string `json:"VodFileId"`
OriginalURL string `json:"OriginalUrl"`
AudioWaveformURL string `json:"AudioWaveformUrl"`
SubtitleURL string `json:"SubtitleUrl"`
TranscodeInfoSet []any `json:"TranscodeInfoSet"`
ImageSpriteInfoSet []any `json:"ImageSpriteInfoSet"`
} `json:"VideoMaterial"`
} `json:"MaterialInfo"`
}
type RspFiles struct {
Code string `json:"Code"`
Message string `json:"Message"`
EnglishMessage string `json:"EnglishMessage"`
Data struct {
TotalCount int `json:"TotalCount"`
ResourceInfoSet []File `json:"ResourceInfoSet"`
ScrollToken string `json:"ScrollToken"`
} `json:"Data"`
}
type RspDown struct {
Code string `json:"Code"`
Message string `json:"Message"`
EnglishMessage string `json:"EnglishMessage"`
Data struct {
DownloadURLInfoSet []struct {
MaterialID string `json:"MaterialId"`
DownloadURL string `json:"DownloadUrl"`
} `json:"DownloadUrlInfoSet"`
} `json:"Data"`
}
type RspCreatrMaterial struct {
Code string `json:"Code"`
Message string `json:"Message"`
EnglishMessage string `json:"EnglishMessage"`
Data struct {
UploadContext string `json:"UploadContext"`
VodUploadSign string `json:"VodUploadSign"`
QuickUpload bool `json:"QuickUpload"`
} `json:"Data"`
}
type RspApplyUploadUGC struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Video struct {
StorageSignature string `json:"storageSignature"`
StoragePath string `json:"storagePath"`
} `json:"video"`
StorageAppID int `json:"storageAppId"`
StorageBucket string `json:"storageBucket"`
StorageRegion string `json:"storageRegion"`
StorageRegionV5 string `json:"storageRegionV5"`
Domain string `json:"domain"`
VodSessionKey string `json:"vodSessionKey"`
TempCertificate struct {
SecretID string `json:"secretId"`
SecretKey string `json:"secretKey"`
Token string `json:"token"`
ExpiredTime int `json:"expiredTime"`
} `json:"tempCertificate"`
AppID int `json:"appId"`
Timestamp int `json:"timestamp"`
StorageRegionV50 string `json:"StorageRegionV5"`
MiniProgramAccelerateHost string `json:"MiniProgramAccelerateHost"`
} `json:"data"`
}
type RspCommitUploadUGC struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Video struct {
URL string `json:"url"`
VerifyContent string `json:"verify_content"`
} `json:"video"`
FileID string `json:"fileId"`
} `json:"data"`
}
type RspFinishUpload struct {
Code string `json:"Code"`
Message string `json:"Message"`
EnglishMessage string `json:"EnglishMessage"`
Data struct {
MaterialID string `json:"MaterialId"`
} `json:"Data"`
}
func fileToObj(f File) *model.Object {
obj := &model.Object{}
if f.Type == "CLASS" {
obj.Name = f.ClassInfo.Name
obj.ID = strconv.Itoa(f.ClassInfo.ClassID)
obj.IsFolder = true
obj.Modified = f.ClassInfo.CreateTime
obj.Size = 0
} else if f.Type == "MATERIAL" {
obj.Name = f.MaterialInfo.BasicInfo.Name
obj.ID = f.MaterialInfo.BasicInfo.MaterialID
obj.IsFolder = false
obj.Modified = f.MaterialInfo.BasicInfo.CreateTime
obj.Size = int64(f.MaterialInfo.MediaInfo.Size)
}
return obj
}

View File

@ -1,289 +0,0 @@
package vtencent
import (
"context"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"path"
"strconv"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/go-resty/resty/v2"
)
func (d *Vtencent) request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"cookie": d.Cookie,
"content-type": "application/json",
"origin": d.conf.origin,
"referer": d.conf.referer,
})
if callback != nil {
callback(req)
} else {
req.SetBody("{}")
}
if resp != nil {
req.SetResult(resp)
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
code := utils.Json.Get(res.Body(), "Code").ToString()
if code != "Success" {
switch code {
case "AuthFailure.SessionInvalid":
if err != nil {
return nil, errors.New(code)
}
default:
return nil, errors.New(code)
}
return d.request(url, method, callback, resp)
}
return res.Body(), nil
}
func (d *Vtencent) ugcRequest(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"cookie": d.Cookie,
"content-type": "application/json",
"origin": d.conf.origin,
"referer": d.conf.referer,
})
if callback != nil {
callback(req)
} else {
req.SetBody("{}")
}
if resp != nil {
req.SetResult(resp)
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
code := utils.Json.Get(res.Body(), "Code").ToInt()
if code != 0 {
message := utils.Json.Get(res.Body(), "message").ToString()
if len(message) == 0 {
message = utils.Json.Get(res.Body(), "msg").ToString()
}
return nil, errors.New(message)
}
return res.Body(), nil
}
func (d *Vtencent) LoadUser() (string, error) {
api := "https://api.vs.tencent.com/SaaS/Account/DescribeAccount"
res, err := d.request(api, http.MethodPost, func(req *resty.Request) {}, nil)
if err != nil {
return "", err
}
return utils.Json.Get(res, "Data", "TfUid").ToString(), nil
}
func (d *Vtencent) GetFiles(dirId string) ([]File, error) {
api := "https://api.vs.tencent.com/PaaS/Material/SearchResource"
form := fmt.Sprintf(`{
"Text":"",
"Text":"",
"Offset":0,
"Limit":20000,
"Sort":{"Field":"%s","Order":"%s"},
"CreateTimeRanges":[],
"MaterialTypes":[],
"ReviewStatuses":[],
"Tags":[],
"SearchScopes":[{"Owner":{"Type":"PERSON","Id":"%s"},"ClassId":%s,"SearchOneDepth":true}]
}`, d.Addition.OrderBy, d.Addition.OrderDirection, d.TfUid, dirId)
var resps RspFiles
_, err := d.request(api, http.MethodPost, func(req *resty.Request) {
req.SetBody(form).ForceContentType("application/json")
}, &resps)
if err != nil {
return []File{}, err
}
return resps.Data.ResourceInfoSet, nil
}
func (d *Vtencent) CreateUploadMaterial(classId int, fileName string, UploadSummaryKey string) (RspCreatrMaterial, error) {
api := "https://api.vs.tencent.com/PaaS/Material/CreateUploadMaterial"
form := base.Json{"Owner": base.Json{"Type": "PERSON", "Id": d.TfUid},
"MaterialType": "VIDEO", "Name": fileName, "ClassId": classId,
"UploadSummaryKey": UploadSummaryKey}
var resps RspCreatrMaterial
_, err := d.request(api, http.MethodPost, func(req *resty.Request) {
req.SetBody(form).ForceContentType("application/json")
}, &resps)
if err != nil {
return RspCreatrMaterial{}, err
}
return resps, nil
}
func (d *Vtencent) ApplyUploadUGC(signature string, stream model.FileStreamer) (RspApplyUploadUGC, error) {
api := "https://vod2.qcloud.com/v3/index.php?Action=ApplyUploadUGC"
form := base.Json{
"signature": signature,
"videoName": stream.GetName(),
"videoType": strings.ReplaceAll(path.Ext(stream.GetName()), ".", ""),
"videoSize": stream.GetSize(),
}
var resps RspApplyUploadUGC
_, err := d.ugcRequest(api, http.MethodPost, func(req *resty.Request) {
req.SetBody(form).ForceContentType("application/json")
}, &resps)
if err != nil {
return RspApplyUploadUGC{}, err
}
return resps, nil
}
func (d *Vtencent) CommitUploadUGC(signature string, vodSessionKey string) (RspCommitUploadUGC, error) {
api := "https://vod2.qcloud.com/v3/index.php?Action=CommitUploadUGC"
form := base.Json{
"signature": signature,
"vodSessionKey": vodSessionKey,
}
var resps RspCommitUploadUGC
rsp, err := d.ugcRequest(api, http.MethodPost, func(req *resty.Request) {
req.SetBody(form).ForceContentType("application/json")
}, &resps)
if err != nil {
return RspCommitUploadUGC{}, err
}
if len(resps.Data.Video.URL) == 0 {
return RspCommitUploadUGC{}, errors.New(string(rsp))
}
return resps, nil
}
func (d *Vtencent) FinishUploadMaterial(SummaryKey string, VodVerifyKey string, UploadContext, VodFileId string) (RspFinishUpload, error) {
api := "https://api.vs.tencent.com/PaaS/Material/FinishUploadMaterial"
form := base.Json{
"UploadContext": UploadContext,
"VodVerifyKey": VodVerifyKey,
"VodFileId": VodFileId,
"UploadFullKey": SummaryKey}
var resps RspFinishUpload
rsp, err := d.request(api, http.MethodPost, func(req *resty.Request) {
req.SetBody(form).ForceContentType("application/json")
}, &resps)
if err != nil {
return RspFinishUpload{}, err
}
if len(resps.Data.MaterialID) == 0 {
return RspFinishUpload{}, errors.New(string(rsp))
}
return resps, nil
}
func (d *Vtencent) FinishHashUploadMaterial(SummaryKey string, UploadContext string) (RspFinishUpload, error) {
api := "https://api.vs.tencent.com/PaaS/Material/FinishUploadMaterial"
var resps RspFinishUpload
form := base.Json{
"UploadContext": UploadContext,
"UploadFullKey": SummaryKey}
rsp, err := d.request(api, http.MethodPost, func(req *resty.Request) {
req.SetBody(form).ForceContentType("application/json")
}, &resps)
if err != nil {
return RspFinishUpload{}, err
}
if len(resps.Data.MaterialID) == 0 {
return RspFinishUpload{}, errors.New(string(rsp))
}
return resps, nil
}
func (d *Vtencent) FileUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
classId, err := strconv.Atoi(dstDir.GetID())
if err != nil {
return err
}
const chunkLength int64 = 1024 * 1024 * 10
reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: chunkLength})
if err != nil {
return err
}
chunkHash, err := utils.HashReader(utils.SHA1, reader)
if err != nil {
return err
}
rspCreatrMaterial, err := d.CreateUploadMaterial(classId, stream.GetName(), chunkHash)
if err != nil {
return err
}
if rspCreatrMaterial.Data.QuickUpload {
SummaryKey := stream.GetHash().GetHash(utils.SHA1)
if len(SummaryKey) < utils.SHA1.Width {
if SummaryKey, err = utils.HashReader(utils.SHA1, stream); err != nil {
return err
}
}
UploadContext := rspCreatrMaterial.Data.UploadContext
_, err = d.FinishHashUploadMaterial(SummaryKey, UploadContext)
if err != nil {
return err
}
return nil
}
hash := sha1.New()
rspUGC, err := d.ApplyUploadUGC(rspCreatrMaterial.Data.VodUploadSign, stream)
if err != nil {
return err
}
params := rspUGC.Data
certificate := params.TempCertificate
cfg := &aws.Config{
HTTPClient: base.HttpClient,
// S3ForcePathStyle: aws.Bool(true),
Credentials: credentials.NewStaticCredentials(certificate.SecretID, certificate.SecretKey, certificate.Token),
Region: aws.String(params.StorageRegionV5),
Endpoint: aws.String(fmt.Sprintf("cos.%s.myqcloud.com", params.StorageRegionV5)),
}
ss, err := session.NewSession(cfg)
if err != nil {
return err
}
uploader := s3manager.NewUploader(ss)
input := &s3manager.UploadInput{
Bucket: aws.String(fmt.Sprintf("%s-%d", params.StorageBucket, params.StorageAppID)),
Key: &params.Video.StoragePath,
Body: io.TeeReader(stream, io.MultiWriter(hash, driver.NewProgress(stream.GetSize(), up))),
}
_, err = uploader.UploadWithContext(ctx, input)
if err != nil {
return err
}
rspCommitUGC, err := d.CommitUploadUGC(rspCreatrMaterial.Data.VodUploadSign, rspUGC.Data.VodSessionKey)
if err != nil {
return err
}
VodVerifyKey := rspCommitUGC.Data.Video.VerifyContent
VodFileId := rspCommitUGC.Data.FileID
UploadContext := rspCreatrMaterial.Data.UploadContext
SummaryKey := hex.EncodeToString(hash.Sum(nil))
_, err = d.FinishUploadMaterial(SummaryKey, VodVerifyKey, UploadContext, VodFileId)
if err != nil {
return err
}
return nil
}

View File

@ -11,7 +11,6 @@ type Addition struct {
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
driver.RootPath
TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" default:"false"`
}
var config = driver.Config{

View File

@ -1,7 +1,6 @@
package webdav
import (
"crypto/tls"
"net/http"
"net/http/cookiejar"
@ -18,10 +17,6 @@ func (d *WebDav) isSharepoint() bool {
func (d *WebDav) setClient() error {
c := gowebdav.NewClient(d.Address, d.Username, d.Password)
c.SetTransport(&http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: d.TlsInsecureSkipVerify},
})
if d.isSharepoint() {
cookie, err := odrvcookie.GetCookie(d.Username, d.Password, d.Address)
if err == nil {

11
go.mod
View File

@ -3,13 +3,13 @@ module github.com/alist-org/alist/v3
go 1.20
require (
github.com/SheltonZhu/115driver v1.0.21
github.com/SheltonZhu/115driver v1.0.16
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
github.com/Xhofe/wopan-sdk-go v0.1.2
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible
github.com/avast/retry-go v3.0.0+incompatible
github.com/aws/aws-sdk-go v1.46.7
github.com/aws/aws-sdk-go v1.44.327
github.com/blevesearch/bleve/v2 v2.3.10
github.com/caarlos0/env/v9 v9.0.0
github.com/charmbracelet/bubbles v0.16.1
@ -21,7 +21,7 @@ require (
github.com/djherbis/times v1.5.0
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564
github.com/foxxorcat/mopan-sdk-go v0.1.4
github.com/foxxorcat/weiyun-sdk-go v0.1.3
github.com/foxxorcat/weiyun-sdk-go v0.1.2
github.com/gin-contrib/cors v1.4.0
github.com/gin-gonic/gin v1.9.1
github.com/go-resty/resty/v2 v2.9.1
@ -53,7 +53,6 @@ require (
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.12.0
golang.org/x/time v0.3.0
google.golang.org/appengine v1.6.7
gorm.io/driver/mysql v1.4.7
gorm.io/driver/postgres v1.4.8
gorm.io/driver/sqlite v1.4.4
@ -124,7 +123,6 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.0 // indirect
github.com/jaevor/go-nanoid v1.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
@ -185,8 +183,6 @@ require (
github.com/u2takey/go-utils v0.3.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 // indirect
github.com/xhofe/tache v0.0.0-20231120085916-722855be0521 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
golang.org/x/arch v0.3.0 // indirect
@ -195,6 +191,7 @@ require (
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/api v0.134.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/grpc v1.57.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect

120
go.sum
View File

@ -8,28 +8,38 @@ github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
github.com/SheltonZhu/115driver v1.0.21 h1:Pz6r14VwIiuSyHj+OmJe57FHhbmWB/6IfnXAFL2iXbU=
github.com/SheltonZhu/115driver v1.0.21/go.mod h1:e3fPOBANbH/FsTya8FquJwOR3ErhCQgEab3q6CVY2k4=
github.com/SheltonZhu/115driver v1.0.15 h1:RRvgXvXEzvrPwkRno0CUIg7ucEphbsfwct2mQxfNOdQ=
github.com/SheltonZhu/115driver v1.0.15/go.mod h1:e3fPOBANbH/FsTya8FquJwOR3ErhCQgEab3q6CVY2k4=
github.com/SheltonZhu/115driver v1.0.16 h1:XOhqRtKF9huTCobM5rWVd9DtJyBKLJSnBwWPF3+GM+k=
github.com/SheltonZhu/115driver v1.0.16/go.mod h1:e3fPOBANbH/FsTya8FquJwOR3ErhCQgEab3q6CVY2k4=
github.com/Unknwon/goconfig v1.0.0 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A=
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a h1:RenIAa2q4H8UcS/cqmwdT1WCWIAH5aumP8m8RpbqVsE=
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a/go.mod h1:sSBbaOg90XwWKtpT56kVujF0bIeVITnPlssLclogS04=
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4 h1:WnvifFgYyogPz2ZFvaVLk4gI/Co0paF92FmxSR6U1zY=
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4/go.mod h1:8pWlL2rpusvx7Xa6yYaIWOJ8bR3gPdFBUT7OystyGOY=
github.com/Xhofe/wopan-sdk-go v0.1.1 h1:dSrTxNYclqNuo9libjtC+R6C4RCen/inh/dUXd12vpM=
github.com/Xhofe/wopan-sdk-go v0.1.1/go.mod h1:xWcUS7PoFLDD9gy2BK2VQfilEsZngLMz2Vkx3oF2zJY=
github.com/Xhofe/wopan-sdk-go v0.1.2 h1:6Gh4YTT7b7YHN0OoJ33j7Jm9ru/ckuvcDxPnRmH07jc=
github.com/Xhofe/wopan-sdk-go v0.1.2/go.mod h1:ktLYb4t7rnPFq1AshLaPXq5kZER+DkEagT6/i/in0uo=
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=
github.com/aead/ecdh v0.2.0/go.mod h1:a9HHtXuSo8J1Js1MwLQx2mBhkXMT6YwUmVVEY4tTB8U=
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible h1:QoRMR0TCctLDqBCMyOu1eXdZyMw3F7uGA9qPn2J4+R8=
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible h1:KpbJFXwhVeuxNtBJ74MCGbIoaBok2uZvkD7QXp2+Wis=
github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k=
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andreburgaud/crypt2go v1.1.0 h1:eitZxTPY1krUsxinsng3Qvt/Ud7q/aQmmYRh8p4hyPw=
github.com/andreburgaud/crypt2go v1.1.0/go.mod h1:4qhZPzarj1dCIRmCkpdgCklwp+hBq9yEt0zPe9Ayuhc=
github.com/andreburgaud/crypt2go v1.2.0 h1:oly/ENAodeqTYpUafgd4r3v+VKLQnmOKUyfpj+TxHbE=
github.com/andreburgaud/crypt2go v1.2.0/go.mod h1:kKRqlrX/3Q9Ki7HdUsoh0cX1Urq14/Hcta4l4VrIXrI=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.46.7 h1:IjvAWeiJZlbETOemOwvheN5L17CvKvKW0T1xOC6d3Sc=
github.com/aws/aws-sdk-go v1.46.7/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.44.327 h1:ZS8oO4+7MOBLhkdwIhgtVeDzCeWOlTfKJS7EgggbIEY=
github.com/aws/aws-sdk-go v1.44.327/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
@ -40,10 +50,16 @@ github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjL
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/blevesearch/bleve/v2 v2.3.9 h1:pUMvK0mxAexqasZcVj8lazmWnEW5XiV0tASIqANiNTQ=
github.com/blevesearch/bleve/v2 v2.3.9/go.mod h1:1PibElcjlQMQHF9uS9mRv58ODQgj4pCWHA1Wfd+qagU=
github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg=
github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA=
github.com/blevesearch/bleve_index_api v1.0.5 h1:Lc986kpC4Z0/n1g3gg8ul7H+lxgOQPcXb9SxvQGu+tw=
github.com/blevesearch/bleve_index_api v1.0.5/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms=
github.com/blevesearch/bleve_index_api v1.0.6 h1:gyUUxdsrvmW3jVhhYdCVL6h9dCjNT/geNU7PxGn37p8=
github.com/blevesearch/bleve_index_api v1.0.6/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms=
github.com/blevesearch/geo v0.1.17 h1:AguzI6/5mHXapzB0gE9IKWo+wWPHZmXZoscHcjFgAFA=
github.com/blevesearch/geo v0.1.17/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM=
github.com/blevesearch/geo v0.1.18 h1:Np8jycHTZ5scFe7VEPLrDoHnnb9C4j636ue/CGrhtDw=
github.com/blevesearch/geo v0.1.18/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
@ -52,6 +68,8 @@ github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZG
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.1.5 h1:1g713kpCQZ8u4a3stRGBfrwVOuGRnmxOVU5MQkUPrHU=
github.com/blevesearch/scorch_segment_api/v2 v2.1.5/go.mod h1:f2nOkKS1HcjgIWZgDAErgBdxmr2eyt0Kn7IY+FU1Xe4=
github.com/blevesearch/scorch_segment_api/v2 v2.1.6 h1:CdekX/Ob6YCYmeHzD72cKpwzBjvkOGegHOqhAkXp6yA=
github.com/blevesearch/scorch_segment_api/v2 v2.1.6/go.mod h1:nQQYlp51XvoSVxcciBjtvuHPIVjlWrN1hX4qwK2cqdc=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
@ -62,14 +80,24 @@ github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMG
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI=
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
github.com/blevesearch/zapx/v11 v11.3.9 h1:y3ijS4h4MJdmQ07MHASxat4owAixreK2xdo76w9ncrw=
github.com/blevesearch/zapx/v11 v11.3.9/go.mod h1:jcAYnQwlr+LqD2vLjDWjWiZDXDXGFqPbpPDRTd3XmS4=
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=
github.com/blevesearch/zapx/v12 v12.3.9 h1:MXGLlZ03oxXH3DMJTZaBaRj2xb6t4wQVZeZK/wu1M6w=
github.com/blevesearch/zapx/v12 v12.3.9/go.mod h1:QXCMwmOkdLnMDgTN1P4CcuX5F851iUOtOwXbw0HMBYs=
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=
github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs=
github.com/blevesearch/zapx/v13 v13.3.9 h1:+VAz9V0VmllHXlZV4DCvfYj0nqaZHgF3MeEHwOyRBwQ=
github.com/blevesearch/zapx/v13 v13.3.9/go.mod h1:s+WjNp4WSDtrBVBpa37DUOd7S/Gr/jTZ7ST/MbCVj/0=
github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8=
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=
github.com/blevesearch/zapx/v14 v14.3.9 h1:wuqxATgsTCNHM9xsOFOeFp8H2heZ/gMX/tsl9lRK8U4=
github.com/blevesearch/zapx/v14 v14.3.9/go.mod h1:MWZ4v8AzFBRurhDzkLvokFW8ljcq9Evm27mkWe8OGbM=
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
github.com/blevesearch/zapx/v15 v15.3.12 h1:w/kU9aHyfMDEdwHGZzCiakC3HZ9z5gYlXaALDC4Dct8=
github.com/blevesearch/zapx/v15 v15.3.12/go.mod h1:tx53gDJS/7Oa3Je820cmVurqCuJ4dqdAy1kiDMV/IUo=
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
@ -87,6 +115,8 @@ github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764=
@ -117,10 +147,12 @@ github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU=
github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0=
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJLNCEi7YHVMkwwtfSr2k9splgdSM=
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=
github.com/foxxorcat/mopan-sdk-go v0.1.3 h1:6ww0ulyLDh6neXZBqUM2PDbxQ6lfdkQbr0FCh9BTY0Y=
github.com/foxxorcat/mopan-sdk-go v0.1.3/go.mod h1:iWHA2JFhzmKR28ySp1ON0g6DjLaYtvb5jhTqPVTDW9A=
github.com/foxxorcat/mopan-sdk-go v0.1.4 h1:6utvPiBv8KDRDVKB7A4FERdrVxcHKZd2fBFCNuKcXzU=
github.com/foxxorcat/mopan-sdk-go v0.1.4/go.mod h1:iWHA2JFhzmKR28ySp1ON0g6DjLaYtvb5jhTqPVTDW9A=
github.com/foxxorcat/weiyun-sdk-go v0.1.3 h1:I5c5nfGErhq9DBumyjCVCggRA74jhgriMqRRFu5jeeY=
github.com/foxxorcat/weiyun-sdk-go v0.1.3/go.mod h1:TPxzN0d2PahweUEHlOBWlwZSA+rELSUlGYMWgXRn9ps=
github.com/foxxorcat/weiyun-sdk-go v0.1.2 h1:waRWIBmjL9GCcndJ8HvOYrrVB4hhoPYzRrn3I/Cnzqw=
github.com/foxxorcat/weiyun-sdk-go v0.1.2/go.mod h1:AKsLFuWhWlClpGrg1zxTdMejugZEZtmhIuElAk3W83s=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
@ -154,7 +186,10 @@ github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXS
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-resty/resty/v2 v2.8.0 h1:J29d0JFWwSWrDCysnOK/YjsPMLQTx0TvgJEHVGvf2L8=
github.com/go-resty/resty/v2 v2.8.0/go.mod h1:UCui0cMHekLrSntoMyofdSTaPpinlRHFtPpizuyDW2w=
github.com/go-resty/resty/v2 v2.9.1 h1:PIgGx4VrHvag0juCJ4dDv3MiFRlDmP0vicBucwf+gLM=
github.com/go-resty/resty/v2 v2.9.1/go.mod h1:4/GYJVjh9nhkhGR6AUNW3XhpDYNUr+Uvy9gV/VGZIy4=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
@ -174,6 +209,7 @@ github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@ -190,6 +226,8 @@ github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM=
@ -206,10 +244,14 @@ github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRK
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/ipfs/boxo v0.8.0 h1:UdjAJmHzQHo/j3g3b1bAcAXCj/GM6iTwvSlBDvPBNBs=
github.com/ipfs/boxo v0.8.0/go.mod h1:RIsi4CnTyQ7AUsNn5gXljJYZlQrHBMnJp94p73liFiA=
github.com/ipfs/boxo v0.12.0 h1:AXHg/1ONZdRQHQLgG5JHsSC3XoE4DjCAMgK+asZvUcQ=
github.com/ipfs/boxo v0.12.0/go.mod h1:xAnfiU6PtxWCnRqu7dcXQ10bB5/kvI1kXRotuGqGBhg=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/ipfs/go-ipfs-api v0.6.1 h1:nK5oeFOdMh1ogT+GCOcyBFOOcFGNuudSb1rg9YDyAKE=
github.com/ipfs/go-ipfs-api v0.6.1/go.mod h1:8pl+ZMF2LX42szbqGbpOBEiI1/rYaImvTvJtG0g+rL4=
github.com/ipfs/go-ipfs-api v0.7.0 h1:CMBNCUl0b45coC+lQCXEVpMhwoqjiaCwUIrM+coYW2Q=
github.com/ipfs/go-ipfs-api v0.7.0/go.mod h1:AIxsTNB0+ZhkqIfTZpdZ0VR/cpX5zrXjATa3prSay3g=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@ -219,8 +261,6 @@ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZ
github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA=
github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jaevor/go-nanoid v1.3.0 h1:nD+iepesZS6pr3uOVf20vR9GdGgJW1HPaR46gtrxzkg=
github.com/jaevor/go-nanoid v1.3.0/go.mod h1:SI+jFaPuddYkqkVQoNGHs81navCtH388TcrH0RqFKgY=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@ -232,9 +272,11 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC9jFiTxyptEKuNIAbiN5ZCQzX2a74lj3xg=
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
@ -278,6 +320,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
@ -307,6 +351,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
@ -317,30 +363,41 @@ github.com/multiformats/go-multiaddr v0.9.0 h1:3h4V1LHIk5w4hJHekMKWALPXErDfz/sgg
github.com/multiformats/go-multiaddr v0.9.0/go.mod h1:mI67Lb1EeTOYb8GQfL/7wpIZwc46ElrvzhYnoJOmTT0=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.8.1 h1:ycepHwavHafh3grIbR1jIXnKCsFm0fqsfEOsJ8NtKE8=
github.com/multiformats/go-multicodec v0.8.1/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108=
github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo=
github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/ncw/swift/v2 v2.0.2 h1:jx282pcAKFhmoZBSdMcCRFn9VWkoBIRsCpe+yZq7vEk=
github.com/ncw/swift/v2 v2.0.2/go.mod h1:z0A9RVdYPjNjXVo2pDOPxZ4eu3oarO1P91fTItcb+Kg=
github.com/orzogc/fake115uploader v0.3.3-0.20221009101310-08b764073b77 h1:dg/EaaJLPIg4xn2kaZil7Ax3wfoxcFXaBwyOTlcz5AI=
github.com/orzogc/fake115uploader v0.3.3-0.20221009101310-08b764073b77/go.mod h1:FD9a09Vw07CSMTdT0Y7ttStOa1WZsnPBslliMw2DkeM=
github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831 h1:K3T3eu4h5aYIOzUtLjN08L4Qt4WGaJONMgcaD0ayBJQ=
github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831/go.mod h1:lSHD4lC4zlMl+zcoysdJcd5KFzsWwOD8BJbyg1Ws9Ng=
github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6 h1:5TvW1dv00Y13njmQ1AWkxSWtPkwE7ZEF6yDuv9q+Als=
github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
@ -418,6 +475,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/u2takey/ffmpeg-go v0.4.1 h1:l5ClIwL3N2LaH1zF3xivb3kP2HW95eyG5xhHE1JdZ9Y=
github.com/u2takey/ffmpeg-go v0.4.1/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc=
github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uklU=
github.com/u2takey/ffmpeg-go v0.5.0/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc=
github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys=
@ -428,21 +487,11 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/upyun/go-sdk/v3 v3.0.4 h1:2DCJa/Yi7/3ZybT9UCPATSzvU3wpPPxhXinNlb1Hi8Q=
github.com/upyun/go-sdk/v3 v3.0.4/go.mod h1:P/SnuuwhrIgAVRd/ZpzDWqCsBAf/oHg7UggbAxyZa0E=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc=
github.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5 h1:jxZvjx8Ve5sOXorZG0KzTxbp0Cr1n3FEegfmyd9br1k=
github.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5/go.mod h1:uxjoF2jEYT3+x+vC2KJddEGdk/LU8pRowXmyVMHSV5I=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 h1:eDfebW/yfq9DtG9RO3KP7BT2dot2CvJGIvrB0NEoDXI=
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25/go.mod h1:fH4oNm5F9NfI5dLi0oIMtsLNKQOirUDbEMCIBb/7SU0=
github.com/xhofe/tache v0.0.0-20231110075853-2bd4b52dad9b h1:958N/31ioR0QSg6RarX1aqBsfmlOI2JeYiVzxeGdUAA=
github.com/xhofe/tache v0.0.0-20231110075853-2bd4b52dad9b/go.mod h1:1ISbKrHZNMMrXvgCdaFV0Vkc9Wbo7WV1q7Teovm4Huc=
github.com/xhofe/tache v0.0.0-20231119124711-c417893fc267 h1:MC271sH8UHYqr/IDz9PsqTlyD51HyFvxtQRTemwxR9s=
github.com/xhofe/tache v0.0.0-20231119124711-c417893fc267/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ=
github.com/xhofe/tache v0.0.0-20231120064353-a3585a237e25 h1:XZBuEzDB9Kqni/+zAKxl30iOdp80/GavUsCkPMiQMjg=
github.com/xhofe/tache v0.0.0-20231120064353-a3585a237e25/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ=
github.com/xhofe/tache v0.0.0-20231120085916-722855be0521 h1:m7O+xOqQRysjFngMhQ39RzCFdiCouFLvsrV7N2ScbUY=
github.com/xhofe/tache v0.0.0-20231120085916-722855be0521/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
@ -463,12 +512,25 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI=
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M=
golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -484,9 +546,18 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -518,6 +589,9 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -526,6 +600,11 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
@ -537,6 +616,9 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=

61
internal/aria2/add.go Normal file
View File

@ -0,0 +1,61 @@
package aria2
import (
"context"
"fmt"
"path/filepath"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/google/uuid"
"github.com/pkg/errors"
)
func AddURI(ctx context.Context, uri string, dstDirPath string) error {
// check storage
storage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
// check is it could upload
if storage.Config().NoUpload {
return errors.WithStack(errs.UploadNotSupported)
}
// check path is valid
obj, err := op.Get(ctx, storage, dstDirActualPath)
if err != nil {
if !errs.IsObjectNotFound(err) {
return errors.WithMessage(err, "failed get object")
}
} else {
if !obj.IsDir() {
// can't add to a file
return errors.WithStack(errs.NotFolder)
}
}
// call aria2 rpc
tempDir := filepath.Join(conf.Conf.TempDir, "aria2", uuid.NewString())
options := map[string]interface{}{
"dir": tempDir,
}
gid, err := client.AddURI([]string{uri}, options)
if err != nil {
return errors.Wrapf(err, "failed to add uri %s", uri)
}
DownTaskManager.Submit(task.WithCancelCtx(&task.Task[string]{
ID: gid,
Name: fmt.Sprintf("download %s to [%s](%s)", uri, storage.GetStorage().MountPath, dstDirActualPath),
Func: func(tsk *task.Task[string]) error {
m := &Monitor{
tsk: tsk,
tempDir: tempDir,
retried: 0,
dstDirPath: dstDirPath,
}
return m.Loop()
},
}))
return nil
}

42
internal/aria2/aria2.go Normal file
View File

@ -0,0 +1,42 @@
package aria2
import (
"context"
"time"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/aria2/rpc"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
var DownTaskManager = task.NewTaskManager[string](3)
var notify = NewNotify()
var client rpc.Client
func InitClient(timeout int) (string, error) {
client = nil
uri := setting.GetStr(conf.Aria2Uri)
secret := setting.GetStr(conf.Aria2Secret)
return InitAria2Client(uri, secret, timeout)
}
func InitAria2Client(uri string, secret string, timeout int) (string, error) {
c, err := rpc.New(context.Background(), uri, secret, time.Duration(timeout)*time.Second, notify)
if err != nil {
return "", errors.Wrap(err, "failed to init aria2 client")
}
version, err := c.GetVersion()
if err != nil {
return "", errors.Wrapf(err, "failed get aria2 version")
}
client = c
log.Infof("using aria2 version: %s", version.Version)
return version.Version, nil
}
func IsAria2Ready() bool {
return client != nil
}

View File

@ -0,0 +1,87 @@
package aria2
import (
"context"
"path/filepath"
"testing"
"time"
_ "github.com/alist-org/alist/v3/drivers"
conf2 "github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/task"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func init() {
conf2.Conf = conf2.DefaultConfig()
absPath, err := filepath.Abs("../../data/temp")
if err != nil {
panic(err)
}
conf2.Conf.TempDir = absPath
dB, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
db.Init(dB)
}
func TestConnect(t *testing.T) {
_, err := InitAria2Client("http://localhost:16800/jsonrpc", "secret", 3)
if err != nil {
t.Errorf("failed to init aria2: %+v", err)
}
}
func TestDown(t *testing.T) {
TestConnect(t)
_, err := op.CreateStorage(context.Background(), model.Storage{
ID: 0,
MountPath: "/",
Order: 0,
Driver: "Local",
Status: "",
Addition: `{"root_folder":"../../data"}`,
Remark: "",
})
if err != nil {
t.Fatalf("failed to create storage: %+v", err)
}
err = AddURI(context.Background(), "https://nodejs.org/dist/index.json", "/test")
if err != nil {
t.Errorf("failed to add uri: %+v", err)
}
tasks := DownTaskManager.GetAll()
if len(tasks) != 1 {
t.Errorf("failed to get tasks: %+v", tasks)
}
for {
tsk := tasks[0]
t.Logf("task: %+v", tsk)
if tsk.GetState() == task.SUCCEEDED {
break
}
if tsk.GetState() == task.ERRORED {
t.Fatalf("failed to download: %+v", tsk)
}
time.Sleep(time.Second)
}
for {
if len(TransferTaskManager.GetAll()) == 0 {
continue
}
tsk := TransferTaskManager.GetAll()[0]
t.Logf("task: %+v", tsk)
if tsk.GetState() == task.SUCCEEDED {
break
}
if tsk.GetState() == task.ERRORED {
t.Fatalf("failed to download: %+v", tsk)
}
time.Sleep(time.Second)
}
}

192
internal/aria2/monitor.go Normal file
View File

@ -0,0 +1,192 @@
package aria2
import (
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
type Monitor struct {
tsk *task.Task[string]
tempDir string
retried int
c chan int
dstDirPath string
finish chan struct{}
}
func (m *Monitor) Loop() error {
defer func() {
notify.Signals.Delete(m.tsk.ID)
// clear temp dir, should do while complete
//_ = os.RemoveAll(m.tempDir)
}()
m.c = make(chan int)
m.finish = make(chan struct{})
notify.Signals.Store(m.tsk.ID, m.c)
var (
err error
ok bool
)
outer:
for {
select {
case <-m.tsk.Ctx.Done():
_, err := client.Remove(m.tsk.ID)
return err
case <-m.c:
ok, err = m.Update()
if ok {
break outer
}
case <-time.After(time.Second * 2):
ok, err = m.Update()
if ok {
break outer
}
}
}
if err != nil {
return err
}
m.tsk.SetStatus("aria2 download completed, transferring")
<-m.finish
m.tsk.SetStatus("completed")
return nil
}
func (m *Monitor) Update() (bool, error) {
info, err := client.TellStatus(m.tsk.ID)
if err != nil {
m.retried++
log.Errorf("failed to get status of %s, retried %d times", m.tsk.ID, m.retried)
return false, nil
}
if m.retried > 5 {
return true, errors.Errorf("failed to get status of %s, retried %d times", m.tsk.ID, m.retried)
}
m.retried = 0
if len(info.FollowedBy) != 0 {
log.Debugf("followen by: %+v", info.FollowedBy)
gid := info.FollowedBy[0]
notify.Signals.Delete(m.tsk.ID)
oldId := m.tsk.ID
m.tsk.ID = gid
DownTaskManager.RawTasks().Delete(oldId)
DownTaskManager.RawTasks().Store(m.tsk.ID, m.tsk)
notify.Signals.Store(gid, m.c)
return false, nil
}
// update download status
total, err := strconv.ParseUint(info.TotalLength, 10, 64)
if err != nil {
total = 0
}
downloaded, err := strconv.ParseUint(info.CompletedLength, 10, 64)
if err != nil {
downloaded = 0
}
progress := float64(downloaded) / float64(total) * 100
m.tsk.SetProgress(progress)
switch info.Status {
case "complete":
err := m.Complete()
return true, errors.WithMessage(err, "failed to transfer file")
case "error":
return true, errors.Errorf("failed to download %s, error: %s", m.tsk.ID, info.ErrorMessage)
case "active":
m.tsk.SetStatus("aria2: " + info.Status)
if info.Seeder == "true" {
err := m.Complete()
return true, errors.WithMessage(err, "failed to transfer file")
}
return false, nil
case "waiting", "paused":
m.tsk.SetStatus("aria2: " + info.Status)
return false, nil
case "removed":
return true, errors.Errorf("failed to download %s, removed", m.tsk.ID)
default:
return true, errors.Errorf("failed to download %s, unknown status %s", m.tsk.ID, info.Status)
}
}
var TransferTaskManager = task.NewTaskManager(3, func(k *uint64) {
atomic.AddUint64(k, 1)
})
func (m *Monitor) Complete() error {
// check dstDir again
storage, dstDirActualPath, err := op.GetStorageAndActualPath(m.dstDirPath)
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
// get files
files, err := client.GetFiles(m.tsk.ID)
log.Debugf("files len: %d", len(files))
if err != nil {
return errors.Wrapf(err, "failed to get files of %s", m.tsk.ID)
}
// upload files
var wg sync.WaitGroup
wg.Add(len(files))
go func() {
wg.Wait()
err := os.RemoveAll(m.tempDir)
m.finish <- struct{}{}
if err != nil {
log.Errorf("failed to remove aria2 temp dir: %+v", err.Error())
}
}()
for i, _ := range files {
file := files[i]
TransferTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
Name: fmt.Sprintf("transfer %s to [%s](%s)", file.Path, storage.GetStorage().MountPath, dstDirActualPath),
Func: func(tsk *task.Task[uint64]) error {
defer wg.Done()
size, _ := strconv.ParseInt(file.Length, 10, 64)
mimetype := utils.GetMimeType(file.Path)
f, err := os.Open(file.Path)
if err != nil {
return errors.Wrapf(err, "failed to open file %s", file.Path)
}
s := stream.FileStream{
Obj: &model.Object{
Name: path.Base(file.Path),
Size: size,
Modified: time.Now(),
IsFolder: false,
},
Reader: f,
Closers: utils.NewClosers(f),
Mimetype: mimetype,
}
ss, err := stream.NewSeekableStream(s, nil)
if err != nil {
return err
}
relDir, err := filepath.Rel(m.tempDir, filepath.Dir(file.Path))
if err != nil {
log.Errorf("find relation directory error: %v", err)
}
newDistDir := filepath.Join(dstDirActualPath, relDir)
return op.Put(tsk.Ctx, storage, newDistDir, ss, tsk.SetProgress)
},
}))
}
return nil
}

70
internal/aria2/notify.go Normal file
View File

@ -0,0 +1,70 @@
package aria2
import (
"github.com/alist-org/alist/v3/pkg/aria2/rpc"
"github.com/alist-org/alist/v3/pkg/generic_sync"
)
const (
Downloading = iota
Paused
Stopped
Completed
Errored
)
type Notify struct {
Signals generic_sync.MapOf[string, chan int]
}
func NewNotify() *Notify {
return &Notify{Signals: generic_sync.MapOf[string, chan int]{}}
}
func (n *Notify) OnDownloadStart(events []rpc.Event) {
for _, e := range events {
if signal, ok := n.Signals.Load(e.Gid); ok {
signal <- Downloading
}
}
}
func (n *Notify) OnDownloadPause(events []rpc.Event) {
for _, e := range events {
if signal, ok := n.Signals.Load(e.Gid); ok {
signal <- Paused
}
}
}
func (n *Notify) OnDownloadStop(events []rpc.Event) {
for _, e := range events {
if signal, ok := n.Signals.Load(e.Gid); ok {
signal <- Stopped
}
}
}
func (n *Notify) OnDownloadComplete(events []rpc.Event) {
for _, e := range events {
if signal, ok := n.Signals.Load(e.Gid); ok {
signal <- Completed
}
}
}
func (n *Notify) OnDownloadError(events []rpc.Event) {
for _, e := range events {
if signal, ok := n.Signals.Load(e.Gid); ok {
signal <- Errored
}
}
}
func (n *Notify) OnBtDownloadComplete(events []rpc.Event) {
for _, e := range events {
if signal, ok := n.Signals.Load(e.Gid); ok {
signal <- Completed
}
}
}

View File

@ -1,15 +0,0 @@
package bootstrap
import (
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/offline_download/tool"
"github.com/xhofe/tache"
)
func InitTaskManager() {
fs.UploadTaskManager = tache.NewManager[*fs.UploadTask](tache.WithWorks(conf.Conf.Tasks.Upload.Workers), tache.WithMaxRetry(conf.Conf.Tasks.Upload.MaxRetry))
fs.CopyTaskManager = tache.NewManager[*fs.CopyTask](tache.WithWorks(conf.Conf.Tasks.Copy.Workers), tache.WithMaxRetry(conf.Conf.Tasks.Copy.MaxRetry))
tool.DownloadTaskManager = tache.NewManager[*tool.DownloadTask](tache.WithWorks(conf.Conf.Tasks.Download.Workers), tache.WithMaxRetry(conf.Conf.Tasks.Download.MaxRetry))
tool.TransferTaskManager = tache.NewManager[*tool.TransferTask](tache.WithWorks(conf.Conf.Tasks.Transfer.Workers), tache.WithMaxRetry(conf.Conf.Tasks.Transfer.MaxRetry))
}

View File

@ -8,15 +8,15 @@ import (
)
type Database struct {
Type string `json:"type" env:"TYPE"`
Host string `json:"host" env:"HOST"`
Port int `json:"port" env:"PORT"`
User string `json:"user" env:"USER"`
Password string `json:"password" env:"PASS"`
Name string `json:"name" env:"NAME"`
DBFile string `json:"db_file" env:"FILE"`
TablePrefix string `json:"table_prefix" env:"TABLE_PREFIX"`
SSLMode string `json:"ssl_mode" env:"SSL_MODE"`
Type string `json:"type" env:"DB_TYPE"`
Host string `json:"host" env:"DB_HOST"`
Port int `json:"port" env:"DB_PORT"`
User string `json:"user" env:"DB_USER"`
Password string `json:"password" env:"DB_PASS"`
Name string `json:"name" env:"DB_NAME"`
DBFile string `json:"db_file" env:"DB_FILE"`
TablePrefix string `json:"table_prefix" env:"DB_TABLE_PREFIX"`
SSLMode string `json:"ssl_mode" env:"DB_SSL_MODE"`
}
type Scheme struct {
@ -39,41 +39,20 @@ type LogConfig struct {
Compress bool `json:"compress" env:"COMPRESS"`
}
type TaskConfig struct {
Workers int `json:"workers" env:"WORKERS"`
MaxRetry int `json:"max_retry" env:"MAX_RETRY"`
}
type TasksConfig struct {
Download TaskConfig `json:"download" envPrefix:"DOWNLOAD_"`
Transfer TaskConfig `json:"transfer" envPrefix:"TRANSFER_"`
Upload TaskConfig `json:"upload" envPrefix:"UPLOAD_"`
Copy TaskConfig `json:"copy" envPrefix:"COPY_"`
}
type Cors struct {
AllowOrigins []string `json:"allow_origins" env:"ALLOW_ORIGINS"`
AllowMethods []string `json:"allow_methods" env:"ALLOW_METHODS"`
AllowHeaders []string `json:"allow_headers" env:"ALLOW_HEADERS"`
}
type Config struct {
Force bool `json:"force" env:"FORCE"`
SiteURL string `json:"site_url" env:"SITE_URL"`
Cdn string `json:"cdn" env:"CDN"`
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
TokenExpiresIn int `json:"token_expires_in" env:"TOKEN_EXPIRES_IN"`
Database Database `json:"database" envPrefix:"DB_"`
Scheme Scheme `json:"scheme"`
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
BleveDir string `json:"bleve_dir" env:"BLEVE_DIR"`
DistDir string `json:"dist_dir"`
Log LogConfig `json:"log"`
DelayedStart int `json:"delayed_start" env:"DELAYED_START"`
MaxConnections int `json:"max_connections" env:"MAX_CONNECTIONS"`
TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" env:"TLS_INSECURE_SKIP_VERIFY"`
Tasks TasksConfig `json:"tasks" envPrefix:"TASKS_"`
Cors Cors `json:"cors" envPrefix:"CORS_"`
Force bool `json:"force" env:"FORCE"`
SiteURL string `json:"site_url" env:"SITE_URL"`
Cdn string `json:"cdn" env:"CDN"`
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
TokenExpiresIn int `json:"token_expires_in" env:"TOKEN_EXPIRES_IN"`
Database Database `json:"database"`
Scheme Scheme `json:"scheme"`
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
BleveDir string `json:"bleve_dir" env:"BLEVE_DIR"`
Log LogConfig `json:"log"`
DelayedStart int `json:"delayed_start" env:"DELAYED_START"`
MaxConnections int `json:"max_connections" env:"MAX_CONNECTIONS"`
TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" env:"TLS_INSECURE_SKIP_VERIFY"`
}
func DefaultConfig() *Config {
@ -110,27 +89,5 @@ func DefaultConfig() *Config {
},
MaxConnections: 0,
TlsInsecureSkipVerify: true,
Tasks: TasksConfig{
Download: TaskConfig{
Workers: 5,
MaxRetry: 1,
},
Transfer: TaskConfig{
Workers: 5,
MaxRetry: 2,
},
Upload: TaskConfig{
Workers: 5,
},
Copy: TaskConfig{
Workers: 5,
MaxRetry: 2,
},
},
Cors: Cors{
AllowOrigins: []string{"*"},
AllowMethods: []string{"*"},
AllowHeaders: []string{"*"},
},
}
}

View File

@ -11,7 +11,7 @@ type Config struct {
DefaultRoot string `json:"default_root"`
CheckStatus bool `json:"-"`
Alert string `json:"alert"` //info,success,warning,danger
NoOverwriteUpload bool `json:"-"` // whether to support overwrite upload
NoOverwriteUpload bool `json:"-"`
}
func (c Config) MustProxy() bool {

View File

@ -29,7 +29,3 @@ func NewErr(err error, format string, a ...any) error {
func IsNotFoundError(err error) bool {
return errors.Is(pkgerr.Cause(err), ObjectNotFound) || errors.Is(pkgerr.Cause(err), StorageNotFound)
}
func IsNotSupportError(err error) bool {
return errors.Is(pkgerr.Cause(err), NotSupport)
}

View File

@ -3,39 +3,24 @@ package fs
import (
"context"
"fmt"
"net/http"
stdpath "path"
"sync/atomic"
"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/stream"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
"github.com/xhofe/tache"
"net/http"
stdpath "path"
log "github.com/sirupsen/logrus"
)
type CopyTask struct {
tache.Base
Status string `json:"status"`
srcStorage, dstStorage driver.Driver
srcObjPath, dstDirPath string
}
func (t *CopyTask) GetName() string {
return fmt.Sprintf("copy [%s](%s) to [%s](%s)",
t.srcStorage.GetStorage().MountPath, t.srcObjPath, t.dstStorage.GetStorage().MountPath, t.dstDirPath)
}
func (t *CopyTask) GetStatus() string {
return t.Status
}
func (t *CopyTask) Run() error {
return copyBetween2Storages(t, t.srcStorage, t.dstStorage, t.srcObjPath, t.dstDirPath)
}
var CopyTaskManager *tache.Manager[*CopyTask]
var CopyTaskManager = task.NewTaskManager(3, func(tid *uint64) {
atomic.AddUint64(tid, 1)
})
// Copy if in the same storage, call move method
// if not, add copy task
@ -78,52 +63,59 @@ func _copy(ctx context.Context, srcObjPath, dstDirPath string, lazyCache ...bool
}
}
// not in the same storage
CopyTaskManager.Add(&CopyTask{
srcStorage: srcStorage,
dstStorage: dstStorage,
srcObjPath: srcObjActualPath,
dstDirPath: dstDirActualPath,
})
CopyTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
Name: fmt.Sprintf("copy [%s](%s) to [%s](%s)", srcStorage.GetStorage().MountPath, srcObjActualPath, dstStorage.GetStorage().MountPath, dstDirActualPath),
Func: func(task *task.Task[uint64]) error {
return copyBetween2Storages(task, srcStorage, dstStorage, srcObjActualPath, dstDirActualPath)
},
}))
return true, nil
}
func copyBetween2Storages(t *CopyTask, srcStorage, dstStorage driver.Driver, srcObjPath, dstDirPath string) error {
t.Status = "getting src object"
srcObj, err := op.Get(t.Ctx(), srcStorage, srcObjPath)
func copyBetween2Storages(t *task.Task[uint64], srcStorage, dstStorage driver.Driver, srcObjPath, dstDirPath string) error {
t.SetStatus("getting src object")
srcObj, err := op.Get(t.Ctx, srcStorage, srcObjPath)
if err != nil {
return errors.WithMessagef(err, "failed get src [%s] file", srcObjPath)
}
if srcObj.IsDir() {
t.Status = "src object is dir, listing objs"
objs, err := op.List(t.Ctx(), srcStorage, srcObjPath, model.ListArgs{})
t.SetStatus("src object is dir, listing objs")
objs, err := op.List(t.Ctx, srcStorage, srcObjPath, model.ListArgs{})
if err != nil {
return errors.WithMessagef(err, "failed list src [%s] objs", srcObjPath)
}
for _, obj := range objs {
if utils.IsCanceled(t.Ctx()) {
if utils.IsCanceled(t.Ctx) {
return nil
}
srcObjPath := stdpath.Join(srcObjPath, obj.GetName())
dstObjPath := stdpath.Join(dstDirPath, srcObj.GetName())
CopyTaskManager.Add(&CopyTask{
srcStorage: srcStorage,
dstStorage: dstStorage,
srcObjPath: srcObjPath,
dstDirPath: dstObjPath,
})
CopyTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
Name: fmt.Sprintf("copy [%s](%s) to [%s](%s)", srcStorage.GetStorage().MountPath, srcObjPath, dstStorage.GetStorage().MountPath, dstObjPath),
Func: func(t *task.Task[uint64]) error {
return copyBetween2Storages(t, srcStorage, dstStorage, srcObjPath, dstObjPath)
},
}))
}
t.Status = "src object is dir, added all copy tasks of objs"
return nil
} else {
CopyTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
Name: fmt.Sprintf("copy [%s](%s) to [%s](%s)", srcStorage.GetStorage().MountPath, srcObjPath, dstStorage.GetStorage().MountPath, dstDirPath),
Func: func(t *task.Task[uint64]) error {
err := copyFileBetween2Storages(t, srcStorage, dstStorage, srcObjPath, dstDirPath)
log.Debugf("copy file between storages: %+v", err)
return err
},
}))
}
return copyFileBetween2Storages(t, srcStorage, dstStorage, srcObjPath, dstDirPath)
return nil
}
func copyFileBetween2Storages(tsk *CopyTask, srcStorage, dstStorage driver.Driver, srcFilePath, dstDirPath string) error {
srcFile, err := op.Get(tsk.Ctx(), srcStorage, srcFilePath)
func copyFileBetween2Storages(tsk *task.Task[uint64], srcStorage, dstStorage driver.Driver, srcFilePath, dstDirPath string) error {
srcFile, err := op.Get(tsk.Ctx, srcStorage, srcFilePath)
if err != nil {
return errors.WithMessagef(err, "failed get src [%s] file", srcFilePath)
}
link, _, err := op.Link(tsk.Ctx(), srcStorage, srcFilePath, model.LinkArgs{
link, _, err := op.Link(tsk.Ctx, srcStorage, srcFilePath, model.LinkArgs{
Header: http.Header{},
})
if err != nil {
@ -131,12 +123,12 @@ func copyFileBetween2Storages(tsk *CopyTask, srcStorage, dstStorage driver.Drive
}
fs := stream.FileStream{
Obj: srcFile,
Ctx: tsk.Ctx(),
Ctx: tsk.Ctx,
}
// any link provided is seekable
ss, err := stream.NewSeekableStream(fs, link)
if err != nil {
return errors.WithMessagef(err, "failed get [%s] stream", srcFilePath)
}
return op.Put(tsk.Ctx(), dstStorage, dstDirPath, ss, tsk.SetProgress, true)
return op.Put(tsk.Ctx, dstStorage, dstDirPath, ss, tsk.SetProgress, true)
}

View File

@ -3,34 +3,18 @@ package fs
import (
"context"
"fmt"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"sync/atomic"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/pkg/errors"
"github.com/xhofe/tache"
)
type UploadTask struct {
tache.Base
storage driver.Driver
dstDirActualPath string
file model.FileStreamer
}
func (t *UploadTask) GetName() string {
return fmt.Sprintf("upload %s to [%s](%s)", t.file.GetName(), t.storage.GetStorage().MountPath, t.dstDirActualPath)
}
func (t *UploadTask) GetStatus() string {
return "uploading"
}
func (t *UploadTask) Run() error {
return op.Put(t.Ctx(), t.storage, t.dstDirActualPath, t.file, t.SetProgress, true)
}
var UploadTaskManager *tache.Manager[*UploadTask]
var UploadTaskManager = task.NewTaskManager(3, func(tid *uint64) {
atomic.AddUint64(tid, 1)
})
// putAsTask add as a put task and return immediately
func putAsTask(dstDirPath string, file model.FileStreamer) error {
@ -49,11 +33,12 @@ func putAsTask(dstDirPath string, file model.FileStreamer) error {
//file.SetReader(tempFile)
//file.SetTmpFile(tempFile)
}
UploadTaskManager.Add(&UploadTask{
storage: storage,
dstDirActualPath: dstDirActualPath,
file: file,
})
UploadTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
Name: fmt.Sprintf("upload %s to [%s](%s)", file.GetName(), storage.GetStorage().MountPath, dstDirActualPath),
Func: func(task *task.Task[uint64]) error {
return op.Put(task.Ctx, storage, dstDirActualPath, file, task.SetProgress, true)
},
}))
return nil
}

View File

@ -4,7 +4,6 @@ import (
"encoding/binary"
"encoding/json"
"fmt"
"time"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/pkg/utils"
@ -25,7 +24,6 @@ type User struct {
ID uint `json:"id" gorm:"primaryKey"` // unique key
Username string `json:"username" gorm:"unique" binding:"required"` // username
PwdHash string `json:"-"` // password hash
PwdTS int64 `json:"-"` // password timestamp
Salt string `json:"-"` // unique salt
Password string `json:"password"` // password
BasePath string `json:"base_path"` // base path
@ -73,7 +71,6 @@ func (u *User) ValidatePwdStaticHash(pwdStaticHash string) error {
func (u *User) SetPassword(pwd string) *User {
u.Salt = random.String(16)
u.PwdHash = TwoHashPwd(pwd, u.Salt)
u.PwdTS = time.Now().Unix()
return u
}

View File

@ -8,6 +8,7 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"sync"
"testing"
@ -168,7 +169,7 @@ func newDownloadRangeClient(data []byte) (*downloadCaptureClient, *int, *[]strin
header := &http.Header{}
header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, fin-1, len(data)))
return &http.Response{
Body: io.NopCloser(bytes.NewReader(bodyBytes)),
Body: ioutil.NopCloser(bytes.NewReader(bodyBytes)),
Header: *header,
ContentLength: int64(len(bodyBytes)),
}, nil

View File

@ -2,6 +2,5 @@ package offline_download
import (
_ "github.com/alist-org/alist/v3/internal/offline_download/aria2"
_ "github.com/alist-org/alist/v3/internal/offline_download/http"
_ "github.com/alist-org/alist/v3/internal/offline_download/qbit"
)

View File

@ -3,7 +3,6 @@ package aria2
import (
"context"
"fmt"
"github.com/alist-org/alist/v3/internal/errs"
"strconv"
"time"
@ -22,14 +21,6 @@ type Aria2 struct {
client rpc.Client
}
func (a *Aria2) Run(task *tool.DownloadTask) error {
return errs.NotSupport
}
func (a *Aria2) Name() string {
return "aria2"
}
func (a *Aria2) Items() []model.SettingItem {
// aria2 settings
return []model.SettingItem{
@ -67,17 +58,16 @@ func (a *Aria2) AddURL(args *tool.AddUrlArgs) (string, error) {
if err != nil {
return "", err
}
notify.Signals.Store(gid, args.Signal)
return gid, nil
}
func (a *Aria2) Remove(task *tool.DownloadTask) error {
_, err := a.client.Remove(task.GID)
func (a *Aria2) Remove(tid string) error {
_, err := a.client.Remove(tid)
return err
}
func (a *Aria2) Status(task *tool.DownloadTask) (*tool.Status, error) {
info, err := a.client.TellStatus(task.GID)
func (a *Aria2) Status(tid string) (*tool.Status, error) {
info, err := a.client.TellStatus(tid)
if err != nil {
return nil, err
}
@ -95,15 +85,15 @@ func (a *Aria2) Status(task *tool.DownloadTask) (*tool.Status, error) {
}
s.Progress = float64(downloaded) / float64(total) * 100
if len(info.FollowedBy) != 0 {
s.NewGID = info.FollowedBy[0]
notify.Signals.Delete(task.GID)
notify.Signals.Store(s.NewGID, task.Signal)
s.NewTID = info.FollowedBy[0]
notify.Signals.Delete(tid)
//notify.Signals.Store(gid, m.c)
}
switch info.Status {
case "complete":
s.Completed = true
case "error":
s.Err = errors.Errorf("failed to download %s, error: %s", task.GID, info.ErrorMessage)
s.Err = errors.Errorf("failed to download %s, error: %s", tid, info.ErrorMessage)
case "active":
s.Status = "aria2: " + info.Status
if info.Seeder == "true" {
@ -112,15 +102,32 @@ func (a *Aria2) Status(task *tool.DownloadTask) (*tool.Status, error) {
case "waiting", "paused":
s.Status = "aria2: " + info.Status
case "removed":
s.Err = errors.Errorf("failed to download %s, removed", task.GID)
s.Err = errors.Errorf("failed to download %s, removed", tid)
default:
return nil, errors.Errorf("[aria2] unknown status %s", info.Status)
}
return s, nil
}
func (a *Aria2) GetFiles(tid string) []tool.File {
//files, err := a.client.GetFiles(tid)
//if err != nil {
// return nil
//}
//return utils.MustSliceConvert(files, func(f rpc.FileInfo) tool.File {
// return tool.File{
// //ReadCloser: nil,
// Name: path.Base(f.Path),
// Size: f.Length,
// Path: "",
// Modified: time.Time{},
// }
//})
return nil
}
var _ tool.Tool = (*Aria2)(nil)
func init() {
tool.Tools.Add(&Aria2{})
tool.Tools.Add("aria2", &Aria2{})
}

View File

@ -1,85 +0,0 @@
package http
import (
"fmt"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/offline_download/tool"
"github.com/alist-org/alist/v3/pkg/utils"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
)
type SimpleHttp struct {
client http.Client
}
func (s SimpleHttp) Name() string {
return "SimpleHttp"
}
func (s SimpleHttp) Items() []model.SettingItem {
return nil
}
func (s SimpleHttp) Init() (string, error) {
return "ok", nil
}
func (s SimpleHttp) IsReady() bool {
return true
}
func (s SimpleHttp) AddURL(args *tool.AddUrlArgs) (string, error) {
panic("should not be called")
}
func (s SimpleHttp) Remove(task *tool.DownloadTask) error {
panic("should not be called")
}
func (s SimpleHttp) Status(task *tool.DownloadTask) (*tool.Status, error) {
panic("should not be called")
}
func (s SimpleHttp) Run(task *tool.DownloadTask) error {
u := task.Url
// parse url
_u, err := url.Parse(u)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(task.Ctx(), http.MethodGet, u, nil)
if err != nil {
return err
}
resp, err := s.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("http status code %d", resp.StatusCode)
}
filename := path.Base(_u.Path)
if n, err := parseFilenameFromContentDisposition(resp.Header.Get("Content-Disposition")); err == nil {
filename = n
}
// save to temp dir
_ = os.MkdirAll(task.TempDir, os.ModePerm)
filePath := filepath.Join(task.TempDir, filename)
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()
fileSize := resp.ContentLength
err = utils.CopyWithCtx(task.Ctx(), file, resp.Body, fileSize, task.SetProgress)
return err
}
func init() {
tool.Tools.Add(&SimpleHttp{})
}

View File

@ -1,21 +0,0 @@
package http
import (
"fmt"
"mime"
)
func parseFilenameFromContentDisposition(contentDisposition string) (string, error) {
if contentDisposition == "" {
return "", fmt.Errorf("Content-Disposition is empty")
}
_, params, err := mime.ParseMediaType(contentDisposition)
if err != nil {
return "", err
}
filename := params["filename"]
if filename == "" {
return "", fmt.Errorf("filename not found in Content-Disposition: [%s]", contentDisposition)
}
return filename, nil
}

View File

@ -2,11 +2,10 @@ package qbit
import (
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/offline_download/tool"
"github.com/alist-org/alist/v3/internal/qbittorrent"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/qbittorrent"
"github.com/pkg/errors"
)
@ -14,14 +13,6 @@ type QBittorrent struct {
client qbittorrent.Client
}
func (a *QBittorrent) Run(task *tool.DownloadTask) error {
return errs.NotSupport
}
func (a *QBittorrent) Name() string {
return "qBittorrent"
}
func (a *QBittorrent) Items() []model.SettingItem {
// qBittorrent settings
return []model.SettingItem{
@ -53,13 +44,13 @@ func (a *QBittorrent) AddURL(args *tool.AddUrlArgs) (string, error) {
return args.UID, nil
}
func (a *QBittorrent) Remove(task *tool.DownloadTask) error {
err := a.client.Delete(task.GID, true)
func (a *QBittorrent) Remove(tid string) error {
err := a.client.Delete(tid, true)
return err
}
func (a *QBittorrent) Status(task *tool.DownloadTask) (*tool.Status, error) {
info, err := a.client.GetInfo(task.GID)
func (a *QBittorrent) Status(tid string) (*tool.Status, error) {
info, err := a.client.GetInfo(tid)
if err != nil {
return nil, err
}
@ -71,15 +62,19 @@ func (a *QBittorrent) Status(task *tool.DownloadTask) (*tool.Status, error) {
case qbittorrent.ALLOCATING, qbittorrent.DOWNLOADING, qbittorrent.METADL, qbittorrent.PAUSEDDL, qbittorrent.QUEUEDDL, qbittorrent.STALLEDDL, qbittorrent.CHECKINGDL, qbittorrent.FORCEDDL, qbittorrent.CHECKINGRESUMEDATA, qbittorrent.MOVING:
s.Status = "[qBittorrent] downloading"
case qbittorrent.ERROR, qbittorrent.MISSINGFILES, qbittorrent.UNKNOWN:
s.Err = errors.Errorf("[qBittorrent] failed to download %s, error: %s", task.GID, info.State)
s.Err = errors.Errorf("[qBittorrent] failed to download %s, error: %s", tid, info.State)
default:
s.Err = errors.Errorf("[qBittorrent] unknown error occurred downloading %s", task.GID)
s.Err = errors.Errorf("[qBittorrent] unknown error occurred downloading %s", tid)
}
return s, nil
}
func (a *QBittorrent) GetFiles(tid string) []tool.File {
return nil
}
var _ tool.Tool = (*QBittorrent)(nil)
func init() {
tool.Tools.Add(&QBittorrent{})
tool.Tools.Add("qBittorrent", &QBittorrent{})
}

View File

@ -2,28 +2,21 @@ package tool
import (
"context"
"fmt"
"path/filepath"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/google/uuid"
"github.com/pkg/errors"
"path/filepath"
)
type DeletePolicy string
const (
DeleteOnUploadSucceed DeletePolicy = "delete_on_upload_succeed"
DeleteOnUploadFailed DeletePolicy = "delete_on_upload_failed"
DeleteNever DeletePolicy = "delete_never"
DeleteAlways DeletePolicy = "delete_always"
)
type AddURLArgs struct {
URL string
DstDirPath string
Tool string
DeletePolicy DeletePolicy
URL string
DstDirPath string
Tool string
}
func AddURL(ctx context.Context, args *AddURLArgs) error {
@ -63,13 +56,29 @@ func AddURL(ctx context.Context, args *AddURLArgs) error {
uid := uuid.NewString()
tempDir := filepath.Join(conf.Conf.TempDir, args.Tool, uid)
t := &DownloadTask{
Url: args.URL,
DstDirPath: args.DstDirPath,
TempDir: tempDir,
DeletePolicy: args.DeletePolicy,
tool: tool,
signal := make(chan int)
gid, err := tool.AddURL(&AddUrlArgs{
Url: args.URL,
UID: uid,
TempDir: tempDir,
Signal: signal,
})
if err != nil {
return errors.Wrapf(err, "[%s] failed to add uri %s", args.Tool, args.URL)
}
DownloadTaskManager.Add(t)
DownTaskManager.Submit(task.WithCancelCtx(&task.Task[string]{
ID: gid,
Name: fmt.Sprintf("download %s to [%s](%s)", args.URL, storage.GetStorage().MountPath, dstDirActualPath),
Func: func(tsk *task.Task[string]) error {
m := &Monitor{
tool: tool,
tsk: tsk,
tempDir: tempDir,
dstDirPath: args.DstDirPath,
signal: signal,
}
return m.Loop()
},
}))
return nil
}

View File

@ -17,14 +17,13 @@ type AddUrlArgs struct {
type Status struct {
Progress float64
NewGID string
NewTID string
Completed bool
Status string
Err error
}
type Tool interface {
Name() string
// Items return the setting items the tool need
Items() []model.SettingItem
Init() (string, error)
@ -32,26 +31,20 @@ type Tool interface {
// AddURL add an uri to download, return the task id
AddURL(args *AddUrlArgs) (string, error)
// Remove the download if task been canceled
Remove(task *DownloadTask) error
Remove(tid string) error
// Status return the status of the download task, if an error occurred, return the error in Status.Err
Status(task *DownloadTask) (*Status, error)
// Run for simple http download
Run(task *DownloadTask) error
}
type GetFileser interface {
Status(tid string) (*Status, error)
// GetFiles return the files of the download task, if nil, means walk the temp dir to get the files
GetFiles(task *DownloadTask) []File
GetFiles(tid string) []File
}
type File struct {
// ReadCloser for http client
ReadCloser io.ReadCloser
Name string
Size int64
Path string
Modified time.Time
io.ReadCloser
Name string
Size int64
Path string
Modified time.Time
}
func (f *File) GetReadCloser() (io.ReadCloser, error) {

View File

@ -1,155 +0,0 @@
package tool
import (
"fmt"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/xhofe/tache"
"sync"
"time"
)
type DownloadTask struct {
tache.Base
Url string `json:"url"`
DstDirPath string `json:"dst_dir_path"`
TempDir string `json:"temp_dir"`
DeletePolicy DeletePolicy `json:"delete_policy"`
Status string `json:"status"`
Signal chan int `json:"-"`
GID string `json:"-"`
finish chan struct{}
tool Tool
callStatusRetried int
}
func (t *DownloadTask) Run() error {
if err := t.tool.Run(t); !errs.IsNotSupportError(err) {
if err == nil {
return t.Complete()
}
return err
}
t.Signal = make(chan int)
t.finish = make(chan struct{})
defer func() {
t.Signal = nil
t.finish = nil
}()
gid, err := t.tool.AddURL(&AddUrlArgs{
Url: t.Url,
UID: t.ID,
TempDir: t.TempDir,
Signal: t.Signal,
})
if err != nil {
return err
}
t.GID = gid
var (
ok bool
)
outer:
for {
select {
case <-t.CtxDone():
err := t.tool.Remove(t)
return err
case <-t.Signal:
ok, err = t.Update()
if ok {
break outer
}
case <-time.After(time.Second * 3):
ok, err = t.Update()
if ok {
break outer
}
}
}
if err != nil {
return err
}
t.Status = "aria2 download completed, maybe transferring"
t.finish <- struct{}{}
t.Status = "offline download completed"
return nil
}
// Update download status, return true if download completed
func (t *DownloadTask) Update() (bool, error) {
info, err := t.tool.Status(t)
if err != nil {
t.callStatusRetried++
log.Errorf("failed to get status of %s, retried %d times", t.ID, t.callStatusRetried)
return false, nil
}
if t.callStatusRetried > 5 {
return true, errors.Errorf("failed to get status of %s, retried %d times", t.ID, t.callStatusRetried)
}
t.callStatusRetried = 0
t.SetProgress(info.Progress)
t.Status = fmt.Sprintf("[%s]: %s", t.tool.Name(), info.Status)
if info.NewGID != "" {
log.Debugf("followen by: %+v", info.NewGID)
t.GID = info.NewGID
return false, nil
}
// if download completed
if info.Completed {
err := t.Complete()
return true, errors.WithMessage(err, "failed to transfer file")
}
// if download failed
if info.Err != nil {
return true, errors.Errorf("failed to download %s, error: %s", t.ID, info.Err.Error())
}
return false, nil
}
func (t *DownloadTask) Complete() error {
var (
files []File
err error
)
if getFileser, ok := t.tool.(GetFileser); ok {
files = getFileser.GetFiles(t)
} else {
files, err = GetFiles(t.TempDir)
if err != nil {
return errors.Wrapf(err, "failed to get files")
}
}
// upload files
var wg sync.WaitGroup
wg.Add(len(files))
go func() {
wg.Wait()
t.finish <- struct{}{}
}()
for i, _ := range files {
file := files[i]
TransferTaskManager.Add(&TransferTask{
file: file,
dstDirPath: t.DstDirPath,
wg: &wg,
tempDir: t.TempDir,
deletePolicy: t.DeletePolicy,
})
}
return nil
}
func (t *DownloadTask) GetName() string {
return fmt.Sprintf("download %s to (%s)", t.Url, t.DstDirPath)
}
func (t *DownloadTask) GetStatus() string {
return t.Status
}
var (
DownloadTaskManager *tache.Manager[*DownloadTask]
)

View File

@ -0,0 +1,159 @@
package tool
import (
"fmt"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
"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/task"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
type Monitor struct {
tool Tool
tsk *task.Task[string]
tempDir string
retried int
dstDirPath string
finish chan struct{}
signal chan int
}
func (m *Monitor) Loop() error {
m.finish = make(chan struct{})
var (
err error
ok bool
)
outer:
for {
select {
case <-m.tsk.Ctx.Done():
err := m.tool.Remove(m.tsk.ID)
return err
case <-m.signal:
ok, err = m.Update()
if ok {
break outer
}
case <-time.After(time.Second * 2):
ok, err = m.Update()
if ok {
break outer
}
}
}
if err != nil {
return err
}
m.tsk.SetStatus("aria2 download completed, transferring")
<-m.finish
m.tsk.SetStatus("completed")
return nil
}
// Update download status, return true if download completed
func (m *Monitor) Update() (bool, error) {
info, err := m.tool.Status(m.tsk.ID)
if err != nil {
m.retried++
log.Errorf("failed to get status of %s, retried %d times", m.tsk.ID, m.retried)
return false, nil
}
if m.retried > 5 {
return true, errors.Errorf("failed to get status of %s, retried %d times", m.tsk.ID, m.retried)
}
m.retried = 0
m.tsk.SetProgress(info.Progress)
m.tsk.SetStatus("tool: " + info.Status)
if info.NewTID != "" {
log.Debugf("followen by: %+v", info.NewTID)
DownTaskManager.RawTasks().Delete(m.tsk.ID)
m.tsk.ID = info.NewTID
DownTaskManager.RawTasks().Store(m.tsk.ID, m.tsk)
return false, nil
}
// if download completed
if info.Completed {
err := m.Complete()
return true, errors.WithMessage(err, "failed to transfer file")
}
// if download failed
if info.Err != nil {
return true, errors.Errorf("failed to download %s, error: %s", m.tsk.ID, info.Err.Error())
}
return false, nil
}
var TransferTaskManager = task.NewTaskManager(3, func(k *uint64) {
atomic.AddUint64(k, 1)
})
func (m *Monitor) Complete() error {
// check dstDir again
storage, dstDirActualPath, err := op.GetStorageAndActualPath(m.dstDirPath)
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
var files []File
if f := m.tool.GetFiles(m.tsk.ID); f != nil {
files = f
} else {
files, err = GetFiles(m.tempDir)
if err != nil {
return errors.Wrapf(err, "failed to get files")
}
}
// upload files
var wg sync.WaitGroup
wg.Add(len(files))
go func() {
wg.Wait()
err := os.RemoveAll(m.tempDir)
m.finish <- struct{}{}
if err != nil {
log.Errorf("failed to remove aria2 temp dir: %+v", err.Error())
}
}()
for i, _ := range files {
file := files[i]
TransferTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
Name: fmt.Sprintf("transfer %s to [%s](%s)", file.Path, storage.GetStorage().MountPath, dstDirActualPath),
Func: func(tsk *task.Task[uint64]) error {
defer wg.Done()
mimetype := utils.GetMimeType(file.Path)
rc, err := file.GetReadCloser()
if err != nil {
return errors.Wrapf(err, "failed to open file %s", file.Path)
}
s := &stream.FileStream{
Ctx: nil,
Obj: &model.Object{
Name: filepath.Base(file.Path),
Size: file.Size,
Modified: file.Modified,
IsFolder: false,
},
Reader: rc,
Mimetype: mimetype,
Closers: utils.NewClosers(rc),
}
relDir, err := filepath.Rel(m.tempDir, filepath.Dir(file.Path))
if err != nil {
log.Errorf("find relation directory error: %v", err)
}
newDistDir := filepath.Join(dstDirActualPath, relDir)
return op.Put(tsk.Ctx, storage, newDistDir, s, tsk.SetProgress)
},
}))
}
return nil
}

View File

@ -2,11 +2,14 @@ package tool
import (
"fmt"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/task"
)
var (
Tools = make(ToolsManager)
Tools = make(ToolsManager)
DownTaskManager = task.NewTaskManager[string](3)
)
type ToolsManager map[string]Tool
@ -18,8 +21,8 @@ func (t ToolsManager) Get(name string) (Tool, error) {
return nil, fmt.Errorf("tool %s not found", name)
}
func (t ToolsManager) Add(tool Tool) {
t[tool.Name()] = tool
func (t ToolsManager) Add(name string, tool Tool) {
t[name] = tool
}
func (t ToolsManager) Names() []string {

View File

@ -1,86 +0,0 @@
package tool
import (
"fmt"
"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/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/xhofe/tache"
"os"
"path/filepath"
"sync"
)
type TransferTask struct {
tache.Base
file File
dstDirPath string
wg *sync.WaitGroup
tempDir string
deletePolicy DeletePolicy
}
func (t *TransferTask) Run() error {
defer t.wg.Done()
// check dstDir again
storage, dstDirActualPath, err := op.GetStorageAndActualPath(t.dstDirPath)
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
mimetype := utils.GetMimeType(t.file.Path)
rc, err := t.file.GetReadCloser()
if err != nil {
return errors.Wrapf(err, "failed to open file %s", t.file.Path)
}
s := &stream.FileStream{
Ctx: nil,
Obj: &model.Object{
Name: filepath.Base(t.file.Path),
Size: t.file.Size,
Modified: t.file.Modified,
IsFolder: false,
},
Reader: rc,
Mimetype: mimetype,
Closers: utils.NewClosers(rc),
}
relDir, err := filepath.Rel(t.tempDir, filepath.Dir(t.file.Path))
if err != nil {
log.Errorf("find relation directory error: %v", err)
}
newDistDir := filepath.Join(dstDirActualPath, relDir)
return op.Put(t.Ctx(), storage, newDistDir, s, t.SetProgress)
}
func (t *TransferTask) GetName() string {
return fmt.Sprintf("transfer %s to [%s]", t.file.Path, t.dstDirPath)
}
func (t *TransferTask) GetStatus() string {
return "transferring"
}
func (t *TransferTask) OnSucceeded() {
if t.deletePolicy == DeleteOnUploadSucceed || t.deletePolicy == DeleteAlways {
err := os.Remove(t.file.Path)
if err != nil {
log.Errorf("failed to delete file %s, error: %s", t.file.Path, err.Error())
}
}
}
func (t *TransferTask) OnFailed() {
if t.deletePolicy == DeleteOnUploadFailed || t.deletePolicy == DeleteAlways {
err := os.Remove(t.file.Path)
if err != nil {
log.Errorf("failed to delete file %s, error: %s", t.file.Path, err.Error())
}
}
}
var (
TransferTaskManager *tache.Manager[*TransferTask]
)

View File

@ -0,0 +1,60 @@
package qbittorrent
import (
"context"
"fmt"
"path/filepath"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/google/uuid"
"github.com/pkg/errors"
)
func AddURL(ctx context.Context, url string, dstDirPath string) error {
// check storage
storage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
// check is it could upload
if storage.Config().NoUpload {
return errors.WithStack(errs.UploadNotSupported)
}
// check path is valid
obj, err := op.Get(ctx, storage, dstDirActualPath)
if err != nil {
if !errs.IsObjectNotFound(err) {
return errors.WithMessage(err, "failed get object")
}
} else {
if !obj.IsDir() {
// can't add to a file
return errors.WithStack(errs.NotFolder)
}
}
// call qbittorrent
id := uuid.NewString()
tempDir := filepath.Join(conf.Conf.TempDir, "qbittorrent", id)
err = qbclient.AddFromLink(url, tempDir, id)
if err != nil {
return errors.Wrapf(err, "failed to add url %s", url)
}
DownTaskManager.Submit(task.WithCancelCtx(&task.Task[string]{
ID: id,
Name: fmt.Sprintf("download %s to [%s](%s)", url, storage.GetStorage().MountPath, dstDirActualPath),
Func: func(tsk *task.Task[string]) error {
m := &Monitor{
tsk: tsk,
tempDir: tempDir,
dstDirPath: dstDirPath,
seedtime: setting.GetInt(conf.QbittorrentSeedtime, 0),
}
return m.Loop()
},
}))
return nil
}

View File

@ -0,0 +1,366 @@
package qbittorrent
import (
"bytes"
"errors"
"io"
"mime/multipart"
"net/http"
"net/http/cookiejar"
"net/url"
"github.com/alist-org/alist/v3/pkg/utils"
)
type Client interface {
AddFromLink(link string, savePath string, id string) error
GetInfo(id string) (TorrentInfo, error)
GetFiles(id string) ([]FileInfo, error)
Delete(id string, deleteFiles bool) error
}
type client struct {
url *url.URL
client http.Client
Client
}
func New(webuiUrl string) (Client, error) {
u, err := url.Parse(webuiUrl)
if err != nil {
return nil, err
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
var c = &client{
url: u,
client: http.Client{Jar: jar},
}
err = c.checkAuthorization()
if err != nil {
return nil, err
}
return c, nil
}
func (c *client) checkAuthorization() error {
// check authorization
if c.authorized() {
return nil
}
// check authorization after logging in
err := c.login()
if err != nil {
return err
}
if c.authorized() {
return nil
}
return errors.New("unauthorized qbittorrent url")
}
func (c *client) authorized() bool {
resp, err := c.post("/api/v2/app/version", nil)
if err != nil {
return false
}
return resp.StatusCode == 200 // the status code will be 403 if not authorized
}
func (c *client) login() error {
// prepare HTTP request
v := url.Values{}
v.Set("username", c.url.User.Username())
passwd, _ := c.url.User.Password()
v.Set("password", passwd)
resp, err := c.post("/api/v2/auth/login", v)
if err != nil {
return err
}
// check result
body := make([]byte, 2)
_, err = resp.Body.Read(body)
if err != nil {
return err
}
if string(body) != "Ok" {
return errors.New("failed to login into qBittorrent webui with url: " + c.url.String())
}
return nil
}
func (c *client) post(path string, data url.Values) (*http.Response, error) {
u := c.url.JoinPath(path)
u.User = nil // remove userinfo for requests
req, err := http.NewRequest("POST", u.String(), bytes.NewReader([]byte(data.Encode())))
if err != nil {
return nil, err
}
if data != nil {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
if resp.Cookies() != nil {
c.client.Jar.SetCookies(u, resp.Cookies())
}
return resp, nil
}
func (c *client) AddFromLink(link string, savePath string, id string) error {
err := c.checkAuthorization()
if err != nil {
return err
}
buf := new(bytes.Buffer)
writer := multipart.NewWriter(buf)
addField := func(name string, value string) {
if err != nil {
return
}
err = writer.WriteField(name, value)
}
addField("urls", link)
addField("savepath", savePath)
addField("tags", "alist-"+id)
addField("autoTMM", "false")
if err != nil {
return err
}
err = writer.Close()
if err != nil {
return err
}
u := c.url.JoinPath("/api/v2/torrents/add")
u.User = nil // remove userinfo for requests
req, err := http.NewRequest("POST", u.String(), buf)
if err != nil {
return err
}
req.Header.Add("Content-Type", writer.FormDataContentType())
resp, err := c.client.Do(req)
if err != nil {
return err
}
// check result
body := make([]byte, 2)
_, err = resp.Body.Read(body)
if err != nil {
return err
}
if resp.StatusCode != 200 || string(body) != "Ok" {
return errors.New("failed to add qBittorrent task: " + link)
}
return nil
}
type TorrentStatus string
const (
ERROR TorrentStatus = "error"
MISSINGFILES TorrentStatus = "missingFiles"
UPLOADING TorrentStatus = "uploading"
PAUSEDUP TorrentStatus = "pausedUP"
QUEUEDUP TorrentStatus = "queuedUP"
STALLEDUP TorrentStatus = "stalledUP"
CHECKINGUP TorrentStatus = "checkingUP"
FORCEDUP TorrentStatus = "forcedUP"
ALLOCATING TorrentStatus = "allocating"
DOWNLOADING TorrentStatus = "downloading"
METADL TorrentStatus = "metaDL"
PAUSEDDL TorrentStatus = "pausedDL"
QUEUEDDL TorrentStatus = "queuedDL"
STALLEDDL TorrentStatus = "stalledDL"
CHECKINGDL TorrentStatus = "checkingDL"
FORCEDDL TorrentStatus = "forcedDL"
CHECKINGRESUMEDATA TorrentStatus = "checkingResumeData"
MOVING TorrentStatus = "moving"
UNKNOWN TorrentStatus = "unknown"
)
// https://github.com/DGuang21/PTGo/blob/main/app/client/client_distributer.go
type TorrentInfo struct {
AddedOn int `json:"added_on"` // 将 torrent 添加到客户端的时间Unix Epoch
AmountLeft int64 `json:"amount_left"` // 剩余大小(字节)
AutoTmm bool `json:"auto_tmm"` // 此 torrent 是否由 Automatic Torrent Management 管理
Availability float64 `json:"availability"` // 当前百分比
Category string `json:"category"` //
Completed int64 `json:"completed"` // 完成的传输数据量(字节)
CompletionOn int `json:"completion_on"` // Torrent 完成的时间Unix Epoch
ContentPath string `json:"content_path"` // torrent 内容的绝对路径(多文件 torrent 的根路径,单文件 torrent 的绝对文件路径)
DlLimit int `json:"dl_limit"` // Torrent 下载速度限制(字节/秒)
Dlspeed int `json:"dlspeed"` // Torrent 下载速度(字节/秒)
Downloaded int64 `json:"downloaded"` // 已经下载大小
DownloadedSession int64 `json:"downloaded_session"` // 此会话下载的数据量
Eta int `json:"eta"` //
FLPiecePrio bool `json:"f_l_piece_prio"` // 如果第一个最后一块被优先考虑则为true
ForceStart bool `json:"force_start"` // 如果为此 torrent 启用了强制启动则为true
Hash string `json:"hash"` //
LastActivity int `json:"last_activity"` // 上次活跃的时间Unix Epoch
MagnetURI string `json:"magnet_uri"` // 与此 torrent 对应的 Magnet URI
MaxRatio float64 `json:"max_ratio"` // 种子/上传停止种子前的最大共享比率
MaxSeedingTime int `json:"max_seeding_time"` // 停止种子种子前的最长种子时间(秒)
Name string `json:"name"` //
NumComplete int `json:"num_complete"` //
NumIncomplete int `json:"num_incomplete"` //
NumLeechs int `json:"num_leechs"` // 连接到的 leechers 的数量
NumSeeds int `json:"num_seeds"` // 连接到的种子数
Priority int `json:"priority"` // 速度优先。如果队列被禁用或 torrent 处于种子模式,则返回 -1
Progress float64 `json:"progress"` // 进度
Ratio float64 `json:"ratio"` // Torrent 共享比率
RatioLimit int `json:"ratio_limit"` //
SavePath string `json:"save_path"`
SeedingTime int `json:"seeding_time"` // Torrent 完成用时(秒)
SeedingTimeLimit int `json:"seeding_time_limit"` // max_seeding_time
SeenComplete int `json:"seen_complete"` // 上次 torrent 完成的时间
SeqDl bool `json:"seq_dl"` // 如果启用顺序下载则为true
Size int64 `json:"size"` //
State TorrentStatus `json:"state"` // 参见https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list
SuperSeeding bool `json:"super_seeding"` // 如果启用超级播种则为true
Tags string `json:"tags"` // Torrent 的逗号连接标签列表
TimeActive int `json:"time_active"` // 总活动时间(秒)
TotalSize int64 `json:"total_size"` // 此 torrent 中所有文件的总大小(字节)(包括未选择的文件)
Tracker string `json:"tracker"` // 第一个具有工作状态的tracker。如果没有tracker在工作则返回空字符串。
TrackersCount int `json:"trackers_count"` //
UpLimit int `json:"up_limit"` // 上传限制
Uploaded int64 `json:"uploaded"` // 累计上传
UploadedSession int64 `json:"uploaded_session"` // 当前session累计上传
Upspeed int `json:"upspeed"` // 上传速度(字节/秒)
}
type InfoNotFoundError struct {
Id string
Err error
}
func (i InfoNotFoundError) Error() string {
return "there should be exactly one task with tag \"alist-" + i.Id + "\""
}
func NewInfoNotFoundError(id string) InfoNotFoundError {
return InfoNotFoundError{Id: id}
}
func (c *client) GetInfo(id string) (TorrentInfo, error) {
var infos []TorrentInfo
err := c.checkAuthorization()
if err != nil {
return TorrentInfo{}, err
}
v := url.Values{}
v.Set("tag", "alist-"+id)
response, err := c.post("/api/v2/torrents/info", v)
if err != nil {
return TorrentInfo{}, err
}
body, err := io.ReadAll(response.Body)
if err != nil {
return TorrentInfo{}, err
}
err = utils.Json.Unmarshal(body, &infos)
if err != nil {
return TorrentInfo{}, err
}
if len(infos) != 1 {
return TorrentInfo{}, NewInfoNotFoundError(id)
}
return infos[0], nil
}
type FileInfo struct {
Index int `json:"index"`
Name string `json:"name"`
Size int64 `json:"size"`
Progress float32 `json:"progress"`
Priority int `json:"priority"`
IsSeed bool `json:"is_seed"`
PieceRange []int `json:"piece_range"`
Availability float32 `json:"availability"`
}
func (c *client) GetFiles(id string) ([]FileInfo, error) {
var infos []FileInfo
err := c.checkAuthorization()
if err != nil {
return []FileInfo{}, err
}
tInfo, err := c.GetInfo(id)
if err != nil {
return []FileInfo{}, err
}
v := url.Values{}
v.Set("hash", tInfo.Hash)
response, err := c.post("/api/v2/torrents/files", v)
if err != nil {
return []FileInfo{}, err
}
body, err := io.ReadAll(response.Body)
if err != nil {
return []FileInfo{}, err
}
err = utils.Json.Unmarshal(body, &infos)
if err != nil {
return []FileInfo{}, err
}
return infos, nil
}
func (c *client) Delete(id string, deleteFiles bool) error {
err := c.checkAuthorization()
if err != nil {
return err
}
info, err := c.GetInfo(id)
if err != nil {
return err
}
v := url.Values{}
v.Set("hashes", info.Hash)
if deleteFiles {
v.Set("deleteFiles", "true")
} else {
v.Set("deleteFiles", "false")
}
response, err := c.post("/api/v2/torrents/delete", v)
if err != nil {
return err
}
if response.StatusCode != 200 {
return errors.New("failed to delete qbittorrent task")
}
v = url.Values{}
v.Set("tags", "alist-"+id)
response, err = c.post("/api/v2/torrents/deleteTags", v)
if err != nil {
return err
}
if response.StatusCode != 200 {
return errors.New("failed to delete qbittorrent tag")
}
return nil
}

View File

@ -0,0 +1,154 @@
package qbittorrent
import (
"net/http"
"net/http/cookiejar"
"net/url"
"testing"
)
func TestLogin(t *testing.T) {
// test logging in with wrong password
u, err := url.Parse("http://admin:admin@127.0.0.1:8080/")
if err != nil {
t.Error(err)
}
jar, err := cookiejar.New(nil)
if err != nil {
t.Error(err)
}
var c = &client{
url: u,
client: http.Client{Jar: jar},
}
err = c.login()
if err == nil {
t.Error(err)
}
// test logging in with correct password
u, err = url.Parse("http://admin:adminadmin@127.0.0.1:8080/")
if err != nil {
t.Error(err)
}
c.url = u
err = c.login()
if err != nil {
t.Error(err)
}
}
// in this test, the `Bypass authentication for clients on localhost` option in qBittorrent webui should be disabled
func TestAuthorized(t *testing.T) {
// init client
u, err := url.Parse("http://admin:adminadmin@127.0.0.1:8080/")
if err != nil {
t.Error(err)
}
jar, err := cookiejar.New(nil)
if err != nil {
t.Error(err)
}
var c = &client{
url: u,
client: http.Client{Jar: jar},
}
// test without logging in, which should be unauthorized
authorized := c.authorized()
if authorized {
t.Error("Should not be authorized")
}
// test after logging in
err = c.login()
if err != nil {
t.Error(err)
}
authorized = c.authorized()
if !authorized {
t.Error("Should be authorized")
}
}
func TestNew(t *testing.T) {
_, err := New("http://admin:adminadmin@127.0.0.1:8080/")
if err != nil {
t.Error(err)
}
_, err = New("http://admin:wrong_password@127.0.0.1:8080/")
if err == nil {
t.Error("Should get an error")
}
}
func TestAdd(t *testing.T) {
// init client
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
if err != nil {
t.Error(err)
}
err = c.AddFromLink(
"https://releases.ubuntu.com/22.04/ubuntu-22.04.1-desktop-amd64.iso.torrent",
"D:\\qBittorrentDownload\\alist",
"uuid-1",
)
if err != nil {
t.Error(err)
}
err = c.AddFromLink(
"magnet:?xt=urn:btih:375ae3280cd80a8e9d7212e11dfaf7c45069dd35&dn=archlinux-2023.02.01-x86_64.iso",
"D:\\qBittorrentDownload\\alist",
"uuid-2",
)
if err != nil {
t.Error(err)
}
}
func TestGetInfo(t *testing.T) {
// init client
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
if err != nil {
t.Error(err)
}
_, err = c.GetInfo("uuid-1")
if err != nil {
t.Error(err)
}
}
func TestGetFiles(t *testing.T) {
// init client
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
if err != nil {
t.Error(err)
}
files, err := c.GetFiles("uuid-1")
if err != nil {
t.Error(err)
}
if len(files) != 1 {
t.Error("should have exactly one file")
}
}
func TestDelete(t *testing.T) {
// init client
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
if err != nil {
t.Error(err)
}
err = c.AddFromLink(
"https://releases.ubuntu.com/22.04/ubuntu-22.04.1-desktop-amd64.iso.torrent",
"D:\\qBittorrentDownload\\alist",
"uuid-1",
)
if err != nil {
t.Error(err)
}
err = c.Delete("uuid-1", true)
if err != nil {
t.Error(err)
}
}

View File

@ -0,0 +1,181 @@
package qbittorrent
import (
"fmt"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
type Monitor struct {
tsk *task.Task[string]
tempDir string
dstDirPath string
seedtime int
finish chan struct{}
}
func (m *Monitor) Loop() error {
var (
err error
completed bool
)
m.finish = make(chan struct{})
// wait for qbittorrent to parse torrent and create task
m.tsk.SetStatus("waiting for qbittorrent to parse torrent and create task")
waitCount := 0
for {
_, err := qbclient.GetInfo(m.tsk.ID)
if err == nil {
break
}
switch err.(type) {
case InfoNotFoundError:
break
default:
return err
}
waitCount += 1
if waitCount >= 60 {
return errors.New("torrent parse timeout")
}
timer := time.NewTimer(time.Second)
<-timer.C
}
outer:
for {
select {
case <-m.tsk.Ctx.Done():
// delete qbittorrent task and downloaded files when the task exits with error
return qbclient.Delete(m.tsk.ID, true)
case <-time.After(time.Second * 2):
completed, err = m.update()
if completed {
break outer
}
}
}
if err != nil {
return err
}
m.tsk.SetStatus("qbittorrent download completed, transferring")
<-m.finish
m.tsk.SetStatus("completed")
return nil
}
func (m *Monitor) update() (bool, error) {
info, err := qbclient.GetInfo(m.tsk.ID)
if err != nil {
m.tsk.SetStatus("qbittorrent " + string(info.State))
return true, err
}
progress := float64(info.Completed) / float64(info.Size) * 100
m.tsk.SetProgress(progress)
switch info.State {
case UPLOADING, PAUSEDUP, QUEUEDUP, STALLEDUP, FORCEDUP, CHECKINGUP:
err = m.complete()
return true, errors.WithMessage(err, "failed to transfer file")
case ALLOCATING, DOWNLOADING, METADL, PAUSEDDL, QUEUEDDL, STALLEDDL, CHECKINGDL, FORCEDDL, CHECKINGRESUMEDATA, MOVING:
m.tsk.SetStatus("qbittorrent downloading")
return false, nil
case ERROR, MISSINGFILES, UNKNOWN:
return true, errors.Errorf("failed to download %s, error: %s", m.tsk.ID, info.State)
}
return true, errors.New("unknown error occurred downloading qbittorrent") // should never happen
}
var TransferTaskManager = task.NewTaskManager(3, func(k *uint64) {
atomic.AddUint64(k, 1)
})
func (m *Monitor) complete() error {
// check dstDir again
storage, dstBaseDir, err := op.GetStorageAndActualPath(m.dstDirPath)
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
// get files
files, err := qbclient.GetFiles(m.tsk.ID)
if err != nil {
return errors.Wrapf(err, "failed to get files of %s", m.tsk.ID)
}
log.Debugf("files len: %d", len(files))
// delete qbittorrent task but do not delete the files before transferring to avoid qbittorrent
// accessing downloaded files and throw `cannot access the file because it is being used by another process` error
// err = qbclient.Delete(m.tsk.ID, false)
// if err != nil {
// return err
// }
// upload files
var wg sync.WaitGroup
wg.Add(len(files))
go func() {
wg.Wait()
m.finish <- struct{}{}
if m.seedtime < 0 {
log.Debugf("do not delete qb task %s", m.tsk.ID)
return
}
log.Debugf("delete qb task %s after %d minutes", m.tsk.ID, m.seedtime)
<-time.After(time.Duration(m.seedtime) * time.Minute)
err := qbclient.Delete(m.tsk.ID, true)
if err != nil {
log.Errorln(err.Error())
}
err = os.RemoveAll(m.tempDir)
if err != nil {
log.Errorf("failed to remove qbittorrent temp dir: %+v", err.Error())
}
}()
for _, file := range files {
tempPath := filepath.Join(m.tempDir, file.Name)
dstPath := filepath.Join(dstBaseDir, file.Name)
dstDir := filepath.Dir(dstPath)
fileName := filepath.Base(dstPath)
TransferTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
Name: fmt.Sprintf("transfer %s to [%s](%s)", tempPath, storage.GetStorage().MountPath, dstPath),
Func: func(tsk *task.Task[uint64]) error {
defer wg.Done()
size := file.Size
mimetype := utils.GetMimeType(tempPath)
f, err := os.Open(tempPath)
if err != nil {
return errors.Wrapf(err, "failed to open file %s", tempPath)
}
s := stream.FileStream{
Obj: &model.Object{
Name: fileName,
Size: size,
Modified: time.Now(),
IsFolder: false,
},
Reader: f,
Closers: utils.NewClosers(f),
Mimetype: mimetype,
}
ss, err := stream.NewSeekableStream(s, nil)
if err != nil {
return err
}
return op.Put(tsk.Ctx, storage, dstDir, ss, tsk.SetProgress)
},
}))
}
return nil
}

View File

@ -0,0 +1,23 @@
package qbittorrent
import (
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/task"
)
var DownTaskManager = task.NewTaskManager[string](3)
var qbclient Client
func InitClient() error {
var err error
qbclient = nil
url := setting.GetStr(conf.QbittorrentUrl)
qbclient, err = New(url)
return err
}
func IsQbittorrentReady() bool {
return qbclient != nil
}

View File

@ -163,15 +163,8 @@ func GetObjType(filename string, isDir bool) int {
return GetFileType(filename)
}
var extraMimeTypes = map[string]string{
".apk": "application/vnd.android.package-archive",
}
func GetMimeType(name string) string {
ext := path.Ext(name)
if m, ok := extraMimeTypes[ext]; ok {
return m
}
m := mime.TypeByExtension(ext)
if m != "" {
return m

View File

@ -4,7 +4,6 @@ import (
"time"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/model"
"github.com/golang-jwt/jwt/v4"
"github.com/pkg/errors"
)
@ -13,14 +12,12 @@ var SecretKey []byte
type UserClaims struct {
Username string `json:"username"`
PwdTS int64 `json:"pwd_ts"`
jwt.RegisteredClaims
}
func GenerateToken(user *model.User) (tokenString string, err error) {
func GenerateToken(username string) (tokenString string, err error) {
claim := UserClaims{
Username: user.Username,
PwdTS: user.PwdTS,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(conf.Conf.TokenExpiresIn) * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),

View File

@ -17,10 +17,6 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.
if link.MFile != nil {
defer link.MFile.Close()
attachFileName(w, file)
contentType := link.Header.Get("Content-Type")
if contentType != "" {
w.Header().Set("Content-Type", contentType)
}
http.ServeContent(w, r, file.GetName(), file.ModTime(), link.MFile)
return nil
} else if link.RangeReadCloser != nil {
@ -80,5 +76,4 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.
func attachFileName(w http.ResponseWriter, file model.Obj) {
fileName := file.GetName()
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, fileName, url.PathEscape(fileName)))
w.Header().Set("Content-Type", utils.GetMimeType(fileName))
}

View File

@ -78,7 +78,7 @@ func loginHash(c *gin.Context, req *LoginReq) {
}
}
// generate token
token, err := common.GenerateToken(user)
token, err := common.GenerateToken(user.Username)
if err != nil {
common.ErrorResp(c, err, 400, true)
return

View File

@ -45,8 +45,6 @@ func Plist(c *gin.Context) {
}
fullName := c.Param("name")
Url := link.String()
Url = strings.ReplaceAll(Url, "<", "[")
Url = strings.ReplaceAll(Url, ">", "]")
nameEncode := linkNameSplit[1]
fullName, err = url.PathUnescape(nameEncode)
if err != nil {

View File

@ -74,10 +74,9 @@ func OfflineDownloadTools(c *gin.Context) {
}
type AddOfflineDownloadReq struct {
Urls []string `json:"urls"`
Path string `json:"path"`
Tool string `json:"tool"`
DeletePolicy string `json:"delete_policy"`
Urls []string `json:"urls"`
Path string `json:"path"`
Tool string `json:"tool"`
}
func AddOfflineDownload(c *gin.Context) {
@ -99,10 +98,9 @@ func AddOfflineDownload(c *gin.Context) {
}
for _, url := range req.Urls {
err := tool.AddURL(c, &tool.AddURLArgs{
URL: url,
DstDirPath: reqPath,
Tool: req.Tool,
DeletePolicy: tool.DeletePolicy(req.DeletePolicy),
URL: url,
DstDirPath: reqPath,
Tool: req.Tool,
})
if err != nil {
common.ErrorResp(c, err, 500)

View File

@ -260,7 +260,7 @@ func OIDCLoginCallback(c *gin.Context) {
common.ErrorResp(c, err, 400)
}
}
token, err := common.GenerateToken(user)
token, err := common.GenerateToken(user.Username)
if err != nil {
common.ErrorResp(c, err, 400)
}
@ -426,7 +426,7 @@ func SSOLoginCallback(c *gin.Context) {
return
}
}
token, err := common.GenerateToken(user)
token, err := common.GenerateToken(user.Username)
if err != nil {
common.ErrorResp(c, err, 400)
}

View File

@ -1,85 +1,127 @@
package handles
import (
"strconv"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/offline_download/tool"
"github.com/alist-org/alist/v3/internal/qbittorrent"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
"github.com/xhofe/tache"
)
type TaskInfo struct {
ID string `json:"id"`
Name string `json:"name"`
State tache.State `json:"state"`
Status string `json:"status"`
Progress float64 `json:"progress"`
Error string `json:"error"`
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
Status string `json:"status"`
Progress float64 `json:"progress"`
Error string `json:"error"`
}
func getTaskInfo[T tache.TaskWithInfo](task T) TaskInfo {
errMsg := ""
if task.GetErr() != nil {
errMsg = task.GetErr().Error()
}
type K2Str[K comparable] func(k K) string
func uint64K2Str(k uint64) string {
return strconv.FormatUint(k, 10)
}
func strK2Str(str string) string {
return str
}
func getTaskInfo[K comparable](task *task.Task[K], k2Str K2Str[K]) TaskInfo {
return TaskInfo{
ID: task.GetID(),
Name: task.GetName(),
ID: k2Str(task.ID),
Name: task.Name,
State: task.GetState(),
Status: task.GetStatus(),
Progress: task.GetProgress(),
Error: errMsg,
Error: task.GetErrMsg(),
}
}
func getTaskInfos[T tache.TaskWithInfo](tasks []T) []TaskInfo {
func getTaskInfos[K comparable](tasks []*task.Task[K], k2Str K2Str[K]) []TaskInfo {
var infos []TaskInfo
for _, t := range tasks {
infos = append(infos, getTaskInfo(t))
infos = append(infos, getTaskInfo(t, k2Str))
}
return infos
}
func taskRoute[T tache.TaskWithInfo](g *gin.RouterGroup, manager *tache.Manager[T]) {
type Str2K[K comparable] func(str string) (K, error)
func str2Uint64K(str string) (uint64, error) {
return strconv.ParseUint(str, 10, 64)
}
func str2StrK(str string) (string, error) {
return str, nil
}
func taskRoute[K comparable](g *gin.RouterGroup, manager *task.Manager[K], k2Str K2Str[K], str2K Str2K[K]) {
g.GET("/undone", func(c *gin.Context) {
common.SuccessResp(c, getTaskInfos(manager.GetByState(tache.StatePending, tache.StateRunning,
tache.StateCanceling, tache.StateErrored, tache.StateFailing, tache.StateWaitingRetry, tache.StateBeforeRetry)))
common.SuccessResp(c, getTaskInfos(manager.ListUndone(), k2Str))
})
g.GET("/done", func(c *gin.Context) {
common.SuccessResp(c, getTaskInfos(manager.GetByState(tache.StateCanceled, tache.StateFailed, tache.StateSucceeded)))
common.SuccessResp(c, getTaskInfos(manager.ListDone(), k2Str))
})
g.POST("/cancel", func(c *gin.Context) {
tid := c.Query("tid")
manager.Cancel(tid)
common.SuccessResp(c)
id, err := str2K(tid)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := manager.Cancel(id); err != nil {
common.ErrorResp(c, err, 500)
} else {
common.SuccessResp(c)
}
})
g.POST("/delete", func(c *gin.Context) {
tid := c.Query("tid")
manager.Remove(tid)
common.SuccessResp(c)
id, err := str2K(tid)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := manager.Remove(id); err != nil {
common.ErrorResp(c, err, 500)
} else {
common.SuccessResp(c)
}
})
g.POST("/retry", func(c *gin.Context) {
tid := c.Query("tid")
manager.Retry(tid)
common.SuccessResp(c)
id, err := str2K(tid)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := manager.Retry(id); err != nil {
common.ErrorResp(c, err, 500)
} else {
common.SuccessResp(c)
}
})
g.POST("/clear_done", func(c *gin.Context) {
manager.RemoveByState(tache.StateCanceled, tache.StateFailed, tache.StateSucceeded)
manager.ClearDone()
common.SuccessResp(c)
})
g.POST("/clear_succeeded", func(c *gin.Context) {
manager.RemoveByState(tache.StateSucceeded)
common.SuccessResp(c)
})
g.POST("/retry_failed", func(c *gin.Context) {
manager.RetryAllFailed()
manager.ClearSucceeded()
common.SuccessResp(c)
})
}
func SetupTaskRoute(g *gin.RouterGroup) {
taskRoute(g.Group("/upload"), fs.UploadTaskManager)
taskRoute(g.Group("/copy"), fs.CopyTaskManager)
taskRoute(g.Group("/offline_download"), tool.DownloadTaskManager)
taskRoute(g.Group("/offline_download_transfer"), tool.TransferTaskManager)
taskRoute(g.Group("/upload"), fs.UploadTaskManager, uint64K2Str, str2Uint64K)
taskRoute(g.Group("/copy"), fs.CopyTaskManager, uint64K2Str, str2Uint64K)
taskRoute(g.Group("/qbit_down"), qbittorrent.DownTaskManager, strK2Str, str2StrK)
taskRoute(g.Group("/qbit_transfer"), qbittorrent.TransferTaskManager, uint64K2Str, str2Uint64K)
//taskRoute(g.Group("/aria2_down"), aria2.DownTaskManager, strK2Str, str2StrK)
//taskRoute(g.Group("/aria2_transfer"), aria2.TransferTaskManager, uint64K2Str, str2Uint64K)
taskRoute(g.Group("/offline_download"), tool.DownTaskManager, strK2Str, str2StrK)
taskRoute(g.Group("/offline_download_transfer"), tool.TransferTaskManager, uint64K2Str, str2Uint64K)
}

View File

@ -94,7 +94,7 @@ func FinishAuthnLogin(c *gin.Context) {
return
}
token, err := common.GenerateToken(user)
token, err := common.GenerateToken(user.Username)
if err != nil {
common.ErrorResp(c, err, 400, true)
return

View File

@ -57,12 +57,6 @@ func Auth(c *gin.Context) {
c.Abort()
return
}
// validate password timestamp
if userClaims.PwdTS != user.PwdTS {
common.ErrorStrResp(c, "Password has been changed, login please", 401)
c.Abort()
return
}
if user.Disabled {
common.ErrorStrResp(c, "Current user is disabled, replace please", 401)
c.Abort()
@ -111,12 +105,6 @@ func Authn(c *gin.Context) {
c.Abort()
return
}
// validate password timestamp
if userClaims.PwdTS != user.PwdTS {
common.ErrorStrResp(c, "Password has been changed, login please", 401)
c.Abort()
return
}
if user.Disabled {
common.ErrorStrResp(c, "Current user is disabled, replace please", 401)
c.Abort()

View File

@ -163,9 +163,8 @@ func _fs(g *gin.RouterGroup) {
func Cors(r *gin.Engine) {
config := cors.DefaultConfig()
//config.AllowAllOrigins = true
config.AllowOrigins = conf.Conf.Cors.AllowOrigins
config.AllowHeaders = conf.Conf.Cors.AllowHeaders
config.AllowMethods = conf.Conf.Cors.AllowMethods
config.AllowAllOrigins = true
config.AllowHeaders = []string{"*"}
config.AllowMethods = []string{"*"}
r.Use(cors.New(config))
}

View File

@ -3,48 +3,25 @@ package static
import (
"errors"
"fmt"
"github.com/alist-org/alist/v3/public"
"io"
"io/fs"
"net/http"
"os"
"strings"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/public"
"github.com/gin-gonic/gin"
)
var static fs.FS = public.Public
func initStatic() {
if conf.Conf.DistDir == "" {
dist, err := fs.Sub(static, "dist")
if err != nil {
utils.Log.Fatalf("failed to read dist dir")
}
static = dist
return
}
static = os.DirFS(conf.Conf.DistDir)
}
func initIndex() {
indexFile, err := static.Open("index.html")
func InitIndex() {
index, err := public.Public.ReadFile("dist/index.html")
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
utils.Log.Fatalf("index.html not exist, you may forget to put dist of frontend to public/dist")
}
utils.Log.Fatalf("failed to read index.html: %v", err)
}
defer func() {
_ = indexFile.Close()
}()
index, err := io.ReadAll(indexFile)
if err != nil {
utils.Log.Fatalf("failed to read dist/index.html")
}
conf.RawIndexHtml = string(index)
siteConfig := getSiteConfig()
replaceMap := map[string]string{
@ -83,8 +60,7 @@ func UpdateIndex() {
}
func Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {
initStatic()
initIndex()
InitIndex()
folders := []string{"assets", "images", "streamer", "static"}
r.Use(func(c *gin.Context) {
for i := range folders {
@ -94,7 +70,8 @@ func Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {
}
})
for i, folder := range folders {
sub, err := fs.Sub(static, folder)
folder = "dist/" + folder
sub, err := fs.Sub(public.Public, folder)
if err != nil {
utils.Log.Fatalf("can't find folder: %s", folder)
}