Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
8e2b9c681a | |||
0a8d710e01 | |||
d781f7127a | |||
85d743c5d2 | |||
5f60b51cf8 | |||
7013d1b7b8 | |||
9eec872637 | |||
037850bbd5 | |||
bbe3d4e19f | |||
78a9676c7c | |||
8bf93562eb | |||
b57afd0a98 | |||
f261ef50cc | |||
7e7b9b9b48 | |||
2313213f59 | |||
5f28532423 | |||
4cbbda8832 | |||
7bf5014417 | |||
b704bba444 | |||
eecea3febd | |||
0e246a7b0c | |||
b95df1d745 | |||
ec08ecdf6c | |||
479fc6d466 | |||
32ddab9b01 | |||
0c9dcec9cd | |||
793a4ea6ca | |||
c3c5181847 | |||
cd5a8a011d | |||
1756036a21 | |||
58c3cb3cf6 | |||
d8e190406a | |||
2880ed70ce | |||
0e86036874 | |||
e37465e67e | |||
d517adde71 | |||
8a18f47e68 | |||
cf08aa3668 | |||
9c84b6596f | |||
022e0ca292 | |||
88947f6676 | |||
b07ddfbc13 | |||
9a0a63d34c | |||
195c869272 | |||
bdfc1591bd | |||
82222840fe | |||
45e009a22c | |||
ac68079a76 | |||
2a17d0c2cd | |||
6f6a8e6dfc | |||
7d9ecba99c | |||
ae6984714d | |||
d0f88bd1cb | |||
f8b1f87a5f | |||
71e4e1ab6e | |||
7e6522c81e | |||
94a80bccfe |
44
.air.toml
Normal file
44
.air.toml
Normal file
@ -0,0 +1,44 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = ["server"]
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 0
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
18
.github/workflows/build_docker.yml
vendored
18
.github/workflows/build_docker.yml
vendored
@ -32,10 +32,21 @@ jobs:
|
||||
flavor: |
|
||||
suffix=-ffmpeg,onlatest=true
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
|
||||
- name: Cache Musl
|
||||
id: cache-musl
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: build/musl-libs
|
||||
key: docker-musl-libs
|
||||
|
||||
- name: Download Musl Library
|
||||
if: steps.cache-musl.outputs.cache-hit != 'true'
|
||||
run: bash build.sh prepare docker-multiplatform
|
||||
|
||||
- name: Build go binary
|
||||
run: bash build.sh dev docker-multiplatform
|
||||
|
||||
@ -63,10 +74,15 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
||||
|
||||
- name: Replace dockerfile tag
|
||||
run: |
|
||||
sed -i -e "s/latest/main/g" Dockerfile.ffmpeg
|
||||
|
||||
- name: Build and push with ffmpeg
|
||||
id: docker_build_ffmpeg
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ffmpeg
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
|
||||
|
14
.github/workflows/release_docker.yml
vendored
14
.github/workflows/release_docker.yml
vendored
@ -13,10 +13,21 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
|
||||
- name: Cache Musl
|
||||
id: cache-musl
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: build/musl-libs
|
||||
key: docker-musl-libs
|
||||
|
||||
- name: Download Musl Library
|
||||
if: steps.cache-musl.outputs.cache-hit != 'true'
|
||||
run: bash build.sh prepare docker-multiplatform
|
||||
|
||||
- name: Build go binary
|
||||
run: bash build.sh release docker-multiplatform
|
||||
|
||||
@ -62,6 +73,7 @@ jobs:
|
||||
id: docker_build_ffmpeg
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ffmpeg
|
||||
push: true
|
||||
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,6 +24,7 @@ output/
|
||||
*.json
|
||||
/build
|
||||
/data/
|
||||
/tmp/
|
||||
/log/
|
||||
/lang/
|
||||
/daemon/
|
||||
|
@ -3,7 +3,7 @@ ARG TARGETPLATFORM
|
||||
LABEL MAINTAINER="i@nn.ci"
|
||||
VOLUME /opt/alist/data/
|
||||
WORKDIR /opt/alist/
|
||||
COPY /${TARGETPLATFORM}/alist ./
|
||||
COPY /build/${TARGETPLATFORM}/alist ./
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN apk update && \
|
||||
apk upgrade --no-cache && \
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||
<p><em>🗂️A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>
|
||||
<div>
|
||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
||||
@ -76,6 +76,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
||||
- [X] Cloudreve
|
||||
- [x] [Dropbox](https://www.dropbox.com/)
|
||||
- [x] [FeijiPan](https://www.feijipan.com/)
|
||||
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
||||
- [x] Easy to deploy and out-of-the-box
|
||||
- [x] File preview (PDF, markdown, code, plain text, ...)
|
||||
- [x] Image preview in gallery mode
|
||||
@ -114,7 +115,7 @@ https://alist.nn.ci/guide/sponsor.html
|
||||
|
||||
### Special sponsors
|
||||
|
||||
- [VidHub](https://okaapps.com/product/1659622164?ref=alist) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
|
||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
|
||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
||||
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
|
||||
<div>
|
||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
||||
@ -75,6 +75,7 @@
|
||||
- [X] Cloudreve
|
||||
- [x] [Dropbox](https://www.dropbox.com/)
|
||||
- [x] [飞机盘](https://www.feijipan.com/)
|
||||
- [x] [多吉云](https://www.dogecloud.com/product/oss)
|
||||
- [x] 部署方便,开箱即用
|
||||
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
||||
- [x] 画廊模式下的图像预览
|
||||
@ -112,7 +113,7 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
|
||||
|
||||
### 特别赞助
|
||||
|
||||
- [VidHub](https://zh.okaapps.com/product/1659622164?ref=alist) - 苹果生态下优雅的网盘视频播放器,iPhone,iPad,Mac,Apple TV全平台支持。
|
||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器,iPhone,iPad,Mac,Apple TV全平台支持。
|
||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
|
||||
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||
<p><em>🗂️Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p>
|
||||
<div>
|
||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
||||
@ -76,6 +76,7 @@
|
||||
- [X] Cloudreve
|
||||
- [x] [Dropbox](https://www.dropbox.com/)
|
||||
- [x] [FeijiPan](https://www.feijipan.com/)
|
||||
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
||||
- [x] デプロイが簡単で、すぐに使える
|
||||
- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)
|
||||
- [x] ギャラリーモードでの画像プレビュー
|
||||
@ -114,7 +115,7 @@ https://alist.nn.ci/guide/sponsor.html
|
||||
|
||||
### スペシャルスポンサー
|
||||
|
||||
- [VidHub](https://okaapps.com/product/1659622164?ref=alist) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
|
||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
|
||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
||||
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
|
||||
|
||||
|
27
build.sh
27
build.sh
@ -96,17 +96,24 @@ BuildDocker() {
|
||||
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
|
||||
}
|
||||
|
||||
BuildDockerMultiplatform() {
|
||||
PrepareBuildDocker
|
||||
|
||||
PrepareBuildDockerMusl() {
|
||||
mkdir -p build/musl-libs
|
||||
BASE="https://musl.cc/"
|
||||
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross)
|
||||
for i in "${FILES[@]}"; do
|
||||
url="${BASE}${i}.tgz"
|
||||
curl -L -o "${i}.tgz" "${url}"
|
||||
sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
|
||||
rm -f "${i}.tgz"
|
||||
lib_tgz="build/${i}.tgz"
|
||||
curl -L -o "${lib_tgz}" "${url}"
|
||||
tar xf "${lib_tgz}" --strip-components 1 -C build/musl-libs
|
||||
rm -f "${lib_tgz}"
|
||||
done
|
||||
}
|
||||
|
||||
BuildDockerMultiplatform() {
|
||||
PrepareBuildDocker
|
||||
|
||||
# run PrepareBuildDockerMusl before build
|
||||
export PATH=$PATH:$PWD/build/musl-libs/bin
|
||||
|
||||
docker_lflags="--extldflags '-static -fpic' $ldflags"
|
||||
export CGO_ENABLED=1
|
||||
@ -122,7 +129,7 @@ BuildDockerMultiplatform() {
|
||||
export GOARCH=$arch
|
||||
export CC=${cgo_cc}
|
||||
echo "building for $os_arch"
|
||||
go build -o ./$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter .
|
||||
go build -o build/$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter .
|
||||
done
|
||||
|
||||
DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)
|
||||
@ -136,7 +143,7 @@ BuildDockerMultiplatform() {
|
||||
export GOARM=${GO_ARM[$i]}
|
||||
export CC=${cgo_cc}
|
||||
echo "building for $docker_arch"
|
||||
go build -o ./${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter .
|
||||
go build -o build/${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter .
|
||||
done
|
||||
}
|
||||
|
||||
@ -289,6 +296,10 @@ elif [ "$1" = "release" ]; then
|
||||
BuildRelease
|
||||
MakeRelease "md5.txt"
|
||||
fi
|
||||
elif [ "$1" = "prepare" ]; then
|
||||
if [ "$2" = "docker-multiplatform" ]; then
|
||||
PrepareBuildDockerMusl
|
||||
fi
|
||||
else
|
||||
echo -e "Parameter error"
|
||||
fi
|
||||
|
@ -91,6 +91,27 @@ the address is defined in config file`,
|
||||
}
|
||||
}()
|
||||
}
|
||||
if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
|
||||
s3r := gin.New()
|
||||
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
|
||||
server.InitS3(s3r)
|
||||
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
|
||||
utils.Log.Infof("start S3 server @ %s", s3Base)
|
||||
go func() {
|
||||
var err error
|
||||
if conf.Conf.S3.SSL {
|
||||
httpsSrv = &http.Server{Addr: s3Base, Handler: s3r}
|
||||
err = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||
}
|
||||
if !conf.Conf.S3.SSL {
|
||||
httpSrv = &http.Server{Addr: s3Base, Handler: s3r}
|
||||
err = httpSrv.ListenAndServe()
|
||||
}
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
utils.Log.Fatalf("failed to start s3 server: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
// Wait for interrupt signal to gracefully shutdown the server with
|
||||
// a timeout of 1 second.
|
||||
quit := make(chan os.Signal, 1)
|
||||
|
@ -19,7 +19,7 @@ var config = driver.Config{
|
||||
DefaultRoot: "0",
|
||||
//OnlyProxy: true,
|
||||
//OnlyLocal: true,
|
||||
NoOverwriteUpload: true,
|
||||
//NoOverwriteUpload: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -194,7 +194,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
}()
|
||||
if _, err = io.Copy(h, tempFile); err != nil {
|
||||
if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
|
@ -4,8 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"golang.org/x/time/rate"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
@ -19,6 +22,7 @@ import (
|
||||
type Pan123Share struct {
|
||||
model.Storage
|
||||
Addition
|
||||
apiRateLimit sync.Map
|
||||
}
|
||||
|
||||
func (d *Pan123Share) Config() driver.Config {
|
||||
@ -146,4 +150,11 @@ func (d *Pan123Share) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
func (d *Pan123Share) APIRateLimit(api string) bool {
|
||||
limiter, _ := d.apiRateLimit.LoadOrStore(api,
|
||||
rate.NewLimiter(rate.Every(time.Millisecond*700), 1))
|
||||
ins := limiter.(*rate.Limiter)
|
||||
return ins.Allow()
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Pan123Share)(nil)
|
||||
|
@ -7,10 +7,11 @@ import (
|
||||
|
||||
type Addition struct {
|
||||
ShareKey string `json:"sharekey" required:"true"`
|
||||
SharePwd string `json:"sharepassword" required:"true"`
|
||||
SharePwd string `json:"sharepassword"`
|
||||
driver.RootID
|
||||
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
AccessToken string `json:"accesstoken" type:"text"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -2,8 +2,15 @@ package _123Share
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
@ -15,20 +22,45 @@ const (
|
||||
Api = "https://www.123pan.com/api"
|
||||
AApi = "https://www.123pan.com/a/api"
|
||||
BApi = "https://www.123pan.com/b/api"
|
||||
MainApi = Api
|
||||
MainApi = BApi
|
||||
FileList = MainApi + "/share/get"
|
||||
DownloadInfo = MainApi + "/share/download/info"
|
||||
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
|
||||
)
|
||||
|
||||
func signPath(path string, os string, version string) (k string, v string) {
|
||||
table := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}
|
||||
random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
|
||||
now := time.Now().In(time.FixedZone("CST", 8*3600))
|
||||
timestamp := fmt.Sprint(now.Unix())
|
||||
nowStr := []byte(now.Format("200601021504"))
|
||||
for i := 0; i < len(nowStr); i++ {
|
||||
nowStr[i] = table[nowStr[i]-48]
|
||||
}
|
||||
timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
|
||||
data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
|
||||
dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
|
||||
return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
|
||||
}
|
||||
|
||||
func GetApi(rawUrl string) string {
|
||||
u, _ := url.Parse(rawUrl)
|
||||
query := u.Query()
|
||||
query.Add(signPath(u.Path, "web", "3"))
|
||||
u.RawQuery = query.Encode()
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
"origin": "https://www.123pan.com",
|
||||
"referer": "https://www.123pan.com/",
|
||||
"user-agent": "Dart/2.19(dart:io)",
|
||||
"platform": "android",
|
||||
"app-version": "36",
|
||||
"origin": "https://www.123pan.com",
|
||||
"referer": "https://www.123pan.com/",
|
||||
"authorization": "Bearer " + d.AccessToken,
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
|
||||
"platform": "web",
|
||||
"app-version": "3",
|
||||
//"user-agent": base.UserAgent,
|
||||
})
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
@ -36,7 +68,7 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
res, err := req.Execute(method, url)
|
||||
res, err := req.Execute(method, GetApi(url))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -52,6 +84,10 @@ func (d *Pan123Share) getFiles(parentId string) ([]File, error) {
|
||||
page := 1
|
||||
res := make([]File, 0)
|
||||
for {
|
||||
if !d.APIRateLimit(FileList) {
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
continue
|
||||
}
|
||||
var resp Files
|
||||
query := map[string]string{
|
||||
"limit": "100",
|
||||
|
@ -8,18 +8,21 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/pkg/cron"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Yun139 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
cron *cron.Cron
|
||||
Account string
|
||||
}
|
||||
|
||||
@ -35,6 +38,13 @@ func (d *Yun139) Init(ctx context.Context) error {
|
||||
if d.Authorization == "" {
|
||||
return fmt.Errorf("authorization is empty")
|
||||
}
|
||||
d.cron = cron.NewCron(time.Hour * 24 * 7)
|
||||
d.cron.Do(func() {
|
||||
err := d.refreshToken()
|
||||
if err != nil {
|
||||
log.Errorf("%+v", err)
|
||||
}
|
||||
})
|
||||
switch d.Addition.Type {
|
||||
case MetaPersonalNew:
|
||||
if len(d.Addition.RootFolderID) == 0 {
|
||||
@ -72,6 +82,9 @@ func (d *Yun139) Init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *Yun139) Drop(ctx context.Context) error {
|
||||
if d.cron != nil {
|
||||
d.cron.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -14,12 +14,15 @@ type Addition struct {
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "139Yun",
|
||||
LocalSort: true,
|
||||
Name: "139Yun",
|
||||
LocalSort: true,
|
||||
ProxyRangeOption: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Yun139{}
|
||||
d := &Yun139{}
|
||||
d.ProxyRange = true
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
package _139
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
const (
|
||||
MetaPersonal string = "personal"
|
||||
MetaFamily string = "family"
|
||||
@ -230,3 +234,12 @@ type PersonalUploadResp struct {
|
||||
UploadId string `json:"uploadId"`
|
||||
}
|
||||
}
|
||||
|
||||
type RefreshTokenResp struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Return string `xml:"return"`
|
||||
Token string `xml:"token"`
|
||||
Expiretime int32 `xml:"expiretime"`
|
||||
AccessToken string `xml:"accessToken"`
|
||||
Desc string `xml:"desc"`
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -52,6 +53,32 @@ func getTime(t string) time.Time {
|
||||
return stamp
|
||||
}
|
||||
|
||||
func (d *Yun139) refreshToken() error {
|
||||
url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do"
|
||||
var resp RefreshTokenResp
|
||||
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decodeStr := string(decode)
|
||||
splits := strings.Split(decodeStr, ":")
|
||||
reqBody := "<root><token>" + splits[2] + "</token><account>" + splits[1] + "</account><clienttype>656</clienttype></root>"
|
||||
_, err = base.RestyClient.R().
|
||||
ForceContentType("application/xml").
|
||||
SetBody(reqBody).
|
||||
SetResult(&resp).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Return != "0" {
|
||||
return fmt.Errorf("failed to refresh token: %s", resp.Desc)
|
||||
}
|
||||
d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token))
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
url := "https://yun.139.com" + pathname
|
||||
req := base.RestyClient.R()
|
||||
|
@ -1,6 +1,7 @@
|
||||
package _189pc
|
||||
|
||||
import (
|
||||
"container/ring"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@ -28,6 +29,9 @@ type Cloud189PC struct {
|
||||
|
||||
uploadThread int
|
||||
|
||||
familyTransferFolder *ring.Ring
|
||||
cleanFamilyTransferFile func()
|
||||
|
||||
storageConfig driver.Config
|
||||
}
|
||||
|
||||
@ -52,7 +56,6 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
|
||||
}
|
||||
if !y.isFamily() && y.RootFolderID == "" {
|
||||
y.RootFolderID = "-11"
|
||||
y.FamilyID = ""
|
||||
}
|
||||
|
||||
// 限制上传线程数
|
||||
@ -79,11 +82,24 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
// 处理家庭云ID
|
||||
if y.isFamily() && y.FamilyID == "" {
|
||||
if y.FamilyID == "" {
|
||||
if y.FamilyID, err = y.getFamilyID(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 创建中转文件夹,防止重名文件
|
||||
if y.FamilyTransfer {
|
||||
if y.familyTransferFolder, err = y.createFamilyTransferFolder(32); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
y.cleanFamilyTransferFile = utils.NewThrottle2(time.Minute, func() {
|
||||
if err := y.cleanFamilyTransfer(context.TODO()); err != nil {
|
||||
utils.Log.Errorf("cleanFamilyTransferFolderError:%s", err)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@ -92,7 +108,7 @@ func (y *Cloud189PC) Drop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
return y.getFiles(ctx, dir.GetID())
|
||||
return y.getFiles(ctx, dir.GetID(), y.isFamily())
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
@ -100,8 +116,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
||||
URL string `json:"fileDownloadUrl"`
|
||||
}
|
||||
|
||||
isFamily := y.isFamily()
|
||||
fullUrl := API_URL
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
fullUrl += "/family/file"
|
||||
}
|
||||
fullUrl += "/getFileDownloadUrl.action"
|
||||
@ -109,7 +126,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
||||
_, err := y.get(fullUrl, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetQueryParam("fileId", file.GetID())
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"familyId": y.FamilyID,
|
||||
})
|
||||
@ -119,7 +136,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
||||
"flag": "1",
|
||||
})
|
||||
}
|
||||
}, &downloadUrl)
|
||||
}, &downloadUrl, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -156,8 +173,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
isFamily := y.isFamily()
|
||||
fullUrl := API_URL
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
fullUrl += "/family/file"
|
||||
}
|
||||
fullUrl += "/createFolder.action"
|
||||
@ -169,7 +187,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
|
||||
"folderName": dirName,
|
||||
"relativePath": "",
|
||||
})
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"familyId": y.FamilyID,
|
||||
"parentId": parentDir.GetID(),
|
||||
@ -179,7 +197,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
|
||||
"parentFolderId": parentDir.GetID(),
|
||||
})
|
||||
}
|
||||
}, &newFolder)
|
||||
}, &newFolder, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -187,27 +205,14 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
var resp CreateBatchTaskResp
|
||||
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
||||
req.SetContext(ctx)
|
||||
req.SetFormData(map[string]string{
|
||||
"type": "MOVE",
|
||||
"taskInfos": MustString(utils.Json.MarshalToString(
|
||||
[]BatchTaskInfo{
|
||||
{
|
||||
FileId: srcObj.GetID(),
|
||||
FileName: srcObj.GetName(),
|
||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
||||
},
|
||||
})),
|
||||
"targetFolderId": dstDir.GetID(),
|
||||
})
|
||||
if y.isFamily() {
|
||||
req.SetFormData(map[string]string{
|
||||
"familyId": y.FamilyID,
|
||||
})
|
||||
}
|
||||
}, &resp)
|
||||
isFamily := y.isFamily()
|
||||
other := map[string]string{"targetFileName": dstDir.GetName()}
|
||||
|
||||
resp, err := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
|
||||
FileId: srcObj.GetID(),
|
||||
FileName: srcObj.GetName(),
|
||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -218,10 +223,11 @@ func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
isFamily := y.isFamily()
|
||||
queryParam := make(map[string]string)
|
||||
fullUrl := API_URL
|
||||
method := http.MethodPost
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
fullUrl += "/family/file"
|
||||
method = http.MethodGet
|
||||
queryParam["familyId"] = y.FamilyID
|
||||
@ -245,7 +251,7 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
|
||||
|
||||
_, err := y.request(fullUrl, method, func(req *resty.Request) {
|
||||
req.SetContext(ctx).SetQueryParams(queryParam)
|
||||
}, nil, newObj)
|
||||
}, nil, newObj, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -253,28 +259,15 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
var resp CreateBatchTaskResp
|
||||
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
||||
req.SetContext(ctx)
|
||||
req.SetFormData(map[string]string{
|
||||
"type": "COPY",
|
||||
"taskInfos": MustString(utils.Json.MarshalToString(
|
||||
[]BatchTaskInfo{
|
||||
{
|
||||
FileId: srcObj.GetID(),
|
||||
FileName: srcObj.GetName(),
|
||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
||||
},
|
||||
})),
|
||||
"targetFolderId": dstDir.GetID(),
|
||||
"targetFileName": dstDir.GetName(),
|
||||
})
|
||||
if y.isFamily() {
|
||||
req.SetFormData(map[string]string{
|
||||
"familyId": y.FamilyID,
|
||||
})
|
||||
}
|
||||
}, &resp)
|
||||
isFamily := y.isFamily()
|
||||
other := map[string]string{"targetFileName": dstDir.GetName()}
|
||||
|
||||
resp, err := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
|
||||
FileId: srcObj.GetID(),
|
||||
FileName: srcObj.GetName(),
|
||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -282,27 +275,13 @@ func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
|
||||
var resp CreateBatchTaskResp
|
||||
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
||||
req.SetContext(ctx)
|
||||
req.SetFormData(map[string]string{
|
||||
"type": "DELETE",
|
||||
"taskInfos": MustString(utils.Json.MarshalToString(
|
||||
[]*BatchTaskInfo{
|
||||
{
|
||||
FileId: obj.GetID(),
|
||||
FileName: obj.GetName(),
|
||||
IsFolder: BoolToNumber(obj.IsDir()),
|
||||
},
|
||||
})),
|
||||
})
|
||||
isFamily := y.isFamily()
|
||||
|
||||
if y.isFamily() {
|
||||
req.SetFormData(map[string]string{
|
||||
"familyId": y.FamilyID,
|
||||
})
|
||||
}
|
||||
}, &resp)
|
||||
resp, err := y.CreateBatchTask("DELETE", IF(isFamily, y.FamilyID, ""), "", nil, BatchTaskInfo{
|
||||
FileId: obj.GetID(),
|
||||
FileName: obj.GetName(),
|
||||
IsFolder: BoolToNumber(obj.IsDir()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -310,25 +289,73 @@ func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
|
||||
return y.WaitBatchTask("DELETE", resp.TaskID, time.Millisecond*200)
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (newObj model.Obj, err error) {
|
||||
overwrite := true
|
||||
isFamily := y.isFamily()
|
||||
|
||||
// 响应时间长,按需启用
|
||||
if y.Addition.RapidUpload {
|
||||
if newObj, err := y.RapidUpload(ctx, dstDir, stream); err == nil {
|
||||
if y.Addition.RapidUpload && !stream.IsForceStreamUpload() {
|
||||
if newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {
|
||||
return newObj, nil
|
||||
}
|
||||
}
|
||||
|
||||
switch y.UploadMethod {
|
||||
case "old":
|
||||
return y.OldUpload(ctx, dstDir, stream, up)
|
||||
uploadMethod := y.UploadMethod
|
||||
if stream.IsForceStreamUpload() {
|
||||
uploadMethod = "stream"
|
||||
}
|
||||
|
||||
// 旧版上传家庭云也有限制
|
||||
if uploadMethod == "old" {
|
||||
return y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
||||
}
|
||||
|
||||
// 开启家庭云转存
|
||||
if !isFamily && y.FamilyTransfer {
|
||||
// 修改上传目标为家庭云文件夹
|
||||
transferDstDir := dstDir
|
||||
dstDir = (y.familyTransferFolder.Value).(*Cloud189Folder)
|
||||
y.familyTransferFolder = y.familyTransferFolder.Next()
|
||||
|
||||
isFamily = true
|
||||
overwrite = false
|
||||
|
||||
defer func() {
|
||||
if newObj != nil {
|
||||
// 批量任务有概率删不掉
|
||||
y.cleanFamilyTransferFile()
|
||||
|
||||
// 转存家庭云文件到个人云
|
||||
err = y.SaveFamilyFileToPersonCloud(context.TODO(), y.FamilyID, newObj, transferDstDir, true)
|
||||
|
||||
task := BatchTaskInfo{
|
||||
FileId: newObj.GetID(),
|
||||
FileName: newObj.GetName(),
|
||||
IsFolder: BoolToNumber(newObj.IsDir()),
|
||||
}
|
||||
|
||||
// 删除源文件
|
||||
if resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, task); err == nil {
|
||||
y.WaitBatchTask("DELETE", resp.TaskID, time.Second)
|
||||
// 永久删除
|
||||
if resp, err := y.CreateBatchTask("CLEAR_RECYCLE", y.FamilyID, "", nil, task); err == nil {
|
||||
y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second)
|
||||
}
|
||||
}
|
||||
newObj = nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
switch uploadMethod {
|
||||
case "rapid":
|
||||
return y.FastUpload(ctx, dstDir, stream, up)
|
||||
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
||||
case "stream":
|
||||
if stream.GetSize() == 0 {
|
||||
return y.FastUpload(ctx, dstDir, stream, up)
|
||||
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return y.StreamUpload(ctx, dstDir, stream, up)
|
||||
return y.StreamUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
||||
}
|
||||
}
|
||||
|
@ -192,3 +192,19 @@ func partSize(size int64) int64 {
|
||||
}
|
||||
return DEFAULT
|
||||
}
|
||||
|
||||
func isBool(bs ...bool) bool {
|
||||
for _, b := range bs {
|
||||
if b {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IF[V any](o bool, t V, f V) V {
|
||||
if o {
|
||||
return t
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ type Addition struct {
|
||||
FamilyID string `json:"family_id"`
|
||||
UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"`
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||
FamilyTransfer bool `json:"family_transfer"`
|
||||
RapidUpload bool `json:"rapid_upload"`
|
||||
NoUseOcr bool `json:"no_use_ocr"`
|
||||
}
|
||||
|
@ -3,10 +3,11 @@ package _189pc
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
// 居然有四种返回方式
|
||||
@ -142,7 +143,7 @@ type FamilyInfoListResp struct {
|
||||
type FamilyInfoResp struct {
|
||||
Count int `json:"count"`
|
||||
CreateTime string `json:"createTime"`
|
||||
FamilyID int `json:"familyId"`
|
||||
FamilyID int64 `json:"familyId"`
|
||||
RemarkName string `json:"remarkName"`
|
||||
Type int `json:"type"`
|
||||
UseFlag int `json:"useFlag"`
|
||||
@ -242,7 +243,12 @@ type BatchTaskInfo struct {
|
||||
// IsFolder 是否是文件夹,0-否,1-是
|
||||
IsFolder int `json:"isFolder"`
|
||||
// SrcParentId 文件所在父目录ID
|
||||
//SrcParentId string `json:"srcParentId"`
|
||||
SrcParentId string `json:"srcParentId,omitempty"`
|
||||
|
||||
/* 冲突管理 */
|
||||
// 1 -> 跳过 2 -> 保留 3 -> 覆盖
|
||||
DealWay int `json:"dealWay,omitempty"`
|
||||
IsConflict int `json:"isConflict,omitempty"`
|
||||
}
|
||||
|
||||
/* 上传部分 */
|
||||
@ -355,6 +361,14 @@ type BatchTaskStateResp struct {
|
||||
TaskStatus int `json:"taskStatus"` //1 初始化 2 存在冲突 3 执行中,4 完成
|
||||
}
|
||||
|
||||
type BatchTaskConflictTaskInfoResp struct {
|
||||
SessionKey string `json:"sessionKey"`
|
||||
TargetFolderID int `json:"targetFolderId"`
|
||||
TaskID string `json:"taskId"`
|
||||
TaskInfos []BatchTaskInfo
|
||||
TaskType int `json:"taskType"`
|
||||
}
|
||||
|
||||
/* query 加密参数*/
|
||||
type Params map[string]string
|
||||
|
||||
|
@ -2,6 +2,7 @@ package _189pc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/ring"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
@ -54,11 +55,11 @@ const (
|
||||
CHANNEL_ID = "web_cloud.189.cn"
|
||||
)
|
||||
|
||||
func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]string {
|
||||
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
|
||||
dateOfGmt := getHttpDateStr()
|
||||
sessionKey := y.tokenInfo.SessionKey
|
||||
sessionSecret := y.tokenInfo.SessionSecret
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
sessionKey = y.tokenInfo.FamilySessionKey
|
||||
sessionSecret = y.tokenInfo.FamilySessionSecret
|
||||
}
|
||||
@ -72,9 +73,9 @@ func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]stri
|
||||
return header
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) EncryptParams(params Params) string {
|
||||
func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
|
||||
sessionSecret := y.tokenInfo.SessionSecret
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
sessionSecret = y.tokenInfo.FamilySessionSecret
|
||||
}
|
||||
if params != nil {
|
||||
@ -83,17 +84,17 @@ func (y *Cloud189PC) EncryptParams(params Params) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}) ([]byte, error) {
|
||||
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) {
|
||||
req := y.client.R().SetQueryParams(clientSuffix())
|
||||
|
||||
// 设置params
|
||||
paramsData := y.EncryptParams(params)
|
||||
paramsData := y.EncryptParams(params, isBool(isFamily...))
|
||||
if paramsData != "" {
|
||||
req.SetQueryParam("params", paramsData)
|
||||
}
|
||||
|
||||
// Signature
|
||||
req.SetHeaders(y.SignatureHeader(url, method, paramsData))
|
||||
req.SetHeaders(y.SignatureHeader(url, method, paramsData, isBool(isFamily...)))
|
||||
|
||||
var erron RespErr
|
||||
req.SetError(&erron)
|
||||
@ -129,15 +130,15 @@ func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, para
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
return y.request(url, http.MethodGet, callback, nil, resp)
|
||||
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
|
||||
return y.request(url, http.MethodGet, callback, nil, resp, isFamily...)
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
return y.request(url, http.MethodPost, callback, nil, resp)
|
||||
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
|
||||
return y.request(url, http.MethodPost, callback, nil, resp, isFamily...)
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader) ([]byte, error) {
|
||||
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader, isFamily bool) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -154,7 +155,7 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
|
||||
}
|
||||
|
||||
if sign {
|
||||
for key, value := range y.SignatureHeader(url, http.MethodPut, "") {
|
||||
for key, value := range y.SignatureHeader(url, http.MethodPut, "", isFamily) {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
@ -181,9 +182,9 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, error) {
|
||||
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
|
||||
fullUrl := API_URL
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
fullUrl += "/family/file"
|
||||
}
|
||||
fullUrl += "/listFiles.action"
|
||||
@ -201,7 +202,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj,
|
||||
"pageNum": fmt.Sprint(pageNum),
|
||||
"pageSize": "130",
|
||||
})
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"familyId": y.FamilyID,
|
||||
"orderBy": toFamilyOrderBy(y.OrderBy),
|
||||
@ -214,7 +215,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj,
|
||||
"descending": toDesc(y.OrderDirection),
|
||||
})
|
||||
}
|
||||
}, &resp)
|
||||
}, &resp, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -437,7 +438,7 @@ func (y *Cloud189PC) refreshSession() (err error) {
|
||||
|
||||
// 普通上传
|
||||
// 无法上传大小为0的文件
|
||||
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||
var sliceSize = partSize(file.GetSize())
|
||||
count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize)))
|
||||
lastPartSize := file.GetSize() % sliceSize
|
||||
@ -454,7 +455,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
||||
}
|
||||
|
||||
fullUrl := UPLOAD_URL
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
params.Set("familyId", y.FamilyID)
|
||||
fullUrl += "/family"
|
||||
} else {
|
||||
@ -466,7 +467,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
||||
var initMultiUpload InitMultiUploadResp
|
||||
_, err := y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetContext(ctx)
|
||||
}, params, &initMultiUpload)
|
||||
}, params, &initMultiUpload, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -502,14 +503,14 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
||||
partInfo := fmt.Sprintf("%d-%s", i, base64.StdEncoding.EncodeToString(md5Bytes))
|
||||
|
||||
threadG.Go(func(ctx context.Context) error {
|
||||
uploadUrls, err := y.GetMultiUploadUrls(ctx, initMultiUpload.Data.UploadFileID, partInfo)
|
||||
uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, initMultiUpload.Data.UploadFileID, partInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// step.4 上传切片
|
||||
uploadUrl := uploadUrls[0]
|
||||
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData))
|
||||
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData), isFamily)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -538,21 +539,21 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
||||
"sliceMd5": sliceMd5Hex,
|
||||
"lazyCheck": "1",
|
||||
"isLog": "0",
|
||||
"opertype": "3",
|
||||
}, &resp)
|
||||
"opertype": IF(overwrite, "3", "1"),
|
||||
}, &resp, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.toFile(), nil
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) (model.Obj, error) {
|
||||
func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||
fileMd5 := stream.GetHash().GetHash(utils.MD5)
|
||||
if len(fileMd5) < utils.MD5.Width {
|
||||
return nil, errors.New("invalid hash")
|
||||
}
|
||||
|
||||
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()))
|
||||
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()), isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -561,11 +562,11 @@ func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream m
|
||||
return nil, errors.New("rapid upload fail")
|
||||
}
|
||||
|
||||
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId)
|
||||
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId, isFamily, overwrite)
|
||||
}
|
||||
|
||||
// 快传
|
||||
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||
tempFile, err := file.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -594,7 +595,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
||||
}
|
||||
|
||||
silceMd5.Reset()
|
||||
if _, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
|
||||
if _, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
md5Byte := silceMd5.Sum(nil)
|
||||
@ -609,7 +610,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
||||
}
|
||||
|
||||
fullUrl := UPLOAD_URL
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
fullUrl += "/family"
|
||||
} else {
|
||||
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
|
||||
@ -628,13 +629,13 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
||||
"sliceSize": fmt.Sprint(sliceSize),
|
||||
"sliceMd5": sliceMd5Hex,
|
||||
}
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
params.Set("familyId", y.FamilyID)
|
||||
}
|
||||
var uploadInfo InitMultiUploadResp
|
||||
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetContext(ctx)
|
||||
}, params, &uploadInfo)
|
||||
}, params, &uploadInfo, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -659,7 +660,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
||||
i, uploadPart := i, uploadPart
|
||||
threadG.Go(func(ctx context.Context) error {
|
||||
// step.3 获取上传链接
|
||||
uploadUrls, err := y.GetMultiUploadUrls(ctx, uploadInfo.UploadFileID, uploadPart)
|
||||
uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, uploadInfo.UploadFileID, uploadPart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -671,7 +672,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
||||
}
|
||||
|
||||
// step.4 上传切片
|
||||
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize))
|
||||
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize), isFamily)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -698,8 +699,8 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
||||
}, Params{
|
||||
"uploadFileId": uploadInfo.UploadFileID,
|
||||
"isLog": "0",
|
||||
"opertype": "3",
|
||||
}, &resp)
|
||||
"opertype": IF(overwrite, "3", "1"),
|
||||
}, &resp, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -708,9 +709,9 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
||||
|
||||
// 获取上传切片信息
|
||||
// 对http body有大小限制,分片信息太多会出错
|
||||
func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
|
||||
func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
|
||||
fullUrl := UPLOAD_URL
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
fullUrl += "/family"
|
||||
} else {
|
||||
fullUrl += "/person"
|
||||
@ -723,7 +724,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string
|
||||
}, Params{
|
||||
"uploadFileId": uploadFileId,
|
||||
"partInfo": strings.Join(partInfo, ","),
|
||||
}, &uploadUrlsResp)
|
||||
}, &uploadUrlsResp, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -752,7 +753,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string
|
||||
}
|
||||
|
||||
// 旧版本上传,家庭云不支持覆盖
|
||||
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||
tempFile, err := file.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -763,7 +764,7 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
|
||||
}
|
||||
|
||||
// 创建上传会话
|
||||
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()))
|
||||
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -780,14 +781,14 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
|
||||
"Expect": "100-continue",
|
||||
}
|
||||
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
header["FamilyId"] = fmt.Sprint(y.FamilyID)
|
||||
header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
||||
} else {
|
||||
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
||||
}
|
||||
|
||||
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile))
|
||||
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile), isFamily)
|
||||
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
|
||||
return nil, err
|
||||
}
|
||||
@ -802,10 +803,10 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
|
||||
"uploadFileId": fmt.Sprint(status.UploadFileId),
|
||||
"resumePolicy": "1",
|
||||
})
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
|
||||
}
|
||||
}, &status)
|
||||
}, &status, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -815,20 +816,20 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
|
||||
up(float64(status.GetSize()) / float64(file.GetSize()) * 100)
|
||||
}
|
||||
|
||||
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId)
|
||||
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId, isFamily, overwrite)
|
||||
}
|
||||
|
||||
// 创建上传会话
|
||||
func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string) (*CreateUploadFileResp, error) {
|
||||
func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string, isFamily bool) (*CreateUploadFileResp, error) {
|
||||
var uploadInfo CreateUploadFileResp
|
||||
|
||||
fullUrl := API_URL + "/createUploadFile.action"
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
fullUrl = API_URL + "/family/file/createFamilyFile.action"
|
||||
}
|
||||
_, err := y.post(fullUrl, func(req *resty.Request) {
|
||||
req.SetContext(ctx)
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"familyId": y.FamilyID,
|
||||
"parentId": parentID,
|
||||
@ -849,7 +850,7 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
|
||||
"isLog": "0",
|
||||
})
|
||||
}
|
||||
}, &uploadInfo)
|
||||
}, &uploadInfo, isFamily)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -858,11 +859,11 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
|
||||
}
|
||||
|
||||
// 提交上传文件
|
||||
func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64) (model.Obj, error) {
|
||||
func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||
var resp OldCommitUploadFileResp
|
||||
_, err := y.post(fileCommitUrl, func(req *resty.Request) {
|
||||
req.SetContext(ctx)
|
||||
if y.isFamily() {
|
||||
if isFamily {
|
||||
req.SetHeaders(map[string]string{
|
||||
"ResumePolicy": "1",
|
||||
"UploadFileId": fmt.Sprint(uploadFileID),
|
||||
@ -870,13 +871,13 @@ func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string,
|
||||
})
|
||||
} else {
|
||||
req.SetFormData(map[string]string{
|
||||
"opertype": "3",
|
||||
"opertype": IF(overwrite, "3", "1"),
|
||||
"resumePolicy": "1",
|
||||
"uploadFileId": fmt.Sprint(uploadFileID),
|
||||
"isLog": "0",
|
||||
})
|
||||
}
|
||||
}, &resp)
|
||||
}, &resp, isFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -895,10 +896,100 @@ func (y *Cloud189PC) isLogin() bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// 创建家庭云中转文件夹
|
||||
func (y *Cloud189PC) createFamilyTransferFolder(count int) (*ring.Ring, error) {
|
||||
folders := ring.New(count)
|
||||
var rootFolder Cloud189Folder
|
||||
_, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"folderName": "FamilyTransferFolder",
|
||||
"familyId": y.FamilyID,
|
||||
})
|
||||
}, &rootFolder, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folderCount := 0
|
||||
|
||||
// 获取已有目录
|
||||
files, err := y.getFiles(context.TODO(), rootFolder.GetID(), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if folder, ok := file.(*Cloud189Folder); ok {
|
||||
folders.Value = folder
|
||||
folders = folders.Next()
|
||||
folderCount++
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的目录
|
||||
for folderCount < count {
|
||||
var newFolder Cloud189Folder
|
||||
_, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"folderName": uuid.NewString(),
|
||||
"familyId": y.FamilyID,
|
||||
"parentId": rootFolder.GetID(),
|
||||
})
|
||||
}, &newFolder, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folders.Value = &newFolder
|
||||
folders = folders.Next()
|
||||
folderCount++
|
||||
}
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
// 清理中转文件夹
|
||||
func (y *Cloud189PC) cleanFamilyTransfer(ctx context.Context) error {
|
||||
var tasks []BatchTaskInfo
|
||||
r := y.familyTransferFolder
|
||||
for p := r.Next(); p != r; p = p.Next() {
|
||||
folder := p.Value.(*Cloud189Folder)
|
||||
|
||||
files, err := y.getFiles(ctx, folder.GetID(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
tasks = append(tasks, BatchTaskInfo{
|
||||
FileId: file.GetID(),
|
||||
FileName: file.GetName(),
|
||||
IsFolder: BoolToNumber(file.IsDir()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(tasks) > 0 {
|
||||
// 删除
|
||||
resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, tasks...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = y.WaitBatchTask("DELETE", resp.TaskID, time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 永久删除
|
||||
resp, err = y.CreateBatchTask("CLEAR_RECYCLE", y.FamilyID, "", nil, tasks...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取家庭云所有用户信息
|
||||
func (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
|
||||
var resp FamilyInfoListResp
|
||||
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp)
|
||||
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -922,6 +1013,73 @@ func (y *Cloud189PC) getFamilyID() (string, error) {
|
||||
return fmt.Sprint(infos[0].FamilyID), nil
|
||||
}
|
||||
|
||||
// 保存家庭云中的文件到个人云
|
||||
func (y *Cloud189PC) SaveFamilyFileToPersonCloud(ctx context.Context, familyId string, srcObj, dstDir model.Obj, overwrite bool) error {
|
||||
// _, err := y.post(API_URL+"/family/file/saveFileToMember.action", func(req *resty.Request) {
|
||||
// req.SetQueryParams(map[string]string{
|
||||
// "channelId": "home",
|
||||
// "familyId": familyId,
|
||||
// "destParentId": destParentId,
|
||||
// "fileIdList": familyFileId,
|
||||
// })
|
||||
// }, nil)
|
||||
// return err
|
||||
|
||||
task := BatchTaskInfo{
|
||||
FileId: srcObj.GetID(),
|
||||
FileName: srcObj.GetName(),
|
||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
||||
}
|
||||
resp, err := y.CreateBatchTask("COPY", familyId, dstDir.GetID(), map[string]string{
|
||||
"groupId": "null",
|
||||
"copyType": "2",
|
||||
"shareId": "null",
|
||||
}, task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
state, err := y.CheckBatchTask("COPY", resp.TaskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch state.TaskStatus {
|
||||
case 2:
|
||||
task.DealWay = IF(overwrite, 3, 2)
|
||||
// 冲突时覆盖文件
|
||||
if err := y.ManageBatchTask("COPY", resp.TaskID, dstDir.GetID(), task); err != nil {
|
||||
return err
|
||||
}
|
||||
case 4:
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Millisecond * 400)
|
||||
}
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) CreateBatchTask(aType string, familyID string, targetFolderId string, other map[string]string, taskInfos ...BatchTaskInfo) (*CreateBatchTaskResp, error) {
|
||||
var resp CreateBatchTaskResp
|
||||
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
||||
req.SetFormData(map[string]string{
|
||||
"type": aType,
|
||||
"taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
|
||||
})
|
||||
if targetFolderId != "" {
|
||||
req.SetFormData(map[string]string{"targetFolderId": targetFolderId})
|
||||
}
|
||||
if familyID != "" {
|
||||
req.SetFormData(map[string]string{"familyId": familyID})
|
||||
}
|
||||
req.SetFormData(other)
|
||||
}, &resp, familyID != "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// 检测任务状态
|
||||
func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStateResp, error) {
|
||||
var resp BatchTaskStateResp
|
||||
_, err := y.post(API_URL+"/batch/checkBatchTask.action", func(req *resty.Request) {
|
||||
@ -936,6 +1094,37 @@ func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStat
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// 获取冲突的任务信息
|
||||
func (y *Cloud189PC) GetConflictTaskInfo(aType string, taskID string) (*BatchTaskConflictTaskInfoResp, error) {
|
||||
var resp BatchTaskConflictTaskInfoResp
|
||||
_, err := y.post(API_URL+"/batch/getConflictTaskInfo.action", func(req *resty.Request) {
|
||||
req.SetFormData(map[string]string{
|
||||
"type": aType,
|
||||
"taskId": taskID,
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// 处理冲突
|
||||
func (y *Cloud189PC) ManageBatchTask(aType string, taskID string, targetFolderId string, taskInfos ...BatchTaskInfo) error {
|
||||
_, err := y.post(API_URL+"/batch/manageBatchTask.action", func(req *resty.Request) {
|
||||
req.SetFormData(map[string]string{
|
||||
"targetFolderId": targetFolderId,
|
||||
"type": aType,
|
||||
"taskId": taskID,
|
||||
"taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
|
||||
})
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
var ErrIsConflict = errors.New("there is a conflict with the target object")
|
||||
|
||||
// 等待任务完成
|
||||
func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration) error {
|
||||
for {
|
||||
state, err := y.CheckBatchTask(aType, taskID)
|
||||
@ -944,7 +1133,7 @@ func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration)
|
||||
}
|
||||
switch state.TaskStatus {
|
||||
case 2:
|
||||
return errors.New("there is a conflict with the target object")
|
||||
return ErrIsConflict
|
||||
case 4:
|
||||
return nil
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
@ -45,6 +46,9 @@ func (d *Alias) Init(ctx context.Context) error {
|
||||
d.oneKey = k
|
||||
}
|
||||
d.autoFlatten = true
|
||||
} else {
|
||||
d.oneKey = ""
|
||||
d.autoFlatten = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -111,4 +115,26 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
func (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
reqPath, err := d.getReqPath(ctx, srcObj)
|
||||
if err == nil {
|
||||
return fs.Rename(ctx, *reqPath, newName)
|
||||
}
|
||||
if errs.IsNotImplement(err) {
|
||||
return errors.New("same-name files cannot be Rename")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Alias) Remove(ctx context.Context, obj model.Obj) error {
|
||||
reqPath, err := d.getReqPath(ctx, obj)
|
||||
if err == nil {
|
||||
return fs.Remove(ctx, *reqPath)
|
||||
}
|
||||
if errs.IsNotImplement(err) {
|
||||
return errors.New("same-name files cannot be Delete")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Alias)(nil)
|
||||
|
@ -9,19 +9,25 @@ type Addition struct {
|
||||
// Usually one of two
|
||||
// driver.RootPath
|
||||
// define other
|
||||
Paths string `json:"paths" required:"true" type:"text"`
|
||||
Paths string `json:"paths" required:"true" type:"text"`
|
||||
ProtectSameName bool `json:"protect_same_name" default:"true" required:"false" help:"Protects same-name files from Delete or Rename"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Alias",
|
||||
LocalSort: true,
|
||||
NoCache: true,
|
||||
NoUpload: true,
|
||||
DefaultRoot: "/",
|
||||
Name: "Alias",
|
||||
LocalSort: true,
|
||||
NoCache: true,
|
||||
NoUpload: true,
|
||||
DefaultRoot: "/",
|
||||
ProxyRangeOption: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Alias{}
|
||||
return &Alias{
|
||||
Addition: Addition{
|
||||
ProtectSameName: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
stdpath "path"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/sign"
|
||||
@ -102,13 +103,49 @@ func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs)
|
||||
return nil, err
|
||||
}
|
||||
if common.ShouldProxy(storage, stdpath.Base(sub)) {
|
||||
return &model.Link{
|
||||
link := &model.Link{
|
||||
URL: fmt.Sprintf("%s/p%s?sign=%s",
|
||||
common.GetApiUrl(args.HttpReq),
|
||||
utils.EncodePath(reqPath, true),
|
||||
sign.Sign(reqPath)),
|
||||
}, nil
|
||||
}
|
||||
if args.HttpReq != nil && d.ProxyRange {
|
||||
link.RangeReadCloser = common.NoProxyRange
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
link, _, err := fs.Link(ctx, reqPath, args)
|
||||
return link, err
|
||||
}
|
||||
|
||||
func (d *Alias) getReqPath(ctx context.Context, obj model.Obj) (*string, error) {
|
||||
root, sub := d.getRootAndPath(obj.GetPath())
|
||||
if sub == "" || sub == "/" {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
dsts, ok := d.pathMap[root]
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
var reqPath string
|
||||
var err error
|
||||
for _, dst := range dsts {
|
||||
reqPath = stdpath.Join(dst, sub)
|
||||
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
|
||||
if err == nil {
|
||||
if d.ProtectSameName {
|
||||
if ok {
|
||||
ok = false
|
||||
} else {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
return &reqPath, nil
|
||||
}
|
||||
|
@ -109,11 +109,19 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
|
||||
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var resp common.Resp[FsGetResp]
|
||||
// if PassUAToUpsteam is true, then pass the user-agent to the upstream
|
||||
userAgent := base.UserAgent
|
||||
if d.PassUAToUpsteam {
|
||||
userAgent = args.Header.Get("user-agent")
|
||||
if userAgent == "" {
|
||||
userAgent = base.UserAgent
|
||||
}
|
||||
}
|
||||
_, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetBody(FsGetReq{
|
||||
Path: file.GetPath(),
|
||||
Password: d.MetaPassword,
|
||||
})
|
||||
}).SetHeader("user-agent", userAgent)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -7,18 +7,20 @@ import (
|
||||
|
||||
type Addition struct {
|
||||
driver.RootPath
|
||||
Address string `json:"url" required:"true"`
|
||||
MetaPassword string `json:"meta_password"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
Address string `json:"url" required:"true"`
|
||||
MetaPassword string `json:"meta_password"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
PassUAToUpsteam bool `json:"pass_ua_to_upsteam" default:"true"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "AList V3",
|
||||
LocalSort: true,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: true,
|
||||
Name: "AList V3",
|
||||
LocalSort: true,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: true,
|
||||
ProxyRangeOption: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -194,7 +194,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
|
||||
}
|
||||
if d.RapidUpload {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
io.CopyN(buf, file, 1024)
|
||||
utils.CopyWithBufferN(buf, file, 1024)
|
||||
reqBody["pre_hash"] = utils.HashData(utils.SHA1, buf.Bytes())
|
||||
if localFile != nil {
|
||||
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
|
||||
|
@ -136,7 +136,7 @@ func (d *AliyundriveOpen) calProofCode(stream model.FileStreamer) (string, error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = io.CopyN(buf, reader, length)
|
||||
_, err = utils.CopyWithBufferN(buf, reader, length)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -164,7 +164,7 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
|
||||
count := int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
|
||||
createData["part_info_list"] = makePartInfos(count)
|
||||
// rapid upload
|
||||
rapidUpload := stream.GetSize() > 100*utils.KB && d.RapidUpload
|
||||
rapidUpload := !stream.IsForceStreamUpload() && stream.GetSize() > 100*utils.KB && d.RapidUpload
|
||||
if rapidUpload {
|
||||
log.Debugf("[aliyundrive_open] start cal pre_hash")
|
||||
// read 1024 bytes to calculate pre hash
|
||||
@ -242,13 +242,16 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
|
||||
if remain := stream.GetSize() - offset; length > remain {
|
||||
length = remain
|
||||
}
|
||||
//rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
|
||||
rd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
|
||||
if rapidUpload {
|
||||
srd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rd = utils.NewMultiReadable(srd)
|
||||
}
|
||||
err = retry.Do(func() error {
|
||||
//rd.Reset()
|
||||
rd.Reset()
|
||||
return d.uploadPart(ctx, rd, createResp.PartInfoList[i])
|
||||
},
|
||||
retry.Attempts(3),
|
||||
|
@ -32,6 +32,7 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
||||
_ "github.com/alist-org/alist/v3/drivers/mega"
|
||||
_ "github.com/alist-org/alist/v3/drivers/mopan"
|
||||
_ "github.com/alist-org/alist/v3/drivers/netease_music"
|
||||
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
||||
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
|
||||
_ "github.com/alist-org/alist/v3/drivers/pikpak"
|
||||
@ -45,6 +46,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/thunderx"
|
||||
_ "github.com/alist-org/alist/v3/drivers/trainbit"
|
||||
_ "github.com/alist-org/alist/v3/drivers/url_tree"
|
||||
_ "github.com/alist-org/alist/v3/drivers/uss"
|
||||
|
@ -165,9 +165,16 @@ func (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream mo
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
||||
newFile.Ctime = stream.CreateTime().Unix()
|
||||
newFile.Mtime = stream.ModTime().Unix()
|
||||
return fileToObj(newFile), nil
|
||||
}
|
||||
|
||||
// Put
|
||||
//
|
||||
// **注意**: 截至 2024/04/20 百度云盘 api 接口返回的时间永远是当前时间,而不是文件时间。
|
||||
// 而实际上云盘存储的时间是文件时间,所以此处需要覆盖时间,保证缓存与云盘的数据一致
|
||||
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
// rapid upload
|
||||
if newObj, err := d.PutRapid(ctx, dstDir, stream); err == nil {
|
||||
@ -204,7 +211,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
||||
if i == count {
|
||||
byteSize = lastBlockSize
|
||||
}
|
||||
_, err := io.CopyN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
|
||||
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
@ -245,9 +252,9 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
||||
log.Debugf("%+v", precreateResp)
|
||||
if precreateResp.ReturnType == 2 {
|
||||
//rapid upload, since got md5 match from baidu server
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
||||
precreateResp.File.Ctime = ctime
|
||||
precreateResp.File.Mtime = mtime
|
||||
return fileToObj(precreateResp.File), nil
|
||||
}
|
||||
}
|
||||
@ -298,6 +305,9 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
||||
newFile.Ctime = ctime
|
||||
newFile.Mtime = mtime
|
||||
return fileToObj(newFile), nil
|
||||
}
|
||||
|
||||
|
@ -8,15 +8,16 @@ import (
|
||||
type Addition struct {
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
driver.RootPath
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
|
||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
||||
AccessToken string
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
|
||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
||||
AccessToken string
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
|
||||
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -249,6 +249,9 @@ const (
|
||||
)
|
||||
|
||||
func (d *BaiduNetdisk) getSliceSize() int64 {
|
||||
if d.CustomUploadPartSize != 0 {
|
||||
return d.CustomUploadPartSize
|
||||
}
|
||||
switch d.vipType {
|
||||
case 1:
|
||||
return VipSliceSize
|
||||
|
@ -261,7 +261,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
||||
if i == count {
|
||||
byteSize = lastBlockSize
|
||||
}
|
||||
_, err := io.CopyN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
|
||||
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -229,7 +229,7 @@ func (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(filePart, stream)
|
||||
_, err = utils.CopyWithBuffer(filePart, stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ type File struct {
|
||||
Topsort int `json:"topsort"`
|
||||
Restype string `json:"restype"`
|
||||
Size int_str `json:"size"`
|
||||
UploadDate string `json:"uploadDate"`
|
||||
UploadDate int64 `json:"uploadDate"`
|
||||
FileSize string `json:"fileSize"`
|
||||
Name string `json:"name"`
|
||||
FileID string `json:"fileId"`
|
||||
@ -265,10 +265,7 @@ func fileToObj(f File) *model.Object {
|
||||
IsFolder: true,
|
||||
}
|
||||
}
|
||||
paserTime, err := time.Parse("2006-01-02 15:04", f.Content.UploadDate)
|
||||
if err != nil {
|
||||
paserTime = time.Now()
|
||||
}
|
||||
paserTime := time.UnixMilli(f.Content.UploadDate)
|
||||
return &model.Object{
|
||||
ID: fmt.Sprintf("%d$%s", f.ID, f.Content.FileID),
|
||||
Name: f.Content.Name,
|
||||
|
@ -71,6 +71,9 @@ func (d *Cloudreve) Link(ctx context.Context, file model.Obj, args model.LinkArg
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.HasPrefix(dUrl, "/api") {
|
||||
dUrl = d.Address + dUrl
|
||||
}
|
||||
return &model.Link{
|
||||
URL: dUrl,
|
||||
}, nil
|
||||
|
@ -3,7 +3,6 @@ package crypt
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"io"
|
||||
stdpath "path"
|
||||
"regexp"
|
||||
@ -14,6 +13,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
@ -160,7 +160,7 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
|
||||
// discarding hash as it's encrypted
|
||||
}
|
||||
if d.Thumbnail && thumb == "" {
|
||||
thumb = utils.EncodePath(common.GetApiUrl(nil) + stdpath.Join("/d", args.ReqPath, ".thumbnails", name+".webp"), true)
|
||||
thumb = utils.EncodePath(common.GetApiUrl(nil)+stdpath.Join("/d", args.ReqPath, ".thumbnails", name+".webp"), true)
|
||||
}
|
||||
if !ok && !d.Thumbnail {
|
||||
result = append(result, &objRes)
|
||||
@ -389,10 +389,11 @@ func (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileSt
|
||||
Modified: streamer.ModTime(),
|
||||
IsFolder: streamer.IsDir(),
|
||||
},
|
||||
Reader: wrappedIn,
|
||||
Mimetype: "application/octet-stream",
|
||||
WebPutAsTask: streamer.NeedStore(),
|
||||
Exist: streamer.GetExist(),
|
||||
Reader: wrappedIn,
|
||||
Mimetype: "application/octet-stream",
|
||||
WebPutAsTask: streamer.NeedStore(),
|
||||
ForceStreamUpload: true,
|
||||
Exist: streamer.GetExist(),
|
||||
}
|
||||
err = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false)
|
||||
if err != nil {
|
||||
|
@ -123,14 +123,14 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
query.Set("devType", "6")
|
||||
query.Set("devCode", d.UUID)
|
||||
query.Set("devModel", "chrome")
|
||||
query.Set("devVersion", "120")
|
||||
query.Set("devVersion", d.conf.devVersion)
|
||||
query.Set("appVersion", "")
|
||||
ts, err := getTimestamp(d.conf.secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query.Set("timestamp", ts)
|
||||
//query.Set("appToken", d.Token)
|
||||
query.Set("appToken", d.Token)
|
||||
query.Set("enable", "1")
|
||||
downloadId, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%s", file.GetID(), d.userID)), d.conf.secret)
|
||||
if err != nil {
|
||||
@ -143,7 +143,27 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
}
|
||||
query.Set("auth", hex.EncodeToString(auth))
|
||||
u.RawQuery = query.Encode()
|
||||
link := model.Link{URL: u.String()}
|
||||
realURL := u.String()
|
||||
// get the url after redirect
|
||||
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
|
||||
//"Origin": d.conf.site,
|
||||
"Referer": d.conf.site + "/",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
|
||||
}).Get(realURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode() == 302 {
|
||||
realURL = res.Header().Get("location")
|
||||
} else {
|
||||
contentLengthStr := res.Header().Get("Content-Length")
|
||||
contentLength, err := strconv.Atoi(contentLengthStr)
|
||||
if err != nil || contentLength == 0 || contentLength > 1024*10 {
|
||||
return nil, fmt.Errorf("redirect failed, status: %d", res.StatusCode())
|
||||
}
|
||||
return nil, fmt.Errorf("redirect failed, content: %s", res.String())
|
||||
}
|
||||
link := model.Link{URL: realURL}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
@ -257,7 +277,7 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
}()
|
||||
if _, err = io.Copy(h, tempFile); err != nil {
|
||||
if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
|
@ -15,11 +15,13 @@ type Addition struct {
|
||||
}
|
||||
|
||||
type Conf struct {
|
||||
base string
|
||||
secret []byte
|
||||
bucket string
|
||||
unproved string
|
||||
proved string
|
||||
base string
|
||||
secret []byte
|
||||
bucket string
|
||||
unproved string
|
||||
proved string
|
||||
devVersion string
|
||||
site string
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -39,11 +41,13 @@ func init() {
|
||||
NoOverwriteUpload: false,
|
||||
},
|
||||
conf: Conf{
|
||||
base: "https://api.ilanzou.com",
|
||||
secret: []byte("lanZouY-disk-app"),
|
||||
bucket: "wpanstore-lanzou",
|
||||
unproved: "unproved",
|
||||
proved: "proved",
|
||||
base: "https://api.ilanzou.com",
|
||||
secret: []byte("lanZouY-disk-app"),
|
||||
bucket: "wpanstore-lanzou",
|
||||
unproved: "unproved",
|
||||
proved: "proved",
|
||||
devVersion: "125",
|
||||
site: "https://www.ilanzou.com",
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -63,11 +67,13 @@ func init() {
|
||||
NoOverwriteUpload: false,
|
||||
},
|
||||
conf: Conf{
|
||||
base: "https://api.feijipan.com",
|
||||
secret: []byte("dingHao-disk-app"),
|
||||
bucket: "wpanstore",
|
||||
unproved: "ws",
|
||||
proved: "app",
|
||||
base: "https://api.feijipan.com",
|
||||
secret: []byte("dingHao-disk-app"),
|
||||
bucket: "wpanstore",
|
||||
unproved: "ws",
|
||||
proved: "app",
|
||||
devVersion: "125",
|
||||
site: "https://www.feijipan.com",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -52,12 +52,17 @@ func (d *ILanZou) request(pathname, method string, callback base.ReqCallback, pr
|
||||
"devType": "6",
|
||||
"devCode": d.UUID,
|
||||
"devModel": "chrome",
|
||||
"devVersion": "120",
|
||||
"devVersion": d.conf.devVersion,
|
||||
"appVersion": "",
|
||||
"timestamp": ts,
|
||||
//"appToken": d.Token,
|
||||
"extra": "2",
|
||||
})
|
||||
req.SetHeaders(map[string]string{
|
||||
"Origin": d.conf.site,
|
||||
"Referer": d.conf.site + "/",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
|
||||
})
|
||||
if proved {
|
||||
req.SetQueryParam("appToken", d.Token)
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ func (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
|
||||
for _, file := range dirs {
|
||||
gateurl := *d.gateURL
|
||||
gateurl.Path = "ipfs/" + file.Hash
|
||||
gateurl.RawQuery = "filename=" + file.Name
|
||||
gateurl.RawQuery = "filename=" + url.PathEscape(file.Name)
|
||||
objlist = append(objlist, &model.ObjectURL{
|
||||
Object: model.Object{ID: file.Hash, Name: file.Name, Size: int64(file.Size), IsFolder: file.Type == 1},
|
||||
Url: model.Url{Url: gateurl.String()},
|
||||
@ -73,7 +73,7 @@ func (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
|
||||
}
|
||||
|
||||
func (d *IPFS) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + file.GetName()
|
||||
link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + url.PathEscape(file.GetName())
|
||||
return &model.Link{URL: link}, nil
|
||||
}
|
||||
|
||||
|
8
drivers/lark.go
Normal file
8
drivers/lark.go
Normal file
@ -0,0 +1,8 @@
|
||||
// +build linux darwin windows
|
||||
// +build amd64 arm64
|
||||
|
||||
package drivers
|
||||
|
||||
import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/lark"
|
||||
)
|
396
drivers/lark/driver.go
Normal file
396
drivers/lark/driver.go
Normal file
@ -0,0 +1,396 @@
|
||||
package lark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/ipfs/boxo/path"
|
||||
lark "github.com/larksuite/oapi-sdk-go/v3"
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1"
|
||||
"golang.org/x/time/rate"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Lark struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
client *lark.Client
|
||||
rootFolderToken string
|
||||
}
|
||||
|
||||
func (c *Lark) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (c *Lark) GetAddition() driver.Additional {
|
||||
return &c.Addition
|
||||
}
|
||||
|
||||
func (c *Lark) Init(ctx context.Context) error {
|
||||
c.client = lark.NewClient(c.AppId, c.AppSecret, lark.WithTokenCache(newTokenCache()))
|
||||
|
||||
paths := path.SplitList(c.RootFolderPath)
|
||||
token := ""
|
||||
|
||||
var ok bool
|
||||
var file *larkdrive.File
|
||||
for _, p := range paths {
|
||||
if p == "" {
|
||||
token = ""
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
ok, file, err = resp.Next()
|
||||
if !ok {
|
||||
return errs.ObjectNotFound
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *file.Type == "folder" && *file.Name == p {
|
||||
token = *file.Token
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.rootFolderToken = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Lark) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Lark) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
token, ok := c.getObjToken(ctx, dir.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
if token == emptyFolderToken {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ok = false
|
||||
var file *larkdrive.File
|
||||
var res []model.Obj
|
||||
|
||||
for {
|
||||
ok, file, err = resp.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modifiedUnix, _ := strconv.ParseInt(*file.ModifiedTime, 10, 64)
|
||||
createdUnix, _ := strconv.ParseInt(*file.CreatedTime, 10, 64)
|
||||
|
||||
f := model.Object{
|
||||
ID: *file.Token,
|
||||
Path: path.Join([]string{c.RootFolderPath, dir.GetPath(), *file.Name}),
|
||||
Name: *file.Name,
|
||||
Size: 0,
|
||||
Modified: time.Unix(modifiedUnix, 0),
|
||||
Ctime: time.Unix(createdUnix, 0),
|
||||
IsFolder: *file.Type == "folder",
|
||||
}
|
||||
res = append(res, &f)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Lark) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
token, ok := c.getObjToken(ctx, file.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
resp, err := c.client.GetTenantAccessTokenBySelfBuiltApp(ctx, &larkcore.SelfBuiltTenantAccessTokenReq{
|
||||
AppID: c.AppId,
|
||||
AppSecret: c.AppSecret,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !c.ExternalMode {
|
||||
accessToken := resp.TenantAccessToken
|
||||
|
||||
url := fmt.Sprintf("https://open.feishu.cn/open-apis/drive/v1/files/%s/download", token)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||
req.Header.Set("Range", "bytes=0-1")
|
||||
|
||||
ar, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ar.StatusCode != http.StatusPartialContent {
|
||||
return nil, errors.New("failed to get download link")
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: url,
|
||||
Header: http.Header{
|
||||
"Authorization": []string{fmt.Sprintf("Bearer %s", accessToken)},
|
||||
},
|
||||
}, nil
|
||||
} else {
|
||||
url := path.Join([]string{c.TenantUrlPrefix, "file", token})
|
||||
|
||||
return &model.Link{
|
||||
URL: url,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Lark) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
token, ok := c.getObjToken(ctx, parentDir.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
body, err := larkdrive.NewCreateFolderFilePathReqBodyBuilder().FolderToken(token).Name(dirName).Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.Drive.File.CreateFolder(ctx,
|
||||
larkdrive.NewCreateFolderFileReqBuilder().Body(body).Build())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return nil, errors.New(resp.Error())
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: *resp.Data.Token,
|
||||
Path: path.Join([]string{c.RootFolderPath, parentDir.GetPath(), dirName}),
|
||||
Name: dirName,
|
||||
Size: 0,
|
||||
IsFolder: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Lark) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
srcToken, ok := c.getObjToken(ctx, srcObj.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
req := larkdrive.NewMoveFileReqBuilder().
|
||||
Body(larkdrive.NewMoveFileReqBodyBuilder().
|
||||
Type("file").
|
||||
FolderToken(dstDirToken).
|
||||
Build()).FileToken(srcToken).
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
resp, err := c.client.Drive.File.Move(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return nil, errors.New(resp.Error())
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Lark) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
// TODO rename obj, optional
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (c *Lark) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
srcToken, ok := c.getObjToken(ctx, srcObj.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
req := larkdrive.NewCopyFileReqBuilder().
|
||||
Body(larkdrive.NewCopyFileReqBodyBuilder().
|
||||
Name(srcObj.GetName()).
|
||||
Type("file").
|
||||
FolderToken(dstDirToken).
|
||||
Build()).FileToken(srcToken).
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
resp, err := c.client.Drive.File.Copy(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return nil, errors.New(resp.Error())
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Lark) Remove(ctx context.Context, obj model.Obj) error {
|
||||
token, ok := c.getObjToken(ctx, obj.GetPath())
|
||||
if !ok {
|
||||
return errs.ObjectNotFound
|
||||
}
|
||||
|
||||
req := larkdrive.NewDeleteFileReqBuilder().
|
||||
FileToken(token).
|
||||
Type("file").
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
resp, err := c.client.Drive.File.Delete(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return errors.New(resp.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var uploadLimit = rate.NewLimiter(rate.Every(time.Second), 5)
|
||||
|
||||
func (c *Lark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
token, ok := c.getObjToken(ctx, dstDir.GetPath())
|
||||
if !ok {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
// prepare
|
||||
req := larkdrive.NewUploadPrepareFileReqBuilder().
|
||||
FileUploadInfo(larkdrive.NewFileUploadInfoBuilder().
|
||||
FileName(stream.GetName()).
|
||||
ParentType(`explorer`).
|
||||
ParentNode(token).
|
||||
Size(int(stream.GetSize())).
|
||||
Build()).
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
uploadLimit.Wait(ctx)
|
||||
resp, err := c.client.Drive.File.UploadPrepare(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return nil, errors.New(resp.Error())
|
||||
}
|
||||
|
||||
uploadId := *resp.Data.UploadId
|
||||
blockSize := *resp.Data.BlockSize
|
||||
blockCount := *resp.Data.BlockNum
|
||||
|
||||
// upload
|
||||
for i := 0; i < blockCount; i++ {
|
||||
length := int64(blockSize)
|
||||
if i == blockCount-1 {
|
||||
length = stream.GetSize() - int64(i*blockSize)
|
||||
}
|
||||
|
||||
reader := io.LimitReader(stream, length)
|
||||
|
||||
req := larkdrive.NewUploadPartFileReqBuilder().
|
||||
Body(larkdrive.NewUploadPartFileReqBodyBuilder().
|
||||
UploadId(uploadId).
|
||||
Seq(i).
|
||||
Size(int(length)).
|
||||
File(reader).
|
||||
Build()).
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
uploadLimit.Wait(ctx)
|
||||
resp, err := c.client.Drive.File.UploadPart(ctx, req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return nil, errors.New(resp.Error())
|
||||
}
|
||||
|
||||
up(float64(i) / float64(blockCount))
|
||||
}
|
||||
|
||||
//close
|
||||
closeReq := larkdrive.NewUploadFinishFileReqBuilder().
|
||||
Body(larkdrive.NewUploadFinishFileReqBodyBuilder().
|
||||
UploadId(uploadId).
|
||||
BlockNum(blockCount).
|
||||
Build()).
|
||||
Build()
|
||||
|
||||
// 发起请求
|
||||
closeResp, err := c.client.Drive.File.UploadFinish(ctx, closeReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !closeResp.Success() {
|
||||
return nil, errors.New(closeResp.Error())
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: *closeResp.Data.FileToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
//func (d *Lark) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
var _ driver.Driver = (*Lark)(nil)
|
36
drivers/lark/meta.go
Normal file
36
drivers/lark/meta.go
Normal file
@ -0,0 +1,36 @@
|
||||
package lark
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
// Usually one of two
|
||||
driver.RootPath
|
||||
// define other
|
||||
AppId string `json:"app_id" type:"text" help:"app id"`
|
||||
AppSecret string `json:"app_secret" type:"text" help:"app secret"`
|
||||
ExternalMode bool `json:"external_mode" type:"bool" help:"external mode"`
|
||||
TenantUrlPrefix string `json:"tenant_url_prefix" type:"text" help:"tenant url prefix"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Lark",
|
||||
LocalSort: false,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "/",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Lark{}
|
||||
})
|
||||
}
|
32
drivers/lark/types.go
Normal file
32
drivers/lark/types.go
Normal file
@ -0,0 +1,32 @@
|
||||
package lark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Xhofe/go-cache"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TokenCache struct {
|
||||
cache.ICache[string]
|
||||
}
|
||||
|
||||
func (t *TokenCache) Set(_ context.Context, key string, value string, expireTime time.Duration) error {
|
||||
t.ICache.Set(key, value, cache.WithEx[string](expireTime))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TokenCache) Get(_ context.Context, key string) (string, error) {
|
||||
v, ok := t.ICache.Get(key)
|
||||
if ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func newTokenCache() *TokenCache {
|
||||
c := cache.NewMemCache[string]()
|
||||
|
||||
return &TokenCache{c}
|
||||
}
|
66
drivers/lark/util.go
Normal file
66
drivers/lark/util.go
Normal file
@ -0,0 +1,66 @@
|
||||
package lark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Xhofe/go-cache"
|
||||
larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
const objTokenCacheDuration = 5 * time.Minute
|
||||
const emptyFolderToken = "empty"
|
||||
|
||||
var objTokenCache = cache.NewMemCache[string]()
|
||||
var exOpts = cache.WithEx[string](objTokenCacheDuration)
|
||||
|
||||
func (c *Lark) getObjToken(ctx context.Context, folderPath string) (string, bool) {
|
||||
if token, ok := objTokenCache.Get(folderPath); ok {
|
||||
return token, true
|
||||
}
|
||||
|
||||
dir, name := path.Split(folderPath)
|
||||
// strip the last slash of dir if it exists
|
||||
if len(dir) > 0 && dir[len(dir)-1] == '/' {
|
||||
dir = dir[:len(dir)-1]
|
||||
}
|
||||
if name == "" {
|
||||
return c.rootFolderToken, true
|
||||
}
|
||||
|
||||
var parentToken string
|
||||
var found bool
|
||||
parentToken, found = c.getObjToken(ctx, dir)
|
||||
if !found {
|
||||
return emptyFolderToken, false
|
||||
}
|
||||
|
||||
req := larkdrive.NewListFileReqBuilder().FolderToken(parentToken).Build()
|
||||
resp, err := c.client.Drive.File.ListByIterator(ctx, req)
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to list files")
|
||||
return emptyFolderToken, false
|
||||
}
|
||||
|
||||
var file *larkdrive.File
|
||||
for {
|
||||
found, file, err = resp.Next()
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to get next file")
|
||||
break
|
||||
}
|
||||
|
||||
if *file.Name == name {
|
||||
objTokenCache.Set(folderPath, *file.Token, exOpts)
|
||||
return *file.Token, true
|
||||
}
|
||||
}
|
||||
|
||||
return emptyFolderToken, false
|
||||
}
|
@ -206,7 +206,7 @@ func (d *MediaTrack) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
||||
return err
|
||||
}
|
||||
h := md5.New()
|
||||
_, err = io.Copy(h, tempFile)
|
||||
_, err = utils.CopyWithBuffer(h, tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rclone/rclone/lib/readers"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
@ -33,8 +34,16 @@ func (d *Mega) GetAddition() driver.Additional {
|
||||
}
|
||||
|
||||
func (d *Mega) Init(ctx context.Context) error {
|
||||
var twoFACode = d.TwoFACode
|
||||
d.c = mega.New()
|
||||
return d.c.Login(d.Email, d.Password)
|
||||
if d.TwoFASecret != "" {
|
||||
code, err := totp.GenerateCode(d.TwoFASecret, time.Now())
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate totp code failed: %w", err)
|
||||
}
|
||||
twoFACode = code
|
||||
}
|
||||
return d.c.MultiFactorLogin(d.Email, d.Password, twoFACode)
|
||||
}
|
||||
|
||||
func (d *Mega) Drop(ctx context.Context) error {
|
||||
|
@ -9,8 +9,10 @@ type Addition struct {
|
||||
// Usually one of two
|
||||
//driver.RootPath
|
||||
//driver.RootID
|
||||
Email string `json:"email" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
Email string `json:"email" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
TwoFACode string `json:"two_fa_code" required:"false" help:"2FA 6-digit code, filling in the 2FA code alone will not support reloading driver"`
|
||||
TwoFASecret string `json:"two_fa_secret" required:"false" help:"2FA secret"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -295,7 +295,7 @@ func (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
|
||||
}
|
||||
|
||||
if !initUpdload.FileDataExists {
|
||||
utils.Log.Error(d.client.CloudDiskStartBusiness())
|
||||
// utils.Log.Error(d.client.CloudDiskStartBusiness())
|
||||
|
||||
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
|
||||
retry.Attempts(3),
|
||||
@ -323,6 +323,7 @@ func (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ContentLength = byteSize
|
||||
resp, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
|
135
drivers/netease_music/crypto.go
Normal file
135
drivers/netease_music/crypto.go
Normal file
@ -0,0 +1,135 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||
)
|
||||
|
||||
var (
|
||||
linuxapiKey = []byte("rFgB&h#%2?^eDg:Q")
|
||||
eapiKey = []byte("e82ckenh8dichen8")
|
||||
iv = []byte("0102030405060708")
|
||||
presetKey = []byte("0CoJUm6Qyw8W8jud")
|
||||
publicKey = []byte("-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----")
|
||||
stdChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
)
|
||||
|
||||
func aesKeyPending(key []byte) []byte {
|
||||
k := len(key)
|
||||
count := 0
|
||||
switch true {
|
||||
case k <= 16:
|
||||
count = 16 - k
|
||||
case k <= 24:
|
||||
count = 24 - k
|
||||
case k <= 32:
|
||||
count = 32 - k
|
||||
default:
|
||||
return key[:32]
|
||||
}
|
||||
if count == 0 {
|
||||
return key
|
||||
}
|
||||
|
||||
return append(key, bytes.Repeat([]byte{0}, count)...)
|
||||
}
|
||||
|
||||
func pkcs7Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padtext...)
|
||||
}
|
||||
|
||||
func aesCBCEncrypt(src, key, iv []byte) []byte {
|
||||
block, _ := aes.NewCipher(aesKeyPending(key))
|
||||
src = pkcs7Padding(src, block.BlockSize())
|
||||
dst := make([]byte, len(src))
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(dst, src)
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func aesECBEncrypt(src, key []byte) []byte {
|
||||
block, _ := aes.NewCipher(aesKeyPending(key))
|
||||
|
||||
src = pkcs7Padding(src, block.BlockSize())
|
||||
dst := make([]byte, len(src))
|
||||
|
||||
ecbCryptBlocks(block, dst, src)
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func ecbCryptBlocks(block cipher.Block, dst, src []byte) {
|
||||
bs := block.BlockSize()
|
||||
|
||||
for len(src) > 0 {
|
||||
block.Encrypt(dst, src[:bs])
|
||||
src = src[bs:]
|
||||
dst = dst[bs:]
|
||||
}
|
||||
}
|
||||
|
||||
func rsaEncrypt(buffer, key []byte) []byte {
|
||||
buffers := make([]byte, 128-16, 128)
|
||||
buffers = append(buffers, buffer...)
|
||||
block, _ := pem.Decode(key)
|
||||
pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
pub := pubInterface.(*rsa.PublicKey)
|
||||
c := new(big.Int).SetBytes([]byte(buffers))
|
||||
return c.Exp(c, big.NewInt(int64(pub.E)), pub.N).Bytes()
|
||||
}
|
||||
|
||||
func getSecretKey() ([]byte, []byte) {
|
||||
key := make([]byte, 16)
|
||||
reversed := make([]byte, 16)
|
||||
for i := 0; i < 16; i++ {
|
||||
result := stdChars[random.RangeInt64(0, 62)]
|
||||
key[i] = result
|
||||
reversed[15-i] = result
|
||||
}
|
||||
return key, reversed
|
||||
}
|
||||
|
||||
func weapi(data map[string]string) map[string]string {
|
||||
text, _ := utils.Json.Marshal(data)
|
||||
secretKey, reversedKey := getSecretKey()
|
||||
params := []byte(base64.StdEncoding.EncodeToString(aesCBCEncrypt(text, presetKey, iv)))
|
||||
return map[string]string{
|
||||
"params": base64.StdEncoding.EncodeToString(aesCBCEncrypt(params, reversedKey, iv)),
|
||||
"encSecKey": hex.EncodeToString(rsaEncrypt(secretKey, publicKey)),
|
||||
}
|
||||
}
|
||||
|
||||
func eapi(url string, data map[string]interface{}) map[string]string {
|
||||
text, _ := utils.Json.Marshal(data)
|
||||
msg := "nobody" + url + "use" + string(text) + "md5forencrypt"
|
||||
h := md5.New()
|
||||
h.Write([]byte(msg))
|
||||
digest := hex.EncodeToString(h.Sum(nil))
|
||||
params := []byte(url + "-36cd479b6b5-" + string(text) + "-36cd479b6b5-" + digest)
|
||||
return map[string]string{
|
||||
"params": hex.EncodeToString(aesECBEncrypt(params, eapiKey)),
|
||||
}
|
||||
}
|
||||
|
||||
func linuxapi(data map[string]interface{}) map[string]string {
|
||||
text, _ := utils.Json.Marshal(data)
|
||||
return map[string]string{
|
||||
"eparams": strings.ToUpper(hex.EncodeToString(aesECBEncrypt(text, linuxapiKey))),
|
||||
}
|
||||
}
|
110
drivers/netease_music/driver.go
Normal file
110
drivers/netease_music/driver.go
Normal file
@ -0,0 +1,110 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
type NeteaseMusic struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
csrfToken string
|
||||
musicU string
|
||||
fileMapByName map[string]model.Obj
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Init(ctx context.Context) error {
|
||||
d.csrfToken = d.Addition.getCookie("__csrf")
|
||||
d.musicU = d.Addition.getCookie("MUSIC_U")
|
||||
|
||||
if d.csrfToken == "" || d.musicU == "" {
|
||||
return errs.EmptyToken
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Get(ctx context.Context, path string) (model.Obj, error) {
|
||||
if path == "/" {
|
||||
return &model.Object{
|
||||
IsFolder: true,
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
fragments := strings.Split(path, "/")
|
||||
if len(fragments) > 1 {
|
||||
fileName := fragments[1]
|
||||
if strings.HasSuffix(fileName, ".lrc") {
|
||||
lrc := d.fileMapByName[fileName]
|
||||
return d.getLyricObj(lrc)
|
||||
}
|
||||
if song, ok := d.fileMapByName[fileName]; ok {
|
||||
return song, nil
|
||||
} else {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
return d.getSongObjs(args)
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
if lrc, ok := file.(*LyricObj); ok {
|
||||
if args.Type == "parsed" {
|
||||
return lrc.getLyricLink(), nil
|
||||
} else {
|
||||
return lrc.getProxyLink(args), nil
|
||||
}
|
||||
}
|
||||
|
||||
return d.getSongLink(file)
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Remove(ctx context.Context, obj model.Obj) error {
|
||||
return d.removeSongObj(obj)
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
return d.putSongStream(stream)
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*NeteaseMusic)(nil)
|
32
drivers/netease_music/meta.go
Normal file
32
drivers/netease_music/meta.go
Normal file
@ -0,0 +1,32 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
Cookie string `json:"cookie" type:"text" required:"true" help:""`
|
||||
SongLimit uint64 `json:"song_limit" default:"200" type:"number" help:"only get 200 songs by default"`
|
||||
}
|
||||
|
||||
func (ad *Addition) getCookie(name string) string {
|
||||
re := regexp.MustCompile(name + "=([^(;|$)]+)")
|
||||
matches := re.FindStringSubmatch(ad.Cookie)
|
||||
if len(matches) < 2 {
|
||||
return ""
|
||||
}
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "NeteaseMusic",
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &NeteaseMusic{}
|
||||
})
|
||||
}
|
116
drivers/netease_music/types.go
Normal file
116
drivers/netease_music/types.go
Normal file
@ -0,0 +1,116 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/sign"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
)
|
||||
|
||||
type HostsResp struct {
|
||||
Upload []string `json:"upload"`
|
||||
}
|
||||
|
||||
type SongResp struct {
|
||||
Data []struct {
|
||||
Url string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ListResp struct {
|
||||
Size string `json:"size"`
|
||||
MaxSize string `json:"maxSize"`
|
||||
Data []struct {
|
||||
AddTime int64 `json:"addTime"`
|
||||
FileName string `json:"fileName"`
|
||||
FileSize int64 `json:"fileSize"`
|
||||
SongId int64 `json:"songId"`
|
||||
SimpleSong struct {
|
||||
Al struct {
|
||||
PicUrl string `json:"picUrl"`
|
||||
} `json:"al"`
|
||||
} `json:"simpleSong"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type LyricObj struct {
|
||||
model.Object
|
||||
lyric string
|
||||
}
|
||||
|
||||
func (lrc *LyricObj) getProxyLink(args model.LinkArgs) *model.Link {
|
||||
rawURL := common.GetApiUrl(args.HttpReq) + "/p" + lrc.Path
|
||||
rawURL = utils.EncodePath(rawURL, true) + "?type=parsed&sign=" + sign.Sign(lrc.Path)
|
||||
return &model.Link{URL: rawURL}
|
||||
}
|
||||
|
||||
func (lrc *LyricObj) getLyricLink() *model.Link {
|
||||
reader := strings.NewReader(lrc.lyric)
|
||||
return &model.Link{
|
||||
RangeReadCloser: &model.RangeReadCloser{
|
||||
RangeReader: func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
|
||||
if httpRange.Length < 0 {
|
||||
return io.NopCloser(reader), nil
|
||||
}
|
||||
sr := io.NewSectionReader(reader, httpRange.Start, httpRange.Length)
|
||||
return io.NopCloser(sr), nil
|
||||
},
|
||||
Closers: utils.EmptyClosers(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ReqOption struct {
|
||||
crypto string
|
||||
stream model.FileStreamer
|
||||
data map[string]string
|
||||
headers map[string]string
|
||||
cookies []*http.Cookie
|
||||
url string
|
||||
}
|
||||
|
||||
type Characteristic map[string]string
|
||||
|
||||
func (ch *Characteristic) fromDriver(d *NeteaseMusic) *Characteristic {
|
||||
*ch = map[string]string{
|
||||
"osver": "",
|
||||
"deviceId": "",
|
||||
"mobilename": "",
|
||||
"appver": "6.1.1",
|
||||
"versioncode": "140",
|
||||
"buildver": strconv.FormatInt(time.Now().Unix(), 10),
|
||||
"resolution": "1920x1080",
|
||||
"os": "android",
|
||||
"channel": "",
|
||||
"requestId": strconv.FormatInt(time.Now().Unix()*1000, 10) + strconv.Itoa(int(random.RangeInt64(0, 1000))),
|
||||
"MUSIC_U": d.musicU,
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (ch Characteristic) toCookies() []*http.Cookie {
|
||||
cookies := make([]*http.Cookie, 0)
|
||||
for k, v := range ch {
|
||||
cookies = append(cookies, &http.Cookie{Name: k, Value: v})
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
|
||||
func (ch *Characteristic) merge(data map[string]string) map[string]interface{} {
|
||||
body := map[string]interface{}{
|
||||
"header": ch,
|
||||
}
|
||||
for k, v := range data {
|
||||
body[k] = v
|
||||
}
|
||||
return body
|
||||
}
|
208
drivers/netease_music/upload.go
Normal file
208
drivers/netease_music/upload.go
Normal file
@ -0,0 +1,208 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/dhowden/tag"
|
||||
)
|
||||
|
||||
type token struct {
|
||||
resourceId string
|
||||
objectKey string
|
||||
token string
|
||||
}
|
||||
|
||||
type songmeta struct {
|
||||
needUpload bool
|
||||
songId string
|
||||
name string
|
||||
artist string
|
||||
album string
|
||||
}
|
||||
|
||||
type uploader struct {
|
||||
driver *NeteaseMusic
|
||||
file model.File
|
||||
meta songmeta
|
||||
md5 string
|
||||
ext string
|
||||
size string
|
||||
filename string
|
||||
}
|
||||
|
||||
func (u *uploader) init(stream model.FileStreamer) error {
|
||||
u.filename = stream.GetName()
|
||||
u.size = strconv.FormatInt(stream.GetSize(), 10)
|
||||
|
||||
u.ext = "mp3"
|
||||
if strings.HasSuffix(stream.GetMimetype(), "flac") {
|
||||
u.ext = "flac"
|
||||
}
|
||||
|
||||
h := md5.New()
|
||||
io.Copy(h, stream)
|
||||
u.md5 = hex.EncodeToString(h.Sum(nil))
|
||||
_, err := u.file.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m, err := tag.ReadFrom(u.file); err != nil {
|
||||
u.meta = songmeta{}
|
||||
} else {
|
||||
u.meta = songmeta{
|
||||
name: m.Title(),
|
||||
artist: m.Artist(),
|
||||
album: m.Album(),
|
||||
}
|
||||
}
|
||||
if u.meta.name == "" {
|
||||
u.meta.name = u.filename
|
||||
}
|
||||
if u.meta.album == "" {
|
||||
u.meta.album = "未知专辑"
|
||||
}
|
||||
if u.meta.artist == "" {
|
||||
u.meta.artist = "未知艺术家"
|
||||
}
|
||||
_, err = u.file.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *uploader) checkIfExisted() error {
|
||||
body, err := u.driver.request("https://interface.music.163.com/api/cloud/upload/check", http.MethodPost,
|
||||
ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"ext": "",
|
||||
"songId": "0",
|
||||
"version": "1",
|
||||
"bitrate": "999000",
|
||||
"length": u.size,
|
||||
"md5": u.md5,
|
||||
},
|
||||
cookies: []*http.Cookie{
|
||||
{Name: "os", Value: "pc"},
|
||||
{Name: "appver", Value: "2.9.7"},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.meta.songId = utils.Json.Get(body, "songId").ToString()
|
||||
u.meta.needUpload = utils.Json.Get(body, "needUpload").ToBool()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *uploader) allocToken(bucket ...string) (token, error) {
|
||||
if len(bucket) == 0 {
|
||||
bucket = []string{""}
|
||||
}
|
||||
|
||||
body, err := u.driver.request("https://music.163.com/weapi/nos/token/alloc", http.MethodPost, ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"bucket": bucket[0],
|
||||
"local": "false",
|
||||
"type": "audio",
|
||||
"nos_product": "3",
|
||||
"filename": u.filename,
|
||||
"md5": u.md5,
|
||||
"ext": u.ext,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return token{}, err
|
||||
}
|
||||
|
||||
return token{
|
||||
resourceId: utils.Json.Get(body, "result", "resourceId").ToString(),
|
||||
objectKey: utils.Json.Get(body, "result", "objectKey").ToString(),
|
||||
token: utils.Json.Get(body, "result", "token").ToString(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *uploader) publishInfo(resourceId string) error {
|
||||
body, err := u.driver.request("https://music.163.com/api/upload/cloud/info/v2", http.MethodPost, ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"md5": u.md5,
|
||||
"filename": u.filename,
|
||||
"song": u.meta.name,
|
||||
"album": u.meta.album,
|
||||
"artist": u.meta.artist,
|
||||
"songid": u.meta.songId,
|
||||
"resourceId": resourceId,
|
||||
"bitrate": "999000",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = u.driver.request("https://interface.music.163.com/api/cloud/pub/v2", http.MethodPost, ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"songid": utils.Json.Get(body, "songId").ToString(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *uploader) upload(stream model.FileStreamer) error {
|
||||
bucket := "jd-musicrep-privatecloud-audio-public"
|
||||
token, err := u.allocToken(bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := u.driver.request("https://wanproxy.127.net/lbs?version=1.0&bucketname="+bucket, http.MethodGet,
|
||||
ReqOption{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var resp HostsResp
|
||||
err = utils.Json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objectKey := strings.ReplaceAll(token.objectKey, "/", "%2F")
|
||||
_, err = u.driver.request(
|
||||
resp.Upload[0]+"/"+bucket+"/"+objectKey+"?offset=0&complete=true&version=1.0",
|
||||
http.MethodPost,
|
||||
ReqOption{
|
||||
stream: stream,
|
||||
headers: map[string]string{
|
||||
"x-nos-token": token.token,
|
||||
"Content-Type": "audio/mpeg",
|
||||
"Content-Length": u.size,
|
||||
"Content-MD5": u.md5,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
246
drivers/netease_music/util.go
Normal file
246
drivers/netease_music/util.go
Normal file
@ -0,0 +1,246 @@
|
||||
package netease_music
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
func (d *NeteaseMusic) request(url, method string, opt ReqOption) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
|
||||
req.SetHeader("Cookie", d.Addition.Cookie)
|
||||
|
||||
if strings.Contains(url, "music.163.com") {
|
||||
req.SetHeader("Referer", "https://music.163.com")
|
||||
}
|
||||
|
||||
if opt.cookies != nil {
|
||||
for _, cookie := range opt.cookies {
|
||||
req.SetCookie(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
if opt.headers != nil {
|
||||
for header, value := range opt.headers {
|
||||
req.SetHeader(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
data := opt.data
|
||||
if opt.crypto == "weapi" {
|
||||
data = weapi(data)
|
||||
re, _ := regexp.Compile(`/\w*api/`)
|
||||
url = re.ReplaceAllString(url, "/weapi/")
|
||||
} else if opt.crypto == "eapi" {
|
||||
ch := new(Characteristic).fromDriver(d)
|
||||
req.SetCookies(ch.toCookies())
|
||||
data = eapi(opt.url, ch.merge(data))
|
||||
re, _ := regexp.Compile(`/\w*api/`)
|
||||
url = re.ReplaceAllString(url, "/eapi/")
|
||||
} else if opt.crypto == "linuxapi" {
|
||||
re, _ := regexp.Compile(`/\w*api/`)
|
||||
data = linuxapi(map[string]interface{}{
|
||||
"url": re.ReplaceAllString(url, "/api/"),
|
||||
"method": method,
|
||||
"params": data,
|
||||
})
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36")
|
||||
url = "https://music.163.com/api/linux/forward"
|
||||
}
|
||||
|
||||
if method == http.MethodPost {
|
||||
if opt.stream != nil {
|
||||
req.SetContentLength(true)
|
||||
req.SetBody(io.ReadCloser(opt.stream))
|
||||
} else {
|
||||
req.SetFormData(data)
|
||||
}
|
||||
res, err := req.Post(url)
|
||||
return res.Body(), err
|
||||
}
|
||||
|
||||
if method == http.MethodGet {
|
||||
res, err := req.Get(url)
|
||||
return res.Body(), err
|
||||
}
|
||||
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) getSongObjs(args model.ListArgs) ([]model.Obj, error) {
|
||||
body, err := d.request("https://music.163.com/weapi/v1/cloud/get", http.MethodPost, ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"limit": strconv.FormatUint(d.Addition.SongLimit, 10),
|
||||
"offset": "0",
|
||||
},
|
||||
cookies: []*http.Cookie{
|
||||
{Name: "os", Value: "pc"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp ListResp
|
||||
err = utils.Json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.fileMapByName = make(map[string]model.Obj)
|
||||
files := make([]model.Obj, 0, len(resp.Data))
|
||||
for _, f := range resp.Data {
|
||||
song := &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
IsFolder: false,
|
||||
Size: f.FileSize,
|
||||
Name: f.FileName,
|
||||
Modified: time.UnixMilli(f.AddTime),
|
||||
ID: strconv.FormatInt(f.SongId, 10),
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: f.SimpleSong.Al.PicUrl},
|
||||
}
|
||||
d.fileMapByName[song.Name] = song
|
||||
files = append(files, song)
|
||||
|
||||
// map song id for lyric
|
||||
lrcName := strings.Split(f.FileName, ".")[0] + ".lrc"
|
||||
lrc := &model.Object{
|
||||
IsFolder: false,
|
||||
Name: lrcName,
|
||||
Path: path.Join(args.ReqPath, lrcName),
|
||||
ID: strconv.FormatInt(f.SongId, 10),
|
||||
}
|
||||
d.fileMapByName[lrc.Name] = lrc
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) getSongLink(file model.Obj) (*model.Link, error) {
|
||||
body, err := d.request(
|
||||
"https://music.163.com/api/song/enhance/player/url", http.MethodPost, ReqOption{
|
||||
crypto: "linuxapi",
|
||||
data: map[string]string{
|
||||
"ids": "[" + file.GetID() + "]",
|
||||
"br": "999000",
|
||||
},
|
||||
cookies: []*http.Cookie{
|
||||
{Name: "os", Value: "pc"},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp SongResp
|
||||
err = utils.Json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.Data) < 1 {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
|
||||
return &model.Link{URL: resp.Data[0].Url}, nil
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) getLyricObj(file model.Obj) (model.Obj, error) {
|
||||
if lrc, ok := file.(*LyricObj); ok {
|
||||
return lrc, nil
|
||||
}
|
||||
|
||||
body, err := d.request(
|
||||
"https://music.163.com/api/song/lyric?_nmclfl=1", http.MethodPost, ReqOption{
|
||||
data: map[string]string{
|
||||
"id": file.GetID(),
|
||||
"tv": "-1",
|
||||
"lv": "-1",
|
||||
"rv": "-1",
|
||||
"kv": "-1",
|
||||
},
|
||||
cookies: []*http.Cookie{
|
||||
{Name: "os", Value: "ios"},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lyric := utils.Json.Get(body, "lrc", "lyric").ToString()
|
||||
|
||||
return &LyricObj{
|
||||
lyric: lyric,
|
||||
Object: model.Object{
|
||||
IsFolder: false,
|
||||
ID: file.GetID(),
|
||||
Name: file.GetName(),
|
||||
Path: file.GetPath(),
|
||||
Size: int64(len(lyric)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) removeSongObj(file model.Obj) error {
|
||||
_, err := d.request("http://music.163.com/weapi/cloud/del", http.MethodPost, ReqOption{
|
||||
crypto: "weapi",
|
||||
data: map[string]string{
|
||||
"songIds": "[" + file.GetID() + "]",
|
||||
},
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *NeteaseMusic) putSongStream(stream model.FileStreamer) error {
|
||||
tmp, err := stream.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tmp.Close()
|
||||
|
||||
u := uploader{driver: d, file: tmp}
|
||||
|
||||
err = u.init(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = u.checkIfExisted()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := u.allocToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.meta.needUpload {
|
||||
err = u.upload(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = u.publishInfo(token.resourceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -118,6 +118,7 @@ func (d *Onedrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName str
|
||||
"folder": base.Json{},
|
||||
"@microsoft.graph.conflictBehavior": "rename",
|
||||
}
|
||||
// todo 修复文件夹 ctime/mtime, onedrive 可在 data 里设置 fileSystemInfo 字段, 但是此接口未提供 ctime/mtime
|
||||
_, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
|
@ -24,12 +24,12 @@ type RespErr struct {
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
LastModifiedDateTime time.Time `json:"lastModifiedDateTime"`
|
||||
Url string `json:"@microsoft.graph.downloadUrl"`
|
||||
File *struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
FileSystemInfo *FileSystemInfoFacet `json:"fileSystemInfo"`
|
||||
Url string `json:"@microsoft.graph.downloadUrl"`
|
||||
File *struct {
|
||||
MimeType string `json:"mimeType"`
|
||||
} `json:"file"`
|
||||
Thumbnails []struct {
|
||||
@ -58,7 +58,7 @@ func fileToObj(f File, parentID string) *Object {
|
||||
ID: f.Id,
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Modified: f.LastModifiedDateTime,
|
||||
Modified: f.FileSystemInfo.LastModifiedDateTime,
|
||||
IsFolder: f.File == nil,
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: thumb},
|
||||
@ -72,3 +72,20 @@ type Files struct {
|
||||
Value []File `json:"value"`
|
||||
NextLink string `json:"@odata.nextLink"`
|
||||
}
|
||||
|
||||
// Metadata represents a request to update Metadata.
|
||||
// It includes only the writeable properties.
|
||||
// omitempty is intentionally included for all, per https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update?view=odsp-graph-online#request-body
|
||||
type Metadata struct {
|
||||
Description string `json:"description,omitempty"` // Provides a user-visible description of the item. Read-write. Only on OneDrive Personal. Undocumented limit of 1024 characters.
|
||||
FileSystemInfo *FileSystemInfoFacet `json:"fileSystemInfo,omitempty"` // File system information on client. Read-write.
|
||||
}
|
||||
|
||||
// FileSystemInfoFacet contains properties that are reported by the
|
||||
// device's local file system for the local version of an item. This
|
||||
// facet can be used to specify the last modified date or created date
|
||||
// of the item as it was on the local device.
|
||||
type FileSystemInfoFacet struct {
|
||||
CreatedDateTime time.Time `json:"createdDateTime,omitempty"` // The UTC date and time the file was created on a client.
|
||||
LastModifiedDateTime time.Time `json:"lastModifiedDateTime,omitempty"` // The UTC date and time the file was last modified on a client.
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ func (d *Onedrive) Request(url string, method string, callback base.ReqCallback,
|
||||
|
||||
func (d *Onedrive) getFiles(path string) ([]File, error) {
|
||||
var res []File
|
||||
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,lastModifiedDateTime,content.downloadUrl,file,parentReference"
|
||||
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,fileSystemInfo,content.downloadUrl,file,parentReference"
|
||||
for nextLink != "" {
|
||||
var files Files
|
||||
_, err := d.Request(nextLink, http.MethodGet, nil, &files)
|
||||
@ -148,7 +148,10 @@ func (d *Onedrive) GetFile(path string) (*File, error) {
|
||||
}
|
||||
|
||||
func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) error {
|
||||
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/content"
|
||||
filepath := stdpath.Join(dstDir.GetPath(), stream.GetName())
|
||||
// 1. upload new file
|
||||
// ApiDoc: https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online
|
||||
url := d.GetMetaUrl(false, filepath) + "/content"
|
||||
data, err := io.ReadAll(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -156,12 +159,50 @@ func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.F
|
||||
_, err = d.Request(url, http.MethodPut, func(req *resty.Request) {
|
||||
req.SetBody(data).SetContext(ctx)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("onedrive: Failed to upload new file(path=%v): %w", filepath, err)
|
||||
}
|
||||
|
||||
// 2. update metadata
|
||||
err = d.updateMetadata(ctx, stream, filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("onedrive: Failed to update file(path=%v) metadata: %w", filepath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Onedrive) updateMetadata(ctx context.Context, stream model.FileStreamer, filepath string) error {
|
||||
url := d.GetMetaUrl(false, filepath)
|
||||
metadata := toAPIMetadata(stream)
|
||||
// ApiDoc: https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update?view=odsp-graph-online
|
||||
_, err := d.Request(url, http.MethodPatch, func(req *resty.Request) {
|
||||
req.SetBody(metadata).SetContext(ctx)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func toAPIMetadata(stream model.FileStreamer) Metadata {
|
||||
metadata := Metadata{
|
||||
FileSystemInfo: &FileSystemInfoFacet{},
|
||||
}
|
||||
if !stream.ModTime().IsZero() {
|
||||
metadata.FileSystemInfo.LastModifiedDateTime = stream.ModTime()
|
||||
}
|
||||
if !stream.CreateTime().IsZero() {
|
||||
metadata.FileSystemInfo.CreatedDateTime = stream.CreateTime()
|
||||
}
|
||||
if stream.CreateTime().IsZero() && !stream.ModTime().IsZero() {
|
||||
metadata.FileSystemInfo.CreatedDateTime = stream.CreateTime()
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/createUploadSession"
|
||||
res, err := d.Request(url, http.MethodPost, nil, nil)
|
||||
metadata := map[string]interface{}{"item": toAPIMetadata(stream)}
|
||||
res, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(metadata).SetContext(ctx)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -17,13 +17,14 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type PikPak struct {
|
||||
model.Storage
|
||||
Addition
|
||||
RefreshToken string
|
||||
AccessToken string
|
||||
|
||||
oauth2Token oauth2.TokenSource
|
||||
}
|
||||
|
||||
func (d *PikPak) Config() driver.Config {
|
||||
@ -34,8 +35,32 @@ func (d *PikPak) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *PikPak) Init(ctx context.Context) error {
|
||||
return d.login()
|
||||
func (d *PikPak) Init(ctx context.Context) (err error) {
|
||||
if d.ClientID == "" || d.ClientSecret == "" {
|
||||
d.ClientID = "YNxT9w7GMdWvEOKa"
|
||||
d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
}
|
||||
|
||||
withClient := func(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, oauth2.HTTPClient, base.HttpClient)
|
||||
}
|
||||
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: d.ClientID,
|
||||
ClientSecret: d.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://user.mypikpak.com/v1/auth/signin",
|
||||
TokenURL: "https://user.mypikpak.com/v1/auth/token",
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
},
|
||||
}
|
||||
|
||||
oauth2Token, err := oauth2Config.PasswordCredentialsToken(withClient(ctx), d.Username, d.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.oauth2Token = oauth2Config.TokenSource(withClient(context.Background()), oauth2Token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) Drop(ctx context.Context) error {
|
||||
|
@ -9,6 +9,8 @@ type Addition struct {
|
||||
driver.RootID
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
|
||||
DisableMediaLink bool `json:"disable_media_link"`
|
||||
}
|
||||
|
||||
|
@ -1,77 +1,24 @@
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *PikPak) login() error {
|
||||
url := "https://user.mypikpak.com/v1/auth/signin"
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
|
||||
"captcha_token": "",
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"username": d.Username,
|
||||
"password": d.Password,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
return errors.New(e.Error)
|
||||
}
|
||||
data := res.Body()
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) refreshToken() error {
|
||||
url := "https://user.mypikpak.com/v1/auth/token"
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).
|
||||
SetHeader("user-agent", "").SetBody(base.Json{
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": d.RefreshToken,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
d.Status = err.Error()
|
||||
op.MustSaveDriverStorage(d)
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 4126 {
|
||||
// refresh_token invalid, re-login
|
||||
return d.login()
|
||||
}
|
||||
d.Status = e.Error
|
||||
op.MustSaveDriverStorage(d)
|
||||
return errors.New(e.Error)
|
||||
}
|
||||
data := res.Body()
|
||||
d.Status = "work"
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
@ -84,17 +31,9 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 16 {
|
||||
// login / refresh token
|
||||
err = d.refreshToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
} else {
|
||||
return nil, errors.New(e.Error)
|
||||
}
|
||||
return nil, errors.New(e.Error)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
@ -126,28 +65,3 @@ func (d *PikPak) getFiles(id string) ([]File, error) {
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getGcid(r io.Reader, size int64) (string, error) {
|
||||
calcBlockSize := func(j int64) int64 {
|
||||
var psize int64 = 0x40000
|
||||
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
|
||||
psize = psize << 1
|
||||
}
|
||||
return psize
|
||||
}
|
||||
|
||||
hash1 := sha1.New()
|
||||
hash2 := sha1.New()
|
||||
readSize := calcBlockSize(size)
|
||||
for {
|
||||
hash2.Reset()
|
||||
if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
|
||||
if err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
}
|
||||
hash1.Write(hash2.Sum(nil))
|
||||
}
|
||||
return hex.EncodeToString(hash1.Sum(nil)), nil
|
||||
}
|
||||
|
@ -4,17 +4,18 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type PikPakShare struct {
|
||||
model.Storage
|
||||
Addition
|
||||
RefreshToken string
|
||||
AccessToken string
|
||||
oauth2Token oauth2.TokenSource
|
||||
PassCodeToken string
|
||||
}
|
||||
|
||||
@ -27,10 +28,31 @@ func (d *PikPakShare) GetAddition() driver.Additional {
|
||||
}
|
||||
|
||||
func (d *PikPakShare) Init(ctx context.Context) error {
|
||||
err := d.login()
|
||||
if d.ClientID == "" || d.ClientSecret == "" {
|
||||
d.ClientID = "YNxT9w7GMdWvEOKa"
|
||||
d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
}
|
||||
|
||||
withClient := func(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, oauth2.HTTPClient, base.HttpClient)
|
||||
}
|
||||
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: d.ClientID,
|
||||
ClientSecret: d.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://user.mypikpak.com/v1/auth/signin",
|
||||
TokenURL: "https://user.mypikpak.com/v1/auth/token",
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
},
|
||||
}
|
||||
|
||||
oauth2Token, err := oauth2Config.PasswordCredentialsToken(withClient(ctx), d.Username, d.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.oauth2Token = oauth2Config.TokenSource(withClient(context.Background()), oauth2Token)
|
||||
|
||||
if d.SharePwd != "" {
|
||||
err = d.getSharePassToken()
|
||||
if err != nil {
|
||||
@ -67,8 +89,14 @@ func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkA
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadUrl := resp.FileInfo.WebContentLink
|
||||
if downloadUrl == "" && len(resp.FileInfo.Medias) > 0 {
|
||||
downloadUrl = resp.FileInfo.Medias[0].Link.Url
|
||||
}
|
||||
|
||||
link := model.Link{
|
||||
URL: resp.FileInfo.WebContentLink,
|
||||
URL: downloadUrl,
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
@ -7,10 +7,12 @@ import (
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
ShareId string `json:"share_id" required:"true"`
|
||||
SharePwd string `json:"share_pwd"`
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
ShareId string `json:"share_id" required:"true"`
|
||||
SharePwd string `json:"share_pwd"`
|
||||
ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -5,70 +5,18 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *PikPakShare) login() error {
|
||||
url := "https://user.mypikpak.com/v1/auth/signin"
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
|
||||
"captcha_token": "",
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"username": d.Username,
|
||||
"password": d.Password,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
return errors.New(e.Error)
|
||||
}
|
||||
data := res.Body()
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPakShare) refreshToken() error {
|
||||
url := "https://user.mypikpak.com/v1/auth/token"
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).
|
||||
SetHeader("user-agent", "").SetBody(base.Json{
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": d.RefreshToken,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
d.Status = err.Error()
|
||||
op.MustSaveDriverStorage(d)
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 4126 {
|
||||
// refresh_token invalid, re-login
|
||||
return d.login()
|
||||
}
|
||||
d.Status = e.Error
|
||||
op.MustSaveDriverStorage(d)
|
||||
return errors.New(e.Error)
|
||||
}
|
||||
data := res.Body()
|
||||
d.Status = "work"
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
@ -82,14 +30,6 @@ func (d *PikPakShare) request(url string, method string, callback base.ReqCallba
|
||||
return nil, err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 16 {
|
||||
// login / refresh token
|
||||
err = d.refreshToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
}
|
||||
return nil, errors.New(e.Error)
|
||||
}
|
||||
return res.Body(), nil
|
||||
|
@ -143,7 +143,7 @@ func (d *QuarkOrUC) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
||||
_ = tempFile.Close()
|
||||
}()
|
||||
m := md5.New()
|
||||
_, err = io.Copy(m, tempFile)
|
||||
_, err = utils.CopyWithBuffer(m, tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -153,7 +153,7 @@ func (d *QuarkOrUC) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
||||
}
|
||||
md5Str := hex.EncodeToString(m.Sum(nil))
|
||||
s := sha1.New()
|
||||
_, err = io.Copy(s, tempFile)
|
||||
_, err = utils.CopyWithBuffer(s, tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
63
drivers/s3/doge.go
Normal file
63
drivers/s3/doge.go
Normal file
@ -0,0 +1,63 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TmpTokenResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data TmpTokenResponseData `json:"data,omitempty"`
|
||||
}
|
||||
type TmpTokenResponseData struct {
|
||||
Credentials Credentials `json:"Credentials"`
|
||||
ExpiredAt int `json:"ExpiredAt"`
|
||||
}
|
||||
type Credentials struct {
|
||||
AccessKeyId string `json:"accessKeyId,omitempty"`
|
||||
SecretAccessKey string `json:"secretAccessKey,omitempty"`
|
||||
SessionToken string `json:"sessionToken,omitempty"`
|
||||
}
|
||||
|
||||
func getCredentials(AccessKey, SecretKey string) (rst Credentials, err error) {
|
||||
apiPath := "/auth/tmp_token.json"
|
||||
reqBody, err := json.Marshal(map[string]interface{}{"channel": "OSS_FULL", "scopes": []string{"*"}})
|
||||
if err != nil {
|
||||
return rst, err
|
||||
}
|
||||
|
||||
signStr := apiPath + "\n" + string(reqBody)
|
||||
hmacObj := hmac.New(sha1.New, []byte(SecretKey))
|
||||
hmacObj.Write([]byte(signStr))
|
||||
sign := hex.EncodeToString(hmacObj.Sum(nil))
|
||||
Authorization := "TOKEN " + AccessKey + ":" + sign
|
||||
|
||||
req, err := http.NewRequest("POST", "https://api.dogecloud.com"+apiPath, strings.NewReader(string(reqBody)))
|
||||
if err != nil {
|
||||
return rst, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("Authorization", Authorization)
|
||||
client := http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return rst, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
ret, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return rst, err
|
||||
}
|
||||
var tmpTokenResp TmpTokenResponse
|
||||
err = json.Unmarshal(ret, &tmpTokenResp)
|
||||
if err != nil {
|
||||
return rst, err
|
||||
}
|
||||
return tmpTokenResp.Data.Credentials, nil
|
||||
}
|
@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/pkg/cron"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
@ -26,10 +27,13 @@ type S3 struct {
|
||||
Session *session.Session
|
||||
client *s3.S3
|
||||
linkClient *s3.S3
|
||||
|
||||
config driver.Config
|
||||
cron *cron.Cron
|
||||
}
|
||||
|
||||
func (d *S3) Config() driver.Config {
|
||||
return config
|
||||
return d.config
|
||||
}
|
||||
|
||||
func (d *S3) GetAddition() driver.Additional {
|
||||
@ -40,6 +44,18 @@ func (d *S3) Init(ctx context.Context) error {
|
||||
if d.Region == "" {
|
||||
d.Region = "alist"
|
||||
}
|
||||
if d.config.Name == "Doge" {
|
||||
// 多吉云每次临时生成的秘钥有效期为 2h,所以这里设置为 118 分钟重新生成一次
|
||||
d.cron = cron.NewCron(time.Minute * 118)
|
||||
d.cron.Do(func() {
|
||||
err := d.initSession()
|
||||
if err != nil {
|
||||
log.Errorln("Doge init session error:", err)
|
||||
}
|
||||
d.client = d.getClient(false)
|
||||
d.linkClient = d.getClient(true)
|
||||
})
|
||||
}
|
||||
err := d.initSession()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -50,6 +66,9 @@ func (d *S3) Init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *S3) Drop(ctx context.Context) error {
|
||||
if d.cron != nil {
|
||||
d.cron.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -22,15 +22,25 @@ type Addition struct {
|
||||
AddFilenameToDisposition bool `json:"add_filename_to_disposition" help:"Add filename to Content-Disposition header."`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "S3",
|
||||
DefaultRoot: "/",
|
||||
LocalSort: true,
|
||||
CheckStatus: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &S3{}
|
||||
return &S3{
|
||||
config: driver.Config{
|
||||
Name: "S3",
|
||||
DefaultRoot: "/",
|
||||
LocalSort: true,
|
||||
CheckStatus: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &S3{
|
||||
config: driver.Config{
|
||||
Name: "Doge",
|
||||
DefaultRoot: "/",
|
||||
LocalSort: true,
|
||||
CheckStatus: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -21,13 +21,21 @@ import (
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *S3) initSession() error {
|
||||
var err error
|
||||
accessKeyID, secretAccessKey, sessionToken := d.AccessKeyID, d.SecretAccessKey, d.SessionToken
|
||||
if d.config.Name == "Doge" {
|
||||
credentialsTmp, err := getCredentials(d.AccessKeyID, d.SecretAccessKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessKeyID, secretAccessKey, sessionToken = credentialsTmp.AccessKeyId, credentialsTmp.SecretAccessKey, credentialsTmp.SessionToken
|
||||
}
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(d.AccessKeyID, d.SecretAccessKey, d.SessionToken),
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyID, secretAccessKey, sessionToken),
|
||||
Region: &d.Region,
|
||||
Endpoint: &d.Endpoint,
|
||||
S3ForcePathStyle: aws.Bool(d.ForcePathStyle),
|
||||
}
|
||||
var err error
|
||||
d.Session, err = session.NewSession(cfg)
|
||||
return err
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -19,6 +18,7 @@ type Seafile struct {
|
||||
Addition
|
||||
|
||||
authorization string
|
||||
libraryMap map[string]*LibraryInfo
|
||||
}
|
||||
|
||||
func (d *Seafile) Config() driver.Config {
|
||||
@ -31,6 +31,8 @@ func (d *Seafile) GetAddition() driver.Additional {
|
||||
|
||||
func (d *Seafile) Init(ctx context.Context) error {
|
||||
d.Address = strings.TrimSuffix(d.Address, "/")
|
||||
d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)
|
||||
d.libraryMap = make(map[string]*LibraryInfo)
|
||||
return d.getToken()
|
||||
}
|
||||
|
||||
@ -38,10 +40,37 @@ func (d *Seafile) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) (result []model.Obj, err error) {
|
||||
path := dir.GetPath()
|
||||
if path == d.RootFolderPath {
|
||||
libraries, err := d.listLibraries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if path == "/" && d.RepoId == "" {
|
||||
return utils.SliceConvert(libraries, func(f LibraryItemResp) (model.Obj, error) {
|
||||
return &model.Object{
|
||||
Name: f.Name,
|
||||
Modified: time.Unix(f.Modified, 0),
|
||||
Size: f.Size,
|
||||
IsFolder: true,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
var repo *LibraryInfo
|
||||
repo, path, err = d.getRepoAndPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if repo.Encrypted {
|
||||
err = d.decryptLibrary(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var resp []RepoDirItemResp
|
||||
_, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
_, err = d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", repo.Id), func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetQueryParams(map[string]string{
|
||||
"p": path,
|
||||
})
|
||||
@ -63,9 +92,13 @@ func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
}
|
||||
|
||||
func (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(file.GetPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": file.GetPath(),
|
||||
"p": path,
|
||||
"reuse": "1",
|
||||
})
|
||||
})
|
||||
@ -78,9 +111,14 @@ func (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
}
|
||||
|
||||
func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(parentDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, _ = utils.JoinBasePath(path, dirName)
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/dir/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": filepath.Join(parentDir.GetPath(), dirName),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "mkdir",
|
||||
})
|
||||
@ -89,22 +127,34 @@ func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
|
||||
}
|
||||
|
||||
func (d *Seafile) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstRepo, dstPath, err := d.getRepoAndPath(dstDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": srcObj.GetPath(),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "move",
|
||||
"dst_repo": d.Addition.RepoId,
|
||||
"dst_dir": dstDir.GetPath(),
|
||||
"dst_repo": dstRepo.Id,
|
||||
"dst_dir": dstPath,
|
||||
})
|
||||
}, true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": srcObj.GetPath(),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "rename",
|
||||
"newname": newName,
|
||||
@ -114,31 +164,47 @@ func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string)
|
||||
}
|
||||
|
||||
func (d *Seafile) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstRepo, dstPath, err := d.getRepoAndPath(dstDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": srcObj.GetPath(),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "copy",
|
||||
"dst_repo": d.Addition.RepoId,
|
||||
"dst_dir": dstDir.GetPath(),
|
||||
"dst_repo": dstRepo.Id,
|
||||
"dst_dir": dstPath,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Seafile) Remove(ctx context.Context, obj model.Obj) error {
|
||||
_, err := d.request(http.MethodDelete, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(obj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodDelete, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": obj.GetPath(),
|
||||
"p": path,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/upload-link/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
repo, path, err := d.getRepoAndPath(dstDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/upload-link/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": dstDir.GetPath(),
|
||||
"p": path,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
@ -150,7 +216,7 @@ func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
||||
_, err = d.request(http.MethodPost, u, func(req *resty.Request) {
|
||||
req.SetFileReader("file", stream.GetName(), stream).
|
||||
SetFormData(map[string]string{
|
||||
"parent_dir": dstDir.GetPath(),
|
||||
"parent_dir": path,
|
||||
"replace": "1",
|
||||
})
|
||||
})
|
||||
|
@ -9,9 +9,11 @@ type Addition struct {
|
||||
driver.RootPath
|
||||
|
||||
Address string `json:"address" required:"true"`
|
||||
UserName string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
RepoId string `json:"repoId" required:"true"`
|
||||
UserName string `json:"username" required:"false"`
|
||||
Password string `json:"password" required:"false"`
|
||||
Token string `json:"token" required:"false"`
|
||||
RepoId string `json:"repoId" required:"false"`
|
||||
RepoPwd string `json:"repoPwd" required:"false"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -1,14 +1,44 @@
|
||||
package seafile
|
||||
|
||||
import "time"
|
||||
|
||||
type AuthTokenResp struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type RepoDirItemResp struct {
|
||||
type RepoItemResp struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"` // dir, file
|
||||
Type string `json:"type"` // repo, dir, file
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Modified int64 `json:"mtime"`
|
||||
Permission string `json:"permission"`
|
||||
}
|
||||
|
||||
type LibraryItemResp struct {
|
||||
RepoItemResp
|
||||
OwnerContactEmail string `json:"owner_contact_email"`
|
||||
OwnerName string `json:"owner_name"`
|
||||
Owner string `json:"owner"`
|
||||
ModifierEmail string `json:"modifier_email"`
|
||||
ModifierContactEmail string `json:"modifier_contact_email"`
|
||||
ModifierName string `json:"modifier_name"`
|
||||
Virtual bool `json:"virtual"`
|
||||
MtimeRelative string `json:"mtime_relative"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
Version int `json:"version"`
|
||||
HeadCommitId string `json:"head_commit_id"`
|
||||
Root string `json:"root"`
|
||||
Salt string `json:"salt"`
|
||||
SizeFormatted string `json:"size_formatted"`
|
||||
}
|
||||
|
||||
type RepoDirItemResp struct {
|
||||
RepoItemResp
|
||||
}
|
||||
|
||||
type LibraryInfo struct {
|
||||
LibraryItemResp
|
||||
decryptedTime time.Time
|
||||
decryptedSuccess bool
|
||||
}
|
@ -1,14 +1,23 @@
|
||||
package seafile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func (d *Seafile) getToken() error {
|
||||
if d.Token != "" {
|
||||
d.authorization = fmt.Sprintf("Token %s", d.Token)
|
||||
return nil
|
||||
}
|
||||
var authResp AuthTokenResp
|
||||
res, err := base.RestyClient.R().
|
||||
SetResult(&authResp).
|
||||
@ -60,3 +69,110 @@ func (d *Seafile) request(method string, pathname string, callback base.ReqCallb
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *Seafile) getRepoAndPath(fullPath string) (repo *LibraryInfo, path string, err error) {
|
||||
libraryMap := d.libraryMap
|
||||
repoId := d.Addition.RepoId
|
||||
if repoId != "" {
|
||||
if len(repoId) == 36 /* uuid */ {
|
||||
for _, library := range libraryMap {
|
||||
if library.Id == repoId {
|
||||
return library, fullPath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var repoName string
|
||||
str := fullPath[1:]
|
||||
pos := strings.IndexRune(str, '/')
|
||||
if pos == -1 {
|
||||
repoName = str
|
||||
} else {
|
||||
repoName = str[:pos]
|
||||
}
|
||||
path = utils.FixAndCleanPath(fullPath[1+len(repoName):])
|
||||
if library, ok := libraryMap[repoName]; ok {
|
||||
return library, path, nil
|
||||
}
|
||||
}
|
||||
return nil, "", errs.ObjectNotFound
|
||||
}
|
||||
|
||||
func (d *Seafile) listLibraries() (resp []LibraryItemResp, err error) {
|
||||
repoId := d.Addition.RepoId
|
||||
if repoId == "" {
|
||||
_, err = d.request(http.MethodGet, "/api2/repos/", func(req *resty.Request) {
|
||||
req.SetResult(&resp)
|
||||
})
|
||||
} else {
|
||||
var oneResp LibraryItemResp
|
||||
_, err = d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/", repoId), func(req *resty.Request) {
|
||||
req.SetResult(&oneResp)
|
||||
})
|
||||
if err == nil {
|
||||
resp = append(resp, oneResp)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
libraryMap := make(map[string]*LibraryInfo)
|
||||
var putLibraryMap func(library LibraryItemResp, index int)
|
||||
putLibraryMap = func(library LibraryItemResp, index int) {
|
||||
name := library.Name
|
||||
if index > 0 {
|
||||
name = fmt.Sprintf("%s (%d)", name, index)
|
||||
}
|
||||
if _, exist := libraryMap[name]; exist {
|
||||
putLibraryMap(library, index+1)
|
||||
} else {
|
||||
libraryInfo := LibraryInfo{}
|
||||
data, _ := utils.Json.Marshal(library)
|
||||
_ = utils.Json.Unmarshal(data, &libraryInfo)
|
||||
libraryMap[name] = &libraryInfo
|
||||
}
|
||||
}
|
||||
for _, library := range resp {
|
||||
putLibraryMap(library, 0)
|
||||
}
|
||||
d.libraryMap = libraryMap
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
var repoPwdNotConfigured = errors.New("library password not configured")
|
||||
var repoPwdIncorrect = errors.New("library password is incorrect")
|
||||
|
||||
func (d *Seafile) decryptLibrary(repo *LibraryInfo) (err error) {
|
||||
if !repo.Encrypted {
|
||||
return nil
|
||||
}
|
||||
if d.RepoPwd == "" {
|
||||
return repoPwdNotConfigured
|
||||
}
|
||||
now := time.Now()
|
||||
decryptedTime := repo.decryptedTime
|
||||
if repo.decryptedSuccess {
|
||||
if now.Sub(decryptedTime).Minutes() <= 30 {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if now.Sub(decryptedTime).Seconds() <= 10 {
|
||||
return repoPwdIncorrect
|
||||
}
|
||||
}
|
||||
var resp string
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/", repo.Id), func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetFormData(map[string]string{
|
||||
"password": d.RepoPwd,
|
||||
})
|
||||
})
|
||||
repo.decryptedTime = time.Now()
|
||||
if err != nil || !strings.Contains(resp, "success") {
|
||||
repo.decryptedSuccess = false
|
||||
return err
|
||||
}
|
||||
repo.decryptedSuccess = true
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
@ -16,7 +16,8 @@ import (
|
||||
type SFTP struct {
|
||||
model.Storage
|
||||
Addition
|
||||
client *sftp.Client
|
||||
client *sftp.Client
|
||||
clientConnectionError error
|
||||
}
|
||||
|
||||
func (d *SFTP) Config() driver.Config {
|
||||
@ -39,6 +40,9 @@ func (d *SFTP) Drop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *SFTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("[sftp] list dir: %s", dir.GetPath())
|
||||
files, err := d.client.ReadDir(dir.GetPath())
|
||||
if err != nil {
|
||||
@ -51,6 +55,9 @@ func (d *SFTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
|
||||
}
|
||||
|
||||
func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteFile, err := d.client.Open(file.GetPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -62,14 +69,23 @@ func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
|
||||
}
|
||||
|
||||
func (d *SFTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.client.MkdirAll(path.Join(parentDir.GetPath(), dirName))
|
||||
}
|
||||
|
||||
func (d *SFTP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.client.Rename(srcObj.GetPath(), path.Join(dstDir.GetPath(), srcObj.GetName()))
|
||||
}
|
||||
|
||||
func (d *SFTP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.client.Rename(srcObj.GetPath(), path.Join(path.Dir(srcObj.GetPath()), newName))
|
||||
}
|
||||
|
||||
@ -78,10 +94,16 @@ func (d *SFTP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
}
|
||||
|
||||
func (d *SFTP) Remove(ctx context.Context, obj model.Obj) error {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.remove(obj.GetPath())
|
||||
}
|
||||
|
||||
func (d *SFTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
||||
return err
|
||||
}
|
||||
dstFile, err := d.client.Create(path.Join(dstDir.GetPath(), stream.GetName()))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@ -30,6 +31,23 @@ func (d *SFTP) initClient() error {
|
||||
return err
|
||||
}
|
||||
d.client, err = sftp.NewClient(conn)
|
||||
if err == nil {
|
||||
d.clientConnectionError = nil
|
||||
go func(d *SFTP) {
|
||||
d.clientConnectionError = d.client.Wait()
|
||||
}(d)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *SFTP) clientReconnectOnConnectionError() error {
|
||||
err := d.clientConnectionError
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
log.Debugf("[sftp] discarding closed sftp connection: %v", err)
|
||||
_ = d.client.Close()
|
||||
err = d.initClient()
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"io"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
@ -74,7 +74,7 @@ func (d *SMB) CopyFile(src, dst string) error {
|
||||
}
|
||||
defer dstfd.Close()
|
||||
|
||||
if _, err = io.Copy(dstfd, srcfd); err != nil {
|
||||
if _, err = utils.CopyWithBuffer(dstfd, srcfd); err != nil {
|
||||
return err
|
||||
}
|
||||
if srcinfo, err = d.fs.Stat(src); err != nil {
|
||||
|
@ -190,7 +190,7 @@ func getGcid(r io.Reader, size int64) (string, error) {
|
||||
readSize := calcBlockSize(size)
|
||||
for {
|
||||
hash2.Reset()
|
||||
if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
|
||||
if n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {
|
||||
if err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
|
527
drivers/thunderx/driver.go
Normal file
527
drivers/thunderx/driver.go
Normal file
@ -0,0 +1,527 @@
|
||||
package thunderx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"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/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
)
|
||||
|
||||
type ThunderX struct {
|
||||
*XunLeiXCommon
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
identity string
|
||||
}
|
||||
|
||||
func (x *ThunderX) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (x *ThunderX) GetAddition() driver.Additional {
|
||||
return &x.Addition
|
||||
}
|
||||
|
||||
func (x *ThunderX) Init(ctx context.Context) (err error) {
|
||||
// 初始化所需参数
|
||||
if x.XunLeiXCommon == nil {
|
||||
x.XunLeiXCommon = &XunLeiXCommon{
|
||||
Common: &Common{
|
||||
client: base.NewRestyClient(),
|
||||
Algorithms: []string{
|
||||
"lHwINjLeqssT28Ym99p5MvR",
|
||||
"xvFcxvtqPKCa9Ajf",
|
||||
"2ywOP8spKHzfuhZMUYZ9IpsViq0t8vT0",
|
||||
"FTBrJism20SHKQ2m2",
|
||||
"BHrWJsPwjnr5VeLtOUr2191X9uXhWmt",
|
||||
"yu0QgHEjNmDoPNwXN17so2hQlDT83T",
|
||||
"OcaMfLMCGZ7oYlvZGIbTqb4U7cCY",
|
||||
"jBGGu0GzXOjtCXYwkOBb+c6TZ/Nymv",
|
||||
"YLWRjVor2rOuYEL",
|
||||
"94wjoPazejyNC+gRpOj+JOm1XXvxa",
|
||||
},
|
||||
DeviceID: utils.GetMD5EncodeStr(x.Username + x.Password),
|
||||
ClientID: "ZQL_zwA4qhHcoe_2",
|
||||
ClientSecret: "Og9Vr1L8Ee6bh0olFxFDRg",
|
||||
ClientVersion: "1.05.0.2115",
|
||||
PackageName: "com.thunder.downloader",
|
||||
UserAgent: "ANDROID-com.thunder.downloader/1.05.0.2115 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",
|
||||
DownloadUserAgent: "Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",
|
||||
UseVideoUrl: x.UseVideoUrl,
|
||||
|
||||
refreshCTokenCk: func(token string) {
|
||||
x.CaptchaToken = token
|
||||
op.MustSaveDriverStorage(x)
|
||||
},
|
||||
},
|
||||
refreshTokenFunc: func() error {
|
||||
// 通过RefreshToken刷新
|
||||
token, err := x.RefreshToken(x.TokenResp.RefreshToken)
|
||||
if err != nil {
|
||||
// 重新登录
|
||||
token, err = x.Login(x.Username, x.Password)
|
||||
if err != nil {
|
||||
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义验证码token
|
||||
ctoekn := strings.TrimSpace(x.CaptchaToken)
|
||||
if ctoekn != "" {
|
||||
x.SetCaptchaToken(ctoekn)
|
||||
}
|
||||
x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.Addition.RootFolderID = x.RootFolderID
|
||||
// 防止重复登录
|
||||
identity := x.GetIdentity()
|
||||
if x.identity != identity || !x.IsLogin() {
|
||||
x.identity = identity
|
||||
// 登录
|
||||
token, err := x.Login(x.Username, x.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderX) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ThunderXExpert struct {
|
||||
*XunLeiXCommon
|
||||
model.Storage
|
||||
ExpertAddition
|
||||
|
||||
identity string
|
||||
}
|
||||
|
||||
func (x *ThunderXExpert) Config() driver.Config {
|
||||
return configExpert
|
||||
}
|
||||
|
||||
func (x *ThunderXExpert) GetAddition() driver.Additional {
|
||||
return &x.ExpertAddition
|
||||
}
|
||||
|
||||
func (x *ThunderXExpert) Init(ctx context.Context) (err error) {
|
||||
// 防止重复登录
|
||||
identity := x.GetIdentity()
|
||||
if identity != x.identity || !x.IsLogin() {
|
||||
x.identity = identity
|
||||
x.XunLeiXCommon = &XunLeiXCommon{
|
||||
Common: &Common{
|
||||
client: base.NewRestyClient(),
|
||||
|
||||
DeviceID: func() string {
|
||||
if len(x.DeviceID) != 32 {
|
||||
return utils.GetMD5EncodeStr(x.DeviceID)
|
||||
}
|
||||
return x.DeviceID
|
||||
}(),
|
||||
ClientID: x.ClientID,
|
||||
ClientSecret: x.ClientSecret,
|
||||
ClientVersion: x.ClientVersion,
|
||||
PackageName: x.PackageName,
|
||||
UserAgent: x.UserAgent,
|
||||
DownloadUserAgent: x.DownloadUserAgent,
|
||||
UseVideoUrl: x.UseVideoUrl,
|
||||
|
||||
refreshCTokenCk: func(token string) {
|
||||
x.CaptchaToken = token
|
||||
op.MustSaveDriverStorage(x)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if x.CaptchaToken != "" {
|
||||
x.SetCaptchaToken(x.CaptchaToken)
|
||||
}
|
||||
x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.ExpertAddition.RootFolderID = x.RootFolderID
|
||||
// 签名方法
|
||||
if x.SignType == "captcha_sign" {
|
||||
x.Common.Timestamp = x.Timestamp
|
||||
x.Common.CaptchaSign = x.CaptchaSign
|
||||
} else {
|
||||
x.Common.Algorithms = strings.Split(x.Algorithms, ",")
|
||||
}
|
||||
|
||||
// 登录方式
|
||||
if x.LoginType == "refresh_token" {
|
||||
// 通过RefreshToken登录
|
||||
token, err := x.XunLeiXCommon.RefreshToken(x.ExpertAddition.RefreshToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
|
||||
// 刷新token方法
|
||||
x.SetRefreshTokenFunc(func() error {
|
||||
token, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken)
|
||||
if err != nil {
|
||||
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
op.MustSaveDriverStorage(x)
|
||||
return err
|
||||
})
|
||||
} else {
|
||||
// 通过用户密码登录
|
||||
token, err := x.Login(x.Username, x.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
x.SetRefreshTokenFunc(func() error {
|
||||
token, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken)
|
||||
if err != nil {
|
||||
token, err = x.Login(x.Username, x.Password)
|
||||
if err != nil {
|
||||
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||
}
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
op.MustSaveDriverStorage(x)
|
||||
return err
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 仅修改验证码token
|
||||
if x.CaptchaToken != "" {
|
||||
x.SetCaptchaToken(x.CaptchaToken)
|
||||
}
|
||||
x.XunLeiXCommon.UserAgent = x.UserAgent
|
||||
x.XunLeiXCommon.DownloadUserAgent = x.DownloadUserAgent
|
||||
x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.ExpertAddition.RootFolderID = x.RootFolderID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderXExpert) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderXExpert) SetTokenResp(token *TokenResp) {
|
||||
x.XunLeiXCommon.SetTokenResp(token)
|
||||
if token != nil {
|
||||
x.ExpertAddition.RefreshToken = token.RefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
type XunLeiXCommon struct {
|
||||
*Common
|
||||
*TokenResp // 登录信息
|
||||
|
||||
refreshTokenFunc func() error
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
return xc.getFiles(ctx, dir.GetID())
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var lFile Files
|
||||
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetPathParam("fileID", file.GetID())
|
||||
//r.SetQueryParam("space", "")
|
||||
}, &lFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link := &model.Link{
|
||||
URL: lFile.WebContentLink,
|
||||
Header: http.Header{
|
||||
"User-Agent": {xc.DownloadUserAgent},
|
||||
},
|
||||
}
|
||||
|
||||
if xc.UseVideoUrl {
|
||||
for _, media := range lFile.Medias {
|
||||
if media.Link.URL != "" {
|
||||
link.URL = media.Link.URL
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
strs := regexp.MustCompile(`e=([0-9]*)`).FindStringSubmatch(lFile.WebContentLink)
|
||||
if len(strs) == 2 {
|
||||
timestamp, err := strconv.ParseInt(strs[1], 10, 64)
|
||||
if err == nil {
|
||||
expired := time.Duration(timestamp-time.Now().Unix()) * time.Second
|
||||
link.Expiration = &expired
|
||||
}
|
||||
}
|
||||
*/
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&base.Json{
|
||||
"kind": FOLDER,
|
||||
"name": dirName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
})
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
_, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID()},
|
||||
"ids": []string{srcObj.GetID()},
|
||||
})
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodPatch, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetPathParam("fileID", srcObj.GetID())
|
||||
r.SetBody(&base.Json{"name": newName})
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
_, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID()},
|
||||
"ids": []string{srcObj.GetID()},
|
||||
})
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) Remove(ctx context.Context, obj model.Obj) error {
|
||||
_, err := xc.Request(FILE_API_URL+"/{fileID}/trash", http.MethodPatch, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetPathParam("fileID", obj.GetID())
|
||||
r.SetBody("{}")
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
hi := stream.GetHash()
|
||||
gcid := hi.GetHash(hash_extend.GCID)
|
||||
if len(gcid) < hash_extend.GCID.Width {
|
||||
tFile, err := stream.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gcid, err = utils.HashFile(hash_extend.GCID, tFile, stream.GetSize())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var resp UploadTaskResponse
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&base.Json{
|
||||
"kind": FILE,
|
||||
"parent_id": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"size": stream.GetSize(),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
param := resp.Resumable.Params
|
||||
if resp.UploadType == UPLOAD_TYPE_RESUMABLE {
|
||||
param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".")
|
||||
s, err := session.NewSession(&aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),
|
||||
Region: aws.String("xunlei"),
|
||||
Endpoint: aws.String(param.Endpoint),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
||||
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
|
||||
}
|
||||
_, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{
|
||||
Bucket: aws.String(param.Bucket),
|
||||
Key: aws.String(param.Key),
|
||||
Expires: aws.Time(param.Expiration),
|
||||
Body: stream,
|
||||
})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) getFiles(ctx context.Context, folderId string) ([]model.Obj, error) {
|
||||
files := make([]model.Obj, 0)
|
||||
var pageToken string
|
||||
for {
|
||||
var fileList FileList
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetQueryParams(map[string]string{
|
||||
"space": "",
|
||||
"__type": "drive",
|
||||
"refresh": "true",
|
||||
"__sync": "true",
|
||||
"parent_id": folderId,
|
||||
"page_token": pageToken,
|
||||
"with_audit": "true",
|
||||
"limit": "100",
|
||||
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
|
||||
})
|
||||
}, &fileList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := 0; i < len(fileList.Files); i++ {
|
||||
files = append(files, &fileList.Files[i])
|
||||
}
|
||||
|
||||
if fileList.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = fileList.NextPageToken
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// 设置刷新Token的方法
|
||||
func (xc *XunLeiXCommon) SetRefreshTokenFunc(fn func() error) {
|
||||
xc.refreshTokenFunc = fn
|
||||
}
|
||||
|
||||
// 设置Token
|
||||
func (xc *XunLeiXCommon) SetTokenResp(tr *TokenResp) {
|
||||
xc.TokenResp = tr
|
||||
}
|
||||
|
||||
// 携带Authorization和CaptchaToken的请求
|
||||
func (xc *XunLeiXCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
data, err := xc.Common.Request(url, method, func(req *resty.Request) {
|
||||
req.SetHeaders(map[string]string{
|
||||
"Authorization": xc.Token(),
|
||||
"X-Captcha-Token": xc.GetCaptchaToken(),
|
||||
})
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
}, resp)
|
||||
|
||||
errResp, ok := err.(*ErrResp)
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch errResp.ErrorCode {
|
||||
case 0:
|
||||
return data, nil
|
||||
case 4122, 4121, 10, 16:
|
||||
if xc.refreshTokenFunc != nil {
|
||||
if err = xc.refreshTokenFunc(); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
case 9: // 验证码token过期
|
||||
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
return xc.Request(url, method, callback, resp)
|
||||
}
|
||||
|
||||
// 刷新Token
|
||||
func (xc *XunLeiXCommon) RefreshToken(refreshToken string) (*TokenResp, error) {
|
||||
var resp TokenResp
|
||||
_, err := xc.Common.Request(XLUSER_API_URL+"/auth/token", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(&base.Json{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refreshToken,
|
||||
"client_id": xc.ClientID,
|
||||
"client_secret": xc.ClientSecret,
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.RefreshToken == "" {
|
||||
return nil, errs.EmptyToken
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// 登录
|
||||
func (xc *XunLeiXCommon) Login(username, password string) (*TokenResp, error) {
|
||||
url := XLUSER_API_URL + "/auth/signin"
|
||||
err := xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp TokenResp
|
||||
_, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(&SignInRequest{
|
||||
CaptchaToken: xc.GetCaptchaToken(),
|
||||
ClientID: xc.ClientID,
|
||||
ClientSecret: xc.ClientSecret,
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (xc *XunLeiXCommon) IsLogin() bool {
|
||||
if xc.TokenResp == nil {
|
||||
return false
|
||||
}
|
||||
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
|
||||
return err == nil
|
||||
}
|
103
drivers/thunderx/meta.go
Normal file
103
drivers/thunderx/meta.go
Normal file
@ -0,0 +1,103 @@
|
||||
package thunderx
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
// 高级设置
|
||||
type ExpertAddition struct {
|
||||
driver.RootID
|
||||
|
||||
LoginType string `json:"login_type" type:"select" options:"user,refresh_token" default:"user"`
|
||||
SignType string `json:"sign_type" type:"select" options:"algorithms,captcha_sign" default:"algorithms"`
|
||||
|
||||
// 登录方式1
|
||||
Username string `json:"username" required:"true" help:"login type is user,this is required"`
|
||||
Password string `json:"password" required:"true" help:"login type is user,this is required"`
|
||||
// 登录方式2
|
||||
RefreshToken string `json:"refresh_token" required:"true" help:"login type is refresh_token,this is required"`
|
||||
|
||||
// 签名方法1
|
||||
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"lHwINjLeqssT28Ym99p5MvR,xvFcxvtqPKCa9Ajf,2ywOP8spKHzfuhZMUYZ9IpsViq0t8vT0,FTBrJism20SHKQ2m2,BHrWJsPwjnr5VeLtOUr2191X9uXhWmt,yu0QgHEjNmDoPNwXN17so2hQlDT83T,OcaMfLMCGZ7oYlvZGIbTqb4U7cCY,jBGGu0GzXOjtCXYwkOBb+c6TZ/Nymv,YLWRjVor2rOuYEL,94wjoPazejyNC+gRpOj+JOm1XXvxa"`
|
||||
// 签名方法2
|
||||
CaptchaSign string `json:"captcha_sign" required:"true" help:"sign type is captcha_sign,this is required"`
|
||||
Timestamp string `json:"timestamp" required:"true" help:"sign type is captcha_sign,this is required"`
|
||||
|
||||
// 验证码
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
|
||||
// 必要且影响登录,由签名决定
|
||||
DeviceID string `json:"device_id" required:"true" default:"9aa5c268e7bcfc197a9ad88e2fb330e5"`
|
||||
ClientID string `json:"client_id" required:"true" default:"ZQL_zwA4qhHcoe_2"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"Og9Vr1L8Ee6bh0olFxFDRg"`
|
||||
ClientVersion string `json:"client_version" required:"true" default:"1.05.0.2115"`
|
||||
PackageName string `json:"package_name" required:"true" default:"com.thunder.downloader"`
|
||||
|
||||
//不影响登录,影响下载速度
|
||||
UserAgent string `json:"user_agent" required:"true" default:"ANDROID-com.thunder.downloader/1.05.0.2115 netWorkType/4G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gdcf98eab238b) (JAVA 0)"`
|
||||
DownloadUserAgent string `json:"download_user_agent" required:"true" default:"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)"`
|
||||
|
||||
//优先使用视频链接代替下载链接
|
||||
UseVideoUrl bool `json:"use_video_url"`
|
||||
}
|
||||
|
||||
// 登录特征,用于判断是否重新登录
|
||||
func (i *ExpertAddition) GetIdentity() string {
|
||||
hash := md5.New()
|
||||
if i.LoginType == "refresh_token" {
|
||||
hash.Write([]byte(i.RefreshToken))
|
||||
} else {
|
||||
hash.Write([]byte(i.Username + i.Password))
|
||||
}
|
||||
|
||||
if i.SignType == "captcha_sign" {
|
||||
hash.Write([]byte(i.CaptchaSign + i.Timestamp))
|
||||
} else {
|
||||
hash.Write([]byte(i.Algorithms))
|
||||
}
|
||||
|
||||
hash.Write([]byte(i.DeviceID))
|
||||
hash.Write([]byte(i.ClientID))
|
||||
hash.Write([]byte(i.ClientSecret))
|
||||
hash.Write([]byte(i.ClientVersion))
|
||||
hash.Write([]byte(i.PackageName))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
UseVideoUrl bool `json:"use_video_url" default:"true"`
|
||||
}
|
||||
|
||||
// 登录特征,用于判断是否重新登录
|
||||
func (i *Addition) GetIdentity() string {
|
||||
return utils.GetMD5EncodeStr(i.Username + i.Password)
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "ThunderX",
|
||||
LocalSort: true,
|
||||
OnlyProxy: true,
|
||||
}
|
||||
|
||||
var configExpert = driver.Config{
|
||||
Name: "ThunderXExpert",
|
||||
LocalSort: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &ThunderX{}
|
||||
})
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &ThunderXExpert{}
|
||||
})
|
||||
}
|
206
drivers/thunderx/types.go
Normal file
206
drivers/thunderx/types.go
Normal file
@ -0,0 +1,206 @@
|
||||
package thunderx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
|
||||
)
|
||||
|
||||
type ErrResp struct {
|
||||
ErrorCode int64 `json:"error_code"`
|
||||
ErrorMsg string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
// ErrorDetails interface{} `json:"error_details"`
|
||||
}
|
||||
|
||||
func (e *ErrResp) IsError() bool {
|
||||
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
|
||||
}
|
||||
|
||||
func (e *ErrResp) Error() string {
|
||||
return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)
|
||||
}
|
||||
|
||||
/*
|
||||
* 验证码Token
|
||||
**/
|
||||
type CaptchaTokenRequest struct {
|
||||
Action string `json:"action"`
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
ClientID string `json:"client_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Meta map[string]string `json:"meta"`
|
||||
RedirectUri string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type CaptchaTokenResponse struct {
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
/*
|
||||
* 登录
|
||||
**/
|
||||
type TokenResp struct {
|
||||
TokenType string `json:"token_type"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
|
||||
Sub string `json:"sub"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
func (t *TokenResp) Token() string {
|
||||
return fmt.Sprint(t.TokenType, " ", t.AccessToken)
|
||||
}
|
||||
|
||||
type SignInRequest struct {
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
/*
|
||||
* 文件
|
||||
**/
|
||||
type FileList struct {
|
||||
Kind string `json:"kind"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
Files []Files `json:"files"`
|
||||
Version string `json:"version"`
|
||||
VersionOutdated bool `json:"version_outdated"`
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
Expire time.Time `json:"expire"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
var _ model.Obj = (*Files)(nil)
|
||||
|
||||
type Files struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
ParentID string `json:"parent_id"`
|
||||
Name string `json:"name"`
|
||||
//UserID string `json:"user_id"`
|
||||
Size string `json:"size"`
|
||||
//Revision string `json:"revision"`
|
||||
//FileExtension string `json:"file_extension"`
|
||||
//MimeType string `json:"mime_type"`
|
||||
//Starred bool `json:"starred"`
|
||||
WebContentLink string `json:"web_content_link"`
|
||||
CreatedTime time.Time `json:"created_time"`
|
||||
ModifiedTime time.Time `json:"modified_time"`
|
||||
IconLink string `json:"icon_link"`
|
||||
ThumbnailLink string `json:"thumbnail_link"`
|
||||
// Md5Checksum string `json:"md5_checksum"`
|
||||
Hash string `json:"hash"`
|
||||
// Links map[string]Link `json:"links"`
|
||||
// Phase string `json:"phase"`
|
||||
// Audit struct {
|
||||
// Status string `json:"status"`
|
||||
// Message string `json:"message"`
|
||||
// Title string `json:"title"`
|
||||
// } `json:"audit"`
|
||||
Medias []struct {
|
||||
//Category string `json:"category"`
|
||||
//IconLink string `json:"icon_link"`
|
||||
//IsDefault bool `json:"is_default"`
|
||||
//IsOrigin bool `json:"is_origin"`
|
||||
//IsVisible bool `json:"is_visible"`
|
||||
Link Link `json:"link"`
|
||||
//MediaID string `json:"media_id"`
|
||||
//MediaName string `json:"media_name"`
|
||||
//NeedMoreQuota bool `json:"need_more_quota"`
|
||||
//Priority int `json:"priority"`
|
||||
//RedirectLink string `json:"redirect_link"`
|
||||
//ResolutionName string `json:"resolution_name"`
|
||||
// Video struct {
|
||||
// AudioCodec string `json:"audio_codec"`
|
||||
// BitRate int `json:"bit_rate"`
|
||||
// Duration int `json:"duration"`
|
||||
// FrameRate int `json:"frame_rate"`
|
||||
// Height int `json:"height"`
|
||||
// VideoCodec string `json:"video_codec"`
|
||||
// VideoType string `json:"video_type"`
|
||||
// Width int `json:"width"`
|
||||
// } `json:"video"`
|
||||
// VipTypes []string `json:"vip_types"`
|
||||
} `json:"medias"`
|
||||
Trashed bool `json:"trashed"`
|
||||
DeleteTime string `json:"delete_time"`
|
||||
OriginalURL string `json:"original_url"`
|
||||
//Params struct{} `json:"params"`
|
||||
//OriginalFileIndex int `json:"original_file_index"`
|
||||
//Space string `json:"space"`
|
||||
//Apps []interface{} `json:"apps"`
|
||||
//Writable bool `json:"writable"`
|
||||
//FolderType string `json:"folder_type"`
|
||||
//Collection interface{} `json:"collection"`
|
||||
}
|
||||
|
||||
func (c *Files) GetHash() utils.HashInfo {
|
||||
return utils.NewHashInfo(hash_extend.GCID, c.Hash)
|
||||
}
|
||||
|
||||
func (c *Files) GetSize() int64 { size, _ := strconv.ParseInt(c.Size, 10, 64); return size }
|
||||
func (c *Files) GetName() string { return c.Name }
|
||||
func (c *Files) CreateTime() time.Time { return c.CreatedTime }
|
||||
func (c *Files) ModTime() time.Time { return c.ModifiedTime }
|
||||
func (c *Files) IsDir() bool { return c.Kind == FOLDER }
|
||||
func (c *Files) GetID() string { return c.ID }
|
||||
func (c *Files) GetPath() string { return "" }
|
||||
func (c *Files) Thumb() string { return c.ThumbnailLink }
|
||||
|
||||
/*
|
||||
* 上传
|
||||
**/
|
||||
type UploadTaskResponse struct {
|
||||
UploadType string `json:"upload_type"`
|
||||
|
||||
/*//UPLOAD_TYPE_FORM
|
||||
Form struct {
|
||||
//Headers struct{} `json:"headers"`
|
||||
Kind string `json:"kind"`
|
||||
Method string `json:"method"`
|
||||
MultiParts struct {
|
||||
OSSAccessKeyID string `json:"OSSAccessKeyId"`
|
||||
Signature string `json:"Signature"`
|
||||
Callback string `json:"callback"`
|
||||
Key string `json:"key"`
|
||||
Policy string `json:"policy"`
|
||||
XUserData string `json:"x:user_data"`
|
||||
} `json:"multi_parts"`
|
||||
URL string `json:"url"`
|
||||
} `json:"form"`*/
|
||||
|
||||
//UPLOAD_TYPE_RESUMABLE
|
||||
Resumable struct {
|
||||
Kind string `json:"kind"`
|
||||
Params struct {
|
||||
AccessKeyID string `json:"access_key_id"`
|
||||
AccessKeySecret string `json:"access_key_secret"`
|
||||
Bucket string `json:"bucket"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
Key string `json:"key"`
|
||||
SecurityToken string `json:"security_token"`
|
||||
} `json:"params"`
|
||||
Provider string `json:"provider"`
|
||||
} `json:"resumable"`
|
||||
|
||||
File Files `json:"file"`
|
||||
}
|
202
drivers/thunderx/util.go
Normal file
202
drivers/thunderx/util.go
Normal file
@ -0,0 +1,202 @@
|
||||
package thunderx
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
API_URL = "https://api-pan.xunleix.com/drive/v1"
|
||||
FILE_API_URL = API_URL + "/files"
|
||||
XLUSER_API_URL = "https://xluser-ssl.xunleix.com/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
FOLDER = "drive#folder"
|
||||
FILE = "drive#file"
|
||||
RESUMABLE = "drive#resumable"
|
||||
)
|
||||
|
||||
const (
|
||||
UPLOAD_TYPE_UNKNOWN = "UPLOAD_TYPE_UNKNOWN"
|
||||
//UPLOAD_TYPE_FORM = "UPLOAD_TYPE_FORM"
|
||||
UPLOAD_TYPE_RESUMABLE = "UPLOAD_TYPE_RESUMABLE"
|
||||
UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL"
|
||||
)
|
||||
|
||||
func GetAction(method string, url string) string {
|
||||
urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(url)[1]
|
||||
return method + ":" + urlpath
|
||||
}
|
||||
|
||||
type Common struct {
|
||||
client *resty.Client
|
||||
|
||||
captchaToken string
|
||||
|
||||
// 签名相关,二选一
|
||||
Algorithms []string
|
||||
Timestamp, CaptchaSign string
|
||||
|
||||
// 必要值,签名相关
|
||||
DeviceID string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ClientVersion string
|
||||
PackageName string
|
||||
UserAgent string
|
||||
DownloadUserAgent string
|
||||
UseVideoUrl bool
|
||||
|
||||
// 验证码token刷新成功回调
|
||||
refreshCTokenCk func(token string)
|
||||
}
|
||||
|
||||
func (c *Common) SetCaptchaToken(captchaToken string) {
|
||||
c.captchaToken = captchaToken
|
||||
}
|
||||
func (c *Common) GetCaptchaToken() string {
|
||||
return c.captchaToken
|
||||
}
|
||||
|
||||
// 刷新验证码token(登录后)
|
||||
func (c *Common) RefreshCaptchaTokenAtLogin(action, userID string) error {
|
||||
metas := map[string]string{
|
||||
"client_version": c.ClientVersion,
|
||||
"package_name": c.PackageName,
|
||||
"user_id": userID,
|
||||
}
|
||||
metas["timestamp"], metas["captcha_sign"] = c.GetCaptchaSign()
|
||||
return c.refreshCaptchaToken(action, metas)
|
||||
}
|
||||
|
||||
// 刷新验证码token(登录时)
|
||||
func (c *Common) RefreshCaptchaTokenInLogin(action, username string) error {
|
||||
metas := make(map[string]string)
|
||||
if ok, _ := regexp.MatchString(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`, username); ok {
|
||||
metas["email"] = username
|
||||
} else if len(username) >= 11 && len(username) <= 18 {
|
||||
metas["phone_number"] = username
|
||||
} else {
|
||||
metas["username"] = username
|
||||
}
|
||||
return c.refreshCaptchaToken(action, metas)
|
||||
}
|
||||
|
||||
// 获取验证码签名
|
||||
func (c *Common) GetCaptchaSign() (timestamp, sign string) {
|
||||
if len(c.Algorithms) == 0 {
|
||||
return c.Timestamp, c.CaptchaSign
|
||||
}
|
||||
timestamp = fmt.Sprint(time.Now().UnixMilli())
|
||||
str := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)
|
||||
for _, algorithm := range c.Algorithms {
|
||||
str = utils.GetMD5EncodeStr(str + algorithm)
|
||||
}
|
||||
sign = "1." + str
|
||||
return
|
||||
}
|
||||
|
||||
// 刷新验证码token
|
||||
func (c *Common) refreshCaptchaToken(action string, metas map[string]string) error {
|
||||
param := CaptchaTokenRequest{
|
||||
Action: action,
|
||||
CaptchaToken: c.captchaToken,
|
||||
ClientID: c.ClientID,
|
||||
DeviceID: c.DeviceID,
|
||||
Meta: metas,
|
||||
RedirectUri: "xlaccsdk01://xbase.cloud/callback?state=harbor",
|
||||
}
|
||||
var e ErrResp
|
||||
var resp CaptchaTokenResponse
|
||||
_, err := c.Request(XLUSER_API_URL+"/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetError(&e).SetBody(param)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.IsError() {
|
||||
return &e
|
||||
}
|
||||
|
||||
if resp.Url != "" {
|
||||
return fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
|
||||
}
|
||||
|
||||
if resp.CaptchaToken == "" {
|
||||
return fmt.Errorf("empty captchaToken")
|
||||
}
|
||||
|
||||
if c.refreshCTokenCk != nil {
|
||||
c.refreshCTokenCk(resp.CaptchaToken)
|
||||
}
|
||||
c.SetCaptchaToken(resp.CaptchaToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 只有基础信息的请求
|
||||
func (c *Common) Request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := c.client.R().SetHeaders(map[string]string{
|
||||
"user-agent": c.UserAgent,
|
||||
"accept": "application/json;charset=UTF-8",
|
||||
"x-device-id": c.DeviceID,
|
||||
"x-client-id": c.ClientID,
|
||||
"x-client-version": c.ClientVersion,
|
||||
})
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
res, err := req.Execute(method, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var erron ErrResp
|
||||
utils.Json.Unmarshal(res.Body(), &erron)
|
||||
if erron.IsError() {
|
||||
return nil, &erron
|
||||
}
|
||||
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
// 计算文件Gcid
|
||||
func getGcid(r io.Reader, size int64) (string, error) {
|
||||
calcBlockSize := func(j int64) int64 {
|
||||
var psize int64 = 0x40000
|
||||
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
|
||||
psize = psize << 1
|
||||
}
|
||||
return psize
|
||||
}
|
||||
|
||||
hash1 := sha1.New()
|
||||
hash2 := sha1.New()
|
||||
readSize := calcBlockSize(size)
|
||||
for {
|
||||
hash2.Reset()
|
||||
if n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {
|
||||
if err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
}
|
||||
hash1.Write(hash2.Sum(nil))
|
||||
}
|
||||
return hex.EncodeToString(hash1.Sum(nil)), nil
|
||||
}
|
31
go.mod
31
go.mod
@ -7,6 +7,7 @@ require (
|
||||
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a
|
||||
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
|
||||
github.com/Xhofe/wopan-sdk-go v0.1.2
|
||||
github.com/alist-org/gofakes3 v0.0.4
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/avast/retry-go v3.0.0+incompatible
|
||||
github.com/aws/aws-sdk-go v1.50.24
|
||||
@ -17,11 +18,12 @@ require (
|
||||
github.com/charmbracelet/lipgloss v0.9.1
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/deckarep/golang-set/v2 v2.6.0
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/djherbis/times v1.6.0
|
||||
github.com/dlclark/regexp2 v1.10.0
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.5
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.6
|
||||
github.com/foxxorcat/weiyun-sdk-go v0.1.3
|
||||
github.com/gaoyb7/115drive-webdav v0.1.8
|
||||
github.com/gin-contrib/cors v1.5.0
|
||||
@ -35,10 +37,12 @@ require (
|
||||
github.com/ipfs/go-ipfs-api v0.7.0
|
||||
github.com/jlaffaye/ftp v0.2.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.2.5
|
||||
github.com/maruel/natural v1.1.1
|
||||
github.com/meilisearch/meilisearch-go v0.26.1
|
||||
github.com/minio/sio v0.3.0
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
github.com/ncw/swift/v2 v2.0.2
|
||||
github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
@ -47,15 +51,15 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7
|
||||
github.com/u2takey/ffmpeg-go v0.5.0
|
||||
github.com/upyun/go-sdk/v3 v3.0.4
|
||||
github.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5
|
||||
github.com/xhofe/tache v0.1.1
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e
|
||||
golang.org/x/crypto v0.19.0
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/net v0.20.0
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/oauth2 v0.16.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/appengine v1.6.8
|
||||
@ -125,6 +129,7 @@ require (
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/ipfs/boxo v0.12.0 // indirect
|
||||
github.com/ipfs/go-cid v0.4.1 // indirect
|
||||
@ -137,8 +142,8 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
@ -153,7 +158,7 @@ require (
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@ -172,7 +177,6 @@ require (
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-multistream v0.4.1 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/ncw/swift/v2 v2.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
@ -184,6 +188,8 @@ require (
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/rfjakob/eme v1.1.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.23.7 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
@ -201,10 +207,11 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
golang.org/x/arch v0.5.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/term v0.16.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/term v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
google.golang.org/api v0.134.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect
|
||||
google.golang.org/grpc v1.57.0 // indirect
|
||||
|
78
go.sum
78
go.sum
@ -23,6 +23,8 @@ github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0E
|
||||
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
|
||||
github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=
|
||||
github.com/aead/ecdh v0.2.0/go.mod h1:a9HHtXuSo8J1Js1MwLQx2mBhkXMT6YwUmVVEY4tTB8U=
|
||||
github.com/alist-org/gofakes3 v0.0.4 h1:/ID4+1llsiB8EweLcC65rVmgBZKL95e3P7Wa+aJGUiE=
|
||||
github.com/alist-org/gofakes3 v0.0.4/go.mod h1:bLPZXt45XYMgaoGGLe5t0d1p13oZTQTptTEDLrku070=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andreburgaud/crypt2go v1.2.0 h1:oly/ENAodeqTYpUafgd4r3v+VKLQnmOKUyfpj+TxHbE=
|
||||
@ -32,8 +34,6 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
|
||||
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.49.18 h1:g/iMXkfXeJQ7MvnLwroxWsTTNkHtdVJGxIgrAIEG62M=
|
||||
github.com/aws/aws-sdk-go v1.49.18/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aws/aws-sdk-go v1.50.24 h1:3o2Pg7mOoVL0jv54vWtuafoZqAeEXLhm1tltWA2GcEw=
|
||||
github.com/aws/aws-sdk-go v1.50.24/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
@ -123,6 +123,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
@ -131,8 +133,8 @@ github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJLNCEi7YHVMkwwtfSr2k9splgdSM=
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.5 h1:N3LqOvk2aWWxszsFIkArP5udIv74uTei/bH2jM3tfSc=
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.5/go.mod h1:iWHA2JFhzmKR28ySp1ON0g6DjLaYtvb5jhTqPVTDW9A=
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.6 h1:6J37oI4wMZLj8EPgSCcSTTIbnI5D6RCNW/srX8vQd1Y=
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.6/go.mod h1:UaY6D88yBXWGrcu/PcyLWyL4lzrk5pSxSABPHftOvxs=
|
||||
github.com/foxxorcat/weiyun-sdk-go v0.1.3 h1:I5c5nfGErhq9DBumyjCVCggRA74jhgriMqRRFu5jeeY=
|
||||
github.com/foxxorcat/weiyun-sdk-go v0.1.3/go.mod h1:TPxzN0d2PahweUEHlOBWlwZSA+rELSUlGYMWgXRn9ps=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@ -182,6 +184,7 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
@ -213,6 +216,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvki
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@ -220,6 +224,8 @@ 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/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
|
||||
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
|
||||
@ -259,15 +265,15 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
||||
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC9jFiTxyptEKuNIAbiN5ZCQzX2a74lj3xg=
|
||||
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
@ -280,6 +286,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.2.5 h1:MkmkfCHzvmi35EId9SeFPJMZ8bUsijnxwneAWHnnk0k=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.2.5/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
@ -315,8 +323,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/meilisearch/meilisearch-go v0.26.1 h1:3bmo2uLijX7kvBmiZ9LupVfC95TFcRJDgrRTzbOoE4A=
|
||||
github.com/meilisearch/meilisearch-go v0.26.1/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/minio/sio v0.3.0 h1:syEFBewzOMOYVzSTFpp1MqpSZk8rUNbz8VIIc+PNzus=
|
||||
github.com/minio/sio v0.3.0/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
@ -408,6 +416,10 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 h1:WnNuhiq+FOY3jNj6JXFT+eLN3CQ/oPIsDPRanvwsmbI=
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500/go.mod h1:+njLrG5wSeoG4Ds61rFgEzKvenR2UHbjMoDHsczxly0=
|
||||
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
|
||||
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
@ -444,8 +456,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/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/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7 h1:Jtcrb09q0AVWe3BGe8qtuuGxNSHWGkTWr43kHTJ+CpA=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=
|
||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
|
||||
@ -478,6 +490,8 @@ github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 h1:eDfebW/yfq9DtG9RO3K
|
||||
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25/go.mod h1:fH4oNm5F9NfI5dLi0oIMtsLNKQOirUDbEMCIBb/7SU0=
|
||||
github.com/xhofe/tache v0.1.1 h1:O5QY4cVjIGELx3UGh6LbVAc18MWGXgRNQjMt72x6w/8=
|
||||
github.com/xhofe/tache v0.1.1/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
@ -492,6 +506,7 @@ golang.org/x/arch v0.5.0/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-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@ -502,17 +517,21 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE=
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
@ -523,16 +542,18 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -560,16 +581,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/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=
|
||||
@ -589,11 +610,18 @@ golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.134.0 h1:ktL4Goua+UBgoP1eL1/60LwZJqa1sIzkLmvoR3hR6Gw=
|
||||
google.golang.org/api v0.134.0/go.mod h1:sjRL3UnjTx5UqNQS9EWr9N8p7xbHpy1k0XGRLCf3Spk=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
|
@ -1,6 +1,7 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
@ -19,7 +20,7 @@ func NewAuthnInstance(r *http.Request) (*webauthn.WebAuthn, error) {
|
||||
RPDisplayName: setting.GetStr(conf.SiteTitle),
|
||||
RPID: siteUrl.Hostname(),
|
||||
//RPOrigin: siteUrl.String(),
|
||||
RPOrigins: []string{siteUrl.String()},
|
||||
RPOrigins: []string{fmt.Sprintf("%s://%s", siteUrl.Scheme, siteUrl.Host)},
|
||||
// RPOrigin: "http://localhost:5173"
|
||||
})
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ func initSettings() {
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed get settings: %+v", err)
|
||||
}
|
||||
|
||||
for i := range settings {
|
||||
if !isActive(settings[i].Key) && settings[i].Flag != model.DEPRECATED {
|
||||
settings[i].Flag = model.DEPRECATED
|
||||
@ -35,6 +34,9 @@ func initSettings() {
|
||||
// create or save setting
|
||||
for i := range initialSettingItems {
|
||||
item := &initialSettingItems[i]
|
||||
if item.PreDefault == "" {
|
||||
item.PreDefault = item.Value
|
||||
}
|
||||
// err
|
||||
stored, err := op.GetSettingItemByKey(item.Key)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@ -42,7 +44,7 @@ func initSettings() {
|
||||
continue
|
||||
}
|
||||
// save
|
||||
if stored != nil && item.Key != conf.VERSION {
|
||||
if stored != nil && item.Key != conf.VERSION && stored.Value != item.PreDefault {
|
||||
item.Value = stored.Value
|
||||
}
|
||||
if stored == nil || *item != *stored {
|
||||
@ -98,7 +100,7 @@ func InitialSettings() []model.SettingItem {
|
||||
// preview settings
|
||||
{Key: conf.TextTypes, Value: "txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,go,sh,c,cpp,h,hpp,tsx,vtt,srt,ass,rs,lrc", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
{Key: conf.AudioTypes, Value: "mp3,flac,ogg,m4a,wav,opus,wma", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
{Key: conf.VideoTypes, Value: "mp4,mkv,avi,mov,rmvb,webm,flv", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
{Key: conf.VideoTypes, Value: "mp4,mkv,avi,mov,rmvb,webm,flv,m3u8", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
{Key: conf.ImageTypes, Value: "jpg,tiff,jpeg,png,gif,bmp,svg,ico,swf,webp", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
//{Key: conf.OfficeTypes, Value: "doc,docx,xls,xlsx,ppt,pptx", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
{Key: conf.ProxyTypes, Value: "m3u8", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
|
||||
@ -129,7 +131,7 @@ func InitialSettings() []model.SettingItem {
|
||||
// global settings
|
||||
{Key: conf.HideFiles, Value: "/\\/README.md/i", Type: conf.TypeText, Group: model.GLOBAL},
|
||||
{Key: "package_download", Value: "true", Type: conf.TypeBool, Group: model.GLOBAL},
|
||||
{Key: conf.CustomizeHead, 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.CustomizeHead, PreDefault: `<script src="https://polyfill.io/v3/polyfill.min.js?features=String.prototype.replaceAll"></script>`, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
|
||||
{Key: conf.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},
|
||||
@ -176,6 +178,11 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.LdapDefaultDir, Value: "/", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},
|
||||
{Key: conf.LdapDefaultPermission, Value: "0", Type: conf.TypeNumber, Group: model.LDAP, Flag: model.PRIVATE},
|
||||
{Key: conf.LdapLoginTips, Value: "login with ldap", Type: conf.TypeString, Group: model.LDAP, Flag: model.PUBLIC},
|
||||
|
||||
//s3 settings
|
||||
{Key: conf.S3AccessKeyId, Value: "", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
|
||||
{Key: conf.S3SecretAccessKey, Value: "", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
|
||||
{Key: conf.S3Buckets, Value: "[]", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
|
||||
}
|
||||
initialSettingItems = append(initialSettingItems, tool.Tools.Items()...)
|
||||
if flags.Dev {
|
||||
|
@ -31,6 +31,7 @@ func initUser() {
|
||||
PwdHash: model.TwoHashPwd(adminPassword, salt),
|
||||
Role: model.ADMIN,
|
||||
BasePath: "/",
|
||||
Authn: "[]",
|
||||
}
|
||||
if err := op.CreateUser(admin); err != nil {
|
||||
panic(err)
|
||||
@ -53,6 +54,7 @@ func initUser() {
|
||||
BasePath: "/",
|
||||
Permission: 0,
|
||||
Disabled: true,
|
||||
Authn: "[]",
|
||||
}
|
||||
if err := db.CreateUser(guest); err != nil {
|
||||
utils.Log.Fatalf("[init user] Failed to create guest user: %v", err)
|
||||
@ -62,6 +64,7 @@ func initUser() {
|
||||
}
|
||||
}
|
||||
hashPwdForOldVersion()
|
||||
updateAuthnForOldVersion()
|
||||
}
|
||||
|
||||
func hashPwdForOldVersion() {
|
||||
@ -80,3 +83,19 @@ func hashPwdForOldVersion() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateAuthnForOldVersion() {
|
||||
users, _, err := op.GetUsers(1, -1)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("[update authn for old version] failed get users: %v", err)
|
||||
}
|
||||
for i := range users {
|
||||
user := users[i]
|
||||
if user.Authn == "" {
|
||||
user.Authn = "[]"
|
||||
if err := db.UpdateUser(&user); err != nil {
|
||||
utils.Log.Fatalf("[update authn for old version] failed update user: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,8 @@ func LoadStorages() {
|
||||
if err != nil {
|
||||
utils.Log.Errorf("failed get enabled storages: %+v", err)
|
||||
} else {
|
||||
utils.Log.Infof("success load storage: [%s], driver: [%s]",
|
||||
storages[i].MountPath, storages[i].Driver)
|
||||
utils.Log.Infof("success load storage: [%s], driver: [%s], order: [%d]",
|
||||
storages[i].MountPath, storages[i].Driver, storages[i].Order)
|
||||
}
|
||||
}
|
||||
conf.StoragesLoaded = true
|
||||
|
@ -1,9 +1,10 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/alist-org/alist/v3/cmd/flags"
|
||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
@ -63,6 +64,12 @@ type Cors struct {
|
||||
AllowHeaders []string `json:"allow_headers" env:"ALLOW_HEADERS"`
|
||||
}
|
||||
|
||||
type S3 struct {
|
||||
Enable bool `json:"enable" env:"ENABLE"`
|
||||
Port int `json:"port" env:"PORT"`
|
||||
SSL bool `json:"ssl" env:"SSL"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Force bool `json:"force" env:"FORCE"`
|
||||
SiteURL string `json:"site_url" env:"SITE_URL"`
|
||||
@ -70,7 +77,7 @@ type Config struct {
|
||||
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
|
||||
TokenExpiresIn int `json:"token_expires_in" env:"TOKEN_EXPIRES_IN"`
|
||||
Database Database `json:"database" envPrefix:"DB_"`
|
||||
Meilisearch Meilisearch `json:"meilisearch" env:"MEILISEARCH"`
|
||||
Meilisearch Meilisearch `json:"meilisearch" envPrefix:"MEILISEARCH_"`
|
||||
Scheme Scheme `json:"scheme"`
|
||||
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
|
||||
BleveDir string `json:"bleve_dir" env:"BLEVE_DIR"`
|
||||
@ -81,6 +88,7 @@ type Config struct {
|
||||
TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" env:"TLS_INSECURE_SKIP_VERIFY"`
|
||||
Tasks TasksConfig `json:"tasks" envPrefix:"TASKS_"`
|
||||
Cors Cors `json:"cors" envPrefix:"CORS_"`
|
||||
S3 S3 `json:"s3" envPrefix:"S3_"`
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
@ -142,5 +150,10 @@ func DefaultConfig() *Config {
|
||||
AllowMethods: []string{"*"},
|
||||
AllowHeaders: []string{"*"},
|
||||
},
|
||||
S3: S3{
|
||||
Enable: false,
|
||||
Port: 5246,
|
||||
SSL: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,11 @@ const (
|
||||
LdapDefaultDir = "ldap_default_dir"
|
||||
LdapLoginTips = "ldap_login_tips"
|
||||
|
||||
//s3
|
||||
S3Buckets = "s3_buckets"
|
||||
S3AccessKeyId = "s3_access_key_id"
|
||||
S3SecretAccessKey = "s3_secret_access_key"
|
||||
|
||||
// qbittorrent
|
||||
QbittorrentUrl = "qbittorrent_url"
|
||||
QbittorrentSeedtime = "qbittorrent_seedtime"
|
||||
|
@ -2,6 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
@ -65,5 +66,8 @@ func GetEnabledStorages() ([]model.Storage, error) {
|
||||
if err := db.Where(fmt.Sprintf("%s = ?", columnName("disabled")), false).Find(&storages).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
sort.Slice(storages, func(i, j int) bool {
|
||||
return storages[i].Order < storages[j].Order
|
||||
})
|
||||
return storages, nil
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ type Config struct {
|
||||
CheckStatus bool `json:"-"`
|
||||
Alert string `json:"alert"` //info,success,warning,danger
|
||||
NoOverwriteUpload bool `json:"-"` // whether to support overwrite upload
|
||||
ProxyRangeOption bool `json:"-"`
|
||||
}
|
||||
|
||||
func (c Config) MustProxy() bool {
|
||||
|
@ -3,6 +3,7 @@ package errs
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
pkgerr "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -33,3 +34,6 @@ func IsNotFoundError(err error) bool {
|
||||
func IsNotSupportError(err error) bool {
|
||||
return errors.Is(pkgerr.Cause(err), NotSupport)
|
||||
}
|
||||
func IsNotImplement(err error) bool {
|
||||
return errors.Is(pkgerr.Cause(err), NotImplement)
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ type FileStreamer interface {
|
||||
GetMimetype() string
|
||||
//SetReader(io.Reader)
|
||||
NeedStore() bool
|
||||
IsForceStreamUpload() bool
|
||||
GetExist() Obj
|
||||
SetExist(Obj)
|
||||
//for a non-seekable Stream, RangeRead supports peeking some data, and CacheFullInTempFile still works
|
||||
|
@ -10,6 +10,7 @@ const (
|
||||
INDEX
|
||||
SSO
|
||||
LDAP
|
||||
S3
|
||||
)
|
||||
|
||||
const (
|
||||
@ -20,13 +21,14 @@ const (
|
||||
)
|
||||
|
||||
type SettingItem struct {
|
||||
Key string `json:"key" gorm:"primaryKey" binding:"required"` // unique key
|
||||
Value string `json:"value"` // value
|
||||
Help string `json:"help"` // help message
|
||||
Type string `json:"type"` // string, number, bool, select
|
||||
Options string `json:"options"` // values for select
|
||||
Group int `json:"group"` // use to group setting in frontend
|
||||
Flag int `json:"flag"` // 0 = public, 1 = private, 2 = readonly, 3 = deprecated, etc.
|
||||
Key string `json:"key" gorm:"primaryKey" binding:"required"` // unique key
|
||||
Value string `json:"value"` // value
|
||||
PreDefault string `json:"-" gorm:"-:all"` // deprecated value
|
||||
Help string `json:"help"` // help message
|
||||
Type string `json:"type"` // string, number, bool, select
|
||||
Options string `json:"options"` // values for select
|
||||
Group int `json:"group"` // use to group setting in frontend
|
||||
Flag int `json:"flag"` // 0 = public, 1 = private, 2 = readonly, 3 = deprecated, etc.
|
||||
}
|
||||
|
||||
func (s SettingItem) IsDeprecated() bool {
|
||||
|
@ -27,6 +27,7 @@ type Sort struct {
|
||||
type Proxy struct {
|
||||
WebProxy bool `json:"web_proxy"`
|
||||
WebdavPolicy string `json:"webdav_policy"`
|
||||
ProxyRange bool `json:"proxy_range"`
|
||||
DownProxyUrl string `json:"down_proxy_url"`
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
@ -271,7 +272,7 @@ func (d *downloader) tryDownloadChunk(params *HttpRequestParams, ch *chunk) (int
|
||||
}
|
||||
}
|
||||
|
||||
n, err := io.Copy(ch.buf, resp.Body)
|
||||
n, err := utils.CopyWithBuffer(ch.buf, resp.Body)
|
||||
|
||||
if err != nil {
|
||||
return n, &errReadingBody{err: err}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user