Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
8bdc67ec3d | |||
4fabc27366 | |||
e4c7b0f17c | |||
5e8bfb017e | |||
7d20a01dba | |||
59dbf4496f | |||
12f40608e6 | |||
89832c296f | |||
f09bb88846 | |||
c518f59528 | |||
e9c74f9959 | |||
21b8e7f6e5 | |||
2ae9cd8634 | |||
cfee536b96 | |||
1c8fe3b24c | |||
84e23c397d |
@ -73,6 +73,7 @@ English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_O
|
|||||||
- [x] SMB
|
- [x] SMB
|
||||||
- [x] [115](https://115.com/)
|
- [x] [115](https://115.com/)
|
||||||
- [X] Cloudreve
|
- [X] Cloudreve
|
||||||
|
- [x] [Dropbox](https://www.dropbox.com/)
|
||||||
- [x] Easy to deploy and out-of-the-box
|
- [x] Easy to deploy and out-of-the-box
|
||||||
- [x] File preview (PDF, markdown, code, plain text, ...)
|
- [x] File preview (PDF, markdown, code, plain text, ...)
|
||||||
- [x] Image preview in gallery mode
|
- [x] Image preview in gallery mode
|
||||||
|
@ -72,6 +72,7 @@
|
|||||||
- [x] SMB
|
- [x] SMB
|
||||||
- [x] [115](https://115.com/)
|
- [x] [115](https://115.com/)
|
||||||
- [X] Cloudreve
|
- [X] Cloudreve
|
||||||
|
- [x] [Dropbox](https://www.dropbox.com/)
|
||||||
- [x] 部署方便,开箱即用
|
- [x] 部署方便,开箱即用
|
||||||
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
||||||
- [x] 画廊模式下的图像预览
|
- [x] 画廊模式下的图像预览
|
||||||
|
@ -3,6 +3,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@ -42,26 +43,40 @@ the address is defined in config file`,
|
|||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
|
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
|
||||||
server.Init(r)
|
server.Init(r)
|
||||||
var httpSrv, httpsSrv *http.Server
|
var httpSrv, httpsSrv, unixSrv *http.Server
|
||||||
if !conf.Conf.Scheme.DisableHttp {
|
if conf.Conf.Scheme.HttpPort != -1 {
|
||||||
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Address, conf.Conf.Port)
|
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
|
||||||
utils.Log.Infof("start HTTP server @ %s", httpBase)
|
utils.Log.Infof("start HTTP server @ %s", httpBase)
|
||||||
httpSrv = &http.Server{Addr: httpBase, Handler: r}
|
httpSrv = &http.Server{Addr: httpBase, Handler: r}
|
||||||
go func() {
|
go func() {
|
||||||
err := httpSrv.ListenAndServe()
|
err := httpSrv.ListenAndServe()
|
||||||
if err != nil && err != http.ErrServerClosed {
|
if err != nil && err != http.ErrServerClosed {
|
||||||
utils.Log.Fatalf("failed to start: %s", err.Error())
|
utils.Log.Fatalf("failed to start http: %s", err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if conf.Conf.Scheme.Https {
|
if conf.Conf.Scheme.HttpsPort != -1 {
|
||||||
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Address, conf.Conf.HttpsPort)
|
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
|
||||||
utils.Log.Infof("start HTTPS server @ %s", httpsBase)
|
utils.Log.Infof("start HTTPS server @ %s", httpsBase)
|
||||||
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
|
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
|
||||||
go func() {
|
go func() {
|
||||||
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||||
if err != nil && err != http.ErrServerClosed {
|
if err != nil && err != http.ErrServerClosed {
|
||||||
utils.Log.Fatalf("failed to start: %s", err.Error())
|
utils.Log.Fatalf("failed to start https: %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if conf.Conf.Scheme.UnixFile != "" {
|
||||||
|
utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile)
|
||||||
|
unixSrv = &http.Server{Handler: r}
|
||||||
|
go func() {
|
||||||
|
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Fatalf("failed to listen unix: %+v", err)
|
||||||
|
}
|
||||||
|
err = unixSrv.Serve(listener)
|
||||||
|
if err != nil && err != http.ErrServerClosed {
|
||||||
|
utils.Log.Fatalf("failed to start unix: %s", err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@ -78,21 +93,30 @@ the address is defined in config file`,
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
if !conf.Conf.Scheme.DisableHttp {
|
if conf.Conf.Scheme.HttpPort != -1 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := httpSrv.Shutdown(ctx); err != nil {
|
if err := httpSrv.Shutdown(ctx); err != nil {
|
||||||
utils.Log.Fatal("HTTP server shutdown:", err)
|
utils.Log.Fatal("HTTP server shutdown err: ", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if conf.Conf.Scheme.Https {
|
if conf.Conf.Scheme.HttpsPort != -1 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := httpsSrv.Shutdown(ctx); err != nil {
|
if err := httpsSrv.Shutdown(ctx); err != nil {
|
||||||
utils.Log.Fatal("HTTPS server shutdown:", err)
|
utils.Log.Fatal("HTTPS server shutdown err: ", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if conf.Conf.Scheme.UnixFile != "" {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := unixSrv.Shutdown(ctx); err != nil {
|
||||||
|
utils.Log.Fatal("Unix server shutdown err: ", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,7 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
|||||||
}
|
}
|
||||||
u_ := u.String()
|
u_ := u.String()
|
||||||
log.Debug("download url: ", u_)
|
log.Debug("download url: ", u_)
|
||||||
res, err := base.NoRedirectClient.R().Get(u_)
|
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -112,6 +112,9 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
|||||||
} else if res.StatusCode() == 200 {
|
} else if res.StatusCode() == 200 {
|
||||||
link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
|
link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
|
||||||
}
|
}
|
||||||
|
link.Header = http.Header{
|
||||||
|
"Referer": []string{"https://www.123pan.com/"},
|
||||||
|
}
|
||||||
return &link, nil
|
return &link, nil
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("can't convert obj")
|
return nil, fmt.Errorf("can't convert obj")
|
||||||
@ -235,6 +238,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
}
|
}
|
||||||
if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" {
|
if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" {
|
||||||
err = d.newUpload(ctx, &resp, stream, uploadFile, up)
|
err = d.newUpload(ctx, &resp, stream, uploadFile, up)
|
||||||
|
return err
|
||||||
} else {
|
} else {
|
||||||
cfg := &aws.Config{
|
cfg := &aws.Config{
|
||||||
Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),
|
Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),
|
||||||
|
@ -34,14 +34,17 @@ func (d *Pan123) getS3PreSignedUrls(ctx context.Context, upReq *UploadResp, star
|
|||||||
return &s3PreSignedUrls, nil
|
return &s3PreSignedUrls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp) error {
|
func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp, file model.FileStreamer, isMultipart bool) error {
|
||||||
data := base.Json{
|
data := base.Json{
|
||||||
|
"StorageNode": upReq.Data.StorageNode,
|
||||||
"bucket": upReq.Data.Bucket,
|
"bucket": upReq.Data.Bucket,
|
||||||
|
"fileId": upReq.Data.FileId,
|
||||||
|
"fileSize": file.GetSize(),
|
||||||
|
"isMultipart": isMultipart,
|
||||||
"key": upReq.Data.Key,
|
"key": upReq.Data.Key,
|
||||||
"uploadId": upReq.Data.UploadId,
|
"uploadId": upReq.Data.UploadId,
|
||||||
"StorageNode": upReq.Data.StorageNode,
|
|
||||||
}
|
}
|
||||||
_, err := d.request(S3Complete, http.MethodPost, func(req *resty.Request) {
|
_, err := d.request(UploadCompleteV2, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data).SetContext(ctx)
|
req.SetBody(data).SetContext(ctx)
|
||||||
}, nil)
|
}, nil)
|
||||||
return err
|
return err
|
||||||
@ -83,7 +86,7 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// complete s3 upload
|
// complete s3 upload
|
||||||
return d.completeS3(ctx, upReq)
|
return d.completeS3(ctx, upReq, file, chunkCount > 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSignedUrls *S3PreSignedURLs, cur, end int, reader io.Reader, curSize int64, retry bool) error {
|
func (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSignedUrls *S3PreSignedURLs, cur, end int, reader io.Reader, curSize int64, retry bool) error {
|
||||||
|
@ -15,19 +15,23 @@ import (
|
|||||||
// do others that not defined in Driver interface
|
// do others that not defined in Driver interface
|
||||||
|
|
||||||
const (
|
const (
|
||||||
API = "https://www.123pan.com/b/api"
|
AApi = "https://www.123pan.com/a/api"
|
||||||
SignIn = API + "/user/sign_in"
|
BApi = "https://www.123pan.com/b/api"
|
||||||
UserInfo = API + "/user/info"
|
MainApi = AApi
|
||||||
FileList = API + "/file/list/new"
|
SignIn = MainApi + "/user/sign_in"
|
||||||
DownloadInfo = "https://www.123pan.com/a/api/file/download_info"
|
UserInfo = MainApi + "/user/info"
|
||||||
Mkdir = API + "/file/upload_request"
|
FileList = MainApi + "/file/list/new"
|
||||||
Move = API + "/file/mod_pid"
|
DownloadInfo = MainApi + "/file/download_info"
|
||||||
Rename = API + "/file/rename"
|
Mkdir = MainApi + "/file/upload_request"
|
||||||
Trash = API + "/file/trash"
|
Move = MainApi + "/file/mod_pid"
|
||||||
UploadRequest = API + "/file/upload_request"
|
Rename = MainApi + "/file/rename"
|
||||||
UploadComplete = API + "/file/upload_complete"
|
Trash = MainApi + "/file/trash"
|
||||||
S3PreSignedUrls = API + "/file/s3_repare_upload_parts_batch"
|
UploadRequest = MainApi + "/file/upload_request"
|
||||||
S3Complete = API + "/file/s3_complete_multipart_upload"
|
UploadComplete = MainApi + "/file/upload_complete"
|
||||||
|
S3PreSignedUrls = MainApi + "/file/s3_repare_upload_parts_batch"
|
||||||
|
S3Auth = MainApi + "/file/s3_upload_object/auth"
|
||||||
|
UploadCompleteV2 = MainApi + "/file/upload_complete/v2"
|
||||||
|
S3Complete = MainApi + "/file/s3_complete_multipart_upload"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Pan123) login() error {
|
func (d *Pan123) login() error {
|
||||||
@ -42,6 +46,7 @@ func (d *Pan123) login() error {
|
|||||||
body = base.Json{
|
body = base.Json{
|
||||||
"passport": d.Username,
|
"passport": d.Username,
|
||||||
"password": d.Password,
|
"password": d.Password,
|
||||||
|
"remember": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res, err := base.RestyClient.R().
|
res, err := base.RestyClient.R().
|
||||||
@ -61,6 +66,7 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
|
|||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
req.SetHeaders(map[string]string{
|
req.SetHeaders(map[string]string{
|
||||||
"origin": "https://www.123pan.com",
|
"origin": "https://www.123pan.com",
|
||||||
|
"referer": "https://www.123pan.com/",
|
||||||
"authorization": "Bearer " + d.AccessToken,
|
"authorization": "Bearer " + d.AccessToken,
|
||||||
"platform": "web",
|
"platform": "web",
|
||||||
"app-version": "1.2",
|
"app-version": "1.2",
|
||||||
|
@ -2,6 +2,7 @@ package aliyundrive_open
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -50,6 +51,9 @@ func (d *AliyundriveOpen) Drop(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (d *AliyundriveOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
if d.limitList == nil {
|
||||||
|
return nil, fmt.Errorf("driver not init")
|
||||||
|
}
|
||||||
files, err := d.getFiles(ctx, dir.GetID())
|
files, err := d.getFiles(ctx, dir.GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -79,6 +83,9 @@ func (d *AliyundriveOpen) link(ctx context.Context, file model.Obj) (*model.Link
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
if d.limitLink == nil {
|
||||||
|
return nil, fmt.Errorf("driver not init")
|
||||||
|
}
|
||||||
return d.limitLink(ctx, file)
|
return d.limitLink(ctx, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +155,9 @@ func (d *AliyundriveOpen) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
// rapid_upload is not currently supported
|
// rapid_upload is not currently supported
|
||||||
// 1. create
|
// 1. create
|
||||||
const DEFAULT int64 = 20971520
|
// Part Size Unit: Bytes, Default: 20MB,
|
||||||
|
// Maximum number of slices 10,000, ≈195.3125GB
|
||||||
|
var partSize int64 = 20 * 1024 * 1024
|
||||||
createData := base.Json{
|
createData := base.Json{
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
"parent_file_id": dstDir.GetID(),
|
"parent_file_id": dstDir.GetID(),
|
||||||
@ -157,8 +166,21 @@ func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream mode
|
|||||||
"check_name_mode": "ignore",
|
"check_name_mode": "ignore",
|
||||||
}
|
}
|
||||||
count := 1
|
count := 1
|
||||||
if stream.GetSize() > DEFAULT {
|
if stream.GetSize() > partSize {
|
||||||
count = int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT)))
|
if stream.GetSize() > 1*1024*1024*1024*1024 { // file Size over 1TB
|
||||||
|
partSize = 5 * 1024 * 1024 * 1024 // file part size 5GB
|
||||||
|
} else if stream.GetSize() > 768*1024*1024*1024 { // over 768GB
|
||||||
|
partSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part
|
||||||
|
} else if stream.GetSize() > 512*1024*1024*1024 { // over 512GB
|
||||||
|
partSize = 82463373 // ≈ 78.6432MB
|
||||||
|
} else if stream.GetSize() > 384*1024*1024*1024 { // over 384GB
|
||||||
|
partSize = 54975582 // ≈ 52.4288MB
|
||||||
|
} else if stream.GetSize() > 256*1024*1024*1024 { // over 256GB
|
||||||
|
partSize = 41231687 // ≈ 39.3216MB
|
||||||
|
} else if stream.GetSize() > 128*1024*1024*1024 { // over 128GB
|
||||||
|
partSize = 27487791 // ≈ 26.2144MB
|
||||||
|
}
|
||||||
|
count = int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
|
||||||
createData["part_info_list"] = makePartInfos(count)
|
createData["part_info_list"] = makePartInfos(count)
|
||||||
}
|
}
|
||||||
var createResp CreateResp
|
var createResp CreateResp
|
||||||
@ -174,7 +196,7 @@ func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream mode
|
|||||||
if utils.IsCanceled(ctx) {
|
if utils.IsCanceled(ctx) {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
err = d.uploadPart(ctx, i, count, utils.NewMultiReadable(io.LimitReader(stream, DEFAULT)), &createResp, true)
|
err = d.uploadPart(ctx, i, count, utils.NewMultiReadable(io.LimitReader(stream, partSize)), &createResp, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package aliyundrive_share
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -22,6 +23,9 @@ type AliyundriveShare struct {
|
|||||||
ShareToken string
|
ShareToken string
|
||||||
DriveId string
|
DriveId string
|
||||||
cron *cron.Cron
|
cron *cron.Cron
|
||||||
|
|
||||||
|
limitList func(ctx context.Context, dir model.Obj) ([]model.Obj, error)
|
||||||
|
limitLink func(ctx context.Context, file model.Obj) (*model.Link, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveShare) Config() driver.Config {
|
func (d *AliyundriveShare) Config() driver.Config {
|
||||||
@ -48,6 +52,8 @@ func (d *AliyundriveShare) Init(ctx context.Context) error {
|
|||||||
log.Errorf("%+v", err)
|
log.Errorf("%+v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
d.limitList = utils.LimitRateCtx(d.list, time.Second/4)
|
||||||
|
d.limitLink = utils.LimitRateCtx(d.link, time.Second)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +66,13 @@ func (d *AliyundriveShare) Drop(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (d *AliyundriveShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
if d.limitList == nil {
|
||||||
|
return nil, fmt.Errorf("driver not init")
|
||||||
|
}
|
||||||
|
return d.limitList(ctx, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AliyundriveShare) list(ctx context.Context, dir model.Obj) ([]model.Obj, error) {
|
||||||
files, err := d.getFiles(dir.GetID())
|
files, err := d.getFiles(dir.GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -70,6 +83,13 @@ func (d *AliyundriveShare) List(ctx context.Context, dir model.Obj, args model.L
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *AliyundriveShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
if d.limitLink == nil {
|
||||||
|
return nil, fmt.Errorf("driver not init")
|
||||||
|
}
|
||||||
|
return d.limitLink(ctx, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AliyundriveShare) link(ctx context.Context, file model.Obj) (*model.Link, error) {
|
||||||
data := base.Json{
|
data := base.Json{
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
"file_id": file.GetID(),
|
"file_id": file.GetID(),
|
||||||
@ -79,7 +99,7 @@ func (d *AliyundriveShare) Link(ctx context.Context, file model.Obj, args model.
|
|||||||
}
|
}
|
||||||
var resp ShareLinkResp
|
var resp ShareLinkResp
|
||||||
_, err := d.request("https://api.aliyundrive.com/v2/file/get_share_link_download_url", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("https://api.aliyundrive.com/v2/file/get_share_link_download_url", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data).SetResult(&resp)
|
req.SetHeader(CanaryHeaderKey, CanaryHeaderValue).SetBody(data).SetResult(&resp)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -9,6 +9,12 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CanaryHeaderKey CanaryHeaderValue for lifting rate limit restrictions
|
||||||
|
CanaryHeaderKey = "X-Canary"
|
||||||
|
CanaryHeaderValue = "client=web,app=share,version=v2.3.1"
|
||||||
|
)
|
||||||
|
|
||||||
func (d *AliyundriveShare) refreshToken() error {
|
func (d *AliyundriveShare) refreshToken() error {
|
||||||
url := "https://auth.aliyundrive.com/v2/account/token"
|
url := "https://auth.aliyundrive.com/v2/account/token"
|
||||||
var resp base.TokenResp
|
var resp base.TokenResp
|
||||||
@ -58,6 +64,7 @@ func (d *AliyundriveShare) request(url, method string, callback base.ReqCallback
|
|||||||
SetError(&e).
|
SetError(&e).
|
||||||
SetHeader("content-type", "application/json").
|
SetHeader("content-type", "application/json").
|
||||||
SetHeader("Authorization", "Bearer\t"+d.AccessToken).
|
SetHeader("Authorization", "Bearer\t"+d.AccessToken).
|
||||||
|
SetHeader(CanaryHeaderKey, CanaryHeaderValue).
|
||||||
SetHeader("x-share-token", d.ShareToken)
|
SetHeader("x-share-token", d.ShareToken)
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(req)
|
callback(req)
|
||||||
@ -107,6 +114,7 @@ func (d *AliyundriveShare) getFiles(fileId string) ([]File, error) {
|
|||||||
var resp ListResp
|
var resp ListResp
|
||||||
res, err := base.RestyClient.R().
|
res, err := base.RestyClient.R().
|
||||||
SetHeader("x-share-token", d.ShareToken).
|
SetHeader("x-share-token", d.ShareToken).
|
||||||
|
SetHeader(CanaryHeaderKey, CanaryHeaderValue).
|
||||||
SetResult(&resp).SetError(&e).SetBody(data).
|
SetResult(&resp).SetError(&e).SetBody(data).
|
||||||
Post("https://api.aliyundrive.com/adrive/v3/file/list")
|
Post("https://api.aliyundrive.com/adrive/v3/file/list")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
|
_ "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/baidu_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
|
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/dropbox"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/google_photo"
|
_ "github.com/alist-org/alist/v3/drivers/google_photo"
|
||||||
@ -24,6 +25,7 @@ import (
|
|||||||
_ "github.com/alist-org/alist/v3/drivers/local"
|
_ "github.com/alist-org/alist/v3/drivers/local"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mega"
|
_ "github.com/alist-org/alist/v3/drivers/mega"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/mopan"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
|
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/pikpak"
|
_ "github.com/alist-org/alist/v3/drivers/pikpak"
|
||||||
|
@ -154,7 +154,7 @@ func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Li
|
|||||||
"target": fmt.Sprintf("[\"%s\"]", file.GetPath()),
|
"target": fmt.Sprintf("[\"%s\"]", file.GetPath()),
|
||||||
"dlink": "1",
|
"dlink": "1",
|
||||||
"web": "5",
|
"web": "5",
|
||||||
//"origin": "dlna",
|
"origin": "dlna",
|
||||||
}
|
}
|
||||||
_, err := d.request("https://pan.baidu.com/api/filemetas", http.MethodGet, func(req *resty.Request) {
|
_, err := d.request("https://pan.baidu.com/api/filemetas", http.MethodGet, func(req *resty.Request) {
|
||||||
req.SetQueryParams(param)
|
req.SetQueryParams(param)
|
||||||
|
222
drivers/dropbox/driver.go
Normal file
222
drivers/dropbox/driver.go
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
package dropbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/utils"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dropbox struct {
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
base string
|
||||||
|
contentBase string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) GetAddition() driver.Additional {
|
||||||
|
return &d.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) Init(ctx context.Context) error {
|
||||||
|
query := "foo"
|
||||||
|
res, err := d.request("/2/check/user", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetBody(base.Json{
|
||||||
|
"query": query,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result := utils.Json.Get(res, "result").ToString()
|
||||||
|
if result != query {
|
||||||
|
return fmt.Errorf("failed to check user: %s", string(res))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
files, err := d.getFiles(ctx, dir.GetPath())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||||
|
return fileToObj(src), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
res, err := d.request("/2/files/get_temporary_link", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx).SetBody(base.Json{
|
||||||
|
"path": file.GetPath(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
url := utils.Json.Get(res, "link").ToString()
|
||||||
|
exp := time.Hour
|
||||||
|
return &model.Link{
|
||||||
|
URL: url,
|
||||||
|
Expiration: &exp,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
|
_, err := d.request("/2/files/create_folder_v2", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx).SetBody(base.Json{
|
||||||
|
"autorename": false,
|
||||||
|
"path": parentDir.GetPath() + "/" + dirName,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
toPath := dstDir.GetPath() + "/" + srcObj.GetName()
|
||||||
|
|
||||||
|
_, err := d.request("/2/files/move_v2", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx).SetBody(base.Json{
|
||||||
|
"allow_ownership_transfer": false,
|
||||||
|
"allow_shared_folder": false,
|
||||||
|
"autorename": false,
|
||||||
|
"from_path": srcObj.GetID(),
|
||||||
|
"to_path": toPath,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
|
path := srcObj.GetPath()
|
||||||
|
fileName := srcObj.GetName()
|
||||||
|
toPath := path[:len(path)-len(fileName)] + newName
|
||||||
|
|
||||||
|
_, err := d.request("/2/files/move_v2", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx).SetBody(base.Json{
|
||||||
|
"allow_ownership_transfer": false,
|
||||||
|
"allow_shared_folder": false,
|
||||||
|
"autorename": false,
|
||||||
|
"from_path": srcObj.GetID(),
|
||||||
|
"to_path": toPath,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
toPath := dstDir.GetPath() + "/" + srcObj.GetName()
|
||||||
|
_, err := d.request("/2/files/copy_v2", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx).SetBody(base.Json{
|
||||||
|
"allow_ownership_transfer": false,
|
||||||
|
"allow_shared_folder": false,
|
||||||
|
"autorename": false,
|
||||||
|
"from_path": srcObj.GetID(),
|
||||||
|
"to_path": toPath,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
uri := "/2/files/delete_v2"
|
||||||
|
_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx).SetBody(base.Json{
|
||||||
|
"path": obj.GetID(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
|
// 1. start
|
||||||
|
sessionId, err := d.startUploadSession(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.append
|
||||||
|
// A single request should not upload more than 150 MB, and each call must be multiple of 4MB (except for last call)
|
||||||
|
const PartSize = 20971520
|
||||||
|
count := 1
|
||||||
|
if stream.GetSize() > PartSize {
|
||||||
|
count = int(math.Ceil(float64(stream.GetSize()) / float64(PartSize)))
|
||||||
|
}
|
||||||
|
offset := int64(0)
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
if utils.IsCanceled(ctx) {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
start := i * PartSize
|
||||||
|
byteSize := stream.GetSize() - int64(start)
|
||||||
|
if byteSize > PartSize {
|
||||||
|
byteSize = PartSize
|
||||||
|
}
|
||||||
|
|
||||||
|
url := d.contentBase + "/2/files/upload_session/append_v2"
|
||||||
|
reader := io.LimitReader(stream, PartSize)
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, reader)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to update file when append to upload session, err: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+d.AccessToken)
|
||||||
|
|
||||||
|
args := UploadAppendArgs{
|
||||||
|
Close: false,
|
||||||
|
Cursor: UploadCursor{
|
||||||
|
Offset: offset,
|
||||||
|
SessionID: sessionId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
argsJson, err := utils.Json.MarshalToString(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Dropbox-API-Arg", argsJson)
|
||||||
|
|
||||||
|
res, err := base.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = res.Body.Close()
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
up((i + 1) * 100 / count)
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += byteSize
|
||||||
|
|
||||||
|
}
|
||||||
|
// 3.finish
|
||||||
|
toPath := dstDir.GetPath() + "/" + stream.GetName()
|
||||||
|
err2 := d.finishUploadSession(ctx, toPath, offset, sessionId)
|
||||||
|
if err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*Dropbox)(nil)
|
42
drivers/dropbox/meta.go
Normal file
42
drivers/dropbox/meta.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package dropbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultClientID = "76lrwrklhdn1icb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Addition struct {
|
||||||
|
RefreshToken string `json:"refresh_token" required:"true"`
|
||||||
|
driver.RootPath
|
||||||
|
|
||||||
|
OauthTokenURL string `json:"oauth_token_url" default:"https://api.xhofe.top/alist/dropbox/token"`
|
||||||
|
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
|
||||||
|
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
|
||||||
|
|
||||||
|
AccessToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "Dropbox",
|
||||||
|
LocalSort: false,
|
||||||
|
OnlyLocal: false,
|
||||||
|
OnlyProxy: false,
|
||||||
|
NoCache: false,
|
||||||
|
NoUpload: false,
|
||||||
|
NeedMs: false,
|
||||||
|
DefaultRoot: "",
|
||||||
|
NoOverwriteUpload: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(func() driver.Driver {
|
||||||
|
return &Dropbox{
|
||||||
|
base: "https://api.dropboxapi.com",
|
||||||
|
contentBase: "https://content.dropboxapi.com",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
79
drivers/dropbox/types.go
Normal file
79
drivers/dropbox/types.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package dropbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenResp struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorResp struct {
|
||||||
|
Error struct {
|
||||||
|
Tag string `json:".tag"`
|
||||||
|
} `json:"error"`
|
||||||
|
ErrorSummary string `json:"error_summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshTokenErrorResp struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
ErrorDescription string `json:"error_description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Tag string `json:".tag"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PathLower string `json:"path_lower"`
|
||||||
|
PathDisplay string `json:"path_display"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
ClientModified time.Time `json:"client_modified"`
|
||||||
|
ServerModified time.Time `json:"server_modified"`
|
||||||
|
Rev string `json:"rev"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
IsDownloadable bool `json:"is_downloadable"`
|
||||||
|
ContentHash string `json:"content_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListResp struct {
|
||||||
|
Entries []File `json:"entries"`
|
||||||
|
Cursor string `json:"cursor"`
|
||||||
|
HasMore bool `json:"has_more"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadCursor struct {
|
||||||
|
Offset int64 `json:"offset"`
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadAppendArgs struct {
|
||||||
|
Close bool `json:"close"`
|
||||||
|
Cursor UploadCursor `json:"cursor"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadFinishArgs struct {
|
||||||
|
Commit struct {
|
||||||
|
Autorename bool `json:"autorename"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Mute bool `json:"mute"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
StrictConflict bool `json:"strict_conflict"`
|
||||||
|
} `json:"commit"`
|
||||||
|
Cursor UploadCursor `json:"cursor"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileToObj(f File) *model.ObjThumb {
|
||||||
|
return &model.ObjThumb{
|
||||||
|
Object: model.Object{
|
||||||
|
ID: f.ID,
|
||||||
|
Path: f.PathDisplay,
|
||||||
|
Name: f.Name,
|
||||||
|
Size: int64(f.Size),
|
||||||
|
Modified: f.ServerModified,
|
||||||
|
IsFolder: f.Tag == "folder",
|
||||||
|
},
|
||||||
|
Thumbnail: model.Thumbnail{},
|
||||||
|
}
|
||||||
|
}
|
199
drivers/dropbox/util.go
Normal file
199
drivers/dropbox/util.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package dropbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dropbox) refreshToken() error {
|
||||||
|
url := d.base + "/oauth2/token"
|
||||||
|
if utils.SliceContains([]string{"", DefaultClientID}, d.ClientID) {
|
||||||
|
url = d.OauthTokenURL
|
||||||
|
}
|
||||||
|
var tokenResp TokenResp
|
||||||
|
resp, err := base.RestyClient.R().
|
||||||
|
//ForceContentType("application/x-www-form-urlencoded").
|
||||||
|
//SetBasicAuth(d.ClientID, d.ClientSecret).
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": d.RefreshToken,
|
||||||
|
"client_id": d.ClientID,
|
||||||
|
"client_secret": d.ClientSecret,
|
||||||
|
}).
|
||||||
|
Post(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf("[dropbox] refresh token response: %s", resp.String())
|
||||||
|
if resp.StatusCode() != 200 {
|
||||||
|
return fmt.Errorf("failed to refresh token: %s", resp.String())
|
||||||
|
}
|
||||||
|
_ = utils.Json.UnmarshalFromString(resp.String(), &tokenResp)
|
||||||
|
d.AccessToken = tokenResp.AccessToken
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) request(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
|
||||||
|
req := base.RestyClient.R()
|
||||||
|
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||||
|
if method == http.MethodPost {
|
||||||
|
req.SetHeader("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
if callback != nil {
|
||||||
|
callback(req)
|
||||||
|
}
|
||||||
|
var e ErrorResp
|
||||||
|
req.SetError(&e)
|
||||||
|
res, err := req.Execute(method, d.base+uri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Debugf("[dropbox] request (%s) response: %s", uri, res.String())
|
||||||
|
isRetry := len(retry) > 0 && retry[0]
|
||||||
|
if res.StatusCode() != 200 {
|
||||||
|
body := res.String()
|
||||||
|
if !isRetry && (utils.SliceMeet([]string{"expired_access_token", "invalid_access_token", "authorization"}, body,
|
||||||
|
func(item string, v string) bool {
|
||||||
|
return strings.Contains(v, item)
|
||||||
|
}) || d.AccessToken == "") {
|
||||||
|
err = d.refreshToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d.request(uri, method, callback, true)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%s:%s", e.Error, e.ErrorSummary)
|
||||||
|
}
|
||||||
|
return res.Body(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) list(ctx context.Context, data base.Json, isContinue bool) (*ListResp, error) {
|
||||||
|
var resp ListResp
|
||||||
|
uri := "/2/files/list_folder"
|
||||||
|
if isContinue {
|
||||||
|
uri += "/continue"
|
||||||
|
}
|
||||||
|
_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx).SetBody(data).SetResult(&resp)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) getFiles(ctx context.Context, path string) ([]File, error) {
|
||||||
|
hasMore := true
|
||||||
|
var marker string
|
||||||
|
res := make([]File, 0)
|
||||||
|
|
||||||
|
data := base.Json{
|
||||||
|
"include_deleted": false,
|
||||||
|
"include_has_explicit_shared_members": false,
|
||||||
|
"include_mounted_folders": false,
|
||||||
|
"include_non_downloadable_files": false,
|
||||||
|
"limit": 2000,
|
||||||
|
"path": path,
|
||||||
|
"recursive": false,
|
||||||
|
}
|
||||||
|
resp, err := d.list(ctx, data, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
marker = resp.Cursor
|
||||||
|
hasMore = resp.HasMore
|
||||||
|
res = append(res, resp.Entries...)
|
||||||
|
|
||||||
|
for hasMore {
|
||||||
|
data := base.Json{
|
||||||
|
"cursor": marker,
|
||||||
|
}
|
||||||
|
resp, err := d.list(ctx, data, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
marker = resp.Cursor
|
||||||
|
hasMore = resp.HasMore
|
||||||
|
res = append(res, resp.Entries...)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) finishUploadSession(ctx context.Context, toPath string, offset int64, sessionId string) error {
|
||||||
|
url := d.contentBase + "/2/files/upload_session/finish"
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+d.AccessToken)
|
||||||
|
|
||||||
|
uploadFinishArgs := UploadFinishArgs{
|
||||||
|
Commit: struct {
|
||||||
|
Autorename bool `json:"autorename"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Mute bool `json:"mute"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
StrictConflict bool `json:"strict_conflict"`
|
||||||
|
}{
|
||||||
|
Autorename: true,
|
||||||
|
Mode: "add",
|
||||||
|
Mute: false,
|
||||||
|
Path: toPath,
|
||||||
|
StrictConflict: false,
|
||||||
|
},
|
||||||
|
Cursor: UploadCursor{
|
||||||
|
Offset: offset,
|
||||||
|
SessionID: sessionId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
argsJson, err := utils.Json.MarshalToString(uploadFinishArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Dropbox-API-Arg", argsJson)
|
||||||
|
|
||||||
|
res, err := base.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to update file when finish session, err: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = res.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dropbox) startUploadSession(ctx context.Context) (string, error) {
|
||||||
|
url := d.contentBase + "/2/files/upload_session/start"
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+d.AccessToken)
|
||||||
|
req.Header.Set("Dropbox-API-Arg", "{\"close\":false}")
|
||||||
|
|
||||||
|
res, err := base.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to update file when start session, err: %+v", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
sessionId := utils.Json.Get(body, "session_id").ToString()
|
||||||
|
|
||||||
|
_ = res.Body.Close()
|
||||||
|
return sessionId, nil
|
||||||
|
}
|
295
drivers/mopan/driver.go
Normal file
295
drivers/mopan/driver.go
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
package mopan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/internal/op"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/avast/retry-go"
|
||||||
|
"github.com/foxxorcat/mopan-sdk-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MoPan struct {
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
client *mopan.MoClient
|
||||||
|
|
||||||
|
userID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) GetAddition() driver.Additional {
|
||||||
|
return &d.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) Init(ctx context.Context) error {
|
||||||
|
login := func() error {
|
||||||
|
data, err := d.client.Login(d.Phone, d.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.client.SetAuthorization(data.Token)
|
||||||
|
|
||||||
|
info, err := d.client.GetUserInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.userID = info.UserID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
d.client = mopan.NewMoClient().
|
||||||
|
SetRestyClient(base.RestyClient).
|
||||||
|
SetOnAuthorizationExpired(func(_ error) error {
|
||||||
|
err := login()
|
||||||
|
if err != nil {
|
||||||
|
d.Status = err.Error()
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}).SetDeviceInfo(d.DeviceInfo)
|
||||||
|
d.DeviceInfo = d.client.GetDeviceInfo()
|
||||||
|
return login()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) Drop(ctx context.Context) error {
|
||||||
|
d.client = nil
|
||||||
|
d.userID = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
var files []model.Obj
|
||||||
|
for page := 1; ; page++ {
|
||||||
|
data, err := d.client.QueryFiles(dir.GetID(), page, mopan.WarpParamOption(
|
||||||
|
func(j mopan.Json) {
|
||||||
|
j["orderBy"] = d.OrderBy
|
||||||
|
j["descending"] = d.OrderDirection == "desc"
|
||||||
|
},
|
||||||
|
mopan.ParamOptionShareFile(d.CloudID),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.FileListAO.FileList)+len(data.FileListAO.FolderList) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
files = append(files, utils.MustSliceConvert(data.FileListAO.FolderList, folderToObj)...)
|
||||||
|
files = append(files, utils.MustSliceConvert(data.FileListAO.FileList, fileToObj)...)
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
data, err := d.client.GetFileDownloadUrl(file.GetID(), mopan.WarpParamOption(mopan.ParamOptionShareFile(d.CloudID)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Link{
|
||||||
|
URL: data.DownloadUrl,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
|
f, err := d.client.CreateFolder(dirName, parentDir.GetID(), mopan.WarpParamOption(
|
||||||
|
mopan.ParamOptionShareFile(d.CloudID),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return folderToObj(*f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
return d.newTask(srcObj, dstDir, mopan.TASK_MOVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
|
if srcObj.IsDir() {
|
||||||
|
_, err := d.client.RenameFolder(srcObj.GetID(), newName, mopan.WarpParamOption(
|
||||||
|
mopan.ParamOptionShareFile(d.CloudID),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err := d.client.RenameFile(srcObj.GetID(), newName, mopan.WarpParamOption(
|
||||||
|
mopan.ParamOptionShareFile(d.CloudID),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CloneObj(srcObj, srcObj.GetID(), newName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
return d.newTask(srcObj, dstDir, mopan.TASK_COPY)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) newTask(srcObj, dstDir model.Obj, taskType mopan.TaskType) (model.Obj, error) {
|
||||||
|
param := mopan.TaskParam{
|
||||||
|
UserOrCloudID: d.userID,
|
||||||
|
Source: 1,
|
||||||
|
TaskType: taskType,
|
||||||
|
TargetSource: 1,
|
||||||
|
TargetUserOrCloudID: d.userID,
|
||||||
|
TargetType: 1,
|
||||||
|
TargetFolderID: dstDir.GetID(),
|
||||||
|
TaskStatusDetailDTOList: []mopan.TaskFileParam{
|
||||||
|
{
|
||||||
|
FileID: srcObj.GetID(),
|
||||||
|
IsFolder: srcObj.IsDir(),
|
||||||
|
FileName: srcObj.GetName(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if d.CloudID != "" {
|
||||||
|
param.UserOrCloudID = d.CloudID
|
||||||
|
param.Source = 2
|
||||||
|
param.TargetSource = 2
|
||||||
|
param.TargetUserOrCloudID = d.CloudID
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := d.client.AddBatchTask(param)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for count := 0; count < 5; count++ {
|
||||||
|
stat, err := d.client.CheckBatchTask(mopan.TaskCheckParam{
|
||||||
|
TaskId: task.TaskIDList[0],
|
||||||
|
TaskType: task.TaskType,
|
||||||
|
TargetType: 1,
|
||||||
|
TargetFolderID: task.TargetFolderID,
|
||||||
|
TargetSource: param.TargetSource,
|
||||||
|
TargetUserOrCloudID: param.TargetUserOrCloudID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch stat.TaskStatus {
|
||||||
|
case 2:
|
||||||
|
if err := d.client.CancelBatchTask(stat.TaskID, task.TaskType); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, errors.New("file name conflict")
|
||||||
|
case 4:
|
||||||
|
if task.TaskType == mopan.TASK_MOVE {
|
||||||
|
return CloneObj(srcObj, srcObj.GetID(), srcObj.GetName()), nil
|
||||||
|
}
|
||||||
|
return CloneObj(srcObj, stat.SuccessedFileIDList[0], srcObj.GetName()), nil
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
_, err := d.client.DeleteToRecycle([]mopan.TaskFileParam{
|
||||||
|
{
|
||||||
|
FileID: obj.GetID(),
|
||||||
|
IsFolder: obj.IsDir(),
|
||||||
|
FileName: obj.GetName(),
|
||||||
|
},
|
||||||
|
}, mopan.WarpParamOption(mopan.ParamOptionShareFile(d.CloudID)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
|
file, err := utils.CreateTempFile(stream)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = file.Close()
|
||||||
|
_ = os.Remove(file.Name())
|
||||||
|
}()
|
||||||
|
|
||||||
|
initUpdload, err := d.client.InitMultiUpload(ctx, mopan.UpdloadFileParam{
|
||||||
|
ParentFolderId: dstDir.GetID(),
|
||||||
|
FileName: stream.GetName(),
|
||||||
|
FileSize: stream.GetSize(),
|
||||||
|
File: file,
|
||||||
|
}, mopan.WarpParamOption(
|
||||||
|
mopan.ParamOptionShareFile(d.CloudID),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !initUpdload.FileDataExists {
|
||||||
|
parts, err := d.client.GetAllMultiUploadUrls(initUpdload.UploadFileID, initUpdload.PartInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d.client.CloudDiskStartBusiness()
|
||||||
|
for i, part := range parts {
|
||||||
|
if utils.IsCanceled(ctx) {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := retry.Do(func() error {
|
||||||
|
if _, err := file.Seek(int64(part.PartNumber-1)*int64(initUpdload.PartSize), io.SeekStart); err != nil {
|
||||||
|
return retry.Unrecoverable(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := part.NewRequest(ctx, io.LimitReader(file, int64(initUpdload.PartSize)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := base.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("upload err,code=%d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
retry.Context(ctx),
|
||||||
|
retry.Attempts(3),
|
||||||
|
retry.Delay(time.Second),
|
||||||
|
retry.MaxDelay(5*time.Second))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
up(100 * (i + 1) / len(parts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uFile, err := d.client.CommitMultiUploadFile(initUpdload.UploadFileID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.Object{
|
||||||
|
ID: uFile.UserFileID,
|
||||||
|
Name: uFile.FileName,
|
||||||
|
Size: int64(uFile.FileSize),
|
||||||
|
Modified: time.Time(uFile.CreateDate),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*MoPan)(nil)
|
||||||
|
var _ driver.MkdirResult = (*MoPan)(nil)
|
||||||
|
var _ driver.MoveResult = (*MoPan)(nil)
|
||||||
|
var _ driver.RenameResult = (*MoPan)(nil)
|
||||||
|
var _ driver.Remove = (*MoPan)(nil)
|
||||||
|
var _ driver.CopyResult = (*MoPan)(nil)
|
||||||
|
var _ driver.PutResult = (*MoPan)(nil)
|
37
drivers/mopan/meta.go
Normal file
37
drivers/mopan/meta.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package mopan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Addition struct {
|
||||||
|
Phone string `json:"phone" required:"true"`
|
||||||
|
Password string `json:"password" required:"true"`
|
||||||
|
|
||||||
|
RootFolderID string `json:"root_folder_id" default:"-11" required:"true" help:"be careful when using the -11 value, some operations may cause system errors"`
|
||||||
|
|
||||||
|
CloudID string `json:"cloud_id"`
|
||||||
|
|
||||||
|
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
|
||||||
|
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
|
|
||||||
|
DeviceInfo string `json:"device_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Addition) GetRootId() string {
|
||||||
|
return a.RootFolderID
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "MoPan",
|
||||||
|
// DefaultRoot: "root, / or other",
|
||||||
|
CheckStatus: true,
|
||||||
|
Alert: "warning|This network disk may store your password in clear text. Please set your password carefully",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(func() driver.Driver {
|
||||||
|
return &MoPan{}
|
||||||
|
})
|
||||||
|
}
|
1
drivers/mopan/types.go
Normal file
1
drivers/mopan/types.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package mopan
|
58
drivers/mopan/util.go
Normal file
58
drivers/mopan/util.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package mopan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/foxxorcat/mopan-sdk-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fileToObj(f mopan.File) model.Obj {
|
||||||
|
return &model.ObjThumb{
|
||||||
|
Object: model.Object{
|
||||||
|
ID: string(f.ID),
|
||||||
|
Name: f.Name,
|
||||||
|
Size: f.Size,
|
||||||
|
Modified: time.Time(f.LastOpTime),
|
||||||
|
},
|
||||||
|
Thumbnail: model.Thumbnail{
|
||||||
|
Thumbnail: f.Icon.SmallURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func folderToObj(f mopan.Folder) model.Obj {
|
||||||
|
return &model.Object{
|
||||||
|
ID: string(f.ID),
|
||||||
|
Name: f.Name,
|
||||||
|
Modified: time.Time(f.LastOpTime),
|
||||||
|
IsFolder: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloneObj(o model.Obj, newID, newName string) model.Obj {
|
||||||
|
if o.IsDir() {
|
||||||
|
return &model.Object{
|
||||||
|
ID: newID,
|
||||||
|
Name: newName,
|
||||||
|
IsFolder: true,
|
||||||
|
Modified: o.ModTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thumb := ""
|
||||||
|
if o, ok := o.(model.Thumb); ok {
|
||||||
|
thumb = o.Thumb()
|
||||||
|
}
|
||||||
|
return &model.ObjThumb{
|
||||||
|
Object: model.Object{
|
||||||
|
ID: newID,
|
||||||
|
Name: newName,
|
||||||
|
Size: o.GetSize(),
|
||||||
|
Modified: o.ModTime(),
|
||||||
|
},
|
||||||
|
Thumbnail: model.Thumbnail{
|
||||||
|
Thumbnail: thumb,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,9 @@ package thunder
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
@ -331,15 +333,32 @@ func (xc *XunLeiCommon) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (xc *XunLeiCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (xc *XunLeiCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
|
tempFile, err := utils.CreateTempFile(stream.GetReadCloser())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tempFile.Close()
|
||||||
|
_ = os.Remove(tempFile.Name())
|
||||||
|
}()
|
||||||
|
|
||||||
|
gcid, err := getGcid(tempFile, stream.GetSize())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tempFile.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var resp UploadTaskResponse
|
var resp UploadTaskResponse
|
||||||
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
_, err = xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||||
r.SetContext(ctx)
|
r.SetContext(ctx)
|
||||||
r.SetBody(&base.Json{
|
r.SetBody(&base.Json{
|
||||||
"kind": FILE,
|
"kind": FILE,
|
||||||
"parent_id": dstDir.GetID(),
|
"parent_id": dstDir.GetID(),
|
||||||
"name": stream.GetName(),
|
"name": stream.GetName(),
|
||||||
"size": stream.GetSize(),
|
"size": stream.GetSize(),
|
||||||
"hash": "1CF254FBC456E1B012CD45C546636AA62CF8350E",
|
"hash": gcid,
|
||||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||||
})
|
})
|
||||||
}, &resp)
|
}, &resp)
|
||||||
@ -362,7 +381,7 @@ func (xc *XunLeiCommon) Put(ctx context.Context, dstDir model.Obj, stream model.
|
|||||||
Bucket: aws.String(param.Bucket),
|
Bucket: aws.String(param.Bucket),
|
||||||
Key: aws.String(param.Key),
|
Key: aws.String(param.Key),
|
||||||
Expires: aws.Time(param.Expiration),
|
Expires: aws.Time(param.Expiration),
|
||||||
Body: stream,
|
Body: tempFile,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package thunder
|
package thunder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
@ -171,3 +174,29 @@ func (c *Common) Request(url, method string, callback base.ReqCallback, resp int
|
|||||||
|
|
||||||
return res.Body(), nil
|
return res.Body(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算文件Gcid
|
||||||
|
func getGcid(r io.Reader, size int64) (string, error) {
|
||||||
|
calcBlockSize := func(j int64) int64 {
|
||||||
|
var psize int64 = 0x40000
|
||||||
|
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
|
||||||
|
psize = psize << 1
|
||||||
|
}
|
||||||
|
return psize
|
||||||
|
}
|
||||||
|
|
||||||
|
hash1 := sha1.New()
|
||||||
|
hash2 := sha1.New()
|
||||||
|
readSize := calcBlockSize(size)
|
||||||
|
for {
|
||||||
|
hash2.Reset()
|
||||||
|
if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
|
||||||
|
if err != io.EOF {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
hash1.Write(hash2.Sum(nil))
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(hash1.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
1
go.mod
1
go.mod
@ -14,6 +14,7 @@ require (
|
|||||||
github.com/deckarep/golang-set/v2 v2.3.0
|
github.com/deckarep/golang-set/v2 v2.3.0
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564
|
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564
|
||||||
|
github.com/foxxorcat/mopan-sdk-go v0.1.0
|
||||||
github.com/gin-contrib/cors v1.4.0
|
github.com/gin-contrib/cors v1.4.0
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/go-resty/resty/v2 v2.7.0
|
github.com/go-resty/resty/v2 v2.7.0
|
||||||
|
3
go.sum
3
go.sum
@ -86,6 +86,8 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
|
|||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJLNCEi7YHVMkwwtfSr2k9splgdSM=
|
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/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=
|
||||||
|
github.com/foxxorcat/mopan-sdk-go v0.1.0 h1:U/E/uNK4N7xNbcHXdw+DG56LWw2W6Xpjj+yoH8EmTj0=
|
||||||
|
github.com/foxxorcat/mopan-sdk-go v0.1.0/go.mod h1:LpBPmwezjQNyhaNo3HGzgFtQbhvxmF5ZybSVuKi7OVA=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
@ -208,6 +210,7 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP
|
|||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
|
||||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
@ -20,11 +20,13 @@ type Database struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Scheme struct {
|
type Scheme struct {
|
||||||
DisableHttp bool `json:"disable_http" env:"DISABLE_HTTP"`
|
Address string `json:"address" env:"ADDR"`
|
||||||
Https bool `json:"https" env:"HTTPS"`
|
HttpPort int `json:"http_port" env:"HTTP_PORT"`
|
||||||
ForceHttps bool `json:"force_https" env:"FORCE_HTTPS"`
|
HttpsPort int `json:"https_port" env:"HTTPS_PORT"`
|
||||||
CertFile string `json:"cert_file" env:"CERT_FILE"`
|
ForceHttps bool `json:"force_https" env:"FORCE_HTTPS"`
|
||||||
KeyFile string `json:"key_file" env:"KEY_FILE"`
|
CertFile string `json:"cert_file" env:"CERT_FILE"`
|
||||||
|
KeyFile string `json:"key_file" env:"KEY_FILE"`
|
||||||
|
UnixFile string `json:"unix_file" env:"UNIX_FILE"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
@ -38,9 +40,6 @@ type LogConfig struct {
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Force bool `json:"force" env:"FORCE"`
|
Force bool `json:"force" env:"FORCE"`
|
||||||
Address string `json:"address" env:"ADDR"`
|
|
||||||
Port int `json:"port" env:"PORT"`
|
|
||||||
HttpsPort int `json:"https_port" env:"HTTPS_PORT"`
|
|
||||||
SiteURL string `json:"site_url" env:"SITE_URL"`
|
SiteURL string `json:"site_url" env:"SITE_URL"`
|
||||||
Cdn string `json:"cdn" env:"CDN"`
|
Cdn string `json:"cdn" env:"CDN"`
|
||||||
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
|
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
|
||||||
@ -61,9 +60,15 @@ func DefaultConfig() *Config {
|
|||||||
logPath := filepath.Join(flags.DataDir, "log/log.log")
|
logPath := filepath.Join(flags.DataDir, "log/log.log")
|
||||||
dbPath := filepath.Join(flags.DataDir, "data.db")
|
dbPath := filepath.Join(flags.DataDir, "data.db")
|
||||||
return &Config{
|
return &Config{
|
||||||
Address: "0.0.0.0",
|
Scheme: Scheme{
|
||||||
Port: 5244,
|
Address: "0.0.0.0",
|
||||||
HttpsPort: 5245,
|
UnixFile: "",
|
||||||
|
HttpPort: 5244,
|
||||||
|
HttpsPort: -1,
|
||||||
|
ForceHttps: false,
|
||||||
|
CertFile: "",
|
||||||
|
KeyFile: "",
|
||||||
|
},
|
||||||
JwtSecret: random.String(16),
|
JwtSecret: random.String(16),
|
||||||
TokenExpiresIn: 48,
|
TokenExpiresIn: 48,
|
||||||
TempDir: tempDir,
|
TempDir: tempDir,
|
||||||
|
@ -46,9 +46,9 @@ func IsSubPath(path string, subPath string) bool {
|
|||||||
func Ext(path string) string {
|
func Ext(path string) string {
|
||||||
ext := stdpath.Ext(path)
|
ext := stdpath.Ext(path)
|
||||||
if strings.HasPrefix(ext, ".") {
|
if strings.HasPrefix(ext, ".") {
|
||||||
return ext[1:]
|
ext = ext[1:]
|
||||||
}
|
}
|
||||||
return ext
|
return strings.ToLower(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func EncodePath(path string, all ...bool) string {
|
func EncodePath(path string, all ...bool) string {
|
||||||
|
@ -60,3 +60,12 @@ func MergeErrors(errs ...error) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SliceMeet[T1, T2 any](arr []T1, v T2, meet func(item T1, v T2) bool) bool {
|
||||||
|
for _, item := range arr {
|
||||||
|
if meet(item, v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -48,8 +48,16 @@ func AddAria2(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !aria2.IsAria2Ready() {
|
if !aria2.IsAria2Ready() {
|
||||||
common.ErrorStrResp(c, "aria2 not ready", 500)
|
// try to init client
|
||||||
return
|
_, err := aria2.InitClient(2)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !aria2.IsAria2Ready() {
|
||||||
|
common.ErrorStrResp(c, "aria2 still not ready after init", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var req AddAria2Req
|
var req AddAria2Req
|
||||||
if err := c.ShouldBind(&req); err != nil {
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
|
211
server/handles/fsbatch.go
Normal file
211
server/handles/fsbatch.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package handles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
|
"github.com/alist-org/alist/v3/internal/fs"
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/generic"
|
||||||
|
"github.com/alist-org/alist/v3/server/common"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BatchRenameReq struct {
|
||||||
|
SrcDir string `json:"src_dir"`
|
||||||
|
RenameObjects []struct {
|
||||||
|
SrcName string `json:"src_name"`
|
||||||
|
NewName string `json:"new_name"`
|
||||||
|
} `json:"rename_objects"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FsBatchRename(c *gin.Context) {
|
||||||
|
var req BatchRenameReq
|
||||||
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
|
common.ErrorResp(c, err, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := c.MustGet("user").(*model.User)
|
||||||
|
if !user.CanRename() {
|
||||||
|
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reqPath, err := user.JoinPath(req.SrcDir)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := op.GetNearestMeta(reqPath)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||||
|
common.ErrorResp(c, err, 500, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Set("meta", meta)
|
||||||
|
for _, renameObject := range req.RenameObjects {
|
||||||
|
if renameObject.SrcName == "" || renameObject.NewName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filePath := fmt.Sprintf("%s/%s", reqPath, renameObject.SrcName)
|
||||||
|
if err := fs.Rename(c, filePath, renameObject.NewName); err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
common.SuccessResp(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecursiveMoveReq struct {
|
||||||
|
SrcDir string `json:"src_dir"`
|
||||||
|
DstDir string `json:"dst_dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FsRecursiveMove(c *gin.Context) {
|
||||||
|
var req RecursiveMoveReq
|
||||||
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
|
common.ErrorResp(c, err, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := c.MustGet("user").(*model.User)
|
||||||
|
if !user.CanMove() {
|
||||||
|
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srcDir, err := user.JoinPath(req.SrcDir)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dstDir, err := user.JoinPath(req.DstDir)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := op.GetNearestMeta(srcDir)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||||
|
common.ErrorResp(c, err, 500, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Set("meta", meta)
|
||||||
|
|
||||||
|
rootFiles, err := fs.List(c, srcDir, &fs.ListArgs{})
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// record the file path
|
||||||
|
filePathMap := make(map[model.Obj]string)
|
||||||
|
movingFiles := generic.NewQueue[model.Obj]()
|
||||||
|
for _, file := range rootFiles {
|
||||||
|
movingFiles.Push(file)
|
||||||
|
filePathMap[file] = srcDir
|
||||||
|
}
|
||||||
|
|
||||||
|
for !movingFiles.IsEmpty() {
|
||||||
|
|
||||||
|
movingFile := movingFiles.Pop()
|
||||||
|
movingFilePath := filePathMap[movingFile]
|
||||||
|
movingFileName := fmt.Sprintf("%s/%s", movingFilePath, movingFile.GetName())
|
||||||
|
if movingFile.IsDir() {
|
||||||
|
// directory, recursive move
|
||||||
|
subFilePath := movingFileName
|
||||||
|
subFiles, err := fs.List(c, movingFileName, &fs.ListArgs{Refresh: true})
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, subFile := range subFiles {
|
||||||
|
movingFiles.Push(subFile)
|
||||||
|
filePathMap[subFile] = subFilePath
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if movingFilePath == dstDir {
|
||||||
|
// same directory, don't move
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// move
|
||||||
|
err := fs.Move(c, movingFileName, dstDir, movingFiles.IsEmpty())
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
common.SuccessResp(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegexRenameReq struct {
|
||||||
|
SrcDir string `json:"src_dir"`
|
||||||
|
SrcNameRegex string `json:"src_name_regex"`
|
||||||
|
NewNameRegex string `json:"new_name_regex"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FsRegexRename(c *gin.Context) {
|
||||||
|
var req RegexRenameReq
|
||||||
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
|
common.ErrorResp(c, err, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := c.MustGet("user").(*model.User)
|
||||||
|
if !user.CanRename() {
|
||||||
|
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reqPath, err := user.JoinPath(req.SrcDir)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := op.GetNearestMeta(reqPath)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||||
|
common.ErrorResp(c, err, 500, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Set("meta", meta)
|
||||||
|
|
||||||
|
srcRegexp, err := regexp.Compile(req.SrcNameRegex)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := fs.List(c, reqPath, &fs.ListArgs{})
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
|
||||||
|
if srcRegexp.MatchString(file.GetName()) {
|
||||||
|
filePath := fmt.Sprintf("%s/%s", reqPath, file.GetName())
|
||||||
|
newFileName := srcRegexp.ReplaceAllString(file.GetName(), req.NewNameRegex)
|
||||||
|
if err := fs.Rename(c, filePath, newFileName); err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
common.SuccessResp(c)
|
||||||
|
}
|
@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
stdpath "path"
|
stdpath "path"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/fs"
|
"github.com/alist-org/alist/v3/internal/fs"
|
||||||
@ -96,94 +95,6 @@ func FsMove(c *gin.Context) {
|
|||||||
common.SuccessResp(c)
|
common.SuccessResp(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecursiveMoveReq struct {
|
|
||||||
SrcDir string `json:"src_dir"`
|
|
||||||
DstDir string `json:"dst_dir"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func FsRecursiveMove(c *gin.Context) {
|
|
||||||
var req RecursiveMoveReq
|
|
||||||
if err := c.ShouldBind(&req); err != nil {
|
|
||||||
common.ErrorResp(c, err, 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := c.MustGet("user").(*model.User)
|
|
||||||
if !user.CanMove() {
|
|
||||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
srcDir, err := user.JoinPath(req.SrcDir)
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 403)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dstDir, err := user.JoinPath(req.DstDir)
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 403)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
meta, err := op.GetNearestMeta(srcDir)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
|
||||||
common.ErrorResp(c, err, 500, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Set("meta", meta)
|
|
||||||
|
|
||||||
rootFiles, err := fs.List(c, srcDir, &fs.ListArgs{})
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// record the file path
|
|
||||||
filePathMap := make(map[model.Obj]string)
|
|
||||||
movingFiles := generic.NewQueue[model.Obj]()
|
|
||||||
for _, file := range rootFiles {
|
|
||||||
movingFiles.Push(file)
|
|
||||||
filePathMap[file] = srcDir
|
|
||||||
}
|
|
||||||
|
|
||||||
for !movingFiles.IsEmpty() {
|
|
||||||
|
|
||||||
movingFile := movingFiles.Pop()
|
|
||||||
movingFilePath := filePathMap[movingFile]
|
|
||||||
movingFileName := fmt.Sprintf("%s/%s", movingFilePath, movingFile.GetName())
|
|
||||||
if movingFile.IsDir() {
|
|
||||||
// directory, recursive move
|
|
||||||
subFilePath := movingFileName
|
|
||||||
subFiles, err := fs.List(c, movingFileName, &fs.ListArgs{Refresh: true})
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, subFile := range subFiles {
|
|
||||||
movingFiles.Push(subFile)
|
|
||||||
filePathMap[subFile] = subFilePath
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
if movingFilePath == dstDir {
|
|
||||||
// same directory, don't move
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// move
|
|
||||||
err := fs.Move(c, movingFileName, dstDir, movingFiles.IsEmpty())
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FsCopy(c *gin.Context) {
|
func FsCopy(c *gin.Context) {
|
||||||
var req MoveCopyReq
|
var req MoveCopyReq
|
||||||
if err := c.ShouldBind(&req); err != nil {
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
@ -255,67 +166,6 @@ func FsRename(c *gin.Context) {
|
|||||||
common.SuccessResp(c)
|
common.SuccessResp(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegexRenameReq struct {
|
|
||||||
SrcDir string `json:"src_dir"`
|
|
||||||
SrcNameRegex string `json:"src_name_regex"`
|
|
||||||
NewNameRegex string `json:"new_name_regex"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func FsRegexRename(c *gin.Context) {
|
|
||||||
var req RegexRenameReq
|
|
||||||
if err := c.ShouldBind(&req); err != nil {
|
|
||||||
common.ErrorResp(c, err, 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user := c.MustGet("user").(*model.User)
|
|
||||||
if !user.CanRename() {
|
|
||||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reqPath, err := user.JoinPath(req.SrcDir)
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 403)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
meta, err := op.GetNearestMeta(reqPath)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
|
||||||
common.ErrorResp(c, err, 500, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Set("meta", meta)
|
|
||||||
|
|
||||||
srcRegexp, err := regexp.Compile(req.SrcNameRegex)
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := fs.List(c, reqPath, &fs.ListArgs{})
|
|
||||||
if err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
|
|
||||||
if srcRegexp.MatchString(file.GetName()) {
|
|
||||||
filePath := fmt.Sprintf("%s/%s", reqPath, file.GetName())
|
|
||||||
newFileName := srcRegexp.ReplaceAllString(file.GetName(), req.NewNameRegex)
|
|
||||||
if err := fs.Rename(c, filePath, newFileName); err != nil {
|
|
||||||
common.ErrorResp(c, err, 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
common.SuccessResp(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoveReq struct {
|
type RemoveReq struct {
|
||||||
Dir string `json:"dir"`
|
Dir string `json:"dir"`
|
||||||
Names []string `json:"names"`
|
Names []string `json:"names"`
|
||||||
|
@ -47,8 +47,16 @@ func AddQbittorrent(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !qbittorrent.IsQbittorrentReady() {
|
if !qbittorrent.IsQbittorrentReady() {
|
||||||
common.ErrorStrResp(c, "qbittorrent not ready", 500)
|
// try to init client
|
||||||
return
|
err := qbittorrent.InitClient()
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !qbittorrent.IsQbittorrentReady() {
|
||||||
|
common.ErrorStrResp(c, "qbittorrent still not ready after init", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var req AddQbittorrentReq
|
var req AddQbittorrentReq
|
||||||
if err := c.ShouldBind(&req); err != nil {
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package handles
|
package handles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base32"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
"github.com/alist-org/alist/v3/internal/db"
|
"github.com/alist-org/alist/v3/internal/db"
|
||||||
@ -15,9 +17,20 @@ import (
|
|||||||
"github.com/coreos/go-oidc"
|
"github.com/coreos/go-oidc"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var opts = totp.ValidateOpts{
|
||||||
|
// state verify won't expire in 30 secs, which is quite enough for the callback
|
||||||
|
Period: 30,
|
||||||
|
Skew: 1,
|
||||||
|
// in some OIDC providers(such as Authelia), state parameter must be at least 8 characters
|
||||||
|
Digits: otp.DigitsEight,
|
||||||
|
Algorithm: otp.AlgorithmSHA1,
|
||||||
|
}
|
||||||
|
|
||||||
func SSOLoginRedirect(c *gin.Context) {
|
func SSOLoginRedirect(c *gin.Context) {
|
||||||
method := c.Query("method")
|
method := c.Query("method")
|
||||||
enabled := setting.GetBool(conf.SSOLoginEnabled)
|
enabled := setting.GetBool(conf.SSOLoginEnabled)
|
||||||
@ -62,7 +75,13 @@ func SSOLoginRedirect(c *gin.Context) {
|
|||||||
common.ErrorStrResp(c, err.Error(), 400)
|
common.ErrorStrResp(c, err.Error(), 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Redirect(http.StatusFound, oauth2Config.AuthCodeURL("state"))
|
// generate state parameter
|
||||||
|
state,err := totp.GenerateCodeCustom(base32.StdEncoding.EncodeToString([]byte(oauth2Config.ClientSecret)), time.Now(), opts)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorStrResp(c, err.Error(), 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusFound, oauth2Config.AuthCodeURL(state))
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
common.ErrorStrResp(c, "invalid platform", 400)
|
common.ErrorStrResp(c, "invalid platform", 400)
|
||||||
@ -117,6 +136,17 @@ func OIDCLoginCallback(c *gin.Context) {
|
|||||||
common.ErrorResp(c, err, 400)
|
common.ErrorResp(c, err, 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// add state verify process
|
||||||
|
stateVerification, err := totp.ValidateCustom(c.Query("state"), base32.StdEncoding.EncodeToString([]byte(oauth2Config.ClientSecret)), time.Now(), opts)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !stateVerification {
|
||||||
|
common.ErrorStrResp(c, "incorrect or expired state parameter", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
oauth2Token, err := oauth2Config.Exchange(c, c.Query("code"))
|
oauth2Token, err := oauth2Config.Exchange(c, c.Query("code"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResp(c, err, 400)
|
common.ErrorResp(c, err, 400)
|
||||||
|
@ -12,7 +12,7 @@ func ForceHttps(c *gin.Context) {
|
|||||||
if c.Request.TLS == nil {
|
if c.Request.TLS == nil {
|
||||||
host := c.Request.Host
|
host := c.Request.Host
|
||||||
// change port to https port
|
// change port to https port
|
||||||
host = strings.Replace(host, fmt.Sprintf(":%d", conf.Conf.Port), fmt.Sprintf(":%d", conf.Conf.HttpsPort), 1)
|
host = strings.Replace(host, fmt.Sprintf(":%d", conf.Conf.Scheme.HttpPort), fmt.Sprintf(":%d", conf.Conf.Scheme.HttpsPort), 1)
|
||||||
c.Redirect(302, "https://"+host+c.Request.RequestURI)
|
c.Redirect(302, "https://"+host+c.Request.RequestURI)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
|
@ -21,7 +21,7 @@ func Init(e *gin.Engine) {
|
|||||||
}
|
}
|
||||||
Cors(e)
|
Cors(e)
|
||||||
g := e.Group(conf.URL.Path)
|
g := e.Group(conf.URL.Path)
|
||||||
if conf.Conf.Scheme.Https && conf.Conf.Scheme.ForceHttps && !conf.Conf.Scheme.DisableHttp {
|
if conf.Conf.Scheme.HttpPort != -1 && conf.Conf.Scheme.HttpsPort != -1 && conf.Conf.Scheme.ForceHttps {
|
||||||
g.Use(middlewares.ForceHttps)
|
g.Use(middlewares.ForceHttps)
|
||||||
}
|
}
|
||||||
g.Any("/ping", func(c *gin.Context) {
|
g.Any("/ping", func(c *gin.Context) {
|
||||||
@ -130,6 +130,7 @@ func _fs(g *gin.RouterGroup) {
|
|||||||
g.Any("/dirs", handles.FsDirs)
|
g.Any("/dirs", handles.FsDirs)
|
||||||
g.POST("/mkdir", handles.FsMkdir)
|
g.POST("/mkdir", handles.FsMkdir)
|
||||||
g.POST("/rename", handles.FsRename)
|
g.POST("/rename", handles.FsRename)
|
||||||
|
g.POST("/batch_rename", handles.FsBatchRename)
|
||||||
g.POST("/regex_rename", handles.FsRegexRename)
|
g.POST("/regex_rename", handles.FsRegexRename)
|
||||||
g.POST("/move", handles.FsMove)
|
g.POST("/move", handles.FsMove)
|
||||||
g.POST("/recursive_move", handles.FsRecursiveMove)
|
g.POST("/recursive_move", handles.FsRecursiveMove)
|
||||||
|
@ -71,6 +71,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
status, err = h.handleUnlock(brw, r)
|
status, err = h.handleUnlock(brw, r)
|
||||||
case "PROPFIND":
|
case "PROPFIND":
|
||||||
status, err = h.handlePropfind(brw, r)
|
status, err = h.handlePropfind(brw, r)
|
||||||
|
// if there is a error for PROPFIND, we should be as an empty folder to the client
|
||||||
|
if err != nil {
|
||||||
|
status = http.StatusNotFound
|
||||||
|
}
|
||||||
case "PROPPATCH":
|
case "PROPPATCH":
|
||||||
status, err = h.handleProppatch(brw, r)
|
status, err = h.handleProppatch(brw, r)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user