Compare commits
115 Commits
Author | SHA1 | Date | |
---|---|---|---|
6c91cfeb90 | |||
bfd1f25972 | |||
8c0defce09 | |||
a1e88cfa05 | |||
443f5ffbcc | |||
b8bc94306d | |||
d9795ff22f | |||
c4108007cd | |||
f3db23a41e | |||
4741a75c92 | |||
301756ba03 | |||
3b2703a5e5 | |||
2a601f06cb | |||
adc3a56552 | |||
4d9a29bddd | |||
666e02f0c3 | |||
6aaec19c1c | |||
1091e1b740 | |||
d06c605421 | |||
43de823058 | |||
02d0aef611 | |||
5596661ce8 | |||
2379cb8d67 | |||
8c0ebe0841 | |||
fd868bac84 | |||
ebcbb29a0f | |||
00ff0a43a7 | |||
3d3f23ec9e | |||
d484219c48 | |||
dd4c97393e | |||
07b8ff25a7 | |||
0d5c3c5080 | |||
75b4429f73 | |||
34ef6bd18d | |||
c915313ec9 | |||
12a095a1d6 | |||
dc000f640a | |||
aa1c5b2be3 | |||
1d4ec3c50d | |||
ebfeef52f4 | |||
c595fd7f94 | |||
421052f88a | |||
603681fbe6 | |||
f442185aa5 | |||
ca9e739465 | |||
53a1c4283b | |||
93dd768234 | |||
c9c4d6bc7e | |||
81e10f8939 | |||
4dd753de52 | |||
79df63d319 | |||
ec54831162 | |||
c8f3e8ab4d | |||
4be8524d80 | |||
0d3146b51d | |||
f95d843969 | |||
28aee8c493 | |||
de3ea82eb9 | |||
268ba3d069 | |||
309d6558fb | |||
c08fdfc868 | |||
1b28e6af3e | |||
8655e33e60 | |||
50579fef84 | |||
e39299bfe2 | |||
d1ab2443f1 | |||
658cf368bb | |||
fd36ce59f6 | |||
95b3b87672 | |||
0d07d81802 | |||
923937b530 | |||
09492193c4 | |||
40b26a81a0 | |||
4293a0ba8c | |||
6c2f3486fc | |||
3c7512f64a | |||
84219d3d70 | |||
05d3727335 | |||
ee77c3b113 | |||
fcaf485e0b | |||
bd83469bb1 | |||
90f111b24f | |||
7d1034c569 | |||
236c17176c | |||
6ee4c10e8f | |||
3798634028 | |||
567ba5ccd4 | |||
ae2ee1821a | |||
805b1e4fa3 | |||
d92c10da56 | |||
6659f6d367 | |||
fe416ba15c | |||
de66708b24 | |||
2ca3e0b8bc | |||
ae04a0a760 | |||
c28168c970 | |||
46b2ed2507 | |||
22843ffc70 | |||
e1b6368343 | |||
62dae50d70 | |||
43a8ed472b | |||
d87878c232 | |||
ab7dee49b0 | |||
dca115506d | |||
be17fba0c6 | |||
cd58aa5efe | |||
946833d2cc | |||
eb42d09849 | |||
9d00492750 | |||
b6711d6ab9 | |||
7bc46de8aa | |||
a4f4fb2d73 | |||
a181b56ea7 | |||
d0b743d955 | |||
a985b748e9 |
19
.github/stale.yml
vendored
Normal file
19
.github/stale.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 44
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 8
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- accepted
|
||||
- security
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: >
|
||||
This issue was closed due to inactive more than 52 days. You can reopen or
|
||||
recreate it if you think it should continue. Thank you for your contributions again.
|
4
.github/workflows/auto_lang.yml
vendored
4
.github/workflows/auto_lang.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
@ -54,7 +54,7 @@ jobs:
|
||||
cd alist-web
|
||||
git add .
|
||||
git config --local user.email "i@nn.ci"
|
||||
git config --local user.name "Noah Hsu"
|
||||
git config --local user.name "Andy Hsu"
|
||||
git commit -m "chore: auto update i18n file" -a 2>/dev/null || :
|
||||
cd ..
|
||||
|
||||
|
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
@ -25,6 +25,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo snap install zig --classic --beta
|
||||
docker pull crazymax/xgo:latest
|
||||
go install github.com/crazy-max/xgo@latest
|
||||
sudo apt install upx
|
||||
|
2
.github/workflows/build_docker.yml
vendored
2
.github/workflows/build_docker.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
19
.github/workflows/changelog.yml
vendored
Normal file
19
.github/workflows/changelog.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
name: auto changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.MY_TOKEN}}
|
17
.github/workflows/issue_check_inactive.yml
vendored
17
.github/workflows/issue_check_inactive.yml
vendored
@ -1,17 +0,0 @@
|
||||
name: Check inactive
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 1 * *"
|
||||
|
||||
jobs:
|
||||
check-inactive:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: check-inactive
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: 'check-inactive'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
inactive-day: 30
|
||||
body: Hello, this issue has been inactive for more than 30 days and will be closed if inactive for another 30 days.
|
21
.github/workflows/issue_close_inactive.yml
vendored
21
.github/workflows/issue_close_inactive.yml
vendored
@ -1,21 +0,0 @@
|
||||
name: Close inactive
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 */7 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
close-inactive:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: close-issues
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: 'close-issues'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
labels: 'inactive'
|
||||
inactive-day: 30
|
||||
close-reason: 'not_planned'
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}, this issue was closed due to inactive more than 60 days. You can reopen or recreate it if you think it should continue.
|
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.3.3
|
||||
uses: actions-cool/issues-helper@v3.4.0
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
65
.github/workflows/release.yml
vendored
65
.github/workflows/release.yml
vendored
@ -1,33 +1,27 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.MY_TOKEN}}
|
||||
release:
|
||||
needs: changelog
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ubuntu-latest]
|
||||
go-version: [1.19]
|
||||
platform: [ ubuntu-latest ]
|
||||
go-version: [ 1.19 ]
|
||||
name: Release
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: prerelease
|
||||
uses: irongut/EditRelease@v1.2.0
|
||||
with:
|
||||
token: ${{ secrets.MY_TOKEN }}
|
||||
id: ${{ github.event.release.id }}
|
||||
prerelease: true
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
@ -38,6 +32,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo snap install zig --classic --beta
|
||||
docker pull crazymax/xgo:latest
|
||||
go install github.com/crazy-max/xgo@latest
|
||||
sudo apt install upx
|
||||
@ -46,7 +41,41 @@ jobs:
|
||||
run: |
|
||||
bash build.sh release
|
||||
|
||||
- name: prerelease
|
||||
uses: irongut/EditRelease@v1.2.0
|
||||
with:
|
||||
token: ${{ secrets.MY_TOKEN }}
|
||||
id: ${{ github.event.release.id }}
|
||||
prerelease: false
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: build/compress/*
|
||||
|
||||
release_desktop:
|
||||
needs: release
|
||||
name: Release desktop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: alist-org/desktop-release
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Add tag
|
||||
run: |
|
||||
git config --local user.email "i@nn.ci"
|
||||
git config --local user.name "Andy Hsu"
|
||||
version=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||
git tag -a $version -m "release $version"
|
||||
|
||||
- name: Push tags
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.MY_TOKEN }}
|
||||
branch: main
|
||||
repository: alist-org/desktop-release
|
4
.github/workflows/release_docker.yml
vendored
4
.github/workflows/release_docker.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@ -57,7 +57,7 @@ jobs:
|
||||
- name: Add tag
|
||||
run: |
|
||||
git config --local user.email "i@nn.ci"
|
||||
git config --local user.name "Noah Hsu"
|
||||
git config --local user.name "Andy Hsu"
|
||||
git tag -a ${{ github.ref_name }} -m "release ${{ github.ref_name }}"
|
||||
|
||||
- name: Push tags
|
||||
|
@ -15,4 +15,4 @@ RUN apk add --no-cache bash ca-certificates su-exec tzdata; \
|
||||
chmod +x /entrypoint.sh
|
||||
ENV PUID=0 PGID=0 UMASK=022
|
||||
EXPOSE 5244
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
CMD [ "/entrypoint.sh" ]
|
||||
|
12
README_cn.md
12
README_cn.md
@ -41,7 +41,7 @@
|
||||
|
||||
[English](./README.md) | 中文 | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
|
||||
|
||||
## Features
|
||||
## 功能
|
||||
|
||||
- [x] 多种存储
|
||||
- [x] 本地存储
|
||||
@ -89,7 +89,7 @@
|
||||
- [x] 离线下载
|
||||
- [x] 跨存储复制文件
|
||||
|
||||
## Document
|
||||
## 文档
|
||||
|
||||
<https://alist.nn.ci/zh/>
|
||||
|
||||
@ -97,21 +97,21 @@
|
||||
|
||||
<https://al.nn.ci>
|
||||
|
||||
## Discussion
|
||||
## 讨论
|
||||
|
||||
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) ,**issue仅针对错误报告和功能请求。**
|
||||
|
||||
## Sponsor
|
||||
## 赞助
|
||||
|
||||
AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我继续下去,请考虑赞助我或提供一个单一的捐款!感谢所有的爱和支持:https://alist.nn.ci/zh/guide/sponsor.html
|
||||
|
||||
### Special sponsors
|
||||
### 特别赞助
|
||||
|
||||
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.la/)
|
||||
- [KinhDown 百度云盘不限速下载!永久免费!已稳定运行3年!非常可靠!Q群 -> 786799372](https://kinhdown.com)
|
||||
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
|
||||
|
||||
## Contributors
|
||||
## 贡献者
|
||||
|
||||
Thanks goes to these wonderful people:
|
||||
|
||||
|
18
build.sh
18
build.sh
@ -1,7 +1,7 @@
|
||||
appName="alist"
|
||||
builtAt="$(date +'%F %T %z')"
|
||||
goVersion=$(go version | sed 's/go version //')
|
||||
gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
|
||||
gitAuthor="Xhofe <i@nn.ci>"
|
||||
gitCommit=$(git log --pretty=format:"%h" -1)
|
||||
|
||||
if [ "$1" = "dev" ]; then
|
||||
@ -41,6 +41,17 @@ FetchWebRelease() {
|
||||
rm -rf dist.tar.gz
|
||||
}
|
||||
|
||||
BuildWinArm64() {
|
||||
echo building for windows-arm64
|
||||
chmod +x ./wrapper/zcc-arm64
|
||||
chmod +x ./wrapper/zcxx-arm64
|
||||
export GOOS=windows
|
||||
export GOARCH=arm64
|
||||
export CC=$(pwd)/wrapper/zcc-arm64
|
||||
export CXX=$(pwd)/wrapper/zcxx-arm64
|
||||
go build -o "$1" -ldflags="$ldflags" -tags=jsoniter .
|
||||
}
|
||||
|
||||
BuildDev() {
|
||||
rm -rf .git/
|
||||
xgo -targets=linux/amd64,windows/amd64,darwin/amd64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
||||
@ -48,7 +59,7 @@ BuildDev() {
|
||||
mv alist-* dist
|
||||
cd dist
|
||||
upx -9 ./alist-linux*
|
||||
upx -9 ./alist-windows*
|
||||
upx -9 ./alist-windows-amd64.exe
|
||||
find . -type f -print0 | xargs -0 md5sum >md5.txt
|
||||
cat md5.txt
|
||||
}
|
||||
@ -80,10 +91,11 @@ BuildRelease() {
|
||||
export CGO_ENABLED=1
|
||||
go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
||||
done
|
||||
BuildWinArm64 ./build/alist-windows-arm64.exe
|
||||
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
||||
# why? Because some target platforms seem to have issues with upx compression
|
||||
upx -9 ./alist-linux-amd64
|
||||
upx -9 ./alist-windows*
|
||||
upx -9 ./alist-windows-amd64.exe
|
||||
mv alist-* build
|
||||
}
|
||||
|
||||
|
@ -6,4 +6,5 @@ var (
|
||||
NoPrefix bool
|
||||
Dev bool
|
||||
ForceBinDir bool
|
||||
LogStd bool
|
||||
)
|
||||
|
@ -78,10 +78,19 @@ func writeFile(name string, data interface{}) {
|
||||
func generateDriversJson() {
|
||||
drivers := make(Drivers)
|
||||
drivers["drivers"] = make(KV[interface{}])
|
||||
drivers["config"] = make(KV[interface{}])
|
||||
driverInfoMap := op.GetDriverInfoMap()
|
||||
for k, v := range driverInfoMap {
|
||||
drivers["drivers"][k] = convert(k)
|
||||
items := make(KV[interface{}])
|
||||
config := map[string]string{}
|
||||
if v.Config.Alert != "" {
|
||||
alert := strings.SplitN(v.Config.Alert, "|", 2)
|
||||
if len(alert) > 1 {
|
||||
config["alert"] = alert[1]
|
||||
}
|
||||
}
|
||||
drivers["config"][k] = config
|
||||
for i := range v.Additional {
|
||||
item := v.Additional[i]
|
||||
items[item.Name] = convert(item.Name)
|
||||
|
@ -29,4 +29,5 @@ func init() {
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.ForceBinDir, "force-bin-dir", false, "Force to use the directory where the binary file is located as data directory")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "Force to log to std")
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ the address is defined in config file`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Init()
|
||||
bootstrap.InitAria2()
|
||||
bootstrap.InitQbittorrent()
|
||||
bootstrap.LoadStorages()
|
||||
if !flags.Debug && !flags.Dev {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
52
cmd/storage.go
Normal file
52
cmd/storage.go
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// storageCmd represents the storage command
|
||||
var storageCmd = &cobra.Command{
|
||||
Use: "storage",
|
||||
Short: "Manage storage",
|
||||
}
|
||||
|
||||
func init() {
|
||||
var mountPath string
|
||||
var disable = &cobra.Command{
|
||||
Use: "disable",
|
||||
Short: "Disable a storage",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Init()
|
||||
storage, err := db.GetStorageByMountPath(mountPath)
|
||||
if err != nil {
|
||||
utils.Log.Errorf("failed to query storage: %+v", err)
|
||||
} else {
|
||||
storage.Disabled = true
|
||||
err = db.UpdateStorage(storage)
|
||||
if err != nil {
|
||||
utils.Log.Errorf("failed to update storage: %+v", err)
|
||||
} else {
|
||||
utils.Log.Infof("Storage with mount path [%s] have been disabled", mountPath)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
disable.Flags().StringVarP(&mountPath, "mount-path", "m", "", "The mountPath of storage")
|
||||
RootCmd.AddCommand(storageCmd)
|
||||
storageCmd.AddCommand(disable)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// storageCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// storageCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
@ -6,8 +6,9 @@ import (
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
Cookie string `json:"cookie"`
|
||||
QRCodeToken string `json:"qrcode_token"`
|
||||
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
|
||||
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
|
||||
PageSize int64 `json:"page_size" type:"number" default:"56" help:"list api per page size of 115 driver"`
|
||||
driver.RootID
|
||||
}
|
||||
|
||||
@ -16,6 +17,7 @@ var config = driver.Config{
|
||||
DefaultRoot: "0",
|
||||
OnlyProxy: true,
|
||||
OnlyLocal: true,
|
||||
NoOverwriteUpload: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 115Browser/23.9.3.2 115disk/30.1.0"
|
||||
var UserAgent = driver.UA115Desktop
|
||||
|
||||
func (d *Pan115) login() error {
|
||||
var err error
|
||||
@ -38,7 +38,10 @@ func (d *Pan115) login() error {
|
||||
|
||||
func (d *Pan115) getFiles(fileId string) ([]driver.File, error) {
|
||||
res := make([]driver.File, 0)
|
||||
files, err := d.client.List(fileId)
|
||||
if d.PageSize <= 0 {
|
||||
d.PageSize = driver.FileListLimit
|
||||
}
|
||||
files, err := d.client.ListWithLimit(fileId, d.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -96,14 +96,14 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
u_ := fmt.Sprintf("https://%s%s", u.Host, u.Path)
|
||||
u_ := u.String()
|
||||
res, err := base.NoRedirectClient.R().SetQueryParamsFromValues(u.Query()).Head(u_)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
link := model.Link{
|
||||
URL: downloadUrl,
|
||||
URL: u_,
|
||||
}
|
||||
log.Debugln("res code: ", res.StatusCode())
|
||||
if res.StatusCode() == 302 {
|
||||
|
@ -7,18 +7,6 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
//type BaseResp struct {
|
||||
// Code interface{} `json:"code"`
|
||||
// Message string `json:"message"`
|
||||
//}
|
||||
|
||||
type TokenResp struct {
|
||||
//BaseResp
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
FileName string `json:"FileName"`
|
||||
Size int64 `json:"Size"`
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
@ -28,9 +29,7 @@ func (d *Pan123) login() error {
|
||||
"password": d.Password,
|
||||
}
|
||||
}
|
||||
var resp TokenResp
|
||||
res, err := base.RestyClient.R().
|
||||
SetResult(&resp).
|
||||
SetBody(body).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -38,7 +37,7 @@ func (d *Pan123) login() error {
|
||||
if utils.Json.Get(res.Body(), "code").ToInt() != 200 {
|
||||
err = fmt.Errorf(utils.Json.Get(res.Body(), "message").ToString())
|
||||
} else {
|
||||
d.AccessToken = resp.Data.Token
|
||||
d.AccessToken = utils.Json.Get(res.Body(), "data", "token").ToString()
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -77,18 +76,19 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
|
||||
}
|
||||
|
||||
func (d *Pan123) getFiles(parentId string) ([]File, error) {
|
||||
next := "0"
|
||||
page := 1
|
||||
res := make([]File, 0)
|
||||
for next != "-1" {
|
||||
for {
|
||||
var resp Files
|
||||
query := map[string]string{
|
||||
"driveId": "0",
|
||||
"limit": "100",
|
||||
"next": next,
|
||||
"next": "0",
|
||||
"orderBy": d.OrderBy,
|
||||
"orderDirection": d.OrderDirection,
|
||||
"parentFileId": parentId,
|
||||
"trashed": "false",
|
||||
"Page": strconv.Itoa(page),
|
||||
}
|
||||
_, err := d.request("https://www.123pan.com/api/file/list/new", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
@ -96,8 +96,11 @@ func (d *Pan123) getFiles(parentId string) ([]File, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next = resp.Data.Next
|
||||
page++
|
||||
res = append(res, resp.Data.InfoList...)
|
||||
if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
114
drivers/alias/driver.go
Normal file
114
drivers/alias/driver.go
Normal file
@ -0,0 +1,114 @@
|
||||
package alias
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
type Alias struct {
|
||||
model.Storage
|
||||
Addition
|
||||
pathMap map[string][]string
|
||||
autoFlatten bool
|
||||
oneKey string
|
||||
}
|
||||
|
||||
func (d *Alias) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *Alias) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *Alias) Init(ctx context.Context) error {
|
||||
if d.Paths == "" {
|
||||
return errors.New("paths is required")
|
||||
}
|
||||
d.pathMap = make(map[string][]string)
|
||||
for _, path := range strings.Split(d.Paths, "\n") {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
k, v := getPair(path)
|
||||
d.pathMap[k] = append(d.pathMap[k], v)
|
||||
}
|
||||
if len(d.pathMap) == 1 {
|
||||
for k := range d.pathMap {
|
||||
d.oneKey = k
|
||||
}
|
||||
d.autoFlatten = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Alias) Drop(ctx context.Context) error {
|
||||
d.pathMap = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Alias) Get(ctx context.Context, path string) (model.Obj, error) {
|
||||
if utils.PathEqual(path, "/") {
|
||||
return &model.Object{
|
||||
Name: "Root",
|
||||
IsFolder: true,
|
||||
Path: "/",
|
||||
}, nil
|
||||
}
|
||||
root, sub := d.getRootAndPath(path)
|
||||
dsts, ok := d.pathMap[root]
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
for _, dst := range dsts {
|
||||
obj, err := d.get(ctx, path, dst, sub)
|
||||
if err == nil {
|
||||
return obj, nil
|
||||
}
|
||||
}
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
path := dir.GetPath()
|
||||
if utils.PathEqual(path, "/") && !d.autoFlatten {
|
||||
return d.listRoot(), nil
|
||||
}
|
||||
root, sub := d.getRootAndPath(path)
|
||||
dsts, ok := d.pathMap[root]
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
var objs []model.Obj
|
||||
for _, dst := range dsts {
|
||||
tmp, err := d.list(ctx, dst, sub)
|
||||
if err == nil {
|
||||
objs = append(objs, tmp...)
|
||||
}
|
||||
}
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
root, sub := d.getRootAndPath(file.GetPath())
|
||||
dsts, ok := d.pathMap[root]
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
for _, dst := range dsts {
|
||||
link, err := d.link(ctx, dst, sub, args)
|
||||
if err == nil {
|
||||
return link, nil
|
||||
}
|
||||
}
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Alias)(nil)
|
27
drivers/alias/meta.go
Normal file
27
drivers/alias/meta.go
Normal file
@ -0,0 +1,27 @@
|
||||
package alias
|
||||
|
||||
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
|
||||
Paths string `json:"paths" required:"true" type:"text"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Alias",
|
||||
LocalSort: true,
|
||||
NoCache: true,
|
||||
NoUpload: true,
|
||||
DefaultRoot: "/",
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Alias{}
|
||||
})
|
||||
}
|
1
drivers/alias/types.go
Normal file
1
drivers/alias/types.go
Normal file
@ -0,0 +1 @@
|
||||
package alias
|
103
drivers/alias/util.go
Normal file
103
drivers/alias/util.go
Normal file
@ -0,0 +1,103 @@
|
||||
package alias
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
stdpath "path"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/sign"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
)
|
||||
|
||||
func (d *Alias) listRoot() []model.Obj {
|
||||
var objs []model.Obj
|
||||
for k, _ := range d.pathMap {
|
||||
obj := model.Object{
|
||||
Name: k,
|
||||
IsFolder: true,
|
||||
Modified: d.Modified,
|
||||
}
|
||||
objs = append(objs, &obj)
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
func getPair(path string) (string, string) {
|
||||
//path = strings.TrimSpace(path)
|
||||
if strings.Contains(path, ":") {
|
||||
pair := strings.SplitN(path, ":", 2)
|
||||
if !strings.Contains(pair[0], "/") {
|
||||
return pair[0], pair[1]
|
||||
}
|
||||
}
|
||||
return stdpath.Base(path), path
|
||||
}
|
||||
|
||||
func (d *Alias) getRootAndPath(path string) (string, string) {
|
||||
if d.autoFlatten {
|
||||
return d.oneKey, path
|
||||
}
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
if len(parts) == 1 {
|
||||
return parts[0], ""
|
||||
}
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
|
||||
func (d *Alias) get(ctx context.Context, path string, dst, sub string) (model.Obj, error) {
|
||||
obj, err := fs.Get(ctx, stdpath.Join(dst, sub))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.Object{
|
||||
Path: path,
|
||||
Name: obj.GetName(),
|
||||
Size: obj.GetSize(),
|
||||
Modified: obj.ModTime(),
|
||||
IsFolder: obj.IsDir(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Alias) list(ctx context.Context, dst, sub string) ([]model.Obj, error) {
|
||||
objs, err := fs.List(ctx, stdpath.Join(dst, sub))
|
||||
// the obj must implement the model.SetPath interface
|
||||
// return objs, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.SliceConvert(objs, func(obj model.Obj) (model.Obj, error) {
|
||||
return &model.Object{
|
||||
Name: obj.GetName(),
|
||||
Size: obj.GetSize(),
|
||||
Modified: obj.ModTime(),
|
||||
IsFolder: obj.IsDir(),
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) {
|
||||
reqPath := stdpath.Join(dst, sub)
|
||||
storage, err := fs.GetStorage(reqPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = fs.Get(ctx, reqPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if common.ShouldProxy(storage, stdpath.Base(sub)) {
|
||||
return &model.Link{
|
||||
URL: fmt.Sprintf("/p%s?sign=%s",
|
||||
utils.EncodePath(reqPath, true),
|
||||
sign.Sign(reqPath)),
|
||||
}, nil
|
||||
}
|
||||
link, _, err := fs.Link(ctx, reqPath, args)
|
||||
return link, err
|
||||
}
|
@ -2,6 +2,7 @@ package alist_v3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"path"
|
||||
"strconv"
|
||||
@ -55,6 +56,9 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
return nil, errors.New(resp.Message)
|
||||
}
|
||||
var files []model.Obj
|
||||
for _, f := range resp.Data.Content {
|
||||
file := model.ObjThumb{
|
||||
@ -84,6 +88,9 @@ func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
return nil, errors.New(resp.Message)
|
||||
}
|
||||
return &model.Link{
|
||||
URL: resp.Data.RawURL,
|
||||
}, nil
|
||||
|
@ -10,7 +10,7 @@ func checkResp(resp common.Resp[interface{}], err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Message == "success" {
|
||||
if resp.Code == 200 {
|
||||
return nil
|
||||
}
|
||||
return errors.New(resp.Message)
|
||||
|
@ -31,6 +31,7 @@ type AliDrive struct {
|
||||
AccessToken string
|
||||
cron *cron.Cron
|
||||
DriveId string
|
||||
UserID string
|
||||
}
|
||||
|
||||
func (d *AliDrive) Config() driver.Config {
|
||||
@ -54,6 +55,7 @@ func (d *AliDrive) Init(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
d.DriveId = utils.Json.Get(res, "default_drive_id").ToString()
|
||||
d.UserID = utils.Json.Get(res, "user_id").ToString()
|
||||
d.cron = cron.NewCron(time.Hour * 2)
|
||||
d.cron.Do(func() {
|
||||
err := d.refreshToken()
|
||||
@ -61,7 +63,22 @@ func (d *AliDrive) Init(ctx context.Context) error {
|
||||
log.Errorf("%+v", err)
|
||||
}
|
||||
})
|
||||
return err
|
||||
if global.Has(d.UserID) {
|
||||
return nil
|
||||
}
|
||||
// init deviceID
|
||||
deviceID := utils.GetSHA256Encode(d.UserID)
|
||||
// init privateKey
|
||||
privateKey, _ := NewPrivateKeyFromHex(deviceID)
|
||||
state := State{
|
||||
privateKey: privateKey,
|
||||
deviceID: deviceID,
|
||||
}
|
||||
// store state
|
||||
global.Store(d.UserID, &state)
|
||||
// init signature
|
||||
d.sign()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliDrive) Drop(ctx context.Context) error {
|
||||
@ -169,10 +186,19 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
||||
"type": "file",
|
||||
}
|
||||
|
||||
var localFile *os.File
|
||||
if fileStream, ok := file.ReadCloser.(*model.FileStream); ok {
|
||||
localFile, _ = fileStream.ReadCloser.(*os.File)
|
||||
}
|
||||
if d.RapidUpload {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
io.CopyN(buf, file, 1024)
|
||||
reqBody["pre_hash"] = utils.GetSHA1Encode(buf.String())
|
||||
if localFile != nil {
|
||||
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 把头部拼接回去
|
||||
file.ReadCloser = struct {
|
||||
io.Reader
|
||||
@ -181,6 +207,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
||||
Reader: io.MultiReader(buf, file),
|
||||
Closer: file,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reqBody["content_hash_name"] = "none"
|
||||
reqBody["proof_version"] = "v1"
|
||||
@ -196,6 +223,16 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
||||
}
|
||||
|
||||
if d.RapidUpload && e.Code == "PreHashMatched" {
|
||||
delete(reqBody, "pre_hash")
|
||||
h := sha1.New()
|
||||
if localFile != nil {
|
||||
if err = utils.CopyWithCtx(ctx, h, localFile, 0, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = localFile.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
tempFile, err := os.CreateTemp(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
@ -204,11 +241,11 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
delete(reqBody, "pre_hash")
|
||||
h := sha1.New()
|
||||
if _, err = io.Copy(io.MultiWriter(tempFile, h), file); err != nil {
|
||||
if err = utils.CopyWithCtx(ctx, io.MultiWriter(tempFile, h), file, 0, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
localFile = tempFile
|
||||
}
|
||||
reqBody["content_hash"] = hex.EncodeToString(h.Sum(nil))
|
||||
reqBody["content_hash_name"] = "sha1"
|
||||
reqBody["proof_version"] = "v1"
|
||||
@ -228,7 +265,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
||||
if file.GetSize() > 0 {
|
||||
o = r.Mod(r, i)
|
||||
}
|
||||
n, _ := io.NewSectionReader(tempFile, o.Int64(), 8).Read(buf[:8])
|
||||
n, _ := io.NewSectionReader(localFile, o.Int64(), 8).Read(buf[:8])
|
||||
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
|
||||
|
||||
_, err, e := d.request("https://api.aliyundrive.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
|
||||
@ -241,10 +278,10 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
||||
return nil
|
||||
}
|
||||
// 秒传失败
|
||||
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
|
||||
if _, err = localFile.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
file.ReadCloser = tempFile
|
||||
file.ReadCloser = localFile
|
||||
}
|
||||
|
||||
for i, partInfo := range resp.PartInfoList {
|
||||
@ -300,6 +337,7 @@ func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}
|
||||
case "video_preview":
|
||||
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
|
||||
data["category"] = "live_transcoding"
|
||||
data["url_expire_sec"] = 14400
|
||||
default:
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
16
drivers/aliyundrive/global.go
Normal file
16
drivers/aliyundrive/global.go
Normal file
@ -0,0 +1,16 @@
|
||||
package aliyundrive
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
|
||||
"github.com/alist-org/alist/v3/pkg/generic_sync"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
deviceID string
|
||||
signature string
|
||||
retry int
|
||||
privateKey *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
var global = generic_sync.MapOf[string, *State]{}
|
66
drivers/aliyundrive/help.go
Normal file
66
drivers/aliyundrive/help.go
Normal file
@ -0,0 +1,66 @@
|
||||
package aliyundrive
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/dustinxie/ecc"
|
||||
)
|
||||
|
||||
func NewPrivateKey() (*ecdsa.PrivateKey, error) {
|
||||
p256k1 := ecc.P256k1()
|
||||
return ecdsa.GenerateKey(p256k1, rand.Reader)
|
||||
}
|
||||
|
||||
func NewPrivateKeyFromHex(hex_ string) (*ecdsa.PrivateKey, error) {
|
||||
data, err := hex.DecodeString(hex_)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewPrivateKeyFromBytes(data), nil
|
||||
|
||||
}
|
||||
|
||||
func NewPrivateKeyFromBytes(priv []byte) *ecdsa.PrivateKey {
|
||||
p256k1 := ecc.P256k1()
|
||||
x, y := p256k1.ScalarBaseMult(priv)
|
||||
return &ecdsa.PrivateKey{
|
||||
PublicKey: ecdsa.PublicKey{
|
||||
Curve: p256k1,
|
||||
X: x,
|
||||
Y: y,
|
||||
},
|
||||
D: new(big.Int).SetBytes(priv),
|
||||
}
|
||||
}
|
||||
|
||||
func PrivateKeyToHex(private *ecdsa.PrivateKey) string {
|
||||
return hex.EncodeToString(PrivateKeyToBytes(private))
|
||||
}
|
||||
|
||||
func PrivateKeyToBytes(private *ecdsa.PrivateKey) []byte {
|
||||
return private.D.Bytes()
|
||||
}
|
||||
|
||||
func PublicKeyToHex(public *ecdsa.PublicKey) string {
|
||||
return hex.EncodeToString(PublicKeyToBytes(public))
|
||||
}
|
||||
|
||||
func PublicKeyToBytes(public *ecdsa.PublicKey) []byte {
|
||||
x := public.X.Bytes()
|
||||
if len(x) < 32 {
|
||||
for i := 0; i < 32-len(x); i++ {
|
||||
x = append([]byte{0}, x...)
|
||||
}
|
||||
}
|
||||
|
||||
y := public.Y.Bytes()
|
||||
if len(y) < 32 {
|
||||
for i := 0; i < 32-len(y); i++ {
|
||||
y = append([]byte{0}, y...)
|
||||
}
|
||||
}
|
||||
return append(x, y...)
|
||||
}
|
@ -8,6 +8,7 @@ import (
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
//DeviceID string `json:"device_id" required:"true"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
|
||||
RapidUpload bool `json:"rapid_upload"`
|
||||
@ -17,6 +18,9 @@ type Addition struct {
|
||||
var config = driver.Config{
|
||||
Name: "Aliyundrive",
|
||||
DefaultRoot: "root",
|
||||
Alert: `warning|There may be an infinite loop bug in this driver.
|
||||
Deprecated, no longer maintained and will be removed in a future version.
|
||||
We recommend using the official driver AliyundriveOpen.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package aliyundrive
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@ -8,9 +10,51 @@ import (
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/dustinxie/ecc"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (d *AliDrive) createSession() error {
|
||||
state, ok := global.Load(d.UserID)
|
||||
if !ok {
|
||||
return fmt.Errorf("can't load user state, user_id: %s", d.UserID)
|
||||
}
|
||||
d.sign()
|
||||
state.retry++
|
||||
if state.retry > 3 {
|
||||
state.retry = 0
|
||||
return fmt.Errorf("createSession failed after three retries")
|
||||
}
|
||||
_, err, _ := d.request("https://api.aliyundrive.com/users/v1/users/device/create_session", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"deviceName": "samsung",
|
||||
"modelName": "SM-G9810",
|
||||
"nonce": 0,
|
||||
"pubKey": PublicKeyToHex(&state.privateKey.PublicKey),
|
||||
"refreshToken": d.RefreshToken,
|
||||
})
|
||||
}, nil)
|
||||
if err == nil{
|
||||
state.retry = 0
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// func (d *AliDrive) renewSession() error {
|
||||
// _, err, _ := d.request("https://api.aliyundrive.com/users/v1/users/device/renew_session", http.MethodPost, nil, nil)
|
||||
// return err
|
||||
// }
|
||||
|
||||
func (d *AliDrive) sign() {
|
||||
state, _ := global.Load(d.UserID)
|
||||
secpAppID := "5dde4e1bdf9e4966b387ba58f4b3fdc3"
|
||||
singdata := fmt.Sprintf("%s:%s:%s:%d", secpAppID, state.deviceID, d.UserID, 0)
|
||||
hash := sha256.Sum256([]byte(singdata))
|
||||
data, _ := ecc.SignBytes(state.privateKey, hash[:], ecc.RecID|ecc.LowerS)
|
||||
state.signature = hex.EncodeToString(data) //strconv.Itoa(state.nonce)
|
||||
}
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *AliDrive) refreshToken() error {
|
||||
@ -39,9 +83,24 @@ func (d *AliDrive) refreshToken() error {
|
||||
|
||||
func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error, RespErr) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer\t"+d.AccessToken)
|
||||
req.SetHeader("content-type", "application/json")
|
||||
req.SetHeader("origin", "https://www.aliyundrive.com")
|
||||
state, ok := global.Load(d.UserID)
|
||||
if !ok {
|
||||
if url == "https://api.aliyundrive.com/v2/user/get" {
|
||||
state = &State{}
|
||||
} else {
|
||||
return nil, fmt.Errorf("can't load user state, user_id: %s", d.UserID), RespErr{}
|
||||
}
|
||||
}
|
||||
req.SetHeaders(map[string]string{
|
||||
"Authorization": "Bearer\t" + d.AccessToken,
|
||||
"content-type": "application/json",
|
||||
"origin": "https://www.aliyundrive.com",
|
||||
"Referer": "https://aliyundrive.com/",
|
||||
"X-Signature": state.signature,
|
||||
"x-request-id": uuid.NewString(),
|
||||
"X-Canary": "client=Android,app=adrive,version=v4.1.0",
|
||||
"X-Device-Id": state.deviceID,
|
||||
})
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
} else {
|
||||
@ -57,14 +116,21 @@ func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp i
|
||||
return nil, err, e
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
switch e.Code {
|
||||
case "AccessTokenInvalid":
|
||||
err = d.refreshToken()
|
||||
if err != nil {
|
||||
return nil, err, e
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
case "DeviceSessionSignatureInvalid":
|
||||
err = d.createSession()
|
||||
if err != nil {
|
||||
return nil, err, e
|
||||
}
|
||||
default:
|
||||
return nil, errors.New(e.Message), e
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
} else if res.IsError() {
|
||||
return nil, errors.New("bad status code " + res.Status()), e
|
||||
}
|
||||
|
228
drivers/aliyundrive_open/driver.go
Normal file
228
drivers/aliyundrive_open/driver.go
Normal file
@ -0,0 +1,228 @@
|
||||
package aliyundrive_open
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type AliyundriveOpen struct {
|
||||
model.Storage
|
||||
Addition
|
||||
base string
|
||||
|
||||
AccessToken string
|
||||
DriveId string
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Init(ctx context.Context) error {
|
||||
err := d.refreshToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := d.request("/adrive/v1.0/user/getDriveInfo", http.MethodPost, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.DriveId = utils.Json.Get(res, "default_drive_id").ToString()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files, err := d.getFiles(dir.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||
return fileToObj(src), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
res, err := d.request("/adrive/v1.0/openFile/getDownloadUrl", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"file_id": file.GetID(),
|
||||
"expire_sec": 14400,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := utils.Json.Get(res, "url").ToString()
|
||||
return &model.Link{
|
||||
URL: url,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"parent_file_id": parentDir.GetID(),
|
||||
"name": dirName,
|
||||
"type": "folder",
|
||||
"check_name_mode": "refuse",
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
_, err := d.request("/adrive/v1.0/openFile/move", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"file_id": srcObj.GetID(),
|
||||
"to_parent_file_id": dstDir.GetID(),
|
||||
"check_name_mode": "refuse", // optional:ignore,auto_rename,refuse
|
||||
//"new_name": "newName", // The new name to use when a file of the same name exists
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
_, err := d.request("/adrive/v1.0/openFile/update", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"file_id": srcObj.GetID(),
|
||||
"name": newName,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
_, err := d.request("/adrive/v1.0/openFile/copy", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"file_id": srcObj.GetID(),
|
||||
"to_parent_file_id": dstDir.GetID(),
|
||||
"auto_rename": true,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Remove(ctx context.Context, obj model.Obj) error {
|
||||
uri := "/adrive/v1.0/openFile/recyclebin/trash"
|
||||
if d.RemoveWay == "delete" {
|
||||
uri = "/adrive/v1.0/openFile/delete"
|
||||
}
|
||||
_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"file_id": obj.GetID(),
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
// rapid_upload is not currently supported
|
||||
// 1. create
|
||||
const DEFAULT int64 = 20971520
|
||||
createData := base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"parent_file_id": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"type": "file",
|
||||
"check_name_mode": "ignore",
|
||||
}
|
||||
count := 1
|
||||
if stream.GetSize() > DEFAULT {
|
||||
count = int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT)))
|
||||
partInfoList := make([]base.Json, 0, count)
|
||||
for i := 1; i <= count; i++ {
|
||||
partInfoList = append(partInfoList, base.Json{"part_number": i})
|
||||
}
|
||||
createData["part_info_list"] = partInfoList
|
||||
}
|
||||
var createResp CreateResp
|
||||
_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(createData).SetResult(&createResp)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 2. upload
|
||||
for i, partInfo := range createResp.PartInfoList {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
uploadUrl := partInfo.UploadUrl
|
||||
if d.InternalUpload {
|
||||
//Replace a known public Host with an internal Host
|
||||
uploadUrl = strings.ReplaceAll(uploadUrl, "https://cn-beijing-data.aliyundrive.net/", "http://ccp-bj29-bj-1592982087.oss-cn-beijing-internal.aliyuncs.com/")
|
||||
}
|
||||
req, err := http.NewRequest("PUT", uploadUrl, io.LimitReader(stream, DEFAULT))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.Body.Close()
|
||||
if count > 0 {
|
||||
up(i * 100 / count)
|
||||
}
|
||||
}
|
||||
// 3. complete
|
||||
_, err = d.request("/adrive/v1.0/openFile/complete", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"file_id": createResp.FileId,
|
||||
"upload_id": createResp.UploadId,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
var resp base.Json
|
||||
var uri string
|
||||
data := base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"file_id": args.Obj.GetID(),
|
||||
}
|
||||
switch args.Method {
|
||||
case "video_preview":
|
||||
uri = "/adrive/v1.0/openFile/getVideoPreviewPlayInfo"
|
||||
data["category"] = "live_transcoding"
|
||||
data["url_expire_sec"] = 14400
|
||||
default:
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data).SetResult(&resp)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*AliyundriveOpen)(nil)
|
38
drivers/aliyundrive_open/meta.go
Normal file
38
drivers/aliyundrive_open/meta.go
Normal file
@ -0,0 +1,38 @@
|
||||
package aliyundrive_open
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
|
||||
OauthTokenURL string `json:"oauth_token_url" default:"https://api.nn.ci/alist/ali_open/token"`
|
||||
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
|
||||
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
|
||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||
InternalUpload bool `json:"internal_upload" help:"If you are using Aliyun ECS in Beijing, you can turn it on to boost the upload speed"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "AliyundriveOpen",
|
||||
LocalSort: false,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "root",
|
||||
NoOverwriteUpload: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &AliyundriveOpen{
|
||||
base: "https://open.aliyundrive.com",
|
||||
}
|
||||
})
|
||||
}
|
67
drivers/aliyundrive_open/types.go
Normal file
67
drivers/aliyundrive_open/types.go
Normal file
@ -0,0 +1,67 @@
|
||||
package aliyundrive_open
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type ErrResp struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Files struct {
|
||||
Items []File `json:"items"`
|
||||
NextMarker string `json:"next_marker"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
DriveId string `json:"drive_id"`
|
||||
FileId string `json:"file_id"`
|
||||
ParentFileId string `json:"parent_file_id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
FileExtension string `json:"file_extension"`
|
||||
ContentHash string `json:"content_hash"`
|
||||
Category string `json:"category"`
|
||||
Type string `json:"type"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Url string `json:"url"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func fileToObj(f File) *model.ObjThumb {
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: f.FileId,
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Modified: f.UpdatedAt,
|
||||
IsFolder: f.Type == "folder",
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
|
||||
}
|
||||
}
|
||||
|
||||
type CreateResp struct {
|
||||
//Type string `json:"type"`
|
||||
//ParentFileId string `json:"parent_file_id"`
|
||||
//DriveId string `json:"drive_id"`
|
||||
FileId string `json:"file_id"`
|
||||
//RevisionId string `json:"revision_id"`
|
||||
//EncryptMode string `json:"encrypt_mode"`
|
||||
//DomainId string `json:"domain_id"`
|
||||
//FileName string `json:"file_name"`
|
||||
UploadId string `json:"upload_id"`
|
||||
//Location string `json:"location"`
|
||||
RapidUpload bool `json:"rapid_upload"`
|
||||
PartInfoList []struct {
|
||||
Etag interface{} `json:"etag"`
|
||||
PartNumber int `json:"part_number"`
|
||||
PartSize interface{} `json:"part_size"`
|
||||
UploadUrl string `json:"upload_url"`
|
||||
ContentType string `json:"content_type"`
|
||||
} `json:"part_info_list"`
|
||||
}
|
108
drivers/aliyundrive_open/util.go
Normal file
108
drivers/aliyundrive_open/util.go
Normal file
@ -0,0 +1,108 @@
|
||||
package aliyundrive_open
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *AliyundriveOpen) refreshToken() error {
|
||||
url := d.base + "/oauth/access_token"
|
||||
if d.OauthTokenURL != "" && d.ClientID == "" {
|
||||
url = d.OauthTokenURL
|
||||
}
|
||||
var resp base.TokenResp
|
||||
var e ErrResp
|
||||
_, err := base.RestyClient.R().
|
||||
ForceContentType("application/json").
|
||||
SetBody(base.Json{
|
||||
"client_id": d.ClientID,
|
||||
"client_secret": d.ClientSecret,
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": d.RefreshToken,
|
||||
}).
|
||||
SetResult(&resp).
|
||||
SetError(&e).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" {
|
||||
return fmt.Errorf("failed to refresh token: %s", e.Message)
|
||||
}
|
||||
if resp.RefreshToken == "" {
|
||||
return errors.New("failed to refresh token: refresh token is empty")
|
||||
}
|
||||
d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) request(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
// TODO check whether access_token is expired
|
||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||
if method == http.MethodPost {
|
||||
req.SetHeader("Content-Type", "application/json")
|
||||
}
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
var e ErrResp
|
||||
req.SetError(&e)
|
||||
res, err := req.Execute(method, d.base+uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isRetry := len(retry) > 0 && retry[0]
|
||||
if e.Code != "" {
|
||||
if !isRetry && e.Code == "AccessTokenInvalid" {
|
||||
err = d.refreshToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.request(uri, method, callback, true)
|
||||
}
|
||||
return nil, fmt.Errorf("%s:%s", e.Code, e.Message)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) getFiles(fileId string) ([]File, error) {
|
||||
marker := "first"
|
||||
res := make([]File, 0)
|
||||
for marker != "" {
|
||||
if marker == "first" {
|
||||
marker = ""
|
||||
}
|
||||
var resp Files
|
||||
data := base.Json{
|
||||
"drive_id": d.DriveId,
|
||||
"limit": 200,
|
||||
"marker": marker,
|
||||
"order_by": d.OrderBy,
|
||||
"order_direction": d.OrderDirection,
|
||||
"parent_file_id": fileId,
|
||||
//"category": "",
|
||||
//"type": "",
|
||||
//"video_thumbnail_time": 120000,
|
||||
//"video_thumbnail_width": 480,
|
||||
//"image_thumbnail_width": 480,
|
||||
}
|
||||
_, err := d.request("/adrive/v1.0/openFile/list", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data).SetResult(&resp)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
marker = resp.NextMarker
|
||||
res = append(res, resp.Items...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
@ -2,15 +2,16 @@ package aliyundrive_share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/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"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -77,40 +78,43 @@ func (d *AliyundriveShare) Link(ctx context.Context, file model.Obj, args model.
|
||||
"share_id": d.ShareId,
|
||||
}
|
||||
var resp ShareLinkResp
|
||||
var e ErrorResp
|
||||
_, err := base.RestyClient.R().
|
||||
SetError(&e).SetBody(data).SetResult(&resp).
|
||||
SetHeader("content-type", "application/json").
|
||||
SetHeader("Authorization", "Bearer\t"+d.AccessToken).
|
||||
SetHeader("x-share-token", d.ShareToken).
|
||||
Post("https://api.aliyundrive.com/v2/file/get_share_link_download_url")
|
||||
_, err := d.request("https://api.aliyundrive.com/v2/file/get_share_link_download_url", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data).SetResult(&resp)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var u string
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" || e.Code == "ShareLinkTokenInvalid" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = d.refreshToken()
|
||||
} else {
|
||||
err = d.getShareToken()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Link(ctx, file, args)
|
||||
} else {
|
||||
return nil, errors.New(e.Code + ": " + e.Message)
|
||||
}
|
||||
} else {
|
||||
u = resp.DownloadUrl
|
||||
}
|
||||
return &model.Link{
|
||||
Header: http.Header{
|
||||
"Referer": []string{"https://www.aliyundrive.com/"},
|
||||
},
|
||||
URL: u,
|
||||
URL: resp.DownloadUrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveShare) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
var resp base.Json
|
||||
var url string
|
||||
data := base.Json{
|
||||
"share_id": d.ShareId,
|
||||
"file_id": args.Obj.GetID(),
|
||||
}
|
||||
switch args.Method {
|
||||
case "doc_preview":
|
||||
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
|
||||
case "video_preview":
|
||||
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
|
||||
data["category"] = "live_transcoding"
|
||||
default:
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
_, err := d.request(url, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data).SetResult(&resp)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*AliyundriveShare)(nil)
|
||||
|
@ -52,6 +52,40 @@ func (d *AliyundriveShare) getShareToken() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveShare) request(url, method string, callback base.ReqCallback) ([]byte, error) {
|
||||
var e ErrorResp
|
||||
req := base.RestyClient.R().
|
||||
SetError(&e).
|
||||
SetHeader("content-type", "application/json").
|
||||
SetHeader("Authorization", "Bearer\t"+d.AccessToken).
|
||||
SetHeader("x-share-token", d.ShareToken)
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
} else {
|
||||
req.SetBody("{}")
|
||||
}
|
||||
resp, err := req.Execute(method, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" || e.Code == "ShareLinkTokenInvalid" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = d.refreshToken()
|
||||
} else {
|
||||
err = d.getShareToken()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.request(url, method, callback)
|
||||
} else {
|
||||
return nil, errors.New(e.Code + ": " + e.Message)
|
||||
}
|
||||
}
|
||||
return resp.Body(), nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveShare) getFiles(fileId string) ([]File, error) {
|
||||
files := make([]File, 0)
|
||||
data := base.Json{
|
||||
|
@ -6,12 +6,15 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/139"
|
||||
_ "github.com/alist-org/alist/v3/drivers/189"
|
||||
_ "github.com/alist-org/alist/v3/drivers/189pc"
|
||||
_ "github.com/alist-org/alist/v3/drivers/alias"
|
||||
_ "github.com/alist-org/alist/v3/drivers/alist_v2"
|
||||
_ "github.com/alist-org/alist/v3/drivers/alist_v3"
|
||||
_ "github.com/alist-org/alist/v3/drivers/aliyundrive"
|
||||
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_open"
|
||||
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
|
||||
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
|
||||
_ "github.com/alist-org/alist/v3/drivers/baidu_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
|
||||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
||||
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
||||
@ -31,6 +34,7 @@ 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/trainbit"
|
||||
_ "github.com/alist-org/alist/v3/drivers/uss"
|
||||
_ "github.com/alist-org/alist/v3/drivers/virtual"
|
||||
_ "github.com/alist-org/alist/v3/drivers/webdav"
|
||||
|
@ -154,7 +154,7 @@ func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Li
|
||||
"target": fmt.Sprintf("[\"%s\"]", file.GetPath()),
|
||||
"dlink": "1",
|
||||
"web": "5",
|
||||
"origin": "dlna",
|
||||
//"origin": "dlna",
|
||||
}
|
||||
_, err := d.request("https://pan.baidu.com/api/filemetas", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(param)
|
||||
@ -165,7 +165,7 @@ func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Li
|
||||
return &model.Link{
|
||||
URL: resp.Info[0].Dlink,
|
||||
Header: http.Header{
|
||||
"User-Agent": []string{"pan.baidu.com"},
|
||||
"User-Agent": []string{"netdisk"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
153
drivers/baidu_share/driver.go
Normal file
153
drivers/baidu_share/driver.go
Normal file
@ -0,0 +1,153 @@
|
||||
package baidu_share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/go-cache"
|
||||
"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"
|
||||
)
|
||||
|
||||
type BaiduShare struct {
|
||||
model.Storage
|
||||
Addition
|
||||
config map[string]string
|
||||
dlinkCache cache.ICache[string]
|
||||
}
|
||||
|
||||
func (d *BaiduShare) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *BaiduShare) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *BaiduShare) Init(ctx context.Context) error {
|
||||
// TODO login / refresh token
|
||||
//op.MustSaveDriverStorage(d)
|
||||
d.config = map[string]string{}
|
||||
d.dlinkCache = cache.NewMemCache(cache.WithClearInterval[string](time.Duration(d.CacheExpiration) * time.Minute))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *BaiduShare) Drop(ctx context.Context) error {
|
||||
d.dlinkCache.Clear()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *BaiduShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
// TODO return the files list
|
||||
body := url.Values{
|
||||
"shorturl": {d.Surl},
|
||||
"dir": {dir.GetPath()},
|
||||
"root": {"0"},
|
||||
"pwd": {d.Pwd},
|
||||
"num": {"1000"},
|
||||
"order": {"time"},
|
||||
}
|
||||
if body.Get("dir") == "" || body.Get("dir") == d.config["root"] {
|
||||
body.Set("root", "1")
|
||||
}
|
||||
res := []model.Obj{}
|
||||
var err error
|
||||
var page int64 = 1
|
||||
more := true
|
||||
for more {
|
||||
body.Set("page", strconv.FormatInt(page, 10))
|
||||
req := base.RestyClient.R().
|
||||
SetCookies([]*http.Cookie{{Name: "BDUSS", Value: d.BDUSS}}).
|
||||
SetBody(body.Encode())
|
||||
resp, e := req.Post("https://pan.baidu.com/share/wxlist?channel=weixin&version=2.2.2&clienttype=25&web=1")
|
||||
err = e
|
||||
jsonresp := jsonResp{}
|
||||
if err == nil {
|
||||
err = base.RestyClient.JSONUnmarshal(resp.Body(), &jsonresp)
|
||||
}
|
||||
if err == nil && jsonresp.Errno == 0 {
|
||||
more = jsonresp.Data.More
|
||||
page += 1
|
||||
for _, v := range jsonresp.Data.List {
|
||||
size, _ := v.Size.Int64()
|
||||
mtime, _ := v.Time.Int64()
|
||||
res = append(res, &model.Object{
|
||||
ID: v.ID.String(),
|
||||
Path: v.Path,
|
||||
Name: v.Name,
|
||||
Size: size,
|
||||
Modified: time.Unix(mtime, 0),
|
||||
IsFolder: v.Dir.String() == "1",
|
||||
})
|
||||
d.dlinkCache.Set(v.Path, v.Dlink, cache.WithEx[string](time.Duration(d.CacheExpiration/2)*time.Minute))
|
||||
}
|
||||
if len(res) > 0 && body.Get("root") == "1" {
|
||||
d.config["root"] = path.Dir(res[0].GetPath())
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("errno:%d", jsonresp.Errno)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (d *BaiduShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
// TODO return link of file
|
||||
var err error
|
||||
req := base.RestyClient.R().SetCookies([]*http.Cookie{{Name: "BDUSS", Value: d.BDUSS}})
|
||||
url, found := d.dlinkCache.Get(file.GetPath())
|
||||
if !found {
|
||||
_, err = d.List(ctx, &model.Object{Path: path.Dir(file.GetPath())}, model.ListArgs{})
|
||||
url, found = d.dlinkCache.Get(file.GetPath())
|
||||
if err == nil && !found {
|
||||
err = errs.NotSupport
|
||||
}
|
||||
}
|
||||
return &model.Link{URL: url, Header: req.Header}, err
|
||||
}
|
||||
|
||||
func (d *BaiduShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
// TODO create folder
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *BaiduShare) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
// TODO move obj
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *BaiduShare) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
// TODO rename obj
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *BaiduShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
// TODO copy obj
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *BaiduShare) Remove(ctx context.Context, obj model.Obj) error {
|
||||
// TODO remove obj
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *BaiduShare) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
// TODO upload file
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
var _ driver.Driver = (*BaiduShare)(nil)
|
34
drivers/baidu_share/meta.go
Normal file
34
drivers/baidu_share/meta.go
Normal file
@ -0,0 +1,34 @@
|
||||
package baidu_share
|
||||
|
||||
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
|
||||
// driver.RootID
|
||||
// define other
|
||||
// Field string `json:"field" type:"select" required:"true" options:"a,b,c" default:"a"`
|
||||
Surl string `json:"surl"`
|
||||
Pwd string `json:"pwd"`
|
||||
BDUSS string `json:"BDUSS"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "BaiduShare",
|
||||
LocalSort: true,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: true,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "",
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &BaiduShare{}
|
||||
})
|
||||
}
|
19
drivers/baidu_share/types.go
Normal file
19
drivers/baidu_share/types.go
Normal file
@ -0,0 +1,19 @@
|
||||
package baidu_share
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type jsonResp struct {
|
||||
Errno int64 `json:"errno"`
|
||||
Data struct {
|
||||
More bool `json:"has_more"`
|
||||
List []struct {
|
||||
ID json.Number `json:"fs_id"`
|
||||
Dir json.Number `json:"isdir"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"server_filename"`
|
||||
Time json.Number `json:"server_mtime"`
|
||||
Size json.Number `json:"size"`
|
||||
Dlink string `json:"dlink"`
|
||||
} `json:"list"`
|
||||
} `json:"data"`
|
||||
}
|
3
drivers/baidu_share/util.go
Normal file
3
drivers/baidu_share/util.go
Normal file
@ -0,0 +1,3 @@
|
||||
package baidu_share
|
||||
|
||||
// do others that not defined in Driver interface
|
@ -23,8 +23,9 @@ func init() {
|
||||
}
|
||||
|
||||
func NewRestyClient() *resty.Client {
|
||||
return resty.New().
|
||||
client := resty.New().
|
||||
SetHeader("user-agent", UserAgent).
|
||||
SetRetryCount(3).
|
||||
SetTimeout(DefaultTimeout)
|
||||
return client
|
||||
}
|
||||
|
30
drivers/base/util.go
Normal file
30
drivers/base/util.go
Normal file
@ -0,0 +1,30 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
func HandleRange(link *model.Link, file io.ReadSeekCloser, header http.Header, size int64) {
|
||||
if header.Get("Range") != "" {
|
||||
r, err := http_range.ParseRange(header.Get("Range"), size)
|
||||
if err == nil && len(r) > 0 {
|
||||
_, err := file.Seek(r[0].Start, io.SeekStart)
|
||||
if err == nil {
|
||||
link.Data = utils.NewLimitReadCloser(file, func() error {
|
||||
return file.Close()
|
||||
}, r[0].Length)
|
||||
link.Status = http.StatusPartialContent
|
||||
link.Header = http.Header{
|
||||
"Content-Range": []string{r[0].ContentRange(size)},
|
||||
"Content-Length": []string{strconv.FormatInt(r[0].Length, 10)},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
stdpath "path"
|
||||
|
||||
"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"
|
||||
@ -44,8 +45,7 @@ func (d *FTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]m
|
||||
return nil, err
|
||||
}
|
||||
res := make([]model.Obj, 0)
|
||||
for i, _ := range entries {
|
||||
entry := entries[i]
|
||||
for _, entry := range entries {
|
||||
if entry.Name == "." || entry.Name == ".." {
|
||||
continue
|
||||
}
|
||||
@ -64,13 +64,13 @@ func (d *FTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*m
|
||||
if err := d.login(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := d.conn.Retr(file.GetPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
r := NewFTPFileReader(d.conn, file.GetPath())
|
||||
link := &model.Link{
|
||||
Data: r,
|
||||
}
|
||||
return &model.Link{
|
||||
Data: resp,
|
||||
}, nil
|
||||
base.HandleRange(link, r, args.Header, file.GetSize())
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (d *FTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
|
@ -1,6 +1,13 @@
|
||||
package ftp
|
||||
|
||||
import "github.com/jlaffaye/ftp"
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jlaffaye/ftp"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
@ -11,7 +18,7 @@ func (d *FTP) login() error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
conn, err := ftp.Dial(d.Address)
|
||||
conn, err := ftp.Dial(d.Address, ftp.DialWithShutTimeout(10*time.Second))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -22,3 +29,81 @@ func (d *FTP) login() error {
|
||||
d.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
// An FTP file reader that implements io.ReadSeekCloser for seeking.
|
||||
type FTPFileReader struct {
|
||||
conn *ftp.ServerConn
|
||||
resp *ftp.Response
|
||||
offset int64
|
||||
mu sync.Mutex
|
||||
path string
|
||||
}
|
||||
|
||||
func NewFTPFileReader(conn *ftp.ServerConn, path string) *FTPFileReader {
|
||||
return &FTPFileReader{
|
||||
conn: conn,
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FTPFileReader) Read(buf []byte) (n int, err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.resp == nil {
|
||||
r.resp, err = r.conn.RetrFrom(r.path, uint64(r.offset))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
n, err = r.resp.Read(buf)
|
||||
r.offset += int64(n)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *FTPFileReader) Seek(offset int64, whence int) (int64, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
oldOffset := r.offset
|
||||
var newOffset int64
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
newOffset = offset
|
||||
case io.SeekCurrent:
|
||||
newOffset = oldOffset + offset
|
||||
case io.SeekEnd:
|
||||
size, err := r.conn.FileSize(r.path)
|
||||
if err != nil {
|
||||
return oldOffset, err
|
||||
}
|
||||
newOffset = offset + int64(size)
|
||||
default:
|
||||
return -1, os.ErrInvalid
|
||||
}
|
||||
|
||||
if newOffset < 0 {
|
||||
// offset out of range
|
||||
return oldOffset, os.ErrInvalid
|
||||
}
|
||||
if newOffset == oldOffset {
|
||||
// offset not changed, so return directly
|
||||
return oldOffset, nil
|
||||
}
|
||||
r.offset = newOffset
|
||||
|
||||
if r.resp != nil {
|
||||
// close the existing ftp data connection, otherwise the next read will be blocked
|
||||
_ = r.resp.Close() // we do not care about whether it returns an error
|
||||
r.resp = nil
|
||||
}
|
||||
return newOffset, nil
|
||||
}
|
||||
|
||||
func (r *FTPFileReader) Close() error {
|
||||
if r.resp != nil {
|
||||
return r.resp.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
type Local struct {
|
||||
model.Storage
|
||||
Addition
|
||||
mkdirPerm int32
|
||||
}
|
||||
|
||||
func (d *Local) Config() driver.Config {
|
||||
@ -35,6 +36,15 @@ func (d *Local) Config() driver.Config {
|
||||
}
|
||||
|
||||
func (d *Local) Init(ctx context.Context) error {
|
||||
if d.MkdirPerm == "" {
|
||||
d.mkdirPerm = 0777
|
||||
} else {
|
||||
v, err := strconv.ParseUint(d.MkdirPerm, 8, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.mkdirPerm = int32(v)
|
||||
}
|
||||
if !utils.Exists(d.GetRootPath()) {
|
||||
return fmt.Errorf("root folder %s not exists", d.GetRootPath())
|
||||
}
|
||||
@ -68,11 +78,14 @@ func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
|
||||
continue
|
||||
}
|
||||
thumb := ""
|
||||
if d.Thumbnail && utils.GetFileType(f.Name()) == conf.IMAGE {
|
||||
if d.Thumbnail {
|
||||
typeName := utils.GetFileType(f.Name())
|
||||
if typeName == conf.IMAGE || typeName == conf.VIDEO {
|
||||
thumb = common.GetApiUrl(nil) + stdpath.Join("/d", args.ReqPath, f.Name())
|
||||
thumb = utils.EncodePath(thumb, true)
|
||||
thumb += "?type=thumb&sign=" + sign.Sign(stdpath.Join(args.ReqPath, f.Name()))
|
||||
}
|
||||
}
|
||||
isFolder := f.IsDir() || isSymlinkDir(f, fullPath)
|
||||
var size int64
|
||||
if !isFolder {
|
||||
@ -123,11 +136,22 @@ func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
||||
fullPath := file.GetPath()
|
||||
var link model.Link
|
||||
if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" {
|
||||
imgData, err := ioutil.ReadFile(fullPath)
|
||||
var srcBuf *bytes.Buffer
|
||||
if utils.GetFileType(file.GetName()) == conf.VIDEO {
|
||||
videoBuf, err := GetSnapshot(fullPath, 10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srcBuf := bytes.NewBuffer(imgData)
|
||||
srcBuf = videoBuf
|
||||
} else {
|
||||
imgData, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imgBuf := bytes.NewBuffer(imgData)
|
||||
srcBuf = imgBuf
|
||||
}
|
||||
|
||||
image, err := imaging.Decode(srcBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -151,7 +175,7 @@ func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
||||
|
||||
func (d *Local) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
fullPath := filepath.Join(parentDir.GetPath(), dirName)
|
||||
err := os.MkdirAll(fullPath, 0777)
|
||||
err := os.MkdirAll(fullPath, os.FileMode(d.mkdirPerm))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ type Addition struct {
|
||||
driver.RootPath
|
||||
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
|
||||
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
|
||||
MkdirPerm string `json:"mkdir_perm" default:"777"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -1,6 +1,9 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
ffmpeg "github.com/u2takey/ffmpeg-go"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -23,3 +26,16 @@ func isSymlinkDir(f fs.FileInfo, path string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetSnapshot(videoPath string, frameNum int) (imgData *bytes.Buffer, err error) {
|
||||
srcBuf := bytes.NewBuffer(nil)
|
||||
err = ffmpeg.Input(videoPath).Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}).
|
||||
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
|
||||
WithOutput(srcBuf, os.Stdout).
|
||||
Run()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return srcBuf, nil
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
@ -75,9 +76,19 @@ func (d *Onedrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName str
|
||||
}
|
||||
|
||||
func (d *Onedrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
parentPath := ""
|
||||
if dstDir.GetID() == "" {
|
||||
parentPath = dstDir.GetPath()
|
||||
if utils.PathEqual(parentPath, "/") {
|
||||
parentPath = path.Join("/drive/root", parentPath)
|
||||
} else {
|
||||
parentPath = path.Join("/drive/root:/", parentPath)
|
||||
}
|
||||
}
|
||||
data := base.Json{
|
||||
"parentReference": base.Json{
|
||||
"id": dstDir.GetID(),
|
||||
"path": parentPath,
|
||||
},
|
||||
"name": srcObj.GetName(),
|
||||
}
|
||||
@ -89,13 +100,15 @@ func (d *Onedrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
}
|
||||
|
||||
func (d *Onedrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
//dstDir, err := op.GetUnwrap(ctx, d, stdpath.Dir(srcObj.GetPath()))
|
||||
var parentID string
|
||||
if o, ok := srcObj.(*Object); ok {
|
||||
parentID = o.ParentID
|
||||
} else {
|
||||
return fmt.Errorf("srcObj is not Object")
|
||||
}
|
||||
if parentID == "" {
|
||||
parentID = "root"
|
||||
}
|
||||
data := base.Json{
|
||||
"parentReference": base.Json{
|
||||
"id": parentID,
|
||||
|
@ -11,7 +11,7 @@ type Addition struct {
|
||||
IsSharepoint bool `json:"is_sharepoint"`
|
||||
ClientID string `json:"client_id" required:"true"`
|
||||
ClientSecret string `json:"client_secret" required:"true"`
|
||||
RedirectUri string `json:"redirect_uri" required:"true" default:"https://tool.nn.ci/onedrive/callback"`
|
||||
RedirectUri string `json:"redirect_uri" required:"true" default:"https://alist.nn.ci/tool/onedrive/callback"`
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
SiteId string `json:"site_id"`
|
||||
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
|
||||
|
@ -43,7 +43,7 @@ type File struct {
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
model.ObjThumbURL
|
||||
model.ObjThumb
|
||||
ParentID string
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ func fileToObj(f File, parentID string) *Object {
|
||||
thumb = f.Thumbnails[0].Medium.Url
|
||||
}
|
||||
return &Object{
|
||||
ObjThumbURL: model.ObjThumbURL{
|
||||
ObjThumb: model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: f.Id,
|
||||
Name: f.Name,
|
||||
@ -62,7 +62,7 @@ func fileToObj(f File, parentID string) *Object {
|
||||
IsFolder: f.File == nil,
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: thumb},
|
||||
Url: model.Url{Url: f.Url},
|
||||
//Url: model.Url{Url: f.Url},
|
||||
},
|
||||
ParentID: parentID,
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PikPakShare struct {
|
||||
@ -71,10 +70,6 @@ func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkA
|
||||
link := model.Link{
|
||||
URL: resp.FileInfo.WebContentLink,
|
||||
}
|
||||
if len(resp.FileInfo.Medias) > 0 && resp.FileInfo.Medias[0].Link.Url != "" {
|
||||
log.Debugln("use media link")
|
||||
link.URL = resp.FileInfo.Medias[0].Link.Url
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ var config = driver.Config{
|
||||
Name: "Quark",
|
||||
OnlyProxy: true,
|
||||
DefaultRoot: "0",
|
||||
NoOverwriteUpload: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -59,7 +59,8 @@ func (d *S3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]mo
|
||||
|
||||
func (d *S3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
path := getKey(file.GetPath(), false)
|
||||
disposition := fmt.Sprintf(`attachment;filename="%s"`, url.QueryEscape(stdpath.Base(path)))
|
||||
filename := stdpath.Base(path)
|
||||
disposition := fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, filename, url.PathEscape(filename))
|
||||
input := &s3.GetObjectInput{
|
||||
Bucket: &d.Bucket,
|
||||
Key: &path,
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"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"
|
||||
@ -52,9 +53,11 @@ func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.Link{
|
||||
link := &model.Link{
|
||||
Data: remoteFile,
|
||||
}, nil
|
||||
}
|
||||
base.HandleRange(link, remoteFile, args.Header, file.GetSize())
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (d *SFTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
|
@ -5,8 +5,8 @@ import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
@ -18,7 +18,7 @@ type SMB struct {
|
||||
model.Storage
|
||||
Addition
|
||||
fs *smb2.Share
|
||||
lastConnTime time.Time
|
||||
lastConnTime int64
|
||||
}
|
||||
|
||||
func (d *SMB) Config() driver.Config {
|
||||
@ -79,10 +79,12 @@ func (d *SMB) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*m
|
||||
d.cleanLastConnTime()
|
||||
return nil, err
|
||||
}
|
||||
d.updateLastConnTime()
|
||||
return &model.Link{
|
||||
link := &model.Link{
|
||||
Data: remoteFile,
|
||||
}, nil
|
||||
}
|
||||
base.HandleRange(link, remoteFile, args.Header, file.GetSize())
|
||||
d.updateLastConnTime()
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (d *SMB) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
|
@ -6,17 +6,22 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hirochachacha/go-smb2"
|
||||
)
|
||||
|
||||
func (d *SMB) updateLastConnTime() {
|
||||
d.lastConnTime = time.Now()
|
||||
atomic.StoreInt64(&d.lastConnTime, time.Now().Unix())
|
||||
}
|
||||
|
||||
func (d *SMB) cleanLastConnTime() {
|
||||
d.lastConnTime = time.Now().AddDate(0, 0, -1)
|
||||
atomic.StoreInt64(&d.lastConnTime, 0)
|
||||
}
|
||||
|
||||
func (d *SMB) getLastConnTime() time.Time {
|
||||
return time.Unix(atomic.LoadInt64(&d.lastConnTime), 0)
|
||||
}
|
||||
|
||||
func (d *SMB) initFS() error {
|
||||
@ -43,7 +48,7 @@ func (d *SMB) initFS() error {
|
||||
}
|
||||
|
||||
func (d *SMB) checkConn() error {
|
||||
if time.Since(d.lastConnTime) < 5*time.Minute {
|
||||
if time.Since(d.getLastConnTime()) < 5*time.Minute {
|
||||
return nil
|
||||
}
|
||||
if d.fs != nil {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -124,11 +125,11 @@ func (d *Teambition) Remove(ctx context.Context, obj model.Obj) error {
|
||||
}
|
||||
|
||||
func (d *Teambition) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
res, err := d.request("/projects", http.MethodGet, nil, nil)
|
||||
res, err := d.request("/api/v2/users/me", http.MethodGet, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token := GetBetweenStr(string(res), "strikerAuth":"", "","phoneForLogin")
|
||||
token := utils.Json.Get(res, "strikerAuth").ToString()
|
||||
var newFile *FileUpload
|
||||
if stream.GetSize() <= 20971520 {
|
||||
// post upload
|
||||
|
@ -210,7 +210,7 @@ func (d *Teambition) finishUpload(file *FileUpload, parentId string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func GetBetweenStr(str, start, end string) string {
|
||||
func getBetweenStr(str, start, end string) string {
|
||||
n := strings.Index(str, start)
|
||||
if n == -1 {
|
||||
return ""
|
||||
|
@ -32,42 +32,42 @@ func (d *Template) Drop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *Template) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
// TODO return the files list
|
||||
// TODO return the files list, required
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Template) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
// TODO return link of file
|
||||
// TODO return link of file, required
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Template) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
// TODO create folder
|
||||
// TODO create folder, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Template) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
// TODO move obj
|
||||
// TODO move obj, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Template) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
// TODO rename obj
|
||||
// TODO rename obj, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Template) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
// TODO copy obj
|
||||
// TODO copy obj, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Template) Remove(ctx context.Context, obj model.Obj) error {
|
||||
// TODO remove obj
|
||||
// TODO remove obj, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Template) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
// TODO upload file
|
||||
// TODO upload file, optional
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,9 @@ var config = driver.Config{
|
||||
NoUpload: false,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "root, / or other",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
142
drivers/trainbit/driver.go
Normal file
142
drivers/trainbit/driver.go
Normal file
@ -0,0 +1,142 @@
|
||||
package trainbit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"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"
|
||||
)
|
||||
|
||||
type Trainbit struct {
|
||||
model.Storage
|
||||
Addition
|
||||
}
|
||||
|
||||
var apiExpiredate, guid string
|
||||
|
||||
func (d *Trainbit) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *Trainbit) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *Trainbit) Init(ctx context.Context) error {
|
||||
http.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
var err error
|
||||
apiExpiredate, guid, err = getToken(d.ApiKey, d.AUSHELLPORTAL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Trainbit) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Trainbit) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
form := make(url.Values)
|
||||
form.Set("parentid", strings.Split(dir.GetID(), "_")[0])
|
||||
res, err := postForm("https://trainbit.com/lib/api/v1/listoffiles", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var jsonData any
|
||||
json.Unmarshal(data, &jsonData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
object, err := parseRawFileObject(jsonData.(map[string]any)["items"].([]any))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
|
||||
func (d *Trainbit) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
res, err := get(fmt.Sprintf("https://trainbit.com/files/%s/", strings.Split(file.GetID(), "_")[0]), d.ApiKey, d.AUSHELLPORTAL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.Link{
|
||||
URL: res.Header.Get("Location"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Trainbit) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
form := make(url.Values)
|
||||
form.Set("name", local2provider(dirName, true))
|
||||
form.Set("parentid", strings.Split(parentDir.GetID(), "_")[0])
|
||||
_, err := postForm("https://trainbit.com/lib/api/v1/createfolder", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Trainbit) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
form := make(url.Values)
|
||||
form.Set("sourceid", strings.Split(srcObj.GetID(), "_")[0])
|
||||
form.Set("destinationid", strings.Split(dstDir.GetID(), "_")[0])
|
||||
_, err := postForm("https://trainbit.com/lib/api/v1/move", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Trainbit) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
form := make(url.Values)
|
||||
form.Set("id", strings.Split(srcObj.GetID(), "_")[0])
|
||||
form.Set("name", local2provider(newName, srcObj.IsDir()))
|
||||
_, err := postForm("https://trainbit.com/lib/api/v1/edit", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Trainbit) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Trainbit) Remove(ctx context.Context, obj model.Obj) error {
|
||||
form := make(url.Values)
|
||||
form.Set("id", strings.Split(obj.GetID(), "_")[0])
|
||||
_, err := postForm("https://trainbit.com/lib/api/v1/delete", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Trainbit) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
endpoint, _ := url.Parse("https://tb28.trainbit.com/api/upload/send_raw/")
|
||||
query := &url.Values{}
|
||||
query.Add("q", strings.Split(dstDir.GetID(), "_")[1])
|
||||
query.Add("guid", guid)
|
||||
query.Add("name", url.QueryEscape(local2provider(stream.GetName(), false)))
|
||||
endpoint.RawQuery = query.Encode()
|
||||
var total int64
|
||||
total = 0
|
||||
progressReader := &ProgressReader{
|
||||
stream,
|
||||
func(byteNum int) {
|
||||
total += int64(byteNum)
|
||||
up(int(math.Round(float64(total) / float64(stream.GetSize()) * 100)))
|
||||
},
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, endpoint.String(), progressReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "text/json; charset=UTF-8")
|
||||
_, err = http.DefaultClient.Do(req)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Trainbit)(nil)
|
29
drivers/trainbit/meta.go
Normal file
29
drivers/trainbit/meta.go
Normal file
@ -0,0 +1,29 @@
|
||||
package trainbit
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
AUSHELLPORTAL string `json:"AUSHELLPORTAL" required:"true"`
|
||||
ApiKey string `json:"apikey" required:"true"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Trainbit",
|
||||
LocalSort: false,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "0_000",
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Trainbit{}
|
||||
})
|
||||
}
|
1
drivers/trainbit/types.go
Normal file
1
drivers/trainbit/types.go
Normal file
@ -0,0 +1 @@
|
||||
package trainbit
|
150
drivers/trainbit/util.go
Normal file
150
drivers/trainbit/util.go
Normal file
@ -0,0 +1,150 @@
|
||||
package trainbit
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type ProgressReader struct {
|
||||
io.Reader
|
||||
reporter func(byteNum int)
|
||||
}
|
||||
|
||||
func (progressReader *ProgressReader) Read(data []byte) (int, error) {
|
||||
byteNum, err := progressReader.Reader.Read(data)
|
||||
progressReader.reporter(byteNum)
|
||||
return byteNum, err
|
||||
}
|
||||
|
||||
func get(url string, apiKey string, AUSHELLPORTAL string) (*http.Response, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: ".AUSHELLPORTAL",
|
||||
Value: AUSHELLPORTAL,
|
||||
MaxAge: 2 * 60,
|
||||
})
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "retkeyapi",
|
||||
Value: apiKey,
|
||||
MaxAge: 2 * 60,
|
||||
})
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func postForm(endpoint string, data url.Values, apiExpiredate string, apiKey string, AUSHELLPORTAL string) (*http.Response, error) {
|
||||
extData := make(url.Values)
|
||||
for key, value := range data {
|
||||
extData[key] = make([]string, len(value))
|
||||
copy(extData[key], value)
|
||||
}
|
||||
extData.Set("apikey", apiKey)
|
||||
extData.Set("expiredate", apiExpiredate)
|
||||
req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(extData.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: ".AUSHELLPORTAL",
|
||||
Value: AUSHELLPORTAL,
|
||||
MaxAge: 2 * 60,
|
||||
})
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "retkeyapi",
|
||||
Value: apiKey,
|
||||
MaxAge: 2 * 60,
|
||||
})
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func getToken(apiKey string, AUSHELLPORTAL string) (string, string, error) {
|
||||
res, err := get("https://trainbit.com/files/", apiKey, AUSHELLPORTAL)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
text := string(data)
|
||||
apiExpiredateReg := regexp.MustCompile(`core.api.expiredate = '([^']*)';`)
|
||||
result := apiExpiredateReg.FindAllStringSubmatch(text, -1)
|
||||
apiExpiredate := result[0][1]
|
||||
guidReg := regexp.MustCompile(`app.vars.upload.guid = '([^']*)';`)
|
||||
result = guidReg.FindAllStringSubmatch(text, -1)
|
||||
guid := result[0][1]
|
||||
return apiExpiredate, guid, nil
|
||||
}
|
||||
|
||||
func local2provider(filename string, isFolder bool) string {
|
||||
filename = strings.Replace(filename, "%", url.QueryEscape("%"), -1)
|
||||
filename = strings.Replace(filename, "/", url.QueryEscape("/"), -1)
|
||||
filename = strings.Replace(filename, ":", url.QueryEscape(":"), -1)
|
||||
filename = strings.Replace(filename, "*", url.QueryEscape("*"), -1)
|
||||
filename = strings.Replace(filename, "?", url.QueryEscape("?"), -1)
|
||||
filename = strings.Replace(filename, "\"", url.QueryEscape("\""), -1)
|
||||
filename = strings.Replace(filename, "<", url.QueryEscape("<"), -1)
|
||||
filename = strings.Replace(filename, ">", url.QueryEscape(">"), -1)
|
||||
filename = strings.Replace(filename, "|", url.QueryEscape("|"), -1)
|
||||
if isFolder {
|
||||
return filename
|
||||
}
|
||||
return strings.Join([]string{filename, ".delete_suffix."}, "")
|
||||
}
|
||||
|
||||
func provider2local(filename string) string {
|
||||
index := strings.LastIndex(filename, ".delete_suffix.")
|
||||
if index != -1 {
|
||||
filename = filename[:index]
|
||||
}
|
||||
rawName := strings.Replace(filename, url.QueryEscape("/"), "/", -1)
|
||||
rawName = strings.Replace(rawName, url.QueryEscape(":"), ":", -1)
|
||||
rawName = strings.Replace(rawName, url.QueryEscape("*"), "*", -1)
|
||||
rawName = strings.Replace(rawName, url.QueryEscape("?"), "?", -1)
|
||||
rawName = strings.Replace(rawName, url.QueryEscape("\""), "\"", -1)
|
||||
rawName = strings.Replace(rawName, url.QueryEscape("<"), "<", -1)
|
||||
rawName = strings.Replace(rawName, url.QueryEscape(">"), ">", -1)
|
||||
rawName = strings.Replace(rawName, url.QueryEscape("|"), "|", -1)
|
||||
rawName = strings.Replace(rawName, url.QueryEscape("%"), "%", -1)
|
||||
return rawName
|
||||
}
|
||||
|
||||
func parseRawFileObject(rawObject []any) ([]model.Obj, error) {
|
||||
objectList := make([]model.Obj, 0)
|
||||
for _, each := range rawObject {
|
||||
object := each.(map[string]any)
|
||||
if object["id"].(string) == "0" {
|
||||
continue
|
||||
}
|
||||
isFolder := int64(object["ty"].(float64)) == 1
|
||||
var name string
|
||||
if isFolder {
|
||||
name = object["name"].(string)
|
||||
} else {
|
||||
name = strings.Join([]string{object["name"].(string), object["ext"].(string)}, ".")
|
||||
}
|
||||
modified, err := time.Parse("2006/01/02 15:04:05", object["modified"].(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objectList = append(objectList, model.Obj(&model.Object{
|
||||
ID: strings.Join([]string{object["id"].(string), strings.Split(object["uploadurl"].(string), "=")[1]}, "_"),
|
||||
Name: provider2local(name),
|
||||
Size: int64(object["byte"].(float64)),
|
||||
Modified: modified.Add(-210 * time.Minute),
|
||||
IsFolder: isFolder,
|
||||
}))
|
||||
}
|
||||
return objectList, nil
|
||||
}
|
51
go.mod
51
go.mod
@ -3,18 +3,18 @@ module github.com/alist-org/alist/v3
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/SheltonZhu/115driver v1.0.13
|
||||
github.com/SheltonZhu/115driver v1.0.14
|
||||
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a
|
||||
github.com/aws/aws-sdk-go v1.44.194
|
||||
github.com/blevesearch/bleve/v2 v2.3.6
|
||||
github.com/caarlos0/env/v6 v6.10.1
|
||||
github.com/caarlos0/env/v7 v7.0.0
|
||||
github.com/deckarep/golang-set/v2 v2.1.0
|
||||
github.com/caarlos0/env/v7 v7.1.0
|
||||
github.com/deckarep/golang-set/v2 v2.3.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564
|
||||
github.com/gin-contrib/cors v1.4.0
|
||||
github.com/gin-gonic/gin v1.8.2
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hirochachacha/go-smb2 v1.1.0
|
||||
@ -27,14 +27,15 @@ require (
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20220725095014-c4e0c2b5debf
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca
|
||||
github.com/u2takey/ffmpeg-go v0.4.1
|
||||
github.com/upyun/go-sdk/v3 v3.0.3
|
||||
github.com/winfsp/cgofuse v1.5.0
|
||||
golang.org/x/crypto v0.5.0
|
||||
golang.org/x/image v0.3.0
|
||||
golang.org/x/net v0.5.0
|
||||
gorm.io/driver/mysql v1.4.5
|
||||
gorm.io/driver/postgres v1.4.6
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/image v0.6.0
|
||||
golang.org/x/net v0.8.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
|
||||
)
|
||||
@ -63,14 +64,16 @@ require (
|
||||
github.com/blevesearch/zapx/v15 v15.3.8 // 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.8.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/gaoyb7/115drive-webdav v0.1.8 // indirect
|
||||
github.com/geoffgarside/ber v1.1.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.1 // 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.11.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/goccy/go-json v0.10.0 // indirect
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
||||
github.com/golang/protobuf v1.5.0 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
@ -79,13 +82,14 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.2.0 // indirect
|
||||
github.com/jackc/pgx/v5 v5.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/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@ -95,13 +99,16 @@ require (
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // 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.9 // indirect
|
||||
go.etcd.io/bbolt v1.3.5 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
155
go.sum
155
go.sum
@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo=
|
||||
github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
|
||||
github.com/SheltonZhu/115driver v1.0.13 h1:YEhQ3iMvd5TD6Xp1wKg+73KgdCMjc0pDoT1eCNXnA3M=
|
||||
github.com/SheltonZhu/115driver v1.0.13/go.mod h1:00ixivHH5HqDj4S7kAWbkuUrjtsJTxc7cGv5RMw3RVs=
|
||||
github.com/SheltonZhu/115driver v1.0.14 h1:uW3dl8J9KDMw+3gPxQdhTysoGhw0/uI1484GT9xhfU4=
|
||||
github.com/SheltonZhu/115driver v1.0.14/go.mod h1:00ixivHH5HqDj4S7kAWbkuUrjtsJTxc7cGv5RMw3RVs=
|
||||
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/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=
|
||||
@ -12,10 +12,7 @@ github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible h1:QoRMR0TCctLDqBCMyOu1e
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andreburgaud/crypt2go v1.1.0 h1:eitZxTPY1krUsxinsng3Qvt/Ud7q/aQmmYRh8p4hyPw=
|
||||
github.com/andreburgaud/crypt2go v1.1.0/go.mod h1:4qhZPzarj1dCIRmCkpdgCklwp+hBq9yEt0zPe9Ayuhc=
|
||||
github.com/aws/aws-sdk-go v1.44.173 h1:8kXIxvQnBpGhmR3Eof6SnCKgR0q5/L/3Qbv9vAC5wic=
|
||||
github.com/aws/aws-sdk-go v1.44.173/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go v1.44.174 h1:9lR4a6MKQW/t6YCG0ZKAt1GAkjdEPP8sWch/pfcuR0c=
|
||||
github.com/aws/aws-sdk-go v1.44.174/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.44.194 h1:1ZDK+QDcc5oRbZGgRZSz561eR8XVizXCeGpoZKo33NU=
|
||||
github.com/aws/aws-sdk-go v1.44.194/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
|
||||
@ -56,18 +53,28 @@ 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/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=
|
||||
github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
|
||||
github.com/caarlos0/env/v7 v7.0.0/go.mod h1:LPPWniDUq4JaO6Q41vtlyikhMknqymCLBw0eX4dcH1E=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
|
||||
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/caarlos0/env/v7 v7.1.0 h1:9lzTF5amyQeWHZzuZeKlCb5FWSUxpG1js43mhbY8ozg=
|
||||
github.com/caarlos0/env/v7 v7.1.0/go.mod h1:LPPWniDUq4JaO6Q41vtlyikhMknqymCLBw0eX4dcH1E=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI=
|
||||
github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/deckarep/golang-set/v2 v2.2.0 h1:2pMQd3Soi6qfw7E5MMKaEh5W5ES18bW3AbFFnGl6LgQ=
|
||||
github.com/deckarep/golang-set/v2 v2.2.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g=
|
||||
github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
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/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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
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=
|
||||
@ -77,27 +84,31 @@ github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURU
|
||||
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.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
|
||||
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
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.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
|
||||
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
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/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
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/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/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
||||
@ -108,6 +119,7 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
@ -117,18 +129,18 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
||||
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/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=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
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.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8=
|
||||
github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
|
||||
github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels=
|
||||
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/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=
|
||||
@ -141,8 +153,13 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@ -158,13 +175,15 @@ github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ic
|
||||
github.com/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ=
|
||||
github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
@ -173,12 +192,14 @@ github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||
github.com/orzogc/fake115uploader v0.3.3-0.20221009101310-08b764073b77 h1:dg/EaaJLPIg4xn2kaZil7Ax3wfoxcFXaBwyOTlcz5AI=
|
||||
github.com/orzogc/fake115uploader v0.3.3-0.20221009101310-08b764073b77/go.mod h1:FD9a09Vw07CSMTdT0Y7ttStOa1WZsnPBslliMw2DkeM=
|
||||
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.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
|
||||
@ -196,6 +217,7 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@ -214,11 +236,18 @@ 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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20220725095014-c4e0c2b5debf h1:Y43S3e9P1NPs/QF4R5/SdlXj2d31540hP4Gk8VKNvDg=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20220725095014-c4e0c2b5debf/go.mod h1:c+cGNU1qi9bO7ZF4IRMYk+KaZTNiQ/gQrSbyMmGFq1Q=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca h1:I9rVnNXdIkij4UvMT7OmKhH9sOIvS8iXkxfPdnn9wQA=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=
|
||||
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.4.1 h1:l5ClIwL3N2LaH1zF3xivb3kP2HW95eyG5xhHE1JdZ9Y=
|
||||
github.com/u2takey/ffmpeg-go v0.4.1/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc=
|
||||
github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys=
|
||||
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 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
|
||||
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/upyun/go-sdk/v3 v3.0.3 h1:2wUkNk2fyJReMYHMvJyav050D83rYwSjN7mEPR0Pp8Q=
|
||||
github.com/upyun/go-sdk/v3 v3.0.3/go.mod h1:P/SnuuwhrIgAVRd/ZpzDWqCsBAf/oHg7UggbAxyZa0E=
|
||||
github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc=
|
||||
@ -227,40 +256,46 @@ github.com/winfsp/cgofuse v1.5.0/go.mod h1:h3awhoUOcn2VYVKCwDaYxSLlZwnyK+A8KaDoL
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
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-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=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
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.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg=
|
||||
golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
|
||||
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
||||
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||
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-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=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -273,29 +308,32 @@ 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.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/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=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y=
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
@ -312,6 +350,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
@ -319,16 +358,16 @@ 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.5 h1:u1lytId4+o9dDaNcPCFzNv7h6wvmc92UjNk3z8enSBU=
|
||||
gorm.io/driver/mysql v1.4.5/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
|
||||
gorm.io/driver/postgres v1.4.6 h1:1FPESNXqIKG5JmraaH2bfCVlMQ7paLoCreFxDtqzwdc=
|
||||
gorm.io/driver/postgres v1.4.6/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4=
|
||||
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.3 h1:WL2ifUmzR/SLp85CSURAfybcHnGZ+yLSGSxgYXlFBHg=
|
||||
gorm.io/gorm v1.24.3/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=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
|
@ -1,13 +1,17 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/cmd/flags"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/caarlos0/env/v6"
|
||||
"github.com/caarlos0/env/v7"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -49,7 +53,7 @@ func InitConfig() {
|
||||
if err != nil {
|
||||
log.Fatalf("marshal config error: %+v", err)
|
||||
}
|
||||
err = os.WriteFile(configPath, confBody, 0777)
|
||||
err = os.WriteFile(configPath, confBody, 0o777)
|
||||
if err != nil {
|
||||
log.Fatalf("update config struct error: %+v", err)
|
||||
}
|
||||
@ -69,11 +73,15 @@ func InitConfig() {
|
||||
if err != nil {
|
||||
log.Errorln("failed delete temp file:", err)
|
||||
}
|
||||
err = os.MkdirAll(conf.Conf.TempDir, 0777)
|
||||
err = os.MkdirAll(conf.Conf.TempDir, 0o777)
|
||||
if err != nil {
|
||||
log.Fatalf("create temp dir error: %+v", err)
|
||||
}
|
||||
log.Debugf("config: %+v", conf.Conf)
|
||||
if conf.Conf.TlsInsecureSkipVerify {
|
||||
base.RestyClient = base.RestyClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
|
||||
}
|
||||
initURL()
|
||||
}
|
||||
|
||||
func confFromEnv() {
|
||||
@ -88,3 +96,14 @@ func confFromEnv() {
|
||||
log.Fatalf("load config from env error: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func initURL() {
|
||||
if !strings.Contains(conf.Conf.SiteURL, "://") {
|
||||
conf.Conf.SiteURL = utils.FixAndCleanPath(conf.Conf.SiteURL)
|
||||
}
|
||||
u, err := url.Parse(conf.Conf.SiteURL)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("can't parse site_url: %+v", err)
|
||||
}
|
||||
conf.URL = u
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ func InitialSettings() []model.SettingItem {
|
||||
"PDF.js":"https://alist-org.github.io/pdf.js/web/viewer.html?file=$e_url"
|
||||
},
|
||||
"epub": {
|
||||
"EPUB.js":"/static/epub.js/viewer.html?url=$e_url"
|
||||
"EPUB.js":"https://alist-org.github.io/static/epub.js/viewer.html?url=$e_url"
|
||||
}
|
||||
}`, Type: conf.TypeText, Group: model.PREVIEW},
|
||||
// {Key: conf.OfficeViewers, Value: `{
|
||||
@ -129,13 +129,14 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.CustomizeHead, Value: `<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.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},
|
||||
{Key: conf.SignAll, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.PrivacyRegs, Value: `(?:(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])
|
||||
([[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}|::|:(?::[[:xdigit:]]{1,4}){1,6}|[[:xdigit:]]{1,4}:(?::[[:xdigit:]]{1,4}){1,5}|(?:[[:xdigit:]]{1,4}:){2}(?::[[:xdigit:]]{1,4}){1,4}|(?:[[:xdigit:]]{1,4}:){3}(?::[[:xdigit:]]{1,4}){1,3}|(?:[[:xdigit:]]{1,4}:){4}(?::[[:xdigit:]]{1,4}){1,2}|(?:[[:xdigit:]]{1,4}:){5}:[[:xdigit:]]{1,4}|(?:[[:xdigit:]]{1,4}:){1,6}:)
|
||||
(?U)access_token=(.*)&`,
|
||||
Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.OcrApi, Value: "https://api.nn.ci/ocr/file/json", Type: conf.TypeString, Group: model.GLOBAL},
|
||||
{Key: conf.FilenameCharMapping, Value: `{"/": "|"}`, Type: conf.TypeText, Group: model.GLOBAL},
|
||||
{Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL},
|
||||
|
||||
// aria2 settings
|
||||
{Key: conf.Aria2Uri, Value: "http://localhost:6800/jsonrpc", Type: conf.TypeString, Group: model.ARIA2, Flag: model.PRIVATE},
|
||||
@ -149,10 +150,15 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.MaxIndexDepth, Value: "20", Type: conf.TypeNumber, Group: model.INDEX, Flag: model.PRIVATE, Help: `max depth of index`},
|
||||
{Key: conf.IndexProgress, Value: "{}", Type: conf.TypeText, Group: model.SINGLE, Flag: model.PRIVATE},
|
||||
|
||||
// GitHub settings
|
||||
{Key: conf.GithubClientId, Value: "", Type: conf.TypeString, Group: model.GITHUB, Flag: model.PRIVATE},
|
||||
{Key: conf.GithubClientSecrets, Value: "", Type: conf.TypeString, Group: model.GITHUB, Flag: model.PRIVATE},
|
||||
{Key: conf.GithubLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GITHUB, Flag: model.PUBLIC},
|
||||
// SSO settings
|
||||
{Key: conf.SSOLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.SSO, Flag: model.PUBLIC},
|
||||
{Key: conf.SSOLoginplatform, Type: conf.TypeSelect, Options: "Github,Microsoft,Google,Dingtalk", Group: model.SSO, Flag: model.PUBLIC},
|
||||
{Key: conf.SSOClientId, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSOClientSecret, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
|
||||
// qbittorrent settings
|
||||
{Key: conf.QbittorrentUrl, Value: "http://admin:adminadmin@localhost:8080/", Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},
|
||||
{Key: conf.QbittorrentSeedtime, Value: "0", Type: conf.TypeNumber, Group: model.SINGLE, Flag: model.PRIVATE},
|
||||
}
|
||||
if flags.Dev {
|
||||
initialSettingItems = append(initialSettingItems, []model.SettingItem{
|
||||
|
@ -46,7 +46,7 @@ func Log() {
|
||||
MaxAge: logConfig.MaxAge, //days
|
||||
Compress: logConfig.Compress, // disabled by default
|
||||
}
|
||||
if flags.Debug || flags.Dev {
|
||||
if flags.Debug || flags.Dev || flags.LogStd {
|
||||
w = io.MultiWriter(os.Stdout, w)
|
||||
}
|
||||
logrus.SetOutput(w)
|
||||
|
15
internal/bootstrap/qbittorrent.go
Normal file
15
internal/bootstrap/qbittorrent.go
Normal file
@ -0,0 +1,15 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/qbittorrent"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
func InitQbittorrent() {
|
||||
go func() {
|
||||
err := qbittorrent.InitClient()
|
||||
if err != nil {
|
||||
utils.Log.Infof("qbittorrent not ready.")
|
||||
}
|
||||
}()
|
||||
}
|
@ -48,6 +48,7 @@ type Config struct {
|
||||
BleveDir string `json:"bleve_dir" env:"BLEVE_DIR"`
|
||||
Log LogConfig `json:"log"`
|
||||
MaxConnections int `json:"max_connections" env:"MAX_CONNECTIONS"`
|
||||
TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" env:"TLS_INSECURE_SKIP_VERIFY"`
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
@ -76,5 +77,6 @@ func DefaultConfig() *Config {
|
||||
MaxAge: 28,
|
||||
},
|
||||
MaxConnections: 0,
|
||||
TlsInsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ const (
|
||||
PrivacyRegs = "privacy_regs"
|
||||
OcrApi = "ocr_api"
|
||||
FilenameCharMapping = "filename_char_mapping"
|
||||
ForwardDirectLinkParams = "forward_direct_link_params"
|
||||
|
||||
// index
|
||||
SearchIndex = "search_index"
|
||||
@ -53,10 +54,15 @@ const (
|
||||
Token = "token"
|
||||
IndexProgress = "index_progress"
|
||||
|
||||
//Github
|
||||
GithubClientId = "github_client_id"
|
||||
GithubClientSecrets = "github_client_secrets"
|
||||
GithubLoginEnabled = "github_login_enabled"
|
||||
//SSO
|
||||
SSOClientId = "sso_client_id"
|
||||
SSOClientSecret = "sso_client_secret"
|
||||
SSOLoginEnabled = "sso_login_enabled"
|
||||
SSOLoginplatform = "sso_login_platform"
|
||||
|
||||
// qbittorrent
|
||||
QbittorrentUrl = "qbittorrent_url"
|
||||
QbittorrentSeedtime = "qbittorrent_seedtime"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -1,6 +1,9 @@
|
||||
package conf
|
||||
|
||||
import "regexp"
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
BuiltAt string
|
||||
@ -13,6 +16,7 @@ var (
|
||||
|
||||
var (
|
||||
Conf *Config
|
||||
URL *url.URL
|
||||
)
|
||||
|
||||
var SlicesMap = make(map[string][]string)
|
||||
|
@ -51,6 +51,15 @@ func GetStorageById(id uint) (*model.Storage, error) {
|
||||
return &storage, nil
|
||||
}
|
||||
|
||||
// GetStorageByMountPath Get Storage by mountPath, used to update storage usually
|
||||
func GetStorageByMountPath(mountPath string) (*model.Storage, error) {
|
||||
var storage model.Storage
|
||||
if err := db.Where("mount_path = ?", mountPath).First(&storage).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return &storage, nil
|
||||
}
|
||||
|
||||
func GetEnabledStorages() ([]model.Storage, error) {
|
||||
var storages []model.Storage
|
||||
if err := db.Where(fmt.Sprintf("%s = ?", columnName("disabled")), false).Find(&storages).Error; err != nil {
|
||||
|
@ -21,10 +21,10 @@ func GetUserByName(username string) (*model.User, error) {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func GetUserByGithubID(githubID int) (*model.User, error) {
|
||||
user := model.User{GithubID: githubID}
|
||||
func GetUserBySSOID(ssoID string) (*model.User, error) {
|
||||
user := model.User{SsoID: ssoID}
|
||||
if err := db.Where(user).First(&user).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "The Github ID is not associated with a user")
|
||||
return nil, errors.Wrapf(err, "The single sign on platform is not bound to any users")
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
@ -9,7 +9,9 @@ type Config struct {
|
||||
NoUpload bool `json:"no_upload"`
|
||||
NeedMs bool `json:"need_ms"` // if need get message from user, such as validate code
|
||||
DefaultRoot string `json:"default_root"`
|
||||
CheckStatus bool
|
||||
CheckStatus bool `json:"-"`
|
||||
Alert string `json:"alert"` //info,success,warning,danger
|
||||
NoOverwriteUpload bool `json:"-"`
|
||||
}
|
||||
|
||||
func (c Config) MustProxy() bool {
|
||||
|
@ -2,9 +2,12 @@ package fs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -13,5 +16,14 @@ func link(ctx context.Context, path string, args model.LinkArgs) (*model.Link, m
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "failed get storage")
|
||||
}
|
||||
return op.Link(ctx, storage, actualPath, args)
|
||||
l, obj, err := op.Link(ctx, storage, actualPath, args)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "failed link")
|
||||
}
|
||||
if l.URL != "" && !strings.HasPrefix(l.URL, "http://") && !strings.HasPrefix(l.URL, "https://") {
|
||||
if c, ok := ctx.(*gin.Context); ok {
|
||||
l.URL = common.GetApiUrl(c.Request) + l.URL
|
||||
}
|
||||
}
|
||||
return l, obj, nil
|
||||
}
|
||||
|
@ -41,9 +41,9 @@ func getFileStreamFromLink(file model.Obj, link *model.Link) (*model.FileStream,
|
||||
if link.Data != nil {
|
||||
rc = link.Data
|
||||
} else if link.FilePath != nil {
|
||||
// copy a new temp, because will be deleted after upload
|
||||
// create a new temp symbolic link, because it will be deleted after upload
|
||||
newFilePath := stdpath.Join(conf.Conf.TempDir, fmt.Sprintf("%s-%s", uuid.NewString(), file.GetName()))
|
||||
err := utils.CopyFile(*link.FilePath, newFilePath)
|
||||
err := utils.SymlinkOrCopyFile(*link.FilePath, newFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ const (
|
||||
GLOBAL
|
||||
ARIA2
|
||||
INDEX
|
||||
GITHUB
|
||||
SSO
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -30,9 +30,10 @@ type User struct {
|
||||
// 7: can remove
|
||||
// 8: webdav read
|
||||
// 9: webdav write
|
||||
// 10: can add qbittorrent tasks
|
||||
Permission int32 `json:"permission"`
|
||||
OtpSecret string `json:"-"`
|
||||
GithubID int `json:"github_id"`
|
||||
SsoID string `json:"sso_id"`
|
||||
}
|
||||
|
||||
func (u User) IsGuest() bool {
|
||||
@ -93,6 +94,10 @@ func (u User) CanWebdavManage() bool {
|
||||
return u.IsAdmin() || (u.Permission>>9)&1 == 1
|
||||
}
|
||||
|
||||
func (u User) CanAddQbittorrentTasks() bool {
|
||||
return u.IsAdmin() || (u.Permission>>10)&1 == 1
|
||||
}
|
||||
|
||||
func (u User) JoinPath(reqPath string) (string, error) {
|
||||
return utils.JoinBasePath(u.BasePath, reqPath)
|
||||
}
|
||||
|
@ -2,5 +2,6 @@ package op
|
||||
|
||||
const (
|
||||
WORK = "work"
|
||||
DISABLED = "disabled"
|
||||
RootName = "root"
|
||||
)
|
||||
|
@ -459,6 +459,7 @@ func Remove(ctx context.Context, storage driver.Driver, path string) error {
|
||||
if err != nil {
|
||||
// if object not found, it's ok
|
||||
if errs.IsObjectNotFound(err) {
|
||||
log.Debugf("%s have been removed", path)
|
||||
return nil
|
||||
}
|
||||
return errors.WithMessage(err, "failed to get object")
|
||||
@ -497,6 +498,8 @@ func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file *mo
|
||||
// if file exist and size = 0, delete it
|
||||
dstDirPath = utils.FixAndCleanPath(dstDirPath)
|
||||
dstPath := stdpath.Join(dstDirPath, file.GetName())
|
||||
tempName := file.GetName() + ".alist_to_delete"
|
||||
tempPath := stdpath.Join(dstDirPath, tempName)
|
||||
fi, err := GetUnwrap(ctx, storage, dstPath)
|
||||
if err == nil {
|
||||
if fi.GetSize() == 0 {
|
||||
@ -504,6 +507,12 @@ func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file *mo
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed remove file that exist and have size 0")
|
||||
}
|
||||
} else if storage.Config().NoOverwriteUpload {
|
||||
// try to rename old obj
|
||||
err = Rename(ctx, storage, dstPath, tempName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
file.Old = fi
|
||||
}
|
||||
@ -542,10 +551,20 @@ func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file *mo
|
||||
return errs.NotImplement
|
||||
}
|
||||
log.Debugf("put file [%s] done", file.GetName())
|
||||
//if err == nil {
|
||||
// //clear cache
|
||||
// key := stdpath.Join(storage.GetStorage().MountPath, dstDirPath)
|
||||
// listCache.Del(key)
|
||||
//}
|
||||
if storage.Config().NoOverwriteUpload && fi != nil && fi.GetSize() > 0 {
|
||||
if err != nil {
|
||||
// upload failed, recover old obj
|
||||
err := Rename(ctx, storage, tempPath, file.GetName())
|
||||
if err != nil {
|
||||
log.Errorf("failed recover old obj: %+v", err)
|
||||
}
|
||||
} else {
|
||||
// upload success, remove old obj
|
||||
err := Remove(ctx, storage, tempPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -15,6 +15,10 @@ func GetStorageAndActualPath(rawPath string) (storage driver.Driver, actualPath
|
||||
rawPath = utils.FixAndCleanPath(rawPath)
|
||||
storage = GetBalancedStorage(rawPath)
|
||||
if storage == nil {
|
||||
if rawPath == "/" {
|
||||
err = errors.New("please add a storage first.")
|
||||
return
|
||||
}
|
||||
err = errors.Errorf("can't find storage with rawPath: %s", rawPath)
|
||||
return
|
||||
}
|
||||
|
@ -142,6 +142,7 @@ func DisableStorage(ctx context.Context, id uint) error {
|
||||
}
|
||||
// delete the storage in the memory
|
||||
storage.Disabled = true
|
||||
storage.SetStatus(DISABLED)
|
||||
err = db.UpdateStorage(storage)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed update storage in db")
|
||||
|
60
internal/qbittorrent/add.go
Normal file
60
internal/qbittorrent/add.go
Normal file
@ -0,0 +1,60 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/task"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func AddURL(ctx context.Context, url string, dstDirPath string) error {
|
||||
// check storage
|
||||
storage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed get storage")
|
||||
}
|
||||
// check is it could upload
|
||||
if storage.Config().NoUpload {
|
||||
return errors.WithStack(errs.UploadNotSupported)
|
||||
}
|
||||
// check path is valid
|
||||
obj, err := op.Get(ctx, storage, dstDirActualPath)
|
||||
if err != nil {
|
||||
if !errs.IsObjectNotFound(err) {
|
||||
return errors.WithMessage(err, "failed get object")
|
||||
}
|
||||
} else {
|
||||
if !obj.IsDir() {
|
||||
// can't add to a file
|
||||
return errors.WithStack(errs.NotFolder)
|
||||
}
|
||||
}
|
||||
// call qbittorrent
|
||||
id := uuid.NewString()
|
||||
tempDir := filepath.Join(conf.Conf.TempDir, "qbittorrent", id)
|
||||
err = qbclient.AddFromLink(url, tempDir, id)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to add url %s", url)
|
||||
}
|
||||
DownTaskManager.Submit(task.WithCancelCtx(&task.Task[string]{
|
||||
ID: id,
|
||||
Name: fmt.Sprintf("download %s to [%s](%s)", url, storage.GetStorage().MountPath, dstDirActualPath),
|
||||
Func: func(tsk *task.Task[string]) error {
|
||||
m := &Monitor{
|
||||
tsk: tsk,
|
||||
tempDir: tempDir,
|
||||
dstDirPath: dstDirPath,
|
||||
seedtime: setting.GetInt(conf.QbittorrentSeedtime, 0),
|
||||
}
|
||||
return m.Loop()
|
||||
},
|
||||
}))
|
||||
return nil
|
||||
}
|
364
internal/qbittorrent/client.go
Normal file
364
internal/qbittorrent/client.go
Normal file
@ -0,0 +1,364 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
AddFromLink(link string, savePath string, id string) error
|
||||
GetInfo(id string) (TorrentInfo, error)
|
||||
GetFiles(id string) ([]FileInfo, error)
|
||||
Delete(id string, deleteFiles bool) error
|
||||
}
|
||||
|
||||
type client struct {
|
||||
url *url.URL
|
||||
client http.Client
|
||||
Client
|
||||
}
|
||||
|
||||
func New(webuiUrl string) (Client, error) {
|
||||
u, err := url.Parse(webuiUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c = &client{
|
||||
url: u,
|
||||
client: http.Client{Jar: jar},
|
||||
}
|
||||
|
||||
err = c.checkAuthorization()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *client) checkAuthorization() error {
|
||||
// check authorization
|
||||
if c.authorized() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check authorization after logging in
|
||||
err := c.login()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.authorized() {
|
||||
return nil
|
||||
}
|
||||
return errors.New("unauthorized qbittorrent url")
|
||||
}
|
||||
|
||||
func (c *client) authorized() bool {
|
||||
resp, err := c.post("/api/v2/app/version", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return resp.StatusCode == 200 // the status code will be 403 if not authorized
|
||||
}
|
||||
|
||||
func (c *client) login() error {
|
||||
// prepare HTTP request
|
||||
v := url.Values{}
|
||||
v.Set("username", c.url.User.Username())
|
||||
passwd, _ := c.url.User.Password()
|
||||
v.Set("password", passwd)
|
||||
resp, err := c.post("/api/v2/auth/login", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check result
|
||||
body := make([]byte, 2)
|
||||
_, err = resp.Body.Read(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(body) != "Ok" {
|
||||
return errors.New("failed to login into qBittorrent webui with url: " + c.url.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) post(path string, data url.Values) (*http.Response, error) {
|
||||
u := c.url.JoinPath(path)
|
||||
u.User = nil // remove userinfo for requests
|
||||
|
||||
req, err := http.NewRequest("POST", u.String(), bytes.NewReader([]byte(data.Encode())))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data != nil {
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Cookies() != nil {
|
||||
c.client.Jar.SetCookies(u, resp.Cookies())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *client) AddFromLink(link string, savePath string, id string) error {
|
||||
err := c.checkAuthorization()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
writer := multipart.NewWriter(buf)
|
||||
|
||||
addField := func(name string, value string) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = writer.WriteField(name, value)
|
||||
}
|
||||
addField("urls", link)
|
||||
addField("savepath", savePath)
|
||||
addField("tags", "alist-"+id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := c.url.JoinPath("/api/v2/torrents/add")
|
||||
u.User = nil // remove userinfo for requests
|
||||
req, err := http.NewRequest("POST", u.String(), buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check result
|
||||
body := make([]byte, 2)
|
||||
_, err = resp.Body.Read(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 || string(body) != "Ok" {
|
||||
return errors.New("failed to add qBittorrent task: " + link)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TorrentStatus string
|
||||
|
||||
const (
|
||||
ERROR TorrentStatus = "error"
|
||||
MISSINGFILES TorrentStatus = "missingFiles"
|
||||
UPLOADING TorrentStatus = "uploading"
|
||||
PAUSEDUP TorrentStatus = "pausedUP"
|
||||
QUEUEDUP TorrentStatus = "queuedUP"
|
||||
STALLEDUP TorrentStatus = "stalledUP"
|
||||
CHECKINGUP TorrentStatus = "checkingUP"
|
||||
FORCEDUP TorrentStatus = "forcedUP"
|
||||
ALLOCATING TorrentStatus = "allocating"
|
||||
DOWNLOADING TorrentStatus = "downloading"
|
||||
METADL TorrentStatus = "metaDL"
|
||||
PAUSEDDL TorrentStatus = "pausedDL"
|
||||
QUEUEDDL TorrentStatus = "queuedDL"
|
||||
STALLEDDL TorrentStatus = "stalledDL"
|
||||
CHECKINGDL TorrentStatus = "checkingDL"
|
||||
FORCEDDL TorrentStatus = "forcedDL"
|
||||
CHECKINGRESUMEDATA TorrentStatus = "checkingResumeData"
|
||||
MOVING TorrentStatus = "moving"
|
||||
UNKNOWN TorrentStatus = "unknown"
|
||||
)
|
||||
|
||||
// https://github.com/DGuang21/PTGo/blob/main/app/client/client_distributer.go
|
||||
type TorrentInfo struct {
|
||||
AddedOn int `json:"added_on"` // 将 torrent 添加到客户端的时间(Unix Epoch)
|
||||
AmountLeft int64 `json:"amount_left"` // 剩余大小(字节)
|
||||
AutoTmm bool `json:"auto_tmm"` // 此 torrent 是否由 Automatic Torrent Management 管理
|
||||
Availability float64 `json:"availability"` // 当前百分比
|
||||
Category string `json:"category"` //
|
||||
Completed int64 `json:"completed"` // 完成的传输数据量(字节)
|
||||
CompletionOn int `json:"completion_on"` // Torrent 完成的时间(Unix Epoch)
|
||||
ContentPath string `json:"content_path"` // torrent 内容的绝对路径(多文件 torrent 的根路径,单文件 torrent 的绝对文件路径)
|
||||
DlLimit int `json:"dl_limit"` // Torrent 下载速度限制(字节/秒)
|
||||
Dlspeed int `json:"dlspeed"` // Torrent 下载速度(字节/秒)
|
||||
Downloaded int64 `json:"downloaded"` // 已经下载大小
|
||||
DownloadedSession int64 `json:"downloaded_session"` // 此会话下载的数据量
|
||||
Eta int `json:"eta"` //
|
||||
FLPiecePrio bool `json:"f_l_piece_prio"` // 如果第一个最后一块被优先考虑,则为true
|
||||
ForceStart bool `json:"force_start"` // 如果为此 torrent 启用了强制启动,则为true
|
||||
Hash string `json:"hash"` //
|
||||
LastActivity int `json:"last_activity"` // 上次活跃的时间(Unix Epoch)
|
||||
MagnetURI string `json:"magnet_uri"` // 与此 torrent 对应的 Magnet URI
|
||||
MaxRatio int `json:"max_ratio"` // 种子/上传停止种子前的最大共享比率
|
||||
MaxSeedingTime int `json:"max_seeding_time"` // 停止种子种子前的最长种子时间(秒)
|
||||
Name string `json:"name"` //
|
||||
NumComplete int `json:"num_complete"` //
|
||||
NumIncomplete int `json:"num_incomplete"` //
|
||||
NumLeechs int `json:"num_leechs"` // 连接到的 leechers 的数量
|
||||
NumSeeds int `json:"num_seeds"` // 连接到的种子数
|
||||
Priority int `json:"priority"` // 速度优先。如果队列被禁用或 torrent 处于种子模式,则返回 -1
|
||||
Progress float64 `json:"progress"` // 进度
|
||||
Ratio float64 `json:"ratio"` // Torrent 共享比率
|
||||
RatioLimit int `json:"ratio_limit"` //
|
||||
SavePath string `json:"save_path"`
|
||||
SeedingTime int `json:"seeding_time"` // Torrent 完成用时(秒)
|
||||
SeedingTimeLimit int `json:"seeding_time_limit"` // max_seeding_time
|
||||
SeenComplete int `json:"seen_complete"` // 上次 torrent 完成的时间
|
||||
SeqDl bool `json:"seq_dl"` // 如果启用顺序下载,则为true
|
||||
Size int64 `json:"size"` //
|
||||
State TorrentStatus `json:"state"` // 参见https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list
|
||||
SuperSeeding bool `json:"super_seeding"` // 如果启用超级播种,则为true
|
||||
Tags string `json:"tags"` // Torrent 的逗号连接标签列表
|
||||
TimeActive int `json:"time_active"` // 总活动时间(秒)
|
||||
TotalSize int64 `json:"total_size"` // 此 torrent 中所有文件的总大小(字节)(包括未选择的文件)
|
||||
Tracker string `json:"tracker"` // 第一个具有工作状态的tracker。如果没有tracker在工作,则返回空字符串。
|
||||
TrackersCount int `json:"trackers_count"` //
|
||||
UpLimit int `json:"up_limit"` // 上传限制
|
||||
Uploaded int64 `json:"uploaded"` // 累计上传
|
||||
UploadedSession int64 `json:"uploaded_session"` // 当前session累计上传
|
||||
Upspeed int `json:"upspeed"` // 上传速度(字节/秒)
|
||||
}
|
||||
|
||||
type InfoNotFoundError struct {
|
||||
Id string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (i InfoNotFoundError) Error() string {
|
||||
return "there should be exactly one task with tag \"alist-" + i.Id + "\""
|
||||
}
|
||||
|
||||
func NewInfoNotFoundError(id string) InfoNotFoundError {
|
||||
return InfoNotFoundError{Id: id}
|
||||
}
|
||||
|
||||
func (c *client) GetInfo(id string) (TorrentInfo, error) {
|
||||
var infos []TorrentInfo
|
||||
|
||||
err := c.checkAuthorization()
|
||||
if err != nil {
|
||||
return TorrentInfo{}, err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("tag", "alist-"+id)
|
||||
response, err := c.post("/api/v2/torrents/info", v)
|
||||
if err != nil {
|
||||
return TorrentInfo{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return TorrentInfo{}, err
|
||||
}
|
||||
err = utils.Json.Unmarshal(body, &infos)
|
||||
if err != nil {
|
||||
return TorrentInfo{}, err
|
||||
}
|
||||
if len(infos) != 1 {
|
||||
return TorrentInfo{}, NewInfoNotFoundError(id)
|
||||
}
|
||||
return infos[0], nil
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
Index int `json:"index"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Progress float32 `json:"progress"`
|
||||
Priority int `json:"priority"`
|
||||
IsSeed bool `json:"is_seed"`
|
||||
PieceRange []int `json:"piece_range"`
|
||||
Availability float32 `json:"availability"`
|
||||
}
|
||||
|
||||
func (c *client) GetFiles(id string) ([]FileInfo, error) {
|
||||
var infos []FileInfo
|
||||
|
||||
err := c.checkAuthorization()
|
||||
if err != nil {
|
||||
return []FileInfo{}, err
|
||||
}
|
||||
|
||||
tInfo, err := c.GetInfo(id)
|
||||
if err != nil {
|
||||
return []FileInfo{}, err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("hash", tInfo.Hash)
|
||||
response, err := c.post("/api/v2/torrents/files", v)
|
||||
if err != nil {
|
||||
return []FileInfo{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return []FileInfo{}, err
|
||||
}
|
||||
err = utils.Json.Unmarshal(body, &infos)
|
||||
if err != nil {
|
||||
return []FileInfo{}, err
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
func (c *client) Delete(id string, deleteFiles bool) error {
|
||||
err := c.checkAuthorization()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := c.GetInfo(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := url.Values{}
|
||||
v.Set("hashes", info.Hash)
|
||||
if deleteFiles {
|
||||
v.Set("deleteFiles", "true")
|
||||
} else {
|
||||
v.Set("deleteFiles", "false")
|
||||
}
|
||||
response, err := c.post("/api/v2/torrents/delete", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != 200 {
|
||||
return errors.New("failed to delete qbittorrent task")
|
||||
}
|
||||
|
||||
v = url.Values{}
|
||||
v.Set("tags", "alist-"+id)
|
||||
response, err = c.post("/api/v2/torrents/deleteTags", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != 200 {
|
||||
return errors.New("failed to delete qbittorrent tag")
|
||||
}
|
||||
return nil
|
||||
}
|
154
internal/qbittorrent/client_test.go
Normal file
154
internal/qbittorrent/client_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
// test logging in with wrong password
|
||||
u, err := url.Parse("http://admin:admin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var c = &client{
|
||||
url: u,
|
||||
client: http.Client{Jar: jar},
|
||||
}
|
||||
err = c.login()
|
||||
if err == nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// test logging in with correct password
|
||||
u, err = url.Parse("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
c.url = u
|
||||
err = c.login()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// in this test, the `Bypass authentication for clients on localhost` option in qBittorrent webui should be disabled
|
||||
func TestAuthorized(t *testing.T) {
|
||||
// init client
|
||||
u, err := url.Parse("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var c = &client{
|
||||
url: u,
|
||||
client: http.Client{Jar: jar},
|
||||
}
|
||||
|
||||
// test without logging in, which should be unauthorized
|
||||
authorized := c.authorized()
|
||||
if authorized {
|
||||
t.Error("Should not be authorized")
|
||||
}
|
||||
|
||||
// test after logging in
|
||||
err = c.login()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
authorized = c.authorized()
|
||||
if !authorized {
|
||||
t.Error("Should be authorized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
_, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = New("http://admin:wrong_password@127.0.0.1:8080/")
|
||||
if err == nil {
|
||||
t.Error("Should get an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
// init client
|
||||
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = c.AddFromLink(
|
||||
"https://releases.ubuntu.com/22.04/ubuntu-22.04.1-desktop-amd64.iso.torrent",
|
||||
"D:\\qBittorrentDownload\\alist",
|
||||
"uuid-1",
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = c.AddFromLink(
|
||||
"magnet:?xt=urn:btih:375ae3280cd80a8e9d7212e11dfaf7c45069dd35&dn=archlinux-2023.02.01-x86_64.iso",
|
||||
"D:\\qBittorrentDownload\\alist",
|
||||
"uuid-2",
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInfo(t *testing.T) {
|
||||
// init client
|
||||
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = c.GetInfo("uuid-1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFiles(t *testing.T) {
|
||||
// init client
|
||||
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
files, err := c.GetFiles("uuid-1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(files) != 1 {
|
||||
t.Error("should have exactly one file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
// init client
|
||||
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = c.AddFromLink(
|
||||
"https://releases.ubuntu.com/22.04/ubuntu-22.04.1-desktop-amd64.iso.torrent",
|
||||
"D:\\qBittorrentDownload\\alist",
|
||||
"uuid-1",
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = c.Delete("uuid-1", true)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
175
internal/qbittorrent/monitor.go
Normal file
175
internal/qbittorrent/monitor.go
Normal file
@ -0,0 +1,175 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/task"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
tsk *task.Task[string]
|
||||
tempDir string
|
||||
dstDirPath string
|
||||
seedtime int
|
||||
finish chan struct{}
|
||||
}
|
||||
|
||||
func (m *Monitor) Loop() error {
|
||||
var (
|
||||
err error
|
||||
completed bool
|
||||
)
|
||||
m.finish = make(chan struct{})
|
||||
|
||||
// wait for qbittorrent to parse torrent and create task
|
||||
m.tsk.SetStatus("waiting for qbittorrent to parse torrent and create task")
|
||||
waitCount := 0
|
||||
for {
|
||||
_, err := qbclient.GetInfo(m.tsk.ID)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
switch err.(type) {
|
||||
case InfoNotFoundError:
|
||||
break
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
waitCount += 1
|
||||
if waitCount >= 60 {
|
||||
return errors.New("torrent parse timeout")
|
||||
}
|
||||
timer := time.NewTimer(time.Second)
|
||||
<-timer.C
|
||||
}
|
||||
|
||||
outer:
|
||||
for {
|
||||
select {
|
||||
case <-m.tsk.Ctx.Done():
|
||||
// delete qbittorrent task and downloaded files when the task exits with error
|
||||
return qbclient.Delete(m.tsk.ID, true)
|
||||
case <-time.After(time.Second * 2):
|
||||
completed, err = m.update()
|
||||
if completed {
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.tsk.SetStatus("qbittorrent download completed, transferring")
|
||||
<-m.finish
|
||||
m.tsk.SetStatus("completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Monitor) update() (bool, error) {
|
||||
info, err := qbclient.GetInfo(m.tsk.ID)
|
||||
if err != nil {
|
||||
m.tsk.SetStatus("qbittorrent " + string(info.State))
|
||||
return true, err
|
||||
}
|
||||
|
||||
progress := float64(info.Completed) / float64(info.Size) * 100
|
||||
m.tsk.SetProgress(int(progress))
|
||||
switch info.State {
|
||||
case UPLOADING, PAUSEDUP, QUEUEDUP, STALLEDUP, FORCEDUP, CHECKINGUP:
|
||||
err = m.complete()
|
||||
return true, errors.WithMessage(err, "failed to transfer file")
|
||||
case ALLOCATING, DOWNLOADING, METADL, PAUSEDDL, QUEUEDDL, STALLEDDL, CHECKINGDL, FORCEDDL, CHECKINGRESUMEDATA, MOVING:
|
||||
m.tsk.SetStatus("qbittorrent downloading")
|
||||
return false, nil
|
||||
case ERROR, MISSINGFILES, UNKNOWN:
|
||||
return true, errors.Errorf("failed to download %s, error: %s", m.tsk.ID, info.State)
|
||||
}
|
||||
return true, errors.New("unknown error occurred downloading qbittorrent") // should never happen
|
||||
}
|
||||
|
||||
var TransferTaskManager = task.NewTaskManager(3, func(k *uint64) {
|
||||
atomic.AddUint64(k, 1)
|
||||
})
|
||||
|
||||
func (m *Monitor) complete() error {
|
||||
// check dstDir again
|
||||
storage, dstBaseDir, err := op.GetStorageAndActualPath(m.dstDirPath)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed get storage")
|
||||
}
|
||||
// get files
|
||||
files, err := qbclient.GetFiles(m.tsk.ID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get files of %s", m.tsk.ID)
|
||||
}
|
||||
log.Debugf("files len: %d", len(files))
|
||||
// delete qbittorrent task but do not delete the files before transferring to avoid qbittorrent
|
||||
// accessing downloaded files and throw `cannot access the file because it is being used by another process` error
|
||||
// err = qbclient.Delete(m.tsk.ID, false)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// upload files
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(files))
|
||||
go func() {
|
||||
wg.Wait()
|
||||
m.finish <- struct{}{}
|
||||
if m.seedtime < 0 {
|
||||
log.Debugf("do not delete qb task %s", m.tsk.ID)
|
||||
return
|
||||
}
|
||||
log.Debugf("delete qb task %s after %d minutes", m.tsk.ID, m.seedtime)
|
||||
<-time.After(time.Duration(m.seedtime) * time.Minute)
|
||||
err := qbclient.Delete(m.tsk.ID, true)
|
||||
if err != nil {
|
||||
log.Errorln(err.Error())
|
||||
}
|
||||
err = os.RemoveAll(m.tempDir)
|
||||
if err != nil {
|
||||
log.Errorf("failed to remove qbittorrent temp dir: %+v", err.Error())
|
||||
}
|
||||
}()
|
||||
for _, file := range files {
|
||||
tempPath := filepath.Join(m.tempDir, file.Name)
|
||||
dstPath := filepath.Join(dstBaseDir, file.Name)
|
||||
dstDir := filepath.Dir(dstPath)
|
||||
fileName := filepath.Base(dstPath)
|
||||
TransferTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
|
||||
Name: fmt.Sprintf("transfer %s to [%s](%s)", tempPath, storage.GetStorage().MountPath, dstPath),
|
||||
Func: func(tsk *task.Task[uint64]) error {
|
||||
defer wg.Done()
|
||||
size := file.Size
|
||||
mimetype := utils.GetMimeType(tempPath)
|
||||
f, err := os.Open(tempPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open file %s", tempPath)
|
||||
}
|
||||
stream := &model.FileStream{
|
||||
Obj: &model.Object{
|
||||
Name: fileName,
|
||||
Size: size,
|
||||
Modified: time.Now(),
|
||||
IsFolder: false,
|
||||
},
|
||||
ReadCloser: struct{ io.ReadSeekCloser }{f},
|
||||
Mimetype: mimetype,
|
||||
}
|
||||
return op.Put(tsk.Ctx, storage, dstDir, stream, tsk.SetProgress)
|
||||
},
|
||||
}))
|
||||
}
|
||||
return nil
|
||||
}
|
23
internal/qbittorrent/qbittorrent.go
Normal file
23
internal/qbittorrent/qbittorrent.go
Normal file
@ -0,0 +1,23 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/task"
|
||||
)
|
||||
|
||||
var DownTaskManager = task.NewTaskManager[string](3)
|
||||
var qbclient Client
|
||||
|
||||
func InitClient() error {
|
||||
var err error
|
||||
qbclient = nil
|
||||
|
||||
url := setting.GetStr(conf.QbittorrentUrl)
|
||||
qbclient, err = New(url)
|
||||
return err
|
||||
}
|
||||
|
||||
func IsQbittorrentReady() bool {
|
||||
return qbclient != nil
|
||||
}
|
@ -52,10 +52,12 @@ func updateIgnorePaths() {
|
||||
url := addition.Address + "/api/public/settings"
|
||||
res, err := base.RestyClient.R().Get(url)
|
||||
if err == nil {
|
||||
allowIndexed = utils.Json.Get(res.Body(), "data", conf.AllowIndexed).ToBool()
|
||||
log.Debugf("allow_indexed body: %+v", res.String())
|
||||
allowIndexed = utils.Json.Get(res.Body(), "data", conf.AllowIndexed).ToString() == "true"
|
||||
v3Visited[addition.Address] = allowIndexed
|
||||
}
|
||||
}
|
||||
log.Debugf("%s allow_indexed: %v", addition.Address, allowIndexed)
|
||||
if !allowIndexed {
|
||||
ignorePaths = append(ignorePaths, storage.GetStorage().MountPath)
|
||||
}
|
||||
|
107
pkg/http_range/range.go
Normal file
107
pkg/http_range/range.go
Normal file
@ -0,0 +1,107 @@
|
||||
// Package http_range implements http range parsing.
|
||||
package http_range
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Range specifies the byte range to be sent to the client.
|
||||
type Range struct {
|
||||
Start int64
|
||||
Length int64
|
||||
}
|
||||
|
||||
// ContentRange returns Content-Range header value.
|
||||
func (r Range) ContentRange(size int64) string {
|
||||
return fmt.Sprintf("bytes %d-%d/%d", r.Start, r.Start+r.Length-1, size)
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrNoOverlap is returned by ParseRange if first-byte-pos of
|
||||
// all of the byte-range-spec values is greater than the content size.
|
||||
ErrNoOverlap = errors.New("invalid range: failed to overlap")
|
||||
|
||||
// ErrInvalid is returned by ParseRange on invalid input.
|
||||
ErrInvalid = errors.New("invalid range")
|
||||
)
|
||||
|
||||
// ParseRange parses a Range header string as per RFC 7233.
|
||||
// ErrNoOverlap is returned if none of the ranges overlap.
|
||||
// ErrInvalid is returned if s is invalid range.
|
||||
func ParseRange(s string, size int64) ([]Range, error) { // nolint:gocognit
|
||||
if s == "" {
|
||||
return nil, nil // header not present
|
||||
}
|
||||
const b = "bytes="
|
||||
if !strings.HasPrefix(s, b) {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
var ranges []Range
|
||||
noOverlap := false
|
||||
for _, ra := range strings.Split(s[len(b):], ",") {
|
||||
ra = textproto.TrimString(ra)
|
||||
if ra == "" {
|
||||
continue
|
||||
}
|
||||
i := strings.Index(ra, "-")
|
||||
if i < 0 {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:])
|
||||
var r Range
|
||||
if start == "" {
|
||||
// If no start is specified, end specifies the
|
||||
// range start relative to the end of the file,
|
||||
// and we are dealing with <suffix-length>
|
||||
// which has to be a non-negative integer as per
|
||||
// RFC 7233 Section 2.1 "Byte-Ranges".
|
||||
if end == "" || end[0] == '-' {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if i < 0 || err != nil {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
if i > size {
|
||||
i = size
|
||||
}
|
||||
r.Start = size - i
|
||||
r.Length = size - r.Start
|
||||
} else {
|
||||
i, err := strconv.ParseInt(start, 10, 64)
|
||||
if err != nil || i < 0 {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
if i >= size {
|
||||
// If the range begins after the size of the content,
|
||||
// then it does not overlap.
|
||||
noOverlap = true
|
||||
continue
|
||||
}
|
||||
r.Start = i
|
||||
if end == "" {
|
||||
// If no end is specified, range extends to end of the file.
|
||||
r.Length = size - r.Start
|
||||
} else {
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil || r.Start > i {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
if i >= size {
|
||||
i = size - 1
|
||||
}
|
||||
r.Length = i - r.Start + 1
|
||||
}
|
||||
}
|
||||
ranges = append(ranges, r)
|
||||
}
|
||||
if noOverlap && len(ranges) == 0 {
|
||||
// The specified ranges did not overlap with the content.
|
||||
return nil, ErrNoOverlap
|
||||
}
|
||||
return ranges, nil
|
||||
}
|
@ -122,6 +122,10 @@ func (tm *Manager[K]) ClearDone() {
|
||||
tm.RemoveByStates(SUCCEEDED, CANCELED, ERRORED)
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) ClearSucceeded() {
|
||||
tm.RemoveByStates(SUCCEEDED)
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) RawTasks() *generic_sync.MapOf[K, *Task[K]] {
|
||||
return &tm.tasks
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ func CopyFile(src, dst string) error {
|
||||
}
|
||||
|
||||
// CopyDir Dir copies a whole directory recursively
|
||||
func CopyDir(src string, dst string) error {
|
||||
func CopyDir(src, dst string) error {
|
||||
var err error
|
||||
var fds []os.DirEntry
|
||||
var srcinfo os.FileInfo
|
||||
@ -71,6 +71,17 @@ func CopyDir(src string, dst string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SymlinkOrCopyFile symlinks a file or copy if symlink failed
|
||||
func SymlinkOrCopyFile(src, dst string) error {
|
||||
if err := CreateNestedDirectory(filepath.Dir(dst)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Symlink(src, dst); err != nil {
|
||||
return CopyFile(src, dst)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exists determine whether the file exists
|
||||
func Exists(name string) bool {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
@ -81,16 +92,21 @@ func Exists(name string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// CreateNestedDirectory create nested directory
|
||||
func CreateNestedDirectory(path string) error {
|
||||
err := os.MkdirAll(path, 0700)
|
||||
if err != nil {
|
||||
log.Errorf("can't create folder, %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateNestedFile create nested file
|
||||
func CreateNestedFile(path string) (*os.File, error) {
|
||||
basePath := filepath.Dir(path)
|
||||
if !Exists(basePath) {
|
||||
err := os.MkdirAll(basePath, 0700)
|
||||
if err != nil {
|
||||
log.Errorf("can't create folder, %s", err)
|
||||
if err := CreateNestedDirectory(basePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return os.Create(path)
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
@ -14,6 +15,12 @@ func GetSHA1Encode(data string) string {
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func GetSHA256Encode(data string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func GetMD5Encode(data string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(data))
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user