Compare commits

...

30 Commits

Author SHA1 Message Date
4bf46268da feat(alias): support thumbnail (close #4256) 2023-04-28 00:17:15 +08:00
b7ea73b3c2 fix(aliyundrive_open): can't refresh token if access_token is empty (#4255) 2023-04-28 00:01:47 +08:00
9fbc54314d chore(aliyundrive_open): change base url 2023-04-27 16:38:40 +08:00
cf8ab29a17 feat: optional allow be mounted (close #4218) 2023-04-27 16:33:01 +08:00
51cadd2d49 fix: ignore handle in json (close #4251 close #4252) 2023-04-27 15:39:32 +08:00
2bae8e129e feat: add Casdoor single sign-on (#4222) 2023-04-26 16:01:40 +08:00
9d55ad3af6 fix(123): get download url (close #4244) 2023-04-26 15:06:24 +08:00
36cd504783 fix(alist_v3): missed meta_password update
fix: adb0739dfe (commitcomment-110328033)
2023-04-24 20:56:46 +08:00
49f13b9b90 fix(baidu_photo): upload file has web prefix (close #4233 in #4235) 2023-04-24 19:13:33 +08:00
adb0739dfe feat!(alist_v3): support username & password login (close #4226)
Breaking changes:
- rename access_token to token
- rename old password to meta_password
2023-04-23 17:48:26 +08:00
340cb940e3 fix(qbittorrent): set autoTMM (#4217) 2023-04-22 13:33:54 +08:00
8711f2a1c5 feat(quark): shard request file (close #4175) 2023-04-17 15:33:38 +08:00
7f35aab071 revert(quark): remove preset range header 2023-04-17 14:39:21 +08:00
ecd167d2f9 feat(quark): add preset range header (close #4166) 2023-04-16 19:26:03 +08:00
220fd30830 fix: the recursive subdirectory moving bug (#4171) 2023-04-16 16:08:12 +08:00
5cba10446e fix(123): adapt new upload method (close #4141) 2023-04-14 15:48:39 +08:00
a9bdb15205 ci: fix golang version in auto_lang [skip ci] 2023-04-14 13:49:13 +08:00
c5f6a90f54 fix(quark): download file size limit (close #4140) 2023-04-14 13:47:05 +08:00
46f9aefb04 feat: empty folder clear API [ckip ci] (#4132)
* 增加清理空文件夹API

* 修复嵌套文件夹删除Bug

 Author:    varg247 <varg247@gmail.com>

---------

Co-authored-by: varg247 <varg247@qq.com>
2023-04-13 15:39:21 +08:00
fdcad9c154 fix(123): incorrect endpoint (close #4046) 2023-04-12 23:04:12 +08:00
027025361a ci: fixed version of alpine 2023-04-12 16:01:49 +08:00
f1245153b9 chore(deps): upgrade to go@1.20 2023-04-12 15:42:27 +08:00
570b8be022 fix(onedrive): error check in upBig 2023-04-11 22:52:42 +08:00
86a773674a feat(task): print stack trace if panic 2023-04-11 15:16:57 +08:00
75fd0ee185 feat(s3): optional remove bucket name from path (close #4069) 2023-04-09 19:25:52 +08:00
cc43238bd1 fix(alias): disable log completely (#4054) 2023-04-09 15:46:26 +08:00
c0a6beecea fix(alias): panic on nil pointer (close #4093) 2023-04-09 14:06:04 +08:00
c77eebb035 fix(deps): update module golang.org/x/image to v0.7.0 (#4065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-08 21:51:51 +08:00
b1efb86b28 fix(deps): update module golang.org/x/net to v0.9.0 [skip ci] (#4066)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-08 21:20:19 +08:00
0707449c8f fix(deps): update module golang.org/x/crypto to v0.8.0 [skip ci] (#4076)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-08 21:18:39 +08:00
36 changed files with 594 additions and 174 deletions

View File

@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
platform: [ ubuntu-latest ] platform: [ ubuntu-latest ]
go-version: [ 1.19 ] go-version: [ '1.20' ]
name: auto generate lang.json name: auto generate lang.json
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:

View File

@ -11,7 +11,7 @@ jobs:
strategy: strategy:
matrix: matrix:
platform: [ubuntu-latest] platform: [ubuntu-latest]
go-version: [1.19] go-version: [ '1.20' ]
name: Build name: Build
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:

View File

@ -9,7 +9,7 @@ jobs:
strategy: strategy:
matrix: matrix:
platform: [ ubuntu-latest ] platform: [ ubuntu-latest ]
go-version: [ 1.19 ] go-version: [ '1.20' ]
name: Release name: Release
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:

View File

@ -1,11 +1,11 @@
FROM alpine:edge as builder FROM alpine:3.17 as builder
LABEL stage=go-builder LABEL stage=go-builder
WORKDIR /app/ WORKDIR /app/
COPY ./ ./ COPY ./ ./
RUN apk add --no-cache bash curl gcc git go musl-dev; \ RUN apk add --no-cache bash curl gcc git go musl-dev; \
bash build.sh release docker bash build.sh release docker
FROM alpine:edge FROM alpine:3.17
LABEL MAINTAINER="i@nn.ci" LABEL MAINTAINER="i@nn.ci"
VOLUME /opt/alist/data/ VOLUME /opt/alist/data/
WORKDIR /opt/alist/ WORKDIR /opt/alist/

View File

@ -97,7 +97,8 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
} }
} }
u_ := u.String() u_ := u.String()
res, err := base.NoRedirectClient.R().SetQueryParamsFromValues(u.Query()).Head(u_) log.Debug("download url: ", u_)
res, err := base.NoRedirectClient.R().Get(u_)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -108,6 +109,8 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
log.Debugln("res code: ", res.StatusCode()) log.Debugln("res code: ", res.StatusCode())
if res.StatusCode() == 302 { if res.StatusCode() == 302 {
link.URL = res.Header().Get("location") link.URL = res.Header().Get("location")
} else if res.StatusCode() == 200 {
link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
} }
return &link, nil return &link, nil
} else { } else {
@ -220,32 +223,37 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
"type": 0, "type": 0,
} }
var resp UploadResp var resp UploadResp
_, err := d.request(UploadRequest, http.MethodPost, func(req *resty.Request) { res, err := d.request(UploadRequest, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx) req.SetBody(data).SetContext(ctx)
}, &resp) }, &resp)
if err != nil { if err != nil {
return err return err
} }
log.Debugln("upload request res: ", string(res))
if resp.Data.Reuse || resp.Data.Key == "" { if resp.Data.Reuse || resp.Data.Key == "" {
return nil return nil
} }
cfg := &aws.Config{ if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" {
Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken), err = d.newUpload(ctx, &resp, stream, uploadFile, up)
Region: aws.String("123pan"), } else {
Endpoint: aws.String("file.123pan.com"), cfg := &aws.Config{
S3ForcePathStyle: aws.Bool(true), Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),
Region: aws.String("123pan"),
Endpoint: aws.String(resp.Data.EndPoint),
S3ForcePathStyle: aws.Bool(true),
}
s, err := session.NewSession(cfg)
if err != nil {
return err
}
uploader := s3manager.NewUploader(s)
input := &s3manager.UploadInput{
Bucket: &resp.Data.Bucket,
Key: &resp.Data.Key,
Body: uploadFile,
}
_, err = uploader.UploadWithContext(ctx, input)
} }
s, err := session.NewSession(cfg)
if err != nil {
return err
}
uploader := s3manager.NewUploader(s)
input := &s3manager.UploadInput{
Bucket: &resp.Data.Bucket,
Key: &resp.Data.Key,
Body: uploadFile,
}
_, err = uploader.UploadWithContext(ctx, input)
if err != nil { if err != nil {
return err return err
} }

View File

@ -74,5 +74,14 @@ type UploadResp struct {
SessionToken string `json:"SessionToken"` SessionToken string `json:"SessionToken"`
FileId int64 `json:"FileId"` FileId int64 `json:"FileId"`
Reuse bool `json:"Reuse"` Reuse bool `json:"Reuse"`
EndPoint string `json:"EndPoint"`
StorageNode string `json:"StorageNode"`
UploadId string `json:"UploadId"`
} `json:"data"`
}
type S3PreSignedURLs struct {
Data struct {
PreSignedUrls map[string]string `json:"presignedUrls"`
} `json:"data"` } `json:"data"`
} }

127
drivers/123/upload.go Normal file
View File

@ -0,0 +1,127 @@
package _123
import (
"context"
"fmt"
"io"
"math"
"net/http"
"strconv"
"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"
)
func (d *Pan123) getS3PreSignedUrls(ctx context.Context, upReq *UploadResp, start, end int) (*S3PreSignedURLs, error) {
data := base.Json{
"bucket": upReq.Data.Bucket,
"key": upReq.Data.Key,
"partNumberEnd": end,
"partNumberStart": start,
"uploadId": upReq.Data.UploadId,
"StorageNode": upReq.Data.StorageNode,
}
var s3PreSignedUrls S3PreSignedURLs
_, err := d.request(S3PreSignedUrls, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, &s3PreSignedUrls)
if err != nil {
return nil, err
}
return &s3PreSignedUrls, nil
}
func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp) error {
data := base.Json{
"bucket": upReq.Data.Bucket,
"key": upReq.Data.Key,
"uploadId": upReq.Data.UploadId,
"StorageNode": upReq.Data.StorageNode,
}
_, err := d.request(S3Complete, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, nil)
return err
}
func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, reader io.Reader, up driver.UpdateProgress) error {
chunkSize := int64(1024 * 1024 * 5)
// fetch s3 pre signed urls
chunkCount := int(math.Ceil(float64(file.GetSize()) / float64(chunkSize)))
// upload 10 chunks each batch
batchSize := 10
for i := 1; i <= chunkCount; i += batchSize {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
start := i
end := i + batchSize
if end > chunkCount+1 {
end = chunkCount + 1
}
s3PreSignedUrls, err := d.getS3PreSignedUrls(ctx, upReq, start, end)
if err != nil {
return err
}
// upload each chunk
for j := start; j < end; j++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
curSize := chunkSize
if j == chunkCount {
curSize = file.GetSize() - (int64(chunkCount)-1)*chunkSize
}
err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.LimitReader(reader, chunkSize), curSize, false)
if err != nil {
return err
}
up(j * 100 / chunkCount)
}
}
// complete s3 upload
return d.completeS3(ctx, upReq)
}
func (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSignedUrls *S3PreSignedURLs, cur, end int, reader io.Reader, curSize int64, retry bool) error {
uploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]
if uploadUrl == "" {
return fmt.Errorf("upload url is empty, s3PreSignedUrls: %+v", s3PreSignedUrls)
}
req, err := http.NewRequest("PUT", uploadUrl, reader)
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = curSize
//req.Header.Set("Content-Length", strconv.FormatInt(curSize, 10))
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode == http.StatusForbidden {
if retry {
return fmt.Errorf("upload s3 chunk %d failed, status code: %d", cur, res.StatusCode)
}
// refresh s3 pre signed urls
newS3PreSignedUrls, err := d.getS3PreSignedUrls(ctx, upReq, cur, end)
if err != nil {
return err
}
s3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls
// retry
return d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, cur, end, reader, curSize, true)
}
if res.StatusCode != http.StatusOK {
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
return fmt.Errorf("upload s3 chunk %d failed, status code: %d, body: %s", cur, res.StatusCode, body)
}
return nil
}

View File

@ -15,17 +15,19 @@ 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" API = "https://www.123pan.com/b/api"
SignIn = API + "/user/sign_in" SignIn = API + "/user/sign_in"
UserInfo = API + "/user/info" UserInfo = API + "/user/info"
FileList = API + "/file/list/new" FileList = API + "/file/list/new"
DownloadInfo = API + "/file/download_info" DownloadInfo = "https://www.123pan.com/a/api/file/download_info"
Mkdir = API + "/file/upload_request" Mkdir = API + "/file/upload_request"
Move = API + "/file/mod_pid" Move = API + "/file/mod_pid"
Rename = API + "/file/rename" Rename = API + "/file/rename"
Trash = API + "/file/trash" Trash = API + "/file/trash"
UploadRequest = API + "/file/upload_request" UploadRequest = API + "/file/upload_request"
UploadComplete = API + "/file/upload_complete" UploadComplete = API + "/file/upload_complete"
S3PreSignedUrls = API + "/file/s3_repare_upload_parts_batch"
S3Complete = API + "/file/s3_complete_multipart_upload"
) )
func (d *Pan123) login() error { func (d *Pan123) login() error {

View File

@ -72,18 +72,28 @@ func (d *Alias) list(ctx context.Context, dst, sub string) ([]model.Obj, error)
return nil, err return nil, err
} }
return utils.SliceConvert(objs, func(obj model.Obj) (model.Obj, error) { return utils.SliceConvert(objs, func(obj model.Obj) (model.Obj, error) {
return &model.Object{ thumb, ok := model.GetThumb(obj)
objRes := model.Object{
Name: obj.GetName(), Name: obj.GetName(),
Size: obj.GetSize(), Size: obj.GetSize(),
Modified: obj.ModTime(), Modified: obj.ModTime(),
IsFolder: obj.IsDir(), IsFolder: obj.IsDir(),
}
if !ok {
return &objRes, nil
}
return &model.ObjThumb{
Object: objRes,
Thumbnail: model.Thumbnail{
Thumbnail: thumb,
},
}, nil }, nil
}) })
} }
func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) { func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) {
reqPath := stdpath.Join(dst, sub) reqPath := stdpath.Join(dst, sub)
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{NoLog: true}) storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,16 +2,19 @@ package alist_v3
import ( import (
"context" "context"
"errors" "fmt"
"io" "net/http"
"path" "path"
"strconv" "strconv"
"strings" "strings"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common" "github.com/alist-org/alist/v3/server/common"
"github.com/go-resty/resty/v2"
) )
type AListV3 struct { type AListV3 struct {
@ -29,9 +32,39 @@ func (d *AListV3) GetAddition() driver.Additional {
func (d *AListV3) Init(ctx context.Context) error { func (d *AListV3) Init(ctx context.Context) error {
d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/") d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/")
// TODO login / refresh token var resp common.Resp[MeResp]
//op.MustSaveDriverStorage(d) _, err := d.request("/me", http.MethodGet, func(req *resty.Request) {
return nil req.SetResult(&resp)
})
if err != nil {
return err
}
// if the username is not empty and the username is not the same as the current username, then login again
if d.Username != "" && d.Username != resp.Data.Username {
err = d.login()
if err != nil {
return err
}
}
// re-get the user info
_, err = d.request("/me", http.MethodGet, func(req *resty.Request) {
req.SetResult(&resp)
})
if err != nil {
return err
}
if resp.Data.Role == model.GUEST {
url := d.Address + "/api/public/settings"
res, err := base.RestyClient.R().Get(url)
if err != nil {
return err
}
allowMounted := utils.Json.Get(res.Body(), "data", conf.AllowMounted).ToString() == "true"
if !allowMounted {
return fmt.Errorf("the site does not allow mounted")
}
}
return err
} }
func (d *AListV3) Drop(ctx context.Context) error { func (d *AListV3) Drop(ctx context.Context) error {
@ -39,26 +72,21 @@ func (d *AListV3) Drop(ctx context.Context) error {
} }
func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
url := d.Address + "/api/fs/list"
var resp common.Resp[FsListResp] var resp common.Resp[FsListResp]
_, err := base.RestyClient.R(). _, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) {
SetResult(&resp). req.SetResult(&resp).SetBody(ListReq{
SetHeader("Authorization", d.AccessToken).
SetBody(ListReq{
PageReq: model.PageReq{ PageReq: model.PageReq{
Page: 1, Page: 1,
PerPage: 0, PerPage: 0,
}, },
Path: dir.GetPath(), Path: dir.GetPath(),
Password: d.Password, Password: d.MetaPassword,
Refresh: false, Refresh: false,
}).Post(url) })
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if resp.Code != 200 {
return nil, errors.New(resp.Message)
}
var files []model.Obj var files []model.Obj
for _, f := range resp.Data.Content { for _, f := range resp.Data.Content {
file := model.ObjThumb{ file := model.ObjThumb{
@ -76,107 +104,80 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
} }
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
url := d.Address + "/api/fs/get"
var resp common.Resp[FsGetResp] var resp common.Resp[FsGetResp]
_, err := base.RestyClient.R(). _, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
SetResult(&resp). req.SetResult(&resp).SetBody(FsGetReq{
SetHeader("Authorization", d.AccessToken).
SetBody(FsGetReq{
Path: file.GetPath(), Path: file.GetPath(),
Password: d.Password, Password: d.MetaPassword,
}).Post(url) })
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if resp.Code != 200 {
return nil, errors.New(resp.Message)
}
return &model.Link{ return &model.Link{
URL: resp.Data.RawURL, URL: resp.Data.RawURL,
}, nil }, nil
} }
func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
url := d.Address + "/api/fs/mkdir" _, err := d.request("/fs/mkdir", http.MethodPost, func(req *resty.Request) {
var resp common.Resp[interface{}] req.SetBody(MkdirOrLinkReq{
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(MkdirOrLinkReq{
Path: path.Join(parentDir.GetPath(), dirName), Path: path.Join(parentDir.GetPath(), dirName),
}).Post(url) })
return checkResp(resp, err) })
return err
} }
func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
url := d.Address + "/api/fs/move" _, err := d.request("/fs/move", http.MethodPost, func(req *resty.Request) {
var resp common.Resp[interface{}] req.SetBody(MoveCopyReq{
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(MoveCopyReq{
SrcDir: path.Dir(srcObj.GetPath()), SrcDir: path.Dir(srcObj.GetPath()),
DstDir: dstDir.GetPath(), DstDir: dstDir.GetPath(),
Names: []string{srcObj.GetName()}, Names: []string{srcObj.GetName()},
}).Post(url) })
return checkResp(resp, err) })
return err
} }
func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error { func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
url := d.Address + "/api/fs/rename" _, err := d.request("/fs/rename", http.MethodPost, func(req *resty.Request) {
var resp common.Resp[interface{}] req.SetBody(RenameReq{
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(RenameReq{
Path: srcObj.GetPath(), Path: srcObj.GetPath(),
Name: newName, Name: newName,
}).Post(url) })
return checkResp(resp, err) })
return err
} }
func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
url := d.Address + "/api/fs/copy" _, err := d.request("/fs/copy", http.MethodPost, func(req *resty.Request) {
var resp common.Resp[interface{}] req.SetBody(MoveCopyReq{
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(MoveCopyReq{
SrcDir: path.Dir(srcObj.GetPath()), SrcDir: path.Dir(srcObj.GetPath()),
DstDir: dstDir.GetPath(), DstDir: dstDir.GetPath(),
Names: []string{srcObj.GetName()}, Names: []string{srcObj.GetName()},
}).Post(url) })
return checkResp(resp, err) })
return err
} }
func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error { func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
url := d.Address + "/api/fs/remove" _, err := d.request("/fs/remove", http.MethodPost, func(req *resty.Request) {
var resp common.Resp[interface{}] req.SetBody(RemoveReq{
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(RemoveReq{
Dir: path.Dir(obj.GetPath()), Dir: path.Dir(obj.GetPath()),
Names: []string{obj.GetName()}, Names: []string{obj.GetName()},
}).Post(url) })
return checkResp(resp, err) })
return err
} }
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
url := d.Address + "/api/fs/put" _, err := d.request("/fs/put", http.MethodPut, func(req *resty.Request) {
var resp common.Resp[interface{}] req.SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
fileBytes, err := io.ReadAll(stream.GetReadCloser()) SetHeader("Password", d.MetaPassword).
if err != nil { SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
return nil SetBody(stream.GetReadCloser())
} })
_, err = base.RestyClient.R().SetContext(ctx). return err
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
SetHeader("Password", d.Password).
SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
SetBody(fileBytes).Put(url)
return checkResp(resp, err)
} }
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { //func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {

View File

@ -7,15 +7,18 @@ import (
type Addition struct { type Addition struct {
driver.RootPath driver.RootPath
Address string `json:"url" required:"true"` Address string `json:"url" required:"true"`
Password string `json:"password"` MetaPassword string `json:"meta_password"`
AccessToken string `json:"access_token"` Username string `json:"username"`
Password string `json:"password"`
Token string `json:"token"`
} }
var config = driver.Config{ var config = driver.Config{
Name: "AList V3", Name: "AList V3",
LocalSort: true, LocalSort: true,
DefaultRoot: "/", DefaultRoot: "/",
CheckStatus: true,
} }
func init() { func init() {

View File

@ -63,3 +63,19 @@ type RemoveReq struct {
Dir string `json:"dir"` Dir string `json:"dir"`
Names []string `json:"names"` Names []string `json:"names"`
} }
type LoginResp struct {
Token string `json:"token"`
}
type MeResp struct {
Id int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
BasePath string `json:"base_path"`
Role int `json:"role"`
Disabled bool `json:"disabled"`
Permission int `json:"permission"`
SsoId string `json:"sso_id"`
Otp bool `json:"otp"`
}

View File

@ -1,17 +1,56 @@
package alist_v3 package alist_v3
import ( import (
"errors" "fmt"
"net/http"
"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/alist-org/alist/v3/server/common" "github.com/alist-org/alist/v3/server/common"
"github.com/go-resty/resty/v2"
) )
func checkResp(resp common.Resp[interface{}], err error) error { func (d *AListV3) login() error {
var resp common.Resp[LoginResp]
_, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(base.Json{
"username": d.Username,
"password": d.Password,
})
})
if err != nil { if err != nil {
return err return err
} }
if resp.Code == 200 { d.Token = resp.Data.Token
return nil op.MustSaveDriverStorage(d)
} return nil
return errors.New(resp.Message) }
func (d *AListV3) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
url := d.Address + "/api" + api
req := base.RestyClient.R()
req.SetHeader("Authorization", d.Token)
if callback != nil {
callback(req)
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
if res.StatusCode() >= 400 {
return nil, fmt.Errorf("request failed, status: %s", res.Status())
}
code := utils.Json.Get(res.Body(), "code").ToInt()
if code != 200 {
if (code == 401 || code == 403) && !utils.IsBool(retry...) {
err = d.login()
if err != nil {
return nil, err
}
return d.request(api, method, callback, true)
}
return nil, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString())
}
return res.Body(), nil
} }

