Compare commits

...

18 Commits

Author SHA1 Message Date
6aedd0f425 fix: trim slash suffix of sign 2022-09-11 19:39:24 +08:00
5ff0d850d7 feat(aliyundrive): add doc and video preview api 2022-09-11 19:12:54 +08:00
cd73e34ccc chore: optional other interface 2022-09-11 18:40:19 +08:00
107462e42e chore: change default pdf viewer address 2022-09-11 18:27:28 +08:00
e6c2d22700 workflow: update docs address [skip ci] 2022-09-11 17:17:47 +08:00
889ddcef7e feat(baidu): update upload progress 2022-09-11 17:09:48 +08:00
68a6a0c40e fix(aliyundrive): upload empty file 2022-09-11 17:04:05 +08:00
969018db37 fix: is the root folder required (close #1633) 2022-09-11 16:23:46 +08:00
fba1471ec4 docs: add thunder in storage list [skip ci] 2022-09-11 15:26:47 +08:00
8b72ac7f80 chore: rename xunlei to thunder 2022-09-11 14:30:17 +08:00
77a6aa487b chore: cancel sign if no password 2022-09-11 14:14:14 +08:00
fd99c2197b fix: remove relative path check 2022-09-11 14:05:13 +08:00
9c91f062b9 fix(189pc): some minor problems 2022-09-11 13:18:29 +08:00
537ca030b2 chore: fix xunlei some minor problems 2022-09-11 13:09:36 +08:00
b00dcdec0d docs: Create CODE_OF_CONDUCT.md [skip ci] 2022-09-10 22:23:05 +08:00
57bcd376b4 fix(webdav): incorrect href if base_path isn't root (close #1629) 2022-09-10 19:27:34 +08:00
8d4d8648c6 ci: fetch dev version of alist-web 2022-09-10 19:05:02 +08:00
35d177b67b feat: add xunlei driver 2022-09-10 17:40:30 +08:00
46 changed files with 1175 additions and 147 deletions

View File

@ -12,9 +12,10 @@ body:
label: Please make sure of the following things
description: You may select more than one, even select all.
options:
- label: I have read the [documentation](https://alist-doc.nn.ci).
- label: I have read the [documentation](https://alist.nn.ci).
- label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure it's due to `alist` and not something else(such as `Dependencies` or `Operational`).
- label: I'm sure I'm using the latest version
- type: input
id: version
attributes:

View File

@ -7,7 +7,7 @@ body:
label: Please make sure of the following things
description: You may select more than one, even select all.
options:
- label: I have read the [documentation](https://alist-doc.nn.ci).
- label: I have read the [documentation](https://alist.nn.ci).
- label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure this feature is not implemented.
- label: I'm sure it's a reasonable and popular requirement.

128
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
i@nn.ci.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@ -29,7 +29,7 @@
---
English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md)
English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
## Features
@ -51,6 +51,7 @@ English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contri
- [x] [YandexDisk](https://disk.yandex.com/)
- [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode

View File

@ -29,7 +29,7 @@
---
[English](./README.md) | 中文 | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md)
[English](./README.md) | 中文 | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
## Features
@ -51,6 +51,7 @@
- [x] [Yandex.Disk](https://disk.yandex.com/)
- [x] [百度网盘](http://pan.baidu.com/)
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷网盘](https://pan.xunlei.com)
- [x] 部署方便,开箱即用
- [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览

View File

@ -25,11 +25,11 @@ ldflags="\
"
FetchWebDev() {
curl -L https://codeload.github.com/alist-org/web-dist/tar.gz/refs/heads/main -o web-dist-main.tar.gz
tar -zxvf web-dist-main.tar.gz
curl -L https://codeload.github.com/alist-org/web-dist/tar.gz/refs/heads/dev -o web-dist-dev.tar.gz
tar -zxvf web-dist-dev.tar.gz
rm -rf public/dist
mv -f web-dist-main/dist public
rm -rf web-dist-main web-dist-main.tar.gz
mv -f web-dist-dev/dist public
rm -rf web-dist-dev web-dist-dev.tar.gz
}
FetchWebRelease() {

View File

@ -257,8 +257,4 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err
}
func (d *Pan123) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Pan123)(nil)

View File

@ -319,8 +319,4 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return nil
}
func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Yun139)(nil)

View File

@ -7,7 +7,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
@ -208,8 +207,4 @@ func (d *Cloud189) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return d.newUpload(dstDir, stream, up)
}
func (d *Cloud189) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Cloud189)(nil)

View File

@ -10,7 +10,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
@ -278,7 +277,3 @@ func (y *Yun189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
}
return y.CommonUpload(ctx, dstDir, stream, up)
}
func (y *Yun189PC) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}

View File

@ -78,8 +78,7 @@ func timestamp() int64 {
}
func MustParseTime(str string) *time.Time {
loc, _ := time.LoadLocation("Asia/Shanghai")
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", str, loc)
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05 -07", str+" +08", time.Local)
return &lastOpTime
}

View File

@ -6,10 +6,10 @@ import (
)
type Addition struct {
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
VCode string `json:"validate_code"`
RootFolderID string `json:"root_folder_id"`
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
VCode string `json:"validate_code"`
driver.RootID
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"`
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
@ -17,10 +17,6 @@ type Addition struct {
RapidUpload bool `json:"rapid_upload"`
}
func (a Addition) GetRootId() string {
return a.RootFolderID
}
var config = driver.Config{
Name: "189CloudPC",
DefaultRoot: "-11",

View File

@ -140,13 +140,7 @@ func (y *Yun189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, er
fullUrl += "/listFiles.action"
res := make([]model.Obj, 0, 130)
for pageNum := 1; pageNum < 100; pageNum++ {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
for pageNum := 1; ; pageNum++ {
var resp Cloud189FilesResp
_, err := y.get(fullUrl, func(r *resty.Request) {
r.SetContext(ctx)

View File

@ -264,7 +264,9 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return err
}
res.Body.Close()
up(i * 100 / count)
if count > 0 {
up(i * 100 / count)
}
}
var resp2 base.Json
_, err, e = d.request("https://api.aliyundrive.com/v2/file/complete", http.MethodPost, func(req *resty.Request) {
@ -284,7 +286,29 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
}
func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
var resp base.Json
var url string
data := base.Json{
"drive_id": d.DriveId,
"file_id": args.Obj.GetID(),
}
switch args.Method {
case "doc_preview":
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
data["access_token"] = d.AccessToken
case "video_preview":
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
data["category"] = "live_transcoding"
default:
return nil, errs.NotSupport
}
_, err, _ := d.request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, &resp)
if err != nil {
return nil, err
}
return resp, nil
}
var _ driver.Driver = (*AliDrive)(nil)

View File

@ -17,6 +17,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/s3"
_ "github.com/alist-org/alist/v3/drivers/sftp"
_ "github.com/alist-org/alist/v3/drivers/teambition"
_ "github.com/alist-org/alist/v3/drivers/thunder"
_ "github.com/alist-org/alist/v3/drivers/uss"
_ "github.com/alist-org/alist/v3/drivers/virtual"
_ "github.com/alist-org/alist/v3/drivers/webdav"

View File

@ -15,7 +15,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
@ -202,7 +201,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
"uploadid": precreateResp.Uploadid,
}
left = stream.GetSize()
for _, partseq := range precreateResp.BlockList {
for i, partseq := range precreateResp.BlockList {
byteSize := Default
var byteData []byte
if left < Default {
@ -223,13 +222,12 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
return err
}
log.Debugln(res.String())
if len(precreateResp.BlockList) > 0 {
up(i * 100 / len(precreateResp.BlockList))
}
}
_, err = d.create(path, stream.GetSize(), 0, precreateResp.Uploadid, block_list_str)
return err
}
func (d *BaiduNetdisk) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*BaiduNetdisk)(nil)

View File

@ -127,8 +127,4 @@ func (d *FTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStream
return d.conn.Stor(stdpath.Join(dstDir.GetPath(), stream.GetName()), stream)
}
func (d *FTP) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*FTP)(nil)

View File

@ -141,8 +141,4 @@ func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
return err
}
func (d *GoogleDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*GoogleDrive)(nil)

View File

@ -217,8 +217,4 @@ func (d *Local) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
return nil
}
func (d *Local) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Local)(nil)

