Compare commits
75 Commits
Author | SHA1 | Date | |
---|---|---|---|
d4e3355f56 | |||
94f257e557 | |||
e5f53d6dee | |||
cbd4bef814 | |||
2d57529e77 | |||
2b74999703 | |||
fe081d0ebc | |||
5ef7a27be3 | |||
c9a18f4de6 | |||
f2a24881d0 | |||
cee00005ab | |||
049575b5a5 | |||
a93937f80d | |||
488ebaa1af | |||
8278d3875b | |||
736ba44031 | |||
a6ff6a94df | |||
17f78b948a | |||
fe1040a367 | |||
83048e6c7c | |||
9128647970 | |||
9629705100 | |||
cd663f78af | |||
3c483ace4f | |||
3e949fcf33 | |||
81b0afc349 | |||
a04da3ec50 | |||
9e0482afbb | |||
9de40f8976 | |||
ba4df55d6e | |||
de8d2d6dc0 | |||
65b423c503 | |||
ff20b5a6fb | |||
37d86ff55c | |||
4e1c67617f | |||
9bc2d340a2 | |||
60fc416d8f | |||
99c9632cdc | |||
2fb772c888 | |||
87192ad07d | |||
3746831384 | |||
80d4fbb870 | |||
92c65b450e | |||
213fc0232e | |||
33be44adad | |||
ca0d66bd01 | |||
3a3d0adfa0 | |||
ca30849e24 | |||
316f3569a5 | |||
2705877235 | |||
432901db5a | |||
227d034db8 | |||
453d7da622 | |||
29fe49fb87 | |||
fcf2683112 | |||
3a996a1a3a | |||
1b14d33b9f | |||
639b7817bf | |||
163af0515f | |||
8e2b9c681a | |||
0a8d710e01 | |||
d781f7127a | |||
85d743c5d2 | |||
5f60b51cf8 | |||
7013d1b7b8 | |||
9eec872637 | |||
037850bbd5 | |||
bbe3d4e19f | |||
78a9676c7c | |||
8bf93562eb | |||
b57afd0a98 | |||
f261ef50cc | |||
7e7b9b9b48 | |||
2313213f59 | |||
5f28532423 |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/Xhofe/alist/discussions
|
||||
url: https://github.com/alist-org/alist/discussions
|
||||
about: Use GitHub discussions for message-board style questions and discussions.
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: benjlevesque/short-sha@v2.2
|
||||
- uses: benjlevesque/short-sha@v3.0
|
||||
id: short-sha
|
||||
|
||||
- name: Install dependencies
|
||||
|
4
.github/workflows/build_docker.yml
vendored
4
.github/workflows/build_docker.yml
vendored
@ -65,7 +65,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ci
|
||||
@ -80,7 +80,7 @@ jobs:
|
||||
|
||||
- name: Build and push with ffmpeg
|
||||
id: docker_build_ffmpeg
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ffmpeg
|
||||
|
2
.github/workflows/issue_question.yml
vendored
2
.github/workflows/issue_question.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
if: github.event.label.name == 'question'
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: actions-cool/issues-helper@v3.5.2
|
||||
uses: actions-cool/issues-helper@v3.6.0
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -42,7 +42,7 @@ jobs:
|
||||
bash build.sh release
|
||||
|
||||
- name: Upload assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/compress/*
|
||||
prerelease: false
|
||||
|
2
.github/workflows/release_android.yml
vendored
2
.github/workflows/release_android.yml
vendored
@ -29,6 +29,6 @@ jobs:
|
||||
bash build.sh release android
|
||||
|
||||
- name: Upload assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/compress/*
|
||||
|
4
.github/workflows/release_docker.yml
vendored
4
.github/workflows/release_docker.yml
vendored
@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ci
|
||||
@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
- name: Build and push with ffmpeg
|
||||
id: docker_build_ffmpeg
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ffmpeg
|
||||
|
2
.github/workflows/release_linux_musl.yml
vendored
2
.github/workflows/release_linux_musl.yml
vendored
@ -29,6 +29,6 @@ jobs:
|
||||
bash build.sh release linux_musl
|
||||
|
||||
- name: Upload assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/compress/*
|
||||
|
2
.github/workflows/release_linux_musl_arm.yml
vendored
2
.github/workflows/release_linux_musl_arm.yml
vendored
@ -29,6 +29,6 @@ jobs:
|
||||
bash build.sh release linux_musl_arm
|
||||
|
||||
- name: Upload assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/compress/*
|
||||
|
14
README.md
14
README.md
@ -5,13 +5,13 @@
|
||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
||||
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
|
||||
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
|
||||
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/releases">
|
||||
<a href="https://github.com/alist-org/alist/releases">
|
||||
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
||||
</a>
|
||||
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
||||
@ -19,13 +19,13 @@
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://github.com/Xhofe/alist/discussions">
|
||||
<a href="https://github.com/alist-org/alist/discussions">
|
||||
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
||||
</a>
|
||||
<a href="https://discord.gg/F4ymsH4xv2">
|
||||
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/releases">
|
||||
<a href="https://github.com/alist-org/alist/releases">
|
||||
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/xhofe/alist">
|
||||
@ -106,7 +106,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
||||
|
||||
## Discussion
|
||||
|
||||
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports and feature requests only.**
|
||||
Please go to our [discussion forum](https://github.com/alist-org/alist/discussions) for general questions, **issues are for bug reports and feature requests only.**
|
||||
|
||||
## Sponsor
|
||||
|
||||
@ -138,4 +138,4 @@ The `AList` is open-source software licensed under the AGPL-3.0 license.
|
||||
|
||||
---
|
||||
|
||||
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||
|
14
README_cn.md
14
README_cn.md
@ -5,13 +5,13 @@
|
||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
||||
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
|
||||
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
|
||||
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/releases">
|
||||
<a href="https://github.com/alist-org/alist/releases">
|
||||
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
||||
</a>
|
||||
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
||||
@ -19,13 +19,13 @@
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://github.com/Xhofe/alist/discussions">
|
||||
<a href="https://github.com/alist-org/alist/discussions">
|
||||
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
||||
</a>
|
||||
<a href="https://discord.gg/F4ymsH4xv2">
|
||||
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/releases">
|
||||
<a href="https://github.com/alist-org/alist/releases">
|
||||
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/xhofe/alist">
|
||||
@ -105,7 +105,7 @@
|
||||
|
||||
## 讨论
|
||||
|
||||
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) ,**issue仅针对错误报告和功能请求。**
|
||||
一般问题请到[讨论论坛](https://github.com/alist-org/alist/discussions) ,**issue仅针对错误报告和功能请求。**
|
||||
|
||||
## 赞助
|
||||
|
||||
@ -136,4 +136,4 @@ Thanks goes to these wonderful people:
|
||||
|
||||
---
|
||||
|
||||
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||
|
14
README_ja.md
14
README_ja.md
@ -5,13 +5,13 @@
|
||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
||||
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
|
||||
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
|
||||
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/releases">
|
||||
<a href="https://github.com/alist-org/alist/releases">
|
||||
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
||||
</a>
|
||||
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
||||
@ -19,13 +19,13 @@
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://github.com/Xhofe/alist/discussions">
|
||||
<a href="https://github.com/alist-org/alist/discussions">
|
||||
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
||||
</a>
|
||||
<a href="https://discord.gg/F4ymsH4xv2">
|
||||
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
||||
</a>
|
||||
<a href="https://github.com/Xhofe/alist/releases">
|
||||
<a href="https://github.com/alist-org/alist/releases">
|
||||
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/xhofe/alist">
|
||||
@ -106,7 +106,7 @@
|
||||
|
||||
## ディスカッション
|
||||
|
||||
一般的なご質問は[ディスカッションフォーラム](https://github.com/Xhofe/alist/discussions)をご利用ください。**問題はバグレポートと機能リクエストのみです。**
|
||||
一般的なご質問は[ディスカッションフォーラム](https://github.com/alist-org/alist/discussions)をご利用ください。**問題はバグレポートと機能リクエストのみです。**
|
||||
|
||||
## スポンサー
|
||||
|
||||
@ -138,4 +138,4 @@ https://alist.nn.ci/guide/sponsor.html
|
||||
|
||||
---
|
||||
|
||||
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||
|
9
build.sh
9
build.sh
@ -85,14 +85,7 @@ BuildDev() {
|
||||
cat md5.txt
|
||||
}
|
||||
|
||||
PrepareBuildDocker() {
|
||||
echo "replace github.com/mattn/go-sqlite3 => github.com/leso-kn/go-sqlite3 v0.0.0-20230710125852-03158dc838ed" >>go.mod
|
||||
go get gorm.io/driver/sqlite@v1.4.4
|
||||
go mod download
|
||||
}
|
||||
|
||||
BuildDocker() {
|
||||
PrepareBuildDocker
|
||||
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
|
||||
}
|
||||
|
||||
@ -110,7 +103,7 @@ PrepareBuildDockerMusl() {
|
||||
}
|
||||
|
||||
BuildDockerMultiplatform() {
|
||||
PrepareBuildDocker
|
||||
go mod download
|
||||
|
||||
# run PrepareBuildDockerMusl before build
|
||||
export PATH=$PATH:$PWD/build/musl-libs/bin
|
||||
|
@ -53,7 +53,7 @@ func (d *Pan123) Drop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files, err := d.getFiles(dir.GetID())
|
||||
files, err := d.getFiles(dir.GetID(), dir.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -9,14 +9,15 @@ type Addition struct {
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
driver.RootID
|
||||
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
AccessToken string
|
||||
//OrderBy string `json:"order_by" type:"select" options:"file_id,file_name,size,update_at" default:"file_name"`
|
||||
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "123Pan",
|
||||
DefaultRoot: "0",
|
||||
LocalSort: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -87,8 +87,9 @@ var _ model.Thumb = (*File)(nil)
|
||||
type Files struct {
|
||||
//BaseResp
|
||||
Data struct {
|
||||
InfoList []File `json:"InfoList"`
|
||||
Next string `json:"Next"`
|
||||
Total int `json:"Total"`
|
||||
InfoList []File `json:"InfoList"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
resty "github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
@ -232,8 +233,9 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (d *Pan123) getFiles(parentId string) ([]File, error) {
|
||||
func (d *Pan123) getFiles(parentId string, name string) ([]File, error) {
|
||||
page := 1
|
||||
total := 0
|
||||
res := make([]File, 0)
|
||||
// 2024-02-06 fix concurrency by 123pan
|
||||
for {
|
||||
@ -246,8 +248,8 @@ func (d *Pan123) getFiles(parentId string) ([]File, error) {
|
||||
"driveId": "0",
|
||||
"limit": "100",
|
||||
"next": "0",
|
||||
"orderBy": d.OrderBy,
|
||||
"orderDirection": d.OrderDirection,
|
||||
"orderBy": "file_id",
|
||||
"orderDirection": "desc",
|
||||
"parentFileId": parentId,
|
||||
"trashed": "false",
|
||||
"SearchData": "",
|
||||
@ -257,17 +259,22 @@ func (d *Pan123) getFiles(parentId string) ([]File, error) {
|
||||
"operateType": "4",
|
||||
"inDirectSpace": "false",
|
||||
}
|
||||
_, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
|
||||
_res, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(string(_res))
|
||||
page++
|
||||
res = append(res, resp.Data.InfoList...)
|
||||
total = resp.Data.Total
|
||||
if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(res) != total {
|
||||
log.Warnf("incorrect file count from remote at %s: expected %d, got %d", name, total, len(res))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
@ -9,9 +9,9 @@ type Addition struct {
|
||||
ShareKey string `json:"sharekey" required:"true"`
|
||||
SharePwd string `json:"sharepassword"`
|
||||
driver.RootID
|
||||
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
AccessToken string `json:"accesstoken" type:"text"`
|
||||
//OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
|
||||
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
AccessToken string `json:"accesstoken" type:"text"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -92,8 +92,8 @@ func (d *Pan123Share) getFiles(parentId string) ([]File, error) {
|
||||
query := map[string]string{
|
||||
"limit": "100",
|
||||
"next": "0",
|
||||
"orderBy": d.OrderBy,
|
||||
"orderDirection": d.OrderDirection,
|
||||
"orderBy": "file_id",
|
||||
"orderDirection": "desc",
|
||||
"parentFileId": parentId,
|
||||
"Page": strconv.Itoa(page),
|
||||
"shareKey": d.ShareKey,
|
||||
|
@ -14,12 +14,15 @@ type Addition struct {
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "139Yun",
|
||||
LocalSort: true,
|
||||
Name: "139Yun",
|
||||
LocalSort: true,
|
||||
ProxyRangeOption: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Yun139{}
|
||||
d := &Yun139{}
|
||||
d.ProxyRange = true
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ type FamilyInfoListResp struct {
|
||||
type FamilyInfoResp struct {
|
||||
Count int `json:"count"`
|
||||
CreateTime string `json:"createTime"`
|
||||
FamilyID int `json:"familyId"`
|
||||
FamilyID int64 `json:"familyId"`
|
||||
RemarkName string `json:"remarkName"`
|
||||
Type int `json:"type"`
|
||||
UseFlag int `json:"useFlag"`
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
@ -45,6 +46,9 @@ func (d *Alias) Init(ctx context.Context) error {
|
||||
d.oneKey = k
|
||||
}
|
||||
d.autoFlatten = true
|
||||
} else {
|
||||
d.oneKey = ""
|
||||
d.autoFlatten = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -87,8 +91,9 @@ func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
var objs []model.Obj
|
||||
fsArgs := &fs.ListArgs{NoLog: true, Refresh: args.Refresh}
|
||||
for _, dst := range dsts {
|
||||
tmp, err := d.list(ctx, dst, sub)
|
||||
tmp, err := d.list(ctx, dst, sub, fsArgs)
|
||||
if err == nil {
|
||||
objs = append(objs, tmp...)
|
||||
}
|
||||
@ -111,4 +116,26 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
func (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
reqPath, err := d.getReqPath(ctx, srcObj)
|
||||
if err == nil {
|
||||
return fs.Rename(ctx, *reqPath, newName)
|
||||
}
|
||||
if errs.IsNotImplement(err) {
|
||||
return errors.New("same-name files cannot be Rename")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Alias) Remove(ctx context.Context, obj model.Obj) error {
|
||||
reqPath, err := d.getReqPath(ctx, obj)
|
||||
if err == nil {
|
||||
return fs.Remove(ctx, *reqPath)
|
||||
}
|
||||
if errs.IsNotImplement(err) {
|
||||
return errors.New("same-name files cannot be Delete")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Alias)(nil)
|
||||
|
@ -9,19 +9,25 @@ type Addition struct {
|
||||
// Usually one of two
|
||||
// driver.RootPath
|
||||
// define other
|
||||
Paths string `json:"paths" required:"true" type:"text"`
|
||||
Paths string `json:"paths" required:"true" type:"text"`
|
||||
ProtectSameName bool `json:"protect_same_name" default:"true" required:"false" help:"Protects same-name files from Delete or Rename"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Alias",
|
||||
LocalSort: true,
|
||||
NoCache: true,
|
||||
NoUpload: true,
|
||||
DefaultRoot: "/",
|
||||
Name: "Alias",
|
||||
LocalSort: true,
|
||||
NoCache: true,
|
||||
NoUpload: true,
|
||||
DefaultRoot: "/",
|
||||
ProxyRangeOption: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Alias{}
|
||||
return &Alias{
|
||||
Addition: Addition{
|
||||
ProtectSameName: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
stdpath "path"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/sign"
|
||||
@ -15,7 +16,7 @@ import (
|
||||
|
||||
func (d *Alias) listRoot() []model.Obj {
|
||||
var objs []model.Obj
|
||||
for k, _ := range d.pathMap {
|
||||
for k := range d.pathMap {
|
||||
obj := model.Object{
|
||||
Name: k,
|
||||
IsFolder: true,
|
||||
@ -64,8 +65,8 @@ func (d *Alias) get(ctx context.Context, path string, dst, sub string) (model.Ob
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Alias) list(ctx context.Context, dst, sub string) ([]model.Obj, error) {
|
||||
objs, err := fs.List(ctx, stdpath.Join(dst, sub), &fs.ListArgs{NoLog: true})
|
||||
func (d *Alias) list(ctx context.Context, dst, sub string, args *fs.ListArgs) ([]model.Obj, error) {
|
||||
objs, err := fs.List(ctx, stdpath.Join(dst, sub), args)
|
||||
// the obj must implement the model.SetPath interface
|
||||
// return objs, err
|
||||
if err != nil {
|
||||
@ -102,13 +103,49 @@ func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs)
|
||||
return nil, err
|
||||
}
|
||||
if common.ShouldProxy(storage, stdpath.Base(sub)) {
|
||||
return &model.Link{
|
||||
link := &model.Link{
|
||||
URL: fmt.Sprintf("%s/p%s?sign=%s",
|
||||
common.GetApiUrl(args.HttpReq),
|
||||
utils.EncodePath(reqPath, true),
|
||||
sign.Sign(reqPath)),
|
||||
}, nil
|
||||
}
|
||||
if args.HttpReq != nil && d.ProxyRange {
|
||||
link.RangeReadCloser = common.NoProxyRange
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
link, _, err := fs.Link(ctx, reqPath, args)
|
||||
return link, err
|
||||
}
|
||||
|
||||
func (d *Alias) getReqPath(ctx context.Context, obj model.Obj) (*string, error) {
|
||||
root, sub := d.getRootAndPath(obj.GetPath())
|
||||
if sub == "" {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
dsts, ok := d.pathMap[root]
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
var reqPath *string
|
||||
for _, dst := range dsts {
|
||||
path := stdpath.Join(dst, sub)
|
||||
_, err := fs.Get(ctx, path, &fs.GetArgs{NoLog: true})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !d.ProtectSameName {
|
||||
return &path, nil
|
||||
}
|
||||
if ok {
|
||||
ok = false
|
||||
} else {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
reqPath = &path
|
||||
}
|
||||
if reqPath == nil {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
return reqPath, nil
|
||||
}
|
||||
|
@ -6,9 +6,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
@ -17,6 +15,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AListV3 struct {
|
||||
@ -42,7 +41,7 @@ func (d *AListV3) Init(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
// if the username is not empty and the username is not the same as the current username, then login again
|
||||
if d.Username != "" && d.Username != resp.Data.Username {
|
||||
if d.Username != resp.Data.Username {
|
||||
err = d.login()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -109,11 +108,19 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
|
||||
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var resp common.Resp[FsGetResp]
|
||||
// if PassUAToUpsteam is true, then pass the user-agent to the upstream
|
||||
userAgent := base.UserAgent
|
||||
if d.PassUAToUpsteam {
|
||||
userAgent = args.Header.Get("user-agent")
|
||||
if userAgent == "" {
|
||||
userAgent = base.UserAgent
|
||||
}
|
||||
}
|
||||
_, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetBody(FsGetReq{
|
||||
Path: file.GetPath(),
|
||||
Password: d.MetaPassword,
|
||||
})
|
||||
}).SetHeader("user-agent", userAgent)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -175,14 +182,41 @@ func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
|
||||
}
|
||||
|
||||
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
_, err := d.requestWithTimeout("/fs/put", http.MethodPut, func(req *resty.Request) {
|
||||
req.SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
|
||||
SetHeader("Password", d.MetaPassword).
|
||||
SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
|
||||
SetContentLength(true).
|
||||
SetBody(io.ReadCloser(stream))
|
||||
}, time.Hour*6)
|
||||
return err
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+"/api/fs/put", stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", d.Token)
|
||||
req.Header.Set("File-Path", path.Join(dstDir.GetPath(), stream.GetName()))
|
||||
req.Header.Set("Password", d.MetaPassword)
|
||||
|
||||
req.ContentLength = stream.GetSize()
|
||||
// client := base.NewHttpClient()
|
||||
// client.Timeout = time.Hour * 6
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytes, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("[alist_v3] response body: %s", string(bytes))
|
||||
if res.StatusCode >= 400 {
|
||||
return fmt.Errorf("request failed, status: %s", res.Status)
|
||||
}
|
||||
code := utils.Json.Get(bytes, "code").ToInt()
|
||||
if code != 200 {
|
||||
if code == 401 || code == 403 {
|
||||
err = d.login()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(bytes, "message").ToString())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
|
@ -7,18 +7,20 @@ import (
|
||||
|
||||
type Addition struct {
|
||||
driver.RootPath
|
||||
Address string `json:"url" required:"true"`
|
||||
MetaPassword string `json:"meta_password"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
Address string `json:"url" required:"true"`
|
||||
MetaPassword string `json:"meta_password"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
PassUAToUpsteam bool `json:"pass_ua_to_upsteam" default:"true"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "AList V3",
|
||||
LocalSort: true,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: true,
|
||||
Name: "AList V3",
|
||||
LocalSort: true,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: true,
|
||||
ProxyRangeOption: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -3,7 +3,6 @@ package alist_v3
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
@ -14,6 +13,9 @@ import (
|
||||
)
|
||||
|
||||
func (d *AListV3) login() error {
|
||||
if d.Username == "" {
|
||||
return nil
|
||||
}
|
||||
var resp common.Resp[LoginResp]
|
||||
_, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetBody(base.Json{
|
||||
@ -57,33 +59,3 @@ func (d *AListV3) request(api, method string, callback base.ReqCallback, retry .
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *AListV3) requestWithTimeout(api, method string, callback base.ReqCallback, timeout time.Duration, retry ...bool) ([]byte, error) {
|
||||
url := d.Address + "/api" + api
|
||||
client := base.NewRestyClient().SetTimeout(timeout)
|
||||
req := client.R()
|
||||
req.SetHeader("Authorization", d.Token)
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
res, err := req.Execute(method, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("[alist_v3] response body: %s", res.String())
|
||||
if res.StatusCode() >= 400 {
|
||||
return nil, fmt.Errorf("request failed, status: %s", res.Status())
|
||||
}
|
||||
code := utils.Json.Get(res.Body(), "code").ToInt()
|
||||
if code != 200 {
|
||||
if (code == 401 || code == 403) && !utils.IsBool(retry...) {
|
||||
err = d.login()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.requestWithTimeout(api, method, callback, timeout, true)
|
||||
}
|
||||
return nil, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString())
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
||||
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
||||
_ "github.com/alist-org/alist/v3/drivers/google_photo"
|
||||
_ "github.com/alist-org/alist/v3/drivers/halalcloud"
|
||||
_ "github.com/alist-org/alist/v3/drivers/ilanzou"
|
||||
_ "github.com/alist-org/alist/v3/drivers/ipfs_api"
|
||||
_ "github.com/alist-org/alist/v3/drivers/lanzou"
|
||||
@ -32,8 +33,10 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
||||
_ "github.com/alist-org/alist/v3/drivers/mega"
|
||||
_ "github.com/alist-org/alist/v3/drivers/mopan"
|
||||
_ "github.com/alist-org/alist/v3/drivers/netease_music"
|
||||
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
||||
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
|
||||
_ "github.com/alist-org/alist/v3/drivers/onedrive_sharelink"
|
||||
_ "github.com/alist-org/alist/v3/drivers/pikpak"
|
||||
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
|
||||
@ -45,6 +48,8 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/teambition"
|
||||
_ "github.com/alist-org/alist/v3/drivers/terabox"
|
||||
_ "github.com/alist-org/alist/v3/drivers/thunder"
|
||||
_ "github.com/alist-org/alist/v3/drivers/thunder_browser"
|
||||
_ "github.com/alist-org/alist/v3/drivers/thunderx"
|
||||
_ "github.com/alist-org/alist/v3/drivers/trainbit"
|
||||
_ "github.com/alist-org/alist/v3/drivers/url_tree"
|
||||
_ "github.com/alist-org/alist/v3/drivers/uss"
|
||||
|
@ -17,7 +17,7 @@ type Addition struct {
|
||||
AccessToken string
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
|
||||
CustomUploadPartSize int64 `json:"custom_upload_part_size" default:"0" help:"0 for auto"`
|
||||
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -39,7 +39,7 @@ func (d *FTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]m
|
||||
if err := d.login(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := d.conn.List(dir.GetPath())
|
||||
entries, err := d.conn.List(encode(dir.GetPath(), d.Encoding))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -49,7 +49,7 @@ func (d *FTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]m
|
||||
continue
|
||||
}
|
||||
f := model.Object{
|
||||
Name: entry.Name,
|
||||
Name: decode(entry.Name, d.Encoding),
|
||||
Size: int64(entry.Size),
|
||||
Modified: entry.Time,
|
||||
IsFolder: entry.Type == ftp.EntryTypeFolder,
|
||||
@ -64,7 +64,7 @@ func (d *FTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*m
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := NewFileReader(d.conn, file.GetPath(), file.GetSize())
|
||||
r := NewFileReader(d.conn, encode(file.GetPath(), d.Encoding), file.GetSize())
|
||||
link := &model.Link{
|
||||
MFile: r,
|
||||
}
|
||||
@ -75,21 +75,27 @@ func (d *FTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string)
|
||||
if err := d.login(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.conn.MakeDir(stdpath.Join(parentDir.GetPath(), dirName))
|
||||
return d.conn.MakeDir(encode(stdpath.Join(parentDir.GetPath(), dirName), d.Encoding))
|
||||
}
|
||||
|
||||
func (d *FTP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
if err := d.login(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.conn.Rename(srcObj.GetPath(), stdpath.Join(dstDir.GetPath(), srcObj.GetName()))
|
||||
return d.conn.Rename(
|
||||
encode(srcObj.GetPath(), d.Encoding),
|
||||
encode(stdpath.Join(dstDir.GetPath(), srcObj.GetName()), d.Encoding),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *FTP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
if err := d.login(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.conn.Rename(srcObj.GetPath(), stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName))
|
||||
return d.conn.Rename(
|
||||
encode(srcObj.GetPath(), d.Encoding),
|
||||
encode(stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName), d.Encoding),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *FTP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
@ -100,10 +106,11 @@ func (d *FTP) Remove(ctx context.Context, obj model.Obj) error {
|
||||
if err := d.login(); err != nil {
|
||||
return err
|
||||
}
|
||||
path := encode(obj.GetPath(), d.Encoding)
|
||||
if obj.IsDir() {
|
||||
return d.conn.RemoveDirRecur(obj.GetPath())
|
||||
return d.conn.RemoveDirRecur(path)
|
||||
} else {
|
||||
return d.conn.Delete(obj.GetPath())
|
||||
return d.conn.Delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +119,8 @@ func (d *FTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStream
|
||||
return err
|
||||
}
|
||||
// TODO: support cancel
|
||||
return d.conn.Stor(stdpath.Join(dstDir.GetPath(), stream.GetName()), stream)
|
||||
path := stdpath.Join(dstDir.GetPath(), stream.GetName())
|
||||
return d.conn.Stor(encode(path, d.Encoding), stream)
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*FTP)(nil)
|
||||
|
@ -3,10 +3,28 @@ package ftp
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/axgle/mahonia"
|
||||
)
|
||||
|
||||
func encode(str string, encoding string) string {
|
||||
if encoding == "" {
|
||||
return str
|
||||
}
|
||||
encoder := mahonia.NewEncoder(encoding)
|
||||
return encoder.ConvertString(str)
|
||||
}
|
||||
|
||||
func decode(str string, encoding string) string {
|
||||
if encoding == "" {
|
||||
return str
|
||||
}
|
||||
decoder := mahonia.NewDecoder(encoding)
|
||||
return decoder.ConvertString(str)
|
||||
}
|
||||
|
||||
type Addition struct {
|
||||
Address string `json:"address" required:"true"`
|
||||
Encoding string `json:"encoding" required:"true"`
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
driver.RootPath
|
||||
|
406
drivers/halalcloud/driver.go
Normal file
406
drivers/halalcloud/driver.go
Normal file
@ -0,0 +1,406 @@
|
||||
package halalcloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/city404/v6-public-rpc-proto/go/v6/common"
|
||||
pbPublicUser "github.com/city404/v6-public-rpc-proto/go/v6/user"
|
||||
pubUserFile "github.com/city404/v6-public-rpc-proto/go/v6/userfile"
|
||||
"github.com/rclone/rclone/lib/readers"
|
||||
"github.com/zzzhr1990/go-common-entity/userfile"
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HalalCloud struct {
|
||||
*HalalCommon
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
uploadThread int
|
||||
}
|
||||
|
||||
func (d *HalalCloud) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *HalalCloud) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *HalalCloud) Init(ctx context.Context) error {
|
||||
d.uploadThread, _ = strconv.Atoi(d.UploadThread)
|
||||
if d.uploadThread < 1 || d.uploadThread > 32 {
|
||||
d.uploadThread, d.UploadThread = 3, "3"
|
||||
}
|
||||
|
||||
if d.HalalCommon == nil {
|
||||
d.HalalCommon = &HalalCommon{
|
||||
Common: &Common{},
|
||||
AuthService: &AuthService{
|
||||
appID: func() string {
|
||||
if d.Addition.AppID != "" {
|
||||
return d.Addition.AppID
|
||||
}
|
||||
return AppID
|
||||
}(),
|
||||
appVersion: func() string {
|
||||
if d.Addition.AppVersion != "" {
|
||||
return d.Addition.AppVersion
|
||||
}
|
||||
return AppVersion
|
||||
}(),
|
||||
appSecret: func() string {
|
||||
if d.Addition.AppSecret != "" {
|
||||
return d.Addition.AppSecret
|
||||
}
|
||||
return AppSecret
|
||||
}(),
|
||||
tr: &TokenResp{
|
||||
RefreshToken: d.Addition.RefreshToken,
|
||||
},
|
||||
},
|
||||
UserInfo: &UserInfo{},
|
||||
refreshTokenFunc: func(token string) error {
|
||||
d.Addition.RefreshToken = token
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 防止重复登录
|
||||
if d.Addition.RefreshToken == "" || !d.IsLogin() {
|
||||
as, err := d.NewAuthServiceWithOauth()
|
||||
if err != nil {
|
||||
d.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||
return err
|
||||
}
|
||||
d.HalalCommon.AuthService = as
|
||||
d.SetTokenResp(as.tr)
|
||||
op.MustSaveDriverStorage(d)
|
||||
}
|
||||
var err error
|
||||
d.HalalCommon.serv, err = d.NewAuthService(d.Addition.RefreshToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HalalCloud) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HalalCloud) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
return d.getFiles(ctx, dir)
|
||||
}
|
||||
|
||||
func (d *HalalCloud) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
return d.getLink(ctx, file, args)
|
||||
}
|
||||
|
||||
func (d *HalalCloud) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
return d.makeDir(ctx, parentDir, dirName)
|
||||
}
|
||||
|
||||
func (d *HalalCloud) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return d.move(ctx, srcObj, dstDir)
|
||||
}
|
||||
|
||||
func (d *HalalCloud) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
return d.rename(ctx, srcObj, newName)
|
||||
}
|
||||
|
||||
func (d *HalalCloud) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return d.copy(ctx, srcObj, dstDir)
|
||||
}
|
||||
|
||||
func (d *HalalCloud) Remove(ctx context.Context, obj model.Obj) error {
|
||||
return d.remove(ctx, obj)
|
||||
}
|
||||
|
||||
func (d *HalalCloud) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
return d.put(ctx, dstDir, stream, up)
|
||||
}
|
||||
|
||||
func (d *HalalCloud) IsLogin() bool {
|
||||
if d.AuthService.tr == nil {
|
||||
return false
|
||||
}
|
||||
serv, err := d.NewAuthService(d.Addition.RefreshToken)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
result, err := pbPublicUser.NewPubUserClient(serv.GetGrpcConnection()).Get(ctx, &pbPublicUser.User{
|
||||
Identity: "",
|
||||
})
|
||||
if result == nil || err != nil {
|
||||
return false
|
||||
}
|
||||
d.UserInfo.Identity = result.Identity
|
||||
d.UserInfo.CreateTs = result.CreateTs
|
||||
d.UserInfo.Name = result.Name
|
||||
d.UserInfo.UpdateTs = result.UpdateTs
|
||||
return true
|
||||
}
|
||||
|
||||
type HalalCommon struct {
|
||||
*Common
|
||||
*AuthService // 登录信息
|
||||
*UserInfo // 用户信息
|
||||
refreshTokenFunc func(token string) error
|
||||
serv *AuthService
|
||||
}
|
||||
|
||||
func (d *HalalCloud) SetTokenResp(tr *TokenResp) {
|
||||
d.Addition.RefreshToken = tr.RefreshToken
|
||||
}
|
||||
|
||||
func (d *HalalCloud) getFiles(ctx context.Context, dir model.Obj) ([]model.Obj, error) {
|
||||
|
||||
files := make([]model.Obj, 0)
|
||||
limit := int64(100)
|
||||
token := ""
|
||||
client := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection())
|
||||
|
||||
opDir := d.GetCurrentDir(dir)
|
||||
|
||||
for {
|
||||
result, err := client.List(ctx, &pubUserFile.FileListRequest{
|
||||
Parent: &pubUserFile.File{Path: opDir},
|
||||
ListInfo: &common.ScanListRequest{
|
||||
Limit: limit,
|
||||
Token: token,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := 0; len(result.Files) > i; i++ {
|
||||
files = append(files, (*Files)(result.Files[i]))
|
||||
}
|
||||
|
||||
if result.ListInfo == nil || result.ListInfo.Token == "" {
|
||||
break
|
||||
}
|
||||
token = result.ListInfo.Token
|
||||
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *HalalCloud) getLink(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
|
||||
client := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection())
|
||||
ctx1, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
result, err := client.ParseFileSlice(ctx1, (*pubUserFile.File)(file.(*Files)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileAddrs := []*pubUserFile.SliceDownloadInfo{}
|
||||
var addressDuration int64
|
||||
|
||||
nodesNumber := len(result.RawNodes)
|
||||
nodesIndex := nodesNumber - 1
|
||||
startIndex, endIndex := 0, nodesIndex
|
||||
for nodesIndex >= 0 {
|
||||
if nodesIndex >= 200 {
|
||||
endIndex = 200
|
||||
} else {
|
||||
endIndex = nodesNumber
|
||||
}
|
||||
for ; endIndex <= nodesNumber; endIndex += 200 {
|
||||
if endIndex == 0 {
|
||||
endIndex = 1
|
||||
}
|
||||
sliceAddress, err := client.GetSliceDownloadAddress(ctx, &pubUserFile.SliceDownloadAddressRequest{
|
||||
Identity: result.RawNodes[startIndex:endIndex],
|
||||
Version: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addressDuration = sliceAddress.ExpireAt
|
||||
fileAddrs = append(fileAddrs, sliceAddress.Addresses...)
|
||||
startIndex = endIndex
|
||||
nodesIndex -= 200
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
size := result.FileSize
|
||||
chunks := getChunkSizes(result.Sizes)
|
||||
var finalClosers utils.Closers
|
||||
resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
|
||||
length := httpRange.Length
|
||||
if httpRange.Length >= 0 && httpRange.Start+httpRange.Length >= size {
|
||||
length = -1
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open download file failed: %w", err)
|
||||
}
|
||||
oo := &openObject{
|
||||
ctx: ctx,
|
||||
d: fileAddrs,
|
||||
chunk: &[]byte{},
|
||||
chunks: &chunks,
|
||||
skip: httpRange.Start,
|
||||
sha: result.Sha1,
|
||||
shaTemp: sha1.New(),
|
||||
}
|
||||
finalClosers.Add(oo)
|
||||
|
||||
return readers.NewLimitedReadCloser(oo, length), nil
|
||||
}
|
||||
|
||||
var duration time.Duration
|
||||
if addressDuration != 0 {
|
||||
duration = time.Until(time.UnixMilli(addressDuration))
|
||||
} else {
|
||||
duration = time.Until(time.Now().Add(time.Hour))
|
||||
}
|
||||
|
||||
resultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: finalClosers}
|
||||
return &model.Link{
|
||||
RangeReadCloser: resultRangeReadCloser,
|
||||
Expiration: &duration,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *HalalCloud) makeDir(ctx context.Context, dir model.Obj, name string) (model.Obj, error) {
|
||||
newDir := userfile.NewFormattedPath(d.GetCurrentOpDir(dir, []string{name}, 0)).GetPath()
|
||||
_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Create(ctx, &pubUserFile.File{
|
||||
Path: newDir,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (d *HalalCloud) move(ctx context.Context, obj model.Obj, dir model.Obj) (model.Obj, error) {
|
||||
oldDir := userfile.NewFormattedPath(d.GetCurrentDir(obj)).GetPath()
|
||||
newDir := userfile.NewFormattedPath(d.GetCurrentDir(dir)).GetPath()
|
||||
_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Move(ctx, &pubUserFile.BatchOperationRequest{
|
||||
Source: []*pubUserFile.File{
|
||||
{
|
||||
Identity: obj.GetID(),
|
||||
Path: oldDir,
|
||||
},
|
||||
},
|
||||
Dest: &pubUserFile.File{
|
||||
Identity: dir.GetID(),
|
||||
Path: newDir,
|
||||
},
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (d *HalalCloud) rename(ctx context.Context, obj model.Obj, name string) (model.Obj, error) {
|
||||
id := obj.GetID()
|
||||
newPath := userfile.NewFormattedPath(d.GetCurrentOpDir(obj, []string{name}, 0)).GetPath()
|
||||
|
||||
_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Rename(ctx, &pubUserFile.File{
|
||||
Path: newPath,
|
||||
Identity: id,
|
||||
Name: name,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (d *HalalCloud) copy(ctx context.Context, obj model.Obj, dir model.Obj) (model.Obj, error) {
|
||||
id := obj.GetID()
|
||||
sourcePath := userfile.NewFormattedPath(d.GetCurrentDir(obj)).GetPath()
|
||||
if len(id) > 0 {
|
||||
sourcePath = ""
|
||||
}
|
||||
dest := &pubUserFile.File{
|
||||
Identity: dir.GetID(),
|
||||
Path: userfile.NewFormattedPath(d.GetCurrentDir(dir)).GetPath(),
|
||||
}
|
||||
_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Copy(ctx, &pubUserFile.BatchOperationRequest{
|
||||
Source: []*pubUserFile.File{
|
||||
{
|
||||
Path: sourcePath,
|
||||
Identity: id,
|
||||
},
|
||||
},
|
||||
Dest: dest,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (d *HalalCloud) remove(ctx context.Context, obj model.Obj) error {
|
||||
id := obj.GetID()
|
||||
newPath := userfile.NewFormattedPath(d.GetCurrentDir(obj)).GetPath()
|
||||
//if len(id) > 0 {
|
||||
// newPath = ""
|
||||
//}
|
||||
_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Delete(ctx, &pubUserFile.BatchOperationRequest{
|
||||
Source: []*pubUserFile.File{
|
||||
{
|
||||
Path: newPath,
|
||||
Identity: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *HalalCloud) put(ctx context.Context, dstDir model.Obj, fileStream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
|
||||
newDir := path.Join(dstDir.GetPath(), fileStream.GetName())
|
||||
|
||||
result, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).CreateUploadToken(ctx, &pubUserFile.File{
|
||||
Path: newDir,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, _ := url.Parse(result.Endpoint)
|
||||
u.Host = "s3." + u.Host
|
||||
result.Endpoint = u.String()
|
||||
s, err := session.NewSession(&aws.Config{
|
||||
HTTPClient: base.HttpClient,
|
||||
Credentials: credentials.NewStaticCredentials(result.AccessKey, result.SecretKey, result.Token),
|
||||
Region: aws.String(result.Region),
|
||||
Endpoint: aws.String(result.Endpoint),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uploader := s3manager.NewUploader(s, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = d.uploadThread
|
||||
})
|
||||
if fileStream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
||||
uploader.PartSize = fileStream.GetSize() / (s3manager.MaxUploadParts - 1)
|
||||
}
|
||||
_, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{
|
||||
Bucket: aws.String(result.Bucket),
|
||||
Key: aws.String(result.Key),
|
||||
Body: io.TeeReader(fileStream, driver.NewProgress(fileStream.GetSize(), up)),
|
||||
})
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*HalalCloud)(nil)
|
38
drivers/halalcloud/meta.go
Normal file
38
drivers/halalcloud/meta.go
Normal file
@ -0,0 +1,38 @@
|
||||
package halalcloud
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
// Usually one of two
|
||||
driver.RootPath
|
||||
// define other
|
||||
RefreshToken string `json:"refresh_token" required:"true" help:"login type is refresh_token,this is required"`
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1 <= thread <= 32"`
|
||||
|
||||
AppID string `json:"app_id" required:"true" default:"devDebugger/1.0"`
|
||||
AppVersion string `json:"app_version" required:"true" default:"1.0.0"`
|
||||
AppSecret string `json:"app_secret" required:"true" default:"Nkx3Y2xvZ2luLmNu"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "HalalCloud",
|
||||
LocalSort: false,
|
||||
OnlyLocal: true,
|
||||
OnlyProxy: true,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &HalalCloud{}
|
||||
})
|
||||
}
|
52
drivers/halalcloud/options.go
Normal file
52
drivers/halalcloud/options.go
Normal file
@ -0,0 +1,52 @@
|
||||
package halalcloud
|
||||
|
||||
import "google.golang.org/grpc"
|
||||
|
||||
func defaultOptions() halalOptions {
|
||||
return halalOptions{
|
||||
// onRefreshTokenRefreshed: func(string) {},
|
||||
grpcOptions: []grpc.DialOption{
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024 * 1024 * 32)),
|
||||
// grpc.WithMaxMsgSize(1024 * 1024 * 1024),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type HalalOption interface {
|
||||
apply(*halalOptions)
|
||||
}
|
||||
|
||||
// halalOptions configure a RPC call. halalOptions are set by the HalalOption
|
||||
// values passed to Dial.
|
||||
type halalOptions struct {
|
||||
onTokenRefreshed func(accessToken string, accessTokenExpiredAt int64, refreshToken string, refreshTokenExpiredAt int64)
|
||||
grpcOptions []grpc.DialOption
|
||||
}
|
||||
|
||||
// funcDialOption wraps a function that modifies halalOptions into an
|
||||
// implementation of the DialOption interface.
|
||||
type funcDialOption struct {
|
||||
f func(*halalOptions)
|
||||
}
|
||||
|
||||
func (fdo *funcDialOption) apply(do *halalOptions) {
|
||||
fdo.f(do)
|
||||
}
|
||||
|
||||
func newFuncDialOption(f func(*halalOptions)) *funcDialOption {
|
||||
return &funcDialOption{
|
||||
f: f,
|
||||
}
|
||||
}
|
||||
|
||||
func WithRefreshTokenRefreshedCallback(s func(accessToken string, accessTokenExpiredAt int64, refreshToken string, refreshTokenExpiredAt int64)) HalalOption {
|
||||
return newFuncDialOption(func(o *halalOptions) {
|
||||
o.onTokenRefreshed = s
|
||||
})
|
||||
}
|
||||
|
||||
func WithGrpcDialOptions(opts ...grpc.DialOption) HalalOption {
|
||||
return newFuncDialOption(func(o *halalOptions) {
|
||||
o.grpcOptions = opts
|
||||
})
|
||||
}
|
101
drivers/halalcloud/types.go
Normal file
101
drivers/halalcloud/types.go
Normal file
@ -0,0 +1,101 @@
|
||||
package halalcloud
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/city404/v6-public-rpc-proto/go/v6/common"
|
||||
pubUserFile "github.com/city404/v6-public-rpc-proto/go/v6/userfile"
|
||||
"google.golang.org/grpc"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
appID string
|
||||
appVersion string
|
||||
appSecret string
|
||||
grpcConnection *grpc.ClientConn
|
||||
dopts halalOptions
|
||||
tr *TokenResp
|
||||
}
|
||||
|
||||
type TokenResp struct {
|
||||
AccessToken string `json:"accessToken,omitempty"`
|
||||
AccessTokenExpiredAt int64 `json:"accessTokenExpiredAt,omitempty"`
|
||||
RefreshToken string `json:"refreshToken,omitempty"`
|
||||
RefreshTokenExpiredAt int64 `json:"refreshTokenExpiredAt,omitempty"`
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
Identity string `json:"identity,omitempty"`
|
||||
UpdateTs int64 `json:"updateTs,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
CreateTs int64 `json:"createTs,omitempty"`
|
||||
}
|
||||
|
||||
type OrderByInfo struct {
|
||||
Field string `json:"field,omitempty"`
|
||||
Asc bool `json:"asc,omitempty"`
|
||||
}
|
||||
|
||||
type ListInfo struct {
|
||||
Token string `json:"token,omitempty"`
|
||||
Limit int64 `json:"limit,omitempty"`
|
||||
OrderBy []*OrderByInfo `json:"order_by,omitempty"`
|
||||
Version int32 `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type FilesList struct {
|
||||
Files []*Files `json:"files,omitempty"`
|
||||
ListInfo *common.ScanListRequest `json:"list_info,omitempty"`
|
||||
}
|
||||
|
||||
var _ model.Obj = (*Files)(nil)
|
||||
|
||||
type Files pubUserFile.File
|
||||
|
||||
func (f *Files) GetSize() int64 {
|
||||
return f.Size
|
||||
}
|
||||
|
||||
func (f *Files) GetName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f *Files) ModTime() time.Time {
|
||||
return time.UnixMilli(f.UpdateTs)
|
||||
}
|
||||
|
||||
func (f *Files) CreateTime() time.Time {
|
||||
return time.UnixMilli(f.UpdateTs)
|
||||
}
|
||||
|
||||
func (f *Files) IsDir() bool {
|
||||
return f.Dir
|
||||
}
|
||||
|
||||
func (f *Files) GetHash() utils.HashInfo {
|
||||
return utils.HashInfo{}
|
||||
}
|
||||
|
||||
func (f *Files) GetID() string {
|
||||
if len(f.Identity) == 0 {
|
||||
f.Identity = "/"
|
||||
}
|
||||
return f.Identity
|
||||
}
|
||||
|
||||
func (f *Files) GetPath() string {
|
||||
return f.Path
|
||||
}
|
||||
|
||||
type SteamFile struct {
|
||||
file model.File
|
||||
}
|
||||
|
||||
func (s *SteamFile) Read(p []byte) (n int, err error) {
|
||||
return s.file.Read(p)
|
||||
}
|
||||
|
||||
func (s *SteamFile) Close() error {
|
||||
return s.file.Close()
|
||||
}
|
385
drivers/halalcloud/util.go
Normal file
385
drivers/halalcloud/util.go
Normal file
@ -0,0 +1,385 @@
|
||||
package halalcloud
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
pbPublicUser "github.com/city404/v6-public-rpc-proto/go/v6/user"
|
||||
pubUserFile "github.com/city404/v6-public-rpc-proto/go/v6/userfile"
|
||||
"github.com/google/uuid"
|
||||
"github.com/ipfs/go-cid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"hash"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
AppID = "devDebugger/1.0"
|
||||
AppVersion = "1.0.0"
|
||||
AppSecret = "Nkx3Y2xvZ2luLmNu"
|
||||
)
|
||||
|
||||
const (
|
||||
grpcServer = "grpcuserapi.2dland.cn:443"
|
||||
grpcServerAuth = "grpcuserapi.2dland.cn"
|
||||
)
|
||||
|
||||
func (d *HalalCloud) NewAuthServiceWithOauth(options ...HalalOption) (*AuthService, error) {
|
||||
|
||||
aService := &AuthService{}
|
||||
err2 := errors.New("")
|
||||
|
||||
svc := d.HalalCommon.AuthService
|
||||
for _, opt := range options {
|
||||
opt.apply(&svc.dopts)
|
||||
}
|
||||
|
||||
grpcOptions := svc.dopts.grpcOptions
|
||||
grpcOptions = append(grpcOptions, grpc.WithAuthority(grpcServerAuth), grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
ctxx := svc.signContext(method, ctx)
|
||||
err := invoker(ctxx, method, req, reply, cc, opts...) // invoking RPC method
|
||||
return err
|
||||
}))
|
||||
|
||||
grpcConnection, err := grpc.NewClient(grpcServer, grpcOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer grpcConnection.Close()
|
||||
userClient := pbPublicUser.NewPubUserClient(grpcConnection)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
stateString := uuid.New().String()
|
||||
// queryValues.Add("callback", oauthToken.Callback)
|
||||
oauthToken, err := userClient.CreateAuthToken(ctx, &pbPublicUser.LoginRequest{
|
||||
ReturnType: 2,
|
||||
State: stateString,
|
||||
ReturnUrl: "",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(oauthToken.State) < 1 {
|
||||
oauthToken.State = stateString
|
||||
}
|
||||
|
||||
if oauthToken.Url != "" {
|
||||
|
||||
return nil, fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, oauthToken.Url)
|
||||
}
|
||||
|
||||
return aService, err2
|
||||
|
||||
}
|
||||
|
||||
func (d *HalalCloud) NewAuthService(refreshToken string, options ...HalalOption) (*AuthService, error) {
|
||||
svc := d.HalalCommon.AuthService
|
||||
|
||||
if len(refreshToken) < 1 {
|
||||
refreshToken = d.Addition.RefreshToken
|
||||
}
|
||||
|
||||
if len(d.tr.AccessToken) > 0 {
|
||||
accessTokenExpiredAt := d.tr.AccessTokenExpiredAt
|
||||
current := time.Now().UnixMilli()
|
||||
if accessTokenExpiredAt < current {
|
||||
// access token expired
|
||||
d.tr.AccessToken = ""
|
||||
d.tr.AccessTokenExpiredAt = 0
|
||||
} else {
|
||||
svc.tr.AccessTokenExpiredAt = accessTokenExpiredAt
|
||||
svc.tr.AccessToken = d.tr.AccessToken
|
||||
}
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
opt.apply(&svc.dopts)
|
||||
}
|
||||
|
||||
grpcOptions := svc.dopts.grpcOptions
|
||||
grpcOptions = append(grpcOptions, grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(10*1024*1024), grpc.MaxCallRecvMsgSize(10*1024*1024)), grpc.WithAuthority(grpcServerAuth), grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
ctxx := svc.signContext(method, ctx)
|
||||
err := invoker(ctxx, method, req, reply, cc, opts...) // invoking RPC method
|
||||
if err != nil {
|
||||
grpcStatus, ok := status.FromError(err)
|
||||
|
||||
if ok && grpcStatus.Code() == codes.Unauthenticated && strings.Contains(grpcStatus.Err().Error(), "invalid accesstoken") && len(refreshToken) > 0 {
|
||||
// refresh token
|
||||
refreshResponse, err := pbPublicUser.NewPubUserClient(cc).Refresh(ctx, &pbPublicUser.Token{
|
||||
RefreshToken: refreshToken,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(refreshResponse.AccessToken) > 0 {
|
||||
svc.tr.AccessToken = refreshResponse.AccessToken
|
||||
svc.tr.AccessTokenExpiredAt = refreshResponse.AccessTokenExpireTs
|
||||
svc.OnAccessTokenRefreshed(refreshResponse.AccessToken, refreshResponse.AccessTokenExpireTs, refreshResponse.RefreshToken, refreshResponse.RefreshTokenExpireTs)
|
||||
}
|
||||
// retry
|
||||
ctxx := svc.signContext(method, ctx)
|
||||
err = invoker(ctxx, method, req, reply, cc, opts...) // invoking RPC method
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}))
|
||||
grpcConnection, err := grpc.NewClient(grpcServer, grpcOptions...)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc.grpcConnection = grpcConnection
|
||||
return svc, err
|
||||
}
|
||||
|
||||
func (s *AuthService) OnAccessTokenRefreshed(accessToken string, accessTokenExpiredAt int64, refreshToken string, refreshTokenExpiredAt int64) {
|
||||
s.tr.AccessToken = accessToken
|
||||
s.tr.AccessTokenExpiredAt = accessTokenExpiredAt
|
||||
s.tr.RefreshToken = refreshToken
|
||||
s.tr.RefreshTokenExpiredAt = refreshTokenExpiredAt
|
||||
|
||||
if s.dopts.onTokenRefreshed != nil {
|
||||
s.dopts.onTokenRefreshed(accessToken, accessTokenExpiredAt, refreshToken, refreshTokenExpiredAt)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *AuthService) GetGrpcConnection() *grpc.ClientConn {
|
||||
return s.grpcConnection
|
||||
}
|
||||
|
||||
func (s *AuthService) Close() {
|
||||
_ = s.grpcConnection.Close()
|
||||
}
|
||||
|
||||
func (s *AuthService) signContext(method string, ctx context.Context) context.Context {
|
||||
var kvString []string
|
||||
currentTimeStamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
bufferedString := bytes.NewBufferString(method)
|
||||
kvString = append(kvString, "timestamp", currentTimeStamp)
|
||||
bufferedString.WriteString(currentTimeStamp)
|
||||
kvString = append(kvString, "appid", AppID)
|
||||
bufferedString.WriteString(AppID)
|
||||
kvString = append(kvString, "appversion", AppVersion)
|
||||
bufferedString.WriteString(AppVersion)
|
||||
if s.tr != nil && len(s.tr.AccessToken) > 0 {
|
||||
authorization := "Bearer " + s.tr.AccessToken
|
||||
kvString = append(kvString, "authorization", authorization)
|
||||
bufferedString.WriteString(authorization)
|
||||
}
|
||||
bufferedString.WriteString(AppSecret)
|
||||
sign := GetMD5Hash(bufferedString.String())
|
||||
kvString = append(kvString, "sign", sign)
|
||||
return metadata.AppendToOutgoingContext(ctx, kvString...)
|
||||
}
|
||||
|
||||
func (d *HalalCloud) GetCurrentOpDir(dir model.Obj, args []string, index int) string {
|
||||
currentDir := dir.GetPath()
|
||||
if len(currentDir) == 0 {
|
||||
currentDir = "/"
|
||||
}
|
||||
opPath := currentDir + "/" + args[index]
|
||||
if strings.HasPrefix(args[index], "/") {
|
||||
opPath = args[index]
|
||||
}
|
||||
return opPath
|
||||
}
|
||||
|
||||
func (d *HalalCloud) GetCurrentDir(dir model.Obj) string {
|
||||
currentDir := dir.GetPath()
|
||||
if len(currentDir) == 0 {
|
||||
currentDir = "/"
|
||||
}
|
||||
return currentDir
|
||||
}
|
||||
|
||||
type Common struct {
|
||||
}
|
||||
|
||||
func getRawFiles(addr *pubUserFile.SliceDownloadInfo) ([]byte, error) {
|
||||
|
||||
if addr == nil {
|
||||
return nil, errors.New("addr is nil")
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(60 * time.Second), // Set timeout to 5 seconds
|
||||
}
|
||||
resp, err := client.Get(addr.DownloadAddress)
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("bad status: %s, body: %s", resp.Status, body)
|
||||
}
|
||||
|
||||
if addr.Encrypt > 0 {
|
||||
cd := uint8(addr.Encrypt)
|
||||
for idx := 0; idx < len(body); idx++ {
|
||||
body[idx] = body[idx] ^ cd
|
||||
}
|
||||
}
|
||||
|
||||
if addr.StoreType != 10 {
|
||||
|
||||
sourceCid, err := cid.Decode(addr.Identity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checkCid, err := sourceCid.Prefix().Sum(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !checkCid.Equals(sourceCid) {
|
||||
return nil, fmt.Errorf("bad cid: %s, body: %s", checkCid.String(), body)
|
||||
}
|
||||
}
|
||||
|
||||
return body, nil
|
||||
|
||||
}
|
||||
|
||||
type openObject struct {
|
||||
ctx context.Context
|
||||
mu sync.Mutex
|
||||
d []*pubUserFile.SliceDownloadInfo
|
||||
id int
|
||||
skip int64
|
||||
chunk *[]byte
|
||||
chunks *[]chunkSize
|
||||
closed bool
|
||||
sha string
|
||||
shaTemp hash.Hash
|
||||
}
|
||||
|
||||
// get the next chunk
|
||||
func (oo *openObject) getChunk(ctx context.Context) (err error) {
|
||||
if oo.id >= len(*oo.chunks) {
|
||||
return io.EOF
|
||||
}
|
||||
var chunk []byte
|
||||
err = utils.Retry(3, time.Second, func() (err error) {
|
||||
chunk, err = getRawFiles(oo.d[oo.id])
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oo.id++
|
||||
oo.chunk = &chunk
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read reads up to len(p) bytes into p.
|
||||
func (oo *openObject) Read(p []byte) (n int, err error) {
|
||||
oo.mu.Lock()
|
||||
defer oo.mu.Unlock()
|
||||
if oo.closed {
|
||||
return 0, fmt.Errorf("read on closed file")
|
||||
}
|
||||
// Skip data at the start if requested
|
||||
for oo.skip > 0 {
|
||||
//size := 1024 * 1024
|
||||
_, size, err := oo.ChunkLocation(oo.id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if oo.skip < int64(size) {
|
||||
break
|
||||
}
|
||||
oo.id++
|
||||
oo.skip -= int64(size)
|
||||
}
|
||||
if len(*oo.chunk) == 0 {
|
||||
err = oo.getChunk(oo.ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if oo.skip > 0 {
|
||||
*oo.chunk = (*oo.chunk)[oo.skip:]
|
||||
oo.skip = 0
|
||||
}
|
||||
}
|
||||
n = copy(p, *oo.chunk)
|
||||
*oo.chunk = (*oo.chunk)[n:]
|
||||
|
||||
oo.shaTemp.Write(*oo.chunk)
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Close closed the file - MAC errors are reported here
|
||||
func (oo *openObject) Close() (err error) {
|
||||
oo.mu.Lock()
|
||||
defer oo.mu.Unlock()
|
||||
if oo.closed {
|
||||
return nil
|
||||
}
|
||||
// 校验Sha1
|
||||
if string(oo.shaTemp.Sum(nil)) != oo.sha {
|
||||
return fmt.Errorf("failed to finish download: %w", err)
|
||||
}
|
||||
|
||||
oo.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetMD5Hash(text string) string {
|
||||
tHash := md5.Sum([]byte(text))
|
||||
return hex.EncodeToString(tHash[:])
|
||||
}
|
||||
|
||||
// chunkSize describes a size and position of chunk
|
||||
type chunkSize struct {
|
||||
position int64
|
||||
size int
|
||||
}
|
||||
|
||||
func getChunkSizes(sliceSize []*pubUserFile.SliceSize) (chunks []chunkSize) {
|
||||
chunks = make([]chunkSize, 0)
|
||||
for _, s := range sliceSize {
|
||||
// 对最后一个做特殊处理
|
||||
if s.EndIndex == 0 {
|
||||
s.EndIndex = s.StartIndex
|
||||
}
|
||||
for j := s.StartIndex; j <= s.EndIndex; j++ {
|
||||
chunks = append(chunks, chunkSize{position: j, size: int(s.Size)})
|
||||
}
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func (oo *openObject) ChunkLocation(id int) (position int64, size int, err error) {
|
||||
if id < 0 || id >= len(*oo.chunks) {
|
||||
return 0, 0, errors.New("invalid arguments")
|
||||
}
|
||||
|
||||
return (*oo.chunks)[id].position, (*oo.chunks)[id].size, nil
|
||||
}
|
@ -66,18 +66,18 @@ func (d *ILanZou) Drop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
offset := 1
|
||||
limit := 60
|
||||
var res []ListItem
|
||||
for {
|
||||
var resp ListResp
|
||||
_, err := d.proved("/record/file/list", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"type": "0",
|
||||
"folderId": dir.GetID(),
|
||||
"offset": strconv.Itoa(offset),
|
||||
"limit": strconv.Itoa(limit),
|
||||
}).SetResult(&resp)
|
||||
params := []string{
|
||||
"offset=1",
|
||||
"limit=60",
|
||||
"folderId=" + dir.GetID(),
|
||||
"type=0",
|
||||
}
|
||||
queryString := strings.Join(params, "&")
|
||||
req.SetQueryString(queryString).SetResult(&resp)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -86,7 +86,6 @@ func (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
if resp.TotalPage <= resp.Offset {
|
||||
break
|
||||
}
|
||||
offset++
|
||||
}
|
||||
return utils.SliceConvert(res, func(f ListItem) (model.Obj, error) {
|
||||
updTime, err := time.ParseInLocation("2006-01-02 15:04:05", f.UpdTime, time.Local)
|
||||
@ -118,36 +117,39 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := u.Query()
|
||||
query.Set("uuid", d.UUID)
|
||||
query.Set("devType", "6")
|
||||
query.Set("devCode", d.UUID)
|
||||
query.Set("devModel", "chrome")
|
||||
query.Set("devVersion", d.conf.devVersion)
|
||||
query.Set("appVersion", "")
|
||||
ts, err := getTimestamp(d.conf.secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
ts, ts_str, err := getTimestamp(d.conf.secret)
|
||||
|
||||
params := []string{
|
||||
"uuid=" + url.QueryEscape(d.UUID),
|
||||
"devType=6",
|
||||
"devCode=" + url.QueryEscape(d.UUID),
|
||||
"devModel=chrome",
|
||||
"devVersion=" + url.QueryEscape(d.conf.devVersion),
|
||||
"appVersion=",
|
||||
"timestamp=" + ts_str,
|
||||
"appToken=" + url.QueryEscape(d.Token),
|
||||
"enable=0",
|
||||
}
|
||||
query.Set("timestamp", ts)
|
||||
query.Set("appToken", d.Token)
|
||||
query.Set("enable", "1")
|
||||
|
||||
downloadId, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%s", file.GetID(), d.userID)), d.conf.secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query.Set("downloadId", hex.EncodeToString(downloadId))
|
||||
auth, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%d", file.GetID(), time.Now().UnixMilli())), d.conf.secret)
|
||||
params = append(params, "downloadId="+url.QueryEscape(hex.EncodeToString(downloadId)))
|
||||
|
||||
auth, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%d", file.GetID(), ts)), d.conf.secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query.Set("auth", hex.EncodeToString(auth))
|
||||
u.RawQuery = query.Encode()
|
||||
params = append(params, "auth="+url.QueryEscape(hex.EncodeToString(auth)))
|
||||
|
||||
u.RawQuery = strings.Join(params, "&")
|
||||
realURL := u.String()
|
||||
// get the url after redirect
|
||||
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
|
||||
//"Origin": d.conf.site,
|
||||
"Referer": d.conf.site + "/",
|
||||
"Referer": d.conf.site + "/",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
|
||||
}).Get(realURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -155,7 +157,7 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
if res.StatusCode() == 302 {
|
||||
realURL = res.Header().Get("location")
|
||||
} else {
|
||||
return nil, fmt.Errorf("redirect failed, status: %d", res.StatusCode())
|
||||
return nil, fmt.Errorf("redirect failed, status: %d, msg: %s", res.StatusCode(), utils.Json.Get(res.Body(), "msg").ToString())
|
||||
}
|
||||
link := model.Link{URL: realURL}
|
||||
return &link, nil
|
||||
@ -173,7 +175,7 @@ func (d *ILanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
|
||||
return nil, err
|
||||
}
|
||||
return &model.Object{
|
||||
ID: utils.Json.Get(res, "list", "0", "id").ToString(),
|
||||
ID: utils.Json.Get(res, "list", 0, "id").ToString(),
|
||||
//Path: "",
|
||||
Name: dirName,
|
||||
Size: 0,
|
||||
@ -342,10 +344,12 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
||||
var resp UploadResultResp
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err = d.unproved("/7n/results", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"tokenList": token,
|
||||
"tokenTime": time.Now().Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)"),
|
||||
}).SetResult(&resp)
|
||||
params := []string{
|
||||
"tokenList=" + token,
|
||||
"tokenTime=" + time.Now().Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)"),
|
||||
}
|
||||
queryString := strings.Join(params, "&")
|
||||
req.SetQueryString(queryString).SetResult(&resp)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -46,7 +46,7 @@ func init() {
|
||||
bucket: "wpanstore-lanzou",
|
||||
unproved: "unproved",
|
||||
proved: "proved",
|
||||
devVersion: "122",
|
||||
devVersion: "125",
|
||||
site: "https://www.ilanzou.com",
|
||||
},
|
||||
}
|
||||
@ -72,7 +72,7 @@ func init() {
|
||||
bucket: "wpanstore",
|
||||
unproved: "ws",
|
||||
proved: "app",
|
||||
devVersion: "121",
|
||||
devVersion: "125",
|
||||
site: "https://www.feijipan.com",
|
||||
},
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
@ -31,44 +33,52 @@ func (d *ILanZou) login() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTimestamp(secret []byte) (string, error) {
|
||||
func getTimestamp(secret []byte) (int64, string, error) {
|
||||
ts := time.Now().UnixMilli()
|
||||
tsStr := strconv.FormatInt(ts, 10)
|
||||
res, err := mopan.AesEncrypt([]byte(tsStr), secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return 0, "", err
|
||||
}
|
||||
return hex.EncodeToString(res), nil
|
||||
return ts, hex.EncodeToString(res), nil
|
||||
}
|
||||
|
||||
func (d *ILanZou) request(pathname, method string, callback base.ReqCallback, proved bool, retry ...bool) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
ts, err := getTimestamp(d.conf.secret)
|
||||
_, ts_str, err := getTimestamp(d.conf.secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetQueryParams(map[string]string{
|
||||
"uuid": d.UUID,
|
||||
"devType": "6",
|
||||
"devCode": d.UUID,
|
||||
"devModel": "chrome",
|
||||
"devVersion": d.conf.devVersion,
|
||||
"appVersion": "",
|
||||
"timestamp": ts,
|
||||
//"appToken": d.Token,
|
||||
"extra": "2",
|
||||
})
|
||||
req.SetHeaders(map[string]string{
|
||||
"Origin": d.conf.site,
|
||||
"Referer": d.conf.site + "/",
|
||||
})
|
||||
if proved {
|
||||
req.SetQueryParam("appToken", d.Token)
|
||||
|
||||
params := []string{
|
||||
"uuid=" + url.QueryEscape(d.UUID),
|
||||
"devType=6",
|
||||
"devCode=" + url.QueryEscape(d.UUID),
|
||||
"devModel=chrome",
|
||||
"devVersion=" + url.QueryEscape(d.conf.devVersion),
|
||||
"appVersion=",
|
||||
"timestamp=" + ts_str,
|
||||
}
|
||||
|
||||
if proved {
|
||||
params = append(params, "appToken="+url.QueryEscape(d.Token))
|
||||
}
|
||||
|
||||
params = append(params, "extra=2")
|
||||
|
||||
queryString := strings.Join(params, "&")
|
||||
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
"Origin": d.conf.site,
|
||||
"Referer": d.conf.site + "/",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
|
||||
})
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
res, err := req.Execute(method, d.conf.base+pathname)
|
||||
|
||||
res, err := req.Execute(method, d.conf.base+pathname+"?"+queryString)
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
log.Errorf("[iLanZou] request error: %s", res.String())
|
||||
|
8
drivers/lark.go
Normal file
8
drivers/lark.go
Normal file
@ -0,0 +1,8 @@
|
||||
// +build linux darwin windows
|
||||
// +build amd64 arm64
|
||||
|
||||
package drivers
|
||||
|
||||
import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/lark"
|
||||
)
|
397
drivers/lark/driver.go
Normal file
397
drivers/lark/driver.go
Normal file
@ -0,0 +1,397 @@
|
||||
package lark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
lark "github.com/larksuite/oapi-sdk-go/v3"
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type Lark struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
client *lark.Client
|
||||
rootFolderToken string
|
||||
}
|
||||
|
||||
func (c *Lark) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (c *Lark) GetAddition() driver.Additional {
|
||||
return &c.Addition
|
||||
}
|
||||
|
||||
func (c *Lark) Init(ctx context.Context) error {
|
||||
c.client = lark.NewClient(c.AppId, c.AppSecret, lark.WithTokenCache(newTokenCache()))
|
||||
|
||||
paths := strings.Split(c.RootFolderPath, "/")
|
||||
token := ""
|
||||
|
||||
var ok bool
|
||||
var file *larkdrive.File
|
||||
for _, p := range paths {
|
||||
if p == "" {
|
||||
token = ""
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
ok, file, err = resp.Next()
|
||||
if !ok {
|
||||
return errs.ObjectNotFound
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *file.Type == "folder" && *file.Name == p {
|
||||
token = *file.Token
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.rootFolderToken = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Lark) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Lark) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
token, ok := c.getObjToken(ctx, dir.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
if token == emptyFolderToken {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ok = false
|
||||
var file *larkdrive.File
|
||||
var res []model.Obj
|
||||
|
||||
for {
|
||||
ok, file, err = resp.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modifiedUnix, _ := strconv.ParseInt(*file.ModifiedTime, 10, 64)
|
||||
createdUnix, _ := strconv.ParseInt(*file.CreatedTime, 10, 64)
|
||||
|
||||
f := model.Object{
|
||||
ID: *file.Token,
|
||||
Path: strings.Join([]string{c.RootFolderPath, dir.GetPath(), *file.Name}, "/"),
|
||||
Name: *file.Name,
|
||||
Size: 0,
|
||||
Modified: time.Unix(modifiedUnix, 0),
|
||||
Ctime: time.Unix(createdUnix, 0),
|
||||
IsFolder: *file.Type == "folder",
|
||||
}
|
||||
res = append(res, &f)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Lark) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
token, ok := c.getObjToken(ctx, file.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
resp, err := c.client.GetTenantAccessTokenBySelfBuiltApp(ctx, &larkcore.SelfBuiltTenantAccessTokenReq{
|
||||
AppID: c.AppId,
|
||||
AppSecret: c.AppSecret,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !c.ExternalMode {
|
||||
accessToken := resp.TenantAccessToken
|
||||
|
||||
url := fmt.Sprintf("https://open.feishu.cn/open-apis/drive/v1/files/%s/download", token)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||
req.Header.Set("Range", "bytes=0-1")
|
||||
|
||||
ar, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ar.StatusCode != http.StatusPartialContent {
|
||||
return nil, errors.New("failed to get download link")
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: url,
|
||||
Header: http.Header{
|
||||
"Authorization": []string{fmt.Sprintf("Bearer %s", accessToken)},
|
||||
},
|
||||
}, nil
|
||||
} else {
|
||||
url := strings.Join([]string{c.TenantUrlPrefix, "file", token}, "/")
|
||||
|
||||
return &model.Link{
|
||||
URL: url,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Lark) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
token, ok := c.getObjToken(ctx, parentDir.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
body, err := larkdrive.NewCreateFolderFilePathReqBodyBuilder().FolderToken(token).Name(dirName).Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.Drive.File.CreateFolder(ctx,
|
||||
larkdrive.NewCreateFolderFileReqBuilder().Body(body).Build())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return nil, errors.New(resp.Error())
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: *resp.Data.Token,
|
||||
Path: strings.Join([]string{c.RootFolderPath, parentDir.GetPath(), dirName}, "/"),
|
||||
Name: dirName,
|
||||
Size: 0,
|
||||
IsFolder: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Lark) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
srcToken, ok := c.getObjToken(ctx, srcObj.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
req := larkdrive.NewMoveFileReqBuilder().
|
||||
Body(larkdrive.NewMoveFileReqBodyBuilder().
|
||||
Type("file").
|
||||
FolderToken(dstDirToken).
|
||||
Build()).FileToken(srcToken).
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
resp, err := c.client.Drive.File.Move(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return nil, errors.New(resp.Error())
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Lark) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
// TODO rename obj, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (c *Lark) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
srcToken, ok := c.getObjToken(ctx, srcObj.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
req := larkdrive.NewCopyFileReqBuilder().
|
||||
Body(larkdrive.NewCopyFileReqBodyBuilder().
|
||||
Name(srcObj.GetName()).
|
||||
Type("file").
|
||||
FolderToken(dstDirToken).
|
||||
Build()).FileToken(srcToken).
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
resp, err := c.client.Drive.File.Copy(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return nil, errors.New(resp.Error())
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Lark) Remove(ctx context.Context, obj model.Obj) error {
|
||||
token, ok := c.getObjToken(ctx, obj.GetPath())
|
||||
if !ok {
|
||||
return errs.ObjectNotFound
|
||||
}
|
||||
|
||||
req := larkdrive.NewDeleteFileReqBuilder().
|
||||
FileToken(token).
|
||||
Type("file").
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
resp, err := c.client.Drive.File.Delete(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return errors.New(resp.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var uploadLimit = rate.NewLimiter(rate.Every(time.Second), 5)
|
||||
|
||||
func (c *Lark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
token, ok := c.getObjToken(ctx, dstDir.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
// prepare
|
||||
req := larkdrive.NewUploadPrepareFileReqBuilder().
|
||||
FileUploadInfo(larkdrive.NewFileUploadInfoBuilder().
|
||||
FileName(stream.GetName()).
|
||||
ParentType(`explorer`).
|
||||
ParentNode(token).
|
||||
Size(int(stream.GetSize())).
|
||||
Build()).
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
uploadLimit.Wait(ctx)
|
||||
resp, err := c.client.Drive.File.UploadPrepare(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return nil, errors.New(resp.Error())
|
||||
}
|
||||
|
||||
uploadId := *resp.Data.UploadId
|
||||
blockSize := *resp.Data.BlockSize
|
||||
blockCount := *resp.Data.BlockNum
|
||||
|
||||
// upload
|
||||
for i := 0; i < blockCount; i++ {
|
||||
length := int64(blockSize)
|
||||
if i == blockCount-1 {
|
||||
length = stream.GetSize() - int64(i*blockSize)
|
||||
}
|
||||
|
||||
reader := io.LimitReader(stream, length)
|
||||
|
||||
req := larkdrive.NewUploadPartFileReqBuilder().
|
||||
Body(larkdrive.NewUploadPartFileReqBodyBuilder().
|
||||
UploadId(uploadId).
|
||||
Seq(i).
|
||||
Size(int(length)).
|
||||
File(reader).
|
||||
Build()).
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
uploadLimit.Wait(ctx)
|
||||
resp, err := c.client.Drive.File.UploadPart(ctx, req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return nil, errors.New(resp.Error())
|
||||
}
|
||||
|
||||
up(float64(i) / float64(blockCount))
|
||||
}
|
||||
|
||||
//close
|
||||
closeReq := larkdrive.NewUploadFinishFileReqBuilder().
|
||||
Body(larkdrive.NewUploadFinishFileReqBodyBuilder().
|
||||
UploadId(uploadId).
|
||||
BlockNum(blockCount).
|
||||
Build()).
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
closeResp, err := c.client.Drive.File.UploadFinish(ctx, closeReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !closeResp.Success() {
|
||||
return nil, errors.New(closeResp.Error())
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: *closeResp.Data.FileToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
//func (d *Lark) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
var _ driver.Driver = (*Lark)(nil)
|
36
drivers/lark/meta.go
Normal file
36
drivers/lark/meta.go
Normal file
@ -0,0 +1,36 @@
|
||||
package lark
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
// Usually one of two
|
||||
driver.RootPath
|
||||
// define other
|
||||
AppId string `json:"app_id" type:"text" help:"app id"`
|
||||
AppSecret string `json:"app_secret" type:"text" help:"app secret"`
|
||||
ExternalMode bool `json:"external_mode" type:"bool" help:"external mode"`
|
||||
TenantUrlPrefix string `json:"tenant_url_prefix" type:"text" help:"tenant url prefix"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Lark",
|
||||
LocalSort: false,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Lark{}
|
||||
})
|
||||
}
|
32
drivers/lark/types.go
Normal file
32
drivers/lark/types.go
Normal file
@ -0,0 +1,32 @@
|
||||
package lark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Xhofe/go-cache"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TokenCache struct {
|
||||
cache.ICache[string]
|
||||
}
|
||||
|
||||
func (t *TokenCache) Set(_ context.Context, key string, value string, expireTime time.Duration) error {
|
||||
t.ICache.Set(key, value, cache.WithEx[string](expireTime))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TokenCache) Get(_ context.Context, key string) (string, error) {
|
||||
v, ok := t.ICache.Get(key)
|
||||
if ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func newTokenCache() *TokenCache {
|
||||
c := cache.NewMemCache[string]()
|
||||
|
||||
return &TokenCache{c}
|
||||
}
|
66
drivers/lark/util.go
Normal file
66
drivers/lark/util.go
Normal file
@ -0,0 +1,66 @@
|
||||
package lark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Xhofe/go-cache"
|
||||
larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
const objTokenCacheDuration = 5 * time.Minute
|
||||
const emptyFolderToken = "empty"
|
||||
|
||||
var objTokenCache = cache.NewMemCache[string]()
|
||||
var exOpts = cache.WithEx[string](objTokenCacheDuration)
|
||||
|
||||
func (c *Lark) getObjToken(ctx context.Context, folderPath string) (string, bool) {
|
||||
if token, ok := objTokenCache.Get(folderPath); ok {
|
||||
return token, true
|
||||
}
|
||||
|
||||
dir, name := path.Split(folderPath)
|
||||
// strip the last slash of dir if it exists
|
||||
if len(dir) > 0 && dir[len(dir)-1] == '/' {
|
||||
dir = dir[:len(dir)-1]
|
||||
}
|
||||
if name == "" {
|
||||
return c.rootFolderToken, true
|
||||
}
|
||||
|
||||
var parentToken string
|
||||
var found bool
|
||||
parentToken, found = c.getObjToken(ctx, dir)
|
||||
if !found {
|
||||
return emptyFolderToken, false
|
||||
}
|
||||
|
||||
req := larkdrive.NewListFileReqBuilder().FolderToken(parentToken).Build()
|
||||
resp, err := c.client.Drive.File.ListByIterator(ctx, req)
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to list files")
|
||||
return emptyFolderToken, false
|
||||
}
|
||||
|
||||
var file *larkdrive.File
|
||||
for {
|
||||
found, file, err = resp.Next()
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to get next file")
|
||||
break
|
||||
}
|
||||
|
||||
if *file.Name == name {
|
||||
objTokenCache.Set(folderPath, *file.Token, exOpts)
|
||||
return *file.Token, true
|
||||
}
|
||||
}
|
||||
|
||||
return emptyFolderToken, false
|
||||
}
|
@ -21,7 +21,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/sign"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/djherbis/times"
|
||||
"github.com/alist-org/times"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rclone/rclone/lib/readers"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
@ -33,8 +34,16 @@ func (d *Mega) GetAddition() driver.Additional {
|
||||
}
|
||||
|
||||
func (d *Mega) Init(ctx context.Context) error {
|
||||
var twoFACode = d.TwoFACode
|
||||
d.c = mega.New()
|
||||
return d.c.Login(d.Email, d.Password)
|
||||
if d.TwoFASecret != "" {
|
||||
code, err := totp.GenerateCode(d.TwoFASecret, time.Now())
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate totp code failed: %w", err)
|
||||
}
|
||||
twoFACode = code
|
||||
}
|
||||
return d.c.MultiFactorLogin(d.Email, d.Password, twoFACode)
|
||||
}
|
||||
|
||||
func (d *Mega) Drop(ctx context.Context) error {
|
||||
|
@ -9,8 +9,10 @@ type Addition struct {
|
||||
// Usually one of two
|
||||
//driver.RootPath
|
||||
//driver.RootID
|
||||
Email string `json:"email" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
Email string `json:"email" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
TwoFACode string `json:"two_fa_code" required:"false" help:"2FA 6-digit code, filling in the 2FA code alone will not support reloading driver"`
|
||||
TwoFASecret string `json:"two_fa_secret" required:"false" help:"2FA secret"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
135
drivers/netease_music/crypto.go
Normal file
135
drivers/netease_music/crypto.go
Normal file
@ -0,0 +1,135 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||
)
|
||||
|
||||
var (
|
||||
linuxapiKey = []byte("rFgB&h#%2?^eDg:Q")
|
||||
eapiKey = []byte("e82ckenh8dichen8")
|
||||
iv = []byte("0102030405060708")
|
||||
presetKey = []byte("0CoJUm6Qyw8W8jud")
|
||||
publicKey = []byte("-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----")
|
||||
stdChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
)
|
||||
|
||||
func aesKeyPending(key []byte) []byte {
|
||||
k := len(key)
|
||||
count := 0
|
||||
switch true {
|
||||
case k <= 16:
|
||||
count = 16 - k
|
||||
case k <= 24:
|
||||
count = 24 - k
|
||||
case k <= 32:
|
||||
count = 32 - k
|
||||
default:
|
||||
return key[:32]
|
||||
}
|
||||
if count == 0 {
|
||||
return key
|
||||
}
|
||||
|
||||
return append(key, bytes.Repeat([]byte{0}, count)...)
|
||||
}
|
||||
|
||||
func pkcs7Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padtext...)
|
||||
}
|
||||
|
||||
func aesCBCEncrypt(src, key, iv []byte) []byte {
|
||||
block, _ := aes.NewCipher(aesKeyPending(key))
|
||||
src = pkcs7Padding(src, block.BlockSize())
|
||||
dst := make([]byte, len(src))
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(dst, src)
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func aesECBEncrypt(src, key []byte) []byte {
|
||||
block, _ := aes.NewCipher(aesKeyPending(key))
|
||||
|
||||
src = pkcs7Padding(src, block.BlockSize())
|
||||
dst := make([]byte, len(src))
|
||||
|
||||
ecbCryptBlocks(block, dst, src)
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func ecbCryptBlocks(block cipher.Block, dst, src []byte) {
|
||||
bs := block.BlockSize()
|
||||
|
||||
for len(src) > 0 {
|
||||
block.Encrypt(dst, src[:bs])
|
||||
src = src[bs:]
|
||||
dst = dst[bs:]
|
||||
}
|
||||
}
|
||||
|
||||
func rsaEncrypt(buffer, key []byte) []byte {
|
||||
buffers := make([]byte, 128-16, 128)
|
||||
buffers = append(buffers, buffer...)
|
||||
block, _ := pem.Decode(key)
|
||||
pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
pub := pubInterface.(*rsa.PublicKey)
|
||||
c := new(big.Int).SetBytes([]byte(buffers))
|
||||
return c.Exp(c, big.NewInt(int64(pub.E)), pub.N).Bytes()
|
||||
}
|
||||
|
||||
func getSecretKey() ([]byte, []byte) {
|
||||
key := make([]byte, 16)
|
||||
reversed := make([]byte, 16)
|
||||
for i := 0; i < 16; i++ {
|
||||
result := stdChars[random.RangeInt64(0, 62)]
|
||||
key[i] = result
|
||||
reversed[15-i] = result
|
||||
}
|
||||
return key, reversed
|
||||
}
|
||||
|
||||
func weapi(data map[string]string) map[string]string {
|
||||
text, _ := utils.Json.Marshal(data)
|
||||
secretKey, reversedKey := getSecretKey()
|
||||
params := []byte(base64.StdEncoding.EncodeToString(aesCBCEncrypt(text, presetKey, iv)))
|
||||
return map[string]string{
|
||||
"params": base64.StdEncoding.EncodeToString(aesCBCEncrypt(params, reversedKey, iv)),
|
||||
"encSecKey": hex.EncodeToString(rsaEncrypt(secretKey, publicKey)),
|
||||
}
|
||||
}
|
||||
|
||||
func eapi(url string, data map[string]interface{}) map[string]string {
|
||||
text, _ := utils.Json.Marshal(data)
|
||||
msg := "nobody" + url + "use" + string(text) + "md5forencrypt"
|
||||
h := md5.New()
|
||||
h.Write([]byte(msg))
|
||||
digest := hex.EncodeToString(h.Sum(nil))
|
||||
params := []byte(url + "-36cd479b6b5-" + string(text) + "-36cd479b6b5-" + digest)
|
||||
return map[string]string{
|
||||
"params": hex.EncodeToString(aesECBEncrypt(params, eapiKey)),
|
||||
}
|
||||
}
|
||||
|
||||
func linuxapi(data map[string]interface{}) map[string]string {
|
||||
text, _ := utils.Json.Marshal(data)
|
||||
return map[string]string{
|
||||
"eparams": strings.ToUpper(hex.EncodeToString(aesECBEncrypt(text, linuxapiKey))),
|
||||
}
|
||||
}
|
110
drivers/netease_music/driver.go
Normal file
110
drivers/netease_music/driver.go
Normal file
@ -0,0 +1,110 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
type NeteaseMusic struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
csrfToken string
|
||||
musicU string
|
||||
fileMapByName map[string]model.Obj
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Init(ctx context.Context) error {
|
||||
d.csrfToken = d.Addition.getCookie("__csrf")
|
||||
d.musicU = d.Addition.getCookie("MUSIC_U")
|
||||
|
||||
if d.csrfToken == "" || d.musicU == "" {
|
||||
return errs.EmptyToken
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Get(ctx context.Context, path string) (model.Obj, error) {
|
||||
if path == "/" {
|
||||
return &model.Object{
|
||||
IsFolder: true,
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
fragments := strings.Split(path, "/")
|
||||
if len(fragments) > 1 {
|
||||
fileName := fragments[1]
|
||||
if strings.HasSuffix(fileName, ".lrc") {
|
||||
lrc := d.fileMapByName[fileName]
|
||||
return d.getLyricObj(lrc)
|
||||
}
|
||||
if song, ok := d.fileMapByName[fileName]; ok {
|
||||
return song, nil
|
||||
} else {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
return d.getSongObjs(args)
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
if lrc, ok := file.(*LyricObj); ok {
|
||||
if args.Type == "parsed" {
|
||||
return lrc.getLyricLink(), nil
|
||||
} else {
|
||||
return lrc.getProxyLink(args), nil
|
||||
}
|
||||
}
|
||||
|
||||
return d.getSongLink(file)
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Remove(ctx context.Context, obj model.Obj) error {
|
||||
return d.removeSongObj(obj)
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
return d.putSongStream(stream)
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*NeteaseMusic)(nil)
|
32
drivers/netease_music/meta.go
Normal file
32
drivers/netease_music/meta.go
Normal file
@ -0,0 +1,32 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
Cookie string `json:"cookie" type:"text" required:"true" help:""`
|
||||
SongLimit uint64 `json:"song_limit" default:"200" type:"number" help:"only get 200 songs by default"`
|
||||
}
|
||||
|
||||
func (ad *Addition) getCookie(name string) string {
|
||||
re := regexp.MustCompile(name + "=([^(;|$)]+)")
|
||||
matches := re.FindStringSubmatch(ad.Cookie)
|
||||
if len(matches) < 2 {
|
||||
return ""
|
||||
}
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "NeteaseMusic",
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &NeteaseMusic{}
|
||||
})
|
||||
}
|
116
drivers/netease_music/types.go
Normal file
116
drivers/netease_music/types.go
Normal file
@ -0,0 +1,116 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/sign"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
)
|
||||
|
||||
type HostsResp struct {
|
||||
Upload []string `json:"upload"`
|
||||
}
|
||||
|
||||
type SongResp struct {
|
||||
Data []struct {
|
||||
Url string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ListResp struct {
|
||||
Size string `json:"size"`
|
||||
MaxSize string `json:"maxSize"`
|
||||
Data []struct {
|
||||
AddTime int64 `json:"addTime"`
|
||||
FileName string `json:"fileName"`
|
||||
FileSize int64 `json:"fileSize"`
|
||||
SongId int64 `json:"songId"`
|
||||
SimpleSong struct {
|
||||
Al struct {
|
||||
PicUrl string `json:"picUrl"`
|
||||
} `json:"al"`
|
||||
} `json:"simpleSong"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type LyricObj struct {
|
||||
model.Object
|
||||
lyric string
|
||||
}
|
||||
|
||||
func (lrc *LyricObj) getProxyLink(args model.LinkArgs) *model.Link {
|
||||
rawURL := common.GetApiUrl(args.HttpReq) + "/p" + lrc.Path
|
||||
rawURL = utils.EncodePath(rawURL, true) + "?type=parsed&sign=" + sign.Sign(lrc.Path)
|
||||
return &model.Link{URL: rawURL}
|
||||
}
|
||||
|
||||
func (lrc *LyricObj) getLyricLink() *model.Link {
|
||||
reader := strings.NewReader(lrc.lyric)
|
||||
return &model.Link{
|
||||
RangeReadCloser: &model.RangeReadCloser{
|
||||
RangeReader: func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
|
||||
if httpRange.Length < 0 {
|
||||
return io.NopCloser(reader), nil
|
||||
}
|
||||
sr := io.NewSectionReader(reader, httpRange.Start, httpRange.Length)
|
||||
return io.NopCloser(sr), nil
|
||||
},
|
||||
Closers: utils.EmptyClosers(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ReqOption struct {
|
||||
crypto string
|
||||
stream model.FileStreamer
|
||||
data map[string]string
|
||||
headers map[string]string
|
||||
cookies []*http.Cookie
|
||||
url string
|
||||
}
|
||||
|
||||
type Characteristic map[string]string
|
||||
|
||||
func (ch *Characteristic) fromDriver(d *NeteaseMusic) *Characteristic {
|
||||
*ch = map[string]string{
|
||||
"osver": "",
|
||||
"deviceId": "",
|
||||
"mobilename": "",
|
||||
"appver": "6.1.1",
|
||||
"versioncode": "140",
|
||||
"buildver": strconv.FormatInt(time.Now().Unix(), 10),
|
||||
"resolution": "1920x1080",
|
||||
"os": "android",
|
||||
"channel": "",
|
||||
"requestId": strconv.FormatInt(time.Now().Unix()*1000, 10) + strconv.Itoa(int(random.RangeInt64(0, 1000))),
|
||||
"MUSIC_U": d.musicU,
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (ch Characteristic) toCookies() []*http.Cookie {
|
||||
cookies := make([]*http.Cookie, 0)
|
||||
for k, v := range ch {
|
||||
cookies = append(cookies, &http.Cookie{Name: k, Value: v})
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
|
||||
func (ch *Characteristic) merge(data map[string]string) map[string]interface{} {
|
||||
body := map[string]interface{}{
|
||||
"header": ch,
|
||||
}
|
||||
for k, v := range data {
|
||||
body[k] = v
|
||||
}
|
||||
return body
|
||||
}
|
208
drivers/netease_music/upload.go
Normal file
208
drivers/netease_music/upload.go
Normal file
@ -0,0 +1,208 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/dhowden/tag"
|
||||
)
|
||||
|
||||
type token struct {
|
||||
resourceId string
|
||||
objectKey string
|
||||
token string
|
||||
}
|
||||
|
||||
type songmeta struct {
|
||||
needUpload bool
|
||||
songId string
|
||||
name string
|
||||
artist string
|
||||
album string
|
||||
}
|
||||
|
||||
type uploader struct {
|
||||
driver *NeteaseMusic
|
||||
file model.File
|
||||
meta songmeta
|
||||
md5 string
|
||||
ext string
|
||||
size string
|
||||
filename string
|
||||
}
|
||||
|
||||
func (u *uploader) init(stream model.FileStreamer) error {
|
||||
u.filename = stream.GetName()
|
||||
u.size = strconv.FormatInt(stream.GetSize(), 10)
|
||||
|
||||
u.ext = "mp3"
|
||||
if strings.HasSuffix(stream.GetMimetype(), "flac") {
|
||||
u.ext = "flac"
|
||||
}
|
||||
|
||||
h := md5.New()
|
||||
io.Copy(h, stream)
|
||||
u.md5 = hex.EncodeToString(h.Sum(nil))
|
||||
_, err := u.file.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m, err := tag.ReadFrom(u.file); err != nil {
|
||||
u.meta = songmeta{}
|
||||
} else {
|
||||
u.meta = songmeta{
|
||||
name: m.Title(),
|
||||
artist: m.Artist(),
|
||||
album: m.Album(),
|
||||
}
|
||||
}
|
||||
if u.meta.name == "" {
|
||||
u.meta.name = u.filename
|
||||
}
|
||||
if u.meta.album == "" {
|
||||
u.meta.album = "未知专辑"
|
||||
}
|
||||
if u.meta.artist == "" {
|
||||
u.meta.artist = "未知艺术家"
|
||||
}
|
||||
_, err = u.file.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *uploader) checkIfExisted() error {
|
||||
body, err := u.driver.request("https://interface.music.163.com/api/cloud/upload/check", http.MethodPost,
|
||||
ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"ext": "",
|
||||
"songId": "0",
|
||||
"version": "1",
|
||||
"bitrate": "999000",
|
||||
"length": u.size,
|
||||
"md5": u.md5,
|
||||
},
|
||||
cookies: []*http.Cookie{
|
||||
{Name: "os", Value: "pc"},
|
||||
{Name: "appver", Value: "2.9.7"},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.meta.songId = utils.Json.Get(body, "songId").ToString()
|
||||
u.meta.needUpload = utils.Json.Get(body, "needUpload").ToBool()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *uploader) allocToken(bucket ...string) (token, error) {
|
||||
if len(bucket) == 0 {
|
||||
bucket = []string{""}
|
||||
}
|
||||
|
||||
body, err := u.driver.request("https://music.163.com/weapi/nos/token/alloc", http.MethodPost, ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"bucket": bucket[0],
|
||||
"local": "false",
|
||||
"type": "audio",
|
||||
"nos_product": "3",
|
||||
"filename": u.filename,
|
||||
"md5": u.md5,
|
||||
"ext": u.ext,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return token{}, err
|
||||
}
|
||||
|
||||
return token{
|
||||
resourceId: utils.Json.Get(body, "result", "resourceId").ToString(),
|
||||
objectKey: utils.Json.Get(body, "result", "objectKey").ToString(),
|
||||
token: utils.Json.Get(body, "result", "token").ToString(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *uploader) publishInfo(resourceId string) error {
|
||||
body, err := u.driver.request("https://music.163.com/api/upload/cloud/info/v2", http.MethodPost, ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"md5": u.md5,
|
||||
"filename": u.filename,
|
||||
"song": u.meta.name,
|
||||
"album": u.meta.album,
|
||||
"artist": u.meta.artist,
|
||||
"songid": u.meta.songId,
|
||||
"resourceId": resourceId,
|
||||
"bitrate": "999000",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = u.driver.request("https://interface.music.163.com/api/cloud/pub/v2", http.MethodPost, ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"songid": utils.Json.Get(body, "songId").ToString(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *uploader) upload(stream model.FileStreamer) error {
|
||||
bucket := "jd-musicrep-privatecloud-audio-public"
|
||||
token, err := u.allocToken(bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := u.driver.request("https://wanproxy.127.net/lbs?version=1.0&bucketname="+bucket, http.MethodGet,
|
||||
ReqOption{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var resp HostsResp
|
||||
err = utils.Json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objectKey := strings.ReplaceAll(token.objectKey, "/", "%2F")
|
||||
_, err = u.driver.request(
|
||||
resp.Upload[0]+"/"+bucket+"/"+objectKey+"?offset=0&complete=true&version=1.0",
|
||||
http.MethodPost,
|
||||
ReqOption{
|
||||
stream: stream,
|
||||
headers: map[string]string{
|
||||
"x-nos-token": token.token,
|
||||
"Content-Type": "audio/mpeg",
|
||||
"Content-Length": u.size,
|
||||
"Content-MD5": u.md5,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
246
drivers/netease_music/util.go
Normal file
246
drivers/netease_music/util.go
Normal file
@ -0,0 +1,246 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
func (d *NeteaseMusic) request(url, method string, opt ReqOption) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
|
||||
req.SetHeader("Cookie", d.Addition.Cookie)
|
||||
|
||||
if strings.Contains(url, "music.163.com") {
|
||||
req.SetHeader("Referer", "https://music.163.com")
|
||||
}
|
||||
|
||||
if opt.cookies != nil {
|
||||
for _, cookie := range opt.cookies {
|
||||
req.SetCookie(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
if opt.headers != nil {
|
||||
for header, value := range opt.headers {
|
||||
req.SetHeader(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
data := opt.data
|
||||
if opt.crypto == "weapi" {
|
||||
data = weapi(data)
|
||||
re, _ := regexp.Compile(`/\w*api/`)
|
||||
url = re.ReplaceAllString(url, "/weapi/")
|
||||
} else if opt.crypto == "eapi" {
|
||||
ch := new(Characteristic).fromDriver(d)
|
||||
req.SetCookies(ch.toCookies())
|
||||
data = eapi(opt.url, ch.merge(data))
|
||||
re, _ := regexp.Compile(`/\w*api/`)
|
||||
url = re.ReplaceAllString(url, "/eapi/")
|
||||
} else if opt.crypto == "linuxapi" {
|
||||
re, _ := regexp.Compile(`/\w*api/`)
|
||||
data = linuxapi(map[string]interface{}{
|
||||
"url": re.ReplaceAllString(url, "/api/"),
|
||||
"method": method,
|
||||
"params": data,
|
||||
})
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36")
|
||||
url = "https://music.163.com/api/linux/forward"
|
||||
}
|
||||
|
||||
if method == http.MethodPost {
|
||||
if opt.stream != nil {
|
||||
req.SetContentLength(true)
|
||||
req.SetBody(io.ReadCloser(opt.stream))
|
||||
} else {
|
||||
req.SetFormData(data)
|
||||
}
|
||||
res, err := req.Post(url)
|
||||
return res.Body(), err
|
||||
}
|
||||
|
||||
if method == http.MethodGet {
|
||||
res, err := req.Get(url)
|
||||
return res.Body(), err
|
||||
}
|
||||
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) getSongObjs(args model.ListArgs) ([]model.Obj, error) {
|
||||
body, err := d.request("https://music.163.com/weapi/v1/cloud/get", http.MethodPost, ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"limit": strconv.FormatUint(d.Addition.SongLimit, 10),
|
||||
"offset": "0",
|
||||
},
|
||||
cookies: []*http.Cookie{
|
||||
{Name: "os", Value: "pc"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp ListResp
|
||||
err = utils.Json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.fileMapByName = make(map[string]model.Obj)
|
||||
files := make([]model.Obj, 0, len(resp.Data))
|
||||
for _, f := range resp.Data {
|
||||
song := &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
IsFolder: false,
|
||||
Size: f.FileSize,
|
||||
Name: f.FileName,
|
||||
Modified: time.UnixMilli(f.AddTime),
|
||||
ID: strconv.FormatInt(f.SongId, 10),
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: f.SimpleSong.Al.PicUrl},
|
||||
}
|
||||
d.fileMapByName[song.Name] = song
|
||||
files = append(files, song)
|
||||
|
||||
// map song id for lyric
|
||||
lrcName := strings.Split(f.FileName, ".")[0] + ".lrc"
|
||||
lrc := &model.Object{
|
||||
IsFolder: false,
|
||||
Name: lrcName,
|
||||
Path: path.Join(args.ReqPath, lrcName),
|
||||
ID: strconv.FormatInt(f.SongId, 10),
|
||||
}
|
||||
d.fileMapByName[lrc.Name] = lrc
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) getSongLink(file model.Obj) (*model.Link, error) {
|
||||
body, err := d.request(
|
||||
"https://music.163.com/api/song/enhance/player/url", http.MethodPost, ReqOption{
|
||||
crypto: "linuxapi",
|
||||
data: map[string]string{
|
||||
"ids": "[" + file.GetID() + "]",
|
||||
"br": "999000",
|
||||
},
|
||||
cookies: []*http.Cookie{
|
||||
{Name: "os", Value: "pc"},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp SongResp
|
||||
err = utils.Json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.Data) < 1 {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
return &model.Link{URL: resp.Data[0].Url}, nil
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) getLyricObj(file model.Obj) (model.Obj, error) {
|
||||
if lrc, ok := file.(*LyricObj); ok {
|
||||
return lrc, nil
|
||||
}
|
||||
|
||||
body, err := d.request(
|
||||
"https://music.163.com/api/song/lyric?_nmclfl=1", http.MethodPost, ReqOption{
|
||||
data: map[string]string{
|
||||
"id": file.GetID(),
|
||||
"tv": "-1",
|
||||
"lv": "-1",
|
||||
"rv": "-1",
|
||||
"kv": "-1",
|
||||
},
|
||||
cookies: []*http.Cookie{
|
||||
{Name: "os", Value: "ios"},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lyric := utils.Json.Get(body, "lrc", "lyric").ToString()
|
||||
|
||||
return &LyricObj{
|
||||
lyric: lyric,
|
||||
Object: model.Object{
|
||||
IsFolder: false,
|
||||
ID: file.GetID(),
|
||||
Name: file.GetName(),
|
||||
Path: file.GetPath(),
|
||||
Size: int64(len(lyric)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) removeSongObj(file model.Obj) error {
|
||||
_, err := d.request("http://music.163.com/weapi/cloud/del", http.MethodPost, ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"songIds": "[" + file.GetID() + "]",
|
||||
},
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) putSongStream(stream model.FileStreamer) error {
|
||||
tmp, err := stream.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tmp.Close()
|
||||
|
||||
u := uploader{driver: d, file: tmp}
|
||||
|
||||
err = u.init(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = u.checkIfExisted()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := u.allocToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.meta.needUpload {
|
||||
err = u.upload(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = u.publishInfo(token.resourceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
131
drivers/onedrive_sharelink/driver.go
Normal file
131
drivers/onedrive_sharelink/driver.go
Normal file
@ -0,0 +1,131 @@
|
||||
package onedrive_sharelink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/cron"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type OnedriveSharelink struct {
|
||||
model.Storage
|
||||
cron *cron.Cron
|
||||
Addition
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) Init(ctx context.Context) error {
|
||||
// Initialize error variable
|
||||
var err error
|
||||
|
||||
// If there is "-my" in the URL, it is NOT a SharePoint link
|
||||
d.IsSharepoint = !strings.Contains(d.ShareLinkURL, "-my")
|
||||
|
||||
// Initialize cron job to run every hour
|
||||
d.cron = cron.NewCron(time.Hour * 1)
|
||||
d.cron.Do(func() {
|
||||
var err error
|
||||
d.Headers, err = d.getHeaders()
|
||||
if err != nil {
|
||||
log.Errorf("%+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Get initial headers
|
||||
d.Headers, err = d.getHeaders()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
path := dir.GetPath()
|
||||
files, err := d.getFiles(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert the slice of files to the required model.Obj format
|
||||
return utils.SliceConvert(files, func(src Item) (model.Obj, error) {
|
||||
return fileToObj(src), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
// Get the unique ID of the file
|
||||
uniqueId := file.GetID()
|
||||
// Cut the first char and the last char
|
||||
uniqueId = uniqueId[1 : len(uniqueId)-1]
|
||||
url := d.downloadLinkPrefix + uniqueId
|
||||
header := d.Headers
|
||||
|
||||
// If the headers are older than 30 minutes, get new headers
|
||||
if d.HeaderTime < time.Now().Unix()-1800 {
|
||||
var err error
|
||||
log.Debug("headers are older than 30 minutes, get new headers")
|
||||
header, err = d.getHeaders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: url,
|
||||
Header: header,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
// TODO create folder, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
// TODO move obj, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
// TODO rename obj, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
// TODO copy obj, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) Remove(ctx context.Context, obj model.Obj) error {
|
||||
// TODO remove obj, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *OnedriveSharelink) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
// TODO upload file, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
//func (d *OnedriveSharelink) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
var _ driver.Driver = (*OnedriveSharelink)(nil)
|
32
drivers/onedrive_sharelink/meta.go
Normal file
32
drivers/onedrive_sharelink/meta.go
Normal file
@ -0,0 +1,32 @@
|
||||
package onedrive_sharelink
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootPath
|
||||
ShareLinkURL string `json:"url" required:"true"`
|
||||
ShareLinkPassword string `json:"password"`
|
||||
IsSharepoint bool
|
||||
downloadLinkPrefix string
|
||||
Headers http.Header
|
||||
HeaderTime int64
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Onedrive Sharelink",
|
||||
OnlyProxy: true,
|
||||
NoUpload: true,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &OnedriveSharelink{}
|
||||
})
|
||||
}
|
77
drivers/onedrive_sharelink/types.go
Normal file
77
drivers/onedrive_sharelink/types.go
Normal file
@ -0,0 +1,77 @@
|
||||
package onedrive_sharelink
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
// FolderResp represents the structure of the folder response from the OneDrive API.
|
||||
type FolderResp struct {
|
||||
// Data holds the nested structure of the response.
|
||||
Data struct {
|
||||
Legacy struct {
|
||||
RenderListData struct {
|
||||
ListData struct {
|
||||
Items []Item `json:"Row"` // Items contains the list of items in the folder.
|
||||
} `json:"ListData"`
|
||||
} `json:"renderListDataAsStream"`
|
||||
} `json:"legacy"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// Item represents an individual item in the folder.
|
||||
type Item struct {
|
||||
ObjType string `json:"FSObjType"` // ObjType indicates if the item is a file or folder.
|
||||
Name string `json:"FileLeafRef"` // Name is the name of the item.
|
||||
ModifiedTime time.Time `json:"Modified."` // ModifiedTime is the last modified time of the item.
|
||||
Size string `json:"File_x0020_Size"` // Size is the size of the item in string format.
|
||||
Id string `json:"UniqueId"` // Id is the unique identifier of the item.
|
||||
}
|
||||
|
||||
// fileToObj converts an Item to an ObjThumb.
|
||||
func fileToObj(f Item) *model.ObjThumb {
|
||||
// Convert Size from string to int64.
|
||||
size, _ := strconv.ParseInt(f.Size, 10, 64)
|
||||
// Convert ObjType from string to int.
|
||||
objtype, _ := strconv.Atoi(f.ObjType)
|
||||
|
||||
// Create a new ObjThumb with the converted values.
|
||||
file := &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
Name: f.Name,
|
||||
Modified: f.ModifiedTime,
|
||||
Size: size,
|
||||
IsFolder: objtype == 1, // Check if the item is a folder.
|
||||
ID: f.Id,
|
||||
},
|
||||
Thumbnail: model.Thumbnail{},
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
// GraphQLNEWRequest represents the structure of a new GraphQL request.
|
||||
type GraphQLNEWRequest struct {
|
||||
ListData struct {
|
||||
NextHref string `json:"NextHref"` // NextHref is the link to the next set of data.
|
||||
Row []Item `json:"Row"` // Row contains the list of items.
|
||||
} `json:"ListData"`
|
||||
}
|
||||
|
||||
// GraphQLRequest represents the structure of a GraphQL request.
|
||||
type GraphQLRequest struct {
|
||||
Data struct {
|
||||
Legacy struct {
|
||||
RenderListDataAsStream struct {
|
||||
ListData struct {
|
||||
NextHref string `json:"NextHref"` // NextHref is the link to the next set of data.
|
||||
Row []Item `json:"Row"` // Row contains the list of items.
|
||||
} `json:"ListData"`
|
||||
ViewMetadata struct {
|
||||
ListViewXml string `json:"ListViewXml"` // ListViewXml contains the XML of the list view.
|
||||
} `json:"ViewMetadata"`
|
||||
} `json:"renderListDataAsStream"`
|
||||
} `json:"legacy"`
|
||||
} `json:"data"`
|
||||
}
|
363
drivers/onedrive_sharelink/util.go
Normal file
363
drivers/onedrive_sharelink/util.go
Normal file
@ -0,0 +1,363 @@
|
||||
package onedrive_sharelink
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// NewNoRedirectClient creates an HTTP client that doesn't follow redirects
|
||||
func NewNoRedirectCLient() *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: time.Hour * 48,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify},
|
||||
},
|
||||
// Prevent following redirects
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getCookiesWithPassword fetches cookies required for authenticated access using the provided password
|
||||
func getCookiesWithPassword(link, password string) (string, error) {
|
||||
// Send GET request
|
||||
resp, err := http.Get(link)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Parse the HTML response
|
||||
doc, err := html.Parse(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Initialize variables to store form data
|
||||
var viewstate, eventvalidation, postAction string
|
||||
|
||||
// Recursive function to find input fields by their IDs
|
||||
var findInputFields func(*html.Node)
|
||||
findInputFields = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && n.Data == "input" {
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "id" {
|
||||
switch attr.Val {
|
||||
case "__VIEWSTATE":
|
||||
viewstate = getAttrValue(n, "value")
|
||||
case "__EVENTVALIDATION":
|
||||
eventvalidation = getAttrValue(n, "value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.Type == html.ElementNode && n.Data == "form" {
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "id" && attr.Val == "inputForm" {
|
||||
postAction = getAttrValue(n, "action")
|
||||
}
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
findInputFields(c)
|
||||
}
|
||||
}
|
||||
findInputFields(doc)
|
||||
|
||||
// Prepare the new URL for the POST request
|
||||
linkParts, err := url.Parse(link)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newURL := fmt.Sprintf("%s://%s%s", linkParts.Scheme, linkParts.Host, postAction)
|
||||
|
||||
// Prepare the request body
|
||||
data := url.Values{
|
||||
"txtPassword": []string{password},
|
||||
"__EVENTVALIDATION": []string{eventvalidation},
|
||||
"__VIEWSTATE": []string{viewstate},
|
||||
"__VIEWSTATEENCRYPTED": []string{""},
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
// Send the POST request, preventing redirects
|
||||
resp, err = client.PostForm(newURL, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract the desired cookie value
|
||||
cookie := resp.Cookies()
|
||||
var fedAuthCookie string
|
||||
for _, c := range cookie {
|
||||
if c.Name == "FedAuth" {
|
||||
fedAuthCookie = c.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
if fedAuthCookie == "" {
|
||||
return "", fmt.Errorf("wrong password")
|
||||
}
|
||||
return fmt.Sprintf("FedAuth=%s;", fedAuthCookie), nil
|
||||
}
|
||||
|
||||
// getAttrValue retrieves the value of the specified attribute from an HTML node
|
||||
func getAttrValue(n *html.Node, key string) string {
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == key {
|
||||
return attr.Val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getHeaders constructs and returns the necessary HTTP headers for accessing the OneDrive share link
|
||||
func (d *OnedriveSharelink) getHeaders() (http.Header, error) {
|
||||
header := http.Header{}
|
||||
header.Set("User-Agent", base.UserAgent)
|
||||
header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
|
||||
|
||||
// Save current timestamp to d.HeaderTime
|
||||
d.HeaderTime = time.Now().Unix()
|
||||
|
||||
if d.ShareLinkPassword == "" {
|
||||
// Create a no-redirect client
|
||||
clientNoDirect := NewNoRedirectCLient()
|
||||
req, err := http.NewRequest("GET", d.ShareLinkURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set headers for the request
|
||||
req.Header = header
|
||||
answerNoRedirect, err := clientNoDirect.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
redirectUrl := answerNoRedirect.Header.Get("Location")
|
||||
log.Debugln("redirectUrl:", redirectUrl)
|
||||
if redirectUrl == "" {
|
||||
return nil, fmt.Errorf("password protected link. Please provide password")
|
||||
}
|
||||
header.Set("Cookie", answerNoRedirect.Header.Get("Set-Cookie"))
|
||||
header.Set("Referer", redirectUrl)
|
||||
|
||||
// Extract the host part of the redirect URL and set it as the authority
|
||||
u, err := url.Parse(redirectUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header.Set("authority", u.Host)
|
||||
return header, nil
|
||||
} else {
|
||||
cookie, err := getCookiesWithPassword(d.ShareLinkURL, d.ShareLinkPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header.Set("Cookie", cookie)
|
||||
header.Set("Referer", d.ShareLinkURL)
|
||||
header.Set("authority", strings.Split(strings.Split(d.ShareLinkURL, "//")[1], "/")[0])
|
||||
return header, nil
|
||||
}
|
||||
}
|
||||
|
||||
// getFiles retrieves the files from the OneDrive share link at the specified path
|
||||
func (d *OnedriveSharelink) getFiles(path string) ([]Item, error) {
|
||||
clientNoDirect := NewNoRedirectCLient()
|
||||
req, err := http.NewRequest("GET", d.ShareLinkURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header := req.Header
|
||||
redirectUrl := ""
|
||||
if d.ShareLinkPassword == "" {
|
||||
header.Set("User-Agent", base.UserAgent)
|
||||
header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
|
||||
req.Header = header
|
||||
answerNoRedirect, err := clientNoDirect.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
redirectUrl = answerNoRedirect.Header.Get("Location")
|
||||
} else {
|
||||
header = d.Headers
|
||||
req.Header = header
|
||||
answerNoRedirect, err := clientNoDirect.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
redirectUrl = answerNoRedirect.Header.Get("Location")
|
||||
}
|
||||
redirectSplitURL := strings.Split(redirectUrl, "/")
|
||||
req.Header = d.Headers
|
||||
downloadLinkPrefix := ""
|
||||
rootFolderPre := ""
|
||||
|
||||
// Determine the appropriate URL and root folder based on whether the link is SharePoint
|
||||
if d.IsSharepoint {
|
||||
// update req url
|
||||
req.URL, err = url.Parse(redirectUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Get redirectUrl
|
||||
answer, err := clientNoDirect.Do(req)
|
||||
if err != nil {
|
||||
d.Headers, err = d.getHeaders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.getFiles(path)
|
||||
}
|
||||
defer answer.Body.Close()
|
||||
re := regexp.MustCompile(`templateUrl":"(.*?)"`)
|
||||
body, err := io.ReadAll(answer.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
template := re.FindString(string(body))
|
||||
template = template[strings.Index(template, "templateUrl\":\"")+len("templateUrl\":\""):]
|
||||
template = template[:strings.Index(template, "?id=")]
|
||||
template = template[:strings.LastIndex(template, "/")]
|
||||
downloadLinkPrefix = template + "/download.aspx?UniqueId="
|
||||
params, err := url.ParseQuery(redirectUrl[strings.Index(redirectUrl, "?")+1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rootFolderPre = params.Get("id")
|
||||
} else {
|
||||
redirectUrlCut := redirectUrl[:strings.LastIndex(redirectUrl, "/")]
|
||||
downloadLinkPrefix = redirectUrlCut + "/download.aspx?UniqueId="
|
||||
params, err := url.ParseQuery(redirectUrl[strings.Index(redirectUrl, "?")+1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rootFolderPre = params.Get("id")
|
||||
}
|
||||
d.downloadLinkPrefix = downloadLinkPrefix
|
||||
rootFolder, err := url.QueryUnescape(rootFolderPre)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugln("rootFolder:", rootFolder)
|
||||
// Extract the relative path up to and including "Documents"
|
||||
relativePath := strings.Split(rootFolder, "Documents")[0] + "Documents"
|
||||
|
||||
// URL encode the relative path
|
||||
relativeUrl := url.QueryEscape(relativePath)
|
||||
// Replace underscores and hyphens in the encoded relative path
|
||||
relativeUrl = strings.Replace(relativeUrl, "_", "%5F", -1)
|
||||
relativeUrl = strings.Replace(relativeUrl, "-", "%2D", -1)
|
||||
|
||||
// If the path is not the root, append the path to the root folder
|
||||
if path != "/" {
|
||||
rootFolder = rootFolder + path
|
||||
}
|
||||
|
||||
// URL encode the full root folder path
|
||||
rootFolderUrl := url.QueryEscape(rootFolder)
|
||||
// Replace underscores and hyphens in the encoded root folder URL
|
||||
rootFolderUrl = strings.Replace(rootFolderUrl, "_", "%5F", -1)
|
||||
rootFolderUrl = strings.Replace(rootFolderUrl, "-", "%2D", -1)
|
||||
|
||||
log.Debugln("relativePath:", relativePath, "relativeUrl:", relativeUrl, "rootFolder:", rootFolder, "rootFolderUrl:", rootFolderUrl)
|
||||
|
||||
// Construct the GraphQL query with the encoded paths
|
||||
graphqlVar := fmt.Sprintf(`{"query":"query (\n $listServerRelativeUrl: String!,$renderListDataAsStreamParameters: RenderListDataAsStreamParameters!,$renderListDataAsStreamQueryString: String!\n )\n {\n \n legacy {\n \n renderListDataAsStream(\n listServerRelativeUrl: $listServerRelativeUrl,\n parameters: $renderListDataAsStreamParameters,\n queryString: $renderListDataAsStreamQueryString\n )\n }\n \n \n perf {\n executionTime\n overheadTime\n parsingTime\n queryCount\n validationTime\n resolvers {\n name\n queryCount\n resolveTime\n waitTime\n }\n }\n }","variables":{"listServerRelativeUrl":"%s","renderListDataAsStreamParameters":{"renderOptions":5707527,"allowMultipleValueFilterForTaxonomyFields":true,"addRequiredFields":true,"folderServerRelativeUrl":"%s"},"renderListDataAsStreamQueryString":"@a1=\'%s\'&RootFolder=%s&TryNewExperienceSingle=TRUE"}}`, relativePath, rootFolder, relativeUrl, rootFolderUrl)
|
||||
tempHeader := make(http.Header)
|
||||
for k, v := range d.Headers {
|
||||
tempHeader[k] = v
|
||||
}
|
||||
tempHeader["Content-Type"] = []string{"application/json;odata=verbose"}
|
||||
|
||||
client := &http.Client{}
|
||||
postUrl := strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], "/") + "/_api/v2.1/graphql"
|
||||
req, err = http.NewRequest("POST", postUrl, strings.NewReader(graphqlVar))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = tempHeader
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
d.Headers, err = d.getHeaders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.getFiles(path)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var graphqlReq GraphQLRequest
|
||||
json.NewDecoder(resp.Body).Decode(&graphqlReq)
|
||||
log.Debugln("graphqlReq:", graphqlReq)
|
||||
filesData := graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row
|
||||
if graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.NextHref != "" {
|
||||
nextHref := graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.NextHref + "&@a1=REPLACEME&TryNewExperienceSingle=TRUE"
|
||||
nextHref = strings.Replace(nextHref, "REPLACEME", "%27"+relativeUrl+"%27", -1)
|
||||
log.Debugln("nextHref:", nextHref)
|
||||
filesData = append(filesData, graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row...)
|
||||
|
||||
listViewXml := graphqlReq.Data.Legacy.RenderListDataAsStream.ViewMetadata.ListViewXml
|
||||
log.Debugln("listViewXml:", listViewXml)
|
||||
renderListDataAsStreamVar := `{"parameters":{"__metadata":{"type":"SP.RenderListDataParameters"},"RenderOptions":1216519,"ViewXml":"REPLACEME","AllowMultipleValueFilterForTaxonomyFields":true,"AddRequiredFields":true}}`
|
||||
listViewXml = strings.Replace(listViewXml, `"`, `\"`, -1)
|
||||
renderListDataAsStreamVar = strings.Replace(renderListDataAsStreamVar, "REPLACEME", listViewXml, -1)
|
||||
|
||||
graphqlReqNEW := GraphQLNEWRequest{}
|
||||
postUrl = strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], "/") + "/_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream" + nextHref
|
||||
req, _ = http.NewRequest("POST", postUrl, strings.NewReader(renderListDataAsStreamVar))
|
||||
req.Header = tempHeader
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
d.Headers, err = d.getHeaders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.getFiles(path)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
json.NewDecoder(resp.Body).Decode(&graphqlReqNEW)
|
||||
for graphqlReqNEW.ListData.NextHref != "" {
|
||||
graphqlReqNEW = GraphQLNEWRequest{}
|
||||
postUrl = strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], "/") + "/_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream" + nextHref
|
||||
req, _ = http.NewRequest("POST", postUrl, strings.NewReader(renderListDataAsStreamVar))
|
||||
req.Header = tempHeader
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
d.Headers, err = d.getHeaders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.getFiles(path)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
json.NewDecoder(resp.Body).Decode(&graphqlReqNEW)
|
||||
nextHref = graphqlReqNEW.ListData.NextHref + "&@a1=REPLACEME&TryNewExperienceSingle=TRUE"
|
||||
nextHref = strings.Replace(nextHref, "REPLACEME", "%27"+relativeUrl+"%27", -1)
|
||||
filesData = append(filesData, graphqlReqNEW.ListData.Row...)
|
||||
}
|
||||
filesData = append(filesData, graphqlReqNEW.ListData.Row...)
|
||||
} else {
|
||||
filesData = append(filesData, graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row...)
|
||||
}
|
||||
return filesData, nil
|
||||
}
|
@ -2,8 +2,11 @@ package pikpak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
@ -17,13 +20,14 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type PikPak struct {
|
||||
model.Storage
|
||||
Addition
|
||||
RefreshToken string
|
||||
AccessToken string
|
||||
*Common
|
||||
oauth2Token oauth2.TokenSource
|
||||
}
|
||||
|
||||
func (d *PikPak) Config() driver.Config {
|
||||
@ -34,8 +38,52 @@ func (d *PikPak) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *PikPak) Init(ctx context.Context) error {
|
||||
return d.login()
|
||||
func (d *PikPak) Init(ctx context.Context) (err error) {
|
||||
if d.ClientID == "" || d.ClientSecret == "" {
|
||||
d.ClientID = "YNxT9w7GMdWvEOKa"
|
||||
d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
}
|
||||
|
||||
if d.Common == nil {
|
||||
d.Common = &Common{
|
||||
client: base.NewRestyClient(),
|
||||
CaptchaToken: "",
|
||||
UserID: "",
|
||||
DeviceID: utils.GetMD5EncodeStr(d.Username + d.Password),
|
||||
UserAgent: BuildCustomUserAgent(utils.GetMD5EncodeStr(d.Username+d.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, ""),
|
||||
RefreshCTokenCk: func(token string) {
|
||||
d.Common.CaptchaToken = token
|
||||
op.MustSaveDriverStorage(d)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: d.ClientID,
|
||||
ClientSecret: d.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://user.mypikpak.com/v1/auth/signin",
|
||||
TokenURL: "https://user.mypikpak.com/v1/auth/token",
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
},
|
||||
}
|
||||
|
||||
d.oauth2Token = oauth2.ReuseTokenSource(nil, utils.TokenSource(func() (*oauth2.Token, error) {
|
||||
return oauth2Config.PasswordCredentialsToken(
|
||||
context.WithValue(context.Background(), oauth2.HTTPClient, base.HttpClient),
|
||||
d.Username,
|
||||
d.Password,
|
||||
)
|
||||
}))
|
||||
|
||||
// 获取用户ID
|
||||
_ = d.GetUserID()
|
||||
|
||||
// 获取CaptchaToken
|
||||
_ = d.RefreshCaptchaTokenAtLogin(GetAction(http.MethodGet, "https://api-drive.mypikpak.com/drive/v1/files"), d.Common.UserID)
|
||||
// 更新UserAgent
|
||||
d.Common.UserAgent = BuildCustomUserAgent(d.Common.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, d.Common.UserID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) Drop(ctx context.Context) error {
|
||||
@ -54,7 +102,7 @@ func (d *PikPak) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
|
||||
|
||||
func (d *PikPak) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var resp File
|
||||
_, err := d.request(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.GetID()),
|
||||
_, err := d.requestWithCaptchaToken(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.GetID()),
|
||||
http.MethodGet, nil, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -184,4 +232,107 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
return err
|
||||
}
|
||||
|
||||
// 离线下载文件
|
||||
func (d *PikPak) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
|
||||
requestBody := base.Json{
|
||||
"kind": "drive#file",
|
||||
"name": fileName,
|
||||
"upload_type": "UPLOAD_TYPE_URL",
|
||||
"url": base.Json{
|
||||
"url": fileUrl,
|
||||
},
|
||||
"parent_id": parentDir.GetID(),
|
||||
"folder_type": "",
|
||||
}
|
||||
|
||||
var resp OfflineDownloadResp
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(requestBody)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp.Task, err
|
||||
}
|
||||
|
||||
/*
|
||||
获取离线下载任务列表
|
||||
phase 可能的取值:
|
||||
PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING
|
||||
*/
|
||||
func (d *PikPak) OfflineList(ctx context.Context, nextPageToken string, phase []string) ([]OfflineTask, error) {
|
||||
res := make([]OfflineTask, 0)
|
||||
url := "https://api-drive.mypikpak.com/drive/v1/tasks"
|
||||
|
||||
if len(phase) == 0 {
|
||||
phase = []string{"PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR", "PHASE_TYPE_COMPLETE", "PHASE_TYPE_PENDING"}
|
||||
}
|
||||
params := map[string]string{
|
||||
"type": "offline",
|
||||
"thumbnail_size": "SIZE_SMALL",
|
||||
"limit": "10000",
|
||||
"page_token": nextPageToken,
|
||||
"with": "reference_resource",
|
||||
}
|
||||
|
||||
// 处理 phase 参数
|
||||
if len(phase) > 0 {
|
||||
filters := base.Json{
|
||||
"phase": map[string]string{
|
||||
"in": strings.Join(phase, ","),
|
||||
},
|
||||
}
|
||||
filtersJSON, err := json.Marshal(filters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal filters: %w", err)
|
||||
}
|
||||
params["filters"] = string(filtersJSON)
|
||||
}
|
||||
|
||||
var resp OfflineListResp
|
||||
_, err := d.request(url, http.MethodGet, func(req *resty.Request) {
|
||||
req.SetContext(ctx).
|
||||
SetQueryParams(params)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get offline list: %w", err)
|
||||
}
|
||||
res = append(res, resp.Tasks...)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *PikPak) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {
|
||||
url := "https://api-drive.mypikpak.com/drive/v1/tasks"
|
||||
params := map[string]string{
|
||||
"task_ids": strings.Join(taskIDs, ","),
|
||||
"delete_files": strconv.FormatBool(deleteFiles),
|
||||
}
|
||||
_, err := d.request(url, http.MethodDelete, func(req *resty.Request) {
|
||||
req.SetContext(ctx).
|
||||
SetQueryParams(params)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete tasks %v: %w", taskIDs, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) GetUserID() error {
|
||||
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userID := token.Extra("sub").(string)
|
||||
|
||||
if userID != "" {
|
||||
d.Common.SetUserID(userID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*PikPak)(nil)
|
||||
|
@ -9,6 +9,8 @@ type Addition struct {
|
||||
driver.RootID
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
|
||||
DisableMediaLink bool `json:"disable_media_link"`
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@ -9,11 +10,6 @@ import (
|
||||
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
|
||||
)
|
||||
|
||||
type RespErr struct {
|
||||
ErrorCode int `json:"error_code"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type Files struct {
|
||||
Files []File `json:"files"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
@ -99,3 +95,101 @@ type UploadTaskData struct {
|
||||
|
||||
File File `json:"file"`
|
||||
}
|
||||
|
||||
// 添加离线下载响应
|
||||
type OfflineDownloadResp struct {
|
||||
File *string `json:"file"`
|
||||
Task OfflineTask `json:"task"`
|
||||
UploadType string `json:"upload_type"`
|
||||
URL struct {
|
||||
Kind string `json:"kind"`
|
||||
} `json:"url"`
|
||||
}
|
||||
|
||||
// 离线下载列表
|
||||
type OfflineListResp struct {
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
Tasks []OfflineTask `json:"tasks"`
|
||||
}
|
||||
|
||||
// offlineTask
|
||||
type OfflineTask struct {
|
||||
Callback string `json:"callback"`
|
||||
CreatedTime string `json:"created_time"`
|
||||
FileID string `json:"file_id"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize string `json:"file_size"`
|
||||
IconLink string `json:"icon_link"`
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Message string `json:"message"`
|
||||
Name string `json:"name"`
|
||||
Params Params `json:"params"`
|
||||
Phase string `json:"phase"` // PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING
|
||||
Progress int64 `json:"progress"`
|
||||
ReferenceResource ReferenceResource `json:"reference_resource"`
|
||||
Space string `json:"space"`
|
||||
StatusSize int64 `json:"status_size"`
|
||||
Statuses []string `json:"statuses"`
|
||||
ThirdTaskID string `json:"third_task_id"`
|
||||
Type string `json:"type"`
|
||||
UpdatedTime string `json:"updated_time"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type Params struct {
|
||||
Age string `json:"age"`
|
||||
MIMEType *string `json:"mime_type,omitempty"`
|
||||
PredictType string `json:"predict_type"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type ReferenceResource struct {
|
||||
Type string `json:"@type"`
|
||||
Audit interface{} `json:"audit"`
|
||||
Hash string `json:"hash"`
|
||||
IconLink string `json:"icon_link"`
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Medias []Media `json:"medias"`
|
||||
MIMEType string `json:"mime_type"`
|
||||
Name string `json:"name"`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
ParentID string `json:"parent_id"`
|
||||
Phase string `json:"phase"`
|
||||
Size string `json:"size"`
|
||||
Space string `json:"space"`
|
||||
Starred bool `json:"starred"`
|
||||
Tags []string `json:"tags"`
|
||||
ThumbnailLink string `json:"thumbnail_link"`
|
||||
}
|
||||
|
||||
type ErrResp struct {
|
||||
ErrorCode int64 `json:"error_code"`
|
||||
ErrorMsg string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
@ -1,105 +1,108 @@
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *PikPak) login() error {
|
||||
url := "https://user.mypikpak.com/v1/auth/signin"
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
|
||||
"captcha_token": "",
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"username": d.Username,
|
||||
"password": d.Password,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
return errors.New(e.Error)
|
||||
}
|
||||
data := res.Body()
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
return nil
|
||||
var Algorithms = []string{
|
||||
"PAe56I7WZ6FCSkFy77A96jHWcQA27ui80Qy4",
|
||||
"SUbmk67TfdToBAEe2cZyP8vYVeN",
|
||||
"1y3yFSZVWiGN95fw/2FQlRuH/Oy6WnO",
|
||||
"8amLtHJpGzHPz4m9hGz7r+i+8dqQiAk",
|
||||
"tmIEq5yl2g/XWwM3sKZkY4SbL8YUezrvxPksNabUJ",
|
||||
"4QvudeJwgJuSf/qb9/wjC21L5aib",
|
||||
"D1RJd+FZ+LBbt+dAmaIyYrT9gxJm0BB",
|
||||
"1If",
|
||||
"iGZr/SJPUFRkwvC174eelKy",
|
||||
}
|
||||
|
||||
func (d *PikPak) refreshToken() error {
|
||||
url := "https://user.mypikpak.com/v1/auth/token"
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).
|
||||
SetHeader("user-agent", "").SetBody(base.Json{
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": d.RefreshToken,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
d.Status = err.Error()
|
||||
op.MustSaveDriverStorage(d)
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 4126 {
|
||||
// refresh_token invalid, re-login
|
||||
return d.login()
|
||||
}
|
||||
d.Status = e.Error
|
||||
op.MustSaveDriverStorage(d)
|
||||
return errors.New(e.Error)
|
||||
}
|
||||
data := res.Body()
|
||||
d.Status = "work"
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
const (
|
||||
ClientID = "YNxT9w7GMdWvEOKa"
|
||||
ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
ClientVersion = "1.46.2"
|
||||
PackageName = "com.pikcloud.pikpak"
|
||||
SdkVersion = "2.0.4.204000 "
|
||||
)
|
||||
|
||||
func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var e RespErr
|
||||
var e ErrResp
|
||||
req.SetError(&e)
|
||||
res, err := req.Execute(method, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 16 {
|
||||
// login / refresh token
|
||||
err = d.refreshToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
} else {
|
||||
return nil, errors.New(e.Error)
|
||||
}
|
||||
|
||||
if e.IsError() {
|
||||
return nil, &e
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *PikPak) requestWithCaptchaToken(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
|
||||
data, err := d.request(url, method, func(req *resty.Request) {
|
||||
req.SetHeaders(map[string]string{
|
||||
"User-Agent": d.GetUserAgent(),
|
||||
"X-Device-ID": d.GetDeviceID(),
|
||||
"X-Captcha-Token": d.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 d.refreshTokenFunc != nil {
|
||||
// if err = xc.refreshTokenFunc(); err == nil {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// return nil, err
|
||||
case 9: // 验证码token过期
|
||||
if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.Common.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
return d.requestWithCaptchaToken(url, method, callback, resp)
|
||||
}
|
||||
|
||||
func (d *PikPak) getFiles(id string) ([]File, error) {
|
||||
res := make([]File, 0)
|
||||
pageToken := "first"
|
||||
@ -128,27 +131,173 @@ func (d *PikPak) getFiles(id string) ([]File, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getGcid(r io.Reader, size int64) (string, error) {
|
||||
calcBlockSize := func(j int64) int64 {
|
||||
var psize int64 = 0x40000
|
||||
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
|
||||
psize = psize << 1
|
||||
}
|
||||
return psize
|
||||
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
|
||||
UserID string
|
||||
// 必要值,签名相关
|
||||
DeviceID string
|
||||
UserAgent string
|
||||
// 验证码token刷新成功回调
|
||||
RefreshCTokenCk func(token string)
|
||||
}
|
||||
|
||||
func generateDeviceSign(deviceID, packageName string) string {
|
||||
|
||||
signatureBase := fmt.Sprintf("%s%s%s%s", deviceID, packageName, "1", "appkey")
|
||||
|
||||
sha1Hash := sha1.New()
|
||||
sha1Hash.Write([]byte(signatureBase))
|
||||
sha1Result := sha1Hash.Sum(nil)
|
||||
|
||||
sha1String := hex.EncodeToString(sha1Result)
|
||||
|
||||
md5Hash := md5.New()
|
||||
md5Hash.Write([]byte(sha1String))
|
||||
md5Result := md5Hash.Sum(nil)
|
||||
|
||||
md5String := hex.EncodeToString(md5Result)
|
||||
|
||||
deviceSign := fmt.Sprintf("div101.%s%s", deviceID, md5String)
|
||||
|
||||
return deviceSign
|
||||
}
|
||||
|
||||
func BuildCustomUserAgent(deviceID, clientID, appName, sdkVersion, clientVersion, packageName, userID string) string {
|
||||
deviceSign := generateDeviceSign(deviceID, packageName)
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("ANDROID-%s/%s ", appName, clientVersion))
|
||||
sb.WriteString("protocolVersion/200 ")
|
||||
sb.WriteString("accesstype/ ")
|
||||
sb.WriteString(fmt.Sprintf("clientid/%s ", clientID))
|
||||
sb.WriteString(fmt.Sprintf("clientversion/%s ", clientVersion))
|
||||
sb.WriteString("action_type/ ")
|
||||
sb.WriteString("networktype/WIFI ")
|
||||
sb.WriteString("sessionid/ ")
|
||||
sb.WriteString(fmt.Sprintf("deviceid/%s ", deviceID))
|
||||
sb.WriteString("providername/NONE ")
|
||||
sb.WriteString(fmt.Sprintf("devicesign/%s ", deviceSign))
|
||||
sb.WriteString("refresh_token/ ")
|
||||
sb.WriteString(fmt.Sprintf("sdkversion/%s ", sdkVersion))
|
||||
sb.WriteString(fmt.Sprintf("datetime/%d ", time.Now().UnixMilli()))
|
||||
sb.WriteString(fmt.Sprintf("usrno/%s ", userID))
|
||||
sb.WriteString(fmt.Sprintf("appname/android-%s ", appName))
|
||||
sb.WriteString(fmt.Sprintf("session_origin/ "))
|
||||
sb.WriteString(fmt.Sprintf("grant_type/ "))
|
||||
sb.WriteString(fmt.Sprintf("appid/ "))
|
||||
sb.WriteString(fmt.Sprintf("clientip/ "))
|
||||
sb.WriteString(fmt.Sprintf("devicename/Xiaomi_M2004j7ac "))
|
||||
sb.WriteString(fmt.Sprintf("osversion/13 "))
|
||||
sb.WriteString(fmt.Sprintf("platformversion/10 "))
|
||||
sb.WriteString(fmt.Sprintf("accessmode/ "))
|
||||
sb.WriteString(fmt.Sprintf("devicemodel/M2004J7AC "))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (c *Common) SetDeviceID(deviceID string) {
|
||||
c.DeviceID = deviceID
|
||||
}
|
||||
|
||||
func (c *Common) SetUserID(userID string) {
|
||||
c.UserID = userID
|
||||
}
|
||||
|
||||
func (c *Common) SetUserAgent(userAgent string) {
|
||||
c.UserAgent = userAgent
|
||||
}
|
||||
|
||||
func (c *Common) SetCaptchaToken(captchaToken string) {
|
||||
c.CaptchaToken = captchaToken
|
||||
}
|
||||
func (c *Common) GetCaptchaToken() string {
|
||||
return c.CaptchaToken
|
||||
}
|
||||
|
||||
func (c *Common) GetUserAgent() string {
|
||||
return c.UserAgent
|
||||
}
|
||||
|
||||
func (c *Common) GetDeviceID() string {
|
||||
return c.DeviceID
|
||||
}
|
||||
|
||||
// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
|
||||
func (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error {
|
||||
metas := map[string]string{
|
||||
"client_version": ClientVersion,
|
||||
"package_name": PackageName,
|
||||
"user_id": userID,
|
||||
}
|
||||
metas["timestamp"], metas["captcha_sign"] = d.Common.GetCaptchaSign()
|
||||
return d.refreshCaptchaToken(action, metas)
|
||||
}
|
||||
|
||||
// RefreshCaptchaTokenInLogin 刷新验证码token(登录时)
|
||||
func (d *PikPak) 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 d.refreshCaptchaToken(action, metas)
|
||||
}
|
||||
|
||||
// GetCaptchaSign 获取验证码签名
|
||||
func (c *Common) GetCaptchaSign() (timestamp, sign string) {
|
||||
timestamp = fmt.Sprint(time.Now().UnixMilli())
|
||||
str := fmt.Sprint(ClientID, ClientVersion, PackageName, c.DeviceID, timestamp)
|
||||
for _, algorithm := range Algorithms {
|
||||
str = utils.GetMD5EncodeStr(str + algorithm)
|
||||
}
|
||||
sign = "1." + str
|
||||
return
|
||||
}
|
||||
|
||||
// refreshCaptchaToken 刷新CaptchaToken
|
||||
func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) error {
|
||||
param := CaptchaTokenRequest{
|
||||
Action: action,
|
||||
CaptchaToken: d.Common.CaptchaToken,
|
||||
ClientID: ClientID,
|
||||
DeviceID: d.Common.DeviceID,
|
||||
Meta: metas,
|
||||
RedirectUri: "xlaccsdk01://xbase.cloud/callback?state=harbor",
|
||||
}
|
||||
var e ErrResp
|
||||
var resp CaptchaTokenResponse
|
||||
_, err := d.request("https://user.mypikpak.com/v1/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetError(&e).SetBody(param)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash1 := sha1.New()
|
||||
hash2 := sha1.New()
|
||||
readSize := calcBlockSize(size)
|
||||
for {
|
||||
hash2.Reset()
|
||||
if n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {
|
||||
if err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
}
|
||||
hash1.Write(hash2.Sum(nil))
|
||||
if e.IsError() {
|
||||
return &e
|
||||
}
|
||||
return hex.EncodeToString(hash1.Sum(nil)), nil
|
||||
|
||||
if resp.Url != "" {
|
||||
return fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
|
||||
}
|
||||
|
||||
if resp.CaptchaToken == "" {
|
||||
return fmt.Errorf("empty captchaToken")
|
||||
}
|
||||
|
||||
if d.Common.RefreshCTokenCk != nil {
|
||||
d.Common.RefreshCTokenCk(resp.CaptchaToken)
|
||||
}
|
||||
d.Common.SetCaptchaToken(resp.CaptchaToken)
|
||||
return nil
|
||||
}
|
||||
|
@ -4,17 +4,18 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type PikPakShare struct {
|
||||
model.Storage
|
||||
Addition
|
||||
RefreshToken string
|
||||
AccessToken string
|
||||
oauth2Token oauth2.TokenSource
|
||||
PassCodeToken string
|
||||
}
|
||||
|
||||
@ -27,15 +28,31 @@ func (d *PikPakShare) GetAddition() driver.Additional {
|
||||
}
|
||||
|
||||
func (d *PikPakShare) Init(ctx context.Context) error {
|
||||
err := d.login()
|
||||
if err != nil {
|
||||
return err
|
||||
if d.ClientID == "" || d.ClientSecret == "" {
|
||||
d.ClientID = "YNxT9w7GMdWvEOKa"
|
||||
d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
}
|
||||
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: d.ClientID,
|
||||
ClientSecret: d.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://user.mypikpak.com/v1/auth/signin",
|
||||
TokenURL: "https://user.mypikpak.com/v1/auth/token",
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
},
|
||||
}
|
||||
|
||||
d.oauth2Token = oauth2.ReuseTokenSource(nil, utils.TokenSource(func() (*oauth2.Token, error) {
|
||||
return oauth2Config.PasswordCredentialsToken(
|
||||
context.WithValue(context.Background(), oauth2.HTTPClient, base.HttpClient),
|
||||
d.Username,
|
||||
d.Password,
|
||||
)
|
||||
}))
|
||||
|
||||
if d.SharePwd != "" {
|
||||
err = d.getSharePassToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.getSharePassToken()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -67,8 +84,14 @@ func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkA
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadUrl := resp.FileInfo.WebContentLink
|
||||
if downloadUrl == "" && len(resp.FileInfo.Medias) > 0 {
|
||||
downloadUrl = resp.FileInfo.Medias[0].Link.Url
|
||||
}
|
||||
|
||||
link := model.Link{
|
||||
URL: resp.FileInfo.WebContentLink,
|
||||
URL: downloadUrl,
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
@ -7,10 +7,12 @@ import (
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
ShareId string `json:"share_id" required:"true"`
|
||||
SharePwd string `json:"share_pwd"`
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
ShareId string `json:"share_id" required:"true"`
|
||||
SharePwd string `json:"share_pwd"`
|
||||
ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -5,70 +5,18 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *PikPakShare) login() error {
|
||||
url := "https://user.mypikpak.com/v1/auth/signin"
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
|
||||
"captcha_token": "",
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"username": d.Username,
|
||||
"password": d.Password,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
return errors.New(e.Error)
|
||||
}
|
||||
data := res.Body()
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPakShare) refreshToken() error {
|
||||
url := "https://user.mypikpak.com/v1/auth/token"
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).
|
||||
SetHeader("user-agent", "").SetBody(base.Json{
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": d.RefreshToken,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
d.Status = err.Error()
|
||||
op.MustSaveDriverStorage(d)
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 4126 {
|
||||
// refresh_token invalid, re-login
|
||||
return d.login()
|
||||
}
|
||||
d.Status = e.Error
|
||||
op.MustSaveDriverStorage(d)
|
||||
return errors.New(e.Error)
|
||||
}
|
||||
data := res.Body()
|
||||
d.Status = "work"
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
@ -82,14 +30,6 @@ func (d *PikPakShare) request(url string, method string, callback base.ReqCallba
|
||||
return nil, err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 16 {
|
||||
// login / refresh token
|
||||
err = d.refreshToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
}
|
||||
return nil, errors.New(e.Error)
|
||||
}
|
||||
return res.Body(), nil
|
||||
|
@ -16,7 +16,8 @@ import (
|
||||
type SFTP struct {
|
||||
model.Storage
|
||||
Addition
|
||||
client *sftp.Client
|
||||
client *sftp.Client
|
||||
clientConnectionError error
|
||||
}
|
||||
|
||||
func (d *SFTP) Config() driver.Config {
|
||||
@ -39,6 +40,9 @@ func (d *SFTP) Drop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *SFTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("[sftp] list dir: %s", dir.GetPath())
|
||||
files, err := d.client.ReadDir(dir.GetPath())
|
||||
if err != nil {
|
||||
@ -51,6 +55,9 @@ func (d *SFTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
|
||||
}
|
||||
|
||||
func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteFile, err := d.client.Open(file.GetPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -62,14 +69,23 @@ func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
|
||||
}
|
||||
|
||||
func (d *SFTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.client.MkdirAll(path.Join(parentDir.GetPath(), dirName))
|
||||
}
|
||||
|
||||
func (d *SFTP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.client.Rename(srcObj.GetPath(), path.Join(dstDir.GetPath(), srcObj.GetName()))
|
||||
}
|
||||
|
||||
func (d *SFTP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.client.Rename(srcObj.GetPath(), path.Join(path.Dir(srcObj.GetPath()), newName))
|
||||
}
|
||||
|
||||
@ -78,10 +94,16 @@ func (d *SFTP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
}
|
||||
|
||||
func (d *SFTP) Remove(ctx context.Context, obj model.Obj) error {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.remove(obj.GetPath())
|
||||
}
|
||||
|
||||
func (d *SFTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return err
|
||||
}
|
||||
dstFile, err := d.client.Create(path.Join(dstDir.GetPath(), stream.GetName()))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -10,6 +10,7 @@ type Addition struct {
|
||||
Username string `json:"username" required:"true"`
|
||||
PrivateKey string `json:"private_key" type:"text"`
|
||||
Password string `json:"password"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
driver.RootPath
|
||||
IgnoreSymlinkError bool `json:"ignore_symlink_error" default:"false" info:"Ignore symlink error"`
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@ -11,8 +12,14 @@ import (
|
||||
|
||||
func (d *SFTP) initClient() error {
|
||||
var auth ssh.AuthMethod
|
||||
if d.PrivateKey != "" {
|
||||
signer, err := ssh.ParsePrivateKey([]byte(d.PrivateKey))
|
||||
if len(d.PrivateKey) > 0 {
|
||||
var err error
|
||||
var signer ssh.Signer
|
||||
if len(d.Passphrase) > 0 {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(d.PrivateKey), []byte(d.Passphrase))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey([]byte(d.PrivateKey))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -30,6 +37,23 @@ func (d *SFTP) initClient() error {
|
||||
return err
|
||||
}
|
||||
d.client, err = sftp.NewClient(conn)
|
||||
if err == nil {
|
||||
d.clientConnectionError = nil
|
||||
go func(d *SFTP) {
|
||||
d.clientConnectionError = d.client.Wait()
|
||||
}(d)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *SFTP) clientReconnectOnConnectionError() error {
|
||||
err := d.clientConnectionError
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
log.Debugf("[sftp] discarding closed sftp connection: %v", err)
|
||||
_ = d.client.Close()
|
||||
err = d.initClient()
|
||||
return err
|
||||
}
|
||||
|
||||
|
837
drivers/thunder_browser/driver.go
Normal file
837
drivers/thunder_browser/driver.go
Normal file
@ -0,0 +1,837 @@
|
||||
package thunder_browser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
|
||||
"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"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ThunderBrowser struct {
|
||||
*XunLeiBrowserCommon
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
identity string
|
||||
}
|
||||
|
||||
func (x *ThunderBrowser) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (x *ThunderBrowser) GetAddition() driver.Additional {
|
||||
return &x.Addition
|
||||
}
|
||||
|
||||
func (x *ThunderBrowser) Init(ctx context.Context) (err error) {
|
||||
|
||||
spaceTokenFunc := func() error {
|
||||
// 如果用户未设置 "超级保险柜" 密码 则直接返回
|
||||
if x.SafePassword == "" {
|
||||
return nil
|
||||
}
|
||||
// 通过 GetSafeAccessToken 获取
|
||||
token, err := x.GetSafeAccessToken(x.SafePassword)
|
||||
x.SetSpaceTokenResp(token)
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化所需参数
|
||||
if x.XunLeiBrowserCommon == nil {
|
||||
x.XunLeiBrowserCommon = &XunLeiBrowserCommon{
|
||||
Common: &Common{
|
||||
client: base.NewRestyClient(),
|
||||
Algorithms: Algorithms,
|
||||
DeviceID: utils.GetMD5EncodeStr(x.Username + x.Password),
|
||||
ClientID: ClientID,
|
||||
ClientSecret: ClientSecret,
|
||||
ClientVersion: ClientVersion,
|
||||
PackageName: PackageName,
|
||||
UserAgent: BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), PackageName, SdkVersion, ClientVersion, PackageName),
|
||||
DownloadUserAgent: DownloadUserAgent,
|
||||
UseVideoUrl: x.UseVideoUrl,
|
||||
RemoveWay: x.Addition.RemoveWay,
|
||||
refreshCTokenCk: func(token string) {
|
||||
x.CaptchaToken = token
|
||||
op.MustSaveDriverStorage(x)
|
||||
},
|
||||
},
|
||||
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)
|
||||
}
|
||||
if x.DeviceID == "" {
|
||||
x.SetDeviceID(utils.GetMD5EncodeStr(x.Username + x.Password))
|
||||
}
|
||||
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.Addition.RootFolderID = x.RootFolderID
|
||||
// 防止重复登录
|
||||
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)
|
||||
}
|
||||
|
||||
// 获取 spaceToken
|
||||
err = spaceTokenFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderBrowser) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ThunderBrowserExpert struct {
|
||||
*XunLeiBrowserCommon
|
||||
model.Storage
|
||||
ExpertAddition
|
||||
|
||||
identity string
|
||||
}
|
||||
|
||||
func (x *ThunderBrowserExpert) Config() driver.Config {
|
||||
return configExpert
|
||||
}
|
||||
|
||||
func (x *ThunderBrowserExpert) GetAddition() driver.Additional {
|
||||
return &x.ExpertAddition
|
||||
}
|
||||
|
||||
func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {
|
||||
|
||||
spaceTokenFunc := func() error {
|
||||
// 如果用户未设置 "超级保险柜" 密码 则直接返回
|
||||
if x.SafePassword == "" {
|
||||
return nil
|
||||
}
|
||||
// 通过 GetSafeAccessToken 获取
|
||||
token, err := x.GetSafeAccessToken(x.SafePassword)
|
||||
x.SetSpaceTokenResp(token)
|
||||
return err
|
||||
}
|
||||
|
||||
// 防止重复登录
|
||||
identity := x.GetIdentity()
|
||||
if identity != x.identity || !x.IsLogin() {
|
||||
x.identity = identity
|
||||
x.XunLeiBrowserCommon = &XunLeiBrowserCommon{
|
||||
Common: &Common{
|
||||
client: base.NewRestyClient(),
|
||||
DeviceID: func() string {
|
||||
if len(x.DeviceID) != 32 {
|
||||
if x.LoginType == "user" {
|
||||
return utils.GetMD5EncodeStr(x.Username + x.Password)
|
||||
}
|
||||
return utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken)
|
||||
}
|
||||
return x.DeviceID
|
||||
}(),
|
||||
ClientID: x.ClientID,
|
||||
ClientSecret: x.ClientSecret,
|
||||
ClientVersion: x.ClientVersion,
|
||||
PackageName: x.PackageName,
|
||||
UserAgent: func() string {
|
||||
if x.ExpertAddition.UserAgent != "" {
|
||||
return x.ExpertAddition.UserAgent
|
||||
}
|
||||
if x.LoginType == "user" {
|
||||
return BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), x.PackageName, SdkVersion, x.ClientVersion, x.PackageName)
|
||||
}
|
||||
return BuildCustomUserAgent(utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken), x.PackageName, SdkVersion, x.ClientVersion, x.PackageName)
|
||||
}(),
|
||||
DownloadUserAgent: func() string {
|
||||
if x.ExpertAddition.DownloadUserAgent != "" {
|
||||
return x.ExpertAddition.DownloadUserAgent
|
||||
}
|
||||
return DownloadUserAgent
|
||||
}(),
|
||||
UseVideoUrl: x.UseVideoUrl,
|
||||
RemoveWay: x.ExpertAddition.RemoveWay,
|
||||
refreshCTokenCk: func(token string) {
|
||||
x.CaptchaToken = token
|
||||
op.MustSaveDriverStorage(x)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if x.ExpertAddition.CaptchaToken != "" {
|
||||
x.SetCaptchaToken(x.ExpertAddition.CaptchaToken)
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
if x.Common.DeviceID != "" {
|
||||
x.ExpertAddition.DeviceID = x.Common.DeviceID
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
if x.Common.UserAgent != "" {
|
||||
x.ExpertAddition.UserAgent = x.Common.UserAgent
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
if x.Common.DownloadUserAgent != "" {
|
||||
x.ExpertAddition.DownloadUserAgent = x.Common.DownloadUserAgent
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.ExpertAddition.RootFolderID = x.RootFolderID
|
||||
// 签名方法
|
||||
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.XunLeiBrowserCommon.RefreshToken(x.ExpertAddition.RefreshToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
|
||||
// 刷新token方法
|
||||
x.SetRefreshTokenFunc(func() error {
|
||||
token, err := x.XunLeiBrowserCommon.RefreshToken(x.TokenResp.RefreshToken)
|
||||
if err != nil {
|
||||
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
op.MustSaveDriverStorage(x)
|
||||
return err
|
||||
})
|
||||
|
||||
err = spaceTokenFunc()
|
||||
if err != nil {
|
||||
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.XunLeiBrowserCommon.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
|
||||
})
|
||||
|
||||
err = spaceTokenFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 仅修改验证码token
|
||||
if x.CaptchaToken != "" {
|
||||
x.SetCaptchaToken(x.CaptchaToken)
|
||||
}
|
||||
|
||||
err = spaceTokenFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
x.XunLeiBrowserCommon.UserAgent = x.UserAgent
|
||||
x.XunLeiBrowserCommon.DownloadUserAgent = x.DownloadUserAgent
|
||||
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.ExpertAddition.RootFolderID = x.RootFolderID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderBrowserExpert) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderBrowserExpert) SetTokenResp(token *TokenResp) {
|
||||
x.XunLeiBrowserCommon.SetTokenResp(token)
|
||||
if token != nil {
|
||||
x.ExpertAddition.RefreshToken = token.RefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
type XunLeiBrowserCommon struct {
|
||||
*Common
|
||||
*TokenResp // 登录信息
|
||||
|
||||
refreshTokenFunc func() error
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
return xc.getFiles(ctx, dir.GetID(), args.ReqPath)
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var lFile Files
|
||||
|
||||
params := map[string]string{
|
||||
"_magic": "2021",
|
||||
"space": "SPACE_BROWSER",
|
||||
"thumbnail_size": "SIZE_LARGE",
|
||||
"with": "url",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if file.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
} else if file.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
params["space"] = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetPathParam("fileID", file.GetID())
|
||||
r.SetQueryParams(params)
|
||||
//r.SetQueryParam("space", "")
|
||||
}, &lFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link := &model.Link{
|
||||
URL: lFile.WebContentLink,
|
||||
Header: http.Header{
|
||||
"User-Agent": {xc.DownloadUserAgent},
|
||||
},
|
||||
}
|
||||
|
||||
if xc.UseVideoUrl {
|
||||
for _, media := range lFile.Medias {
|
||||
if media.Link.URL != "" {
|
||||
link.URL = media.Link.URL
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
js := base.Json{
|
||||
"kind": FOLDER,
|
||||
"name": dirName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
if parentDir.GetPath() == ThunderDriveFileID {
|
||||
js = base.Json{
|
||||
"kind": FOLDER,
|
||||
"name": dirName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
}
|
||||
} else if parentDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
js = base.Json{
|
||||
"kind": FOLDER,
|
||||
"name": dirName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
}
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
|
||||
srcSpace := "SPACE_BROWSER"
|
||||
dstSpace := "SPACE_BROWSER"
|
||||
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
srcSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
dstSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"_from": dstSpace,
|
||||
}
|
||||
js := base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID(), "space": dstSpace},
|
||||
"space": srcSpace,
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
js = base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID()},
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
r.SetQueryParams(params)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
|
||||
params := map[string]string{
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
} else if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
params = map[string]string{
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
}
|
||||
|
||||
_, 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})
|
||||
r.SetQueryParams(params)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
|
||||
srcSpace := "SPACE_BROWSER"
|
||||
dstSpace := "SPACE_BROWSER"
|
||||
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
srcSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
dstSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"_from": dstSpace,
|
||||
}
|
||||
js := base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID(), "space": dstSpace},
|
||||
"space": srcSpace,
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
js = base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID()},
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
r.SetQueryParams(params)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Remove(ctx context.Context, obj model.Obj) error {
|
||||
|
||||
js := base.Json{
|
||||
"ids": []string{obj.GetID()},
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if obj.GetPath() == ThunderDriveFileID {
|
||||
js = base.Json{
|
||||
"ids": []string{obj.GetID()},
|
||||
}
|
||||
} else if obj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
js = base.Json{
|
||||
"ids": []string{obj.GetID()},
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
}
|
||||
|
||||
// 先判断是否是特殊情况
|
||||
if obj.GetPath() == ThunderDriveFileID {
|
||||
_, 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
|
||||
} else if obj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
_, err := xc.Request(FILE_API_URL+":batchDelete", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// 根据用户选择的删除方式进行删除
|
||||
if xc.RemoveWay == "delete" {
|
||||
_, err := xc.Request(FILE_API_URL+":batchDelete", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
}, nil)
|
||||
return err
|
||||
} else {
|
||||
_, err := xc.Request(FILE_API_URL+":batchTrash", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
hi := stream.GetHash()
|
||||
gcid := hi.GetHash(hash_extend.GCID)
|
||||
if len(gcid) < hash_extend.GCID.Width {
|
||||
tFile, err := stream.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gcid, err = utils.HashFile(hash_extend.GCID, tFile, stream.GetSize())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
js := base.Json{
|
||||
"kind": FILE,
|
||||
"parent_id": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"size": stream.GetSize(),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if dstDir.GetPath() == ThunderDriveFileID {
|
||||
js = base.Json{
|
||||
"kind": FILE,
|
||||
"parent_id": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"size": stream.GetSize(),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
}
|
||||
} else if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
js = base.Json{
|
||||
"kind": FILE,
|
||||
"parent_id": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"size": stream.GetSize(),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
}
|
||||
|
||||
var resp UploadTaskResponse
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
}, &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
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
||||
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
|
||||
}
|
||||
_, err = uploader.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 *XunLeiBrowserCommon) getFiles(ctx context.Context, folderId string, path string) ([]model.Obj, error) {
|
||||
files := make([]model.Obj, 0)
|
||||
var pageToken string
|
||||
for {
|
||||
var fileList FileList
|
||||
folderSpace := "SPACE_BROWSER"
|
||||
params := map[string]string{
|
||||
"parent_id": folderId,
|
||||
"page_token": pageToken,
|
||||
"space": folderSpace,
|
||||
"filters": `{"trashed":{"eq":false}}`,
|
||||
"with_audit": "true",
|
||||
"thumbnail_size": "SIZE_LARGE",
|
||||
}
|
||||
var fileType int8
|
||||
// 处理特殊目录 “迅雷云盘” 设置特殊的 params 以便正常访问
|
||||
pattern1 := fmt.Sprintf(`^/.*/%s(/.*)?$`, ThunderDriveFolderName)
|
||||
thunderDriveMatch, _ := regexp.MatchString(pattern1, path)
|
||||
// 处理特殊目录 “超级保险箱” 设置特殊的 params 以便正常访问
|
||||
pattern2 := fmt.Sprintf(`^/.*/%s(/.*)?$`, ThunderBrowserDriveSafeFolderName)
|
||||
thunderBrowserDriveSafeMatch, _ := regexp.MatchString(pattern2, path)
|
||||
|
||||
// 如果是 "迅雷云盘" 内的
|
||||
if folderId == ThunderDriveFileID || thunderDriveMatch {
|
||||
params = 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}}`,
|
||||
}
|
||||
// 如果不是 "迅雷云盘"的"根目录"
|
||||
if folderId == ThunderDriveFileID {
|
||||
params["parent_id"] = ""
|
||||
}
|
||||
fileType = ThunderDriveType
|
||||
} else if thunderBrowserDriveSafeMatch {
|
||||
// 如果是 "超级保险箱" 内的
|
||||
fileType = ThunderBrowserDriveSafeType
|
||||
params["space"] = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetQueryParams(params)
|
||||
}, &fileList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 对文件夹也进行处理
|
||||
fileList.FolderType = fileType
|
||||
|
||||
for i := 0; i < len(fileList.Files); i++ {
|
||||
file := &fileList.Files[i]
|
||||
// 标记 文件夹内的文件
|
||||
file.FileType = fileList.FolderType
|
||||
// 解决 "迅雷云盘" 重复出现问题————迅雷后端发送错误
|
||||
if file.Name == ThunderDriveFolderName && file.ID == "" && file.FolderType == ThunderDriveFolderType && folderId != "" {
|
||||
continue
|
||||
}
|
||||
// 处理特殊目录 “迅雷云盘” 设置特殊的文件夹ID
|
||||
if file.Name == ThunderDriveFolderName && file.ID == "" && file.FolderType == ThunderDriveFolderType {
|
||||
file.ID = ThunderDriveFileID
|
||||
} else if file.Name == ThunderBrowserDriveSafeFolderName && file.FolderType == ThunderBrowserDriveSafeFolderType {
|
||||
file.FileType = ThunderBrowserDriveSafeType
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
if fileList.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = fileList.NextPageToken
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// SetRefreshTokenFunc 设置刷新Token的方法
|
||||
func (xc *XunLeiBrowserCommon) SetRefreshTokenFunc(fn func() error) {
|
||||
xc.refreshTokenFunc = fn
|
||||
}
|
||||
|
||||
// SetTokenResp 设置Token
|
||||
func (xc *XunLeiBrowserCommon) SetTokenResp(tr *TokenResp) {
|
||||
xc.TokenResp = tr
|
||||
}
|
||||
|
||||
// SetSpaceTokenResp 设置Token
|
||||
func (xc *XunLeiBrowserCommon) SetSpaceTokenResp(spaceToken string) {
|
||||
xc.TokenResp.Token = spaceToken
|
||||
}
|
||||
|
||||
// Request 携带Authorization和CaptchaToken的请求
|
||||
func (xc *XunLeiBrowserCommon) 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.GetToken(),
|
||||
"X-Captcha-Token": xc.GetCaptchaToken(),
|
||||
"X-Space-Authorization": xc.GetSpaceToken(),
|
||||
})
|
||||
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:
|
||||
// space_token 获取失败
|
||||
if errResp.ErrorMsg == "space_token_invalid" {
|
||||
if token, err := xc.GetSafeAccessToken(xc.Token); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
xc.SetSpaceTokenResp(token)
|
||||
}
|
||||
|
||||
}
|
||||
if errResp.ErrorMsg == "captcha_invalid" {
|
||||
// 验证码token过期
|
||||
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
return xc.Request(url, method, callback, resp)
|
||||
}
|
||||
|
||||
// RefreshToken 刷新Token
|
||||
func (xc *XunLeiBrowserCommon) 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, errors.New("refresh token is empty")
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetSafeAccessToken 获取 超级保险柜 AccessToken
|
||||
func (xc *XunLeiBrowserCommon) GetSafeAccessToken(safePassword string) (string, error) {
|
||||
var resp TokenResp
|
||||
_, err := xc.Request(XLUSER_API_URL+"/password/check", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(&base.Json{
|
||||
"scene": "box",
|
||||
"password": EncryptPassword(safePassword),
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Token == "" {
|
||||
return "", errors.New("SafePassword is incorrect ")
|
||||
}
|
||||
return resp.Token, nil
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
func (xc *XunLeiBrowserCommon) 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 *XunLeiBrowserCommon) IsLogin() bool {
|
||||
if xc.TokenResp == nil {
|
||||
return false
|
||||
}
|
||||
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
|
||||
return err == nil
|
||||
}
|
108
drivers/thunder_browser/meta.go
Normal file
108
drivers/thunder_browser/meta.go
Normal file
@ -0,0 +1,108 @@
|
||||
package thunder_browser
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// ExpertAddition 高级设置
|
||||
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"`
|
||||
|
||||
SafePassword string `json:"safe_password" required:"true" help:"super safe password"` // 超级保险箱密码
|
||||
|
||||
// 签名方法1
|
||||
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"p+ExqPV,LwdwKlprzv7cQBQmxN5,vc08P1NwUBnbGsl58LzTW,VVNeXaXmZ8HH1SJEnp6YpVFSFU,pNAOJ,CNChvyDehAmUR1TDodfOusBAx,MS98NnX4Np8nxvEh6Ulv+SMMKMzKvD34C7lGWbb,9MpFF21GnVOYku0NM9Y/hzsK471UCUZ2o+,EY1QfeA06fXlw9wZNoZaXEED5zZPvNWI,,sciE,FIPqgQDUUW1e0GkiBFd5w7mCQ,zW,75XFdEO0Gi"`
|
||||
// 签名方法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:"false" default:""`
|
||||
ClientID string `json:"client_id" required:"true" default:"ZUBzD9J_XPXfn7f7"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"yESVmHecEe6F0aou69vl-g"`
|
||||
ClientVersion string `json:"client_version" required:"true" default:"1.0.8.2215"`
|
||||
PackageName string `json:"package_name" required:"true" default:"com.xunlei.browser"`
|
||||
|
||||
// 不影响登录,影响下载速度
|
||||
UserAgent string `json:"user_agent" required:"false" default:""`
|
||||
DownloadUserAgent string `json:"download_user_agent" required:"false" default:""`
|
||||
|
||||
// 优先使用视频链接代替下载链接
|
||||
UseVideoUrl bool `json:"use_video_url"`
|
||||
// 移除方式
|
||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||
}
|
||||
|
||||
// GetIdentity 登录特征,用于判断是否重新登录
|
||||
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"`
|
||||
SafePassword string `json:"safe_password" required:"true"` // 超级保险箱密码
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
UseVideoUrl bool `json:"use_video_url" default:"false"`
|
||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||
}
|
||||
|
||||
// GetIdentity 登录特征,用于判断是否重新登录
|
||||
func (i *Addition) GetIdentity() string {
|
||||
return utils.GetMD5EncodeStr(i.Username + i.Password)
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "ThunderBrowser",
|
||||
LocalSort: true,
|
||||
}
|
||||
|
||||
var configExpert = driver.Config{
|
||||
Name: "ThunderBrowserExpert",
|
||||
LocalSort: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &ThunderBrowser{}
|
||||
})
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &ThunderBrowserExpert{}
|
||||
})
|
||||
}
|
223
drivers/thunder_browser/types.go
Normal file
223
drivers/thunder_browser/types.go
Normal file
@ -0,0 +1,223 @@
|
||||
package thunder_browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
|
||||
)
|
||||
|
||||
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"`
|
||||
|
||||
Token string `json:"token"` // "超级保险箱" 访问Token
|
||||
}
|
||||
|
||||
func (t *TokenResp) GetToken() string {
|
||||
return fmt.Sprint(t.TokenType, " ", t.AccessToken)
|
||||
}
|
||||
|
||||
// GetSpaceToken 获取"超级保险箱" 访问Token
|
||||
func (t *TokenResp) GetSpaceToken() string {
|
||||
return t.Token
|
||||
}
|
||||
|
||||
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"`
|
||||
FolderType int8
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
Expire time.Time `json:"expire"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
var _ model.Obj = (*Files)(nil)
|
||||
|
||||
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 CustomTime `json:"created_time"`
|
||||
ModifiedTime CustomTime `json:"modified_time"`
|
||||
IconLink string `json:"icon_link"`
|
||||
ThumbnailLink string `json:"thumbnail_link"`
|
||||
// Md5Checksum string `json:"md5_checksum"`
|
||||
Hash string `json:"hash"`
|
||||
// Links map[string]Link `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 Link `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"`
|
||||
FileType int8
|
||||
}
|
||||
|
||||
func (c *Files) GetHash() utils.HashInfo {
|
||||
return utils.NewHashInfo(hash_extend.GCID, c.Hash)
|
||||
}
|
||||
|
||||
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) CreateTime() time.Time { return c.CreatedTime.Time }
|
||||
func (c *Files) ModTime() time.Time { return c.ModifiedTime.Time }
|
||||
func (c *Files) IsDir() bool { return c.Kind == FOLDER }
|
||||
func (c *Files) GetID() string { return c.ID }
|
||||
func (c *Files) GetPath() string {
|
||||
// 对特殊文件进行特殊处理
|
||||
if c.FileType == ThunderDriveType {
|
||||
return ThunderDriveFileID
|
||||
} else if c.FileType == ThunderBrowserDriveSafeType {
|
||||
return ThunderBrowserDriveSafeFileID
|
||||
}
|
||||
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"`
|
||||
}
|
318
drivers/thunder_browser/util.go
Normal file
318
drivers/thunder_browser/util.go
Normal file
@ -0,0 +1,318 @@
|
||||
package thunder_browser
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"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://x-api-pan.xunlei.com/drive/v1"
|
||||
FILE_API_URL = API_URL + "/files"
|
||||
XLUSER_API_URL = "https://xluser-ssl.xunlei.com/v1"
|
||||
)
|
||||
|
||||
var Algorithms = []string{
|
||||
"p+ExqPV",
|
||||
"LwdwKlprzv7cQBQmxN5",
|
||||
"vc08P1NwUBnbGsl58LzTW",
|
||||
"VVNeXaXmZ8HH1SJEnp6YpVFSFU",
|
||||
"pNAOJ",
|
||||
"CNChvyDehAmUR1TDodfOusBAx",
|
||||
"MS98NnX4Np8nxvEh6Ulv+SMMKMzKvD34C7lGWbb",
|
||||
"9MpFF21GnVOYku0NM9Y/hzsK471UCUZ2o+",
|
||||
"EY1QfeA06fXlw9wZNoZaXEED5zZPvNWI",
|
||||
"",
|
||||
"sciE",
|
||||
"FIPqgQDUUW1e0GkiBFd5w7mCQ",
|
||||
"zW",
|
||||
"75XFdEO0Gi",
|
||||
}
|
||||
|
||||
const (
|
||||
ClientID = "ZUBzD9J_XPXfn7f7"
|
||||
ClientSecret = "yESVmHecEe6F0aou69vl-g"
|
||||
ClientVersion = "1.0.8.2215"
|
||||
PackageName = "com.xunlei.browser"
|
||||
DownloadUserAgent = "AndroidDownloadManager/13 (Linux; U; Android 13; M2004J7AC Build/SP1A.210812.016)"
|
||||
SdkVersion = "2.0.3.262"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
const (
|
||||
ThunderDriveFileID = "XXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
ThunderBrowserDriveSafeFileID = "YYYYYYYYYYYYYYYYYYYYYYYYYY"
|
||||
ThunderDriveFolderName = "迅雷云盘"
|
||||
ThunderBrowserDriveSafeFolderName = "超级保险箱"
|
||||
ThunderDriveType = 1
|
||||
ThunderBrowserDriveSafeType = 2
|
||||
ThunderDriveFolderType = "DEFAULT_ROOT"
|
||||
ThunderBrowserDriveSafeFolderType = "BROWSER_SAFE"
|
||||
)
|
||||
|
||||
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
|
||||
UseVideoUrl bool
|
||||
RemoveWay string
|
||||
|
||||
// 验证码token刷新成功回调
|
||||
refreshCTokenCk func(token string)
|
||||
}
|
||||
|
||||
func (c *Common) SetDeviceID(deviceID string) {
|
||||
c.DeviceID = deviceID
|
||||
}
|
||||
|
||||
func (c *Common) SetCaptchaToken(captchaToken string) {
|
||||
c.captchaToken = captchaToken
|
||||
}
|
||||
func (c *Common) GetCaptchaToken() string {
|
||||
return c.captchaToken
|
||||
}
|
||||
|
||||
// RefreshCaptchaTokenAtLogin 刷新验证码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)
|
||||
}
|
||||
|
||||
// RefreshCaptchaTokenInLogin 刷新验证码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)
|
||||
}
|
||||
|
||||
// GetCaptchaSign 获取验证码签名
|
||||
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.GetMD5EncodeStr(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: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
|
||||
}
|
||||
|
||||
if resp.CaptchaToken == "" {
|
||||
return fmt.Errorf("empty captchaToken")
|
||||
}
|
||||
|
||||
if c.refreshCTokenCk != nil {
|
||||
c.refreshCTokenCk(resp.CaptchaToken)
|
||||
}
|
||||
c.SetCaptchaToken(resp.CaptchaToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Request 只有基础信息的请求
|
||||
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
|
||||
}
|
||||
|
||||
// 计算文件Gcid
|
||||
func getGcid(r io.Reader, size int64) (string, error) {
|
||||
calcBlockSize := func(j int64) int64 {
|
||||
var psize int64 = 0x40000
|
||||
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
|
||||
psize = psize << 1
|
||||
}
|
||||
return psize
|
||||
}
|
||||
|
||||
hash1 := sha1.New()
|
||||
hash2 := sha1.New()
|
||||
readSize := calcBlockSize(size)
|
||||
for {
|
||||
hash2.Reset()
|
||||
if n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {
|
||||
if err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
}
|
||||
hash1.Write(hash2.Sum(nil))
|
||||
}
|
||||
return hex.EncodeToString(hash1.Sum(nil)), nil
|
||||
}
|
||||
|
||||
type CustomTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
const timeFormat = time.RFC3339
|
||||
|
||||
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
|
||||
str := string(b)
|
||||
if str == `""` {
|
||||
*ct = CustomTime{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}
|
||||
return nil
|
||||
}
|
||||
|
||||
t, err := time.Parse(`"`+timeFormat+`"`, str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*ct = CustomTime{Time: t}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncryptPassword 超级保险箱 加密
|
||||
func EncryptPassword(password string) string {
|
||||
if password == "" {
|
||||
return ""
|
||||
}
|
||||
// 将字符串转换为字节数组
|
||||
byteData := []byte(password)
|
||||
// 计算MD5哈希值
|
||||
hash := md5.Sum(byteData)
|
||||
// 将哈希值转换为十六进制字符串
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func generateDeviceSign(deviceID, packageName string) string {
|
||||
|
||||
signatureBase := fmt.Sprintf("%s%s%s%s", deviceID, packageName, "22062", "a5d7416858147a4ab99573872ffccef8")
|
||||
|
||||
sha1Hash := sha1.New()
|
||||
sha1Hash.Write([]byte(signatureBase))
|
||||
sha1Result := sha1Hash.Sum(nil)
|
||||
|
||||
sha1String := hex.EncodeToString(sha1Result)
|
||||
|
||||
md5Hash := md5.New()
|
||||
md5Hash.Write([]byte(sha1String))
|
||||
md5Result := md5Hash.Sum(nil)
|
||||
|
||||
md5String := hex.EncodeToString(md5Result)
|
||||
|
||||
deviceSign := fmt.Sprintf("div101.%s%s", deviceID, md5String)
|
||||
|
||||
return deviceSign
|
||||
}
|
||||
|
||||
func BuildCustomUserAgent(deviceID, appName, sdkVersion, clientVersion, packageName string) string {
|
||||
//deviceSign := generateDeviceSign(deviceID, packageName)
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("ANDROID-%s/%s ", appName, clientVersion))
|
||||
sb.WriteString("networkType/WIFI ")
|
||||
sb.WriteString(fmt.Sprintf("appid/%s ", "22062"))
|
||||
sb.WriteString(fmt.Sprintf("deviceName/Xiaomi_M2004j7ac "))
|
||||
sb.WriteString(fmt.Sprintf("deviceModel/M2004J7AC "))
|
||||
sb.WriteString(fmt.Sprintf("OSVersion/13 "))
|
||||
sb.WriteString(fmt.Sprintf("protocolVersion/301 "))
|
||||
sb.WriteString(fmt.Sprintf("platformversion/10 "))
|
||||
sb.WriteString(fmt.Sprintf("sdkVersion/%s ", sdkVersion))
|
||||
sb.WriteString(fmt.Sprintf("Oauth2Client/0.9 (Linux 4_9_337-perf-sn-uotan-gd9d488809c3d) (JAVA 0) "))
|
||||
return sb.String()
|
||||
}
|
558
drivers/thunderx/driver.go
Normal file
558
drivers/thunderx/driver.go
Normal file
@ -0,0 +1,558 @@
|
||||
package thunderx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
|
||||
"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"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ThunderX struct {
|
||||
*XunLeiXCommon
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
identity string
|
||||
}
|
||||
|
||||
func (x *ThunderX) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (x *ThunderX) GetAddition() driver.Additional {
|
||||
return &x.Addition
|
||||
}
|
||||
|
||||
func (x *ThunderX) Init(ctx context.Context) (err error) {
|
||||
// 初始化所需参数
|
||||
if x.XunLeiXCommon == nil {
|
||||
x.XunLeiXCommon = &XunLeiXCommon{
|
||||
Common: &Common{
|
||||
client: base.NewRestyClient(),
|
||||
Algorithms: Algorithms,
|
||||
DeviceID: utils.GetMD5EncodeStr(x.Username + x.Password),
|
||||
ClientID: ClientID,
|
||||
ClientSecret: ClientSecret,
|
||||
ClientVersion: ClientVersion,
|
||||
PackageName: PackageName,
|
||||
UserAgent: BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, ""),
|
||||
DownloadUserAgent: DownloadUserAgent,
|
||||
UseVideoUrl: x.UseVideoUrl,
|
||||
|
||||
refreshCTokenCk: func(token string) {
|
||||
x.CaptchaToken = token
|
||||
op.MustSaveDriverStorage(x)
|
||||
},
|
||||
},
|
||||
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()))
|
||||
if token.UserID != "" {
|
||||
x.SetUserID(token.UserID)
|
||||
x.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, token.UserID)
|
||||
}
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义验证码token
|
||||
ctoken := strings.TrimSpace(x.CaptchaToken)
|
||||
if ctoken != "" {
|
||||
x.SetCaptchaToken(ctoken)
|
||||
}
|
||||
if x.DeviceID == "" {
|
||||
x.SetDeviceID(utils.GetMD5EncodeStr(x.Username + x.Password))
|
||||
}
|
||||
|
||||
x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.Addition.RootFolderID = x.RootFolderID
|
||||
// 防止重复登录
|
||||
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)
|
||||
if token.UserID != "" {
|
||||
x.SetUserID(token.UserID)
|
||||
x.UserAgent = BuildCustomUserAgent(x.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, token.UserID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderX) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ThunderXExpert struct {
|
||||
*XunLeiXCommon
|
||||
model.Storage
|
||||
ExpertAddition
|
||||
|
||||
identity string
|
||||
}
|
||||
|
||||
func (x *ThunderXExpert) Config() driver.Config {
|
||||
return configExpert
|
||||
}
|
||||
|
||||
func (x *ThunderXExpert) GetAddition() driver.Additional {
|
||||
return &x.ExpertAddition
|
||||
}
|
||||
|
||||
func (x *ThunderXExpert) Init(ctx context.Context) (err error) {
|
||||
// 防止重复登录
|
||||
identity := x.GetIdentity()
|
||||
if identity != x.identity || !x.IsLogin() {
|
||||
x.identity = identity
|
||||
x.XunLeiXCommon = &XunLeiXCommon{
|
||||
Common: &Common{
|
||||
client: base.NewRestyClient(),
|
||||
|
||||
DeviceID: func() string {
|
||||
if len(x.DeviceID) != 32 {
|
||||
if x.LoginType == "user" {
|
||||
return utils.GetMD5EncodeStr(x.Username + x.Password)
|
||||
}
|
||||
return utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken)
|
||||
}
|
||||
return x.DeviceID
|
||||
}(),
|
||||
ClientID: x.ClientID,
|
||||
ClientSecret: x.ClientSecret,
|
||||
ClientVersion: x.ClientVersion,
|
||||
PackageName: x.PackageName,
|
||||
UserAgent: func() string {
|
||||
if x.ExpertAddition.UserAgent != "" {
|
||||
return x.ExpertAddition.UserAgent
|
||||
}
|
||||
if x.LoginType == "user" {
|
||||
return BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, "")
|
||||
}
|
||||
return BuildCustomUserAgent(utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, "")
|
||||
}(),
|
||||
DownloadUserAgent: func() string {
|
||||
if x.ExpertAddition.DownloadUserAgent != "" {
|
||||
return x.ExpertAddition.DownloadUserAgent
|
||||
}
|
||||
return DownloadUserAgent
|
||||
}(),
|
||||
UseVideoUrl: x.UseVideoUrl,
|
||||
refreshCTokenCk: func(token string) {
|
||||
x.CaptchaToken = token
|
||||
op.MustSaveDriverStorage(x)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if x.ExpertAddition.CaptchaToken != "" {
|
||||
x.SetCaptchaToken(x.ExpertAddition.CaptchaToken)
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
if x.Common.DeviceID != "" {
|
||||
x.ExpertAddition.DeviceID = x.Common.DeviceID
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
if x.Common.DownloadUserAgent != "" {
|
||||
x.ExpertAddition.DownloadUserAgent = x.Common.DownloadUserAgent
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.ExpertAddition.RootFolderID = x.RootFolderID
|
||||
// 签名方法
|
||||
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.XunLeiXCommon.RefreshToken(x.ExpertAddition.RefreshToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
// 刷新token方法
|
||||
x.SetRefreshTokenFunc(func() error {
|
||||
token, err := x.XunLeiXCommon.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.XunLeiXCommon.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
|
||||
})
|
||||
}
|
||||
// 更新 UserAgent
|
||||
if x.TokenResp.UserID != "" {
|
||||
x.ExpertAddition.UserAgent = BuildCustomUserAgent(x.ExpertAddition.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, x.TokenResp.UserID)
|
||||
x.SetUserAgent(x.ExpertAddition.UserAgent)
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
} else {
|
||||
// 仅修改验证码token
|
||||
if x.CaptchaToken != "" {
|
||||
x.SetCaptchaToken(x.CaptchaToken)
|
||||
}
|
||||
x.XunLeiXCommon.UserAgent = x.ExpertAddition.UserAgent
|
||||
x.XunLeiXCommon.DownloadUserAgent = x.ExpertAddition.UserAgent
|
||||
x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.ExpertAddition.RootFolderID = x.RootFolderID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderXExpert) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderXExpert) SetTokenResp(token *TokenResp) {
|
||||
x.XunLeiXCommon.SetTokenResp(token)
|
||||
if token != nil {
|
||||
x.ExpertAddition.RefreshToken = token.RefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
type XunLeiXCommon struct {
|
||||
*Common
|
||||
*TokenResp // 登录信息
|
||||
|
||||
refreshTokenFunc func() error
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
return xc.getFiles(ctx, dir.GetID())
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) 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},
|
||||
},
|
||||
}
|
||||
|
||||
if xc.UseVideoUrl {
|
||||
for _, media := range lFile.Medias {
|
||||
if media.Link.URL != "" {
|
||||
link.URL = media.Link.URL
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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 *XunLeiXCommon) 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 *XunLeiXCommon) 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 *XunLeiXCommon) 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 *XunLeiXCommon) 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 *XunLeiXCommon) 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 *XunLeiXCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
hi := stream.GetHash()
|
||||
gcid := hi.GetHash(hash_extend.GCID)
|
||||
if len(gcid) < hash_extend.GCID.Width {
|
||||
tFile, err := stream.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gcid, err = utils.HashFile(hash_extend.GCID, tFile, stream.GetSize())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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": gcid,
|
||||
"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
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
||||
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
|
||||
}
|
||||
_, err = uploader.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 *XunLeiXCommon) 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
|
||||
}
|
||||
|
||||
// SetRefreshTokenFunc 设置刷新Token的方法
|
||||
func (xc *XunLeiXCommon) SetRefreshTokenFunc(fn func() error) {
|
||||
xc.refreshTokenFunc = fn
|
||||
}
|
||||
|
||||
// SetTokenResp 设置Token
|
||||
func (xc *XunLeiXCommon) SetTokenResp(tr *TokenResp) {
|
||||
xc.TokenResp = tr
|
||||
}
|
||||
|
||||
// Request 携带Authorization和CaptchaToken的请求
|
||||
func (xc *XunLeiXCommon) 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)
|
||||
}
|
||||
|
||||
// RefreshToken 刷新Token
|
||||
func (xc *XunLeiXCommon) 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
|
||||
}
|
||||
resp.UserID = resp.Sub
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
func (xc *XunLeiXCommon) 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
|
||||
}
|
||||
resp.UserID = resp.Sub
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) IsLogin() bool {
|
||||
if xc.TokenResp == nil {
|
||||
return false
|
||||
}
|
||||
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
|
||||
return err == nil
|
||||
}
|
103
drivers/thunderx/meta.go
Normal file
103
drivers/thunderx/meta.go
Normal file
@ -0,0 +1,103 @@
|
||||
package thunderx
|
||||
|
||||
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:"kVy0WbPhiE4v6oxXZ88DvoA3Q,lON/AUoZKj8/nBtcE85mVbkOaVdVa,rLGffQrfBKH0BgwQ33yZofvO3Or,FO6HWqw,GbgvyA2,L1NU9QvIQIH7DTRt,y7llk4Y8WfYflt6,iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe,8C28RTXmVcco0,X5Xh,7xe25YUgfGgD0xW3ezFS,,CKCR,8EmDjBo6h3eLaK7U6vU2Qys0NsMx,t2TeZBXKqbdP09Arh9C3"`
|
||||
// 签名方法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:"false" default:""`
|
||||
ClientID string `json:"client_id" required:"true" default:"ZQL_zwA4qhHcoe_2"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"Og9Vr1L8Ee6bh0olFxFDRg"`
|
||||
ClientVersion string `json:"client_version" required:"true" default:"1.06.0.2132"`
|
||||
PackageName string `json:"package_name" required:"true" default:"com.thunder.downloader"`
|
||||
|
||||
////不影响登录,影响下载速度
|
||||
UserAgent string `json:"user_agent" required:"false" default:""`
|
||||
DownloadUserAgent string `json:"download_user_agent" required:"false" default:""`
|
||||
|
||||
//优先使用视频链接代替下载链接
|
||||
UseVideoUrl bool `json:"use_video_url"`
|
||||
}
|
||||
|
||||
// 登录特征,用于判断是否重新登录
|
||||
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"`
|
||||
UseVideoUrl bool `json:"use_video_url" default:"true"`
|
||||
}
|
||||
|
||||
// 登录特征,用于判断是否重新登录
|
||||
func (i *Addition) GetIdentity() string {
|
||||
return utils.GetMD5EncodeStr(i.Username + i.Password)
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "ThunderX",
|
||||
LocalSort: true,
|
||||
OnlyProxy: false,
|
||||
}
|
||||
|
||||
var configExpert = driver.Config{
|
||||
Name: "ThunderXExpert",
|
||||
LocalSort: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &ThunderX{}
|
||||
})
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &ThunderXExpert{}
|
||||
})
|
||||
}
|
206
drivers/thunderx/types.go
Normal file
206
drivers/thunderx/types.go
Normal file
@ -0,0 +1,206 @@
|
||||
package thunderx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
|
||||
)
|
||||
|
||||
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 Link struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
Expire time.Time `json:"expire"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
var _ model.Obj = (*Files)(nil)
|
||||
|
||||
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 map[string]Link `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 Link `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) GetHash() utils.HashInfo {
|
||||
return utils.NewHashInfo(hash_extend.GCID, c.Hash)
|
||||
}
|
||||
|
||||
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) CreateTime() time.Time { return c.CreatedTime }
|
||||
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"`
|
||||
}
|
297
drivers/thunderx/util.go
Normal file
297
drivers/thunderx/util.go
Normal file
@ -0,0 +1,297 @@
|
||||
package thunderx
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"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.xunleix.com/drive/v1"
|
||||
FILE_API_URL = API_URL + "/files"
|
||||
XLUSER_API_URL = "https://xluser-ssl.xunleix.com/v1"
|
||||
)
|
||||
|
||||
var Algorithms = []string{
|
||||
"kVy0WbPhiE4v6oxXZ88DvoA3Q",
|
||||
"lON/AUoZKj8/nBtcE85mVbkOaVdVa",
|
||||
"rLGffQrfBKH0BgwQ33yZofvO3Or",
|
||||
"FO6HWqw",
|
||||
"GbgvyA2",
|
||||
"L1NU9QvIQIH7DTRt",
|
||||
"y7llk4Y8WfYflt6",
|
||||
"iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe",
|
||||
"8C28RTXmVcco0",
|
||||
"X5Xh",
|
||||
"7xe25YUgfGgD0xW3ezFS",
|
||||
"",
|
||||
"CKCR",
|
||||
"8EmDjBo6h3eLaK7U6vU2Qys0NsMx",
|
||||
"t2TeZBXKqbdP09Arh9C3",
|
||||
}
|
||||
|
||||
const (
|
||||
ClientID = "ZQL_zwA4qhHcoe_2"
|
||||
ClientSecret = "Og9Vr1L8Ee6bh0olFxFDRg"
|
||||
ClientVersion = "1.06.0.2132"
|
||||
PackageName = "com.thunder.downloader"
|
||||
DownloadUserAgent = "Dalvik/2.1.0 (Linux; U; Android 13; M2004J7AC Build/SP1A.210812.016)"
|
||||
SdkVersion = "2.0.3.203100 "
|
||||
)
|
||||
|
||||
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
|
||||
userID string
|
||||
// 签名相关,二选一
|
||||
Algorithms []string
|
||||
Timestamp, CaptchaSign string
|
||||
|
||||
// 必要值,签名相关
|
||||
DeviceID string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ClientVersion string
|
||||
PackageName string
|
||||
UserAgent string
|
||||
DownloadUserAgent string
|
||||
UseVideoUrl bool
|
||||
|
||||
// 验证码token刷新成功回调
|
||||
refreshCTokenCk func(token string)
|
||||
}
|
||||
|
||||
func (c *Common) SetDeviceID(deviceID string) {
|
||||
c.DeviceID = deviceID
|
||||
}
|
||||
|
||||
func (c *Common) SetUserID(userID string) {
|
||||
c.userID = userID
|
||||
}
|
||||
|
||||
func (c *Common) SetUserAgent(userAgent string) {
|
||||
c.UserAgent = userAgent
|
||||
}
|
||||
|
||||
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.GetMD5EncodeStr(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://xbase.cloud/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: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
|
||||
}
|
||||
|
||||
if resp.CaptchaToken == "" {
|
||||
return fmt.Errorf("empty captchaToken")
|
||||
}
|
||||
|
||||
if c.refreshCTokenCk != nil {
|
||||
c.refreshCTokenCk(resp.CaptchaToken)
|
||||
}
|
||||
c.SetCaptchaToken(resp.CaptchaToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Request 只有基础信息的请求
|
||||
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
|
||||
}
|
||||
|
||||
// 计算文件Gcid
|
||||
func getGcid(r io.Reader, size int64) (string, error) {
|
||||
calcBlockSize := func(j int64) int64 {
|
||||
var psize int64 = 0x40000
|
||||
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
|
||||
psize = psize << 1
|
||||
}
|
||||
return psize
|
||||
}
|
||||
|
||||
hash1 := sha1.New()
|
||||
hash2 := sha1.New()
|
||||
readSize := calcBlockSize(size)
|
||||
for {
|
||||
hash2.Reset()
|
||||
if n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {
|
||||
if err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
}
|
||||
hash1.Write(hash2.Sum(nil))
|
||||
}
|
||||
return hex.EncodeToString(hash1.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func generateDeviceSign(deviceID, packageName string) string {
|
||||
|
||||
signatureBase := fmt.Sprintf("%s%s%s%s", deviceID, packageName, "1", "appkey")
|
||||
|
||||
sha1Hash := sha1.New()
|
||||
sha1Hash.Write([]byte(signatureBase))
|
||||
sha1Result := sha1Hash.Sum(nil)
|
||||
|
||||
sha1String := hex.EncodeToString(sha1Result)
|
||||
|
||||
md5Hash := md5.New()
|
||||
md5Hash.Write([]byte(sha1String))
|
||||
md5Result := md5Hash.Sum(nil)
|
||||
|
||||
md5String := hex.EncodeToString(md5Result)
|
||||
|
||||
deviceSign := fmt.Sprintf("div101.%s%s", deviceID, md5String)
|
||||
|
||||
return deviceSign
|
||||
}
|
||||
|
||||
func BuildCustomUserAgent(deviceID, clientID, appName, sdkVersion, clientVersion, packageName, userID string) string {
|
||||
deviceSign := generateDeviceSign(deviceID, packageName)
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("ANDROID-%s/%s ", appName, clientVersion))
|
||||
sb.WriteString("protocolVersion/200 ")
|
||||
sb.WriteString("accesstype/ ")
|
||||
sb.WriteString(fmt.Sprintf("clientid/%s ", clientID))
|
||||
sb.WriteString(fmt.Sprintf("clientversion/%s ", clientVersion))
|
||||
sb.WriteString("action_type/ ")
|
||||
sb.WriteString("networktype/WIFI ")
|
||||
sb.WriteString("sessionid/ ")
|
||||
sb.WriteString(fmt.Sprintf("deviceid/%s ", deviceID))
|
||||
sb.WriteString("providername/NONE ")
|
||||
sb.WriteString(fmt.Sprintf("devicesign/%s ", deviceSign))
|
||||
sb.WriteString("refresh_token/ ")
|
||||
sb.WriteString(fmt.Sprintf("sdkversion/%s ", sdkVersion))
|
||||
sb.WriteString(fmt.Sprintf("datetime/%d ", time.Now().UnixMilli()))
|
||||
sb.WriteString(fmt.Sprintf("usrno/%s ", userID))
|
||||
sb.WriteString(fmt.Sprintf("appname/%s ", appName))
|
||||
sb.WriteString(fmt.Sprintf("session_origin/ "))
|
||||
sb.WriteString(fmt.Sprintf("grant_type/ "))
|
||||
sb.WriteString(fmt.Sprintf("appid/ "))
|
||||
sb.WriteString(fmt.Sprintf("clientip/ "))
|
||||
sb.WriteString(fmt.Sprintf("devicename/Xiaomi_M2004j7ac "))
|
||||
sb.WriteString(fmt.Sprintf("osversion/13 "))
|
||||
sb.WriteString(fmt.Sprintf("platformversion/10 "))
|
||||
sb.WriteString(fmt.Sprintf("accessmode/ "))
|
||||
sb.WriteString(fmt.Sprintf("devicemodel/M2004J7AC "))
|
||||
|
||||
return sb.String()
|
||||
}
|
@ -5,12 +5,12 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/Xhofe/wopan-sdk-go"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/xhofe/wopan-sdk-go"
|
||||
)
|
||||
|
||||
type Wopan struct {
|
||||
|
@ -1,8 +1,8 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/wopan-sdk-go"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/xhofe/wopan-sdk-go"
|
||||
)
|
||||
|
||||
type Object struct {
|
||||
|
@ -3,7 +3,7 @@ package template
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/wopan-sdk-go"
|
||||
"github.com/xhofe/wopan-sdk-go"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
191
go.mod
191
go.mod
@ -1,93 +1,113 @@
|
||||
module github.com/alist-org/alist/v3
|
||||
|
||||
go 1.21
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
github.com/Mikubill/gofakes3 v0.0.3-0.20230622102024-284c0f988700
|
||||
github.com/SheltonZhu/115driver v1.0.22
|
||||
github.com/SheltonZhu/115driver v1.0.25
|
||||
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a
|
||||
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
|
||||
github.com/Xhofe/wopan-sdk-go v0.1.2
|
||||
github.com/alist-org/gofakes3 v0.0.6
|
||||
github.com/alist-org/times v0.0.0-20240721124318-c2e3da27cc69
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/avast/retry-go v3.0.0+incompatible
|
||||
github.com/aws/aws-sdk-go v1.50.24
|
||||
github.com/blevesearch/bleve/v2 v2.3.10
|
||||
github.com/aws/aws-sdk-go v1.54.19
|
||||
github.com/blevesearch/bleve/v2 v2.4.1
|
||||
github.com/caarlos0/env/v9 v9.0.0
|
||||
github.com/charmbracelet/bubbles v0.17.1
|
||||
github.com/charmbracelet/bubbletea v0.25.0
|
||||
github.com/charmbracelet/lipgloss v0.9.1
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/charmbracelet/bubbletea v0.26.6
|
||||
github.com/charmbracelet/lipgloss v0.12.1
|
||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240708163039-9a9b82a0ce4d
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/deckarep/golang-set/v2 v2.6.0
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/djherbis/times v1.6.0
|
||||
github.com/dlclark/regexp2 v1.10.0
|
||||
github.com/dlclark/regexp2 v1.11.2
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.5
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.6
|
||||
github.com/foxxorcat/weiyun-sdk-go v0.1.3
|
||||
github.com/gaoyb7/115drive-webdav v0.1.8
|
||||
github.com/gin-contrib/cors v1.5.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-resty/resty/v2 v2.11.0
|
||||
github.com/go-webauthn/webauthn v0.10.0
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-resty/resty/v2 v2.13.1
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hirochachacha/go-smb2 v1.1.0
|
||||
github.com/ipfs/go-ipfs-api v0.7.0
|
||||
github.com/jlaffaye/ftp v0.2.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.2.8
|
||||
github.com/maruel/natural v1.1.1
|
||||
github.com/meilisearch/meilisearch-go v0.26.1
|
||||
github.com/minio/sio v0.3.0
|
||||
github.com/meilisearch/meilisearch-go v0.27.0
|
||||
github.com/minio/sio v0.4.0
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
github.com/ncw/swift/v2 v2.0.2
|
||||
github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/rclone/rclone v1.63.1
|
||||
github.com/rclone/rclone v1.67.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7
|
||||
github.com/u2takey/ffmpeg-go v0.5.0
|
||||
github.com/upyun/go-sdk/v3 v3.0.4
|
||||
github.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5
|
||||
github.com/xhofe/tache v0.1.1
|
||||
golang.org/x/crypto v0.19.0
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/oauth2 v0.16.0
|
||||
github.com/xhofe/wopan-sdk-go v0.1.3
|
||||
github.com/zzzhr1990/go-common-entity v0.0.0-20221216044934-fd1c571e3a22
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/net v0.27.0
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/appengine v1.6.8
|
||||
gopkg.in/ldap.v3 v3.1.0
|
||||
gorm.io/driver/mysql v1.4.7
|
||||
gorm.io/driver/postgres v1.4.8
|
||||
gorm.io/driver/sqlite v1.4.4
|
||||
gorm.io/gorm v1.24.5
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/postgres v1.5.9
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.11
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.19 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.1.4 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.1.4 // indirect
|
||||
github.com/charmbracelet/x/input v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.1.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/ipfs/boxo v0.12.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd // indirect
|
||||
github.com/RoaringBitmap/roaring v1.2.3 // indirect
|
||||
github.com/RoaringBitmap/roaring v1.9.3 // indirect
|
||||
github.com/abbot/go-http-auth v0.4.0 // indirect
|
||||
github.com/aead/ecdh v0.2.0 // indirect
|
||||
github.com/andreburgaud/crypt2go v1.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/benbjohnson/clock v1.3.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.2.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.12.0 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.0.6 // indirect
|
||||
github.com/blevesearch/geo v0.1.18 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.1.9 // indirect
|
||||
github.com/blevesearch/geo v0.1.20 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.6 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.14 // indirect
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
||||
@ -99,62 +119,58 @@ require (
|
||||
github.com/blevesearch/zapx/v15 v15.3.13 // indirect
|
||||
github.com/bluele/gcache v0.0.2 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/bytedance/sonic v1.10.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/geoffgarside/ber v1.1.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.12 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.15.5 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.6 // indirect
|
||||
github.com/go-webauthn/x v0.1.9 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/ipfs/boxo v0.12.0 // indirect
|
||||
github.com/ipfs/go-cid v0.4.1 // indirect
|
||||
github.com/ipfs/go-cid v0.4.1
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.3.0 // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
github.com/jaevor/go-nanoid v1.3.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/klauspost/compress v1.17.8 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
|
||||
github.com/libp2p/go-libp2p v0.27.8 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@ -162,9 +178,8 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
@ -174,45 +189,45 @@ require (
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-multistream v0.4.1 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||
github.com/pquerna/cachecontrol v0.1.0 // indirect
|
||||
github.com/prometheus/client_golang v1.16.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rfjakob/eme v1.1.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.23.7 // indirect
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.4 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/u2takey/go-utils v0.3.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
golang.org/x/arch v0.5.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/term v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
google.golang.org/api v0.134.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect
|
||||
google.golang.org/grpc v1.57.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.8 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/term v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
google.golang.org/api v0.169.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
|
446
go.sum
446
go.sum
@ -1,30 +1,28 @@
|
||||
cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA=
|
||||
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
|
||||
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/compute v1.23.4 h1:EBT9Nw4q3zyE7G45Wvv3MzolIrCJEuHys5muLY0wvAw=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
|
||||
github.com/Mikubill/gofakes3 v0.0.3-0.20230622102024-284c0f988700 h1:r3fp2/Ro+0RtpjNY0/wsbN7vRmCW//dXTOZDQTct25Q=
|
||||
github.com/Mikubill/gofakes3 v0.0.3-0.20230622102024-284c0f988700/go.mod h1:OSXqXEGUe9CmPiwLMMnVrbXonMf4BeLBkBdLufxxiyY=
|
||||
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
|
||||
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
|
||||
github.com/SheltonZhu/115driver v1.0.22 h1:Wp8pN7/gK3YwEO5P18ggbIOHM++lo9eP/pBhuvXfI6U=
|
||||
github.com/SheltonZhu/115driver v1.0.22/go.mod h1:e3fPOBANbH/FsTya8FquJwOR3ErhCQgEab3q6CVY2k4=
|
||||
github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=
|
||||
github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/SheltonZhu/115driver v1.0.25 h1:i101yanLKUwV1Pi7x+vgNOwgz7Hp9JbNmo6BCZ9/4wo=
|
||||
github.com/SheltonZhu/115driver v1.0.25/go.mod h1:e3fPOBANbH/FsTya8FquJwOR3ErhCQgEab3q6CVY2k4=
|
||||
github.com/Unknwon/goconfig v1.0.0 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A=
|
||||
github.com/Unknwon/goconfig v1.0.0/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=
|
||||
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a h1:RenIAa2q4H8UcS/cqmwdT1WCWIAH5aumP8m8RpbqVsE=
|
||||
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a/go.mod h1:sSBbaOg90XwWKtpT56kVujF0bIeVITnPlssLclogS04=
|
||||
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4 h1:WnvifFgYyogPz2ZFvaVLk4gI/Co0paF92FmxSR6U1zY=
|
||||
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4/go.mod h1:8pWlL2rpusvx7Xa6yYaIWOJ8bR3gPdFBUT7OystyGOY=
|
||||
github.com/Xhofe/wopan-sdk-go v0.1.2 h1:6Gh4YTT7b7YHN0OoJ33j7Jm9ru/ckuvcDxPnRmH07jc=
|
||||
github.com/Xhofe/wopan-sdk-go v0.1.2/go.mod h1:ktLYb4t7rnPFq1AshLaPXq5kZER+DkEagT6/i/in0uo=
|
||||
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
|
||||
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
|
||||
github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=
|
||||
github.com/aead/ecdh v0.2.0/go.mod h1:a9HHtXuSo8J1Js1MwLQx2mBhkXMT6YwUmVVEY4tTB8U=
|
||||
github.com/alist-org/gofakes3 v0.0.6 h1:kenkDSqOIJt5ZDJ9KW91YkwplFXpfToPDjP3Bd6GZRg=
|
||||
github.com/alist-org/gofakes3 v0.0.6/go.mod h1:6IyGtYGIX29fLvtXo+XZhtwX2P33KVYYj8uTgAHSu58=
|
||||
github.com/alist-org/times v0.0.0-20240721124318-c2e3da27cc69 h1:E9QJ4vVTu1KYRhelnCsQImCsbl7NlkH3Yxs3/L2ldDk=
|
||||
github.com/alist-org/times v0.0.0-20240721124318-c2e3da27cc69/go.mod h1:oPJwGY3sLmGgcJamGumz//0A35f4BwQRacyqLNcJTOU=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andreburgaud/crypt2go v1.2.0 h1:oly/ENAodeqTYpUafgd4r3v+VKLQnmOKUyfpj+TxHbE=
|
||||
@ -34,32 +32,36 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
|
||||
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.50.24 h1:3o2Pg7mOoVL0jv54vWtuafoZqAeEXLhm1tltWA2GcEw=
|
||||
github.com/aws/aws-sdk-go v1.50.24/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aws/aws-sdk-go v1.54.19 h1:tyWV+07jagrNiCcGRzRhdtVjQs7Vy41NwsuOcl0IbVI=
|
||||
github.com/aws/aws-sdk-go v1.54.19/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
|
||||
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
|
||||
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||
github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg=
|
||||
github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA=
|
||||
github.com/blevesearch/bleve_index_api v1.0.6 h1:gyUUxdsrvmW3jVhhYdCVL6h9dCjNT/geNU7PxGn37p8=
|
||||
github.com/blevesearch/bleve_index_api v1.0.6/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms=
|
||||
github.com/blevesearch/geo v0.1.18 h1:Np8jycHTZ5scFe7VEPLrDoHnnb9C4j636ue/CGrhtDw=
|
||||
github.com/blevesearch/geo v0.1.18/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM=
|
||||
github.com/blevesearch/bleve/v2 v2.4.1 h1:8QWqsifq693mN3h6cSigKqkKUsUfv5hu0FDgz/4bFuA=
|
||||
github.com/blevesearch/bleve/v2 v2.4.1/go.mod h1:Ezmvsouspi+uVwnDzjIsCeUIT0WuBKlicP5JZnExWzo=
|
||||
github.com/blevesearch/bleve_index_api v1.1.9 h1:Cpq0Lp3As0Gfk3+PmcoNDRKeI50C5yuFNpj0YlN/bOE=
|
||||
github.com/blevesearch/bleve_index_api v1.1.9/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
|
||||
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
|
||||
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
|
||||
github.com/blevesearch/go-faiss v1.0.19 h1:UKoP8hS7DVsVSRRloNJb4qPfe2UQ99pP4D3oXd23g2A=
|
||||
github.com/blevesearch/go-faiss v1.0.19/go.mod h1:jrxHrbl42X/RnDPI+wBoZU8joxxuRwedrxqswQ3xfU8=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.6 h1:CdekX/Ob6YCYmeHzD72cKpwzBjvkOGegHOqhAkXp6yA=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.6/go.mod h1:nQQYlp51XvoSVxcciBjtvuHPIVjlWrN1hX4qwK2cqdc=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.14 h1:fgMLMpGWR7u2TdRm7XSZVWhPvMAcdYHh25Lq1fQ6Fjo=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.14/go.mod h1:B7+a7vfpY4NsjuTkpv/eY7RZ91Xr90VaJzT2t7upZN8=
|
||||
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||
@ -78,84 +80,102 @@ github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz7
|
||||
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
|
||||
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
|
||||
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
|
||||
github.com/blevesearch/zapx/v16 v16.1.4 h1:TBQfG77g2UUXwfjOVcEtB9pXkg6JBmGXkeZKI67+TiA=
|
||||
github.com/blevesearch/zapx/v16 v16.1.4/go.mod h1:+Q+Z89Iv7ewhdX2jyE6Qs/RUnN4tZuokaQ0xvTaFmx8=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
|
||||
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=
|
||||
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4=
|
||||
github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o=
|
||||
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
|
||||
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s=
|
||||
github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk=
|
||||
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
|
||||
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
|
||||
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
|
||||
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
|
||||
github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28=
|
||||
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
|
||||
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
|
||||
github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4=
|
||||
github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
|
||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764=
|
||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240708163039-9a9b82a0ce4d h1:p5T6ZPvh7nihJfjI9M/W2cbcX7n766u/OGorLmE4xoQ=
|
||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240708163039-9a9b82a0ce4d/go.mod h1:akxZg8LuwOIeCPRjcDrUS1WWcIwmLNSR2lfe4y85PH4=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg=
|
||||
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.2 h1:/u628IuisSTwri5/UKloiIsH8+qF2Pu7xEQX+yIKg68=
|
||||
github.com/dlclark/regexp2 v1.11.2/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJLNCEi7YHVMkwwtfSr2k9splgdSM=
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.5 h1:N3LqOvk2aWWxszsFIkArP5udIv74uTei/bH2jM3tfSc=
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.5/go.mod h1:iWHA2JFhzmKR28ySp1ON0g6DjLaYtvb5jhTqPVTDW9A=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.6 h1:6J37oI4wMZLj8EPgSCcSTTIbnI5D6RCNW/srX8vQd1Y=
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.6/go.mod h1:UaY6D88yBXWGrcu/PcyLWyL4lzrk5pSxSABPHftOvxs=
|
||||
github.com/foxxorcat/weiyun-sdk-go v0.1.3 h1:I5c5nfGErhq9DBumyjCVCggRA74jhgriMqRRFu5jeeY=
|
||||
github.com/foxxorcat/weiyun-sdk-go v0.1.3/go.mod h1:TPxzN0d2PahweUEHlOBWlwZSA+rELSUlGYMWgXRn9ps=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
|
||||
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gaoyb7/115drive-webdav v0.1.8 h1:EJt4PSmcbvBY4KUh2zSo5p6fN9LZFNkIzuKejipubVw=
|
||||
github.com/gaoyb7/115drive-webdav v0.1.8/go.mod h1:BKbeY6j8SKs3+rzBFFALznGxbPmefEm3vA+dGhqgOGU=
|
||||
github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w=
|
||||
github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||
github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
|
||||
github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI=
|
||||
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
@ -167,59 +187,63 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
|
||||
github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8=
|
||||
github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
||||
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
|
||||
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-webauthn/webauthn v0.10.0 h1:yuW2e1tXnRAwAvKrR4q4LQmc6XtCMH639/ypZGhZCwk=
|
||||
github.com/go-webauthn/webauthn v0.10.0/go.mod h1:l0NiauXhL6usIKqNLCUM3Qir43GK7ORg8ggold0Uv/Y=
|
||||
github.com/go-webauthn/x v0.1.6 h1:QNAX+AWeqRt9loE8mULeWJCqhVG5D/jvdmJ47fIWCkQ=
|
||||
github.com/go-webauthn/x v0.1.6/go.mod h1:W8dFVZ79o4f+nY1eOUICy/uq5dhrRl7mxQkYhXTo0FA=
|
||||
github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4=
|
||||
github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs=
|
||||
github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE=
|
||||
github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
|
||||
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
|
||||
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
|
||||
@ -235,14 +259,14 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA=
|
||||
github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
|
||||
github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jaevor/go-nanoid v1.3.0 h1:nD+iepesZS6pr3uOVf20vR9GdGgJW1HPaR46gtrxzkg=
|
||||
github.com/jaevor/go-nanoid v1.3.0/go.mod h1:SI+jFaPuddYkqkVQoNGHs81navCtH388TcrH0RqFKgY=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
|
||||
@ -259,14 +283,15 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
||||
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC9jFiTxyptEKuNIAbiN5ZCQzX2a74lj3xg=
|
||||
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
@ -279,9 +304,11 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.2.8 h1:elbufnS+gQVOkzX9JLkS/N9u3ay/IAIE17nE4kNoYZ4=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.2.8/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||
github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM=
|
||||
@ -291,8 +318,8 @@ github.com/libp2p/go-libp2p v0.27.8/go.mod h1:eCFFtd0s5i/EVKR7+5Ki8bM7qwkNW3TPTT
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed h1:036IscGBfJsFIgJQzlui7nK1Ncm0tp2ktmPj8xO4N/0=
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||
@ -301,23 +328,20 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/meilisearch/meilisearch-go v0.26.1 h1:3bmo2uLijX7kvBmiZ9LupVfC95TFcRJDgrRTzbOoE4A=
|
||||
github.com/meilisearch/meilisearch-go v0.26.1/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/meilisearch/meilisearch-go v0.27.0 h1:lDFq8WzbsZCtt3/byr7GFqfOygWF5iy9TtDgzJo0Ds8=
|
||||
github.com/meilisearch/meilisearch-go v0.27.0/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/minio/sio v0.3.0 h1:syEFBewzOMOYVzSTFpp1MqpSZk8rUNbz8VIIc+PNzus=
|
||||
github.com/minio/sio v0.3.0/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw=
|
||||
github.com/minio/sio v0.4.0 h1:u4SWVEm5lXSqU42ZWawV0D9I5AZ5YMmo2RXpEQ/kRhc=
|
||||
github.com/minio/sio v0.4.0/go.mod h1:oBSjJeGbBdRMZZwna07sX9EFzZy+ywu5aofRiV1g79I=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@ -333,12 +357,10 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||
@ -365,8 +387,8 @@ github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831 h1:K3T3eu
|
||||
github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831/go.mod h1:lSHD4lC4zlMl+zcoysdJcd5KFzsWwOD8BJbyg1Ws9Ng=
|
||||
github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
@ -377,8 +399,9 @@ github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
@ -386,33 +409,32 @@ github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8
|
||||
github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/rclone/rclone v1.63.1 h1:iITCUNBfAXnguHjRPFq+w/gGIW0L0las78h4H5CH2Ms=
|
||||
github.com/rclone/rclone v1.63.1/go.mod h1:eUQaKsf1wJfHKB0RDoM8RaPAeRB2eI/Qw+Vc9Ho5FGM=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rclone/rclone v1.67.0 h1:yLRNgHEG2vQ60HCuzFqd0hYwKCRuWuvPUhvhMJ2jI5E=
|
||||
github.com/rclone/rclone v1.67.0/go.mod h1:Cb3Ar47M/SvwfhAjZTbVXdtrP/JLtPFCq2tkdtBVC6w=
|
||||
github.com/rfjakob/eme v1.1.2 h1:SxziR8msSOElPayZNFfQw4Tjx/Sbaeeh3eRvrHVMUs4=
|
||||
github.com/rfjakob/eme v1.1.2/go.mod h1:cVvpasglm/G3ngEfcfT/Wt0GwhkuO32pf/poW6Nyk1k=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 h1:WnNuhiq+FOY3jNj6JXFT+eLN3CQ/oPIsDPRanvwsmbI=
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500/go.mod h1:+njLrG5wSeoG4Ds61rFgEzKvenR2UHbjMoDHsczxly0=
|
||||
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
|
||||
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIGMa7oSwp8SQPp7Hb4ZiI38K3RNBKD2LLeEM=
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
@ -427,13 +449,14 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:s
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@ -445,15 +468,17 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7 h1:Jtcrb09q0AVWe3BGe8qtuuGxNSHWGkTWr43kHTJ+CpA=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=
|
||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
|
||||
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
|
||||
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uklU=
|
||||
@ -462,8 +487,8 @@ github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4y
|
||||
github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/upyun/go-sdk/v3 v3.0.4 h1:2DCJa/Yi7/3ZybT9UCPATSzvU3wpPPxhXinNlb1Hi8Q=
|
||||
github.com/upyun/go-sdk/v3 v3.0.4/go.mod h1:P/SnuuwhrIgAVRd/ZpzDWqCsBAf/oHg7UggbAxyZa0E=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
@ -481,20 +506,36 @@ github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 h1:eDfebW/yfq9DtG9RO3K
|
||||
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25/go.mod h1:fH4oNm5F9NfI5dLi0oIMtsLNKQOirUDbEMCIBb/7SU0=
|
||||
github.com/xhofe/tache v0.1.1 h1:O5QY4cVjIGELx3UGh6LbVAc18MWGXgRNQjMt72x6w/8=
|
||||
github.com/xhofe/tache v0.1.1/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ=
|
||||
github.com/xhofe/wopan-sdk-go v0.1.3 h1:J58X6v+n25ewBZjb05pKOr7AWGohb+Rdll4CThGh6+A=
|
||||
github.com/xhofe/wopan-sdk-go v0.1.3/go.mod h1:dcY9yA28fnaoZPnXZiVTFSkcd7GnIPTpTIIlfSI5z5Q=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zzzhr1990/go-common-entity v0.0.0-20221216044934-fd1c571e3a22 h1:X+lHsNTlbatQ1cErXIbtyrh+3MTWxqQFS+sBP/wpFXo=
|
||||
github.com/zzzhr1990/go-common-entity v0.0.0-20221216044934-fd1c571e3a22/go.mod h1:1zGRDJd8zlG6P8azG96+uywfh6udYWwhOmUivw+xsuM=
|
||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
|
||||
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@ -503,19 +544,23 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
@ -525,17 +570,19 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -548,6 +595,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -557,22 +605,24 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@ -582,37 +632,41 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.134.0 h1:ktL4Goua+UBgoP1eL1/60LwZJqa1sIzkLmvoR3hR6Gw=
|
||||
google.golang.org/api v0.134.0/go.mod h1:sjRL3UnjTx5UqNQS9EWr9N8p7xbHpy1k0XGRLCf3Spk=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.169.0 h1:QwWPy71FgMWqJN/l6jVlFHUa29a7dcUy02I8o799nPY=
|
||||
google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 h1:eSaPbMR4T7WfH9FvABk36NBMacoTUKdWCvV0dx+KfOg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I=
|
||||
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
||||
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@ -635,17 +689,15 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
|
||||
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
|
||||
gorm.io/driver/postgres v1.4.8 h1:NDWizaclb7Q2aupT0jkwK8jx1HVCNzt+PQ8v/VnxviA=
|
||||
gorm.io/driver/postgres v1.4.8/go.mod h1:O9MruWGNLUBUWVYfWuBClpf3HeGjOoybY0SNmCs3wsw=
|
||||
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
|
||||
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE=
|
||||
gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
|
||||
lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
@ -131,7 +131,7 @@ func InitialSettings() []model.SettingItem {
|
||||
// global settings
|
||||
{Key: conf.HideFiles, Value: "/\\/README.md/i", Type: conf.TypeText, Group: model.GLOBAL},
|
||||
{Key: "package_download", Value: "true", Type: conf.TypeBool, Group: model.GLOBAL},
|
||||
{Key: conf.CustomizeHead, PreDefault: `<script src="https://polyfill.io/v3/polyfill.min.js?features=String.prototype.replaceAll"></script>`, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.CustomizeHead, PreDefault: `<script src="https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=String.prototype.replaceAll"></script>`, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.CustomizeBody, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.LinkExpiration, Value: "0", Type: conf.TypeNumber, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.SignAll, Value: "true", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
|
@ -12,6 +12,7 @@ type Config struct {
|
||||
CheckStatus bool `json:"-"`
|
||||
Alert string `json:"alert"` //info,success,warning,danger
|
||||
NoOverwriteUpload bool `json:"-"` // whether to support overwrite upload
|
||||
ProxyRangeOption bool `json:"-"`
|
||||
}
|
||||
|
||||
func (c Config) MustProxy() bool {
|
||||
|
@ -3,6 +3,7 @@ package errs
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
pkgerr "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -33,3 +34,6 @@ func IsNotFoundError(err error) bool {
|
||||
func IsNotSupportError(err error) bool {
|
||||
return errors.Is(pkgerr.Cause(err), NotSupport)
|
||||
}
|
||||
func IsNotImplement(err error) bool {
|
||||
return errors.Is(pkgerr.Cause(err), NotImplement)
|
||||
}
|
||||
|
@ -24,7 +24,8 @@ func list(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error)
|
||||
if storage != nil {
|
||||
_objs, err = op.List(ctx, storage, actualPath, model.ListArgs{
|
||||
ReqPath: path,
|
||||
}, args.Refresh)
|
||||
Refresh: args.Refresh,
|
||||
})
|
||||
if err != nil {
|
||||
if !args.NoLog {
|
||||
log.Errorf("fs/list: %+v", err)
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
type ListArgs struct {
|
||||
ReqPath string
|
||||
S3ShowPlaceholder bool
|
||||
Refresh bool
|
||||
}
|
||||
|
||||
type LinkArgs struct {
|
||||
|
@ -27,6 +27,7 @@ type Sort struct {
|
||||
type Proxy struct {
|
||||
WebProxy bool `json:"web_proxy"`
|
||||
WebdavPolicy string `json:"webdav_policy"`
|
||||
ProxyRange bool `json:"proxy_range"`
|
||||
DownProxyUrl string `json:"down_proxy_url"`
|
||||
}
|
||||
|
||||
|
@ -3,5 +3,6 @@ package offline_download
|
||||
import (
|
||||
_ "github.com/alist-org/alist/v3/internal/offline_download/aria2"
|
||||
_ "github.com/alist-org/alist/v3/internal/offline_download/http"
|
||||
_ "github.com/alist-org/alist/v3/internal/offline_download/pikpak"
|
||||
_ "github.com/alist-org/alist/v3/internal/offline_download/qbit"
|
||||
)
|
||||
|
120
internal/offline_download/pikpak/pikpak.go
Normal file
120
internal/offline_download/pikpak/pikpak.go
Normal file
@ -0,0 +1,120 @@
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/pikpak"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/offline_download/tool"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type PikPak struct {
|
||||
refreshTaskCache bool
|
||||
}
|
||||
|
||||
func (p *PikPak) Name() string {
|
||||
return "pikpak"
|
||||
}
|
||||
|
||||
func (p *PikPak) Items() []model.SettingItem {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PikPak) Run(task *tool.DownloadTask) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (p *PikPak) Init() (string, error) {
|
||||
p.refreshTaskCache = false
|
||||
return "ok", nil
|
||||
}
|
||||
|
||||
func (p *PikPak) IsReady() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *PikPak) AddURL(args *tool.AddUrlArgs) (string, error) {
|
||||
// 添加新任务刷新缓存
|
||||
p.refreshTaskCache = true
|
||||
// args.TempDir 已经被修改为了 DstDirPath
|
||||
storage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pikpakDriver, ok := storage.(*pikpak.PikPak)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unsupported storage driver for offline download, only Pikpak is supported")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
parentDir, err := op.GetUnwrap(ctx, storage, actualPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
t, err := pikpakDriver.OfflineDownload(ctx, args.Url, parentDir, "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to add offline download task: %w", err)
|
||||
}
|
||||
|
||||
return t.ID, nil
|
||||
}
|
||||
|
||||
func (p *PikPak) Remove(task *tool.DownloadTask) error {
|
||||
storage, _, err := op.GetStorageAndActualPath(task.DstDirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pikpakDriver, ok := storage.(*pikpak.PikPak)
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported storage driver for offline download, only Pikpak is supported")
|
||||
}
|
||||
ctx := context.Background()
|
||||
err = pikpakDriver.DeleteOfflineTasks(ctx, []string{task.GID}, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PikPak) Status(task *tool.DownloadTask) (*tool.Status, error) {
|
||||
storage, _, err := op.GetStorageAndActualPath(task.DstDirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pikpakDriver, ok := storage.(*pikpak.PikPak)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported storage driver for offline download, only Pikpak is supported")
|
||||
}
|
||||
tasks, err := p.GetTasks(pikpakDriver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &tool.Status{
|
||||
Progress: 0,
|
||||
NewGID: "",
|
||||
Completed: false,
|
||||
Status: "the task has been deleted",
|
||||
Err: nil,
|
||||
}
|
||||
for _, t := range tasks {
|
||||
if t.ID == task.GID {
|
||||
s.Progress = float64(t.Progress)
|
||||
s.Status = t.Message
|
||||
s.Completed = (t.Phase == "PHASE_TYPE_COMPLETE")
|
||||
if t.Phase == "PHASE_TYPE_ERROR" {
|
||||
s.Err = fmt.Errorf(t.Message)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
s.Err = fmt.Errorf("the task has been deleted")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
tool.Tools.Add(&PikPak{})
|
||||
}
|
43
internal/offline_download/pikpak/util.go
Normal file
43
internal/offline_download/pikpak/util.go
Normal file
@ -0,0 +1,43 @@
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/go-cache"
|
||||
"github.com/alist-org/alist/v3/drivers/pikpak"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/singleflight"
|
||||
)
|
||||
|
||||
var taskCache = cache.NewMemCache(cache.WithShards[[]pikpak.OfflineTask](16))
|
||||
var taskG singleflight.Group[[]pikpak.OfflineTask]
|
||||
|
||||
func (p *PikPak) GetTasks(pikpakDriver *pikpak.PikPak) ([]pikpak.OfflineTask, error) {
|
||||
key := op.Key(pikpakDriver, "/drive/v1/task")
|
||||
if !p.refreshTaskCache {
|
||||
if tasks, ok := taskCache.Get(key); ok {
|
||||
return tasks, nil
|
||||
}
|
||||
}
|
||||
p.refreshTaskCache = false
|
||||
tasks, err, _ := taskG.Do(key, func() ([]pikpak.OfflineTask, error) {
|
||||
ctx := context.Background()
|
||||
phase := []string{"PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR", "PHASE_TYPE_PENDING", "PHASE_TYPE_COMPLETE"}
|
||||
tasks, err := pikpakDriver.OfflineList(ctx, "", phase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 添加缓存 10s
|
||||
if len(tasks) > 0 {
|
||||
taskCache.Set(key, tasks, cache.WithEx[[]pikpak.OfflineTask](time.Second*10))
|
||||
} else {
|
||||
taskCache.Del(key)
|
||||
}
|
||||
return tasks, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
@ -2,13 +2,14 @@ package tool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/xhofe/tache"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type DeletePolicy string
|
||||
@ -64,11 +65,17 @@ func AddURL(ctx context.Context, args *AddURLArgs) (tache.TaskWithInfo, error) {
|
||||
|
||||
uid := uuid.NewString()
|
||||
tempDir := filepath.Join(conf.Conf.TempDir, args.Tool, uid)
|
||||
deletePolicy := args.DeletePolicy
|
||||
if args.Tool == "pikpak" {
|
||||
tempDir = args.DstDirPath
|
||||
// 防止将下载好的文件删除
|
||||
deletePolicy = DeleteNever
|
||||
}
|
||||
t := &DownloadTask{
|
||||
Url: args.URL,
|
||||
DstDirPath: args.DstDirPath,
|
||||
TempDir: tempDir,
|
||||
DeletePolicy: args.DeletePolicy,
|
||||
DeletePolicy: deletePolicy,
|
||||
tool: tool,
|
||||
}
|
||||
DownloadTaskManager.Add(t)
|
||||
|
@ -71,6 +71,9 @@ outer:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.tool.Name() == "pikpak" {
|
||||
return nil
|
||||
}
|
||||
t.Status = "offline download completed, maybe transferring"
|
||||
// hack for qBittorrent
|
||||
if t.tool.Name() == "qBittorrent" {
|
||||
@ -123,6 +126,9 @@ func (t *DownloadTask) Complete() error {
|
||||
files []File
|
||||
err error
|
||||
)
|
||||
if t.tool.Name() == "pikpak" {
|
||||
return nil
|
||||
}
|
||||
if getFileser, ok := t.tool.(GetFileser); ok {
|
||||
files = getFileser.GetFiles(t)
|
||||
} else {
|
||||
@ -132,7 +138,7 @@ func (t *DownloadTask) Complete() error {
|
||||
}
|
||||
}
|
||||
// upload files
|
||||
for i, _ := range files {
|
||||
for i := range files {
|
||||
file := files[i]
|
||||
TransferTaskManager.Add(&TransferTask{
|
||||
file: file,
|
||||
|
@ -93,6 +93,17 @@ func getMainItems(config driver.Config) []driver.Item {
|
||||
Required: true,
|
||||
},
|
||||
}...)
|
||||
if config.ProxyRangeOption {
|
||||
item := driver.Item{
|
||||
Name: "proxy_range",
|
||||
Type: conf.TypeBool,
|
||||
Help: "Need to enable proxy",
|
||||
}
|
||||
if config.Name == "139Yun" {
|
||||
item.Default = "true"
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
} else {
|
||||
items = append(items, driver.Item{
|
||||
Name: "webdav_policy",
|
||||
|
@ -100,14 +100,14 @@ func Key(storage driver.Driver, path string) string {
|
||||
}
|
||||
|
||||
// List files in storage, not contains virtual file
|
||||
func List(ctx context.Context, storage driver.Driver, path string, args model.ListArgs, refresh ...bool) ([]model.Obj, error) {
|
||||
func List(ctx context.Context, storage driver.Driver, path string, args model.ListArgs) ([]model.Obj, error) {
|
||||
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
|
||||
return nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
|
||||
}
|
||||
path = utils.FixAndCleanPath(path)
|
||||
log.Debugf("op.List %s", path)
|
||||
key := Key(storage, path)
|
||||
if !utils.IsBool(refresh...) {
|
||||
if !args.Refresh {
|
||||
if files, ok := listCache.Get(key); ok {
|
||||
log.Debugf("use cache when list %s", path)
|
||||
return files, nil
|
||||
|
@ -60,7 +60,6 @@ func TestGetStorageVirtualFilesByPath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetBalancedStorage(t *testing.T) {
|
||||
setupStorages(t)
|
||||
set := mapset.NewSet[string]()
|
||||
for i := 0; i < 5; i++ {
|
||||
storage := op.GetBalancedStorage("/a/d/e1")
|
||||
|
@ -51,7 +51,11 @@ func (f *FileStream) IsForceStreamUpload() bool {
|
||||
|
||||
func (f *FileStream) Close() error {
|
||||
var err1, err2 error
|
||||
|
||||
err1 = f.Closers.Close()
|
||||
if errors.Is(err1, os.ErrClosed) {
|
||||
err1 = nil
|
||||
}
|
||||
if f.tmpFile != nil {
|
||||
err2 = os.RemoveAll(f.tmpFile.Name())
|
||||
if err2 != nil {
|
||||
|
@ -138,7 +138,7 @@ func (mr *MultiReadable) Close() error {
|
||||
|
||||
func Retry(attempts int, sleep time.Duration, f func() error) (err error) {
|
||||
for i := 0; i < attempts; i++ {
|
||||
fmt.Println("This is attempt number", i)
|
||||
//fmt.Println("This is attempt number", i)
|
||||
if i > 0 {
|
||||
log.Println("retrying after error:", err)
|
||||
time.Sleep(sleep)
|
||||
|
15
pkg/utils/oauth2.go
Normal file
15
pkg/utils/oauth2.go
Normal file
@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import "golang.org/x/oauth2"
|
||||
|
||||
type tokenSource struct {
|
||||
fn func() (*oauth2.Token, error)
|
||||
}
|
||||
|
||||
func (t *tokenSource) Token() (*oauth2.Token, error) {
|
||||
return t.fn()
|
||||
}
|
||||
|
||||
func TokenSource(fn func() (*oauth2.Token, error)) oauth2.TokenSource {
|
||||
return &tokenSource{fn}
|
||||
}
|
@ -9,8 +9,10 @@ import (
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/net"
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.Obj) error {
|
||||
@ -82,3 +84,21 @@ func attachFileName(w http.ResponseWriter, file model.Obj) {
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, fileName, url.PathEscape(fileName)))
|
||||
w.Header().Set("Content-Type", utils.GetMimeType(fileName))
|
||||
}
|
||||
|
||||
var NoProxyRange = &model.RangeReadCloser{}
|
||||
|
||||
func ProxyRange(link *model.Link, size int64) {
|
||||
if link.MFile != nil {
|
||||
return
|
||||
}
|
||||
if link.RangeReadCloser == nil {
|
||||
var rrc, err = stream.GetRangeReadCloserFromLink(size, link)
|
||||
if err != nil {
|
||||
log.Warnf("ProxyRange error: %s", err)
|
||||
return
|
||||
}
|
||||
link.RangeReadCloser = rrc
|
||||
} else if link.RangeReadCloser == NoProxyRange {
|
||||
link.RangeReadCloser = nil
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +106,9 @@ func Proxy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if storage.GetStorage().ProxyRange {
|
||||
common.ProxyRange(link, file.GetSize())
|
||||
}
|
||||
err = common.Proxy(c.Writer, c.Request, link, file)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
|
@ -6,13 +6,13 @@ import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Mikubill/gofakes3"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
@ -20,12 +20,14 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/gofakes3"
|
||||
"github.com/ncw/swift/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
emptyPrefix = &gofakes3.Prefix{}
|
||||
timeFormat = "Mon, 2 Jan 2006 15:04:05.999999999 GMT"
|
||||
timeFormat = "Mon, 2 Jan 2006 15:04:05 GMT"
|
||||
)
|
||||
|
||||
// s3Backend implements the gofacess3.Backend interface to make an S3
|
||||
@ -42,13 +44,12 @@ func newBackend() gofakes3.Backend {
|
||||
}
|
||||
|
||||
// ListBuckets always returns the default bucket.
|
||||
func (b *s3Backend) ListBuckets() ([]gofakes3.BucketInfo, error) {
|
||||
func (b *s3Backend) ListBuckets(ctx context.Context) ([]gofakes3.BucketInfo, error) {
|
||||
buckets, err := getAndParseBuckets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response []gofakes3.BucketInfo
|
||||
ctx := context.Background()
|
||||
for _, b := range buckets {
|
||||
node, _ := fs.Get(ctx, b.Path, &fs.GetArgs{})
|
||||
response = append(response, gofakes3.BucketInfo{
|
||||
@ -61,7 +62,7 @@ func (b *s3Backend) ListBuckets() ([]gofakes3.BucketInfo, error) {
|
||||
}
|
||||
|
||||
// ListBucket lists the objects in the given bucket.
|
||||
func (b *s3Backend) ListBucket(bucketName string, prefix *gofakes3.Prefix, page gofakes3.ListBucketPage) (*gofakes3.ObjectList, error) {
|
||||
func (b *s3Backend) ListBucket(ctx context.Context, bucketName string, prefix *gofakes3.Prefix, page gofakes3.ListBucketPage) (*gofakes3.ObjectList, error) {
|
||||
bucket, err := getBucketByName(bucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -97,8 +98,7 @@ func (b *s3Backend) ListBucket(bucketName string, prefix *gofakes3.Prefix, page
|
||||
// HeadObject returns the fileinfo for the given object name.
|
||||
//
|
||||
// Note that the metadata is not supported yet.
|
||||
func (b *s3Backend) HeadObject(bucketName, objectName string) (*gofakes3.Object, error) {
|
||||
ctx := context.Background()
|
||||
func (b *s3Backend) HeadObject(ctx context.Context, bucketName, objectName string) (*gofakes3.Object, error) {
|
||||
bucket, err := getBucketByName(bucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -141,8 +141,7 @@ func (b *s3Backend) HeadObject(bucketName, objectName string) (*gofakes3.Object,
|
||||
}
|
||||
|
||||
// GetObject fetchs the object from the filesystem.
|
||||
func (b *s3Backend) GetObject(bucketName, objectName string, rangeRequest *gofakes3.ObjectRangeRequest) (obj *gofakes3.Object, err error) {
|
||||
ctx := context.Background()
|
||||
func (b *s3Backend) GetObject(ctx context.Context, bucketName, objectName string, rangeRequest *gofakes3.ObjectRangeRequest) (obj *gofakes3.Object, err error) {
|
||||
bucket, err := getBucketByName(bucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -251,18 +250,17 @@ func (b *s3Backend) GetObject(bucketName, objectName string, rangeRequest *gofak
|
||||
}
|
||||
|
||||
// TouchObject creates or updates meta on specified object.
|
||||
func (b *s3Backend) TouchObject(fp string, meta map[string]string) (result gofakes3.PutObjectResult, err error) {
|
||||
func (b *s3Backend) TouchObject(ctx context.Context, fp string, meta map[string]string) (result gofakes3.PutObjectResult, err error) {
|
||||
//TODO: implement
|
||||
return result, gofakes3.ErrNotImplemented
|
||||
}
|
||||
|
||||
// PutObject creates or overwrites the object with the given name.
|
||||
func (b *s3Backend) PutObject(
|
||||
bucketName, objectName string,
|
||||
ctx context.Context, bucketName, objectName string,
|
||||
meta map[string]string,
|
||||
input io.Reader, size int64,
|
||||
) (result gofakes3.PutObjectResult, err error) {
|
||||
ctx := context.Background()
|
||||
bucket, err := getBucketByName(bucketName)
|
||||
if err != nil {
|
||||
return result, err
|
||||
@ -272,9 +270,19 @@ func (b *s3Backend) PutObject(
|
||||
fp := path.Join(bucketPath, objectName)
|
||||
reqPath := path.Dir(fp)
|
||||
fmeta, _ := op.GetNearestMeta(fp)
|
||||
_, err = fs.Get(context.WithValue(ctx, "meta", fmeta), reqPath, &fs.GetArgs{})
|
||||
ctx = context.WithValue(ctx, "meta", fmeta)
|
||||
|
||||
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{})
|
||||
if err != nil {
|
||||
return result, gofakes3.KeyNotFound(objectName)
|
||||
if errs.IsObjectNotFound(err) && strings.Contains(objectName, "/") {
|
||||
log.Debugf("reqPath: %s not found and objectName contains /, need to makeDir", reqPath)
|
||||
err = fs.MakeDir(ctx, reqPath, true)
|
||||
if err != nil {
|
||||
return result, errors.WithMessagef(err, "failed to makeDir, reqPath: %s", reqPath)
|
||||
}
|
||||
} else {
|
||||
return result, gofakes3.KeyNotFound(objectName)
|
||||
}
|
||||
}
|
||||
|
||||
var ti time.Time
|
||||
@ -316,9 +324,9 @@ func (b *s3Backend) PutObject(
|
||||
}
|
||||
|
||||
// DeleteMulti deletes multiple objects in a single request.
|
||||
func (b *s3Backend) DeleteMulti(bucketName string, objects ...string) (result gofakes3.MultiDeleteResult, rerr error) {
|
||||
func (b *s3Backend) DeleteMulti(ctx context.Context, bucketName string, objects ...string) (result gofakes3.MultiDeleteResult, rerr error) {
|
||||
for _, object := range objects {
|
||||
if err := b.deleteObject(bucketName, object); err != nil {
|
||||
if err := b.deleteObject(ctx, bucketName, object); err != nil {
|
||||
utils.Log.Errorf("serve s3", "delete object failed: %v", err)
|
||||
result.Error = append(result.Error, gofakes3.ErrorResult{
|
||||
Code: gofakes3.ErrInternal,
|
||||
@ -336,13 +344,12 @@ func (b *s3Backend) DeleteMulti(bucketName string, objects ...string) (result go
|
||||
}
|
||||
|
||||
// DeleteObject deletes the object with the given name.
|
||||
func (b *s3Backend) DeleteObject(bucketName, objectName string) (result gofakes3.ObjectDeleteResult, rerr error) {
|
||||
return result, b.deleteObject(bucketName, objectName)
|
||||
func (b *s3Backend) DeleteObject(ctx context.Context, bucketName, objectName string) (result gofakes3.ObjectDeleteResult, rerr error) {
|
||||
return result, b.deleteObject(ctx, bucketName, objectName)
|
||||
}
|
||||
|
||||
// deleteObject deletes the object from the filesystem.
|
||||
func (b *s3Backend) deleteObject(bucketName, objectName string) error {
|
||||
ctx := context.Background()
|
||||
func (b *s3Backend) deleteObject(ctx context.Context, bucketName, objectName string) error {
|
||||
bucket, err := getBucketByName(bucketName)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -362,17 +369,17 @@ func (b *s3Backend) deleteObject(bucketName, objectName string) error {
|
||||
}
|
||||
|
||||
// CreateBucket creates a new bucket.
|
||||
func (b *s3Backend) CreateBucket(name string) error {
|
||||
func (b *s3Backend) CreateBucket(ctx context.Context, name string) error {
|
||||
return gofakes3.ErrNotImplemented
|
||||
}
|
||||
|
||||
// DeleteBucket deletes the bucket with the given name.
|
||||
func (b *s3Backend) DeleteBucket(name string) error {
|
||||
func (b *s3Backend) DeleteBucket(ctx context.Context, name string) error {
|
||||
return gofakes3.ErrNotImplemented
|
||||
}
|
||||
|
||||
// BucketExists checks if the bucket exists.
|
||||
func (b *s3Backend) BucketExists(name string) (exists bool, err error) {
|
||||
func (b *s3Backend) BucketExists(ctx context.Context, name string) (exists bool, err error) {
|
||||
buckets, err := getAndParseBuckets()
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -386,13 +393,12 @@ func (b *s3Backend) BucketExists(name string) (exists bool, err error) {
|
||||
}
|
||||
|
||||
// CopyObject copy specified object from srcKey to dstKey.
|
||||
func (b *s3Backend) CopyObject(srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (result gofakes3.CopyObjectResult, err error) {
|
||||
func (b *s3Backend) CopyObject(ctx context.Context, srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (result gofakes3.CopyObjectResult, err error) {
|
||||
if srcBucket == dstBucket && srcKey == dstKey {
|
||||
//TODO: update meta
|
||||
return result, nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
srcB, err := getBucketByName(srcBucket)
|
||||
if err != nil {
|
||||
return result, err
|
||||
@ -403,7 +409,7 @@ func (b *s3Backend) CopyObject(srcBucket, srcKey, dstBucket, dstKey string, meta
|
||||
fmeta, _ := op.GetNearestMeta(srcFp)
|
||||
srcNode, err := fs.Get(context.WithValue(ctx, "meta", fmeta), srcFp, &fs.GetArgs{})
|
||||
|
||||
c, err := b.GetObject(srcBucket, srcKey, nil)
|
||||
c, err := b.GetObject(ctx, srcBucket, srcKey, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -420,7 +426,7 @@ func (b *s3Backend) CopyObject(srcBucket, srcKey, dstBucket, dstKey string, meta
|
||||
meta["mtime"] = swift.TimeToFloatString(srcNode.ModTime())
|
||||
}
|
||||
|
||||
_, err = b.PutObject(dstBucket, dstKey, meta, c.Contents, c.Size)
|
||||
_, err = b.PutObject(ctx, dstBucket, dstKey, meta, c.Contents, c.Size)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user