View File

@ -33,7 +33,7 @@ var config = driver.Config{
func init() { func init() {
op.RegisterDriver(func() driver.Driver { op.RegisterDriver(func() driver.Driver {
return &AliyundriveOpen{ return &AliyundriveOpen{
base: "https://open.aliyundrive.com", base: "https://openapi.aliyundrive.com",
} }
}) })
} }

View File

@ -65,7 +65,7 @@ func (d *AliyundriveOpen) request(uri, method string, callback base.ReqCallback,
} }
isRetry := len(retry) > 0 && retry[0] isRetry := len(retry) > 0 && retry[0]
if e.Code != "" { if e.Code != "" {
if !isRetry && e.Code == "AccessTokenInvalid" { if !isRetry && (utils.SliceContains([]string{"AccessTokenInvalid", "AccessTokenExpired", "I400JD"}, e.Code) || d.AccessToken == "") {
err = d.refreshToken() err = d.refreshToken()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -249,7 +249,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
"isdir": "0", "isdir": "0",
"rtype": "1", "rtype": "1",
"ctype": "11", "ctype": "11",
"path": stream.GetName(), "path": fmt.Sprintf("/%s", stream.GetName()),
"size": fmt.Sprint(stream.GetSize()), "size": fmt.Sprint(stream.GetSize()),
"slice-md5": slice_md5, "slice-md5": slice_md5,
"content-md5": content_md5, "content-md5": content_md5,

View File

@ -193,6 +193,9 @@ func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.Fil
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())) req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
finish += byteSize finish += byteSize
res, err := base.HttpClient.Do(req) res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
if res.StatusCode != 201 && res.StatusCode != 202 { if res.StatusCode != 201 && res.StatusCode != 202 {
data, _ := io.ReadAll(res.Body) data, _ := io.ReadAll(res.Body)
res.Body.Close() res.Body.Close()

View File

@ -184,6 +184,9 @@ func (d *OnedriveAPP) upBig(ctx context.Context, dstDir model.Obj, stream model.
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())) req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
finish += byteSize finish += byteSize
res, err := base.HttpClient.Do(req) res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
if res.StatusCode != 201 && res.StatusCode != 202 { if res.StatusCode != 201 && res.StatusCode != 202 {
data, _ := io.ReadAll(res.Body) data, _ := io.ReadAll(res.Body)
res.Body.Close() res.Body.Close()

View File

@ -5,15 +5,18 @@ import (
"crypto/md5" "crypto/md5"
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
"strconv"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "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/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -56,17 +59,66 @@ func (d *Quark) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
"fids": []string{file.GetID()}, "fids": []string{file.GetID()},
} }
var resp DownResp var resp DownResp
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"
_, err := d.request("/file/download", http.MethodPost, func(req *resty.Request) { _, err := d.request("/file/download", http.MethodPost, func(req *resty.Request) {
req.SetBody(data) req.SetHeader("User-Agent", ua).
SetBody(data)
}, &resp) }, &resp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
u := resp.Data[0].DownloadUrl
start, end := int64(0), file.GetSize()
return &model.Link{ return &model.Link{
URL: resp.Data[0].DownloadUrl, Handle: func(w http.ResponseWriter, r *http.Request) error {
Header: http.Header{ if rg := r.Header.Get("Range"); rg != "" {
"Cookie": []string{d.Cookie}, parseRange, err := http_range.ParseRange(rg, file.GetSize())
"Referer": []string{"https://pan.quark.cn"}, if err != nil {
return err
}
start, end = parseRange[0].Start, parseRange[0].Start+parseRange[0].Length
w.Header().Set("Content-Range", parseRange[0].ContentRange(file.GetSize()))
w.Header().Set("Content-Length", strconv.FormatInt(parseRange[0].Length, 10))
w.WriteHeader(http.StatusPartialContent)
} else {
w.Header().Set("Content-Length", strconv.FormatInt(file.GetSize(), 10))
w.WriteHeader(http.StatusOK)
}
// request 10 MB at a time
chunkSize := int64(10 * 1024 * 1024)
for start < end {
_end := start + chunkSize
if _end > end {
_end = end
}
_range := "bytes=" + strconv.FormatInt(start, 10) + "-" + strconv.FormatInt(_end-1, 10)
start = _end
err = func() error {
req, err := http.NewRequest(r.Method, u, nil)
if err != nil {
return err
}
req.Header.Set("Range", _range)
req.Header.Set("User-Agent", ua)
req.Header.Set("Cookie", d.Cookie)
req.Header.Set("Referer", "https://pan.quark.cn")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusPartialContent {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
_, err = io.Copy(w, resp.Body)
return err
}()
if err != nil {
return err
}
}
return nil
}, },
}, nil }, nil
} }

View File

@ -13,9 +13,9 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "Quark", Name: "Quark",
OnlyProxy: true, OnlyLocal: true,
DefaultRoot: "0", DefaultRoot: "0",
NoOverwriteUpload: true, NoOverwriteUpload: true,
} }

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"net/url" "net/url"
stdpath "path" stdpath "path"
"strings"
"time" "time"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
@ -75,6 +76,9 @@ func (d *S3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*mo
if d.CustomHost != "" { if d.CustomHost != "" {
err = req.Build() err = req.Build()
link = req.HTTPRequest.URL.String() link = req.HTTPRequest.URL.String()
if d.RemoveBucket {
link = strings.Replace(link, "/"+d.Bucket, "", 1)
}
} else { } else {
link, err = req.Presign(time.Hour * time.Duration(d.SignURLExpire)) link, err = req.Presign(time.Hour * time.Duration(d.SignURLExpire))
} }

View File

@ -17,6 +17,7 @@ type Addition struct {
Placeholder string `json:"placeholder"` Placeholder string `json:"placeholder"`
ForcePathStyle bool `json:"force_path_style"` ForcePathStyle bool `json:"force_path_style"`
ListObjectVersion string `json:"list_object_version" type:"select" options:"v1,v2" default:"v1"` ListObjectVersion string `json:"list_object_version" type:"select" options:"v1,v2" default:"v1"`
RemoveBucket bool `json:"remove_bucket" help:"Remove bucket name from path when using custom host."`
} }
var config = driver.Config{ var config = driver.Config{

12
go.mod
View File

@ -1,6 +1,6 @@
module github.com/alist-org/alist/v3 module github.com/alist-org/alist/v3
go 1.19 go 1.20
require ( require (
github.com/SheltonZhu/115driver v1.0.14 github.com/SheltonZhu/115driver v1.0.14
@ -31,9 +31,9 @@ require (
github.com/u2takey/ffmpeg-go v0.4.1 github.com/u2takey/ffmpeg-go v0.4.1
github.com/upyun/go-sdk/v3 v3.0.4 github.com/upyun/go-sdk/v3 v3.0.4
github.com/winfsp/cgofuse v1.5.0 github.com/winfsp/cgofuse v1.5.0
golang.org/x/crypto v0.7.0 golang.org/x/crypto v0.8.0
golang.org/x/image v0.6.0 golang.org/x/image v0.7.0
golang.org/x/net v0.8.0 golang.org/x/net v0.9.0
gorm.io/driver/mysql v1.4.7 gorm.io/driver/mysql v1.4.7
gorm.io/driver/postgres v1.4.8 gorm.io/driver/postgres v1.4.8
gorm.io/driver/sqlite v1.4.4 gorm.io/driver/sqlite v1.4.4
@ -104,8 +104,8 @@ require (
github.com/ugorji/go/codec v1.2.9 // indirect github.com/ugorji/go/codec v1.2.9 // indirect
go.etcd.io/bbolt v1.3.5 // indirect go.etcd.io/bbolt v1.3.5 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/sys v0.6.0 // indirect golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect

22
go.sum
View File

@ -265,11 +265,11 @@ 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.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.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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -281,8 +281,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -305,13 +305,13 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -319,8 +319,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y=

View File

@ -85,6 +85,7 @@ func InitialSettings() []model.SettingItem {
{Key: "pagination_type", Value: "all", Type: conf.TypeSelect, Options: "all,pagination,load_more,auto_load_more", Group: model.SITE}, {Key: "pagination_type", Value: "all", Type: conf.TypeSelect, Options: "all,pagination,load_more,auto_load_more", Group: model.SITE},
{Key: "default_page_size", Value: "30", Type: conf.TypeNumber, Group: model.SITE}, {Key: "default_page_size", Value: "30", Type: conf.TypeNumber, Group: model.SITE},
{Key: conf.AllowIndexed, Value: "false", Type: conf.TypeBool, Group: model.SITE}, {Key: conf.AllowIndexed, Value: "false", Type: conf.TypeBool, Group: model.SITE},
{Key: conf.AllowMounted, Value: "true", Type: conf.TypeBool, Group: model.SITE},
// style settings // style settings
{Key: conf.Logo, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeText, Group: model.STYLE}, {Key: conf.Logo, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeText, Group: model.STYLE},
{Key: conf.Favicon, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE}, {Key: conf.Favicon, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE},
@ -152,9 +153,13 @@ func InitialSettings() []model.SettingItem {
// SSO settings // SSO settings
{Key: conf.SSOLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.SSO, Flag: model.PUBLIC}, {Key: conf.SSOLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.SSO, Flag: model.PUBLIC},
{Key: conf.SSOLoginplatform, Type: conf.TypeSelect, Options: "Github,Microsoft,Google,Dingtalk", Group: model.SSO, Flag: model.PUBLIC}, {Key: conf.SSOLoginplatform, Type: conf.TypeSelect, Options: "Casdoor,Github,Microsoft,Google,Dingtalk", Group: model.SSO, Flag: model.PUBLIC},
{Key: conf.SSOClientId, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE}, {Key: conf.SSOClientId, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
{Key: conf.SSOClientSecret, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE}, {Key: conf.SSOClientSecret, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
{Key: conf.SSOOrganizationName, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
{Key: conf.SSOApplicationName, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
{Key: conf.SSOEndpointName, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
{Key: conf.SSOJwtPublicKey, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
// qbittorrent settings // qbittorrent settings
{Key: conf.QbittorrentUrl, Value: "http://admin:adminadmin@localhost:8080/", Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE}, {Key: conf.QbittorrentUrl, Value: "http://admin:adminadmin@localhost:8080/", Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},

View File

@ -14,6 +14,7 @@ const (
SiteTitle = "site_title" SiteTitle = "site_title"
Announcement = "announcement" Announcement = "announcement"
AllowIndexed = "allow_indexed" AllowIndexed = "allow_indexed"
AllowMounted = "allow_mounted"
Logo = "logo" Logo = "logo"
Favicon = "favicon" Favicon = "favicon"
@ -55,10 +56,14 @@ const (
IndexProgress = "index_progress" IndexProgress = "index_progress"
//SSO //SSO
SSOClientId = "sso_client_id" SSOClientId = "sso_client_id"
SSOClientSecret = "sso_client_secret" SSOClientSecret = "sso_client_secret"
SSOLoginEnabled = "sso_login_enabled" SSOLoginEnabled = "sso_login_enabled"
SSOLoginplatform = "sso_login_platform" SSOLoginplatform = "sso_login_platform"
SSOOrganizationName = "sso_organization_name"
SSOApplicationName = "sso_application_name"
SSOEndpointName = "sso_endpoint_name"
SSOJwtPublicKey = "sso_jwt_public_key"
// qbittorrent // qbittorrent
QbittorrentUrl = "qbittorrent_url" QbittorrentUrl = "qbittorrent_url"

View File

@ -21,7 +21,9 @@ type ListArgs struct {
func List(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error) { func List(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error) {
res, err := list(ctx, path, args) res, err := list(ctx, path, args)
if err != nil { if err != nil {
log.Errorf("failed list %s: %+v", path, err) if !args.NoLog {
log.Errorf("failed list %s: %+v", path, err)
}
return nil, err return nil, err
} }
return res, nil return res, nil
@ -33,8 +35,10 @@ type GetArgs struct {
func Get(ctx context.Context, path string, args *GetArgs) (model.Obj, error) { func Get(ctx context.Context, path string, args *GetArgs) (model.Obj, error) {
res, err := get(ctx, path) res, err := get(ctx, path)
if err != nil && !args.NoLog { if err != nil {
log.Errorf("failed get %s: %+v", path, err) if !args.NoLog {
log.Errorf("failed get %s: %+v", path, err)
}
return nil, err return nil, err
} }
return res, nil return res, nil
@ -106,12 +110,11 @@ func PutAsTask(dstDirPath string, file *model.FileStream) error {
} }
type GetStoragesArgs struct { type GetStoragesArgs struct {
NoLog bool
} }
func GetStorage(path string, args *GetStoragesArgs) (driver.Driver, error) { func GetStorage(path string, args *GetStoragesArgs) (driver.Driver, error) {
storageDriver, _, err := op.GetStorageAndActualPath(path) storageDriver, _, err := op.GetStorageAndActualPath(path)
if err != nil && !args.NoLog { if err != nil {
return nil, err return nil, err
} }
return storageDriver, nil return storageDriver, nil

View File

@ -26,7 +26,9 @@ func list(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error)
ReqPath: path, ReqPath: path,
}, args.Refresh) }, args.Refresh)
if err != nil { if err != nil {
log.Errorf("%+v", err) if !args.NoLog {
log.Errorf("fs/list: %+v", err)
}
if len(virtualFiles) == 0 { if len(virtualFiles) == 0 {
return nil, errors.WithMessage(err, "failed get objs") return nil, errors.WithMessage(err, "failed get objs")
} }

View File

@ -17,12 +17,13 @@ type LinkArgs struct {
} }
type Link struct { type Link struct {
URL string `json:"url"` URL string `json:"url"`
Header http.Header `json:"header"` // needed header Header http.Header `json:"header"` // needed header
Data io.ReadCloser // return file reader directly Data io.ReadCloser // return file reader directly
Status int // status maybe 200 or 206, etc Status int // status maybe 200 or 206, etc
FilePath *string // local file, return the filepath FilePath *string // local file, return the filepath
Expiration *time.Duration // url expiration time Expiration *time.Duration // url expiration time
Handle func(w http.ResponseWriter, r *http.Request) error `json:"-"` // custom handler
} }
type OtherArgs struct { type OtherArgs struct {

View File

@ -134,6 +134,7 @@ func (c *client) AddFromLink(link string, savePath string, id string) error {
addField("urls", link) addField("urls", link)
addField("savepath", savePath) addField("savepath", savePath)
addField("tags", "alist-"+id) addField("tags", "alist-"+id)
addField("autoTMM", "false")
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,6 +3,7 @@ package task
import ( import (
"context" "context"
"runtime"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -63,11 +64,17 @@ func (t Task[K]) GetErrMsg() string {
return t.Error.Error() return t.Error.Error()
} }
func getCurrentGoroutineStack() string {
buf := make([]byte, 1<<16)
n := runtime.Stack(buf, false)
return string(buf[:n])
}
func (t *Task[K]) run() { func (t *Task[K]) run() {
t.state = RUNNING t.state = RUNNING
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Errorf("error [%+v] while run task [%s]", err, t.Name) log.Errorf("error [%s] while run task [%s],stack trace:\n%s", err, t.Name, getCurrentGoroutineStack())
t.Error = errors.Errorf("panic: %+v", err) t.Error = errors.Errorf("panic: %+v", err)
t.state = ERRORED t.state = ERRORED
} }

View File

@ -72,17 +72,21 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, filename, url.PathEscape(filename))) w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, filename, url.PathEscape(filename)))
http.ServeContent(w, r, file.GetName(), fileStat.ModTime(), f) http.ServeContent(w, r, file.GetName(), fileStat.ModTime(), f)
return nil return nil
} else if link.Handle != nil {
return link.Handle(w, r)
} else { } else {
req, err := http.NewRequest(r.Method, link.URL, nil) req, err := http.NewRequest(r.Method, link.URL, nil)
if err != nil { if err != nil {
return err return err
} }
// client header
for h, val := range r.Header { for h, val := range r.Header {
if utils.SliceContains(conf.SlicesMap[conf.ProxyIgnoreHeaders], strings.ToLower(h)) { if utils.SliceContains(conf.SlicesMap[conf.ProxyIgnoreHeaders], strings.ToLower(h)) {
continue continue
} }
req.Header[h] = val req.Header[h] = val
} }
// needed header
for h, val := range link.Header { for h, val := range link.Header {
req.Header[h] = val req.Header[h] = val
} }

View File

@ -150,11 +150,12 @@ func FsRecursiveMove(c *gin.Context) {
for !movingFiles.IsEmpty() { for !movingFiles.IsEmpty() {
movingFile := movingFiles.Pop() movingFile := movingFiles.Pop()
movingFilePath := fmt.Sprintf("%s/%s", filePathMap[movingFile], movingFile.GetName()) movingFilePath := filePathMap[movingFile]
movingFileName := fmt.Sprintf("%s/%s", movingFilePath, movingFile.GetName())
if movingFile.IsDir() { if movingFile.IsDir() {
// directory, recursive move // directory, recursive move
subFilePath := movingFilePath subFilePath := movingFileName
subFiles, err := fs.List(c, subFilePath, &fs.ListArgs{Refresh: true}) subFiles, err := fs.List(c, movingFileName, &fs.ListArgs{Refresh: true})
if err != nil { if err != nil {
common.ErrorResp(c, err, 500) common.ErrorResp(c, err, 500)
return return
@ -171,7 +172,7 @@ func FsRecursiveMove(c *gin.Context) {
} }
// move // move
err := fs.Move(c, movingFilePath, dstDir, movingFiles.IsEmpty()) err := fs.Move(c, movingFileName, dstDir, movingFiles.IsEmpty())
if err != nil { if err != nil {
common.ErrorResp(c, err, 500) common.ErrorResp(c, err, 500)
return return
@ -351,6 +352,105 @@ func FsRemove(c *gin.Context) {
common.SuccessResp(c) common.SuccessResp(c)
} }
type RemoveEmptyDirectoryReq struct {
SrcDir string `json:"src_dir"`
}
func FsRemoveEmptyDirectory(c *gin.Context) {
var req RemoveEmptyDirectoryReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
user := c.MustGet("user").(*model.User)
if !user.CanRemove() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
srcDir, err := user.JoinPath(req.SrcDir)
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)
// record the parent file
fileParentMap := make(map[model.Obj]model.Obj)
// removing files
removingFiles := generic.NewQueue[model.Obj]()
// removed files
removedFiles := make(map[string]bool)
for _, file := range rootFiles {
if !file.IsDir() {
continue
}
removingFiles.Push(file)
filePathMap[file] = srcDir
}
for !removingFiles.IsEmpty() {
removingFile := removingFiles.Pop()
removingFilePath := fmt.Sprintf("%s/%s", filePathMap[removingFile], removingFile.GetName())
if removedFiles[removingFilePath] {
continue
}
subFiles, err := fs.List(c, removingFilePath, &fs.ListArgs{Refresh: true})
if err != nil {
common.ErrorResp(c, err, 500)
return
}
if len(subFiles) == 0 {
// remove empty directory
err = fs.Remove(c, removingFilePath)
removedFiles[removingFilePath] = true
if err != nil {
common.ErrorResp(c, err, 500)
return
}
// recheck parent folder
parentFile, exist := fileParentMap[removingFile]
if exist {
removingFiles.Push(parentFile)
}
} else {
// recursive remove
for _, subFile := range subFiles {
if !subFile.IsDir() {
continue
}
removingFiles.Push(subFile)
filePathMap[subFile] = removingFilePath
fileParentMap[subFile] = removingFile
}
}
}
common.SuccessResp(c)
}
// Link return real link, just for proxy program, it may contain cookie, so just allowed for admin // Link return real link, just for proxy program, it may contain cookie, so just allowed for admin
func Link(c *gin.Context) { func Link(c *gin.Context) {
var req MkdirOrLinkReq var req MkdirOrLinkReq

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
"strings"
"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"
@ -47,6 +48,11 @@ func SSOLoginRedirect(c *gin.Context) {
urlValues.Add("scope", "openid") urlValues.Add("scope", "openid")
urlValues.Add("prompt", "consent") urlValues.Add("prompt", "consent")
urlValues.Add("response_type", "code") urlValues.Add("response_type", "code")
case "Casdoor":
endpoint := strings.TrimSuffix(setting.GetStr(conf.SSOEndpointName), "/")
r_url = endpoint + "/login/oauth/authorize?"
urlValues.Add("scope", "read")
urlValues.Add("state", endpoint)
default: default:
common.ErrorStrResp(c, "invalid platform", 400) common.ErrorStrResp(c, "invalid platform", 400)
return return
@ -94,6 +100,11 @@ func SSOLoginCallback(c *gin.Context) {
url2 = "https://api.dingtalk.com/v1.0/contact/users/me" url2 = "https://api.dingtalk.com/v1.0/contact/users/me"
authstring = "authCode" authstring = "authCode"
idstring = "unionId" idstring = "unionId"
case "Casdoor":
endpoint := strings.TrimSuffix(setting.GetStr(conf.SSOEndpointName), "/")
url1 = endpoint + "/api/login/oauth/access_token"
url2 = endpoint + "/account"
authstring = "code"
default: default:
common.ErrorStrResp(c, "invalid platform", 400) common.ErrorStrResp(c, "invalid platform", 400)
return return

View File

@ -131,6 +131,7 @@ func _fs(g *gin.RouterGroup) {
g.POST("/recursive_move", handles.FsRecursiveMove) g.POST("/recursive_move", handles.FsRecursiveMove)
g.POST("/copy", handles.FsCopy) g.POST("/copy", handles.FsCopy)
g.POST("/remove", handles.FsRemove) g.POST("/remove", handles.FsRemove)
g.POST("/remove_empty_directory", handles.FsRemoveEmptyDirectory)
g.PUT("/put", middlewares.FsUp, handles.FsStream) g.PUT("/put", middlewares.FsUp, handles.FsStream)
g.PUT("/form", middlewares.FsUp, handles.FsForm) g.PUT("/form", middlewares.FsUp, handles.FsForm)
g.POST("/link", middlewares.AuthAdmin, handles.Link) g.POST("/link", middlewares.AuthAdmin, handles.Link)

View File

@ -21,6 +21,7 @@ import (
"github.com/alist-org/alist/v3/internal/sign" "github.com/alist-org/alist/v3/internal/sign"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common" "github.com/alist-org/alist/v3/server/common"
log "github.com/sirupsen/logrus"
) )
type Handler struct { type Handler struct {
@ -237,6 +238,7 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
} }
err = common.Proxy(w, r, link, fi) err = common.Proxy(w, r, link, fi)
if err != nil { if err != nil {
log.Errorf("webdav proxy error: %+v", err)
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
} else if storage.GetStorage().WebdavProxy() && downProxyUrl != "" { } else if storage.GetStorage().WebdavProxy() && downProxyUrl != "" {