View File

@ -13,7 +13,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
@ -221,8 +220,4 @@ func (d *MediaTrack) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
return err
}
func (d *MediaTrack) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*MediaTrack)(nil)

View File

@ -144,8 +144,4 @@ func (d *Onedrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return err
}
func (d *Onedrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Onedrive)(nil)

View File

@ -12,7 +12,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/aws/aws-sdk-go/aws"
@ -199,8 +198,4 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err
}
func (d *PikPak) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*PikPak)(nil)

View File

@ -215,8 +215,4 @@ func (d *Quark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
return d.upFinish(pre)
}
func (d *Quark) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Quark)(nil)

View File

@ -10,7 +10,6 @@ import (
"time"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/aws/aws-sdk-go/aws/session"
@ -153,8 +152,4 @@ func (d *S3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreame
return err
}
func (d *S3) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*S3)(nil)

View File

@ -99,8 +99,4 @@ func (d *SFTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStrea
return err
}
func (d *SFTP) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*SFTP)(nil)

View File

@ -7,7 +7,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
@ -156,8 +155,4 @@ func (d *Teambition) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
return d.finishUpload(newFile, dstDir.GetID())
}
func (d *Teambition) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Teambition)(nil)

View File

@ -42,10 +42,10 @@ func (d *Template) List(ctx context.Context, dir model.Obj, args model.ListArgs)
return nil, errs.NotImplement
}
func (d *Template) Get(ctx context.Context, path string) (model.Obj, error) {
// this is optional
return nil, errs.NotImplement
}
//func (d *Template) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *Template) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
// TODO return link of file
@ -82,8 +82,8 @@ func (d *Template) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return errs.NotImplement
}
func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*Template)(nil)

493
drivers/thunder/driver.go Normal file
View File

