Compare commits
40 Commits
Author | SHA1 | Date | |
---|---|---|---|
cc62cc99d2 | |||
270349f37c | |||
977888070a | |||
192d0f2bf3 | |||
f2ec7884ec | |||
815975a4d2 | |||
f96a0238fc | |||
f695bd0959 | |||
efe8f46e17 | |||
515daa22a9 | |||
f11e22deaf | |||
5be976169f | |||
a6e08f3bf4 | |||
944e68a979 | |||
b3a6e33ce1 | |||
cb53ddc8e8 | |||
693417be4f | |||
5c3f91bb55 | |||
8a219d0732 | |||
146a544af3 | |||
48dccc6c0b | |||
ce1740cec4 | |||
d40dbeae3e | |||
5094b673c4 | |||
228e6d10e7 | |||
e90b979d15 | |||
fb05a6ca48 | |||
e055ed3afa | |||
4371c470b3 | |||
7bb237d0ef | |||
5c42354b01 | |||
387e8af422 | |||
5dca777caf | |||
0814778a14 | |||
6827af3997 | |||
435bdea8f7 | |||
4f81735af6 | |||
bef3d2f88d | |||
ba99c7dc03 | |||
f5c5162a9b |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -5,7 +5,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
Thanks for taking the time to fill out this bug report, please confirm that your issue is not a duplicate issue
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & Discussions & Feature request
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/Xhofe/alist/discussions
|
||||
about: Use GitHub discussions for message-board style questions and discussions or feature request.
|
||||
about: Use GitHub discussions for message-board style questions and discussions.
|
24
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: "Feature request"
|
||||
description: Feature request
|
||||
labels: ["enhancement: pending triage"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Description of the feature / 需求描述
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: suggested-solution
|
||||
attributes:
|
||||
label: Suggested solution / 实现思路
|
||||
description: |
|
||||
Solutions to achieve this requirement.
|
||||
实现此需求的解决思路。
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context / 附件
|
||||
description: |
|
||||
Any other context or screenshots about the feature request here, or information you find helpful.
|
||||
相关的任何其他上下文或截图,或者你觉得有帮助的信息
|
20
.github/workflows/issue_check_question.yml
vendored
Normal file
20
.github/workflows/issue_check_question.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: Check need info
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 */7 * *"
|
||||
|
||||
jobs:
|
||||
check-need-info:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: close-issues
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'close-issues'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
labels: 'question'
|
||||
inactive-day: 7
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}, this issue was closed due to no activities in 7 days.
|
||||
你好 @${{ github.event.issue.user.login }},此issue因超过7天未回复被关闭。
|
25
.github/workflows/issue_duplicate.yml
vendored
Normal file
25
.github/workflows/issue_duplicate.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Issue Duplicate
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
create-comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'duplicate'
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}, your issue is a duplicate and will be closed.
|
||||
你好 @${{ github.event.issue.user.login }},你的issue是重复的,将被关闭。
|
||||
- name: Close issue
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'close-issue'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
16
.github/workflows/issue_month_statistics.yml
vendored
Normal file
16
.github/workflows/issue_month_statistics.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: Issue Month Statistics
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 1 * *"
|
||||
|
||||
jobs:
|
||||
month-statistics:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: month-statistics
|
||||
uses: actions-cool/issues-month-statistics@v1
|
||||
with:
|
||||
count-lables: true
|
||||
count-comments: true
|
||||
emoji: 'eyes'
|
20
.github/workflows/issue_question.yml
vendored
Normal file
20
.github/workflows/issue_question.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: Issue Question
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
create-comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'question'
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: actions-cool/issues-helper@v2.0.0
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}, please input issue by template and add detail. Issues labeled by `question` will be closed if no activities in 7 days.
|
||||
你好 @${{ github.event.issue.user.login }},请按照issue模板填写, 并详细说明问题/复现步骤/实现思路或提供更多信息等, 7天内未回复issue自动关闭。
|
25
.github/workflows/issue_wontfix.yml
vendored
Normal file
25
.github/workflows/issue_wontfix.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Issue Wontfix
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
lock-issue:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'wontfix'
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}, this issue will not be worked on and will be closed.
|
||||
你好 @${{ github.event.issue.user.login }},这不会被处理,将被关闭。
|
||||
- name: Close issue
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'close-issue'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
@ -38,6 +38,7 @@ English | [中文](./README_cn.md)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family)
|
||||
- [x] [Yandex.Disk](https://disk.yandex.com/)
|
||||
- [x] [Baidu Disk](http://pan.baidu.com/)
|
||||
- [x] [Quark](https://pan.quark.cn)
|
||||
- [x] Easy to deploy and out-of-the-box
|
||||
- [x] File preview (PDF, markdown, code, plain text, ...)
|
||||
- [x] Image preview in gallery mode
|
||||
@ -57,7 +58,7 @@ English | [中文](./README_cn.md)
|
||||
|
||||
## Discussion
|
||||
|
||||
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports only.**
|
||||
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports and feature request only.**
|
||||
|
||||
## Demo
|
||||
|
||||
|
@ -36,7 +36,8 @@
|
||||
- [x] [分秒帧](https://www.mediatrack.cn/)
|
||||
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
|
||||
- [x] [Yandex.Disk](https://disk.yandex.com/)
|
||||
- [x] [百度云](http://pan.baidu.com/)
|
||||
- [x] [百度网盘](http://pan.baidu.com/)
|
||||
- [x] [夸克网盘](https://pan.quark.cn)
|
||||
- [x] 部署方便,开箱即用
|
||||
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
||||
- [x] 画廊模式下的图像预览
|
||||
@ -56,7 +57,7 @@
|
||||
|
||||
## 讨论
|
||||
|
||||
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) ,**issue仅针对错误报告。**
|
||||
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) ,**issue仅针对错误报告和功能请求。**
|
||||
|
||||
## 演示
|
||||
|
||||
|
@ -21,28 +21,28 @@ func InitConf() {
|
||||
if !utils.WriteToJson(conf.ConfigFile, conf.Conf) {
|
||||
log.Fatalf("failed to create default config file")
|
||||
}
|
||||
return
|
||||
} else {
|
||||
config, err := ioutil.ReadFile(conf.ConfigFile)
|
||||
if err != nil {
|
||||
log.Fatalf("reading config file error:%s", err.Error())
|
||||
}
|
||||
conf.Conf = conf.DefaultConfig()
|
||||
err = utils.Json.Unmarshal(config, conf.Conf)
|
||||
if err != nil {
|
||||
log.Fatalf("load config error: %s", err.Error())
|
||||
}
|
||||
log.Debugf("config:%+v", conf.Conf)
|
||||
// update config.json struct
|
||||
confBody, err := utils.Json.MarshalIndent(conf.Conf, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("marshal config error:%s", err.Error())
|
||||
}
|
||||
err = ioutil.WriteFile(conf.ConfigFile, confBody, 0777)
|
||||
if err != nil {
|
||||
log.Fatalf("update config struct error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
config, err := ioutil.ReadFile(conf.ConfigFile)
|
||||
if err != nil {
|
||||
log.Fatalf("reading config file error:%s", err.Error())
|
||||
}
|
||||
conf.Conf = new(conf.Config)
|
||||
err = utils.Json.Unmarshal(config, conf.Conf)
|
||||
if err != nil {
|
||||
log.Fatalf("load config error: %s", err.Error())
|
||||
}
|
||||
log.Debugf("config:%+v", conf.Conf)
|
||||
// update config.json struct
|
||||
confBody, err := utils.Json.MarshalIndent(conf.Conf, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("marshal config error:%s", err.Error())
|
||||
}
|
||||
err = ioutil.WriteFile(conf.ConfigFile, confBody, 0777)
|
||||
if err != nil {
|
||||
log.Fatalf("update config struct error: %s", err.Error())
|
||||
}
|
||||
err = os.MkdirAll("data/temp", 0700)
|
||||
err := os.MkdirAll(conf.Conf.TempDir, 0700)
|
||||
if err != nil {
|
||||
log.Fatalf("create temp dir error: %s", err.Error())
|
||||
}
|
||||
|
@ -65,16 +65,23 @@ func InitSettings() {
|
||||
Description: "text type extensions",
|
||||
Group: model.FRONT,
|
||||
},
|
||||
//{
|
||||
// Key: "hide readme file",
|
||||
// Value: "true",
|
||||
// Type: "bool",
|
||||
// Description: "hide readme file? ",
|
||||
// Group: model.FRONT,
|
||||
//},
|
||||
{
|
||||
Key: "hide readme file",
|
||||
Value: "true",
|
||||
Type: "bool",
|
||||
Description: "hide readme file? ",
|
||||
Key: "hide files",
|
||||
Value: "/\\/README.md/i",
|
||||
Type: "text",
|
||||
Description: "hide files, support RegExp, one per line",
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "music cover",
|
||||
Value: "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png",
|
||||
Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/circle_center.svg",
|
||||
Description: "music cover image",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
|
@ -30,6 +30,7 @@ type Config struct {
|
||||
Database Database `json:"database"`
|
||||
Scheme Scheme `json:"scheme"`
|
||||
Cache CacheConfig `json:"cache"`
|
||||
TempDir string `json:"temp_dir"`
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
@ -37,6 +38,7 @@ func DefaultConfig() *Config {
|
||||
Address: "0.0.0.0",
|
||||
Port: 5244,
|
||||
Assets: "jsdelivr",
|
||||
TempDir: "data/temp",
|
||||
Database: Database{
|
||||
Type: "sqlite3",
|
||||
Port: 0,
|
||||
|
@ -78,7 +78,7 @@ var (
|
||||
"check parent folder", "check down link", "WebDAV username", "WebDAV password",
|
||||
"Visitor WebDAV username", "Visitor WebDAV password",
|
||||
"default page size", "load type",
|
||||
"ocr api",
|
||||
"ocr api", "favicon",
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -57,6 +57,7 @@ func (driver Pan123) Items() []base.Item {
|
||||
Type: base.TypeSelect,
|
||||
Values: "name,fileId,updateAt,createAt",
|
||||
Required: true,
|
||||
Default: "name",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
@ -64,6 +65,7 @@ func (driver Pan123) Items() []base.Item {
|
||||
Type: base.TypeSelect,
|
||||
Values: "asc,desc",
|
||||
Required: true,
|
||||
Default: "asc",
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -298,7 +300,7 @@ func (driver Pan123) Upload(file *model.FileStream, account *model.Account) erro
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
parentFileId, _ := strconv.Atoi(parentFile.Id)
|
||||
tempFile, err := ioutil.TempFile("data/temp", "file-*")
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -334,7 +334,7 @@ func (driver Cloud139) Delete(path string, account *model.Account) error {
|
||||
"taskInfo": base.Json{
|
||||
"newCatalogID": "",
|
||||
"contentInfoList": contentInfoList,
|
||||
"catalogInfoList": contentInfoList,
|
||||
"catalogInfoList": catalogInfoList,
|
||||
},
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
|
@ -107,6 +107,7 @@ func (driver Cloud189) Login(account *model.Account) error {
|
||||
//cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||||
client = resty.New()
|
||||
//client.SetCookieJar(cookieJar)
|
||||
client.SetTimeout(base.DefaultTimeout)
|
||||
client.SetRetryCount(3)
|
||||
client.SetHeader("Referer", "https://cloud.189.cn/")
|
||||
}
|
||||
@ -524,7 +525,7 @@ func (driver Cloud189) NewUpload(file *model.FileStream, account *model.Account)
|
||||
}
|
||||
res, err := driver.UploadRequest("/person/initMultiUpload", map[string]string{
|
||||
"parentFolderId": parentFile.Id,
|
||||
"fileName": file.Name,
|
||||
"fileName": encode(file.Name),
|
||||
"fileSize": strconv.FormatInt(int64(file.Size), 10),
|
||||
"sliceSize": strconv.FormatInt(int64(DEFAULT), 10),
|
||||
"lazyCheck": "1",
|
||||
|
@ -171,7 +171,12 @@ func (driver Cloud189) Link(args base.Args, account *model.Account) (*base.Link,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link := base.Link{}
|
||||
link := base.Link{
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: base.UserAgent},
|
||||
{Name: "Authorization", Value: ""},
|
||||
},
|
||||
}
|
||||
if res.StatusCode() == 302 {
|
||||
link.Url = res.Header().Get("location")
|
||||
} else {
|
||||
|
@ -115,12 +115,24 @@ func EncodeParam(v url.Values) string {
|
||||
}
|
||||
buf.WriteString(k)
|
||||
buf.WriteByte('=')
|
||||
//if k == "fileName" {
|
||||
// buf.WriteString(encode(v))
|
||||
//} else {
|
||||
buf.WriteString(v)
|
||||
//}
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func encode(str string) string {
|
||||
//str = strings.ReplaceAll(str, "%", "%25")
|
||||
//str = strings.ReplaceAll(str, "&", "%26")
|
||||
//str = strings.ReplaceAll(str, "+", "%2B")
|
||||
//return str
|
||||
return url.QueryEscape(str)
|
||||
}
|
||||
|
||||
func AesEncrypt(data, key []byte) []byte {
|
||||
block, _ := aes.NewCipher(key)
|
||||
if block == nil {
|
||||
|
@ -236,6 +236,7 @@ func (driver AliDrive) batch(srcId, dstId string, account *model.Account) error
|
||||
func init() {
|
||||
base.RegisterDriver(&AliDrive{})
|
||||
aliClient.
|
||||
SetTimeout(base.DefaultTimeout).
|
||||
SetRetryCount(3).
|
||||
SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36").
|
||||
SetHeader("content-type", "application/json").
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
_ "github.com/Xhofe/alist/drivers/native"
|
||||
_ "github.com/Xhofe/alist/drivers/onedrive"
|
||||
_ "github.com/Xhofe/alist/drivers/pikpak"
|
||||
_ "github.com/Xhofe/alist/drivers/quark"
|
||||
_ "github.com/Xhofe/alist/drivers/s3"
|
||||
_ "github.com/Xhofe/alist/drivers/shandian"
|
||||
_ "github.com/Xhofe/alist/drivers/teambition"
|
||||
|
@ -140,13 +140,13 @@ func (driver Baidu) Link(args base.Args, account *model.Account) (*base.Link, er
|
||||
return nil, err
|
||||
}
|
||||
u := fmt.Sprintf("%s&access_token=%s", resp.List[0].Dlink, account.AccessToken)
|
||||
res, err := base.NoRedirectClient.R().Head(u)
|
||||
res, err := base.NoRedirectClient.R().SetHeader("User-Agent", "pan.baidu.com").Head(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode() == 302 {
|
||||
u = res.Header().Get("location")
|
||||
}
|
||||
//if res.StatusCode() == 302 {
|
||||
u = res.Header().Get("location")
|
||||
//}
|
||||
return &base.Link{
|
||||
Url: u,
|
||||
Headers: []base.Header{
|
||||
@ -234,7 +234,7 @@ func (driver Baidu) Upload(file *model.FileStream, account *model.Account) error
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
tempFile, err := ioutil.TempFile("data/temp", "file-*")
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DriverConfig struct {
|
||||
@ -86,17 +87,19 @@ func GetDriversMap() map[string]Driver {
|
||||
func GetDrivers() map[string][]Item {
|
||||
res := make(map[string][]Item)
|
||||
for k, v := range driversMap {
|
||||
webdavDirect := Item{
|
||||
Name: "webdav_direct",
|
||||
Label: "webdav direct",
|
||||
Type: TypeBool,
|
||||
Required: true,
|
||||
Description: "Transfer the WebDAV of this account through the native",
|
||||
}
|
||||
if v.Config().OnlyProxy {
|
||||
res[k] = v.Items()
|
||||
res[k] = append([]Item{
|
||||
webdavDirect,
|
||||
}, v.Items()...)
|
||||
} else {
|
||||
res[k] = append([]Item{
|
||||
//{
|
||||
// Name: "allow_proxy",
|
||||
// Label: "allow_proxy",
|
||||
// Type: TypeBool,
|
||||
// Required: true,
|
||||
// Description: "allow proxy",
|
||||
//},
|
||||
{
|
||||
Name: "proxy",
|
||||
Label: "proxy",
|
||||
@ -111,6 +114,7 @@ func GetDrivers() map[string][]Item {
|
||||
Required: true,
|
||||
Description: "Transfer the WebDAV of this account through the server",
|
||||
},
|
||||
webdavDirect,
|
||||
}, v.Items()...)
|
||||
}
|
||||
res[k] = append([]Item{
|
||||
@ -160,6 +164,8 @@ func GetDrivers() map[string][]Item {
|
||||
var NoRedirectClient *resty.Client
|
||||
var RestyClient = resty.New()
|
||||
var HttpClient = &http.Client{}
|
||||
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||
var DefaultTimeout = time.Second * 20
|
||||
|
||||
func init() {
|
||||
NoRedirectClient = resty.New().SetRedirectPolicy(
|
||||
@ -167,8 +173,8 @@ func init() {
|
||||
return http.ErrUseLastResponse
|
||||
}),
|
||||
)
|
||||
userAgent := "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||
NoRedirectClient.SetHeader("user-agent", userAgent)
|
||||
RestyClient.SetHeader("user-agent", userAgent)
|
||||
NoRedirectClient.SetHeader("user-agent", UserAgent)
|
||||
RestyClient.SetHeader("user-agent", UserAgent)
|
||||
RestyClient.SetRetryCount(3)
|
||||
RestyClient.SetTimeout(DefaultTimeout)
|
||||
}
|
||||
|
@ -44,7 +44,8 @@ type Header struct {
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Url string `json:"url"`
|
||||
Headers []Header `json:"headers"`
|
||||
Data io.ReadCloser
|
||||
Url string `json:"url"`
|
||||
Headers []Header `json:"headers"`
|
||||
Data io.ReadCloser
|
||||
FilePath string `json:"path"` // for native
|
||||
}
|
||||
|
@ -137,6 +137,9 @@ func (driver Lanzou) Link(args base.Args, account *model.Account) (*base.Link, e
|
||||
}
|
||||
link := base.Link{
|
||||
Url: url,
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: base.UserAgent},
|
||||
},
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
@ -15,8 +14,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var lanzouClient = resty.New()
|
||||
|
||||
type LanZouFile struct {
|
||||
Name string `json:"name"`
|
||||
NameAll string `json:"name_all"`
|
||||
@ -58,7 +55,7 @@ func (driver *Lanzou) GetFiles(folderId string, account *model.Account) ([]LanZo
|
||||
files := make([]LanZouFile, 0)
|
||||
var resp LanZouFilesResp
|
||||
// folders
|
||||
res, err := lanzouClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
res, err := base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
SetFormData(map[string]string{
|
||||
"task": "47",
|
||||
"folder_id": folderId,
|
||||
@ -77,7 +74,7 @@ func (driver *Lanzou) GetFiles(folderId string, account *model.Account) ([]LanZo
|
||||
// files
|
||||
pg := 1
|
||||
for {
|
||||
_, err = lanzouClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
_, err = base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
SetFormData(map[string]string{
|
||||
"task": "5",
|
||||
"folder_id": folderId,
|
||||
@ -108,7 +105,7 @@ func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := lanzouClient.R().Get(shareUrl)
|
||||
res, err := base.RestyClient.R().Get(shareUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -129,7 +126,7 @@ func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error
|
||||
pg := 1
|
||||
for {
|
||||
var resp LanZouFilesResp
|
||||
res, err = lanzouClient.R().SetResult(&resp).SetFormData(map[string]string{
|
||||
res, err = base.RestyClient.R().SetResult(&resp).SetFormData(map[string]string{
|
||||
"lx": lx,
|
||||
"fid": fid,
|
||||
"uid": uid,
|
||||
@ -166,7 +163,7 @@ func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error
|
||||
// GetDownPageId 获取下载页面的ID
|
||||
func (driver *Lanzou) GetDownPageId(fileId string, account *model.Account) (string, error) {
|
||||
var resp LanZouFilesResp
|
||||
res, err := lanzouClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
res, err := base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
SetFormData(map[string]string{
|
||||
"task": "22",
|
||||
"file_id": fileId,
|
||||
@ -201,7 +198,7 @@ func (driver *Lanzou) GetLink(downId string, account *model.Account) (string, er
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := lanzouClient.R().Get(fmt.Sprintf("https://%s/%s", u.Host, downId))
|
||||
res, err := base.RestyClient.R().Get(fmt.Sprintf("https://%s/%s", u.Host, downId))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -210,7 +207,7 @@ func (driver *Lanzou) GetLink(downId string, account *model.Account) (string, er
|
||||
return "", fmt.Errorf("get down empty page")
|
||||
}
|
||||
iframeUrl := fmt.Sprintf("https://%s%s", u.Host, iframe[1])
|
||||
res, err = lanzouClient.R().Get(iframeUrl)
|
||||
res, err = base.RestyClient.R().Get(iframeUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -237,7 +234,7 @@ func (driver *Lanzou) GetLink(downId string, account *model.Account) (string, er
|
||||
"websignkey": websignkey,
|
||||
}
|
||||
log.Debugf("form: %+v", form)
|
||||
res, err = lanzouClient.R().SetResult(&resp).
|
||||
res, err = base.RestyClient.R().SetResult(&resp).
|
||||
SetHeader("origin", "https://"+u.Host).
|
||||
SetHeader("referer", iframeUrl).
|
||||
SetFormData(form).Post(fmt.Sprintf("https://%s/ajaxm.php", u.Host))
|
||||
@ -253,7 +250,4 @@ func (driver *Lanzou) GetLink(downId string, account *model.Account) (string, er
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Lanzou{})
|
||||
lanzouClient.
|
||||
SetRetryCount(3).
|
||||
SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36")
|
||||
}
|
||||
|
@ -45,11 +45,12 @@ func (driver MediaTrack) Items() []base.Item {
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "updated_at,title,size",
|
||||
Required: true,
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "updated_at,title,size",
|
||||
Required: true,
|
||||
Description: "title",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
@ -57,6 +58,7 @@ func (driver MediaTrack) Items() []base.Item {
|
||||
Type: base.TypeSelect,
|
||||
Values: "true,false",
|
||||
Required: true,
|
||||
Default: "false",
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -265,7 +267,7 @@ func (driver MediaTrack) Upload(file *model.FileStream, account *model.Account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempFile, err := ioutil.TempFile("data/temp", "file-*")
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ func (driver Native) Link(args base.Args, account *model.Account) (*base.Link, e
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
link := base.Link{
|
||||
Url: fullPath,
|
||||
FilePath: fullPath,
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ var onedriveHostMap = map[string]Host{
|
||||
|
||||
func (driver Onedrive) GetMetaUrl(account *model.Account, auth bool, path string) string {
|
||||
path = filepath.Join(account.RootFolder, path)
|
||||
log.Debugf(path)
|
||||
//log.Debugf(path)
|
||||
host, _ := onedriveHostMap[account.Zone]
|
||||
if auth {
|
||||
return host.Oauth
|
||||
@ -253,7 +253,7 @@ func (driver Onedrive) Request(url string, method int, headers, query, form map[
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
//log.Debug(res.String())
|
||||
if e.Error.Code != "" {
|
||||
if e.Error.Code == "InvalidAuthenticationToken" {
|
||||
err = driver.RefreshToken(account)
|
||||
|
320
drivers/quark/driver.go
Normal file
320
drivers/quark/driver.go
Normal file
@ -0,0 +1,320 @@
|
||||
package quark
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Quark struct{}
|
||||
|
||||
func (driver Quark) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Quark",
|
||||
OnlyProxy: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Quark) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "access_token",
|
||||
Label: "Cookie",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "Unknown expiration time",
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Default: "0",
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "file_type,file_name,updated_at",
|
||||
Required: true,
|
||||
Default: "file_name",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "asc,desc",
|
||||
Required: true,
|
||||
Default: "asc",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Quark) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := driver.Get("/config", nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Quark) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Quark) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var files []model.File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ = cache.([]model.File)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Quark) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
path := args.Path
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := base.Json{
|
||||
"fids": []string{file.Id},
|
||||
}
|
||||
var resp DownResp
|
||||
_, err = driver.Post("/file/download", data, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{
|
||||
Url: resp.Data[0].DownloadUrl,
|
||||
Headers: []base.Header{
|
||||
{Name: "Cookie", Value: account.AccessToken},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver Quark) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("quark path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
func (driver Quark) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Quark) MakeDir(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"dir_init_lock": false,
|
||||
"dir_path": "",
|
||||
"file_name": utils.Base(path),
|
||||
"pdir_fid": parentFile.Id,
|
||||
}
|
||||
_, err = driver.Post("/file", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Quark) Move(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstParentFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"action_type": 1,
|
||||
"exclude_fids": []string{},
|
||||
"filelist": []string{srcFile.Id},
|
||||
"to_pdir_fid": dstParentFile.Id,
|
||||
}
|
||||
_, err = driver.Post("/file/move", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Quark) Rename(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"fid": srcFile.Id,
|
||||
"file_name": utils.Base(dst),
|
||||
}
|
||||
_, err = driver.Post("/file/rename", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Quark) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Quark) Delete(path string, account *model.Account) error {
|
||||
srcFile, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"action_type": 1,
|
||||
"exclude_fids": []string{},
|
||||
"filelist": []string{srcFile.Id},
|
||||
}
|
||||
_, err = driver.Post("/file/delete", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Quark) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
_, err = io.Copy(tempFile, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := md5.New()
|
||||
_, err = io.Copy(m, tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
md5Str := hex.EncodeToString(m.Sum(nil))
|
||||
s := sha1.New()
|
||||
_, err = io.Copy(s, tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sha1Str := hex.EncodeToString(s.Sum(nil))
|
||||
// pre
|
||||
pre, err := driver.UpPre(file, parentFile.Id, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugln("hash: ", md5Str, sha1Str)
|
||||
// hash
|
||||
finish, err := driver.UpHash(md5Str, sha1Str, pre.Data.TaskId, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if finish {
|
||||
return nil
|
||||
}
|
||||
// part up
|
||||
partSize := pre.Metadata.PartSize
|
||||
var bytes []byte
|
||||
md5s := make([]string, 0)
|
||||
defaultBytes := make([]byte, partSize)
|
||||
left := int64(file.GetSize())
|
||||
partNumber := 1
|
||||
for left > 0 {
|
||||
if left > int64(partSize) {
|
||||
bytes = defaultBytes
|
||||
} else {
|
||||
bytes = make([]byte, left)
|
||||
}
|
||||
_, err := io.ReadFull(tempFile, bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
left -= int64(partSize)
|
||||
log.Debugf("left: %d", left)
|
||||
m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account)
|
||||
//m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account, md5Str, sha1Str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m == "finish" {
|
||||
return nil
|
||||
}
|
||||
md5s = append(md5s, m)
|
||||
partNumber++
|
||||
}
|
||||
err = driver.UpCommit(pre, md5s, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return driver.UpFinish(pre, account)
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Quark)(nil)
|
266
drivers/quark/quark.go
Normal file
266
drivers/quark/quark.go
Normal file
@ -0,0 +1,266 @@
|
||||
package quark
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (driver Quark) Request(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
u := "https://drive.quark.cn/1/clouddrive" + pathname
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
"Cookie": account.AccessToken,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": "https://pan.quark.cn/",
|
||||
})
|
||||
req.SetQueryParam("pr", "ucpro")
|
||||
req.SetQueryParam("fr", "pc")
|
||||
if headers != nil {
|
||||
req.SetHeaders(headers)
|
||||
}
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
if form != nil {
|
||||
req.SetFormData(form)
|
||||
}
|
||||
if data != nil {
|
||||
req.SetBody(data)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var e Resp
|
||||
var err error
|
||||
var res *resty.Response
|
||||
req.SetError(&e)
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(u)
|
||||
case base.Post:
|
||||
res, err = req.Post(u)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(u)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(u)
|
||||
case base.Put:
|
||||
res, err = req.Put(u)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("%s response: %s", pathname, res.String())
|
||||
if e.Status >= 400 || e.Code != 0 {
|
||||
return nil, errors.New(e.Message)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (driver Quark) Get(pathname string, query map[string]string, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
return driver.Request(pathname, base.Get, nil, query, nil, nil, resp, account)
|
||||
}
|
||||
|
||||
func (driver Quark) Post(pathname string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
return driver.Request(pathname, base.Post, nil, nil, nil, data, resp, account)
|
||||
}
|
||||
|
||||
func (driver Quark) GetFiles(parent string, account *model.Account) ([]model.File, error) {
|
||||
files := make([]model.File, 0)
|
||||
page := 1
|
||||
size := 100
|
||||
query := map[string]string{
|
||||
"pdir_fid": parent,
|
||||
"_size": strconv.Itoa(size),
|
||||
"_fetch_total": "1",
|
||||
"_sort": "file_type:asc," + account.OrderBy + ":" + account.OrderDirection,
|
||||
}
|
||||
for {
|
||||
query["_page"] = strconv.Itoa(page)
|
||||
var resp SortResp
|
||||
_, err := driver.Get("/file/sort", query, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range resp.Data.List {
|
||||
files = append(files, *driver.formatFile(&f))
|
||||
}
|
||||
if page*size >= resp.Metadata.Count {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Quark) UpPre(file *model.FileStream, parentId string, account *model.Account) (UpPreResp, error) {
|
||||
now := time.Now()
|
||||
data := base.Json{
|
||||
"ccp_hash_update": true,
|
||||
"dir_name": "",
|
||||
"file_name": file.Name,
|
||||
"format_type": file.MIMEType,
|
||||
"l_created_at": now.UnixMilli(),
|
||||
"l_updated_at": now.UnixMilli(),
|
||||
"pdir_fid": parentId,
|
||||
"size": file.Size,
|
||||
}
|
||||
log.Debugf("uppre data: %+v", data)
|
||||
var resp UpPreResp
|
||||
_, err := driver.Post("/file/upload/pre", data, &resp, account)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (driver Quark) UpHash(md5, sha1, taskId string, account *model.Account) (bool, error) {
|
||||
data := base.Json{
|
||||
"md5": md5,
|
||||
"sha1": sha1,
|
||||
"task_id": taskId,
|
||||
}
|
||||
log.Debugf("hash: %+v", data)
|
||||
var resp HashResp
|
||||
_, err := driver.Post("/file/update/hash", data, &resp, account)
|
||||
return resp.Data.Finish, err
|
||||
}
|
||||
|
||||
func (driver Quark) UpPart(pre UpPreResp, mineType string, partNumber int, bytes []byte, account *model.Account) (string, error) {
|
||||
//func (driver Quark) UpPart(pre UpPreResp, mineType string, partNumber int, bytes []byte, account *model.Account, md5Str, sha1Str string) (string, error) {
|
||||
timeStr := time.Now().UTC().Format(http.TimeFormat)
|
||||
data := base.Json{
|
||||
"auth_info": pre.Data.AuthInfo,
|
||||
"auth_meta": fmt.Sprintf(`PUT
|
||||
|
||||
%s
|
||||
%s
|
||||
x-oss-date:%s
|
||||
x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit
|
||||
/%s/%s?partNumber=%d&uploadId=%s`,
|
||||
mineType, timeStr, timeStr, pre.Data.Bucket, pre.Data.ObjKey, partNumber, pre.Data.UploadId),
|
||||
"task_id": pre.Data.TaskId,
|
||||
}
|
||||
var resp UpAuthResp
|
||||
_, err := driver.Post("/file/upload/auth", data, &resp, account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
//if partNumber == 1 {
|
||||
// finish, err := driver.UpHash(md5Str, sha1Str, pre.Data.TaskId, account)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// if finish {
|
||||
// return "finish", nil
|
||||
// }
|
||||
//}
|
||||
u := fmt.Sprintf("https://%s.%s/%s", pre.Data.Bucket, pre.Data.UploadUrl[7:], pre.Data.ObjKey)
|
||||
res, err := base.RestyClient.R().
|
||||
SetHeaders(map[string]string{
|
||||
"Authorization": resp.Data.AuthKey,
|
||||
"Content-Type": mineType,
|
||||
"Referer": "https://pan.quark.cn/",
|
||||
"x-oss-date": timeStr,
|
||||
"x-oss-user-agent": "aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit",
|
||||
}).
|
||||
SetQueryParams(map[string]string{
|
||||
"partNumber": strconv.Itoa(partNumber),
|
||||
"uploadId": pre.Data.UploadId,
|
||||
}).SetBody(bytes).Put(u)
|
||||
if res.StatusCode() != 200 {
|
||||
return "", fmt.Errorf("up status: %d, error: %s", res.StatusCode(), res.String())
|
||||
}
|
||||
return res.Header().Get("ETag"), nil
|
||||
}
|
||||
|
||||
func (driver Quark) UpCommit(pre UpPreResp, md5s []string, account *model.Account) error {
|
||||
timeStr := time.Now().UTC().Format(http.TimeFormat)
|
||||
log.Debugf("md5s: %+v", md5s)
|
||||
bodyBuilder := strings.Builder{}
|
||||
bodyBuilder.WriteString(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CompleteMultipartUpload>
|
||||
`)
|
||||
for i, m := range md5s {
|
||||
bodyBuilder.WriteString(fmt.Sprintf(`<Part>
|
||||
<PartNumber>%d</PartNumber>
|
||||
<ETag>%s</ETag>
|
||||
</Part>
|
||||
`, i+1, m))
|
||||
}
|
||||
bodyBuilder.WriteString("</CompleteMultipartUpload>")
|
||||
body := bodyBuilder.String()
|
||||
m := md5.New()
|
||||
m.Write([]byte(body))
|
||||
contentMd5 := base64.StdEncoding.EncodeToString(m.Sum(nil))
|
||||
callbackBytes, err := utils.Json.Marshal(pre.Data.Callback)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
callbackBase64 := base64.StdEncoding.EncodeToString(callbackBytes)
|
||||
data := base.Json{
|
||||
"auth_info": pre.Data.AuthInfo,
|
||||
"auth_meta": fmt.Sprintf(`POST
|
||||
%s
|
||||
application/xml
|
||||
%s
|
||||
x-oss-callback:%s
|
||||
x-oss-date:%s
|
||||
x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit
|
||||
/%s/%s?uploadId=%s`,
|
||||
contentMd5, timeStr, callbackBase64, timeStr,
|
||||
pre.Data.Bucket, pre.Data.ObjKey, pre.Data.UploadId),
|
||||
"task_id": pre.Data.TaskId,
|
||||
}
|
||||
log.Debugf("xml: %s", body)
|
||||
log.Debugf("auth data: %+v", data)
|
||||
var resp UpAuthResp
|
||||
_, err = driver.Post("/file/upload/auth", data, &resp, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := fmt.Sprintf("https://%s.%s/%s", pre.Data.Bucket, pre.Data.UploadUrl[7:], pre.Data.ObjKey)
|
||||
res, err := base.RestyClient.R().
|
||||
SetHeaders(map[string]string{
|
||||
"Authorization": resp.Data.AuthKey,
|
||||
"Content-MD5": contentMd5,
|
||||
"Content-Type": "application/xml",
|
||||
"Referer": "https://pan.quark.cn/",
|
||||
"x-oss-callback": callbackBase64,
|
||||
"x-oss-date": timeStr,
|
||||
"x-oss-user-agent": "aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit",
|
||||
}).
|
||||
SetQueryParams(map[string]string{
|
||||
"uploadId": pre.Data.UploadId,
|
||||
}).SetBody(body).Post(u)
|
||||
if res.StatusCode() != 200 {
|
||||
return fmt.Errorf("up status: %d, error: %s", res.StatusCode(), res.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Quark) UpFinish(pre UpPreResp, account *model.Account) error {
|
||||
data := base.Json{
|
||||
"obj_key": pre.Data.ObjKey,
|
||||
"task_id": pre.Data.TaskId,
|
||||
}
|
||||
_, err := driver.Post("/file/upload/finish", data, nil, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Quark{})
|
||||
}
|
134
drivers/quark/types.go
Normal file
134
drivers/quark/types.go
Normal file
@ -0,0 +1,134 @@
|
||||
package quark
|
||||
|
||||
type Resp struct {
|
||||
Status int `json:"status"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
//ReqId string `json:"req_id"`
|
||||
//Timestamp int `json:"timestamp"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Fid string `json:"fid"`
|
||||
FileName string `json:"file_name"`
|
||||
//PdirFid string `json:"pdir_fid"`
|
||||
//Category int `json:"category"`
|
||||
//FileType int `json:"file_type"`
|
||||
Size int64 `json:"size"`
|
||||
//FormatType string `json:"format_type"`
|
||||
//Status int `json:"status"`
|
||||
//Tags string `json:"tags,omitempty"`
|
||||
//LCreatedAt int64 `json:"l_created_at"`
|
||||
LUpdatedAt int64 `json:"l_updated_at"`
|
||||
//NameSpace int `json:"name_space"`
|
||||
//IncludeItems int `json:"include_items,omitempty"`
|
||||
//RiskType int `json:"risk_type"`
|
||||
//BackupSign int `json:"backup_sign"`
|
||||
//Duration int `json:"duration"`
|
||||
//FileSource string `json:"file_source"`
|
||||
File bool `json:"file"`
|
||||
//CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
//PrivateExtra struct {} `json:"_private_extra"`
|
||||
//ObjCategory string `json:"obj_category,omitempty"`
|
||||
//Thumbnail string `json:"thumbnail,omitempty"`
|
||||
}
|
||||
|
||||
type SortResp struct {
|
||||
Resp
|
||||
Data struct {
|
||||
List []File `json:"list"`
|
||||
} `json:"data"`
|
||||
Metadata struct {
|
||||
Size int `json:"_size"`
|
||||
Page int `json:"_page"`
|
||||
Count int `json:"_count"`
|
||||
Total int `json:"_total"`
|
||||
Way string `json:"way"`
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
type DownResp struct {
|
||||
Resp
|
||||
Data []struct {
|
||||
Fid string `json:"fid"`
|
||||
FileName string `json:"file_name"`
|
||||
PdirFid string `json:"pdir_fid"`
|
||||
Category int `json:"category"`
|
||||
FileType int `json:"file_type"`
|
||||
Size int `json:"size"`
|
||||
FormatType string `json:"format_type"`
|
||||
Status int `json:"status"`
|
||||
Tags string `json:"tags"`
|
||||
LCreatedAt int64 `json:"l_created_at"`
|
||||
LUpdatedAt int64 `json:"l_updated_at"`
|
||||
NameSpace int `json:"name_space"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
DownloadUrl string `json:"download_url"`
|
||||
Md5 string `json:"md5"`
|
||||
RiskType int `json:"risk_type"`
|
||||
RangeSize int `json:"range_size"`
|
||||
BackupSign int `json:"backup_sign"`
|
||||
ObjCategory string `json:"obj_category"`
|
||||
Duration int `json:"duration"`
|
||||
FileSource string `json:"file_source"`
|
||||
File bool `json:"file"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
PrivateExtra struct {
|
||||
} `json:"_private_extra"`
|
||||
} `json:"data"`
|
||||
Metadata struct {
|
||||
Acc2 string `json:"acc2"`
|
||||
Acc1 string `json:"acc1"`
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
type UpPreResp struct {
|
||||
Resp
|
||||
Data struct {
|
||||
TaskId string `json:"task_id"`
|
||||
Finish bool `json:"finish"`
|
||||
UploadId string `json:"upload_id"`
|
||||
ObjKey string `json:"obj_key"`
|
||||
UploadUrl string `json:"upload_url"`
|
||||
Fid string `json:"fid"`
|
||||
Bucket string `json:"bucket"`
|
||||
Callback struct {
|
||||
CallbackUrl string `json:"callbackUrl"`
|
||||
CallbackBody string `json:"callbackBody"`
|
||||
} `json:"callback"`
|
||||
FormatType string `json:"format_type"`
|
||||
Size int `json:"size"`
|
||||
AuthInfo string `json:"auth_info"`
|
||||
} `json:"data"`
|
||||
Metadata struct {
|
||||
PartThread int `json:"part_thread"`
|
||||
Acc2 string `json:"acc2"`
|
||||
Acc1 string `json:"acc1"`
|
||||
PartSize int `json:"part_size"` // 分片大小
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
type HashResp struct {
|
||||
Resp
|
||||
Data struct {
|
||||
Finish bool `json:"finish"`
|
||||
Fid string `json:"fid"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
FormatType string `json:"format_type"`
|
||||
} `json:"data"`
|
||||
Metadata struct {
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
type UpAuthResp struct {
|
||||
Resp
|
||||
Data struct {
|
||||
AuthKey string `json:"auth_key"`
|
||||
Speed int `json:"speed"`
|
||||
Headers []interface{} `json:"headers"`
|
||||
} `json:"data"`
|
||||
Metadata struct {
|
||||
} `json:"metadata"`
|
||||
}
|
31
drivers/quark/util.go
Normal file
31
drivers/quark/util.go
Normal file
@ -0,0 +1,31 @@
|
||||
package quark
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getTime(t int64) *time.Time {
|
||||
tm := time.UnixMilli(t)
|
||||
//log.Debugln(tm)
|
||||
return &tm
|
||||
}
|
||||
|
||||
func (driver Quark) formatFile(f *File) *model.File {
|
||||
file := model.File{
|
||||
Id: f.Fid,
|
||||
Name: f.FileName,
|
||||
Size: f.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(f.UpdatedAt),
|
||||
}
|
||||
if f.File {
|
||||
file.Type = utils.GetFileType(path.Ext(f.FileName))
|
||||
} else {
|
||||
file.Type = conf.FOLDER
|
||||
}
|
||||
return &file
|
||||
}
|
@ -79,6 +79,18 @@ func (driver S3) Items() []base.Item {
|
||||
Type: base.TypeString,
|
||||
Description: "default empty string",
|
||||
},
|
||||
{
|
||||
Name: "bool_1",
|
||||
Label: "S3ForcePathStyle",
|
||||
Type: base.TypeBool,
|
||||
},
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "ListObject Version",
|
||||
Type: base.TypeSelect,
|
||||
Values: "v1,v2",
|
||||
Default: "v1",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,7 +144,11 @@ func (driver S3) Files(path string, account *model.Account) ([]model.File, error
|
||||
if err == nil {
|
||||
files, _ = cache.([]model.File)
|
||||
} else {
|
||||
files, err = driver.List(path, account)
|
||||
if account.InternalType == "v2" {
|
||||
files, err = driver.ListV2(path, account)
|
||||
} else {
|
||||
files, err = driver.List(path, account)
|
||||
}
|
||||
if err == nil && len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
@ -22,9 +23,10 @@ var sessionsMap map[string]*session.Session
|
||||
|
||||
func (driver S3) NewSession(account *model.Account) (*session.Session, error) {
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(account.AccessKey, account.AccessSecret, ""),
|
||||
Region: &account.Region,
|
||||
Endpoint: &account.Endpoint,
|
||||
Credentials: credentials.NewStaticCredentials(account.AccessKey, account.AccessSecret, ""),
|
||||
Region: &account.Region,
|
||||
Endpoint: &account.Endpoint,
|
||||
S3ForcePathStyle: aws.Bool(account.Bool1),
|
||||
}
|
||||
return session.NewSession(cfg)
|
||||
}
|
||||
@ -99,6 +101,9 @@ func (driver S3) List(prefix string, account *model.Account) ([]model.File, erro
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
if listObjectsResult.IsTruncated == nil {
|
||||
return nil, errors.New("IsTruncated nil")
|
||||
}
|
||||
if *listObjectsResult.IsTruncated {
|
||||
marker = *listObjectsResult.NextMarker
|
||||
} else {
|
||||
@ -108,6 +113,73 @@ func (driver S3) List(prefix string, account *model.Account) ([]model.File, erro
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver S3) ListV2(prefix string, account *model.Account) ([]model.File, error) {
|
||||
prefix = driver.GetKey(prefix, account, true)
|
||||
//if prefix == "" {
|
||||
// prefix = "/"
|
||||
//}
|
||||
log.Debugf("list: %s", prefix)
|
||||
client, err := driver.GetClient(account, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
var continuationToken, startAfter *string
|
||||
for {
|
||||
input := &s3.ListObjectsV2Input{
|
||||
Bucket: &account.Bucket,
|
||||
ContinuationToken: continuationToken,
|
||||
Prefix: &prefix,
|
||||
Delimiter: aws.String("/"),
|
||||
StartAfter: startAfter,
|
||||
}
|
||||
listObjectsResult, err := client.ListObjectsV2(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("resp: %+v", listObjectsResult)
|
||||
for _, object := range listObjectsResult.CommonPrefixes {
|
||||
name := utils.Base(strings.Trim(*object.Prefix, "/"))
|
||||
file := model.File{
|
||||
//Id: *object.Key,
|
||||
Name: name,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
TimeStr: "-",
|
||||
Type: conf.FOLDER,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
for _, object := range listObjectsResult.Contents {
|
||||
name := utils.Base(*object.Key)
|
||||
if name == account.Zone {
|
||||
continue
|
||||
}
|
||||
file := model.File{
|
||||
//Id: *object.Key,
|
||||
Name: name,
|
||||
Size: *object.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: object.LastModified,
|
||||
Type: utils.GetFileType(path.Ext(*object.Key)),
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
if !aws.BoolValue(listObjectsResult.IsTruncated) {
|
||||
break
|
||||
}
|
||||
if listObjectsResult.NextContinuationToken != nil {
|
||||
continuationToken = listObjectsResult.NextContinuationToken
|
||||
continue
|
||||
}
|
||||
if len(listObjectsResult.Contents) == 0 {
|
||||
break
|
||||
}
|
||||
startAfter = listObjectsResult.Contents[len(listObjectsResult.Contents)-1].Key
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver S3) GetKey(path string, account *model.Account, dir bool) string {
|
||||
path = utils.Join(account.RootFolder, path)
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
|
@ -51,6 +51,7 @@ func (driver Teambition) Items() []base.Item {
|
||||
Type: base.TypeSelect,
|
||||
Values: "fileName,fileSize,updated,created",
|
||||
Required: true,
|
||||
Default: "fileName",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
@ -58,6 +59,7 @@ func (driver Teambition) Items() []base.Item {
|
||||
Type: base.TypeSelect,
|
||||
Values: "Asc,Desc",
|
||||
Required: true,
|
||||
Default: "Asc",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -157,8 +157,10 @@ func (driver Teambition) upload(file *model.FileStream, token string, account *m
|
||||
|
||||
func (driver Teambition) chunkUpload(file *model.FileStream, token string, account *model.Account) (*FileUpload, error) {
|
||||
prefix := "tcs"
|
||||
referer := "https://www.teambition.com/"
|
||||
if account.InternalType == "International" {
|
||||
prefix = "us-tcs"
|
||||
referer = "https://us.teambition.com/"
|
||||
}
|
||||
var newChunk ChunkUpload
|
||||
_, err := base.RestyClient.R().SetResult(&newChunk).SetHeader("Authorization", token).
|
||||
@ -187,7 +189,7 @@ func (driver Teambition) chunkUpload(file *model.FileStream, token string, accou
|
||||
res, err := base.RestyClient.R().SetHeaders(map[string]string{
|
||||
"Authorization": token,
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Referer": "https://www.teambition.com/",
|
||||
"Referer": referer,
|
||||
}).SetBody(chunkData).Post(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -2,6 +2,9 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -30,8 +33,9 @@ type Account struct {
|
||||
SiteUrl string `json:"site_url"`
|
||||
SiteId string `json:"site_id"`
|
||||
InternalType string `json:"internal_type"`
|
||||
WebdavProxy bool `json:"webdav_proxy"` // 开启之后只会webdav走中转
|
||||
Proxy bool `json:"proxy"` // 是否中转,开启之后web和webdav都会走中转
|
||||
WebdavProxy bool `json:"webdav_proxy"` // 开启之后只会webdav走中转
|
||||
Proxy bool `json:"proxy"` // 是否中转,开启之后web和webdav都会走中转
|
||||
WebdavDirect bool `json:"webdav_direct"` // webdav 下载不跳转
|
||||
//AllowProxy bool `json:"allow_proxy"` // 是否允许中转下载
|
||||
DownProxyUrl string `json:"down_proxy_url"` // 用于中转下载服务的URL 两处 1. path请求中返回的链接 2. down下载时进行302
|
||||
APIProxyUrl string `json:"api_proxy_url"` // 用于中转api的地址
|
||||
@ -43,9 +47,12 @@ type Account struct {
|
||||
AccessSecret string `json:"access_secret"`
|
||||
CustomHost string `json:"custom_host"`
|
||||
ExtractFolder string `json:"extract_folder"`
|
||||
Bool1 bool `json:"bool_1"`
|
||||
}
|
||||
|
||||
var accountsMap = map[string]Account{}
|
||||
var accountsMap = make(map[string]Account)
|
||||
|
||||
var balance = ".balance"
|
||||
|
||||
// SaveAccount save account to database
|
||||
func SaveAccount(account *Account) error {
|
||||
@ -100,6 +107,46 @@ func GetAccount(name string) (Account, bool) {
|
||||
return account, ok
|
||||
}
|
||||
|
||||
func GetAccountsByName(name string) []Account {
|
||||
accounts := make([]Account, 0)
|
||||
if AccountsCount() == 1 {
|
||||
account, _ := GetAccount("")
|
||||
accounts = append(accounts, account)
|
||||
return accounts
|
||||
}
|
||||
for _, v := range accountsMap {
|
||||
if v.Name == name || v.Name == name+balance {
|
||||
accounts = append(accounts, v)
|
||||
}
|
||||
}
|
||||
return accounts
|
||||
}
|
||||
|
||||
var balanceMap sync.Map
|
||||
|
||||
func GetBalancedAccount(name string) (Account, bool) {
|
||||
accounts := GetAccountsByName(name)
|
||||
accountNum := len(accounts)
|
||||
switch accountNum {
|
||||
case 0:
|
||||
return Account{}, false
|
||||
case 1:
|
||||
return accounts[0], true
|
||||
default:
|
||||
cur, ok := balanceMap.Load(name)
|
||||
if ok {
|
||||
i := cur.(int)
|
||||
i = (i + 1) % accountNum
|
||||
balanceMap.Store(name, i)
|
||||
log.Debugln("use: ", i)
|
||||
return accounts[i], true
|
||||
} else {
|
||||
balanceMap.Store(name, 0)
|
||||
return accounts[0], true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetAccountById(id uint) (*Account, error) {
|
||||
var account Account
|
||||
account.ID = id
|
||||
@ -116,6 +163,9 @@ func GetAccountFiles() ([]File, error) {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range accounts {
|
||||
if strings.HasSuffix(v.Name, balance) {
|
||||
continue
|
||||
}
|
||||
files = append(files, File{
|
||||
Name: v.Name,
|
||||
Size: 0,
|
||||
|
@ -97,7 +97,7 @@ func LoadSettings() {
|
||||
favicon, err := GetSettingByKey("favicon")
|
||||
if err == nil {
|
||||
//conf.Favicon = favicon.Value
|
||||
conf.ManageHtml = strings.Replace(conf.RawIndexHtml, "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png", favicon.Value, 1)
|
||||
conf.ManageHtml = strings.Replace(conf.RawIndexHtml, "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", favicon.Value, 1)
|
||||
}
|
||||
title, err := GetSettingByKey("title")
|
||||
if err == nil {
|
||||
@ -114,7 +114,11 @@ func LoadSettings() {
|
||||
// token
|
||||
adminPassword, err := GetSettingByKey("password")
|
||||
if err == nil {
|
||||
conf.Token = utils.GetMD5Encode(fmt.Sprintf("https://github.com/Xhofe/alist-%s", adminPassword.Value))
|
||||
if adminPassword.Value != "" {
|
||||
conf.Token = utils.GetMD5Encode(fmt.Sprintf("https://github.com/Xhofe/alist-%s", adminPassword.Value))
|
||||
} else {
|
||||
conf.Token = ""
|
||||
}
|
||||
}
|
||||
// load settings
|
||||
for _, key := range conf.LoadSettings {
|
||||
|
@ -40,7 +40,7 @@ func ParsePath(rawPath string) (*model.Account, string, base.Driver, error) {
|
||||
path = "/" + strings.Join(paths[2:], "/")
|
||||
name = paths[1]
|
||||
}
|
||||
account, ok := model.GetAccount(name)
|
||||
account, ok := model.GetBalancedAccount(name)
|
||||
if !ok {
|
||||
return nil, "", nil, fmt.Errorf("no [%s] account", name)
|
||||
}
|
||||
|
89
server/common/proxy.go
Normal file
89
server/common/proxy.go
Normal file
@ -0,0 +1,89 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var HttpClient = &http.Client{}
|
||||
|
||||
func Proxy(w http.ResponseWriter, r *http.Request, link *base.Link, file *model.File) error {
|
||||
// 本机读取数据
|
||||
var err error
|
||||
if link.Data != nil {
|
||||
//c.Data(http.StatusOK, "application/octet-stream", link.Data)
|
||||
defer func() {
|
||||
_ = link.Data.Close()
|
||||
}()
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%s`, url.QueryEscape(file.Name)))
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(file.Size, 10))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = io.Copy(w, link.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// 本机文件直接返回文件
|
||||
if link.FilePath != "" {
|
||||
f, err := os.Open(link.FilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
fileStat, err := os.Stat(link.FilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%s`, url.QueryEscape(file.Name)))
|
||||
http.ServeContent(w, r, file.Name, fileStat.ModTime(), f)
|
||||
return nil
|
||||
} else {
|
||||
req, err := http.NewRequest(r.Method, link.Url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for h, val := range r.Header {
|
||||
req.Header[h] = val
|
||||
}
|
||||
for _, header := range link.Headers {
|
||||
req.Header.Set(header.Name, header.Value)
|
||||
}
|
||||
res, err := HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
log.Debugf("proxy status: %d", res.StatusCode)
|
||||
for h, v := range res.Header {
|
||||
w.Header()[h] = v
|
||||
}
|
||||
w.WriteHeader(res.StatusCode)
|
||||
if res.StatusCode >= 400 {
|
||||
all, _ := ioutil.ReadAll(res.Body)
|
||||
msg := string(all)
|
||||
log.Debugln(msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
_, err = io.Copy(w, res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
65
server/controllers/other.go
Normal file
65
server/controllers/other.go
Normal file
@ -0,0 +1,65 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/server/common"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Favicon(c *gin.Context) {
|
||||
c.Redirect(302, conf.GetStr("favicon"))
|
||||
}
|
||||
|
||||
func Plist(c *gin.Context) {
|
||||
data := c.Param("data")
|
||||
data = strings.ReplaceAll(data, "_", "/")
|
||||
data = strings.ReplaceAll(data, "-", "=")
|
||||
bytes, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
u := string(bytes)
|
||||
name := utils.Base(u)
|
||||
ipaIndex := strings.Index(name, ".ipa")
|
||||
if ipaIndex != -1 {
|
||||
name = name[:ipaIndex]
|
||||
}
|
||||
plist := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>items</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>assets</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>kind</key>
|
||||
<string>software-package</string>
|
||||
<key>url</key>
|
||||
<string>%s</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>metadata</key>
|
||||
<dict>
|
||||
<key>bundle-identifier</key>
|
||||
<string>ci.nn.%s</string>
|
||||
<key>bundle-version</key>
|
||||
<string>4.4</string>
|
||||
<key>kind</key>
|
||||
<string>software</string>
|
||||
<key>title</key>
|
||||
<string>%s</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>`, u, name, name)
|
||||
c.Header("Content-Type", "application/xml;charset=utf-8")
|
||||
c.Status(200)
|
||||
_, _ = c.Writer.WriteString(plist)
|
||||
}
|
@ -10,12 +10,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Proxy(c *gin.Context) {
|
||||
@ -62,94 +57,17 @@ func Proxy(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
// 本机读取数据
|
||||
if link.Data != nil {
|
||||
//c.Data(http.StatusOK, "application/octet-stream", link.Data)
|
||||
defer func() {
|
||||
_ = link.Data.Close()
|
||||
}()
|
||||
c.Status(http.StatusOK)
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename=%s`, url.QueryEscape(file.Name)))
|
||||
c.Header("Content-Length", strconv.FormatInt(file.Size, 10))
|
||||
_, err = io.Copy(c.Writer, link.Data)
|
||||
if err != nil {
|
||||
_, _ = c.Writer.WriteString(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
// 本机文件直接返回文件
|
||||
if account.Type == "Native" {
|
||||
// 对于名称为index.html的文件需要特殊处理
|
||||
if utils.Base(rawPath) == "index.html" {
|
||||
file, err := os.Open(link.Url)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
fileStat, err := os.Stat(link.Url)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
http.ServeContent(c.Writer, c.Request, utils.Base(rawPath), fileStat.ModTime(), file)
|
||||
return
|
||||
}
|
||||
c.File(link.Url)
|
||||
return
|
||||
} else {
|
||||
//if utils.GetFileType(filepath.Ext(rawPath)) == conf.TEXT {
|
||||
// Text(c, link)
|
||||
// return
|
||||
//}
|
||||
r := c.Request
|
||||
w := c.Writer
|
||||
//target, err := url.Parse(link.Url)
|
||||
//if err != nil {
|
||||
// common.ErrorResp(c, err, 500)
|
||||
// return
|
||||
//}
|
||||
req, err := http.NewRequest("GET", link.Url, nil)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
for h, val := range r.Header {
|
||||
req.Header[h] = val
|
||||
}
|
||||
for _, header := range link.Headers {
|
||||
req.Header.Set(header.Name, header.Value)
|
||||
}
|
||||
res, err := HttpClient.Do(req)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
log.Debugf("proxy status: %d", res.StatusCode)
|
||||
w.WriteHeader(res.StatusCode)
|
||||
for h, v := range res.Header {
|
||||
w.Header()[h] = v
|
||||
}
|
||||
_, err = io.Copy(w, res.Body)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
err = common.Proxy(c.Writer, c.Request, link, file)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
}
|
||||
}
|
||||
|
||||
var client *resty.Client
|
||||
var HttpClient = &http.Client{}
|
||||
|
||||
func init() {
|
||||
client = resty.New()
|
||||
client.SetRetryCount(3)
|
||||
client.SetRetryCount(3).SetTimeout(base.DefaultTimeout)
|
||||
}
|
||||
|
||||
func Text(c *gin.Context, link *base.Link) {
|
||||
|
@ -15,6 +15,8 @@ func InitApiRouter(r *gin.Engine) {
|
||||
Cors(r)
|
||||
r.GET("/d/*path", middlewares.DownCheck, controllers.Down)
|
||||
r.GET("/p/*path", middlewares.DownCheck, controllers.Proxy)
|
||||
r.GET("/favicon.ico", controllers.Favicon)
|
||||
r.GET("/i/:data/ipa.plist", controllers.Plist)
|
||||
|
||||
api := r.Group("/api")
|
||||
public := api.Group("/public")
|
||||
|
@ -48,8 +48,8 @@ func Static(r *gin.Engine) {
|
||||
r.StaticFS("/assets/", http.FS(assets))
|
||||
r.StaticFS("/public/", http.FS(pub))
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.Status(200)
|
||||
c.Header("Content-Type", "text/html")
|
||||
c.Status(200)
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/@manage") {
|
||||
_, _ = c.Writer.WriteString(conf.ManageHtml)
|
||||
} else {
|
||||
|
@ -50,18 +50,17 @@ func (fs *FileSystem) File(rawPath string) (*model.File, error) {
|
||||
|
||||
func (fs *FileSystem) Files(ctx context.Context, rawPath string) ([]model.File, error) {
|
||||
rawPath = utils.ParsePath(rawPath)
|
||||
var files []model.File
|
||||
var err error
|
||||
if model.AccountsCount() > 1 && rawPath == "/" {
|
||||
files, err := model.GetAccountFiles()
|
||||
files, err = model.GetAccountFiles()
|
||||
} else {
|
||||
account, path_, driver, err := common.ParsePath(rawPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return files, nil
|
||||
files, err = operate.Files(driver, account, path_)
|
||||
}
|
||||
account, path_, driver, err := common.ParsePath(rawPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files, err := operate.Files(driver, account, path_)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -96,7 +95,7 @@ func ClientIP(r *http.Request) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (fs *FileSystem) Link(r *http.Request, rawPath string) (string, error) {
|
||||
func (fs *FileSystem) Link(w http.ResponseWriter, r *http.Request, rawPath string) (string, error) {
|
||||
rawPath = utils.ParsePath(rawPath)
|
||||
log.Debugf("get link path: %s", rawPath)
|
||||
if model.AccountsCount() > 1 && rawPath == "/" {
|
||||
@ -111,6 +110,19 @@ func (fs *FileSystem) Link(r *http.Request, rawPath string) (string, error) {
|
||||
if r.TLS != nil {
|
||||
protocol = "https"
|
||||
}
|
||||
// 直接返回
|
||||
if account.WebdavDirect {
|
||||
file, err := fs.File(rawPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
link_, err := driver.Link(base.Args{Path: path_}, account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = common.Proxy(w, r, link_, file)
|
||||
return "", err
|
||||
}
|
||||
if driver.Config().OnlyProxy || account.WebdavProxy {
|
||||
link = fmt.Sprintf("%s://%s/p%s", protocol, r.Host, rawPath)
|
||||
if conf.GetBool("check down link") {
|
||||
|
@ -237,11 +237,14 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request, fs *
|
||||
}
|
||||
w.Header().Set("ETag", etag)
|
||||
log.Debugf("url: %+v", r.URL)
|
||||
link, err := fs.Link(r, reqPath)
|
||||
link, err := fs.Link(w, r, reqPath)
|
||||
if err != nil {
|
||||
log.Debugf("webdav link error: %s", err.Error())
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
http.Redirect(w, r, link, 302)
|
||||
if link != "" {
|
||||
http.Redirect(w, r, link, 302)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
|
@ -127,3 +127,11 @@ func Join(elem ...string) string {
|
||||
func Split(p string) (string, string) {
|
||||
return path.Split(p)
|
||||
}
|
||||
|
||||
// FormatName TODO
|
||||
func FormatName(name string) string {
|
||||
name = strings.ReplaceAll(name, "/", " ")
|
||||
name = strings.ReplaceAll(name, "#", " ")
|
||||
name = strings.ReplaceAll(name, "?", " ")
|
||||
return name
|
||||
}
|
||||
|
Reference in New Issue
Block a user