@ -0,0 +1,493 @@
package thunder
import (
"context"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/go-resty/resty/v2"
)
type Thunder struct {
*XunLeiCommon
model.Storage
Addition
identity string
}
func (x *Thunder) Config() driver.Config {
return config
}
func (x *Thunder) GetAddition() driver.Additional {
return x.Addition
}
func (x *Thunder) Init(ctx context.Context, storage model.Storage) (err error) {
x.Storage = storage
if err = utils.Json.UnmarshalFromString(x.Storage.Addition, &x.Addition); err != nil {
return err
}
// 初始化所需参数
if x.XunLeiCommon == nil {
x.XunLeiCommon = &XunLeiCommon{
Common: &Common{
client: base.NewRestyClient(),
Algorithms: []string{
"HPxr4BVygTQVtQkIMwQH33ywbgYG5l4JoR",
"GzhNkZ8pOBsCY+7",
"v+l0ImTpG7c7/",
"e5ztohgVXNP",
"t",
"EbXUWyVVqQbQX39Mbjn2geok3/0WEkAVxeqhtx857++kjJiRheP8l77gO",
"o7dvYgbRMOpHXxCs",
"6MW8TD8DphmakaxCqVrfv7NReRRN7ck3KLnXBculD58MvxjFRqT+",
"kmo0HxCKVfmxoZswLB4bVA/dwqbVAYghSb",
"j",
"4scKJNdd7F27Hv7tbt",
},
DeviceID: "9aa5c268e7bcfc197a9ad88e2fb330e5",
ClientID: "Xp6vsxz_7IYVw2BB",
ClientSecret: "Xp6vsy4tN9toTVdMSpomVdXpRmES",
ClientVersion: "7.51.0.8196",
PackageName: "com.xunlei.downloadprovider",
UserAgent: "ANDROID-com.xunlei.downloadprovider/7.51.0.8196 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",
DownloadUserAgent: "Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",
},
refreshTokenFunc: func() error {
// 通过RefreshToken刷新
token, err := x.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
// 重新登录
token, err = x.Login(x.Username, x.Password)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
op.MustSaveDriverStorage(x)
}
}
x.SetTokenResp(token)
return err
},
}
}
// 自定义验证码token
ctoekn := strings.TrimSpace(x.CaptchaToken)
if ctoekn != "" {
x.SetCaptchaToken(ctoekn)
x.CaptchaToken = ""
}
// 防止重复登录
identity := x.GetIdentity()
if x.identity != identity || !x.IsLogin() {
x.identity = identity
// 登录
token, err := x.Login(x.Username, x.Password)
if err != nil {
return err
}
x.SetTokenResp(token)
}
return nil
}
func (x *Thunder) Drop(ctx context.Context) error {
return nil
}
type ThunderExpert struct {
*XunLeiCommon
model.Storage
ExpertAddition
identity string
}
func (x *ThunderExpert) Config() driver.Config {
return configExpert
}
func (x *ThunderExpert) GetAddition() driver.Additional {
return x.ExpertAddition
}
func (x *ThunderExpert) Init(ctx context.Context, storage model.Storage) (err error) {
x.Storage = storage
if err = utils.Json.UnmarshalFromString(x.Storage.Addition, &x.ExpertAddition); err != nil {
return err
}
// 防止重复登录
identity := x.GetIdentity()
if identity != x.identity || !x.IsLogin() {
x.identity = identity
x.XunLeiCommon = &XunLeiCommon{
Common: &Common{
client: base.NewRestyClient(),
DeviceID: x.DeviceID,
ClientID: x.ClientID,
ClientSecret: x.ClientSecret,
ClientVersion: x.ClientVersion,
PackageName: x.PackageName,
UserAgent: x.UserAgent,
DownloadUserAgent: x.DownloadUserAgent,
},
}
if x.CaptchaToken != "" {
x.SetCaptchaToken(x.CaptchaToken)
x.CaptchaToken = ""
}
// 签名方法
if x.SignType == "captcha_sign" {
x.Common.Timestamp = x.Timestamp
x.Common.CaptchaSign = x.CaptchaSign
} else {
x.Common.Algorithms = strings.Split(x.Algorithms, ",")
}
// 登录方式
if x.LoginType == "refresh_token" {
// 通过RefreshToken登录
token, err := x.XunLeiCommon.RefreshToken(x.ExpertAddition.RefreshToken)
if err != nil {
return err
}
x.SetTokenResp(token)
// 刷新token方法
x.SetRefreshTokenFunc(func() error {
token, err := x.XunLeiCommon.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
}
x.SetTokenResp(token)
op.MustSaveDriverStorage(x)
return err
})
} else {
// 通过用户密码登录
token, err := x.Login(x.Username, x.Password)
if err != nil {
return err
}
x.SetTokenResp(token)
x.SetRefreshTokenFunc(func() error {
token, err := x.XunLeiCommon.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
token, err = x.Login(x.Username, x.Password)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
}
}
x.SetTokenResp(token)
op.MustSaveDriverStorage(x)
return err
})
}
} else {
// 仅修改验证码token
if x.CaptchaToken != "" {
x.SetCaptchaToken(x.CaptchaToken)
x.CaptchaToken = ""
}
x.XunLeiCommon.UserAgent = x.UserAgent
x.XunLeiCommon.DownloadUserAgent = x.DownloadUserAgent
}
return nil
}
func (x *ThunderExpert) Drop(ctx context.Context) error {
return nil
}
func (x *ThunderExpert) SetTokenResp(token *TokenResp) {
x.XunLeiCommon.SetTokenResp(token)
if token != nil {
x.ExpertAddition.RefreshToken = token.RefreshToken
}
}
type XunLeiCommon struct {
*Common
*TokenResp // 登录信息
refreshTokenFunc func() error
}
func (xc *XunLeiCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return xc.getFiles(ctx, dir.GetID())
}
func (xc *XunLeiCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var lFile Files
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", file.GetID())
//r.SetQueryParam("space", "")
}, &lFile)
if err != nil {
return nil, err
}
link := &model.Link{
URL: lFile.WebContentLink,
Header: http.Header{
"User-Agent": {xc.DownloadUserAgent},
},
}
strs := regexp.MustCompile(`e=([0-9]*)`).FindStringSubmatch(lFile.WebContentLink)
if len(strs) == 2 {
timestamp, err := strconv.ParseInt(strs[1], 10, 64)
if err == nil {
expired := time.Duration(timestamp-time.Now().Unix()) * time.Second
link.Expiration = &expired
}
}
return link, nil
}
func (xc *XunLeiCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"kind": FOLDER,
"name": dirName,
"parent_id": parentDir.GetID(),
})
}, nil)
return err
}
func (xc *XunLeiCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"to": base.Json{"parent_id": dstDir.GetID()},
"ids": []string{srcObj.GetID()},
})
}, nil)
return err
}
func (xc *XunLeiCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodPatch, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", srcObj.GetID())
r.SetBody(&base.Json{"name": newName})
}, nil)
return err
}
func (xc *XunLeiCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"to": base.Json{"parent_id": dstDir.GetID()},
"ids": []string{srcObj.GetID()},
})
}, nil)
return err
}
func (xc *XunLeiCommon) Remove(ctx context.Context, obj model.Obj) error {
_, err := xc.Request(FILE_API_URL+"/{fileID}/trash", http.MethodPatch, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", obj.GetID())
r.SetBody("{}")
}, nil)
return err
}
func (xc *XunLeiCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
var resp UploadTaskResponse
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"kind": FILE,
"parent_id": dstDir.GetID(),
"name": stream.GetName(),
"size": stream.GetSize(),
"hash": "1CF254FBC456E1B012CD45C546636AA62CF8350E",
"upload_type": UPLOAD_TYPE_RESUMABLE,
})
}, &resp)
if err != nil {
return err
}
param := resp.Resumable.Params
if resp.UploadType == UPLOAD_TYPE_RESUMABLE {
param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".")
s, err := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),
Region: aws.String("xunlei"),
Endpoint: aws.String(param.Endpoint),
})
if err != nil {
return err
}
_, err = s3manager.NewUploader(s).UploadWithContext(ctx, &s3manager.UploadInput{
Bucket: aws.String(param.Bucket),
Key: aws.String(param.Key),
Expires: aws.Time(param.Expiration),
Body: stream,
})
return err
}
return nil
}
func (xc *XunLeiCommon) getFiles(ctx context.Context, folderId string) ([]model.Obj, error) {
files := make([]model.Obj, 0)
var pageToken string
for {
var fileList FileList
_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"space": "",
"__type": "drive",
"refresh": "true",
"__sync": "true",
"parent_id": folderId,
"page_token": pageToken,
"with_audit": "true",
"limit": "100",
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
})
}, &fileList)
if err != nil {
return nil, err
}
for i := 0; i < len(fileList.Files); i++ {
files = append(files, &fileList.Files[i])
}
if fileList.NextPageToken == "" {
break
}
pageToken = fileList.NextPageToken
}
return files, nil
}
// 设置刷新Token的方法
func (xc *XunLeiCommon) SetRefreshTokenFunc(fn func() error) {
xc.refreshTokenFunc = fn
}
// 设置Token
func (xc *XunLeiCommon) SetTokenResp(tr *TokenResp) {
xc.TokenResp = tr
}
// 携带Authorization和CaptchaToken的请求
func (xc *XunLeiCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
data, err := xc.Common.Request(url, method, func(req *resty.Request) {
req.SetHeaders(map[string]string{
"Authorization": xc.Token(),
"X-Captcha-Token": xc.GetCaptchaToken(),
})
if callback != nil {
callback(req)
}
}, resp)
errResp, ok := err.(*ErrResp)
if !ok {
return nil, err
}
switch errResp.ErrorCode {
case 0:
return data, nil
case 4122, 4121, 10, 16:
if xc.refreshTokenFunc != nil {
if err = xc.refreshTokenFunc(); err == nil {
break
}
}
return nil, err
case 9: // 验证码token过期
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {
return nil, err
}
default:
return nil, err
}
return xc.Request(url, method, callback, resp)
}
// 刷新Token
func (xc *XunLeiCommon) RefreshToken(refreshToken string) (*TokenResp, error) {
var resp TokenResp
_, err := xc.Common.Request(XLUSER_API_URL+"/auth/token", http.MethodPost, func(req *resty.Request) {
req.SetBody(&base.Json{
"grant_type": "refresh_token",
"refresh_token": refreshToken,
"client_id": xc.ClientID,
"client_secret": xc.ClientSecret,
})
}, &resp)
if err != nil {
return nil, err
}
if resp.RefreshToken == "" {
return nil, errs.EmptyToken
}
return &resp, nil
}
// 登录
func (xc *XunLeiCommon) Login(username, password string) (*TokenResp, error) {
url := XLUSER_API_URL + "/auth/signin"
err := xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username)
if err != nil {
return nil, err
}
var resp TokenResp
_, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(&SignInRequest{
CaptchaToken: xc.GetCaptchaToken(),
ClientID: xc.ClientID,
ClientSecret: xc.ClientSecret,
Username: username,
Password: password,
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (xc *XunLeiCommon) IsLogin() bool {
if xc.TokenResp == nil {
return false
}
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
return err == nil
}

99
drivers/thunder/meta.go Normal file
View File

@ -0,0 +1,99 @@
package thunder
import (
"crypto/md5"
"encoding/hex"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
)
// 高级设置
type ExpertAddition struct {
driver.RootID
LoginType string `json:"login_type" type:"select" options:"user,refresh_token" default:"user"`
SignType string `json:"sign_type" type:"select" options:"algorithms,captcha_sign" default:"algorithms"`
// 登录方式1
Username string `json:"username" required:"true" help:"login type is user,this is required"`
Password string `json:"password" required:"true" help:"login type is user,this is required"`
// 登录方式2
RefreshToken string `json:"refresh_token" required:"true" help:"login type is refresh_token,this is required"`
// 签名方法1
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"HPxr4BVygTQVtQkIMwQH33ywbgYG5l4JoR,GzhNkZ8pOBsCY+7,v+l0ImTpG7c7/,e5ztohgVXNP,t,EbXUWyVVqQbQX39Mbjn2geok3/0WEkAVxeqhtx857++kjJiRheP8l77gO,o7dvYgbRMOpHXxCs,6MW8TD8DphmakaxCqVrfv7NReRRN7ck3KLnXBculD58MvxjFRqT+,kmo0HxCKVfmxoZswLB4bVA/dwqbVAYghSb,j,4scKJNdd7F27Hv7tbt"`
// 签名方法2
CaptchaSign string `json:"captcha_sign" required:"true" help:"sign type is captcha_sign,this is required"`
Timestamp string `json:"timestamp" required:"true" help:"sign type is captcha_sign,this is required"`
// 验证码
CaptchaToken string `json:"captcha_token"`
// 必要且影响登录,由签名决定
DeviceID string `json:"device_id" required:"true" default:"9aa5c268e7bcfc197a9ad88e2fb330e5"`
ClientID string `json:"client_id" required:"true" default:"Xp6vsxz_7IYVw2BB"`
ClientSecret string `json:"client_secret" required:"true" default:"Xp6vsy4tN9toTVdMSpomVdXpRmES"`
ClientVersion string `json:"client_version" required:"true" default:"7.51.0.8196"`
PackageName string `json:"package_name" required:"true" default:"com.xunlei.downloadprovider"`
//不影响登录,影响下载速度
UserAgent string `json:"user_agent" required:"true" default:"ANDROID-com.xunlei.downloadprovider/7.51.0.8196 netWorkType/4G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gdcf98eab238b) (JAVA 0)"`
DownloadUserAgent string `json:"download_user_agent" required:"true" default:"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)"`
}
// 登录特征,用于判断是否重新登录
func (i *ExpertAddition) GetIdentity() string {
hash := md5.New()
if i.LoginType == "refresh_token" {
hash.Write([]byte(i.RefreshToken))
} else {
hash.Write([]byte(i.Username + i.Password))
}
if i.SignType == "captcha_sign" {
hash.Write([]byte(i.CaptchaSign + i.Timestamp))
} else {
hash.Write([]byte(i.Algorithms))
}
hash.Write([]byte(i.DeviceID))
hash.Write([]byte(i.ClientID))
hash.Write([]byte(i.ClientSecret))
hash.Write([]byte(i.ClientVersion))
hash.Write([]byte(i.PackageName))
return hex.EncodeToString(hash.Sum(nil))
}
type Addition struct {
driver.RootID
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
CaptchaToken string `json:"captcha_token"`
}
// 登录特征,用于判断是否重新登录
func (i *Addition) GetIdentity() string {
return utils.GetMD5Encode(i.Username + i.Password)
}
var config = driver.Config{
Name: "Thunder",
LocalSort: true,
OnlyProxy: true,
}
var configExpert = driver.Config{
Name: "ThunderExpert",
LocalSort: true,
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &Thunder{}
})
op.RegisterDriver(configExpert, func() driver.Driver {
return &ThunderExpert{}
})
}

188
drivers/thunder/types.go Normal file
View File

@ -0,0 +1,188 @@
package thunder
import (
"fmt"
"strconv"
"time"
)
type ErrResp struct {
ErrorCode int64 `json:"error_code"`
ErrorMsg string `json:"error"`
ErrorDescription string `json:"error_description"`
// ErrorDetails interface{} `json:"error_details"`
}
func (e *ErrResp) IsError() bool {
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
}
func (e *ErrResp) Error() string {
return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)
}
/*
* 验证码Token
**/
type CaptchaTokenRequest struct {
Action string `json:"action"`
CaptchaToken string `json:"captcha_token"`
ClientID string `json:"client_id"`
DeviceID string `json:"device_id"`
Meta map[string]string `json:"meta"`
RedirectUri string `json:"redirect_uri"`
}
type CaptchaTokenResponse struct {
CaptchaToken string `json:"captcha_token"`
ExpiresIn int64 `json:"expires_in"`
Url string `json:"url"`
}
/*
* 登录
**/
type TokenResp struct {
TokenType string `json:"token_type"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
Sub string `json:"sub"`
UserID string `json:"user_id"`
}
func (t *TokenResp) Token() string {
return fmt.Sprint(t.TokenType, " ", t.AccessToken)
}
type SignInRequest struct {
CaptchaToken string `json:"captcha_token"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Username string `json:"username"`
Password string `json:"password"`
}
/*
* 文件
**/
type FileList struct {
Kind string `json:"kind"`
NextPageToken string `json:"next_page_token"`
Files []Files `json:"files"`
Version string `json:"version"`
VersionOutdated bool `json:"version_outdated"`
}
type Files struct {
Kind string `json:"kind"`
ID string `json:"id"`
ParentID string `json:"parent_id"`
Name string `json:"name"`
//UserID string `json:"user_id"`
Size string `json:"size"`
//Revision string `json:"revision"`
//FileExtension string `json:"file_extension"`
//MimeType string `json:"mime_type"`
//Starred bool `json:"starred"`
WebContentLink string `json:"web_content_link"`
CreatedTime time.Time `json:"created_time"`
ModifiedTime time.Time `json:"modified_time"`
IconLink string `json:"icon_link"`
ThumbnailLink string `json:"thumbnail_link"`
//Md5Checksum string `json:"md5_checksum"`
//Hash string `json:"hash"`
//Links struct{} `json:"links"`
Phase string `json:"phase"`
Audit struct {
Status string `json:"status"`
Message string `json:"message"`
Title string `json:"title"`
} `json:"audit"`
/* Medias []struct {
Category string `json:"category"`
IconLink string `json:"icon_link"`
IsDefault bool `json:"is_default"`
IsOrigin bool `json:"is_origin"`
IsVisible bool `json:"is_visible"`
//Link interface{} `json:"link"`
MediaID string `json:"media_id"`
MediaName string `json:"media_name"`
NeedMoreQuota bool `json:"need_more_quota"`
Priority int `json:"priority"`
RedirectLink string `json:"redirect_link"`
ResolutionName string `json:"resolution_name"`
Video struct {
AudioCodec string `json:"audio_codec"`
BitRate int `json:"bit_rate"`
Duration int `json:"duration"`
FrameRate int `json:"frame_rate"`
Height int `json:"height"`
VideoCodec string `json:"video_codec"`
VideoType string `json:"video_type"`
Width int `json:"width"`
} `json:"video"`
VipTypes []string `json:"vip_types"`
} `json:"medias"` */
Trashed bool `json:"trashed"`
DeleteTime string `json:"delete_time"`
OriginalURL string `json:"original_url"`
//Params struct{} `json:"params"`
//OriginalFileIndex int `json:"original_file_index"`
//Space string `json:"space"`
//Apps []interface{} `json:"apps"`
//Writable bool `json:"writable"`
//FolderType string `json:"folder_type"`
//Collection interface{} `json:"collection"`
}
func (c *Files) GetSize() int64 { size, _ := strconv.ParseInt(c.Size, 10, 64); return size }
func (c *Files) GetName() string { return c.Name }
func (c *Files) ModTime() time.Time { return c.ModifiedTime }
func (c *Files) IsDir() bool { return c.Kind == FOLDER }
func (c *Files) GetID() string { return c.ID }
func (c *Files) GetPath() string { return "" }
func (c *Files) Thumb() string { return c.ThumbnailLink }
/*
* 上传
**/
type UploadTaskResponse struct {
UploadType string `json:"upload_type"`
/*//UPLOAD_TYPE_FORM
Form struct {
//Headers struct{} `json:"headers"`
Kind string `json:"kind"`
Method string `json:"method"`
MultiParts struct {
OSSAccessKeyID string `json:"OSSAccessKeyId"`
Signature string `json:"Signature"`
Callback string `json:"callback"`
Key string `json:"key"`
Policy string `json:"policy"`
XUserData string `json:"x:user_data"`
} `json:"multi_parts"`
URL string `json:"url"`
} `json:"form"`*/
//UPLOAD_TYPE_RESUMABLE
Resumable struct {
Kind string `json:"kind"`
Params struct {
AccessKeyID string `json:"access_key_id"`
AccessKeySecret string `json:"access_key_secret"`
Bucket string `json:"bucket"`
Endpoint string `json:"endpoint"`
Expiration time.Time `json:"expiration"`
Key string `json:"key"`
SecurityToken string `json:"security_token"`
} `json:"params"`
Provider string `json:"provider"`
} `json:"resumable"`
File Files `json:"file"`
}

166
drivers/thunder/util.go Normal file
View File

@ -0,0 +1,166 @@
package thunder
import (
"fmt"
"net/http"
"regexp"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
const (
API_URL = "https://api-pan.xunlei.com/drive/v1"
FILE_API_URL = API_URL + "/files"
XLUSER_API_URL = "https://xluser-ssl.xunlei.com/v1"
)
const (
FOLDER = "drive#folder"
FILE = "drive#file"
RESUMABLE = "drive#resumable"
)
const (
UPLOAD_TYPE_UNKNOWN = "UPLOAD_TYPE_UNKNOWN"
//UPLOAD_TYPE_FORM = "UPLOAD_TYPE_FORM"
UPLOAD_TYPE_RESUMABLE = "UPLOAD_TYPE_RESUMABLE"
UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL"
)
func GetAction(method string, url string) string {
urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(url)[1]
return method + ":" + urlpath
}
type Common struct {
client *resty.Client
captchaToken string
// 签名相关,二选一
Algorithms []string
Timestamp, CaptchaSign string
// 必要值,签名相关
DeviceID string
ClientID string
ClientSecret string
ClientVersion string
PackageName string
UserAgent string
DownloadUserAgent string
}
func (c *Common) SetCaptchaToken(captchaToken string) {
c.captchaToken = captchaToken
}
func (c *Common) GetCaptchaToken() string {
return c.captchaToken
}
// 刷新验证码token(登录后)
func (c *Common) RefreshCaptchaTokenAtLogin(action, userID string) error {
metas := map[string]string{
"client_version": c.ClientVersion,
"package_name": c.PackageName,
"user_id": userID,
}
metas["timestamp"], metas["captcha_sign"] = c.GetCaptchaSign()
return c.refreshCaptchaToken(action, metas)
}
// 刷新验证码token(登录时)
func (c *Common) RefreshCaptchaTokenInLogin(action, username string) error {
metas := make(map[string]string)
if ok, _ := regexp.MatchString(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`, username); ok {
metas["email"] = username
} else if len(username) >= 11 && len(username) <= 18 {
metas["phone_number"] = username
} else {
metas["username"] = username
}
return c.refreshCaptchaToken(action, metas)
}
// 获取验证码签名
func (c *Common) GetCaptchaSign() (timestamp, sign string) {
if len(c.Algorithms) == 0 {
return c.Timestamp, c.CaptchaSign
}
timestamp = fmt.Sprint(time.Now().UnixMilli())
str := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)
for _, algorithm := range c.Algorithms {
str = utils.GetMD5Encode(str + algorithm)
}
sign = "1." + str
return
}
// 刷新验证码token
func (c *Common) refreshCaptchaToken(action string, metas map[string]string) error {
param := CaptchaTokenRequest{
Action: action,
CaptchaToken: c.captchaToken,
ClientID: c.ClientID,
DeviceID: c.DeviceID,
Meta: metas,
RedirectUri: "xlaccsdk01://xunlei.com/callback?state=harbor",
}
var e ErrResp
var resp CaptchaTokenResponse
_, err := c.Request(XLUSER_API_URL+"/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
req.SetError(&e).SetBody(param)
}, &resp)
if err != nil {
return err
}
if e.IsError() {
return &e
}
if resp.Url != "" {
return fmt.Errorf("need verify:%s", resp.Url)
}
if resp.CaptchaToken == "" {
return fmt.Errorf("empty captchaToken")
}
c.SetCaptchaToken(resp.CaptchaToken)
return nil
}
// 只有基础信息的请求
func (c *Common) Request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := c.client.R().SetHeaders(map[string]string{
"user-agent": c.UserAgent,
"accept": "application/json;charset=UTF-8",
"x-device-id": c.DeviceID,
"x-client-id": c.ClientID,
"x-client-version": c.ClientVersion,
})
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
var erron ErrResp
utils.Json.Unmarshal(res.Body(), &erron)
if erron.IsError() {
return nil, &erron
}
return res.Body(), nil
}

View File

@ -9,7 +9,6 @@ import (
"time"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/upyun/go-sdk/v3/upyun"
@ -140,8 +139,4 @@ func (d *USS) Put(ctx context.Context, dstDir model.Obj, stream model.FileStream
})
}
func (d *USS) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*USS)(nil)

View File

@ -6,7 +6,6 @@ import (
"time"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/pkg/utils/random"
@ -89,8 +88,4 @@ func (d *Virtual) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
return nil
}
func (d *Virtual) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Virtual)(nil)

View File

@ -8,7 +8,6 @@ import (
"time"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/cron"
"github.com/alist-org/alist/v3/pkg/gowebdav"
@ -123,8 +122,4 @@ func (d *WebDav) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err
}
func (d *WebDav) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*WebDav)(nil)

View File

@ -8,7 +8,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
@ -139,8 +138,4 @@ func (d *YandexDisk) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
return err
}
func (d *YandexDisk) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*YandexDisk)(nil)

View File

@ -91,7 +91,7 @@ func InitialSettings() []model.SettingItem {
"Google":"https://docs.google.com/gview?url=$e_url&embedded=true"
},
"pdf": {
"PDF.js":"https://alist.nn.ci/pdf.js/web/viewer.html?file=$e_url"
"PDF.js":"https://alist-org.github.io/pdf.js/web/viewer.html?file=$e_url"
}
}`, Type: conf.TypeText, Group: model.PREVIEW},
// {Key: conf.OfficeViewers, Value: `{

View File

@ -10,7 +10,7 @@ type Driver interface {
Meta
Reader
Writer
Other
//Other
}
type Meta interface {

View File

@ -28,11 +28,11 @@ type IRootId interface {
}
type RootPath struct {
RootFolderPath string `json:"root_folder_path" required:"true"`
RootFolderPath string `json:"root_folder_path"`
}
type RootID struct {
RootFolderID string `json:"root_folder_id" required:"true"`
RootFolderID string `json:"root_folder_id"`
}
func (r RootPath) GetRootPath() string {

View File

@ -145,8 +145,11 @@ func getAdditionalItems(t reflect.Type, defaultRoot string) []driver.Item {
if tag.Get("type") != "" {
item.Type = tag.Get("type")
}
if (item.Name == "root_folder_id" || item.Name == "root_folder_path") && item.Default == "" {
item.Default = defaultRoot
if item.Name == "root_folder_id" || item.Name == "root_folder_path" {
if item.Default == "" {
item.Default = defaultRoot
}
item.Required = item.Default != ""
}
// set default type to string
if item.Type == "" {

View File

@ -166,11 +166,15 @@ func Other(ctx context.Context, storage driver.Driver, args model.FsOtherArgs) (
if err != nil {
return nil, errors.WithMessagef(err, "failed to get obj")
}
return storage.Other(ctx, model.OtherArgs{
Obj: obj,
Method: args.Method,
Data: args.Data,
})
if o, ok := storage.(driver.Other); ok {
return o.Other(ctx, model.OtherArgs{
Obj: obj,
Method: args.Method,
Data: args.Data,
})
} else {
return nil, errs.NotImplement
}
}
func MakeDir(ctx context.Context, storage driver.Driver, path string) error {

View File

@ -5,7 +5,6 @@ import (
"strings"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
@ -24,9 +23,10 @@ func ActualPath(storage driver.Additional, rawPath string) string {
// for path: remove the mount path prefix and join the actual root folder if exists
func GetStorageAndActualPath(rawPath string) (driver.Driver, string, error) {
rawPath = utils.StandardizePath(rawPath)
if strings.Contains(rawPath, "..") {
return nil, "", errors.WithStack(errs.RelativePath)
}
// why can remove this check? because reqPath has joined the base_path of user, no relative path
//if strings.Contains(rawPath, "..") {
// return nil, "", errors.WithStack(errs.RelativePath)
//}
storage := GetBalancedStorage(rawPath)
if storage == nil {
return nil, "", errors.Errorf("can't find storage with rawPath: %s", rawPath)

View File

@ -5,8 +5,8 @@ import (
"github.com/alist-org/alist/v3/internal/sign"
)
func Sign(obj model.Obj) string {
if obj.IsDir() {
func Sign(obj model.Obj, encrypt bool) string {
if obj.IsDir() || !encrypt {
return ""
}
return sign.Sign(obj.GetName())

View File

@ -79,7 +79,7 @@ func FsList(c *gin.Context) {
}
total, objs := pagination(objs, &req.PageReq)
common.SuccessResp(c, FsListResp{
Content: toObjResp(objs),
Content: toObjResp(objs, isEncrypt(meta, req.Path)),
Total: int64(total),
Readme: getReadme(meta, req.Path),
Write: user.CanWrite() || canWrite(meta, req.Path),
@ -157,6 +157,16 @@ func canAccess(user *model.User, meta *model.Meta, path string, password string)
return meta.Password == password
}
func isEncrypt(meta *model.Meta, path string) bool {
if meta == nil || meta.Password == "" {
return false
}
if !utils.PathEqual(meta.Path, path) && !meta.PSub {
return false
}
return true
}
func pagination(objs []model.Obj, req *common.PageReq) (int, []model.Obj) {
pageIndex, pageSize := req.Page, req.PerPage
total := len(objs)
@ -171,7 +181,7 @@ func pagination(objs []model.Obj, req *common.PageReq) (int, []model.Obj) {
return total, objs[start:end]
}
func toObjResp(objs []model.Obj) []ObjResp {
func toObjResp(objs []model.Obj, encrypt bool) []ObjResp {
var resp []ObjResp
for _, obj := range objs {
thumb := ""
@ -187,7 +197,7 @@ func toObjResp(objs []model.Obj) []ObjResp {
Size: obj.GetSize(),
IsDir: obj.IsDir(),
Modified: obj.ModTime(),
Sign: common.Sign(obj),
Sign: common.Sign(obj, encrypt),
Thumb: thumb,
Type: tp,
})
@ -270,23 +280,25 @@ func FsGet(c *gin.Context) {
}
}
var related []model.Obj
sameLevelFiles, err := fs.List(c, stdpath.Dir(req.Path))
parentPath := stdpath.Dir(req.Path)
sameLevelFiles, err := fs.List(c, parentPath)
if err == nil {
related = filterRelated(sameLevelFiles, obj)
}
parentMeta, _ := db.GetNearestMeta(parentPath)
common.SuccessResp(c, FsGetResp{
ObjResp: ObjResp{
Name: obj.GetName(),
Size: obj.GetSize(),
IsDir: obj.IsDir(),
Modified: obj.ModTime(),
Sign: common.Sign(obj),
Sign: common.Sign(obj, isEncrypt(meta, req.Path)),
Type: utils.GetFileType(obj.GetName()),
},
RawURL: rawURL,
Readme: getReadme(meta, req.Path),
Provider: provider,
Related: toObjResp(related),
Related: toObjResp(related, isEncrypt(parentMeta, parentPath)),
})
}

View File

@ -2,6 +2,7 @@ package middlewares
import (
stdpath "path"
"strings"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/errs"
@ -28,7 +29,7 @@ func Down(c *gin.Context) {
// verify sign
if needSign(meta, rawPath) {
s := c.Query("sign")
err = sign.Verify(filename, s)
err = sign.Verify(filename, strings.TrimSuffix(s, "/"))
if err != nil {
common.ErrorResp(c, err, 401)
c.Abort()

View File

@ -112,6 +112,7 @@ func admin(g *gin.RouterGroup) {
func _fs(g *gin.RouterGroup) {
g.Any("/list", handles.FsList)
g.Any("/get", handles.FsGet)
g.Any("/other", handles.FsOther)
g.Any("/dirs", handles.FsDirs)
g.POST("/mkdir", handles.FsMkdir)
g.POST("/rename", handles.FsRename)

View File

@ -597,7 +597,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
if err != nil {
return err
}
href := path.Join(h.Prefix, reqPath)
href := path.Join(h.Prefix, strings.TrimPrefix(reqPath, user.BasePath))
if href != "/" && info.IsDir() {
href += "/"
}