Compare commits
130 Commits
Author | SHA1 | Date | |
---|---|---|---|
cf58ab3a78 | |||
33ba7f1521 | |||
201e25c17f | |||
ecefa5e0eb | |||
650b03aeb1 | |||
7341846499 | |||
a3908fd9a6 | |||
2a035302b2 | |||
016e169c41 | |||
088120df82 | |||
aa45a82914 | |||
5084d98398 | |||
fa15c576f0 | |||
2d3605c684 | |||
492b49d77a | |||
94915b2148 | |||
2dec756f23 | |||
4c0cffd29b | |||
25c5e075a9 | |||
398c04386a | |||
12b429584e | |||
150dcc2147 | |||
0ba754fd40 | |||
28d2367a87 | |||
a4ad98ee3e | |||
1c01dc6839 | |||
c3c5843dce | |||
6c38c5972d | |||
0a46979c51 | |||
67c93eed2b | |||
f58de9923a | |||
2671c876f1 | |||
e707fa38f1 | |||
b803b0070e | |||
64ceb5afb6 | |||
10c7ebb1c0 | |||
d0cda62703 | |||
ce0b99a510 | |||
34a148c83d | |||
4955d8cec8 | |||
216e3909f3 | |||
a701432b8b | |||
a2dc45a80b | |||
48ac23c8de | |||
2830575490 | |||
e8538bd215 | |||
c3e43ff605 | |||
5f19d73fcc | |||
bdf4b52885 | |||
6106a2d4cc | |||
b6451451b1 | |||
f06d2c0348 | |||
b7ae56b109 | |||
5d9167d676 | |||
1b42b9627c | |||
bb58b94a10 | |||
ffce61d227 | |||
0310b70d90 | |||
73f0b135b6 | |||
8316f81e41 | |||
cdbfda8921 | |||
9667832b32 | |||
b36d38f63f | |||
c8317250c1 | |||
0242f36e1c | |||
40a68bcee6 | |||
92713ef5c4 | |||
716d33fddd | |||
c9fa3d7cd6 | |||
4874c9e43b | |||
34ada81582 | |||
ba716ae325 | |||
d4f9c4b6af | |||
b910b8917f | |||
d92744e673 | |||
868b0ec25c | |||
e21edf98e2 | |||
d2514d236f | |||
34b6785fab | |||
48f50a2ceb | |||
74887922b4 | |||
bcb24d61ea | |||
db1494455d | |||
d9a1809313 | |||
0715198c7f | |||
ef5e192c3b | |||
489b28bdf7 | |||
18176c659c | |||
4c48a816bf | |||
9af7aaab59 | |||
a54a09314f | |||
e2fcd73720 | |||
e238b90836 | |||
69e5b66b50 | |||
e8e6d71c41 | |||
4ba476e25c | |||
e5fe9ea5f6 | |||
e1906c9312 | |||
51c95ee117 | |||
1f652e2e7d | |||
8e6c1aa78d | |||
6bff5b6107 | |||
94937db491 | |||
3dc250cc37 | |||
9560799175 | |||
8f3c5b1587 | |||
285125d06a | |||
a26185fe05 | |||
a7efa3a676 | |||
d596ef5c38 | |||
34e34ef564 | |||
8032d0afb6 | |||
d3bc8993ee | |||
62ed169a39 | |||
979d0cfeee | |||
29165d8e60 | |||
2d77db6bc2 | |||
74f8295960 | |||
f2727095d9 | |||
d4285b7c6c | |||
2e4265a778 | |||
81258d3e8a | |||
a6bead90d7 | |||
87caaf2459 | |||
af9c6afd25 | |||
8b5727a0aa | |||
aeae47c9bf | |||
1aff758688 | |||
4a42bc5083 | |||
5fa70e4010 |
133
.github/workflows/beta_release.yml
vendored
Normal file
133
.github/workflows/beta_release.yml
vendored
Normal file
@ -0,0 +1,133 @@
|
||||
name: beta release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'main' ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ ubuntu-latest ]
|
||||
go-version: [ '1.21' ]
|
||||
name: Beta Release Changelog
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create or update ref
|
||||
id: create-or-update-ref
|
||||
uses: ovsds/create-or-update-ref-action@v1
|
||||
with:
|
||||
ref: tags/beta
|
||||
sha: ${{ github.sha }}
|
||||
|
||||
- name: Delete beta tag
|
||||
run: git tag -d beta
|
||||
continue-on-error: true
|
||||
|
||||
- name: changelog # or changelogithub@0.12 if ensure the stable result
|
||||
id: changelog
|
||||
run: |
|
||||
git tag -l
|
||||
npx changelogithub --output CHANGELOG.md
|
||||
# npx changelogen@latest --output CHANGELOG.md
|
||||
|
||||
- name: Upload assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: CHANGELOG.md
|
||||
files: CHANGELOG.md
|
||||
prerelease: true
|
||||
tag_name: beta
|
||||
|
||||
release:
|
||||
needs:
|
||||
- changelog
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: '!(*musl*|*windows-arm64*|*android*|*freebsd*)' # xgo
|
||||
hash: "md5"
|
||||
- target: 'linux-!(arm*)-musl*' #musl-not-arm
|
||||
hash: "md5-linux-musl"
|
||||
- target: 'linux-arm*-musl*' #musl-arm
|
||||
hash: "md5-linux-musl-arm"
|
||||
- target: 'windows-arm64' #win-arm64
|
||||
hash: "md5-windows-arm64"
|
||||
- target: 'android-*' #android
|
||||
hash: "md5-android"
|
||||
- target: 'freebsd-*' #freebsd
|
||||
hash: "md5-freebsd"
|
||||
|
||||
name: Beta Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Setup web
|
||||
run: bash build.sh dev web
|
||||
|
||||
- name: Build
|
||||
id: test-action
|
||||
uses: go-cross/cgo-actions@v1
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
musl-target-format: $os-$musl-$arch
|
||||
out-dir: build
|
||||
|
||||
- name: Compress
|
||||
run: |
|
||||
bash build.sh zip ${{ matrix.hash }}
|
||||
|
||||
- name: Upload assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/compress/*
|
||||
prerelease: true
|
||||
tag_name: beta
|
||||
|
||||
desktop:
|
||||
needs:
|
||||
- release
|
||||
name: Beta Release Desktop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: alist-org/desktop-release
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Commit
|
||||
run: |
|
||||
git config --local user.email "bot@nn.ci"
|
||||
git config --local user.name "IlaBot"
|
||||
git commit --allow-empty -m "Trigger build for ${{ github.sha }}"
|
||||
|
||||
- name: Push commit
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.MY_TOKEN }}
|
||||
branch: main
|
||||
repository: alist-org/desktop-release
|
27
.github/workflows/build_docker.yml
vendored
27
.github/workflows/build_docker.yml
vendored
@ -23,6 +23,12 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: xhofe/alist
|
||||
tags: |
|
||||
type=schedule
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=ref,event=pr
|
||||
type=raw,value=beta,enable={{is_default_branch}}
|
||||
|
||||
- name: Docker meta with ffmpeg
|
||||
id: meta-ffmpeg
|
||||
@ -30,7 +36,13 @@ jobs:
|
||||
with:
|
||||
images: xhofe/alist
|
||||
flavor: |
|
||||
suffix=-ffmpeg,onlatest=true
|
||||
suffix=-ffmpeg
|
||||
tags: |
|
||||
type=schedule
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=ref,event=pr
|
||||
type=raw,value=beta,enable={{is_default_branch}}
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
@ -41,7 +53,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: build/musl-libs
|
||||
key: docker-musl-libs
|
||||
key: docker-musl-libs-v2
|
||||
|
||||
- name: Download Musl Library
|
||||
if: steps.cache-musl.outputs.cache-hit != 'true'
|
||||
@ -72,22 +84,19 @@ jobs:
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
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
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
|
||||
- name: Build and push with ffmpeg
|
||||
id: docker_build_ffmpeg
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ffmpeg
|
||||
file: Dockerfile.ci
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
|
||||
labels: ${{ steps.meta-ffmpeg.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
||||
build-args: INSTALL_FFMPEG=true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
|
||||
build_docker_with_aria2:
|
||||
needs: build_docker
|
||||
|
7
.github/workflows/changelog.yml
vendored
7
.github/workflows/changelog.yml
vendored
@ -3,7 +3,7 @@ name: auto changelog
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
@ -14,6 +14,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Delete beta tag
|
||||
run: git tag -d beta
|
||||
continue-on-error: true
|
||||
|
||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.MY_TOKEN}}
|
||||
|
17
.github/workflows/release.yml
vendored
17
.github/workflows/release.yml
vendored
@ -13,6 +13,23 @@ jobs:
|
||||
name: Release
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Prerelease
|
||||
uses: irongut/EditRelease@v1.2.0
|
||||
with:
|
||||
|
11
.github/workflows/release_docker.yml
vendored
11
.github/workflows/release_docker.yml
vendored
@ -3,7 +3,7 @@ name: release_docker
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release_docker:
|
||||
@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: build/musl-libs
|
||||
key: docker-musl-libs
|
||||
key: docker-musl-libs-v2
|
||||
|
||||
- name: Download Musl Library
|
||||
if: steps.cache-musl.outputs.cache-hit != 'true'
|
||||
@ -58,7 +58,7 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
|
||||
- name: Docker meta with ffmpeg
|
||||
id: meta-ffmpeg
|
||||
@ -74,11 +74,12 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.ffmpeg
|
||||
file: Dockerfile.ci
|
||||
push: true
|
||||
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
|
||||
labels: ${{ steps.meta-ffmpeg.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
||||
build-args: INSTALL_FFMPEG=true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
|
||||
release_docker_with_aria2:
|
||||
needs: release_docker
|
||||
|
34
.github/workflows/release_freebsd.yml
vendored
Normal file
34
.github/workflows/release_freebsd.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: release_freebsd
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
release_freebsd:
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ ubuntu-latest ]
|
||||
go-version: [ '1.21' ]
|
||||
name: Release
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
bash build.sh release freebsd
|
||||
|
||||
- name: Upload assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/compress/*
|
15
Dockerfile
15
Dockerfile
@ -8,16 +8,23 @@ COPY ./ ./
|
||||
RUN bash build.sh release docker
|
||||
|
||||
FROM alpine:edge
|
||||
|
||||
ARG INSTALL_FFMPEG=false
|
||||
LABEL MAINTAINER="i@nn.ci"
|
||||
VOLUME /opt/alist/data/
|
||||
|
||||
WORKDIR /opt/alist/
|
||||
COPY --from=builder /app/bin/alist ./
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade --no-cache && \
|
||||
apk add --no-cache bash ca-certificates su-exec tzdata; \
|
||||
chmod +x /entrypoint.sh && \
|
||||
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY --from=builder /app/bin/alist ./
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh && /entrypoint.sh version
|
||||
|
||||
ENV PUID=0 PGID=0 UMASK=022
|
||||
VOLUME /opt/alist/data/
|
||||
EXPOSE 5244 5245
|
||||
CMD [ "/entrypoint.sh" ]
|
@ -1,16 +1,22 @@
|
||||
FROM alpine:edge
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG INSTALL_FFMPEG=false
|
||||
LABEL MAINTAINER="i@nn.ci"
|
||||
VOLUME /opt/alist/data/
|
||||
|
||||
WORKDIR /opt/alist/
|
||||
COPY /build/${TARGETPLATFORM}/alist ./
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade --no-cache && \
|
||||
apk add --no-cache bash ca-certificates su-exec tzdata; \
|
||||
chmod +x /entrypoint.sh && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
/entrypoint.sh version
|
||||
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY /build/${TARGETPLATFORM}/alist ./
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh && /entrypoint.sh version
|
||||
|
||||
ENV PUID=0 PGID=0 UMASK=022
|
||||
VOLUME /opt/alist/data/
|
||||
EXPOSE 5244 5245
|
||||
CMD [ "/entrypoint.sh" ]
|
@ -1,4 +0,0 @@
|
||||
FROM xhofe/alist:latest
|
||||
RUN apk update && \
|
||||
apk add --no-cache ffmpeg \
|
||||
rm -rf /var/cache/apk/*
|
@ -58,7 +58,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
||||
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
|
||||
- [x] [YandexDisk](https://disk.yandex.com/)
|
||||
- [x] [BaiduNetdisk](http://pan.baidu.com/)
|
||||
- [x] [Terabox](https://www.terabox.com/main)
|
||||
@ -98,7 +98,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
||||
|
||||
## Document
|
||||
|
||||
<https://alist.nn.ci/>
|
||||
<https://alistgo.com/>
|
||||
|
||||
## Demo
|
||||
|
||||
@ -117,7 +117,7 @@ https://alist.nn.ci/guide/sponsor.html
|
||||
|
||||
- [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/) - 阿里云盘资源搜索引擎
|
||||
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||
|
||||
## Contributors
|
||||
|
||||
@ -138,4 +138,4 @@ The `AList` is open-source software licensed under the AGPL-3.0 license.
|
||||
|
||||
---
|
||||
|
||||
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||
> [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||
|
@ -58,7 +58,7 @@
|
||||
- [x] WebDav(支持无API的OneDrive/SharePoint)
|
||||
- [x] Teambition([中国](https://www.teambition.com/ ),[国际](https://us.teambition.com/ ))
|
||||
- [x] [分秒帧](https://www.mediatrack.cn/)
|
||||
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
|
||||
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云,共享群组)
|
||||
- [x] [Yandex.Disk](https://disk.yandex.com/)
|
||||
- [x] [百度网盘](http://pan.baidu.com/)
|
||||
- [x] [UC网盘](https://drive.uc.cn)
|
||||
@ -115,7 +115,7 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
|
||||
|
||||
- [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/) - 阿里云盘资源搜索引擎
|
||||
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||
|
||||
## 贡献者
|
||||
|
||||
|
@ -58,7 +58,7 @@
|
||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
||||
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
|
||||
- [x] [YandexDisk](https://disk.yandex.com/)
|
||||
- [x] [BaiduNetdisk](http://pan.baidu.com/)
|
||||
- [x] [Terabox](https://www.terabox.com/main)
|
||||
@ -117,7 +117,7 @@ https://alist.nn.ci/guide/sponsor.html
|
||||
|
||||
- [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/) - 阿里云盘资源搜索引擎
|
||||
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||
|
||||
## コントリビューター
|
||||
|
||||
|
44
build.sh
44
build.sh
@ -8,6 +8,7 @@ if [ "$1" = "dev" ]; then
|
||||
version="dev"
|
||||
webVersion="dev"
|
||||
else
|
||||
git tag -d beta
|
||||
version=$(git describe --abbrev=0 --tags)
|
||||
webVersion=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||
fi
|
||||
@ -92,7 +93,7 @@ BuildDocker() {
|
||||
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)
|
||||
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 riscv64-linux-musl-cross powerpc64le-linux-musl-cross)
|
||||
for i in "${FILES[@]}"; do
|
||||
url="${BASE}${i}.tgz"
|
||||
lib_tgz="build/${i}.tgz"
|
||||
@ -111,8 +112,8 @@ BuildDockerMultiplatform() {
|
||||
docker_lflags="--extldflags '-static -fpic' $ldflags"
|
||||
export CGO_ENABLED=1
|
||||
|
||||
OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-s390x)
|
||||
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc s390x-linux-musl-gcc)
|
||||
OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-s390x linux-riscv64 linux-ppc64le)
|
||||
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc s390x-linux-musl-gcc riscv64-linux-musl-gcc powerpc64le-linux-musl-gcc)
|
||||
for i in "${!OS_ARCHES[@]}"; do
|
||||
os_arch=${OS_ARCHES[$i]}
|
||||
cgo_cc=${CGO_ARGS[$i]}
|
||||
@ -232,6 +233,29 @@ BuildReleaseAndroid() {
|
||||
done
|
||||
}
|
||||
|
||||
BuildReleaseFreeBSD() {
|
||||
rm -rf .git/
|
||||
mkdir -p "build/freebsd"
|
||||
OS_ARCHES=(amd64 arm64 i386)
|
||||
GO_ARCHES=(amd64 arm64 386)
|
||||
CGO_ARGS=(x86_64-unknown-freebsd14.1 aarch64-unknown-freebsd14.1 i386-unknown-freebsd14.1)
|
||||
for i in "${!OS_ARCHES[@]}"; do
|
||||
os_arch=${OS_ARCHES[$i]}
|
||||
cgo_cc="clang --target=${CGO_ARGS[$i]} --sysroot=/opt/freebsd/${os_arch}"
|
||||
echo building for freebsd-${os_arch}
|
||||
sudo mkdir -p "/opt/freebsd/${os_arch}"
|
||||
wget -q https://download.freebsd.org/releases/${os_arch}/14.1-RELEASE/base.txz
|
||||
sudo tar -xf ./base.txz -C /opt/freebsd/${os_arch}
|
||||
rm base.txz
|
||||
export GOOS=freebsd
|
||||
export GOARCH=${GO_ARCHES[$i]}
|
||||
export CC=${cgo_cc}
|
||||
export CGO_ENABLED=1
|
||||
export CGO_LDFLAGS="-fuse-ld=lld"
|
||||
go build -o ./build/$appName-freebsd-$os_arch -ldflags="$ldflags" -tags=jsoniter .
|
||||
done
|
||||
}
|
||||
|
||||
MakeRelease() {
|
||||
cd build
|
||||
mkdir compress
|
||||
@ -250,6 +274,11 @@ MakeRelease() {
|
||||
tar -czvf compress/"$i".tar.gz alist
|
||||
rm -f alist
|
||||
done
|
||||
for i in $(find . -type f -name "$appName-freebsd-*"); do
|
||||
cp "$i" alist
|
||||
tar -czvf compress/"$i".tar.gz alist
|
||||
rm -f alist
|
||||
done
|
||||
for i in $(find . -type f -name "$appName-windows-*"); do
|
||||
cp "$i" alist.exe
|
||||
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip alist.exe
|
||||
@ -267,6 +296,8 @@ if [ "$1" = "dev" ]; then
|
||||
BuildDocker
|
||||
elif [ "$2" = "docker-multiplatform" ]; then
|
||||
BuildDockerMultiplatform
|
||||
elif [ "$2" = "web" ]; then
|
||||
echo "web only"
|
||||
else
|
||||
BuildDev
|
||||
fi
|
||||
@ -285,6 +316,11 @@ elif [ "$1" = "release" ]; then
|
||||
elif [ "$2" = "android" ]; then
|
||||
BuildReleaseAndroid
|
||||
MakeRelease "md5-android.txt"
|
||||
elif [ "$2" = "freebsd" ]; then
|
||||
BuildReleaseFreeBSD
|
||||
MakeRelease "md5-freebsd.txt"
|
||||
elif [ "$2" = "web" ]; then
|
||||
echo "web only"
|
||||
else
|
||||
BuildRelease
|
||||
MakeRelease "md5.txt"
|
||||
@ -293,6 +329,8 @@ elif [ "$1" = "prepare" ]; then
|
||||
if [ "$2" = "docker-multiplatform" ]; then
|
||||
PrepareBuildDockerMusl
|
||||
fi
|
||||
elif [ "$1" = "zip" ]; then
|
||||
MakeRelease "$2".txt
|
||||
else
|
||||
echo -e "Parameter error"
|
||||
fi
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
func Init() {
|
||||
bootstrap.InitConfig()
|
||||
bootstrap.Log()
|
||||
bootstrap.InitHostKey()
|
||||
bootstrap.InitDB()
|
||||
data.InitData()
|
||||
bootstrap.InitIndex()
|
||||
|
@ -139,7 +139,7 @@ var LangCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := os.MkdirAll("lang", 0777)
|
||||
if err != nil {
|
||||
utils.Log.Fatal("failed create folder: %s", err.Error())
|
||||
utils.Log.Fatalf("failed create folder: %s", err.Error())
|
||||
}
|
||||
generateDriversJson()
|
||||
generateSettingsJson()
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
"github.com/KirCute/sftpd-alist"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -112,6 +114,42 @@ the address is defined in config file`,
|
||||
}
|
||||
}()
|
||||
}
|
||||
var ftpDriver *server.FtpMainDriver
|
||||
var ftpServer *ftpserver.FtpServer
|
||||
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable {
|
||||
var err error
|
||||
ftpDriver, err = server.NewMainDriver()
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to start ftp driver: %s", err.Error())
|
||||
} else {
|
||||
utils.Log.Infof("start ftp server on %s", conf.Conf.FTP.Listen)
|
||||
go func() {
|
||||
ftpServer = ftpserver.NewFtpServer(ftpDriver)
|
||||
err = ftpServer.ListenAndServe()
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("problem ftp server listening: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
var sftpDriver *server.SftpDriver
|
||||
var sftpServer *sftpd.SftpServer
|
||||
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable {
|
||||
var err error
|
||||
sftpDriver, err = server.NewSftpDriver()
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to start sftp driver: %s", err.Error())
|
||||
} else {
|
||||
utils.Log.Infof("start sftp server on %s", conf.Conf.SFTP.Listen)
|
||||
go func() {
|
||||
sftpServer = sftpd.NewSftpServer(sftpDriver)
|
||||
err = sftpServer.RunServer()
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("problem sftp server listening: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
// Wait for interrupt signal to gracefully shutdown the server with
|
||||
// a timeout of 1 second.
|
||||
quit := make(chan os.Signal, 1)
|
||||
@ -152,6 +190,25 @@ the address is defined in config file`,
|
||||
}
|
||||
}()
|
||||
}
|
||||
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable && ftpServer != nil && ftpDriver != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ftpDriver.Stop()
|
||||
if err := ftpServer.Stop(); err != nil {
|
||||
utils.Log.Fatal("FTP server shutdown err: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable && sftpServer != nil && sftpDriver != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := sftpServer.Close(); err != nil {
|
||||
utils.Log.Fatal("SFTP server shutdown err: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
utils.Log.Println("Server exit")
|
||||
},
|
||||
|
43
drivers/115/appver.go
Normal file
43
drivers/115/appver.go
Normal file
@ -0,0 +1,43 @@
|
||||
package _115
|
||||
|
||||
import (
|
||||
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
md5Salt = "Qclm8MGWUv59TnrR0XPg"
|
||||
appVer = "27.0.5.7"
|
||||
)
|
||||
|
||||
func (d *Pan115) getAppVersion() ([]driver115.AppVersion, error) {
|
||||
result := driver115.VersionResp{}
|
||||
resp, err := base.RestyClient.R().Get(driver115.ApiGetVersion)
|
||||
|
||||
err = driver115.CheckErr(err, &result, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Data.GetAppVersions(), nil
|
||||
}
|
||||
|
||||
func (d *Pan115) getAppVer() string {
|
||||
// todo add some cache?
|
||||
vers, err := d.getAppVersion()
|
||||
if err != nil {
|
||||
log.Warnf("[115] get app version failed: %v", err)
|
||||
return appVer
|
||||
}
|
||||
for _, ver := range vers {
|
||||
if ver.AppName == "win" {
|
||||
return ver.Version
|
||||
}
|
||||
}
|
||||
return appVer
|
||||
}
|
||||
|
||||
func (d *Pan115) initAppVer() {
|
||||
appVer = d.getAppVer()
|
||||
}
|
@ -3,6 +3,7 @@ package _115
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
@ -16,8 +17,9 @@ import (
|
||||
type Pan115 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
client *driver115.Pan115Client
|
||||
limiter *rate.Limiter
|
||||
client *driver115.Pan115Client
|
||||
limiter *rate.Limiter
|
||||
appVerOnce sync.Once
|
||||
}
|
||||
|
||||
func (d *Pan115) Config() driver.Config {
|
||||
@ -29,6 +31,7 @@ func (d *Pan115) GetAddition() driver.Additional {
|
||||
}
|
||||
|
||||
func (d *Pan115) Init(ctx context.Context) error {
|
||||
d.appVerOnce.Do(d.initAppVer)
|
||||
if d.LimitRate > 0 {
|
||||
d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
|
||||
}
|
||||
@ -63,7 +66,7 @@ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var userAgent = args.Header.Get("User-Agent")
|
||||
userAgent := args.Header.Get("User-Agent")
|
||||
downloadInfo, err := d.
|
||||
DownloadWithUA(file.(*FileObj).PickCode, userAgent)
|
||||
if err != nil {
|
||||
@ -76,28 +79,60 @@ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if _, err := d.client.Mkdir(parentDir.GetID(), dirName); err != nil {
|
||||
return err
|
||||
|
||||
result := driver115.MkdirResp{}
|
||||
form := map[string]string{
|
||||
"pid": parentDir.GetID(),
|
||||
"cname": dirName,
|
||||
}
|
||||
return nil
|
||||
req := d.client.NewRequest().
|
||||
SetFormData(form).
|
||||
SetResult(&result).
|
||||
ForceContentType("application/json;charset=UTF-8")
|
||||
|
||||
resp, err := req.Post(driver115.ApiDirAdd)
|
||||
|
||||
err = driver115.CheckErr(err, &result, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := d.getNewFile(result.FileID)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return d.client.Move(dstDir.GetID(), srcObj.GetID())
|
||||
if err := d.client.Move(dstDir.GetID(), srcObj.GetID()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := d.getNewFile(srcObj.GetID())
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return d.client.Rename(srcObj.GetID(), newName)
|
||||
if err := d.client.Rename(srcObj.GetID(), newName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := d.getNewFile((srcObj.GetID()))
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
@ -114,9 +149,9 @@ func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {
|
||||
return d.client.Delete(obj.GetID())
|
||||
}
|
||||
|
||||
func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
@ -125,10 +160,10 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
)
|
||||
|
||||
if ok, err := d.client.UploadAvailable(); err != nil || !ok {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if stream.GetSize() > d.client.UploadMetaInfo.SizeLimit {
|
||||
return driver115.ErrUploadTooLarge
|
||||
return nil, driver115.ErrUploadTooLarge
|
||||
}
|
||||
//if digest, err = d.client.GetDigestResult(stream); err != nil {
|
||||
// return err
|
||||
@ -141,22 +176,22 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
}
|
||||
reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: hashSize})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
preHash, err := utils.HashReader(utils.SHA1, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
preHash = strings.ToUpper(preHash)
|
||||
fullHash := stream.GetHash().GetHash(utils.SHA1)
|
||||
if len(fullHash) <= 0 {
|
||||
tmpF, err := stream.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
fullHash, err = utils.HashFile(utils.SHA1, tmpF)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fullHash = strings.ToUpper(fullHash)
|
||||
@ -165,21 +200,52 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
// note that 115 add timeout for rapid-upload,
|
||||
// and "sig invalid" err is thrown even when the hash is correct after timeout.
|
||||
if fastInfo, err = d.rapidUpload(stream.GetSize(), stream.GetName(), dirID, preHash, fullHash, stream); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if matched, err := fastInfo.Ok(); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
} else if matched {
|
||||
return nil
|
||||
f, err := d.getNewFileByPickCode(fastInfo.PickCode)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
var uploadResult *UploadResult
|
||||
// 闪传失败,上传
|
||||
if stream.GetSize() <= utils.KB { // 文件大小小于1KB,改用普通模式上传
|
||||
return d.client.UploadByOSS(&fastInfo.UploadOSSParams, stream, dirID)
|
||||
if stream.GetSize() <= 10*utils.MB { // 文件大小小于10MB,改用普通模式上传
|
||||
if uploadResult, err = d.UploadByOSS(&fastInfo.UploadOSSParams, stream, dirID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// 分片上传
|
||||
if uploadResult, err = d.UploadByMultipart(&fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// 分片上传
|
||||
return d.UploadByMultipart(&fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID)
|
||||
|
||||
file, err := d.getNewFile(uploadResult.Data.FileID)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (d *Pan115) OfflineList(ctx context.Context) ([]*driver115.OfflineTask, error) {
|
||||
resp, err := d.client.ListOfflineTask(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Tasks, nil
|
||||
}
|
||||
|
||||
func (d *Pan115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {
|
||||
return d.client.AddOfflineTaskURIs(uris, dstDir.GetID())
|
||||
}
|
||||
|
||||
func (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, deleteFiles bool) error {
|
||||
return d.client.DeleteOfflineTasks(hashes, deleteFiles)
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Pan115)(nil)
|
||||
|
@ -8,18 +8,18 @@ import (
|
||||
type Addition struct {
|
||||
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
|
||||
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
|
||||
QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,linux,mac,windows,tv" default:"linux" help:"select the QR code device, default linux"`
|
||||
PageSize int64 `json:"page_size" type:"number" default:"56" help:"list api per page size of 115 driver"`
|
||||
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
|
||||
QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,tv,alipaymini,wechatmini,qandroid" default:"linux" help:"select the QR code device, default linux"`
|
||||
PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
|
||||
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate ([limit]r/1s)"`
|
||||
driver.RootID
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "115 Cloud",
|
||||
DefaultRoot: "0",
|
||||
//OnlyProxy: true,
|
||||
//OnlyLocal: true,
|
||||
//NoOverwriteUpload: true,
|
||||
// OnlyProxy: true,
|
||||
// OnlyLocal: true,
|
||||
// NoOverwriteUpload: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -1,10 +1,11 @@
|
||||
package _115
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SheltonZhu/115driver/pkg/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ model.Obj = (*FileObj)(nil)
|
||||
@ -20,3 +21,18 @@ func (f *FileObj) CreateTime() time.Time {
|
||||
func (f *FileObj) GetHash() utils.HashInfo {
|
||||
return utils.NewHashInfo(utils.SHA1, f.Sha1)
|
||||
}
|
||||
|
||||
type UploadResult struct {
|
||||
driver.BasicResp
|
||||
Data struct {
|
||||
PickCode string `json:"pick_code"`
|
||||
FileSize int `json:"file_size"`
|
||||
FileID string `json:"file_id"`
|
||||
ThumbURL string `json:"thumb_url"`
|
||||
Sha1 string `json:"sha1"`
|
||||
Aid int `json:"aid"`
|
||||
FileName string `json:"file_name"`
|
||||
Cid string `json:"cid"`
|
||||
IsVideo int `json:"is_video"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
@ -2,13 +2,14 @@ package _115
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -26,29 +27,28 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var UserAgent = driver115.UA115Desktop
|
||||
|
||||
// var UserAgent = driver115.UA115Browser
|
||||
func (d *Pan115) login() error {
|
||||
var err error
|
||||
opts := []driver115.Option{
|
||||
driver115.UA(UserAgent),
|
||||
driver115.UA(d.getUA()),
|
||||
func(c *driver115.Pan115Client) {
|
||||
c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
|
||||
},
|
||||
}
|
||||
d.client = driver115.New(opts...)
|
||||
cr := &driver115.Credential{}
|
||||
if d.Addition.QRCodeToken != "" {
|
||||
if d.QRCodeToken != "" {
|
||||
s := &driver115.QRCodeSession{
|
||||
UID: d.Addition.QRCodeToken,
|
||||
UID: d.QRCodeToken,
|
||||
}
|
||||
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
|
||||
return errors.Wrap(err, "failed to login by qrcode")
|
||||
}
|
||||
d.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
|
||||
d.Addition.QRCodeToken = ""
|
||||
} else if d.Addition.Cookie != "" {
|
||||
if err = cr.FromCookie(d.Addition.Cookie); err != nil {
|
||||
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
|
||||
d.QRCodeToken = ""
|
||||
} else if d.Cookie != "" {
|
||||
if err = cr.FromCookie(d.Cookie); err != nil {
|
||||
return errors.Wrap(err, "failed to login by cookies")
|
||||
}
|
||||
d.client.ImportCredential(cr)
|
||||
@ -73,11 +73,39 @@ func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
const (
|
||||
appVer = "2.0.3.6"
|
||||
)
|
||||
func (d *Pan115) getNewFile(fileId string) (*FileObj, error) {
|
||||
file, err := d.client.GetFile(fileId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FileObj{*file}, nil
|
||||
}
|
||||
|
||||
func (c *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
|
||||
func (d *Pan115) getNewFileByPickCode(pickCode string) (*FileObj, error) {
|
||||
result := driver115.GetFileInfoResponse{}
|
||||
req := d.client.NewRequest().
|
||||
SetQueryParam("pick_code", pickCode).
|
||||
ForceContentType("application/json;charset=UTF-8").
|
||||
SetResult(&result)
|
||||
resp, err := req.Get(driver115.ApiFileInfo)
|
||||
if err := driver115.CheckErr(err, &result, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result.Files) == 0 {
|
||||
return nil, errors.New("not get file info")
|
||||
}
|
||||
fileInfo := result.Files[0]
|
||||
|
||||
f := &FileObj{}
|
||||
f.From(fileInfo)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (d *Pan115) getUA() string {
|
||||
return fmt.Sprintf("Mozilla/5.0 115Browser/%s", appVer)
|
||||
}
|
||||
|
||||
func (d *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
|
||||
key := crypto.GenerateKey()
|
||||
result := driver115.DownloadResp{}
|
||||
params, err := utils.Json.Marshal(map[string]string{"pickcode": pickCode})
|
||||
@ -91,10 +119,10 @@ func (c *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, e
|
||||
reqUrl := fmt.Sprintf("%s?t=%s", driver115.ApiDownloadGetUrl, driver115.Now().String())
|
||||
req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Cookie", c.Cookie)
|
||||
req.Header.Set("Cookie", d.Cookie)
|
||||
req.Header.Set("User-Agent", ua)
|
||||
|
||||
resp, err := c.client.Client.GetClient().Do(req)
|
||||
resp, err := d.client.Client.GetClient().Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -132,6 +160,13 @@ func (c *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, e
|
||||
return nil, driver115.ErrUnexpected
|
||||
}
|
||||
|
||||
func (c *Pan115) GenerateToken(fileID, preID, timeStamp, fileSize, signKey, signVal string) string {
|
||||
userID := strconv.FormatInt(c.client.UserID, 10)
|
||||
userIDMd5 := md5.Sum([]byte(userID))
|
||||
tokenMd5 := md5.Sum([]byte(md5Salt + fileID + fileSize + signKey + signVal + userID + timeStamp + hex.EncodeToString(userIDMd5[:]) + appVer))
|
||||
return hex.EncodeToString(tokenMd5[:])
|
||||
}
|
||||
|
||||
func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) {
|
||||
var (
|
||||
ecdhCipher *cipher.EcdhCipher
|
||||
@ -161,7 +196,7 @@ func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID stri
|
||||
|
||||
signKey, signVal := "", ""
|
||||
for retry := true; retry; {
|
||||
t := driver115.Now()
|
||||
t := driver115.NowMilli()
|
||||
|
||||
if encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil {
|
||||
return nil, err
|
||||
@ -172,7 +207,7 @@ func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID stri
|
||||
}
|
||||
|
||||
form.Set("t", t.String())
|
||||
form.Set("token", d.client.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))
|
||||
form.Set("token", d.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))
|
||||
if signKey != "" && signVal != "" {
|
||||
form.Set("sign_key", signKey)
|
||||
form.Set("sign_val", signVal)
|
||||
@ -225,6 +260,9 @@ func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result stri
|
||||
|
||||
length := end - start + 1
|
||||
reader, err := stream.RangeRead(http_range.Range{Start: start, Length: length})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hashStr, err := utils.HashReader(utils.SHA1, reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -233,8 +271,38 @@ func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result stri
|
||||
return
|
||||
}
|
||||
|
||||
// UploadByOSS use aliyun sdk to upload
|
||||
func (c *Pan115) UploadByOSS(params *driver115.UploadOSSParams, r io.Reader, dirID string) (*UploadResult, error) {
|
||||
ossToken, err := c.client.GetOSSToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ossClient, err := oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bucket, err := ossClient.Bucket(params.Bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var bodyBytes []byte
|
||||
if err = bucket.PutObject(params.Object, r, append(
|
||||
driver115.OssOption(params, ossToken),
|
||||
oss.CallbackResult(&bodyBytes),
|
||||
)...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var uploadResult UploadResult
|
||||
if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &uploadResult, uploadResult.Err(string(bodyBytes))
|
||||
}
|
||||
|
||||
// UploadByMultipart upload by mutipart blocks
|
||||
func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize int64, stream model.FileStreamer, dirID string, opts ...driver115.UploadMultipartOption) error {
|
||||
func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize int64, stream model.FileStreamer, dirID string, opts ...driver115.UploadMultipartOption) (*UploadResult, error) {
|
||||
var (
|
||||
chunks []oss.FileChunk
|
||||
parts []oss.UploadPart
|
||||
@ -242,12 +310,13 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
|
||||
ossClient *oss.Client
|
||||
bucket *oss.Bucket
|
||||
ossToken *driver115.UploadOSSTokenResp
|
||||
bodyBytes []byte
|
||||
err error
|
||||
)
|
||||
|
||||
tmpF, err := stream.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
options := driver115.DefalutUploadMultipartOptions()
|
||||
@ -256,17 +325,19 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
|
||||
f(options)
|
||||
}
|
||||
}
|
||||
// oss 启用Sequential必须按顺序上传
|
||||
options.ThreadsNum = 1
|
||||
|
||||
if ossToken, err = d.client.GetOSSToken(); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret); err != nil {
|
||||
return err
|
||||
if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bucket, err = ossClient.Bucket(params.Bucket); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ossToken一小时后就会失效,所以每50分钟重新获取一次
|
||||
@ -276,14 +347,15 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
|
||||
timeout := time.NewTimer(options.Timeout)
|
||||
|
||||
if chunks, err = SplitFile(fileSize); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if imur, err = bucket.InitiateMultipartUpload(params.Object,
|
||||
oss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken),
|
||||
oss.UserAgentHeader(driver115.OSSUserAgent),
|
||||
oss.EnableSha1(), oss.Sequential(),
|
||||
); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
@ -325,8 +397,7 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
|
||||
continue
|
||||
}
|
||||
|
||||
b := bytes.NewBuffer(buf)
|
||||
if part, err = bucket.UploadPart(imur, b, chunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {
|
||||
if part, err = bucket.UploadPart(imur, bytes.NewBuffer(buf), chunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -350,51 +421,38 @@ LOOP:
|
||||
case <-ticker.C:
|
||||
// 到时重新获取ossToken
|
||||
if ossToken, err = d.client.GetOSSToken(); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
case <-quit:
|
||||
break LOOP
|
||||
case <-errCh:
|
||||
return err
|
||||
return nil, err
|
||||
case <-timeout.C:
|
||||
return fmt.Errorf("time out")
|
||||
return nil, fmt.Errorf("time out")
|
||||
}
|
||||
}
|
||||
|
||||
// EOF错误是xml的Unmarshal导致的,响应其实是json格式,所以实际上上传是成功的
|
||||
if _, err = bucket.CompleteMultipartUpload(imur, parts, driver115.OssOption(params, ossToken)...); err != nil && !errors.Is(err, io.EOF) {
|
||||
// 当文件名含有 &< 这两个字符之一时响应的xml解析会出现错误,实际上上传是成功的
|
||||
if filename := filepath.Base(stream.GetName()); !strings.ContainsAny(filename, "&<") {
|
||||
return err
|
||||
}
|
||||
// 不知道啥原因,oss那边分片上传不计算sha1,导致115服务器校验错误
|
||||
// params.Callback.Callback = strings.ReplaceAll(params.Callback.Callback, "${sha1}", params.SHA1)
|
||||
if _, err := bucket.CompleteMultipartUpload(imur, parts, append(
|
||||
driver115.OssOption(params, ossToken),
|
||||
oss.CallbackResult(&bodyBytes),
|
||||
)...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.checkUploadStatus(dirID, params.SHA1)
|
||||
|
||||
var uploadResult UploadResult
|
||||
if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &uploadResult, uploadResult.Err(string(bodyBytes))
|
||||
}
|
||||
|
||||
func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
|
||||
for _, chunk := range chunks {
|
||||
ch <- chunk
|
||||
}
|
||||
}
|
||||
func (d *Pan115) checkUploadStatus(dirID, sha1 string) error {
|
||||
// 验证上传是否成功
|
||||
req := d.client.NewRequest().ForceContentType("application/json;charset=UTF-8")
|
||||
opts := []driver115.GetFileOptions{
|
||||
driver115.WithOrder(driver115.FileOrderByTime),
|
||||
driver115.WithShowDirEnable(false),
|
||||
driver115.WithAsc(false),
|
||||
driver115.WithLimit(500),
|
||||
}
|
||||
fResp, err := driver115.GetFiles(req, dirID, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fileInfo := range fResp.Files {
|
||||
if fileInfo.Sha1 == sha1 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return driver115.ErrUploadFailed
|
||||
}
|
||||
|
||||
func SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {
|
||||
for i := int64(1); i < 10; i++ {
|
||||
@ -431,8 +489,8 @@ func SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {
|
||||
}
|
||||
|
||||
var chunks []oss.FileChunk
|
||||
var chunk = oss.FileChunk{}
|
||||
var chunkN = (int64)(chunkNum)
|
||||
chunk := oss.FileChunk{}
|
||||
chunkN := (int64)(chunkNum)
|
||||
for i := int64(0); i < chunkN; i++ {
|
||||
chunk.Number = int(i + 1)
|
||||
chunk.Offset = i * (fileSize / chunkN)
|
||||
@ -454,13 +512,13 @@ func SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, erro
|
||||
return nil, errors.New("chunkSize invalid")
|
||||
}
|
||||
|
||||
var chunkN = fileSize / chunkSize
|
||||
chunkN := fileSize / chunkSize
|
||||
if chunkN >= 10000 {
|
||||
return nil, errors.New("Too many parts, please increase part size")
|
||||
}
|
||||
|
||||
var chunks []oss.FileChunk
|
||||
var chunk = oss.FileChunk{}
|
||||
chunk := oss.FileChunk{}
|
||||
for i := int64(0); i < chunkN; i++ {
|
||||
chunk.Number = int(i + 1)
|
||||
chunk.Offset = i * chunkSize
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
type Addition struct {
|
||||
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
|
||||
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
|
||||
QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,linux,mac,windows,tv" default:"linux" help:"select the QR code device, default linux"`
|
||||
PageSize int64 `json:"page_size" type:"number" default:"20" help:"list api per page size of 115 driver"`
|
||||
QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,tv,alipaymini,wechatmini,qandroid" default:"linux" help:"select the QR code device, default linux"`
|
||||
PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
|
||||
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
|
||||
ShareCode string `json:"share_code" type:"text" required:"true" help:"share code of 115 share link"`
|
||||
ReceiveCode string `json:"receive_code" type:"text" required:"true" help:"receive code of 115 share link"`
|
||||
|
@ -96,7 +96,7 @@ func (d *Pan115Share) login() error {
|
||||
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
|
||||
return errors.Wrap(err, "failed to login by qrcode")
|
||||
}
|
||||
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
|
||||
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
|
||||
d.QRCodeToken = ""
|
||||
} else if d.Cookie != "" {
|
||||
if err = cr.FromCookie(d.Cookie); err != nil {
|
||||
|
@ -53,7 +53,7 @@ func (d *Pan123) Drop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files, err := d.getFiles(dir.GetID(), dir.GetName())
|
||||
files, err := d.getFiles(ctx, dir.GetID(), dir.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -82,6 +82,7 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
"type": f.Type,
|
||||
}
|
||||
resp, err := d.request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
|
||||
|
||||
req.SetBody(data).SetHeaders(headers)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
@ -247,9 +248,6 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
}
|
||||
_, err = uploader.UploadWithContext(ctx, input)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(UploadComplete, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"fileId": resp.Data.FileId,
|
||||
@ -258,11 +256,12 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Pan123) APIRateLimit(api string) bool {
|
||||
limiter, _ := d.apiRateLimit.LoadOrStore(api,
|
||||
rate.NewLimiter(rate.Every(time.Millisecond*700), 1))
|
||||
ins := limiter.(*rate.Limiter)
|
||||
return ins.Allow()
|
||||
func (d *Pan123) APIRateLimit(ctx context.Context, api string) error {
|
||||
value, _ := d.apiRateLimit.LoadOrStore(api,
|
||||
rate.NewLimiter(rate.Every(700*time.Millisecond), 1))
|
||||
limiter := value.(*rate.Limiter)
|
||||
|
||||
return limiter.Wait(ctx)
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Pan123)(nil)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package _123
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
@ -14,7 +15,7 @@ import (
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
resty "github.com/go-resty/resty/v2"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -25,8 +26,9 @@ const (
|
||||
Api = "https://www.123pan.com/api"
|
||||
AApi = "https://www.123pan.com/a/api"
|
||||
BApi = "https://www.123pan.com/b/api"
|
||||
LoginApi = "https://login.123pan.com/api"
|
||||
MainApi = BApi
|
||||
SignIn = MainApi + "/user/sign_in"
|
||||
SignIn = LoginApi + "/user/sign_in"
|
||||
Logout = MainApi + "/user/logout"
|
||||
UserInfo = MainApi + "/user/info"
|
||||
FileList = MainApi + "/file/list/new"
|
||||
@ -233,15 +235,14 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (d *Pan123) getFiles(parentId string, name string) ([]File, error) {
|
||||
func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]File, error) {
|
||||
page := 1
|
||||
total := 0
|
||||
res := make([]File, 0)
|
||||
// 2024-02-06 fix concurrency by 123pan
|
||||
for {
|
||||
if !d.APIRateLimit(FileList) {
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
continue
|
||||
if err := d.APIRateLimit(ctx, FileList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp Files
|
||||
query := map[string]string{
|
||||
|
@ -45,7 +45,7 @@ func (d *Pan123Share) Drop(ctx context.Context) error {
|
||||
|
||||
func (d *Pan123Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
// TODO return the files list, required
|
||||
files, err := d.getFiles(dir.GetID())
|
||||
files, err := d.getFiles(ctx, dir.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -150,11 +150,12 @@ 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()
|
||||
func (d *Pan123Share) APIRateLimit(ctx context.Context, api string) error {
|
||||
value, _ := d.apiRateLimit.LoadOrStore(api,
|
||||
rate.NewLimiter(rate.Every(700*time.Millisecond), 1))
|
||||
limiter := value.(*rate.Limiter)
|
||||
|
||||
return limiter.Wait(ctx)
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Pan123Share)(nil)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package _123Share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
@ -80,13 +81,12 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (d *Pan123Share) getFiles(parentId string) ([]File, error) {
|
||||
func (d *Pan123Share) getFiles(ctx context.Context, parentId string) ([]File, error) {
|
||||
page := 1
|
||||
res := make([]File, 0)
|
||||
for {
|
||||
if !d.APIRateLimit(FileList) {
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
continue
|
||||
if err := d.APIRateLimit(ctx, FileList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp Files
|
||||
query := map[string]string{
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -14,15 +15,16 @@ import (
|
||||
"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"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Yun139 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
cron *cron.Cron
|
||||
cron *cron.Cron
|
||||
Account string
|
||||
}
|
||||
|
||||
@ -56,6 +58,11 @@ func (d *Yun139) Init(ctx context.Context) error {
|
||||
d.RootFolderID = "root"
|
||||
}
|
||||
fallthrough
|
||||
case MetaGroup:
|
||||
if len(d.Addition.RootFolderID) == 0 {
|
||||
d.RootFolderID = d.CloudID
|
||||
}
|
||||
fallthrough
|
||||
case MetaFamily:
|
||||
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
|
||||
if err != nil {
|
||||
@ -96,6 +103,8 @@ func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
|
||||
return d.getFiles(dir.GetID())
|
||||
case MetaFamily:
|
||||
return d.familyGetFiles(dir.GetID())
|
||||
case MetaGroup:
|
||||
return d.groupGetFiles(dir.GetID())
|
||||
default:
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
@ -108,9 +117,11 @@ func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
case MetaPersonalNew:
|
||||
url, err = d.personalGetLink(file.GetID())
|
||||
case MetaPersonal:
|
||||
fallthrough
|
||||
case MetaFamily:
|
||||
url, err = d.getLink(file.GetID())
|
||||
case MetaFamily:
|
||||
url, err = d.familyGetLink(file.GetID(), file.GetPath())
|
||||
case MetaGroup:
|
||||
url, err = d.groupGetLink(file.GetID(), file.GetPath())
|
||||
default:
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
@ -154,8 +165,22 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
|
||||
"accountType": 1,
|
||||
},
|
||||
"docLibName": dirName,
|
||||
"path": path.Join(parentDir.GetPath(), parentDir.GetID()),
|
||||
}
|
||||
pathname := "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc"
|
||||
pathname := "/orchestration/familyCloud-rebuild/cloudCatalog/v1.0/createCloudDoc"
|
||||
_, err = d.post(pathname, data, nil)
|
||||
case MetaGroup:
|
||||
data := base.Json{
|
||||
"catalogName": dirName,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
"groupID": d.CloudID,
|
||||
"parentFileId": parentDir.GetID(),
|
||||
"path": path.Join(parentDir.GetPath(), parentDir.GetID()),
|
||||
}
|
||||
pathname := "/orchestration/group-rebuild/catalog/v1.0/createGroupCatalog"
|
||||
_, err = d.post(pathname, data, nil)
|
||||
default:
|
||||
err = errs.NotImplement
|
||||
@ -176,6 +201,34 @@ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj,
|
||||
return nil, err
|
||||
}
|
||||
return srcObj, nil
|
||||
case MetaGroup:
|
||||
var contentList []string
|
||||
var catalogList []string
|
||||
if srcObj.IsDir() {
|
||||
catalogList = append(catalogList, srcObj.GetID())
|
||||
} else {
|
||||
contentList = append(contentList, srcObj.GetID())
|
||||
}
|
||||
data := base.Json{
|
||||
"taskType": 3,
|
||||
"srcType": 2,
|
||||
"srcGroupID": d.CloudID,
|
||||
"destType": 2,
|
||||
"destGroupID": d.CloudID,
|
||||
"destPath": dstDir.GetPath(),
|
||||
"contentList": contentList,
|
||||
"catalogList": catalogList,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/group-rebuild/task/v1.0/createBatchOprTask"
|
||||
_, err := d.post(pathname, data, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return srcObj, nil
|
||||
case MetaPersonal:
|
||||
var contentInfoList []string
|
||||
var catalogInfoList []string
|
||||
@ -246,6 +299,65 @@ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) e
|
||||
pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo"
|
||||
}
|
||||
_, err = d.post(pathname, data, nil)
|
||||
case MetaGroup:
|
||||
var data base.Json
|
||||
var pathname string
|
||||
if srcObj.IsDir() {
|
||||
data = base.Json{
|
||||
"groupID": d.CloudID,
|
||||
"modifyCatalogID": srcObj.GetID(),
|
||||
"modifyCatalogName": newName,
|
||||
"path": srcObj.GetPath(),
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname = "/orchestration/group-rebuild/catalog/v1.0/modifyGroupCatalog"
|
||||
} else {
|
||||
data = base.Json{
|
||||
"groupID": d.CloudID,
|
||||
"contentID": srcObj.GetID(),
|
||||
"contentName": newName,
|
||||
"path": srcObj.GetPath(),
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname = "/orchestration/group-rebuild/content/v1.0/modifyGroupContent"
|
||||
}
|
||||
_, err = d.post(pathname, data, nil)
|
||||
case MetaFamily:
|
||||
var data base.Json
|
||||
var pathname string
|
||||
if srcObj.IsDir() {
|
||||
// 网页接口不支持重命名家庭云文件夹
|
||||
// data = base.Json{
|
||||
// "catalogType": 3,
|
||||
// "catalogID": srcObj.GetID(),
|
||||
// "catalogName": newName,
|
||||
// "commonAccountInfo": base.Json{
|
||||
// "account": d.Account,
|
||||
// "accountType": 1,
|
||||
// },
|
||||
// "path": srcObj.GetPath(),
|
||||
// }
|
||||
// pathname = "/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyCatalogInfo"
|
||||
return errs.NotImplement
|
||||
} else {
|
||||
data = base.Json{
|
||||
"contentID": srcObj.GetID(),
|
||||
"contentName": newName,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
"path": srcObj.GetPath(),
|
||||
}
|
||||
pathname = "/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyContentInfo"
|
||||
}
|
||||
_, err = d.post(pathname, data, nil)
|
||||
default:
|
||||
err = errs.NotImplement
|
||||
}
|
||||
@ -303,6 +415,28 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
|
||||
pathname := "/hcy/recyclebin/batchTrash"
|
||||
_, err := d.personalPost(pathname, data, nil)
|
||||
return err
|
||||
case MetaGroup:
|
||||
var contentList []string
|
||||
var catalogList []string
|
||||
// 必须使用完整路径删除
|
||||
if obj.IsDir() {
|
||||
catalogList = append(catalogList, obj.GetPath())
|
||||
} else {
|
||||
contentList = append(contentList, path.Join(obj.GetPath(), obj.GetID()))
|
||||
}
|
||||
data := base.Json{
|
||||
"taskType": 2,
|
||||
"srcGroupID": d.CloudID,
|
||||
"contentList": contentList,
|
||||
"catalogList": catalogList,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/group-rebuild/task/v1.0/createBatchOprTask"
|
||||
_, err := d.post(pathname, data, nil)
|
||||
return err
|
||||
case MetaPersonal:
|
||||
fallthrough
|
||||
case MetaFamily:
|
||||
@ -337,10 +471,12 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
"sourceCloudID": d.CloudID,
|
||||
"sourceCatalogType": 1002,
|
||||
"taskType": 2,
|
||||
"path": obj.GetPath(),
|
||||
}
|
||||
pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask"
|
||||
pathname = "/orchestration/familyCloud-rebuild/batchOprTask/v1.0/createBatchOprTask"
|
||||
}
|
||||
_, err := d.post(pathname, data, nil)
|
||||
return err
|
||||
@ -357,7 +493,10 @@ const (
|
||||
TB
|
||||
)
|
||||
|
||||
func getPartSize(size int64) int64 {
|
||||
func (d *Yun139) getPartSize(size int64) int64 {
|
||||
if d.CustomUploadPartSize != 0 {
|
||||
return d.CustomUploadPartSize
|
||||
}
|
||||
// 网盘对于分片数量存在上限
|
||||
if size/GB > 30 {
|
||||
return 512 * MB
|
||||
@ -380,24 +519,51 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
return err
|
||||
}
|
||||
}
|
||||
// return errs.NotImplement
|
||||
|
||||
partInfos := []PartInfo{}
|
||||
var partSize = d.getPartSize(stream.GetSize())
|
||||
part := (stream.GetSize() + partSize - 1) / partSize
|
||||
if part == 0 {
|
||||
part = 1
|
||||
}
|
||||
for i := int64(0); i < part; i++ {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
start := i * partSize
|
||||
byteSize := stream.GetSize() - start
|
||||
if byteSize > partSize {
|
||||
byteSize = partSize
|
||||
}
|
||||
partNumber := i + 1
|
||||
partInfo := PartInfo{
|
||||
PartNumber: partNumber,
|
||||
PartSize: byteSize,
|
||||
ParallelHashCtx: ParallelHashCtx{
|
||||
PartOffset: start,
|
||||
},
|
||||
}
|
||||
partInfos = append(partInfos, partInfo)
|
||||
}
|
||||
|
||||
// 筛选出前 100 个 partInfos
|
||||
firstPartInfos := partInfos
|
||||
if len(firstPartInfos) > 100 {
|
||||
firstPartInfos = firstPartInfos[:100]
|
||||
}
|
||||
|
||||
// 获取上传信息和前100个分片的上传地址
|
||||
data := base.Json{
|
||||
"contentHash": fullHash,
|
||||
"contentHashAlgorithm": "SHA256",
|
||||
"contentType": "application/octet-stream",
|
||||
"parallelUpload": false,
|
||||
"partInfos": []base.Json{{
|
||||
"parallelHashCtx": base.Json{
|
||||
"partOffset": 0,
|
||||
},
|
||||
"partNumber": 1,
|
||||
"partSize": stream.GetSize(),
|
||||
}},
|
||||
"size": stream.GetSize(),
|
||||
"parentFileId": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"type": "file",
|
||||
"fileRenameMode": "auto_rename",
|
||||
"partInfos": firstPartInfos,
|
||||
"size": stream.GetSize(),
|
||||
"parentFileId": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"type": "file",
|
||||
"fileRenameMode": "auto_rename",
|
||||
}
|
||||
pathname := "/hcy/file/create"
|
||||
var resp PersonalUploadResp
|
||||
@ -410,32 +576,67 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
return nil
|
||||
}
|
||||
|
||||
uploadPartInfos := resp.Data.PartInfos
|
||||
|
||||
// 获取后续分片的上传地址
|
||||
for i := 101; i < len(partInfos); i += 100 {
|
||||
end := i + 100
|
||||
if end > len(partInfos) {
|
||||
end = len(partInfos)
|
||||
}
|
||||
batchPartInfos := partInfos[i:end]
|
||||
|
||||
moredata := base.Json{
|
||||
"fileId": resp.Data.FileId,
|
||||
"uploadId": resp.Data.UploadId,
|
||||
"partInfos": batchPartInfos,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": d.Account,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname := "/hcy/file/getUploadUrl"
|
||||
var moreresp PersonalUploadUrlResp
|
||||
_, err = d.personalPost(pathname, moredata, &moreresp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadPartInfos = append(uploadPartInfos, moreresp.Data.PartInfos...)
|
||||
}
|
||||
|
||||
// Progress
|
||||
p := driver.NewProgress(stream.GetSize(), up)
|
||||
|
||||
// Update Progress
|
||||
r := io.TeeReader(stream, p)
|
||||
// 上传所有分片
|
||||
for _, uploadPartInfo := range uploadPartInfos {
|
||||
index := uploadPartInfo.PartNumber - 1
|
||||
partSize := partInfos[index].PartSize
|
||||
log.Debugf("[139] uploading part %+v/%+v", index, len(uploadPartInfos))
|
||||
limitReader := io.LimitReader(stream, partSize)
|
||||
|
||||
req, err := http.NewRequest("PUT", resp.Data.PartInfos[0].UploadUrl, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Length", fmt.Sprint(stream.GetSize()))
|
||||
req.Header.Set("Origin", "https://yun.139.com")
|
||||
req.Header.Set("Referer", "https://yun.139.com/")
|
||||
req.ContentLength = stream.GetSize()
|
||||
// Update Progress
|
||||
r := io.TeeReader(limitReader, p)
|
||||
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("PUT", uploadPartInfo.UploadUrl, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Length", fmt.Sprint(partSize))
|
||||
req.Header.Set("Origin", "https://yun.139.com")
|
||||
req.Header.Set("Referer", "https://yun.139.com/")
|
||||
req.ContentLength = partSize
|
||||
|
||||
_ = res.Body.Close()
|
||||
log.Debugf("%+v", res)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = res.Body.Close()
|
||||
log.Debugf("[139] uploaded: %+v", res)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
data = base.Json{
|
||||
@ -471,21 +672,20 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
|
||||
if d.isFamily() {
|
||||
// data = d.newJson(base.Json{
|
||||
// "fileCount": 1,
|
||||
// "manualRename": 2,
|
||||
// "operation": 0,
|
||||
// "path": "",
|
||||
// "seqNo": "",
|
||||
// "totalSize": 0,
|
||||
// "uploadContentList": []base.Json{{
|
||||
// "contentName": stream.GetName(),
|
||||
// "contentSize": 0,
|
||||
// // "digest": "5a3231986ce7a6b46e408612d385bafa"
|
||||
// }},
|
||||
// })
|
||||
// pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL"
|
||||
return errs.NotImplement
|
||||
data = d.newJson(base.Json{
|
||||
"fileCount": 1,
|
||||
"manualRename": 2,
|
||||
"operation": 0,
|
||||
"path": path.Join(dstDir.GetPath(), dstDir.GetID()),
|
||||
"seqNo": random.String(32), //序列号不能为空
|
||||
"totalSize": 0,
|
||||
"uploadContentList": []base.Json{{
|
||||
"contentName": stream.GetName(),
|
||||
"contentSize": 0,
|
||||
// "digest": "5a3231986ce7a6b46e408612d385bafa"
|
||||
}},
|
||||
})
|
||||
pathname = "/orchestration/familyCloud-rebuild/content/v1.0/getFileUploadURL"
|
||||
}
|
||||
var resp UploadResp
|
||||
_, err := d.post(pathname, data, &resp)
|
||||
@ -496,7 +696,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
// Progress
|
||||
p := driver.NewProgress(stream.GetSize(), up)
|
||||
|
||||
var partSize = getPartSize(stream.GetSize())
|
||||
var partSize = d.getPartSize(stream.GetSize())
|
||||
part := (stream.GetSize() + partSize - 1) / partSize
|
||||
if part == 0 {
|
||||
part = 1
|
||||
|
@ -9,8 +9,9 @@ type Addition struct {
|
||||
//Account string `json:"account" required:"true"`
|
||||
Authorization string `json:"authorization" type:"text" required:"true"`
|
||||
driver.RootID
|
||||
Type string `json:"type" type:"select" options:"personal,family,personal_new" default:"personal"`
|
||||
CloudID string `json:"cloud_id"`
|
||||
Type string `json:"type" type:"select" options:"personal,family,personal_new" default:"personal"`
|
||||
CloudID string `json:"cloud_id"`
|
||||
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
const (
|
||||
MetaPersonal string = "personal"
|
||||
MetaFamily string = "family"
|
||||
MetaGroup string = "group"
|
||||
MetaPersonalNew string = "personal_new"
|
||||
)
|
||||
|
||||
@ -54,6 +55,7 @@ type Content struct {
|
||||
//ContentDesc string `json:"contentDesc"`
|
||||
//ContentType int `json:"contentType"`
|
||||
//ContentOrigin int `json:"contentOrigin"`
|
||||
CreateTime string `json:"createTime"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
//CommentCount int `json:"commentCount"`
|
||||
ThumbnailURL string `json:"thumbnailURL"`
|
||||
@ -196,6 +198,37 @@ type QueryContentListResp struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type QueryGroupContentListResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Result struct {
|
||||
ResultCode string `json:"resultCode"`
|
||||
ResultDesc string `json:"resultDesc"`
|
||||
} `json:"result"`
|
||||
GetGroupContentResult struct {
|
||||
ParentCatalogID string `json:"parentCatalogID"` // 根目录是"0"
|
||||
CatalogList []struct {
|
||||
Catalog
|
||||
Path string `json:"path"`
|
||||
} `json:"catalogList"`
|
||||
ContentList []Content `json:"contentList"`
|
||||
NodeCount int `json:"nodeCount"` // 文件+文件夹数量
|
||||
CtlgCnt int `json:"ctlgCnt"` // 文件夹数量
|
||||
ContCnt int `json:"contCnt"` // 文件数量
|
||||
} `json:"getGroupContentResult"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ParallelHashCtx struct {
|
||||
PartOffset int64 `json:"partOffset"`
|
||||
}
|
||||
|
||||
type PartInfo struct {
|
||||
PartNumber int64 `json:"partNumber"`
|
||||
PartSize int64 `json:"partSize"`
|
||||
ParallelHashCtx ParallelHashCtx `json:"parallelHashCtx"`
|
||||
}
|
||||
|
||||
type PersonalThumbnail struct {
|
||||
Style string `json:"style"`
|
||||
Url string `json:"url"`
|
||||
@ -235,6 +268,15 @@ type PersonalUploadResp struct {
|
||||
}
|
||||
}
|
||||
|
||||
type PersonalUploadUrlResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
FileId string `json:"fileId"`
|
||||
UploadId string `json:"uploadId"`
|
||||
PartInfos []PersonalPartInfo `json:"partInfos"`
|
||||
}
|
||||
}
|
||||
|
||||
type RefreshTokenResp struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Return string `xml:"return"`
|
||||
|
@ -13,9 +13,9 @@ import (
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/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"
|
||||
@ -220,10 +220,11 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
||||
"sortDirection": 1,
|
||||
})
|
||||
var resp QueryContentListResp
|
||||
_, err := d.post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp)
|
||||
_, err := d.post("/orchestration/familyCloud-rebuild/content/v1.2/queryContentList", data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := resp.Data.Path
|
||||
for _, catalog := range resp.Data.CloudCatalogList {
|
||||
f := model.Object{
|
||||
ID: catalog.CatalogID,
|
||||
@ -232,6 +233,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
||||
IsFolder: true,
|
||||
Modified: getTime(catalog.LastUpdateTime),
|
||||
Ctime: getTime(catalog.CreateTime),
|
||||
Path: path, // 文件夹上一级的Path
|
||||
}
|
||||
files = append(files, &f)
|
||||
}
|
||||
@ -243,6 +245,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
||||
Size: content.ContentSize,
|
||||
Modified: getTime(content.LastUpdateTime),
|
||||
Ctime: getTime(content.CreateTime),
|
||||
Path: path, // 文件所在目录的Path
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
||||
//Thumbnail: content.BigthumbnailURL,
|
||||
@ -257,6 +260,61 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *Yun139) groupGetFiles(catalogID string) ([]model.Obj, error) {
|
||||
pageNum := 1
|
||||
files := make([]model.Obj, 0)
|
||||
for {
|
||||
data := d.newJson(base.Json{
|
||||
"groupID": d.CloudID,
|
||||
"catalogID": catalogID,
|
||||
"contentSortType": 0,
|
||||
"sortDirection": 1,
|
||||
"startNumber": pageNum,
|
||||
"endNumber": pageNum + 99,
|
||||
"path": catalogID,
|
||||
})
|
||||
|
||||
var resp QueryGroupContentListResp
|
||||
_, err := d.post("/orchestration/group-rebuild/content/v1.0/queryGroupContentList", data, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := resp.Data.GetGroupContentResult.ParentCatalogID
|
||||
for _, catalog := range resp.Data.GetGroupContentResult.CatalogList {
|
||||
f := model.Object{
|
||||
ID: catalog.CatalogID,
|
||||
Name: catalog.CatalogName,
|
||||
Size: 0,
|
||||
IsFolder: true,
|
||||
Modified: getTime(catalog.UpdateTime),
|
||||
Ctime: getTime(catalog.CreateTime),
|
||||
Path: catalog.Path, // 文件夹的真实Path, root:/开头
|
||||
}
|
||||
files = append(files, &f)
|
||||
}
|
||||
for _, content := range resp.Data.GetGroupContentResult.ContentList {
|
||||
f := model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: content.ContentID,
|
||||
Name: content.ContentName,
|
||||
Size: content.ContentSize,
|
||||
Modified: getTime(content.UpdateTime),
|
||||
Ctime: getTime(content.CreateTime),
|
||||
Path: path, // 文件所在目录的Path
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
||||
//Thumbnail: content.BigthumbnailURL,
|
||||
}
|
||||
files = append(files, &f)
|
||||
}
|
||||
if pageNum > resp.Data.GetGroupContentResult.NodeCount {
|
||||
break
|
||||
}
|
||||
pageNum = pageNum + 100
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *Yun139) getLink(contentId string) (string, error) {
|
||||
data := base.Json{
|
||||
"appName": "",
|
||||
@ -273,6 +331,32 @@ func (d *Yun139) getLink(contentId string) (string, error) {
|
||||
}
|
||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||
}
|
||||
func (d *Yun139) familyGetLink(contentId string, path string) (string, error) {
|
||||
data := d.newJson(base.Json{
|
||||
"contentID": contentId,
|
||||
"path": path,
|
||||
})
|
||||
res, err := d.post("/orchestration/familyCloud-rebuild/content/v1.0/getFileDownLoadURL",
|
||||
data, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||
}
|
||||
|
||||
func (d *Yun139) groupGetLink(contentId string, path string) (string, error) {
|
||||
data := d.newJson(base.Json{
|
||||
"contentID": contentId,
|
||||
"groupID": d.CloudID,
|
||||
"path": path,
|
||||
})
|
||||
res, err := d.post("/orchestration/group-rebuild/groupManage/v1.0/getGroupFileDownLoadURL",
|
||||
data, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||
}
|
||||
|
||||
func unicode(str string) string {
|
||||
textQuoted := strconv.QuoteToASCII(str)
|
||||
|
@ -114,17 +114,19 @@ func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, para
|
||||
if err = y.refreshSession(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return y.request(url, method, callback, params, resp)
|
||||
return y.request(url, method, callback, params, resp, isFamily...)
|
||||
}
|
||||
|
||||
// if erron.ErrorCode == "InvalidSessionKey" || erron.Code == "InvalidSessionKey" {
|
||||
if strings.Contains(res.String(), "InvalidSessionKey") {
|
||||
if err = y.refreshSession(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return y.request(url, method, callback, params, resp, isFamily...)
|
||||
}
|
||||
|
||||
// 处理错误
|
||||
if erron.HasError() {
|
||||
if erron.ErrorCode == "InvalidSessionKey" {
|
||||
if err = y.refreshSession(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return y.request(url, method, callback, params, resp)
|
||||
}
|
||||
return nil, &erron
|
||||
}
|
||||
return res.Body(), nil
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
DriveType string `json:"drive_type" type:"select" options:"default,resource,backup" default:"default"`
|
||||
DriveType string `json:"drive_type" type:"select" options:"default,resource,backup" default:"resource"`
|
||||
driver.RootID
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
||||
|
@ -22,13 +22,16 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
|
||||
_ "github.com/alist-org/alist/v3/drivers/crypt"
|
||||
_ "github.com/alist-org/alist/v3/drivers/dropbox"
|
||||
_ "github.com/alist-org/alist/v3/drivers/febbox"
|
||||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
||||
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
||||
_ "github.com/alist-org/alist/v3/drivers/google_photo"
|
||||
_ "github.com/alist-org/alist/v3/drivers/halalcloud"
|
||||
_ "github.com/alist-org/alist/v3/drivers/ilanzou"
|
||||
_ "github.com/alist-org/alist/v3/drivers/ipfs_api"
|
||||
_ "github.com/alist-org/alist/v3/drivers/kodbox"
|
||||
_ "github.com/alist-org/alist/v3/drivers/lanzou"
|
||||
_ "github.com/alist-org/alist/v3/drivers/lenovonas_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/local"
|
||||
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
||||
_ "github.com/alist-org/alist/v3/drivers/mega"
|
||||
@ -40,6 +43,7 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/pikpak"
|
||||
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
|
||||
_ "github.com/alist-org/alist/v3/drivers/quark_uc_tv"
|
||||
_ "github.com/alist-org/alist/v3/drivers/quqi"
|
||||
_ "github.com/alist-org/alist/v3/drivers/s3"
|
||||
_ "github.com/alist-org/alist/v3/drivers/seafile"
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
type TokenErrResp struct {
|
||||
@ -55,11 +56,11 @@ func fileToObj(f File) *model.ObjThumb {
|
||||
if f.ServerFilename == "" {
|
||||
f.ServerFilename = path.Base(f.Path)
|
||||
}
|
||||
if f.LocalCtime == 0 {
|
||||
f.LocalCtime = f.Ctime
|
||||
if f.ServerCtime == 0 {
|
||||
f.ServerCtime = f.Ctime
|
||||
}
|
||||
if f.LocalMtime == 0 {
|
||||
f.LocalMtime = f.Mtime
|
||||
if f.ServerMtime == 0 {
|
||||
f.ServerMtime = f.Mtime
|
||||
}
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
@ -67,12 +68,12 @@ func fileToObj(f File) *model.ObjThumb {
|
||||
Path: f.Path,
|
||||
Name: f.ServerFilename,
|
||||
Size: f.Size,
|
||||
Modified: time.Unix(f.LocalMtime, 0),
|
||||
Ctime: time.Unix(f.LocalCtime, 0),
|
||||
Modified: time.Unix(f.ServerMtime, 0),
|
||||
Ctime: time.Unix(f.ServerCtime, 0),
|
||||
IsFolder: f.Isdir == 1,
|
||||
|
||||
// 直接获取的MD5是错误的
|
||||
// HashInfo: utils.NewHashInfo(utils.MD5, f.Md5),
|
||||
HashInfo: utils.NewHashInfo(utils.MD5, DecryptMd5(f.Md5)),
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbs.Url3},
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
package baidu_netdisk
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
@ -153,8 +156,6 @@ func (d *BaiduNetdisk) linkOfficial(file model.Obj, args model.LinkArgs) (*model
|
||||
u = res.Header().Get("location")
|
||||
//}
|
||||
|
||||
updateObjMd5(file, "pan.baidu.com", u)
|
||||
|
||||
return &model.Link{
|
||||
URL: u,
|
||||
Header: http.Header{
|
||||
@ -178,8 +179,6 @@ func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Li
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateObjMd5(file, d.CustomCrackUA, resp.Info[0].Dlink)
|
||||
|
||||
return &model.Link{
|
||||
URL: resp.Info[0].Dlink,
|
||||
Header: http.Header{
|
||||
@ -229,19 +228,6 @@ func joinTime(form map[string]string, ctime, mtime int64) {
|
||||
form["local_ctime"] = strconv.FormatInt(ctime, 10)
|
||||
}
|
||||
|
||||
func updateObjMd5(obj model.Obj, userAgent, u string) {
|
||||
object := model.GetRawObject(obj)
|
||||
if object != nil {
|
||||
req, _ := http.NewRequest(http.MethodHead, u, nil)
|
||||
req.Header.Add("User-Agent", userAgent)
|
||||
resp, _ := base.HttpClient.Do(req)
|
||||
if resp != nil {
|
||||
contentMd5 := resp.Header.Get("Content-Md5")
|
||||
object.HashInfo = utils.NewHashInfo(utils.MD5, contentMd5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultSliceSize int64 = 4 * utils.MB
|
||||
VipSliceSize = 16 * utils.MB
|
||||
@ -267,3 +253,40 @@ func (d *BaiduNetdisk) getSliceSize() int64 {
|
||||
// r = strings.ReplaceAll(r, "+", "%20")
|
||||
// return r
|
||||
// }
|
||||
|
||||
func DecryptMd5(encryptMd5 string) string {
|
||||
if _, err := hex.DecodeString(encryptMd5); err == nil {
|
||||
return encryptMd5
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
out.Grow(len(encryptMd5))
|
||||
for i, n := 0, int64(0); i < len(encryptMd5); i++ {
|
||||
if i == 9 {
|
||||
n = int64(unicode.ToLower(rune(encryptMd5[i])) - 'g')
|
||||
} else {
|
||||
n, _ = strconv.ParseInt(encryptMd5[i:i+1], 16, 64)
|
||||
}
|
||||
out.WriteString(strconv.FormatInt(n^int64(15&i), 16))
|
||||
}
|
||||
|
||||
encryptMd5 = out.String()
|
||||
return encryptMd5[8:16] + encryptMd5[:8] + encryptMd5[24:32] + encryptMd5[16:24]
|
||||
}
|
||||
|
||||
func EncryptMd5(originalMd5 string) string {
|
||||
reversed := originalMd5[8:16] + originalMd5[:8] + originalMd5[24:32] + originalMd5[16:24]
|
||||
|
||||
var out strings.Builder
|
||||
out.Grow(len(reversed))
|
||||
for i, n := 0, int64(0); i < len(reversed); i++ {
|
||||
n, _ = strconv.ParseInt(reversed[i:i+1], 16, 64)
|
||||
n ^= int64(15 & i)
|
||||
if i == 9 {
|
||||
out.WriteRune(rune(n) + 'g')
|
||||
} else {
|
||||
out.WriteString(strconv.FormatInt(n, 16))
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
@ -27,9 +27,9 @@ type BaiduPhoto struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
AccessToken string
|
||||
Uk int64
|
||||
root model.Obj
|
||||
// AccessToken string
|
||||
Uk int64
|
||||
root model.Obj
|
||||
|
||||
uploadThread int
|
||||
}
|
||||
@ -48,9 +48,9 @@ func (d *BaiduPhoto) Init(ctx context.Context) error {
|
||||
d.uploadThread, d.UploadThread = 3, "3"
|
||||
}
|
||||
|
||||
if err := d.refreshToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
// if err := d.refreshToken(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// root
|
||||
if d.AlbumID != "" {
|
||||
@ -82,7 +82,7 @@ func (d *BaiduPhoto) GetRoot(ctx context.Context) (model.Obj, error) {
|
||||
}
|
||||
|
||||
func (d *BaiduPhoto) Drop(ctx context.Context) error {
|
||||
d.AccessToken = ""
|
||||
// d.AccessToken = ""
|
||||
d.Uk = 0
|
||||
d.root = nil
|
||||
return nil
|
||||
@ -137,13 +137,18 @@ func (d *BaiduPhoto) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
||||
case *File:
|
||||
return d.linkFile(ctx, file, args)
|
||||
case *AlbumFile:
|
||||
f, err := d.CopyAlbumFile(ctx, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 处理共享相册
|
||||
if d.Uk != file.Uk {
|
||||
// 有概率无法获取到链接
|
||||
// return d.linkAlbum(ctx, file, args)
|
||||
|
||||
f, err := d.CopyAlbumFile(ctx, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.linkFile(ctx, f, args)
|
||||
}
|
||||
return d.linkFile(ctx, f, args)
|
||||
// 有概率无法获取到链接
|
||||
//return d.linkAlbum(ctx, file, args)
|
||||
return d.linkFile(ctx, &file.File, args)
|
||||
}
|
||||
return nil, errs.NotFile
|
||||
}
|
||||
@ -286,7 +291,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
||||
}
|
||||
|
||||
// 尝试获取之前的进度
|
||||
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5)
|
||||
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, strconv.FormatInt(d.Uk, 10), contentMd5)
|
||||
if !ok {
|
||||
_, err = d.Post(FILE_API_URL_V1+"/precreate", func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
@ -337,7 +342,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
||||
if err = threadG.Wait(); err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
precreateResp.BlockList = utils.SliceFilter(precreateResp.BlockList, func(s int) bool { return s >= 0 })
|
||||
base.SaveUploadProgress(d, precreateResp, d.AccessToken, contentMd5)
|
||||
base.SaveUploadProgress(d, strconv.FormatInt(d.Uk, 10), contentMd5)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
@ -6,13 +6,14 @@ import (
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
ShowType string `json:"show_type" type:"select" options:"root,root_only_album,root_only_file" default:"root"`
|
||||
AlbumID string `json:"album_id"`
|
||||
// RefreshToken string `json:"refresh_token" required:"true"`
|
||||
Cookie string `json:"cookie" required:"true"`
|
||||
ShowType string `json:"show_type" type:"select" options:"root,root_only_album,root_only_file" default:"root"`
|
||||
AlbumID string `json:"album_id"`
|
||||
//AlbumPassword string `json:"album_password"`
|
||||
DeleteOrigin bool `json:"delete_origin"`
|
||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||
DeleteOrigin bool `json:"delete_origin"`
|
||||
// ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||
// ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ func (c *File) Thumb() string {
|
||||
}
|
||||
|
||||
func (c *File) GetHash() utils.HashInfo {
|
||||
return utils.NewHashInfo(utils.MD5, c.Md5)
|
||||
return utils.NewHashInfo(utils.MD5, DecryptMd5(c.Md5))
|
||||
}
|
||||
|
||||
/*相册部分*/
|
||||
|
@ -2,13 +2,15 @@ package baiduphoto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"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/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
@ -21,9 +23,10 @@ const (
|
||||
FILE_API_URL_V2 = API_URL + "/file/v2"
|
||||
)
|
||||
|
||||
func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
||||
req := base.RestyClient.R().
|
||||
SetQueryParam("access_token", d.AccessToken)
|
||||
func (d *BaiduPhoto) Request(client *resty.Client, furl string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
||||
req := client.R().
|
||||
// SetQueryParam("access_token", d.AccessToken)
|
||||
SetHeader("Cookie", d.Cookie)
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
@ -45,10 +48,10 @@ func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallba
|
||||
return nil, fmt.Errorf("no shared albums found")
|
||||
case 50100:
|
||||
return nil, fmt.Errorf("illegal title, only supports 50 characters")
|
||||
case -6:
|
||||
if err = d.refreshToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// case -6:
|
||||
// if err = d.refreshToken(); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
default:
|
||||
return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron)
|
||||
}
|
||||
@ -63,36 +66,36 @@ func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallba
|
||||
// return res.Body(), nil
|
||||
//}
|
||||
|
||||
func (d *BaiduPhoto) refreshToken() error {
|
||||
u := "https://openapi.baidu.com/oauth/2.0/token"
|
||||
var resp base.TokenResp
|
||||
var e TokenErrResp
|
||||
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": d.RefreshToken,
|
||||
"client_id": d.ClientID,
|
||||
"client_secret": d.ClientSecret,
|
||||
}).Get(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ErrorMsg != "" {
|
||||
return &e
|
||||
}
|
||||
if resp.RefreshToken == "" {
|
||||
return errs.EmptyToken
|
||||
}
|
||||
d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
// func (d *BaiduPhoto) refreshToken() error {
|
||||
// u := "https://openapi.baidu.com/oauth/2.0/token"
|
||||
// var resp base.TokenResp
|
||||
// var e TokenErrResp
|
||||
// _, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
|
||||
// "grant_type": "refresh_token",
|
||||
// "refresh_token": d.RefreshToken,
|
||||
// "client_id": d.ClientID,
|
||||
// "client_secret": d.ClientSecret,
|
||||
// }).Get(u)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if e.ErrorMsg != "" {
|
||||
// return &e
|
||||
// }
|
||||
// if resp.RefreshToken == "" {
|
||||
// return errs.EmptyToken
|
||||
// }
|
||||
// d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
|
||||
// op.MustSaveDriverStorage(d)
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (d *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
||||
return d.Request(furl, http.MethodGet, callback, resp)
|
||||
return d.Request(base.RestyClient, furl, http.MethodGet, callback, resp)
|
||||
}
|
||||
|
||||
func (d *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
||||
return d.Request(furl, http.MethodPost, callback, resp)
|
||||
return d.Request(base.RestyClient, furl, http.MethodPost, callback, resp)
|
||||
}
|
||||
|
||||
// 获取所有文件
|
||||
@ -338,24 +341,29 @@ func (d *BaiduPhoto) linkAlbum(ctx context.Context, file *AlbumFile, args model.
|
||||
headers["X-Forwarded-For"] = args.IP
|
||||
}
|
||||
|
||||
res, err := base.NoRedirectClient.R().
|
||||
SetContext(ctx).
|
||||
SetHeaders(headers).
|
||||
SetQueryParams(map[string]string{
|
||||
"access_token": d.AccessToken,
|
||||
"fsid": fmt.Sprint(file.Fsid),
|
||||
"album_id": file.AlbumID,
|
||||
"tid": fmt.Sprint(file.Tid),
|
||||
"uk": fmt.Sprint(file.Uk),
|
||||
}).
|
||||
Head(ALBUM_API_URL + "/download")
|
||||
resp, err := d.Request(base.NoRedirectClient, ALBUM_API_URL+"/download", http.MethodHead, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetHeaders(headers)
|
||||
r.SetQueryParams(map[string]string{
|
||||
"fsid": fmt.Sprint(file.Fsid),
|
||||
"album_id": file.AlbumID,
|
||||
"tid": fmt.Sprint(file.Tid),
|
||||
"uk": fmt.Sprint(file.Uk),
|
||||
})
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 302 {
|
||||
return nil, fmt.Errorf("not found 302 redirect")
|
||||
}
|
||||
|
||||
location := resp.Header().Get("Location")
|
||||
|
||||
link := &model.Link{
|
||||
URL: res.Header().Get("location"),
|
||||
URL: location,
|
||||
Header: http.Header{
|
||||
"User-Agent": []string{headers["User-Agent"]},
|
||||
"Referer": []string{"https://photo.baidu.com/"},
|
||||
@ -385,10 +393,24 @@ func (d *BaiduPhoto) linkFile(ctx context.Context, file *File, args model.LinkAr
|
||||
"fsid": fmt.Sprint(file.Fsid),
|
||||
})
|
||||
}, &downloadUrl)
|
||||
|
||||
// resp, err := d.Request(base.NoRedirectClient, FILE_API_URL_V1+"/download", http.MethodHead, func(r *resty.Request) {
|
||||
// r.SetContext(ctx)
|
||||
// r.SetHeaders(headers)
|
||||
// r.SetQueryParams(map[string]string{
|
||||
// "fsid": fmt.Sprint(file.Fsid),
|
||||
// })
|
||||
// }, nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if resp.StatusCode() != 302 {
|
||||
// return nil, fmt.Errorf("not found 302 redirect")
|
||||
// }
|
||||
|
||||
// location := resp.Header().Get("Location")
|
||||
link := &model.Link{
|
||||
URL: downloadUrl.Dlink,
|
||||
Header: http.Header{
|
||||
@ -453,3 +475,40 @@ func (d *BaiduPhoto) uInfo() (*UInfo, error) {
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func DecryptMd5(encryptMd5 string) string {
|
||||
if _, err := hex.DecodeString(encryptMd5); err == nil {
|
||||
return encryptMd5
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
out.Grow(len(encryptMd5))
|
||||
for i, n := 0, int64(0); i < len(encryptMd5); i++ {
|
||||
if i == 9 {
|
||||
n = int64(unicode.ToLower(rune(encryptMd5[i])) - 'g')
|
||||
} else {
|
||||
n, _ = strconv.ParseInt(encryptMd5[i:i+1], 16, 64)
|
||||
}
|
||||
out.WriteString(strconv.FormatInt(n^int64(15&i), 16))
|
||||
}
|
||||
|
||||
encryptMd5 = out.String()
|
||||
return encryptMd5[8:16] + encryptMd5[:8] + encryptMd5[24:32] + encryptMd5[16:24]
|
||||
}
|
||||
|
||||
func EncryptMd5(originalMd5 string) string {
|
||||
reversed := originalMd5[8:16] + originalMd5[:8] + originalMd5[24:32] + originalMd5[16:24]
|
||||
|
||||
var out strings.Builder
|
||||
out.Grow(len(reversed))
|
||||
for i, n := 0, int64(0); i < len(reversed); i++ {
|
||||
n, _ = strconv.ParseInt(reversed[i:i+1], 16, 64)
|
||||
n ^= int64(15 & i)
|
||||
if i == 9 {
|
||||
out.WriteRune(rune(n) + 'g')
|
||||
} else {
|
||||
out.WriteString(strconv.FormatInt(n, 16))
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
@ -67,7 +67,9 @@ func (d *ChaoXing) Init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *ChaoXing) Drop(ctx context.Context) error {
|
||||
d.cron.Stop()
|
||||
if d.cron != nil {
|
||||
d.cron.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -191,33 +191,33 @@ type UploadFileDataRsp struct {
|
||||
Resid int64 `json:"resid"`
|
||||
Puid int `json:"puid"`
|
||||
Data struct {
|
||||
DisableOpt bool `json:"disableOpt"`
|
||||
Resid int64 `json:"resid"`
|
||||
Crc string `json:"crc"`
|
||||
Puid int `json:"puid"`
|
||||
Isfile bool `json:"isfile"`
|
||||
Pantype string `json:"pantype"`
|
||||
Size int `json:"size"`
|
||||
Name string `json:"name"`
|
||||
ObjectID string `json:"objectId"`
|
||||
Restype string `json:"restype"`
|
||||
UploadDate time.Time `json:"uploadDate"`
|
||||
ModifyDate time.Time `json:"modifyDate"`
|
||||
UploadDateFormat string `json:"uploadDateFormat"`
|
||||
Residstr string `json:"residstr"`
|
||||
Suffix string `json:"suffix"`
|
||||
Preview string `json:"preview"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Creator int `json:"creator"`
|
||||
Duration int `json:"duration"`
|
||||
IsImg bool `json:"isImg"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
Filetype string `json:"filetype"`
|
||||
Filepath string `json:"filepath"`
|
||||
Sort int `json:"sort"`
|
||||
Topsort int `json:"topsort"`
|
||||
ResTypeValue int `json:"resTypeValue"`
|
||||
Extinfo string `json:"extinfo"`
|
||||
DisableOpt bool `json:"disableOpt"`
|
||||
Resid int64 `json:"resid"`
|
||||
Crc string `json:"crc"`
|
||||
Puid int `json:"puid"`
|
||||
Isfile bool `json:"isfile"`
|
||||
Pantype string `json:"pantype"`
|
||||
Size int `json:"size"`
|
||||
Name string `json:"name"`
|
||||
ObjectID string `json:"objectId"`
|
||||
Restype string `json:"restype"`
|
||||
UploadDate int64 `json:"uploadDate"`
|
||||
ModifyDate int64 `json:"modifyDate"`
|
||||
UploadDateFormat string `json:"uploadDateFormat"`
|
||||
Residstr string `json:"residstr"`
|
||||
Suffix string `json:"suffix"`
|
||||
Preview string `json:"preview"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Creator int `json:"creator"`
|
||||
Duration int `json:"duration"`
|
||||
IsImg bool `json:"isImg"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
Filetype string `json:"filetype"`
|
||||
Filepath string `json:"filepath"`
|
||||
Sort int `json:"sort"`
|
||||
Topsort int `json:"topsort"`
|
||||
ResTypeValue int `json:"resTypeValue"`
|
||||
Extinfo string `json:"extinfo"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
@ -225,33 +225,33 @@ type UploadDoneParam struct {
|
||||
Cataid string `json:"cataid"`
|
||||
Key string `json:"key"`
|
||||
Param struct {
|
||||
DisableOpt bool `json:"disableOpt"`
|
||||
Resid int64 `json:"resid"`
|
||||
Crc string `json:"crc"`
|
||||
Puid int `json:"puid"`
|
||||
Isfile bool `json:"isfile"`
|
||||
Pantype string `json:"pantype"`
|
||||
Size int `json:"size"`
|
||||
Name string `json:"name"`
|
||||
ObjectID string `json:"objectId"`
|
||||
Restype string `json:"restype"`
|
||||
UploadDate time.Time `json:"uploadDate"`
|
||||
ModifyDate time.Time `json:"modifyDate"`
|
||||
UploadDateFormat string `json:"uploadDateFormat"`
|
||||
Residstr string `json:"residstr"`
|
||||
Suffix string `json:"suffix"`
|
||||
Preview string `json:"preview"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Creator int `json:"creator"`
|
||||
Duration int `json:"duration"`
|
||||
IsImg bool `json:"isImg"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
Filetype string `json:"filetype"`
|
||||
Filepath string `json:"filepath"`
|
||||
Sort int `json:"sort"`
|
||||
Topsort int `json:"topsort"`
|
||||
ResTypeValue int `json:"resTypeValue"`
|
||||
Extinfo string `json:"extinfo"`
|
||||
DisableOpt bool `json:"disableOpt"`
|
||||
Resid int64 `json:"resid"`
|
||||
Crc string `json:"crc"`
|
||||
Puid int `json:"puid"`
|
||||
Isfile bool `json:"isfile"`
|
||||
Pantype string `json:"pantype"`
|
||||
Size int `json:"size"`
|
||||
Name string `json:"name"`
|
||||
ObjectID string `json:"objectId"`
|
||||
Restype string `json:"restype"`
|
||||
UploadDate int64 `json:"uploadDate"`
|
||||
ModifyDate int64 `json:"modifyDate"`
|
||||
UploadDateFormat string `json:"uploadDateFormat"`
|
||||
Residstr string `json:"residstr"`
|
||||
Suffix string `json:"suffix"`
|
||||
Preview string `json:"preview"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Creator int `json:"creator"`
|
||||
Duration int `json:"duration"`
|
||||
IsImg bool `json:"isImg"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
Filetype string `json:"filetype"`
|
||||
Filepath string `json:"filepath"`
|
||||
Sort int `json:"sort"`
|
||||
Topsort int `json:"topsort"`
|
||||
ResTypeValue int `json:"resTypeValue"`
|
||||
Extinfo string `json:"extinfo"`
|
||||
} `json:"param"`
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,13 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
@ -90,7 +92,7 @@ func (d *Cloudreve) MakeDir(ctx context.Context, parentDir model.Obj, dirName st
|
||||
func (d *Cloudreve) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
body := base.Json{
|
||||
"action": "move",
|
||||
"src_dir": srcObj.GetPath(),
|
||||
"src_dir": path.Dir(srcObj.GetPath()),
|
||||
"dst": dstDir.GetPath(),
|
||||
"src": convertSrc(srcObj),
|
||||
}
|
||||
@ -112,7 +114,7 @@ func (d *Cloudreve) Rename(ctx context.Context, srcObj model.Obj, newName string
|
||||
|
||||
func (d *Cloudreve) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
body := base.Json{
|
||||
"src_dir": srcObj.GetPath(),
|
||||
"src_dir": path.Dir(srcObj.GetPath()),
|
||||
"dst": dstDir.GetPath(),
|
||||
"src": convertSrc(srcObj),
|
||||
}
|
||||
@ -133,6 +135,8 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
||||
if io.ReadCloser(stream) == http.NoBody {
|
||||
return d.create(ctx, dstDir, stream)
|
||||
}
|
||||
|
||||
// 获取存储策略
|
||||
var r DirectoryResp
|
||||
err := d.request(http.MethodGet, "/directory"+dstDir.GetPath(), nil, &r)
|
||||
if err != nil {
|
||||
@ -145,6 +149,8 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
||||
"policy_id": r.Policy.Id,
|
||||
"last_modified": stream.ModTime().Unix(),
|
||||
}
|
||||
|
||||
// 获取上传会话信息
|
||||
var u UploadInfo
|
||||
err = d.request(http.MethodPut, "/file/upload", func(req *resty.Request) {
|
||||
req.SetBody(uploadBody)
|
||||
@ -152,36 +158,50 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var chunkSize = u.ChunkSize
|
||||
var buf []byte
|
||||
var chunk int
|
||||
for {
|
||||
var n int
|
||||
buf = make([]byte, chunkSize)
|
||||
n, err = io.ReadAtLeast(stream, buf, chunkSize)
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
|
||||
// 根据存储方式选择分片上传的方法
|
||||
switch r.Policy.Type {
|
||||
case "onedrive":
|
||||
err = d.upOneDrive(ctx, stream, u, up)
|
||||
case "remote": // 从机存储
|
||||
err = d.upRemote(ctx, stream, u, up)
|
||||
case "local": // 本机存储
|
||||
var chunkSize = u.ChunkSize
|
||||
var buf []byte
|
||||
var chunk int
|
||||
for {
|
||||
var n int
|
||||
buf = make([]byte, chunkSize)
|
||||
n, err = io.ReadAtLeast(stream, buf, chunkSize)
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
buf = buf[:n]
|
||||
err = d.request(http.MethodPost, "/file/upload/"+u.SessionID+"/"+strconv.Itoa(chunk), func(req *resty.Request) {
|
||||
req.SetHeader("Content-Type", "application/octet-stream")
|
||||
req.SetHeader("Content-Length", strconv.Itoa(n))
|
||||
req.SetBody(buf)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
chunk++
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
buf = buf[:n]
|
||||
err = d.request(http.MethodPost, "/file/upload/"+u.SessionID+"/"+strconv.Itoa(chunk), func(req *resty.Request) {
|
||||
req.SetHeader("Content-Type", "application/octet-stream")
|
||||
req.SetHeader("Content-Length", strconv.Itoa(n))
|
||||
req.SetBody(buf)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
chunk++
|
||||
|
||||
default:
|
||||
err = errs.NotImplement
|
||||
}
|
||||
return err
|
||||
if err != nil {
|
||||
// 删除失败的会话
|
||||
err = d.request(http.MethodDelete, "/file/upload/"+u.SessionID, nil, nil)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Cloudreve) create(ctx context.Context, dir model.Obj, file model.Obj) error {
|
||||
|
@ -21,9 +21,11 @@ type Policy struct {
|
||||
}
|
||||
|
||||
type UploadInfo struct {
|
||||
SessionID string `json:"sessionID"`
|
||||
ChunkSize int `json:"chunkSize"`
|
||||
Expires int `json:"expires"`
|
||||
SessionID string `json:"sessionID"`
|
||||
ChunkSize int `json:"chunkSize"`
|
||||
Expires int `json:"expires"`
|
||||
UploadURLs []string `json:"uploadURLs"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
}
|
||||
|
||||
type DirectoryResp struct {
|
||||
|
@ -1,16 +1,23 @@
|
||||
package cloudreve
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/cookie"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
json "github.com/json-iterator/go"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
@ -172,3 +179,95 @@ func (d *Cloudreve) GetThumb(file Object) (model.Thumbnail, error) {
|
||||
Thumbnail: resp.Header().Get("Location"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Cloudreve) upRemote(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {
|
||||
uploadUrl := u.UploadURLs[0]
|
||||
credential := u.Credential
|
||||
var finish int64 = 0
|
||||
var chunk int = 0
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
for finish < stream.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
utils.Log.Debugf("[Cloudreve-Remote] upload: %d", finish)
|
||||
var byteSize = DEFAULT
|
||||
left := stream.GetSize() - finish
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(stream, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("POST", uploadUrl+"?chunk="+strconv.Itoa(chunk), bytes.NewBuffer(byteData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Authorization", fmt.Sprint(credential))
|
||||
finish += byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.Body.Close()
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
chunk++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Cloudreve) upOneDrive(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {
|
||||
uploadUrl := u.UploadURLs[0]
|
||||
var finish int64 = 0
|
||||
DEFAULT := int64(u.ChunkSize)
|
||||
for finish < stream.GetSize() {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
utils.Log.Debugf("[Cloudreve-OneDrive] upload: %d", finish)
|
||||
var byteSize = DEFAULT
|
||||
left := stream.GetSize() - finish
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(stream, byteData)
|
||||
utils.Log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("PUT", uploadUrl, bytes.NewBuffer(byteData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
|
||||
finish += byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
|
||||
if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 {
|
||||
data, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(string(data))
|
||||
}
|
||||
res.Body.Close()
|
||||
up(float64(finish) * 100 / float64(stream.GetSize()))
|
||||
}
|
||||
// 上传成功发送回调请求
|
||||
err := d.request(http.MethodPost, "/callback/onedrive/finish/"+u.SessionID, func(req *resty.Request) {
|
||||
req.SetBody("{}")
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
132
drivers/febbox/driver.go
Normal file
132
drivers/febbox/driver.go
Normal file
@ -0,0 +1,132 @@
|
||||
package febbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type FebBox struct {
|
||||
model.Storage
|
||||
Addition
|
||||
accessToken string
|
||||
oauth2Token oauth2.TokenSource
|
||||
}
|
||||
|
||||
func (d *FebBox) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *FebBox) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *FebBox) Init(ctx context.Context) error {
|
||||
// 初始化 oauth2Config
|
||||
oauth2Config := &clientcredentials.Config{
|
||||
ClientID: d.ClientID,
|
||||
ClientSecret: d.ClientSecret,
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
TokenURL: "https://api.febbox.com/oauth/token",
|
||||
}
|
||||
|
||||
d.initializeOAuth2Token(ctx, oauth2Config, d.Addition.RefreshToken)
|
||||
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.accessToken = token.AccessToken
|
||||
d.Addition.RefreshToken = token.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *FebBox) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *FebBox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files, err := d.getFilesList(dir.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||
return fileToObj(src), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *FebBox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var ip string
|
||||
if d.Addition.UserIP != "" {
|
||||
ip = d.Addition.UserIP
|
||||
} else {
|
||||
ip = args.IP
|
||||
}
|
||||
|
||||
url, err := d.getDownloadLink(file.GetID(), ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.Link{
|
||||
URL: url,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *FebBox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
err := d.makeDir(parentDir.GetID(), dirName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *FebBox) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
err := d.move(srcObj.GetID(), dstDir.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *FebBox) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
err := d.rename(srcObj.GetID(), newName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *FebBox) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
err := d.copy(srcObj.GetID(), dstDir.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *FebBox) Remove(ctx context.Context, obj model.Obj) error {
|
||||
err := d.remove(obj.GetID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *FebBox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*FebBox)(nil)
|
36
drivers/febbox/meta.go
Normal file
36
drivers/febbox/meta.go
Normal file
@ -0,0 +1,36 @@
|
||||
package febbox
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
ClientID string `json:"client_id" required:"true" default:""`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:""`
|
||||
RefreshToken string
|
||||
SortRule string `json:"sort_rule" required:"true" type:"select" options:"size_asc,size_desc,name_asc,name_desc,update_asc,update_desc,ext_asc,ext_desc" default:"name_asc"`
|
||||
PageSize int64 `json:"page_size" required:"true" type:"number" default:"100" help:"list api per page size of FebBox driver"`
|
||||
UserIP string `json:"user_ip" default:"" help:"user ip address for download link which can speed up the download"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "FebBox",
|
||||
LocalSort: false,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: true,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "0",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &FebBox{}
|
||||
})
|
||||
}
|
88
drivers/febbox/oauth2.go
Normal file
88
drivers/febbox/oauth2.go
Normal file
@ -0,0 +1,88 @@
|
||||
package febbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
)
|
||||
|
||||
type customTokenSource struct {
|
||||
config *clientcredentials.Config
|
||||
ctx context.Context
|
||||
refreshToken string
|
||||
}
|
||||
|
||||
func (c *customTokenSource) Token() (*oauth2.Token, error) {
|
||||
v := url.Values{}
|
||||
if c.refreshToken != "" {
|
||||
v.Set("grant_type", "refresh_token")
|
||||
v.Set("refresh_token", c.refreshToken)
|
||||
} else {
|
||||
v.Set("grant_type", "client_credentials")
|
||||
}
|
||||
|
||||
v.Set("client_id", c.config.ClientID)
|
||||
v.Set("client_secret", c.config.ClientSecret)
|
||||
|
||||
req, err := http.NewRequest("POST", c.config.TokenURL, strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req.WithContext(c.ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("oauth2: cannot fetch token")
|
||||
}
|
||||
|
||||
var tokenResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
Scope string `json:"scope"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenResp.Code != 1 {
|
||||
return nil, errors.New("oauth2: server response error")
|
||||
}
|
||||
|
||||
c.refreshToken = tokenResp.Data.RefreshToken
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: tokenResp.Data.AccessToken,
|
||||
TokenType: tokenResp.Data.TokenType,
|
||||
RefreshToken: tokenResp.Data.RefreshToken,
|
||||
Expiry: time.Now().Add(time.Duration(tokenResp.Data.ExpiresIn) * time.Second),
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (d *FebBox) initializeOAuth2Token(ctx context.Context, oauth2Config *clientcredentials.Config, refreshToken string) {
|
||||
d.oauth2Token = oauth2.ReuseTokenSource(nil, &customTokenSource{
|
||||
config: oauth2Config,
|
||||
ctx: ctx,
|
||||
refreshToken: refreshToken,
|
||||
})
|
||||
}
|
123
drivers/febbox/types.go
Normal file
123
drivers/febbox/types.go
Normal file
@ -0,0 +1,123 @@
|
||||
package febbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ErrResp struct {
|
||||
ErrorCode int64 `json:"code"`
|
||||
ErrorMsg string `json:"msg"`
|
||||
ServerRunTime float64 `json:"server_runtime"`
|
||||
ServerName string `json:"server_name"`
|
||||
}
|
||||
|
||||
func (e *ErrResp) IsError() bool {
|
||||
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ServerRunTime != 0 || e.ServerName != ""
|
||||
}
|
||||
|
||||
func (e *ErrResp) Error() string {
|
||||
return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ServerRunTime: %f ,ServerName: %s", e.ErrorCode, e.ErrorMsg, e.ServerRunTime, e.ServerName)
|
||||
}
|
||||
|
||||
type FileListResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
FileList []File `json:"file_list"`
|
||||
ShowType string `json:"show_type"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type Rules struct {
|
||||
AllowCopy int64 `json:"allow_copy"`
|
||||
AllowDelete int64 `json:"allow_delete"`
|
||||
AllowDownload int64 `json:"allow_download"`
|
||||
AllowComment int64 `json:"allow_comment"`
|
||||
HideLocation int64 `json:"hide_location"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Fid int64 `json:"fid"`
|
||||
UID int64 `json:"uid"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
Path string `json:"path"`
|
||||
FileName string `json:"file_name"`
|
||||
Ext string `json:"ext"`
|
||||
AddTime int64 `json:"add_time"`
|
||||
FileCreateTime int64 `json:"file_create_time"`
|
||||
FileUpdateTime int64 `json:"file_update_time"`
|
||||
ParentID int64 `json:"parent_id"`
|
||||
UpdateTime int64 `json:"update_time"`
|
||||
LastOpenTime int64 `json:"last_open_time"`
|
||||
IsDir int64 `json:"is_dir"`
|
||||
Epub int64 `json:"epub"`
|
||||
IsMusicList int64 `json:"is_music_list"`
|
||||
OssFid int64 `json:"oss_fid"`
|
||||
Faststart int64 `json:"faststart"`
|
||||
HasVideoQuality int64 `json:"has_video_quality"`
|
||||
TotalDownload int64 `json:"total_download"`
|
||||
Status int64 `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
OldHash string `json:"old_hash"`
|
||||
Hash string `json:"hash"`
|
||||
HashType string `json:"hash_type"`
|
||||
FromUID int64 `json:"from_uid"`
|
||||
FidOrg int64 `json:"fid_org"`
|
||||
ShareID int64 `json:"share_id"`
|
||||
InvitePermission int64 `json:"invite_permission"`
|
||||
ThumbSmall string `json:"thumb_small"`
|
||||
ThumbSmallWidth int64 `json:"thumb_small_width"`
|
||||
ThumbSmallHeight int64 `json:"thumb_small_height"`
|
||||
Thumb string `json:"thumb"`
|
||||
ThumbWidth int64 `json:"thumb_width"`
|
||||
ThumbHeight int64 `json:"thumb_height"`
|
||||
ThumbBig string `json:"thumb_big"`
|
||||
ThumbBigWidth int64 `json:"thumb_big_width"`
|
||||
ThumbBigHeight int64 `json:"thumb_big_height"`
|
||||
IsCustomThumb int64 `json:"is_custom_thumb"`
|
||||
Photos int64 `json:"photos"`
|
||||
IsAlbum int64 `json:"is_album"`
|
||||
ReadOnly int64 `json:"read_only"`
|
||||
Rules Rules `json:"rules"`
|
||||
IsShared int64 `json:"is_shared"`
|
||||
}
|
||||
|
||||
func fileToObj(f File) *model.ObjThumb {
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: strconv.FormatInt(f.Fid, 10),
|
||||
Name: f.FileName,
|
||||
Size: f.FileSize,
|
||||
Ctime: time.Unix(f.FileCreateTime, 0),
|
||||
Modified: time.Unix(f.FileUpdateTime, 0),
|
||||
IsFolder: f.IsDir == 1,
|
||||
HashInfo: utils.NewHashInfo(hash_extend.GCID, f.Hash),
|
||||
},
|
||||
Thumbnail: model.Thumbnail{
|
||||
Thumbnail: f.Thumb,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type FileDownloadResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data []struct {
|
||||
Error int `json:"error"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
Hash string `json:"hash"`
|
||||
HashType string `json:"hash_type"`
|
||||
Fid int `json:"fid"`
|
||||
FileName string `json:"file_name"`
|
||||
ParentID int `json:"parent_id"`
|
||||
FileSize int `json:"file_size"`
|
||||
Ext string `json:"ext"`
|
||||
Thumb string `json:"thumb"`
|
||||
VipLink int `json:"vip_link"`
|
||||
} `json:"data"`
|
||||
}
|
224
drivers/febbox/util.go
Normal file
224
drivers/febbox/util.go
Normal file
@ -0,0 +1,224 @@
|
||||
package febbox
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (d *FebBox) refreshTokenByOAuth2() error {
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Status = "work"
|
||||
d.accessToken = token.AccessToken
|
||||
d.Addition.RefreshToken = token.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *FebBox) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
// 使用oauth2 获取 access_token
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var e ErrResp
|
||||
req.SetError(&e)
|
||||
res, err := req.Execute(method, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch e.ErrorCode {
|
||||
case 0:
|
||||
return res.Body(), nil
|
||||
case 1:
|
||||
return res.Body(), nil
|
||||
case -10001:
|
||||
if e.ServerName != "" {
|
||||
// access_token 过期
|
||||
if err = d.refreshTokenByOAuth2(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
} else {
|
||||
return nil, errors.New(e.Error())
|
||||
}
|
||||
default:
|
||||
return nil, errors.New(e.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (d *FebBox) getFilesList(id string) ([]File, error) {
|
||||
if d.PageSize <= 0 {
|
||||
d.PageSize = 100
|
||||
}
|
||||
res, err := d.listWithLimit(id, d.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return *res, nil
|
||||
}
|
||||
|
||||
func (d *FebBox) listWithLimit(dirID string, pageLimit int64) (*[]File, error) {
|
||||
var files []File
|
||||
page := int64(1)
|
||||
for {
|
||||
result, err := d.getFiles(dirID, page, pageLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, *result...)
|
||||
if int64(len(*result)) < pageLimit {
|
||||
break
|
||||
} else {
|
||||
page++
|
||||
}
|
||||
}
|
||||
return &files, nil
|
||||
}
|
||||
|
||||
func (d *FebBox) getFiles(dirID string, page, pageLimit int64) (*[]File, error) {
|
||||
var fileList FileListResp
|
||||
queryParams := map[string]string{
|
||||
"module": "file_list",
|
||||
"parent_id": dirID,
|
||||
"page": strconv.FormatInt(page, 10),
|
||||
"pagelimit": strconv.FormatInt(pageLimit, 10),
|
||||
"order": d.Addition.SortRule,
|
||||
}
|
||||
|
||||
res, err := d.request("https://api.febbox.com/oauth", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetMultipartFormData(queryParams)
|
||||
}, &fileList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(res, &fileList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &fileList.Data.FileList, nil
|
||||
}
|
||||
|
||||
func (d *FebBox) getDownloadLink(id string, ip string) (string, error) {
|
||||
var fileDownloadResp FileDownloadResp
|
||||
queryParams := map[string]string{
|
||||
"module": "file_get_download_url",
|
||||
"fids[]": id,
|
||||
"ip": ip,
|
||||
}
|
||||
|
||||
res, err := d.request("https://api.febbox.com/oauth", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetMultipartFormData(queryParams)
|
||||
}, &fileDownloadResp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(res, &fileDownloadResp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fileDownloadResp.Data[0].DownloadURL, nil
|
||||
}
|
||||
|
||||
func (d *FebBox) makeDir(id string, name string) error {
|
||||
queryParams := map[string]string{
|
||||
"module": "create_dir",
|
||||
"parent_id": id,
|
||||
"name": name,
|
||||
}
|
||||
|
||||
_, err := d.request("https://api.febbox.com/oauth", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetMultipartFormData(queryParams)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *FebBox) move(id string, id2 string) error {
|
||||
queryParams := map[string]string{
|
||||
"module": "file_move",
|
||||
"fids[]": id,
|
||||
"to": id2,
|
||||
}
|
||||
|
||||
_, err := d.request("https://api.febbox.com/oauth", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetMultipartFormData(queryParams)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *FebBox) rename(id string, name string) error {
|
||||
queryParams := map[string]string{
|
||||
"module": "file_rename",
|
||||
"fid": id,
|
||||
"name": name,
|
||||
}
|
||||
|
||||
_, err := d.request("https://api.febbox.com/oauth", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetMultipartFormData(queryParams)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *FebBox) copy(id string, id2 string) error {
|
||||
queryParams := map[string]string{
|
||||
"module": "file_copy",
|
||||
"fids[]": id,
|
||||
"to": id2,
|
||||
}
|
||||
|
||||
_, err := d.request("https://api.febbox.com/oauth", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetMultipartFormData(queryParams)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *FebBox) remove(id string) error {
|
||||
queryParams := map[string]string{
|
||||
"module": "file_delete",
|
||||
"fids[]": id,
|
||||
}
|
||||
|
||||
_, err := d.request("https://api.febbox.com/oauth", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetMultipartFormData(queryParams)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -58,33 +58,9 @@ func (d *GooglePhoto) Link(ctx context.Context, file model.Obj, args model.LinkA
|
||||
URL: f.BaseURL + "=d",
|
||||
}, nil
|
||||
} else if strings.Contains(f.MimeType, "video/") {
|
||||
var width, height int
|
||||
|
||||
fmt.Sscanf(f.MediaMetadata.Width, "%d", &width)
|
||||
fmt.Sscanf(f.MediaMetadata.Height, "%d", &height)
|
||||
|
||||
switch {
|
||||
// 1080P
|
||||
case width == 1920 && height == 1080:
|
||||
return &model.Link{
|
||||
URL: f.BaseURL + "=m37",
|
||||
}, nil
|
||||
// 720P
|
||||
case width == 1280 && height == 720:
|
||||
return &model.Link{
|
||||
URL: f.BaseURL + "=m22",
|
||||
}, nil
|
||||
// 360P
|
||||
case width == 640 && height == 360:
|
||||
return &model.Link{
|
||||
URL: f.BaseURL + "=m18",
|
||||
}, nil
|
||||
default:
|
||||
return &model.Link{
|
||||
URL: f.BaseURL + "=dv",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: f.BaseURL + "=dv",
|
||||
}, nil
|
||||
}
|
||||
return &model.Link{}, nil
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ func (d *HalalCloud) IsLogin() bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
result, err := pbPublicUser.NewPubUserClient(serv.GetGrpcConnection()).Get(ctx, &pbPublicUser.User{
|
||||
Identity: "",
|
||||
|
@ -12,9 +12,9 @@ type Addition struct {
|
||||
RefreshToken string `json:"refresh_token" required:"true" help:"login type is refresh_token,this is required"`
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1 <= thread <= 32"`
|
||||
|
||||
AppID string `json:"app_id" required:"true" default:"devDebugger/1.0"`
|
||||
AppID string `json:"app_id" required:"true" default:"alist/10001"`
|
||||
AppVersion string `json:"app_version" required:"true" default:"1.0.0"`
|
||||
AppSecret string `json:"app_secret" required:"true" default:"Nkx3Y2xvZ2luLmNu"`
|
||||
AppSecret string `json:"app_secret" required:"true" default:"bR4SJwOkvnG5WvVJ"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -29,9 +29,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
AppID = "devDebugger/1.0"
|
||||
AppID = "alist/10001"
|
||||
AppVersion = "1.0.0"
|
||||
AppSecret = "Nkx3Y2xvZ2luLmNu"
|
||||
AppSecret = "bR4SJwOkvnG5WvVJ"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -62,7 +62,7 @@ func (d *HalalCloud) NewAuthServiceWithOauth(options ...HalalOption) (*AuthServi
|
||||
}
|
||||
defer grpcConnection.Close()
|
||||
userClient := pbPublicUser.NewPubUserClient(grpcConnection)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
stateString := uuid.New().String()
|
||||
// queryValues.Add("callback", oauthToken.Callback)
|
||||
@ -179,16 +179,16 @@ func (s *AuthService) signContext(method string, ctx context.Context) context.Co
|
||||
bufferedString := bytes.NewBufferString(method)
|
||||
kvString = append(kvString, "timestamp", currentTimeStamp)
|
||||
bufferedString.WriteString(currentTimeStamp)
|
||||
kvString = append(kvString, "appid", AppID)
|
||||
bufferedString.WriteString(AppID)
|
||||
kvString = append(kvString, "appversion", AppVersion)
|
||||
bufferedString.WriteString(AppVersion)
|
||||
kvString = append(kvString, "appid", s.appID)
|
||||
bufferedString.WriteString(s.appID)
|
||||
kvString = append(kvString, "appversion", s.appVersion)
|
||||
bufferedString.WriteString(s.appVersion)
|
||||
if s.tr != nil && len(s.tr.AccessToken) > 0 {
|
||||
authorization := "Bearer " + s.tr.AccessToken
|
||||
kvString = append(kvString, "authorization", authorization)
|
||||
bufferedString.WriteString(authorization)
|
||||
}
|
||||
bufferedString.WriteString(AppSecret)
|
||||
bufferedString.WriteString(s.appSecret)
|
||||
sign := GetMD5Hash(bufferedString.String())
|
||||
kvString = append(kvString, "sign", sign)
|
||||
return metadata.AppendToOutgoingContext(ctx, kvString...)
|
||||
|
@ -66,12 +66,13 @@ func (d *ILanZou) Drop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
offset := 1
|
||||
var res []ListItem
|
||||
for {
|
||||
var resp ListResp
|
||||
_, err := d.proved("/record/file/list", http.MethodGet, func(req *resty.Request) {
|
||||
params := []string{
|
||||
"offset=1",
|
||||
"offset=" + strconv.Itoa(offset),
|
||||
"limit=60",
|
||||
"folderId=" + dir.GetID(),
|
||||
"type=0",
|
||||
@ -83,7 +84,9 @@ func (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, resp.List...)
|
||||
if resp.TotalPage <= resp.Offset {
|
||||
if resp.Offset < resp.TotalPage {
|
||||
offset++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -286,7 +289,7 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
||||
req.SetBody(base.Json{
|
||||
"fileId": "",
|
||||
"fileName": stream.GetName(),
|
||||
"fileSize": stream.GetSize() / 1024,
|
||||
"fileSize": stream.GetSize()/1024 + 1,
|
||||
"folderId": dstDir.GetID(),
|
||||
"md5": etag,
|
||||
"type": 1,
|
||||
|
273
drivers/kodbox/driver.go
Normal file
273
drivers/kodbox/driver.go
Normal file
@ -0,0 +1,273 @@
|
||||
package kodbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type KodBox struct {
|
||||
model.Storage
|
||||
Addition
|
||||
authorization string
|
||||
}
|
||||
|
||||
func (d *KodBox) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *KodBox) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *KodBox) Init(ctx context.Context) error {
|
||||
d.Address = strings.TrimSuffix(d.Address, "/")
|
||||
d.RootFolderPath = strings.TrimPrefix(utils.FixAndCleanPath(d.RootFolderPath), "/")
|
||||
return d.getToken()
|
||||
}
|
||||
|
||||
func (d *KodBox) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *KodBox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
var (
|
||||
resp *CommonResp
|
||||
listPathData *ListPathData
|
||||
)
|
||||
|
||||
_, err := d.request(http.MethodPost, "/?explorer/list/path", func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetFormData(map[string]string{
|
||||
"path": dir.GetPath(),
|
||||
})
|
||||
}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataBytes, err := utils.Json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = utils.Json.Unmarshal(dataBytes, &listPathData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
FolderAndFiles := append(listPathData.FolderList, listPathData.FileList...)
|
||||
|
||||
return utils.SliceConvert(FolderAndFiles, func(f FolderOrFile) (model.Obj, error) {
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
Path: f.Path,
|
||||
Name: f.Name,
|
||||
Ctime: time.Unix(f.CreateTime, 0),
|
||||
Modified: time.Unix(f.ModifyTime, 0),
|
||||
Size: f.Size,
|
||||
IsFolder: f.Type == "folder",
|
||||
},
|
||||
//Thumbnail: model.Thumbnail{},
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *KodBox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
path := file.GetPath()
|
||||
return &model.Link{
|
||||
URL: fmt.Sprintf("%s/?explorer/index/fileOut&path=%s&download=1&accessToken=%s",
|
||||
d.Address,
|
||||
path,
|
||||
d.authorization)}, nil
|
||||
}
|
||||
|
||||
func (d *KodBox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
var resp *CommonResp
|
||||
newDirPath := filepath.Join(parentDir.GetPath(), dirName)
|
||||
|
||||
_, err := d.request(http.MethodPost, "/?explorer/index/mkdir", func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetFormData(map[string]string{
|
||||
"path": newDirPath,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := resp.Code.(bool)
|
||||
if !code {
|
||||
return nil, fmt.Errorf("%s", resp.Data)
|
||||
}
|
||||
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
Path: resp.Info.(string),
|
||||
Name: dirName,
|
||||
IsFolder: true,
|
||||
Modified: time.Now(),
|
||||
Ctime: time.Now(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *KodBox) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
var resp *CommonResp
|
||||
_, err := d.request(http.MethodPost, "/?explorer/index/pathCuteTo", func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetFormData(map[string]string{
|
||||
"dataArr": fmt.Sprintf("[{\"path\": \"%s\", \"name\": \"%s\"}]",
|
||||
srcObj.GetPath(),
|
||||
srcObj.GetName()),
|
||||
"path": dstDir.GetPath(),
|
||||
})
|
||||
}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := resp.Code.(bool)
|
||||
if !code {
|
||||
return nil, fmt.Errorf("%s", resp.Data)
|
||||
}
|
||||
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
Path: srcObj.GetPath(),
|
||||
Name: srcObj.GetName(),
|
||||
IsFolder: srcObj.IsDir(),
|
||||
Modified: srcObj.ModTime(),
|
||||
Ctime: srcObj.CreateTime(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *KodBox) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
var resp *CommonResp
|
||||
_, err := d.request(http.MethodPost, "/?explorer/index/pathRename", func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetFormData(map[string]string{
|
||||
"path": srcObj.GetPath(),
|
||||
"newName": newName,
|
||||
})
|
||||
}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := resp.Code.(bool)
|
||||
if !code {
|
||||
return nil, fmt.Errorf("%s", resp.Data)
|
||||
}
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
Path: srcObj.GetPath(),
|
||||
Name: newName,
|
||||
IsFolder: srcObj.IsDir(),
|
||||
Modified: time.Now(),
|
||||
Ctime: srcObj.CreateTime(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *KodBox) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
var resp *CommonResp
|
||||
_, err := d.request(http.MethodPost, "/?explorer/index/pathCopyTo", func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetFormData(map[string]string{
|
||||
"dataArr": fmt.Sprintf("[{\"path\": \"%s\", \"name\": \"%s\"}]",
|
||||
srcObj.GetPath(),
|
||||
srcObj.GetName()),
|
||||
"path": dstDir.GetPath(),
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := resp.Code.(bool)
|
||||
if !code {
|
||||
return nil, fmt.Errorf("%s", resp.Data)
|
||||
}
|
||||
|
||||
path := resp.Info.([]interface{})[0].(string)
|
||||
objectName, err := d.getFileOrFolderName(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
Path: path,
|
||||
Name: *objectName,
|
||||
IsFolder: srcObj.IsDir(),
|
||||
Modified: time.Now(),
|
||||
Ctime: time.Now(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *KodBox) Remove(ctx context.Context, obj model.Obj) error {
|
||||
var resp *CommonResp
|
||||
_, err := d.request(http.MethodPost, "/?explorer/index/pathDelete", func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetFormData(map[string]string{
|
||||
"dataArr": fmt.Sprintf("[{\"path\": \"%s\", \"name\": \"%s\"}]",
|
||||
obj.GetPath(),
|
||||
obj.GetName()),
|
||||
"shiftDelete": "1",
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
code := resp.Code.(bool)
|
||||
if !code {
|
||||
return fmt.Errorf("%s", resp.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *KodBox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
var resp *CommonResp
|
||||
_, err := d.request(http.MethodPost, "/?explorer/upload/fileUpload", func(req *resty.Request) {
|
||||
req.SetFileReader("file", stream.GetName(), stream).
|
||||
SetResult(&resp).
|
||||
SetFormData(map[string]string{
|
||||
"path": dstDir.GetPath(),
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := resp.Code.(bool)
|
||||
if !code {
|
||||
return nil, fmt.Errorf("%s", resp.Data)
|
||||
}
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
Path: resp.Info.(string),
|
||||
Name: stream.GetName(),
|
||||
Size: stream.GetSize(),
|
||||
IsFolder: false,
|
||||
Modified: time.Now(),
|
||||
Ctime: time.Now(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *KodBox) getFileOrFolderName(ctx context.Context, path string) (*string, error) {
|
||||
var resp *CommonResp
|
||||
_, err := d.request(http.MethodPost, "/?explorer/index/pathInfo", func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetFormData(map[string]string{
|
||||
"dataArr": fmt.Sprintf("[{\"path\": \"%s\"}]", path)})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := resp.Code.(bool)
|
||||
if !code {
|
||||
return nil, fmt.Errorf("%s", resp.Data)
|
||||
}
|
||||
folderOrFileName := resp.Data.(map[string]any)["name"].(string)
|
||||
return &folderOrFileName, nil
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*KodBox)(nil)
|
25
drivers/kodbox/meta.go
Normal file
25
drivers/kodbox/meta.go
Normal file
@ -0,0 +1,25 @@
|
||||
package kodbox
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootPath
|
||||
|
||||
Address string `json:"address" required:"true"`
|
||||
UserName string `json:"username" required:"false"`
|
||||
Password string `json:"password" required:"false"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "KodBox",
|
||||
DefaultRoot: "",
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &KodBox{}
|
||||
})
|
||||
}
|
24
drivers/kodbox/types.go
Normal file
24
drivers/kodbox/types.go
Normal file
@ -0,0 +1,24 @@
|
||||
package kodbox
|
||||
|
||||
type CommonResp struct {
|
||||
Code any `json:"code"`
|
||||
TimeUse string `json:"timeUse"`
|
||||
TimeNow string `json:"timeNow"`
|
||||
Data any `json:"data"`
|
||||
Info any `json:"info"`
|
||||
}
|
||||
|
||||
type ListPathData struct {
|
||||
FolderList []FolderOrFile `json:"folderList"`
|
||||
FileList []FolderOrFile `json:"fileList"`
|
||||
}
|
||||
|
||||
type FolderOrFile struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Ext string `json:"ext,omitempty"` // 文件特有字段
|
||||
Size int64 `json:"size"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
ModifyTime int64 `json:"modifyTime"`
|
||||
}
|
86
drivers/kodbox/util.go
Normal file
86
drivers/kodbox/util.go
Normal file
@ -0,0 +1,86 @@
|
||||
package kodbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (d *KodBox) getToken() error {
|
||||
var authResp CommonResp
|
||||
res, err := base.RestyClient.R().
|
||||
SetResult(&authResp).
|
||||
SetQueryParams(map[string]string{
|
||||
"name": d.UserName,
|
||||
"password": d.Password,
|
||||
}).
|
||||
Post(d.Address + "/?user/index/loginSubmit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode() >= 400 {
|
||||
return fmt.Errorf("get token failed: %s", res.String())
|
||||
}
|
||||
|
||||
if res.StatusCode() == 200 && authResp.Code.(bool) == false {
|
||||
return fmt.Errorf("get token failed: %s", res.String())
|
||||
}
|
||||
|
||||
d.authorization = fmt.Sprintf("%s", authResp.Info)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *KodBox) request(method string, pathname string, callback base.ReqCallback, noRedirect ...bool) ([]byte, error) {
|
||||
full := pathname
|
||||
if !strings.HasPrefix(pathname, "http") {
|
||||
full = d.Address + pathname
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
if len(noRedirect) > 0 && noRedirect[0] {
|
||||
req = base.NoRedirectClient.R()
|
||||
}
|
||||
req.SetFormData(map[string]string{
|
||||
"accessToken": d.authorization,
|
||||
})
|
||||
callback(req)
|
||||
|
||||
var (
|
||||
res *resty.Response
|
||||
commonResp *CommonResp
|
||||
err error
|
||||
skip bool
|
||||
)
|
||||
for i := 0; i < 2; i++ {
|
||||
if skip {
|
||||
break
|
||||
}
|
||||
res, err = req.Execute(method, full)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err := utils.Json.Unmarshal(res.Body(), &commonResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch commonResp.Code.(type) {
|
||||
case bool:
|
||||
skip = true
|
||||
case string:
|
||||
if commonResp.Code.(string) == "10001" {
|
||||
err = d.getToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetFormData(map[string]string{"accessToken": d.authorization})
|
||||
}
|
||||
}
|
||||
}
|
||||
if commonResp.Code.(bool) == false {
|
||||
return nil, fmt.Errorf("request failed: %s", commonResp.Data)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
@ -30,6 +30,9 @@ func (d *LanZou) GetAddition() driver.Additional {
|
||||
}
|
||||
|
||||
func (d *LanZou) Init(ctx context.Context) (err error) {
|
||||
if d.UserAgent == "" {
|
||||
d.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.39 (KHTML, like Gecko) Chrome/89.0.4389.111 Safari/537.39"
|
||||
}
|
||||
switch d.Type {
|
||||
case "account":
|
||||
_, err := d.Login()
|
||||
|
@ -16,7 +16,8 @@ type Addition struct {
|
||||
driver.RootID
|
||||
SharePassword string `json:"share_password"`
|
||||
BaseUrl string `json:"baseUrl" required:"true" default:"https://pc.woozooo.com" help:"basic URL for file operation"`
|
||||
ShareUrl string `json:"shareUrl" required:"true" default:"https://pan.lanzouo.com" help:"used to get the sharing page"`
|
||||
ShareUrl string `json:"shareUrl" required:"true" default:"https://pan.lanzoui.com" help:"used to get the sharing page"`
|
||||
UserAgent string `json:"user_agent" required:"true" default:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.39 (KHTML, like Gecko) Chrome/89.0.4389.111 Safari/537.39"`
|
||||
RepairFileInfo bool `json:"repair_file_info" help:"To use webdav, you need to enable it"`
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,8 @@ func (d *LanZou) request(url string, method string, callback base.ReqCallback, u
|
||||
}
|
||||
|
||||
req.SetHeaders(map[string]string{
|
||||
"Referer": "https://pc.woozooo.com",
|
||||
"Referer": "https://pc.woozooo.com",
|
||||
"User-Agent": d.UserAgent,
|
||||
})
|
||||
|
||||
if d.Cookie != "" {
|
||||
|
121
drivers/lenovonas_share/driver.go
Normal file
121
drivers/lenovonas_share/driver.go
Normal file
@ -0,0 +1,121 @@
|
||||
package LenovoNasShare
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
type LenovoNasShare struct {
|
||||
model.Storage
|
||||
Addition
|
||||
stoken string
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) Init(ctx context.Context) error {
|
||||
if d.Host == "" {
|
||||
d.Host = "https://siot-share.lenovo.com.cn"
|
||||
}
|
||||
query := map[string]string{
|
||||
"code": d.ShareId,
|
||||
"password": d.SharePwd,
|
||||
}
|
||||
resp, err := d.request(d.Host+"/oneproxy/api/share/v1/access", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.stoken = utils.Json.Get(resp, "data", "stoken").ToString()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files := make([]File, 0)
|
||||
|
||||
var resp Files
|
||||
query := map[string]string{
|
||||
"code": d.ShareId,
|
||||
"num": "5000",
|
||||
"stoken": d.stoken,
|
||||
"path": dir.GetPath(),
|
||||
}
|
||||
_, err := d.request(d.Host+"/oneproxy/api/share/v1/files", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, resp.Data.List...)
|
||||
|
||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||
return src, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
query := map[string]string{
|
||||
"code": d.ShareId,
|
||||
"stoken": d.stoken,
|
||||
"path": file.GetPath(),
|
||||
}
|
||||
resp, err := d.request(d.Host+"/oneproxy/api/share/v1/file/link", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
downloadUrl := d.Host + "/oneproxy/api/share/v1/file/download?code=" + d.ShareId + "&dtoken=" + utils.Json.Get(resp, "data", "param", "dtoken").ToString()
|
||||
|
||||
link := model.Link{
|
||||
URL: downloadUrl,
|
||||
Header: http.Header{
|
||||
"Referer": []string{"https://siot-share.lenovo.com.cn"},
|
||||
},
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) Remove(ctx context.Context, obj model.Obj) error {
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *LenovoNasShare) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*LenovoNasShare)(nil)
|
33
drivers/lenovonas_share/meta.go
Normal file
33
drivers/lenovonas_share/meta.go
Normal file
@ -0,0 +1,33 @@
|
||||
package LenovoNasShare
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootPath
|
||||
ShareId string `json:"share_id" required:"true" help:"The part after the last / in the shared link"`
|
||||
SharePwd string `json:"share_pwd" required:"true" help:"The password of the shared link"`
|
||||
Host string `json:"host" required:"true" default:"https://siot-share.lenovo.com.cn" help:"You can change it to your local area network"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "LenovoNasShare",
|
||||
LocalSort: true,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: true,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &LenovoNasShare{}
|
||||
})
|
||||
}
|
82
drivers/lenovonas_share/types.go
Normal file
82
drivers/lenovonas_share/types.go
Normal file
@ -0,0 +1,82 @@
|
||||
package LenovoNasShare
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
|
||||
_ "github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
func (f *File) UnmarshalJSON(data []byte) error {
|
||||
type Alias File
|
||||
aux := &struct {
|
||||
CreateAt int64 `json:"time"`
|
||||
UpdateAt int64 `json:"chtime"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(f),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.CreateAt = time.Unix(aux.CreateAt, 0)
|
||||
f.UpdateAt = time.Unix(aux.UpdateAt, 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type File struct {
|
||||
FileName string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
CreateAt time.Time `json:"time"`
|
||||
UpdateAt time.Time `json:"chtime"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (f File) GetHash() utils.HashInfo {
|
||||
return utils.HashInfo{}
|
||||
}
|
||||
|
||||
func (f File) GetPath() string {
|
||||
return f.Path
|
||||
}
|
||||
|
||||
func (f File) GetSize() int64 {
|
||||
return f.Size
|
||||
}
|
||||
|
||||
func (f File) GetName() string {
|
||||
return f.FileName
|
||||
}
|
||||
|
||||
func (f File) ModTime() time.Time {
|
||||
return f.UpdateAt
|
||||
}
|
||||
|
||||
func (f File) CreateTime() time.Time {
|
||||
return f.CreateAt
|
||||
}
|
||||
|
||||
func (f File) IsDir() bool {
|
||||
return f.Type == "dir"
|
||||
}
|
||||
|
||||
func (f File) GetID() string {
|
||||
return f.GetPath()
|
||||
}
|
||||
|
||||
func (f File) Thumb() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type Files struct {
|
||||
Data struct {
|
||||
List []File `json:"list"`
|
||||
HasMore bool `json:"has_more"`
|
||||
} `json:"data"`
|
||||
}
|
36
drivers/lenovonas_share/util.go
Normal file
36
drivers/lenovonas_share/util.go
Normal file
@ -0,0 +1,36 @@
|
||||
package LenovoNasShare
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func (d *LenovoNasShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
"origin": "https://siot-share.lenovo.com.cn",
|
||||
"referer": "https://siot-share.lenovo.com.cn/",
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
|
||||
"platform": "web",
|
||||
"app-version": "3",
|
||||
})
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
res, err := req.Execute(method, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := res.Body()
|
||||
result := utils.Json.Get(body, "result").ToBool()
|
||||
if !result {
|
||||
return nil, errors.New(jsoniter.Get(body, "error", "msg").ToString())
|
||||
}
|
||||
return body, nil
|
||||
}
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/alist-org/times"
|
||||
cp "github.com/otiai10/copy"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
@ -30,6 +31,10 @@ type Local struct {
|
||||
model.Storage
|
||||
Addition
|
||||
mkdirPerm int32
|
||||
|
||||
// zero means no limit
|
||||
thumbConcurrency int
|
||||
thumbTokenBucket TokenBucket
|
||||
}
|
||||
|
||||
func (d *Local) Config() driver.Config {
|
||||
@ -62,6 +67,18 @@ func (d *Local) Init(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if d.ThumbConcurrency != "" {
|
||||
v, err := strconv.ParseUint(d.ThumbConcurrency, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.thumbConcurrency = int(v)
|
||||
}
|
||||
if d.thumbConcurrency == 0 {
|
||||
d.thumbTokenBucket = NewNopTokenBucket()
|
||||
} else {
|
||||
d.thumbTokenBucket = NewStaticTokenBucketWithMigration(d.thumbTokenBucket, d.thumbConcurrency)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -126,7 +143,6 @@ func (d *Local) FileInfoToObj(f fs.FileInfo, reqPath string, fullPath string) mo
|
||||
},
|
||||
}
|
||||
return &file
|
||||
|
||||
}
|
||||
func (d *Local) GetMeta(ctx context.Context, path string) (model.Obj, error) {
|
||||
f, err := os.Stat(path)
|
||||
@ -178,7 +194,13 @@ func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
||||
fullPath := file.GetPath()
|
||||
var link model.Link
|
||||
if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" {
|
||||
buf, thumbPath, err := d.getThumb(file)
|
||||
var buf *bytes.Buffer
|
||||
var thumbPath *string
|
||||
err := d.thumbTokenBucket.Do(ctx, func() error {
|
||||
var err error
|
||||
buf, thumbPath, err = d.getThumb(file)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -220,11 +242,22 @@ func (d *Local) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
if utils.IsSubPath(srcPath, dstPath) {
|
||||
return fmt.Errorf("the destination folder is a subfolder of the source folder")
|
||||
}
|
||||
err := os.Rename(srcPath, dstPath)
|
||||
if err != nil {
|
||||
if err := os.Rename(srcPath, dstPath); err != nil && strings.Contains(err.Error(), "invalid cross-device link") {
|
||||
// Handle cross-device file move in local driver
|
||||
if err = d.Copy(ctx, srcObj, dstDir); err != nil {
|
||||
return err
|
||||
} else {
|
||||
// Directly remove file without check recycle bin if successfully copied
|
||||
if srcObj.IsDir() {
|
||||
err = os.RemoveAll(srcObj.GetPath())
|
||||
} else {
|
||||
err = os.Remove(srcObj.GetPath())
|
||||
}
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Local) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
@ -237,22 +270,18 @@ func (d *Local) Rename(ctx context.Context, srcObj model.Obj, newName string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Local) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
func (d *Local) Copy(_ context.Context, srcObj, dstDir model.Obj) error {
|
||||
srcPath := srcObj.GetPath()
|
||||
dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())
|
||||
if utils.IsSubPath(srcPath, dstPath) {
|
||||
return fmt.Errorf("the destination folder is a subfolder of the source folder")
|
||||
}
|
||||
var err error
|
||||
if srcObj.IsDir() {
|
||||
err = utils.CopyDir(srcPath, dstPath)
|
||||
} else {
|
||||
err = utils.CopyFile(srcPath, dstPath)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
// Copy using otiai10/copy to perform more secure & efficient copy
|
||||
return cp.Copy(srcPath, dstPath, cp.Options{
|
||||
Sync: true, // Sync file to disk after copy, may have performance penalty in filesystem such as ZFS
|
||||
PreserveTimes: true,
|
||||
PreserveOwner: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Local) Remove(ctx context.Context, obj model.Obj) error {
|
||||
|
@ -9,6 +9,7 @@ type Addition struct {
|
||||
driver.RootPath
|
||||
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
|
||||
ThumbCacheFolder string `json:"thumb_cache_folder"`
|
||||
ThumbConcurrency string `json:"thumb_concurrency" default:"16" required:"false" help:"Number of concurrent thumbnail generation goroutines. This controls how many thumbnails can be generated in parallel."`
|
||||
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
|
||||
MkdirPerm string `json:"mkdir_perm" default:"777"`
|
||||
RecycleBinPath string `json:"recycle_bin_path" default:"delete permanently" help:"path to recycle bin, delete permanently if empty or keep 'delete permanently'"`
|
||||
|
95
drivers/local/token_bucket.go
Normal file
95
drivers/local/token_bucket.go
Normal file
@ -0,0 +1,95 @@
|
||||
package local
|
||||
|
||||
import "context"
|
||||
|
||||
type TokenBucket interface {
|
||||
Take() <-chan struct{}
|
||||
Put()
|
||||
Do(context.Context, func() error) error
|
||||
}
|
||||
|
||||
// StaticTokenBucket is a bucket with a fixed number of tokens,
|
||||
// where the retrieval and return of tokens are manually controlled.
|
||||
// In the initial state, the bucket is full.
|
||||
type StaticTokenBucket struct {
|
||||
bucket chan struct{}
|
||||
}
|
||||
|
||||
func NewStaticTokenBucket(size int) StaticTokenBucket {
|
||||
bucket := make(chan struct{}, size)
|
||||
for range size {
|
||||
bucket <- struct{}{}
|
||||
}
|
||||
return StaticTokenBucket{bucket: bucket}
|
||||
}
|
||||
|
||||
func NewStaticTokenBucketWithMigration(oldBucket TokenBucket, size int) StaticTokenBucket {
|
||||
if oldBucket != nil {
|
||||
oldStaticBucket, ok := oldBucket.(StaticTokenBucket)
|
||||
if ok {
|
||||
oldSize := cap(oldStaticBucket.bucket)
|
||||
migrateSize := oldSize
|
||||
if size < migrateSize {
|
||||
migrateSize = size
|
||||
}
|
||||
|
||||
bucket := make(chan struct{}, size)
|
||||
for range size - migrateSize {
|
||||
bucket <- struct{}{}
|
||||
}
|
||||
|
||||
if migrateSize != 0 {
|
||||
go func() {
|
||||
for range migrateSize {
|
||||
<-oldStaticBucket.bucket
|
||||
bucket <- struct{}{}
|
||||
}
|
||||
close(oldStaticBucket.bucket)
|
||||
}()
|
||||
}
|
||||
return StaticTokenBucket{bucket: bucket}
|
||||
}
|
||||
}
|
||||
return NewStaticTokenBucket(size)
|
||||
}
|
||||
|
||||
// Take channel maybe closed when local driver is modified.
|
||||
// don't call Put method after the channel is closed.
|
||||
func (b StaticTokenBucket) Take() <-chan struct{} {
|
||||
return b.bucket
|
||||
}
|
||||
|
||||
func (b StaticTokenBucket) Put() {
|
||||
b.bucket <- struct{}{}
|
||||
}
|
||||
|
||||
func (b StaticTokenBucket) Do(ctx context.Context, f func() error) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case _, ok := <-b.Take():
|
||||
if ok {
|
||||
defer b.Put()
|
||||
}
|
||||
}
|
||||
return f()
|
||||
}
|
||||
|
||||
// NopTokenBucket all function calls to this bucket will success immediately
|
||||
type NopTokenBucket struct {
|
||||
nop chan struct{}
|
||||
}
|
||||
|
||||
func NewNopTokenBucket() NopTokenBucket {
|
||||
nop := make(chan struct{})
|
||||
close(nop)
|
||||
return NopTokenBucket{nop}
|
||||
}
|
||||
|
||||
func (b NopTokenBucket) Take() <-chan struct{} {
|
||||
return b.nop
|
||||
}
|
||||
|
||||
func (b NopTokenBucket) Put() {}
|
||||
|
||||
func (b NopTokenBucket) Do(_ context.Context, f func() error) error { return f() }
|
@ -36,12 +36,12 @@ func isSymlinkDir(f fs.FileInfo, path string) bool {
|
||||
|
||||
func GetSnapshot(videoPath string, frameNum int) (imgData *bytes.Buffer, err error) {
|
||||
srcBuf := bytes.NewBuffer(nil)
|
||||
err = ffmpeg.Input(videoPath).Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}).
|
||||
stream := ffmpeg.Input(videoPath).
|
||||
Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}).
|
||||
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
|
||||
WithOutput(srcBuf, os.Stdout).
|
||||
Run()
|
||||
|
||||
if err != nil {
|
||||
GlobalArgs("-loglevel", "error").Silent(true).
|
||||
WithOutput(srcBuf, os.Stdout)
|
||||
if err = stream.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return srcBuf, nil
|
||||
|
@ -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,fileSystemInfo,content.downloadUrl,file,parentReference"
|
||||
nextLink := d.GetMetaUrl(false, path) + "/children?$top=1000&$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)
|
||||
|
@ -118,7 +118,7 @@ func (d *OnedriveAPP) Request(url string, method string, callback base.ReqCallba
|
||||
|
||||
func (d *OnedriveAPP) 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=1000&$expand=thumbnails($select=medium)&$select=id,name,size,lastModifiedDateTime,content.downloadUrl,file,parentReference"
|
||||
for nextLink != "" {
|
||||
var files Files
|
||||
_, err := d.Request(nextLink, http.MethodGet, nil, &files)
|
||||
|
@ -4,30 +4,25 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PikPak struct {
|
||||
model.Storage
|
||||
Addition
|
||||
*Common
|
||||
oauth2Token oauth2.TokenSource
|
||||
RefreshToken string
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
func (d *PikPak) Config() driver.Config {
|
||||
@ -39,10 +34,6 @@ func (d *PikPak) GetAddition() driver.Additional {
|
||||
}
|
||||
|
||||
func (d *PikPak) Init(ctx context.Context) (err error) {
|
||||
if d.ClientID == "" || d.ClientSecret == "" {
|
||||
d.ClientID = "YNxT9w7GMdWvEOKa"
|
||||
d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
}
|
||||
|
||||
if d.Common == nil {
|
||||
d.Common = &Common{
|
||||
@ -50,7 +41,7 @@ func (d *PikPak) Init(ctx context.Context) (err error) {
|
||||
CaptchaToken: "",
|
||||
UserID: "",
|
||||
DeviceID: utils.GetMD5EncodeStr(d.Username + d.Password),
|
||||
UserAgent: BuildCustomUserAgent(utils.GetMD5EncodeStr(d.Username+d.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, ""),
|
||||
UserAgent: "",
|
||||
RefreshCTokenCk: func(token string) {
|
||||
d.Common.CaptchaToken = token
|
||||
op.MustSaveDriverStorage(d)
|
||||
@ -58,31 +49,66 @@ func (d *PikPak) Init(ctx context.Context) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
if d.Platform == "android" {
|
||||
d.ClientID = AndroidClientID
|
||||
d.ClientSecret = AndroidClientSecret
|
||||
d.ClientVersion = AndroidClientVersion
|
||||
d.PackageName = AndroidPackageName
|
||||
d.Algorithms = AndroidAlgorithms
|
||||
d.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(d.Username+d.Password), AndroidClientID, AndroidPackageName, AndroidSdkVersion, AndroidClientVersion, AndroidPackageName, "")
|
||||
} else if d.Platform == "web" {
|
||||
d.ClientID = WebClientID
|
||||
d.ClientSecret = WebClientSecret
|
||||
d.ClientVersion = WebClientVersion
|
||||
d.PackageName = WebPackageName
|
||||
d.Algorithms = WebAlgorithms
|
||||
d.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
|
||||
} else if d.Platform == "pc" {
|
||||
d.ClientID = PCClientID
|
||||
d.ClientSecret = PCClientSecret
|
||||
d.ClientVersion = PCClientVersion
|
||||
d.PackageName = PCPackageName
|
||||
d.Algorithms = PCAlgorithms
|
||||
d.UserAgent = "MainWindow Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) PikPak/2.5.6.4831 Chrome/100.0.4896.160 Electron/18.3.15 Safari/537.36"
|
||||
}
|
||||
|
||||
d.oauth2Token = oauth2.ReuseTokenSource(nil, utils.TokenSource(func() (*oauth2.Token, error) {
|
||||
return oauth2Config.PasswordCredentialsToken(
|
||||
context.WithValue(context.Background(), oauth2.HTTPClient, base.HttpClient),
|
||||
d.Username,
|
||||
d.Password,
|
||||
)
|
||||
}))
|
||||
if d.Addition.CaptchaToken != "" && d.Addition.RefreshToken == "" {
|
||||
d.SetCaptchaToken(d.Addition.CaptchaToken)
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
_ = d.GetUserID()
|
||||
if d.Addition.DeviceID != "" {
|
||||
d.SetDeviceID(d.Addition.DeviceID)
|
||||
} else {
|
||||
d.Addition.DeviceID = d.Common.DeviceID
|
||||
op.MustSaveDriverStorage(d)
|
||||
}
|
||||
// 如果已经有RefreshToken,直接获取AccessToken
|
||||
if d.Addition.RefreshToken != "" {
|
||||
if err = d.refreshToken(d.Addition.RefreshToken); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 如果没有填写RefreshToken,尝试登录 获取 refreshToken
|
||||
if err = d.login(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 获取CaptchaToken
|
||||
_ = d.RefreshCaptchaTokenAtLogin(GetAction(http.MethodGet, "https://api-drive.mypikpak.com/drive/v1/files"), d.Common.UserID)
|
||||
err = d.RefreshCaptchaTokenAtLogin(GetAction(http.MethodGet, "https://api-drive.mypikpak.net/drive/v1/files"), d.Common.GetUserID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新UserAgent
|
||||
d.Common.UserAgent = BuildCustomUserAgent(d.Common.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, d.Common.UserID)
|
||||
if d.Platform == "android" {
|
||||
d.Common.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(d.Username+d.Password), AndroidClientID, AndroidPackageName, AndroidSdkVersion, AndroidClientVersion, AndroidPackageName, d.Common.UserID)
|
||||
}
|
||||
|
||||
// 保存 有效的 RefreshToken
|
||||
d.Addition.RefreshToken = d.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -102,23 +128,36 @@ func (d *PikPak) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
|
||||
|
||||
func (d *PikPak) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var resp File
|
||||
_, err := d.requestWithCaptchaToken(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.GetID()),
|
||||
http.MethodGet, nil, &resp)
|
||||
var url string
|
||||
queryParams := map[string]string{
|
||||
"_magic": "2021",
|
||||
"usage": "FETCH",
|
||||
"thumbnail_size": "SIZE_LARGE",
|
||||
}
|
||||
if !d.DisableMediaLink {
|
||||
queryParams["usage"] = "CACHE"
|
||||
}
|
||||
_, err := d.request(fmt.Sprintf("https://api-drive.mypikpak.net/drive/v1/files/%s", file.GetID()),
|
||||
http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(queryParams)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link := model.Link{
|
||||
URL: resp.WebContentLink,
|
||||
}
|
||||
url = resp.WebContentLink
|
||||
|
||||
if !d.DisableMediaLink && len(resp.Medias) > 0 && resp.Medias[0].Link.Url != "" {
|
||||
log.Debugln("use media link")
|
||||
link.URL = resp.Medias[0].Link.Url
|
||||
url = resp.Medias[0].Link.Url
|
||||
}
|
||||
return &link, nil
|
||||
|
||||
return &model.Link{
|
||||
URL: url,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *PikPak) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files", http.MethodPost, func(req *resty.Request) {
|
||||
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"kind": "drive#folder",
|
||||
"parent_id": parentDir.GetID(),
|
||||
@ -129,7 +168,7 @@ func (d *PikPak) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
|
||||
}
|
||||
|
||||
func (d *PikPak) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files:batchMove", http.MethodPost, func(req *resty.Request) {
|
||||
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files:batchMove", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"ids": []string{srcObj.GetID()},
|
||||
"to": base.Json{
|
||||
@ -141,7 +180,7 @@ func (d *PikPak) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
}
|
||||
|
||||
func (d *PikPak) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files/"+srcObj.GetID(), http.MethodPatch, func(req *resty.Request) {
|
||||
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files/"+srcObj.GetID(), http.MethodPatch, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"name": newName,
|
||||
})
|
||||
@ -150,7 +189,7 @@ func (d *PikPak) Rename(ctx context.Context, srcObj model.Obj, newName string) e
|
||||
}
|
||||
|
||||
func (d *PikPak) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files:batchCopy", http.MethodPost, func(req *resty.Request) {
|
||||
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files:batchCopy", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"ids": []string{srcObj.GetID()},
|
||||
"to": base.Json{
|
||||
@ -162,7 +201,7 @@ func (d *PikPak) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
}
|
||||
|
||||
func (d *PikPak) Remove(ctx context.Context, obj model.Obj) error {
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files:batchTrash", http.MethodPost, func(req *resty.Request) {
|
||||
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files:batchTrash", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"ids": []string{obj.GetID()},
|
||||
})
|
||||
@ -186,7 +225,7 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
}
|
||||
|
||||
var resp UploadTaskData
|
||||
res, err := d.request("https://api-drive.mypikpak.com/drive/v1/files", http.MethodPost, func(req *resty.Request) {
|
||||
res, err := d.request("https://api-drive.mypikpak.net/drive/v1/files", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"kind": "drive#file",
|
||||
"name": stream.GetName(),
|
||||
@ -209,27 +248,17 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
}
|
||||
|
||||
params := resp.Resumable.Params
|
||||
endpoint := strings.Join(strings.Split(params.Endpoint, ".")[1:], ".")
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(params.AccessKeyID, params.AccessKeySecret, params.SecurityToken),
|
||||
Region: aws.String("pikpak"),
|
||||
Endpoint: &endpoint,
|
||||
//endpoint := strings.Join(strings.Split(params.Endpoint, ".")[1:], ".")
|
||||
// web 端上传 返回的endpoint 为 `mypikpak.net` | android 端上传 返回的endpoint 为 `vip-lixian-07.mypikpak.net`·
|
||||
if d.Addition.Platform == "android" {
|
||||
params.Endpoint = "mypikpak.net"
|
||||
}
|
||||
ss, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
if stream.GetSize() <= 10*utils.MB { // 文件大小 小于10MB,改用普通模式上传
|
||||
return d.UploadByOSS(¶ms, stream, up)
|
||||
}
|
||||
uploader := s3manager.NewUploader(ss)
|
||||
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
||||
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
|
||||
}
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: ¶ms.Bucket,
|
||||
Key: ¶ms.Key,
|
||||
Body: stream,
|
||||
}
|
||||
_, err = uploader.UploadWithContext(ctx, input)
|
||||
return err
|
||||
// 分片上传
|
||||
return d.UploadByMultipart(¶ms, stream.GetSize(), stream, up)
|
||||
}
|
||||
|
||||
// 离线下载文件
|
||||
@ -246,7 +275,7 @@ func (d *PikPak) OfflineDownload(ctx context.Context, fileUrl string, parentDir
|
||||
}
|
||||
|
||||
var resp OfflineDownloadResp
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files", http.MethodPost, func(req *resty.Request) {
|
||||
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(requestBody)
|
||||
}, &resp)
|
||||
|
||||
@ -264,7 +293,7 @@ PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING
|
||||
*/
|
||||
func (d *PikPak) OfflineList(ctx context.Context, nextPageToken string, phase []string) ([]OfflineTask, error) {
|
||||
res := make([]OfflineTask, 0)
|
||||
url := "https://api-drive.mypikpak.com/drive/v1/tasks"
|
||||
url := "https://api-drive.mypikpak.net/drive/v1/tasks"
|
||||
|
||||
if len(phase) == 0 {
|
||||
phase = []string{"PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR", "PHASE_TYPE_COMPLETE", "PHASE_TYPE_PENDING"}
|
||||
@ -305,7 +334,7 @@ func (d *PikPak) OfflineList(ctx context.Context, nextPageToken string, phase []
|
||||
}
|
||||
|
||||
func (d *PikPak) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {
|
||||
url := "https://api-drive.mypikpak.com/drive/v1/tasks"
|
||||
url := "https://api-drive.mypikpak.net/drive/v1/tasks"
|
||||
params := map[string]string{
|
||||
"task_ids": strings.Join(taskIDs, ","),
|
||||
"delete_files": strconv.FormatBool(deleteFiles),
|
||||
@ -320,19 +349,4 @@ func (d *PikPak) DeleteOfflineTasks(ctx context.Context, taskIDs []string, delet
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) GetUserID() error {
|
||||
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userID := token.Extra("sub").(string)
|
||||
|
||||
if userID != "" {
|
||||
d.Common.SetUserID(userID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*PikPak)(nil)
|
||||
|
@ -9,9 +9,11 @@ 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"`
|
||||
Platform string `json:"platform" required:"true" default:"web" type:"select" options:"android,web,pc"`
|
||||
RefreshToken string `json:"refresh_token" required:"true" default:""`
|
||||
CaptchaToken string `json:"captcha_token" default:""`
|
||||
DeviceID string `json:"device_id" required:"false" default:""`
|
||||
DisableMediaLink bool `json:"disable_media_link" default:"true"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -80,22 +80,24 @@ type UploadTaskData struct {
|
||||
UploadType string `json:"upload_type"`
|
||||
//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"`
|
||||
Kind string `json:"kind"`
|
||||
Params S3Params `json:"params"`
|
||||
Provider string `json:"provider"`
|
||||
} `json:"resumable"`
|
||||
|
||||
File File `json:"file"`
|
||||
}
|
||||
|
||||
type S3Params 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"`
|
||||
}
|
||||
|
||||
// 添加离线下载响应
|
||||
type OfflineDownloadResp struct {
|
||||
File *string `json:"file"`
|
||||
|
@ -1,50 +1,183 @@
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
var AndroidAlgorithms = []string{
|
||||
"7xOq4Z8s",
|
||||
"QE9/9+IQco",
|
||||
"WdX5J9CPLZp",
|
||||
"NmQ5qFAXqH3w984cYhMeC5TJR8j",
|
||||
"cc44M+l7GDhav",
|
||||
"KxGjo/wHB+Yx8Lf7kMP+/m9I+",
|
||||
"wla81BUVSmDkctHDpUT",
|
||||
"c6wMr1sm1WxiR3i8LDAm3W",
|
||||
"hRLrEQCFNYi0PFPV",
|
||||
"o1J41zIraDtJPNuhBu7Ifb/q3",
|
||||
"U",
|
||||
"RrbZvV0CTu3gaZJ56PVKki4IeP",
|
||||
"NNuRbLckJqUp1Do0YlrKCUP",
|
||||
"UUwnBbipMTvInA0U0E9",
|
||||
"VzGc",
|
||||
}
|
||||
|
||||
var Algorithms = []string{
|
||||
"PAe56I7WZ6FCSkFy77A96jHWcQA27ui80Qy4",
|
||||
"SUbmk67TfdToBAEe2cZyP8vYVeN",
|
||||
"1y3yFSZVWiGN95fw/2FQlRuH/Oy6WnO",
|
||||
"8amLtHJpGzHPz4m9hGz7r+i+8dqQiAk",
|
||||
"tmIEq5yl2g/XWwM3sKZkY4SbL8YUezrvxPksNabUJ",
|
||||
"4QvudeJwgJuSf/qb9/wjC21L5aib",
|
||||
"D1RJd+FZ+LBbt+dAmaIyYrT9gxJm0BB",
|
||||
"1If",
|
||||
"iGZr/SJPUFRkwvC174eelKy",
|
||||
var WebAlgorithms = []string{
|
||||
"fyZ4+p77W1U4zcWBUwefAIFhFxvADWtT1wzolCxhg9q7etmGUjXr",
|
||||
"uSUX02HYJ1IkyLdhINEFcCf7l2",
|
||||
"iWt97bqD/qvjIaPXB2Ja5rsBWtQtBZZmaHH2rMR41",
|
||||
"3binT1s/5a1pu3fGsN",
|
||||
"8YCCU+AIr7pg+yd7CkQEY16lDMwi8Rh4WNp5",
|
||||
"DYS3StqnAEKdGddRP8CJrxUSFh",
|
||||
"crquW+4",
|
||||
"ryKqvW9B9hly+JAymXCIfag5Z",
|
||||
"Hr08T/NDTX1oSJfHk90c",
|
||||
"i",
|
||||
}
|
||||
|
||||
var PCAlgorithms = []string{
|
||||
"KHBJ07an7ROXDoK7Db",
|
||||
"G6n399rSWkl7WcQmw5rpQInurc1DkLmLJqE",
|
||||
"JZD1A3M4x+jBFN62hkr7VDhkkZxb9g3rWqRZqFAAb",
|
||||
"fQnw/AmSlbbI91Ik15gpddGgyU7U",
|
||||
"/Dv9JdPYSj3sHiWjouR95NTQff",
|
||||
"yGx2zuTjbWENZqecNI+edrQgqmZKP",
|
||||
"ljrbSzdHLwbqcRn",
|
||||
"lSHAsqCkGDGxQqqwrVu",
|
||||
"TsWXI81fD1",
|
||||
"vk7hBjawK/rOSrSWajtbMk95nfgf3",
|
||||
}
|
||||
|
||||
const (
|
||||
ClientID = "YNxT9w7GMdWvEOKa"
|
||||
ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
ClientVersion = "1.46.2"
|
||||
PackageName = "com.pikcloud.pikpak"
|
||||
SdkVersion = "2.0.4.204000 "
|
||||
OSSUserAgent = "aliyun-sdk-android/2.9.13(Linux/Android 14/M2004j7ac;UKQ1.231108.001)"
|
||||
OssSecurityTokenHeaderName = "X-OSS-Security-Token"
|
||||
ThreadsNum = 10
|
||||
)
|
||||
|
||||
const (
|
||||
AndroidClientID = "YNxT9w7GMdWvEOKa"
|
||||
AndroidClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
AndroidClientVersion = "1.49.3"
|
||||
AndroidPackageName = "com.pikcloud.pikpak"
|
||||
AndroidSdkVersion = "2.0.4.204101"
|
||||
WebClientID = "YUMx5nI8ZU8Ap8pm"
|
||||
WebClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
WebClientVersion = "undefined"
|
||||
WebPackageName = "drive.mypikpak.com"
|
||||
WebSdkVersion = "8.0.3"
|
||||
PCClientID = "YvtoWO6GNHiuCl7x"
|
||||
PCClientSecret = "1NIH5R1IEe2pAxZE3hv3uA"
|
||||
PCClientVersion = "undefined" // 2.5.6.4831
|
||||
PCPackageName = "mypikpak.com"
|
||||
PCSdkVersion = "8.0.3"
|
||||
)
|
||||
|
||||
func (d *PikPak) login() error {
|
||||
// 检查用户名和密码是否为空
|
||||
if d.Addition.Username == "" || d.Addition.Password == "" {
|
||||
return errors.New("username or password is empty")
|
||||
}
|
||||
|
||||
url := "https://user.mypikpak.net/v1/auth/signin"
|
||||
// 使用 用户填写的 CaptchaToken —————— (验证后的captcha_token)
|
||||
if d.GetCaptchaToken() == "" {
|
||||
if err := d.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), d.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var e ErrResp
|
||||
res, err := base.RestyClient.SetRetryCount(1).R().SetError(&e).SetBody(base.Json{
|
||||
"captcha_token": d.GetCaptchaToken(),
|
||||
"client_id": d.ClientID,
|
||||
"client_secret": d.ClientSecret,
|
||||
"username": d.Username,
|
||||
"password": d.Password,
|
||||
}).SetQueryParam("client_id", d.ClientID).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
return &e
|
||||
}
|
||||
data := res.Body()
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
d.Common.SetUserID(jsoniter.Get(data, "sub").ToString())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) refreshToken(refreshToken string) error {
|
||||
url := "https://user.mypikpak.net/v1/auth/token"
|
||||
var e ErrResp
|
||||
res, err := base.RestyClient.SetRetryCount(1).R().SetError(&e).
|
||||
SetHeader("user-agent", "").SetBody(base.Json{
|
||||
"client_id": d.ClientID,
|
||||
"client_secret": d.ClientSecret,
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refreshToken,
|
||||
}).SetQueryParam("client_id", d.ClientID).Post(url)
|
||||
if err != nil {
|
||||
d.Status = err.Error()
|
||||
op.MustSaveDriverStorage(d)
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 4126 {
|
||||
// 1. 未填写 username 或 password
|
||||
if d.Addition.Username == "" || d.Addition.Password == "" {
|
||||
return errors.New("refresh_token invalid, please re-provide refresh_token")
|
||||
} else {
|
||||
// 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()
|
||||
d.Common.SetUserID(jsoniter.Get(data, "sub").ToString())
|
||||
d.Addition.RefreshToken = d.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
req.SetHeaders(map[string]string{
|
||||
//"Authorization": "Bearer " + d.AccessToken,
|
||||
"User-Agent": d.GetUserAgent(),
|
||||
"X-Device-ID": d.GetDeviceID(),
|
||||
"X-Captcha-Token": d.GetCaptchaToken(),
|
||||
})
|
||||
if d.AccessToken != "" {
|
||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||
}
|
||||
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
@ -59,48 +192,25 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if e.IsError() {
|
||||
return nil, &e
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *PikPak) requestWithCaptchaToken(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
|
||||
data, err := d.request(url, method, func(req *resty.Request) {
|
||||
req.SetHeaders(map[string]string{
|
||||
"User-Agent": d.GetUserAgent(),
|
||||
"X-Device-ID": d.GetDeviceID(),
|
||||
"X-Captcha-Token": d.GetCaptchaToken(),
|
||||
})
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
}, resp)
|
||||
|
||||
errResp, ok := err.(*ErrResp)
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch errResp.ErrorCode {
|
||||
switch e.ErrorCode {
|
||||
case 0:
|
||||
return data, nil
|
||||
//case 4122, 4121, 10, 16:
|
||||
// if d.refreshTokenFunc != nil {
|
||||
// if err = xc.refreshTokenFunc(); err == nil {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// return nil, err
|
||||
return res.Body(), nil
|
||||
case 4122, 4121, 16:
|
||||
// access_token 过期
|
||||
if err1 := d.refreshToken(d.RefreshToken); err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
case 9: // 验证码token过期
|
||||
if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.Common.UserID); err != nil {
|
||||
if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
case 10: // 操作频繁
|
||||
return nil, errors.New(e.ErrorDescription)
|
||||
default:
|
||||
return nil, err
|
||||
return nil, errors.New(e.Error())
|
||||
}
|
||||
return d.requestWithCaptchaToken(url, method, callback, resp)
|
||||
}
|
||||
|
||||
func (d *PikPak) getFiles(id string) ([]File, error) {
|
||||
@ -119,7 +229,7 @@ func (d *PikPak) getFiles(id string) ([]File, error) {
|
||||
"page_token": pageToken,
|
||||
}
|
||||
var resp Files
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files", http.MethodGet, func(req *resty.Request) {
|
||||
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
@ -141,8 +251,13 @@ type Common struct {
|
||||
CaptchaToken string
|
||||
UserID string
|
||||
// 必要值,签名相关
|
||||
DeviceID string
|
||||
UserAgent string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ClientVersion string
|
||||
PackageName string
|
||||
Algorithms []string
|
||||
DeviceID string
|
||||
UserAgent string
|
||||
// 验证码token刷新成功回调
|
||||
RefreshCTokenCk func(token string)
|
||||
}
|
||||
@ -228,11 +343,15 @@ func (c *Common) GetDeviceID() string {
|
||||
return c.DeviceID
|
||||
}
|
||||
|
||||
func (c *Common) GetUserID() string {
|
||||
return c.UserID
|
||||
}
|
||||
|
||||
// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
|
||||
func (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error {
|
||||
metas := map[string]string{
|
||||
"client_version": ClientVersion,
|
||||
"package_name": PackageName,
|
||||
"client_version": d.ClientVersion,
|
||||
"package_name": d.PackageName,
|
||||
"user_id": userID,
|
||||
}
|
||||
metas["timestamp"], metas["captcha_sign"] = d.Common.GetCaptchaSign()
|
||||
@ -255,8 +374,8 @@ func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {
|
||||
// GetCaptchaSign 获取验证码签名
|
||||
func (c *Common) GetCaptchaSign() (timestamp, sign string) {
|
||||
timestamp = fmt.Sprint(time.Now().UnixMilli())
|
||||
str := fmt.Sprint(ClientID, ClientVersion, PackageName, c.DeviceID, timestamp)
|
||||
for _, algorithm := range Algorithms {
|
||||
str := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)
|
||||
for _, algorithm := range c.Algorithms {
|
||||
str = utils.GetMD5EncodeStr(str + algorithm)
|
||||
}
|
||||
sign = "1." + str
|
||||
@ -267,16 +386,16 @@ func (c *Common) GetCaptchaSign() (timestamp, sign string) {
|
||||
func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) error {
|
||||
param := CaptchaTokenRequest{
|
||||
Action: action,
|
||||
CaptchaToken: d.Common.CaptchaToken,
|
||||
ClientID: ClientID,
|
||||
DeviceID: d.Common.DeviceID,
|
||||
CaptchaToken: d.GetCaptchaToken(),
|
||||
ClientID: d.ClientID,
|
||||
DeviceID: d.GetDeviceID(),
|
||||
Meta: metas,
|
||||
RedirectUri: "xlaccsdk01://xbase.cloud/callback?state=harbor",
|
||||
}
|
||||
var e ErrResp
|
||||
var resp CaptchaTokenResponse
|
||||
_, err := d.request("https://user.mypikpak.com/v1/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetError(&e).SetBody(param)
|
||||
_, err := d.request("https://user.mypikpak.net/v1/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetError(&e).SetBody(param).SetQueryParam("client_id", d.ClientID)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
@ -284,20 +403,250 @@ func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) err
|
||||
}
|
||||
|
||||
if e.IsError() {
|
||||
return &e
|
||||
return errors.New(e.Error())
|
||||
}
|
||||
|
||||
if resp.Url != "" {
|
||||
return fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
|
||||
}
|
||||
|
||||
if resp.CaptchaToken == "" {
|
||||
return fmt.Errorf("empty captchaToken")
|
||||
}
|
||||
|
||||
if d.Common.RefreshCTokenCk != nil {
|
||||
d.Common.RefreshCTokenCk(resp.CaptchaToken)
|
||||
}
|
||||
d.Common.SetCaptchaToken(resp.CaptchaToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) UploadByOSS(params *S3Params, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
ossClient, err := oss.New(params.Endpoint, params.AccessKeyID, params.AccessKeySecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bucket, err := ossClient.Bucket(params.Bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.PutObject(params.Key, stream, OssOption(params)...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PikPak) UploadByMultipart(params *S3Params, fileSize int64, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
var (
|
||||
chunks []oss.FileChunk
|
||||
parts []oss.UploadPart
|
||||
imur oss.InitiateMultipartUploadResult
|
||||
ossClient *oss.Client
|
||||
bucket *oss.Bucket
|
||||
err error
|
||||
)
|
||||
|
||||
tmpF, err := stream.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ossClient, err = oss.New(params.Endpoint, params.AccessKeyID, params.AccessKeySecret); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bucket, err = ossClient.Bucket(params.Bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Hour * 12)
|
||||
defer ticker.Stop()
|
||||
// 设置超时
|
||||
timeout := time.NewTimer(time.Hour * 24)
|
||||
|
||||
if chunks, err = SplitFile(fileSize); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if imur, err = bucket.InitiateMultipartUpload(params.Key,
|
||||
oss.SetHeader(OssSecurityTokenHeaderName, params.SecurityToken),
|
||||
oss.UserAgentHeader(OSSUserAgent),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(chunks))
|
||||
|
||||
chunksCh := make(chan oss.FileChunk)
|
||||
errCh := make(chan error)
|
||||
UploadedPartsCh := make(chan oss.UploadPart)
|
||||
quit := make(chan struct{})
|
||||
|
||||
// producer
|
||||
go chunksProducer(chunksCh, chunks)
|
||||
go func() {
|
||||
wg.Wait()
|
||||
quit <- struct{}{}
|
||||
}()
|
||||
|
||||
// consumers
|
||||
for i := 0; i < ThreadsNum; i++ {
|
||||
go func(threadId int) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errCh <- fmt.Errorf("recovered in %v", r)
|
||||
}
|
||||
}()
|
||||
for chunk := range chunksCh {
|
||||
var part oss.UploadPart // 出现错误就继续尝试,共尝试3次
|
||||
for retry := 0; retry < 3; retry++ {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
errCh <- errors.Wrap(err, "ossToken 过期")
|
||||
default:
|
||||
}
|
||||
|
||||
buf := make([]byte, chunk.Size)
|
||||
if _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) {
|
||||
continue
|
||||
}
|
||||
|
||||
b := bytes.NewBuffer(buf)
|
||||
if part, err = bucket.UploadPart(imur, b, chunk.Size, chunk.Number, OssOption(params)...); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
errCh <- errors.Wrap(err, fmt.Sprintf("上传 %s 的第%d个分片时出现错误:%v", stream.GetName(), chunk.Number, err))
|
||||
}
|
||||
UploadedPartsCh <- part
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for part := range UploadedPartsCh {
|
||||
parts = append(parts, part)
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// ossToken 过期
|
||||
return err
|
||||
case <-quit:
|
||||
break LOOP
|
||||
case <-errCh:
|
||||
return err
|
||||
case <-timeout.C:
|
||||
return fmt.Errorf("time out")
|
||||
}
|
||||
}
|
||||
|
||||
// EOF错误是xml的Unmarshal导致的,响应其实是json格式,所以实际上上传是成功的
|
||||
if _, err = bucket.CompleteMultipartUpload(imur, parts, OssOption(params)...); err != nil && !errors.Is(err, io.EOF) {
|
||||
// 当文件名含有 &< 这两个字符之一时响应的xml解析会出现错误,实际上上传是成功的
|
||||
if filename := filepath.Base(stream.GetName()); !strings.ContainsAny(filename, "&<") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
|
||||
for _, chunk := range chunks {
|
||||
ch <- chunk
|
||||
}
|
||||
}
|
||||
|
||||
func SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {
|
||||
for i := int64(1); i < 10; i++ {
|
||||
if fileSize < i*utils.GB { // 文件大小小于iGB时分为i*100片
|
||||
if chunks, err = SplitFileByPartNum(fileSize, int(i*100)); err != nil {
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if fileSize > 9*utils.GB { // 文件大小大于9GB时分为1000片
|
||||
if chunks, err = SplitFileByPartNum(fileSize, 1000); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 单个分片大小不能小于1MB
|
||||
if chunks[0].Size < 1*utils.MB {
|
||||
if chunks, err = SplitFileByPartSize(fileSize, 1*utils.MB); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SplitFileByPartNum splits big file into parts by the num of parts.
|
||||
// Split the file with specified parts count, returns the split result when error is nil.
|
||||
func SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {
|
||||
if chunkNum <= 0 || chunkNum > 10000 {
|
||||
return nil, errors.New("chunkNum invalid")
|
||||
}
|
||||
|
||||
if int64(chunkNum) > fileSize {
|
||||
return nil, errors.New("oss: chunkNum invalid")
|
||||
}
|
||||
|
||||
var chunks []oss.FileChunk
|
||||
chunk := oss.FileChunk{}
|
||||
chunkN := (int64)(chunkNum)
|
||||
for i := int64(0); i < chunkN; i++ {
|
||||
chunk.Number = int(i + 1)
|
||||
chunk.Offset = i * (fileSize / chunkN)
|
||||
if i == chunkN-1 {
|
||||
chunk.Size = fileSize/chunkN + fileSize%chunkN
|
||||
} else {
|
||||
chunk.Size = fileSize / chunkN
|
||||
}
|
||||
chunks = append(chunks, chunk)
|
||||
}
|
||||
|
||||
return chunks, nil
|
||||
}
|
||||
|
||||
// SplitFileByPartSize splits big file into parts by the size of parts.
|
||||
// Splits the file by the part size. Returns the FileChunk when error is nil.
|
||||
func SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, error) {
|
||||
if chunkSize <= 0 {
|
||||
return nil, errors.New("chunkSize invalid")
|
||||
}
|
||||
|
||||
chunkN := fileSize / chunkSize
|
||||
if chunkN >= 10000 {
|
||||
return nil, errors.New("Too many parts, please increase part size")
|
||||
}
|
||||
|
||||
var chunks []oss.FileChunk
|
||||
chunk := oss.FileChunk{}
|
||||
for i := int64(0); i < chunkN; i++ {
|
||||
chunk.Number = int(i + 1)
|
||||
chunk.Offset = i * chunkSize
|
||||
chunk.Size = chunkSize
|
||||
chunks = append(chunks, chunk)
|
||||
}
|
||||
|
||||
if fileSize%chunkSize > 0 {
|
||||
chunk.Number = len(chunks) + 1
|
||||
chunk.Offset = int64(len(chunks)) * chunkSize
|
||||
chunk.Size = fileSize % chunkSize
|
||||
chunks = append(chunks, chunk)
|
||||
}
|
||||
|
||||
return chunks, nil
|
||||
}
|
||||
|
||||
// OssOption get options
|
||||
func OssOption(params *S3Params) []oss.Option {
|
||||
options := []oss.Option{
|
||||
oss.SetHeader(OssSecurityTokenHeaderName, params.SecurityToken),
|
||||
oss.UserAgentHeader(OSSUserAgent),
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
@ -2,20 +2,20 @@ package pikpak_share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/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
|
||||
oauth2Token oauth2.TokenSource
|
||||
*Common
|
||||
PassCodeToken string
|
||||
}
|
||||
|
||||
@ -28,32 +28,57 @@ func (d *PikPakShare) GetAddition() driver.Additional {
|
||||
}
|
||||
|
||||
func (d *PikPakShare) Init(ctx context.Context) error {
|
||||
if d.ClientID == "" || d.ClientSecret == "" {
|
||||
d.ClientID = "YNxT9w7GMdWvEOKa"
|
||||
d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
if d.Common == nil {
|
||||
d.Common = &Common{
|
||||
DeviceID: utils.GetMD5EncodeStr(d.Addition.ShareId + d.Addition.SharePwd + time.Now().String()),
|
||||
UserAgent: "",
|
||||
RefreshCTokenCk: func(token string) {
|
||||
d.Common.CaptchaToken = token
|
||||
op.MustSaveDriverStorage(d)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: d.ClientID,
|
||||
ClientSecret: d.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://user.mypikpak.com/v1/auth/signin",
|
||||
TokenURL: "https://user.mypikpak.com/v1/auth/token",
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
},
|
||||
if d.Addition.DeviceID != "" {
|
||||
d.SetDeviceID(d.Addition.DeviceID)
|
||||
} else {
|
||||
d.Addition.DeviceID = d.Common.DeviceID
|
||||
op.MustSaveDriverStorage(d)
|
||||
}
|
||||
|
||||
d.oauth2Token = oauth2.ReuseTokenSource(nil, utils.TokenSource(func() (*oauth2.Token, error) {
|
||||
return oauth2Config.PasswordCredentialsToken(
|
||||
context.WithValue(context.Background(), oauth2.HTTPClient, base.HttpClient),
|
||||
d.Username,
|
||||
d.Password,
|
||||
)
|
||||
}))
|
||||
if d.Platform == "android" {
|
||||
d.ClientID = AndroidClientID
|
||||
d.ClientSecret = AndroidClientSecret
|
||||
d.ClientVersion = AndroidClientVersion
|
||||
d.PackageName = AndroidPackageName
|
||||
d.Algorithms = AndroidAlgorithms
|
||||
d.UserAgent = BuildCustomUserAgent(d.GetDeviceID(), AndroidClientID, AndroidPackageName, AndroidSdkVersion, AndroidClientVersion, AndroidPackageName, "")
|
||||
} else if d.Platform == "web" {
|
||||
d.ClientID = WebClientID
|
||||
d.ClientSecret = WebClientSecret
|
||||
d.ClientVersion = WebClientVersion
|
||||
d.PackageName = WebPackageName
|
||||
d.Algorithms = WebAlgorithms
|
||||
d.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
|
||||
} else if d.Platform == "pc" {
|
||||
d.ClientID = PCClientID
|
||||
d.ClientSecret = PCClientSecret
|
||||
d.ClientVersion = PCClientVersion
|
||||
d.PackageName = PCPackageName
|
||||
d.Algorithms = PCAlgorithms
|
||||
d.UserAgent = "MainWindow Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) PikPak/2.5.6.4831 Chrome/100.0.4896.160 Electron/18.3.15 Safari/537.36"
|
||||
}
|
||||
|
||||
// 获取CaptchaToken
|
||||
err := d.RefreshCaptchaToken(GetAction(http.MethodGet, "https://api-drive.mypikpak.net/drive/v1/share:batch_file_info"), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.SharePwd != "" {
|
||||
return d.getSharePassToken()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -78,7 +103,7 @@ func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkA
|
||||
"file_id": file.GetID(),
|
||||
"pass_code_token": d.PassCodeToken,
|
||||
}
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/share/file_info", http.MethodGet, func(req *resty.Request) {
|
||||
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/share/file_info", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
@ -87,13 +112,18 @@ func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkA
|
||||
|
||||
downloadUrl := resp.FileInfo.WebContentLink
|
||||
if downloadUrl == "" && len(resp.FileInfo.Medias) > 0 {
|
||||
downloadUrl = resp.FileInfo.Medias[0].Link.Url
|
||||
// 使用转码后的链接
|
||||
if d.Addition.UseTransCodingAddress && len(resp.FileInfo.Medias) > 1 {
|
||||
downloadUrl = resp.FileInfo.Medias[1].Link.Url
|
||||
} else {
|
||||
downloadUrl = resp.FileInfo.Medias[0].Link.Url
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
link := model.Link{
|
||||
return &model.Link{
|
||||
URL: downloadUrl,
|
||||
}
|
||||
return &link, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*PikPakShare)(nil)
|
||||
|
@ -7,12 +7,11 @@ 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"`
|
||||
ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
|
||||
ShareId string `json:"share_id" required:"true"`
|
||||
SharePwd string `json:"share_pwd"`
|
||||
Platform string `json:"platform" default:"web" required:"true" type:"select" options:"android,web,pc"`
|
||||
DeviceID string `json:"device_id" required:"false" default:""`
|
||||
UseTransCodingAddress bool `json:"use_transcoding_address" required:"true" default:"false"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
@ -1,20 +1,16 @@
|
||||
package pikpak_share
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type RespErr struct {
|
||||
ErrorCode int `json:"error_code"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type ShareResp struct {
|
||||
ShareStatus string `json:"share_status"`
|
||||
ShareStatusText string `json:"share_status_text"`
|
||||
ShareStatus string `json:"share_status"`
|
||||
ShareStatusText string `json:"share_status_text"`
|
||||
FileInfo File `json:"file_info"`
|
||||
Files []File `json:"files"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
@ -78,3 +74,32 @@ type Media struct {
|
||||
IsVisible bool `json:"is_visible"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
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 ErrResp struct {
|
||||
ErrorCode int64 `json:"error_code"`
|
||||
ErrorMsg string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
func (e *ErrResp) IsError() bool {
|
||||
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
|
||||
}
|
||||
|
||||
func (e *ErrResp) Error() string {
|
||||
return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)
|
||||
}
|
||||
|
@ -1,21 +1,91 @@
|
||||
package pikpak_share
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
var AndroidAlgorithms = []string{
|
||||
"7xOq4Z8s",
|
||||
"QE9/9+IQco",
|
||||
"WdX5J9CPLZp",
|
||||
"NmQ5qFAXqH3w984cYhMeC5TJR8j",
|
||||
"cc44M+l7GDhav",
|
||||
"KxGjo/wHB+Yx8Lf7kMP+/m9I+",
|
||||
"wla81BUVSmDkctHDpUT",
|
||||
"c6wMr1sm1WxiR3i8LDAm3W",
|
||||
"hRLrEQCFNYi0PFPV",
|
||||
"o1J41zIraDtJPNuhBu7Ifb/q3",
|
||||
"U",
|
||||
"RrbZvV0CTu3gaZJ56PVKki4IeP",
|
||||
"NNuRbLckJqUp1Do0YlrKCUP",
|
||||
"UUwnBbipMTvInA0U0E9",
|
||||
"VzGc",
|
||||
}
|
||||
|
||||
var WebAlgorithms = []string{
|
||||
"fyZ4+p77W1U4zcWBUwefAIFhFxvADWtT1wzolCxhg9q7etmGUjXr",
|
||||
"uSUX02HYJ1IkyLdhINEFcCf7l2",
|
||||
"iWt97bqD/qvjIaPXB2Ja5rsBWtQtBZZmaHH2rMR41",
|
||||
"3binT1s/5a1pu3fGsN",
|
||||
"8YCCU+AIr7pg+yd7CkQEY16lDMwi8Rh4WNp5",
|
||||
"DYS3StqnAEKdGddRP8CJrxUSFh",
|
||||
"crquW+4",
|
||||
"ryKqvW9B9hly+JAymXCIfag5Z",
|
||||
"Hr08T/NDTX1oSJfHk90c",
|
||||
"i",
|
||||
}
|
||||
|
||||
var PCAlgorithms = []string{
|
||||
"KHBJ07an7ROXDoK7Db",
|
||||
"G6n399rSWkl7WcQmw5rpQInurc1DkLmLJqE",
|
||||
"JZD1A3M4x+jBFN62hkr7VDhkkZxb9g3rWqRZqFAAb",
|
||||
"fQnw/AmSlbbI91Ik15gpddGgyU7U",
|
||||
"/Dv9JdPYSj3sHiWjouR95NTQff",
|
||||
"yGx2zuTjbWENZqecNI+edrQgqmZKP",
|
||||
"ljrbSzdHLwbqcRn",
|
||||
"lSHAsqCkGDGxQqqwrVu",
|
||||
"TsWXI81fD1",
|
||||
"vk7hBjawK/rOSrSWajtbMk95nfgf3",
|
||||
}
|
||||
|
||||
const (
|
||||
AndroidClientID = "YNxT9w7GMdWvEOKa"
|
||||
AndroidClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
AndroidClientVersion = "1.49.3"
|
||||
AndroidPackageName = "com.pikcloud.pikpak"
|
||||
AndroidSdkVersion = "2.0.4.204101"
|
||||
WebClientID = "YUMx5nI8ZU8Ap8pm"
|
||||
WebClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
||||
WebClientVersion = "undefined"
|
||||
WebPackageName = "drive.mypikpak.com"
|
||||
WebSdkVersion = "8.0.3"
|
||||
PCClientID = "YvtoWO6GNHiuCl7x"
|
||||
PCClientSecret = "1NIH5R1IEe2pAxZE3hv3uA"
|
||||
PCClientVersion = "undefined" // 2.5.6.4831
|
||||
PCPackageName = "mypikpak.com"
|
||||
PCSdkVersion = "8.0.3"
|
||||
)
|
||||
|
||||
func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
|
||||
token, err := d.oauth2Token.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
|
||||
req.SetHeaders(map[string]string{
|
||||
"User-Agent": d.GetUserAgent(),
|
||||
"X-Client-ID": d.GetClientID(),
|
||||
"X-Device-ID": d.GetDeviceID(),
|
||||
"X-Captcha-Token": d.GetCaptchaToken(),
|
||||
})
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
@ -23,16 +93,25 @@ func (d *PikPakShare) request(url string, method string, callback base.ReqCallba
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var e RespErr
|
||||
var e ErrResp
|
||||
req.SetError(&e)
|
||||
res, err := req.Execute(method, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
return nil, errors.New(e.Error)
|
||||
switch e.ErrorCode {
|
||||
case 0:
|
||||
return res.Body(), nil
|
||||
case 9: // 验证码token过期
|
||||
if err = d.RefreshCaptchaToken(GetAction(method, url), ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
case 10: // 操作频繁
|
||||
return nil, errors.New(e.ErrorDescription)
|
||||
default:
|
||||
return nil, errors.New(e.Error())
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *PikPakShare) getSharePassToken() error {
|
||||
@ -43,7 +122,7 @@ func (d *PikPakShare) getSharePassToken() error {
|
||||
"limit": "100",
|
||||
}
|
||||
var resp ShareResp
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/share", http.MethodGet, func(req *resty.Request) {
|
||||
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/share", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
@ -71,7 +150,7 @@ func (d *PikPakShare) getFiles(id string) ([]File, error) {
|
||||
"pass_code_token": d.PassCodeToken,
|
||||
}
|
||||
var resp ShareResp
|
||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/share/detail", http.MethodGet, func(req *resty.Request) {
|
||||
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/share/detail", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
@ -92,3 +171,161 @@ func (d *PikPakShare) getFiles(id string) ([]File, error) {
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
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
|
||||
// 必要值,签名相关
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ClientVersion string
|
||||
PackageName string
|
||||
Algorithms []string
|
||||
DeviceID string
|
||||
UserAgent string
|
||||
// 验证码token刷新成功回调
|
||||
RefreshCTokenCk func(token string)
|
||||
}
|
||||
|
||||
func (c *Common) SetUserAgent(userAgent string) {
|
||||
c.UserAgent = userAgent
|
||||
}
|
||||
|
||||
func (c *Common) SetCaptchaToken(captchaToken string) {
|
||||
c.CaptchaToken = captchaToken
|
||||
}
|
||||
|
||||
func (c *Common) SetDeviceID(deviceID string) {
|
||||
c.DeviceID = deviceID
|
||||
}
|
||||
|
||||
func (c *Common) GetCaptchaToken() string {
|
||||
return c.CaptchaToken
|
||||
}
|
||||
|
||||
func (c *Common) GetClientID() string {
|
||||
return c.ClientID
|
||||
}
|
||||
|
||||
func (c *Common) GetUserAgent() string {
|
||||
return c.UserAgent
|
||||
}
|
||||
|
||||
func (c *Common) GetDeviceID() string {
|
||||
return c.DeviceID
|
||||
}
|
||||
|
||||
func generateDeviceSign(deviceID, packageName string) string {
|
||||
|
||||
signatureBase := fmt.Sprintf("%s%s%s%s", deviceID, packageName, "1", "appkey")
|
||||
|
||||
sha1Hash := sha1.New()
|
||||
sha1Hash.Write([]byte(signatureBase))
|
||||
sha1Result := sha1Hash.Sum(nil)
|
||||
|
||||
sha1String := hex.EncodeToString(sha1Result)
|
||||
|
||||
md5Hash := md5.New()
|
||||
md5Hash.Write([]byte(sha1String))
|
||||
md5Result := md5Hash.Sum(nil)
|
||||
|
||||
md5String := hex.EncodeToString(md5Result)
|
||||
|
||||
deviceSign := fmt.Sprintf("div101.%s%s", deviceID, md5String)
|
||||
|
||||
return deviceSign
|
||||
}
|
||||
|
||||
func BuildCustomUserAgent(deviceID, clientID, appName, sdkVersion, clientVersion, packageName, userID string) string {
|
||||
deviceSign := generateDeviceSign(deviceID, packageName)
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("ANDROID-%s/%s ", appName, clientVersion))
|
||||
sb.WriteString("protocolVersion/200 ")
|
||||
sb.WriteString("accesstype/ ")
|
||||
sb.WriteString(fmt.Sprintf("clientid/%s ", clientID))
|
||||
sb.WriteString(fmt.Sprintf("clientversion/%s ", clientVersion))
|
||||
sb.WriteString("action_type/ ")
|
||||
sb.WriteString("networktype/WIFI ")
|
||||
sb.WriteString("sessionid/ ")
|
||||
sb.WriteString(fmt.Sprintf("deviceid/%s ", deviceID))
|
||||
sb.WriteString("providername/NONE ")
|
||||
sb.WriteString(fmt.Sprintf("devicesign/%s ", deviceSign))
|
||||
sb.WriteString("refresh_token/ ")
|
||||
sb.WriteString(fmt.Sprintf("sdkversion/%s ", sdkVersion))
|
||||
sb.WriteString(fmt.Sprintf("datetime/%d ", time.Now().UnixMilli()))
|
||||
sb.WriteString(fmt.Sprintf("usrno/%s ", userID))
|
||||
sb.WriteString(fmt.Sprintf("appname/android-%s ", appName))
|
||||
sb.WriteString(fmt.Sprintf("session_origin/ "))
|
||||
sb.WriteString(fmt.Sprintf("grant_type/ "))
|
||||
sb.WriteString(fmt.Sprintf("appid/ "))
|
||||
sb.WriteString(fmt.Sprintf("clientip/ "))
|
||||
sb.WriteString(fmt.Sprintf("devicename/Xiaomi_M2004j7ac "))
|
||||
sb.WriteString(fmt.Sprintf("osversion/13 "))
|
||||
sb.WriteString(fmt.Sprintf("platformversion/10 "))
|
||||
sb.WriteString(fmt.Sprintf("accessmode/ "))
|
||||
sb.WriteString(fmt.Sprintf("devicemodel/M2004J7AC "))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// RefreshCaptchaToken 刷新验证码token
|
||||
func (d *PikPakShare) RefreshCaptchaToken(action, userID string) error {
|
||||
metas := map[string]string{
|
||||
"client_version": d.ClientVersion,
|
||||
"package_name": d.PackageName,
|
||||
"user_id": userID,
|
||||
}
|
||||
metas["timestamp"], metas["captcha_sign"] = d.Common.GetCaptchaSign()
|
||||
return d.refreshCaptchaToken(action, metas)
|
||||
}
|
||||
|
||||
// GetCaptchaSign 获取验证码签名
|
||||
func (c *Common) GetCaptchaSign() (timestamp, sign string) {
|
||||
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
|
||||
}
|
||||
|
||||
// refreshCaptchaToken 刷新CaptchaToken
|
||||
func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string) error {
|
||||
param := CaptchaTokenRequest{
|
||||
Action: action,
|
||||
CaptchaToken: d.GetCaptchaToken(),
|
||||
ClientID: d.ClientID,
|
||||
DeviceID: d.GetDeviceID(),
|
||||
Meta: metas,
|
||||
}
|
||||
var e ErrResp
|
||||
var resp CaptchaTokenResponse
|
||||
_, err := d.request("https://user.mypikpak.net/v1/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetError(&e).SetBody(param)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.IsError() {
|
||||
return errors.New(e.Error())
|
||||
}
|
||||
|
||||
//if resp.Url != "" {
|
||||
// return fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
|
||||
//}
|
||||
|
||||
if d.Common.RefreshCTokenCk != nil {
|
||||
d.Common.RefreshCTokenCk(resp.CaptchaToken)
|
||||
}
|
||||
d.Common.SetCaptchaToken(resp.CaptchaToken)
|
||||
return nil
|
||||
}
|
||||
|
174
drivers/quark_uc_tv/driver.go
Normal file
174
drivers/quark_uc_tv/driver.go
Normal file
@ -0,0 +1,174 @@
|
||||
package quark_uc_tv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type QuarkUCTV struct {
|
||||
*QuarkUCTVCommon
|
||||
model.Storage
|
||||
Addition
|
||||
config driver.Config
|
||||
conf Conf
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Config() driver.Config {
|
||||
return d.config
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Init(ctx context.Context) error {
|
||||
|
||||
if d.Addition.DeviceID == "" {
|
||||
d.Addition.DeviceID = utils.GetMD5EncodeStr(time.Now().String())
|
||||
}
|
||||
op.MustSaveDriverStorage(d)
|
||||
|
||||
if d.QuarkUCTVCommon == nil {
|
||||
d.QuarkUCTVCommon = &QuarkUCTVCommon{
|
||||
AccessToken: "",
|
||||
}
|
||||
}
|
||||
ctx1, cancelFunc := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancelFunc()
|
||||
if d.Addition.RefreshToken == "" {
|
||||
if d.Addition.QueryToken == "" {
|
||||
qrData, err := d.getLoginCode(ctx1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 展示二维码
|
||||
qrTemplate := `<body>
|
||||
<img src="data:image/jpeg;base64,%s"/>
|
||||
</body>`
|
||||
qrPage := fmt.Sprintf(qrTemplate, qrData)
|
||||
return fmt.Errorf("need verify: \n%s", qrPage)
|
||||
} else {
|
||||
// 通过query token获取code -> refresh token
|
||||
code, err := d.getCode(ctx1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 通过code获取refresh token
|
||||
err = d.getRefreshTokenByTV(ctx1, code, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// 通过refresh token获取access token
|
||||
if d.QuarkUCTVCommon.AccessToken == "" {
|
||||
err := d.getRefreshTokenByTV(ctx1, d.Addition.RefreshToken, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 access token 是否有效
|
||||
_, err := d.isLogin(ctx1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files := make([]model.Obj, 0)
|
||||
pageIndex := int64(0)
|
||||
pageSize := int64(100)
|
||||
for {
|
||||
var filesData FilesData
|
||||
_, err := d.request(ctx, "/file", "GET", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"method": "list",
|
||||
"parent_fid": dir.GetID(),
|
||||
"order_by": "3",
|
||||
"desc": "1",
|
||||
"category": "",
|
||||
"source": "",
|
||||
"ex_source": "",
|
||||
"list_all": "0",
|
||||
"page_size": strconv.FormatInt(pageSize, 10),
|
||||
"page_index": strconv.FormatInt(pageIndex, 10),
|
||||
})
|
||||
}, &filesData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range filesData.Data.Files {
|
||||
files = append(files, &filesData.Data.Files[i])
|
||||
}
|
||||
if pageIndex*pageSize >= filesData.Data.TotalCount {
|
||||
break
|
||||
} else {
|
||||
pageIndex++
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
files := &model.Link{}
|
||||
var fileLink FileLink
|
||||
_, err := d.request(ctx, "/file", "GET", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"method": "download",
|
||||
"group_by": "source",
|
||||
"fid": file.GetID(),
|
||||
"resolution": "low,normal,high,super,2k,4k",
|
||||
"support": "dolby_vision",
|
||||
})
|
||||
}, &fileLink)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files.URL = fileLink.Data.DownloadURL
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Remove(ctx context.Context, obj model.Obj) error {
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
type QuarkUCTVCommon struct {
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*QuarkUCTV)(nil)
|
67
drivers/quark_uc_tv/meta.go
Normal file
67
drivers/quark_uc_tv/meta.go
Normal file
@ -0,0 +1,67 @@
|
||||
package quark_uc_tv
|
||||
|
||||
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.RootID
|
||||
// define other
|
||||
RefreshToken string `json:"refresh_token" required:"false" default:""`
|
||||
// 必要且影响登录,由签名决定
|
||||
DeviceID string `json:"device_id" required:"false" default:""`
|
||||
// 登陆所用的数据 无需手动填写
|
||||
QueryToken string `json:"query_token" required:"false" default:"" help:"don't edit'"`
|
||||
}
|
||||
|
||||
type Conf struct {
|
||||
api string
|
||||
clientID string
|
||||
signKey string
|
||||
appVer string
|
||||
channel string
|
||||
codeApi string
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &QuarkUCTV{
|
||||
config: driver.Config{
|
||||
Name: "QuarkTV",
|
||||
OnlyLocal: false,
|
||||
DefaultRoot: "0",
|
||||
NoOverwriteUpload: true,
|
||||
NoUpload: true,
|
||||
},
|
||||
conf: Conf{
|
||||
api: "https://open-api-drive.quark.cn",
|
||||
clientID: "d3194e61504e493eb6222857bccfed94",
|
||||
signKey: "kw2dvtd7p4t3pjl2d9ed9yc8yej8kw2d",
|
||||
appVer: "1.5.6",
|
||||
channel: "CP",
|
||||
codeApi: "http://api.extscreen.com/quarkdrive",
|
||||
},
|
||||
}
|
||||
})
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &QuarkUCTV{
|
||||
config: driver.Config{
|
||||
Name: "UCTV",
|
||||
OnlyLocal: false,
|
||||
DefaultRoot: "0",
|
||||
NoOverwriteUpload: true,
|
||||
NoUpload: true,
|
||||
},
|
||||
conf: Conf{
|
||||
api: "https://open-api-drive.uc.cn",
|
||||
clientID: "5acf882d27b74502b7040b0c65519aa7",
|
||||
signKey: "l3srvtd7p42l0d0x1u8d7yc8ye9kki4d",
|
||||
appVer: "1.6.5",
|
||||
channel: "UCTVOFFICIALWEB",
|
||||
codeApi: "http://api.extscreen.com/ucdrive",
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
102
drivers/quark_uc_tv/types.go
Normal file
102
drivers/quark_uc_tv/types.go
Normal file
@ -0,0 +1,102 @@
|
||||
package quark_uc_tv
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Resp struct {
|
||||
CommonRsp
|
||||
Errno int `json:"errno"`
|
||||
ErrorInfo string `json:"error_info"`
|
||||
}
|
||||
|
||||
type CommonRsp struct {
|
||||
Status int `json:"status"`
|
||||
ReqID string `json:"req_id"`
|
||||
}
|
||||
|
||||
type RefreshTokenAuthResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Status int `json:"status"`
|
||||
Errno int `json:"errno"`
|
||||
ErrorInfo string `json:"error_info"`
|
||||
ReqID string `json:"req_id"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
} `json:"data"`
|
||||
}
|
||||
type Files struct {
|
||||
Fid string `json:"fid"`
|
||||
ParentFid string `json:"parent_fid"`
|
||||
Category int `json:"category"`
|
||||
Filename string `json:"filename"`
|
||||
Size int64 `json:"size"`
|
||||
FileType string `json:"file_type"`
|
||||
SubItems int `json:"sub_items,omitempty"`
|
||||
Isdir int `json:"isdir"`
|
||||
Duration int `json:"duration"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
IsBackup int `json:"is_backup"`
|
||||
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||
}
|
||||
|
||||
func (f *Files) GetSize() int64 {
|
||||
return f.Size
|
||||
}
|
||||
|
||||
func (f *Files) GetName() string {
|
||||
return f.Filename
|
||||
}
|
||||
|
||||
func (f *Files) ModTime() time.Time {
|
||||
//return time.Unix(f.UpdatedAt, 0)
|
||||
return time.Unix(0, f.UpdatedAt*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
func (f *Files) CreateTime() time.Time {
|
||||
//return time.Unix(f.CreatedAt, 0)
|
||||
return time.Unix(0, f.CreatedAt*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
func (f *Files) IsDir() bool {
|
||||
return f.Isdir == 1
|
||||
}
|
||||
|
||||
func (f *Files) GetHash() utils.HashInfo {
|
||||
return utils.HashInfo{}
|
||||
}
|
||||
|
||||
func (f *Files) GetID() string {
|
||||
return f.Fid
|
||||
}
|
||||
|
||||
func (f *Files) GetPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
var _ model.Obj = (*Files)(nil)
|
||||
|
||||
type FilesData struct {
|
||||
CommonRsp
|
||||
Data struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
Files []Files `json:"files"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type FileLink struct {
|
||||
CommonRsp
|
||||
Data struct {
|
||||
Fid string `json:"fid"`
|
||||
FileName string `json:"file_name"`
|
||||
Size int64 `json:"size"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
} `json:"data"`
|
||||
}
|
211
drivers/quark_uc_tv/util.go
Normal file
211
drivers/quark_uc_tv/util.go
Normal file
@ -0,0 +1,211 @@
|
||||
package quark_uc_tv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
UserAgent = "Mozilla/5.0 (Linux; U; Android 13; zh-cn; M2004J7AC Build/UKQ1.231108.001) AppleWebKit/533.1 (KHTML, like Gecko) Mobile Safari/533.1"
|
||||
DeviceBrand = "Xiaomi"
|
||||
Platform = "tv"
|
||||
DeviceName = "M2004J7AC"
|
||||
DeviceModel = "M2004J7AC"
|
||||
BuildDevice = "M2004J7AC"
|
||||
BuildProduct = "M2004J7AC"
|
||||
DeviceGpu = "Adreno (TM) 550"
|
||||
ActivityRect = "{}"
|
||||
)
|
||||
|
||||
func (d *QuarkUCTV) request(ctx context.Context, pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
u := d.conf.api + pathname
|
||||
tm, token, reqID := d.generateReqSign(method, pathname, d.conf.signKey)
|
||||
req := base.RestyClient.R()
|
||||
req.SetContext(ctx)
|
||||
req.SetHeaders(map[string]string{
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"User-Agent": UserAgent,
|
||||
"x-pan-tm": tm,
|
||||
"x-pan-token": token,
|
||||
"x-pan-client-id": d.conf.clientID,
|
||||
})
|
||||
req.SetQueryParams(map[string]string{
|
||||
"req_id": reqID,
|
||||
"access_token": d.QuarkUCTVCommon.AccessToken,
|
||||
"app_ver": d.conf.appVer,
|
||||
"device_id": d.Addition.DeviceID,
|
||||
"device_brand": DeviceBrand,
|
||||
"platform": Platform,
|
||||
"device_name": DeviceName,
|
||||
"device_model": DeviceModel,
|
||||
"build_device": BuildDevice,
|
||||
"build_product": BuildProduct,
|
||||
"device_gpu": DeviceGpu,
|
||||
"activity_rect": ActivityRect,
|
||||
"channel": d.conf.channel,
|
||||
})
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var e Resp
|
||||
req.SetError(&e)
|
||||
res, err := req.Execute(method, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 判断 是否需要 刷新 access_token
|
||||
if e.Status == -1 && e.Errno == 10001 {
|
||||
// token 过期
|
||||
err = d.getRefreshTokenByTV(ctx, d.Addition.RefreshToken, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx1, cancelFunc := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancelFunc()
|
||||
return d.request(ctx1, pathname, method, callback, resp)
|
||||
}
|
||||
|
||||
if e.Status >= 400 || e.Errno != 0 {
|
||||
return nil, errors.New(e.ErrorInfo)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) getLoginCode(ctx context.Context) (string, error) {
|
||||
// 获取登录二维码
|
||||
pathname := "/oauth/authorize"
|
||||
var resp struct {
|
||||
CommonRsp
|
||||
QrData string `json:"qr_data"`
|
||||
QueryToken string `json:"query_token"`
|
||||
}
|
||||
_, err := d.request(ctx, pathname, "GET", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"auth_type": "code",
|
||||
"client_id": d.conf.clientID,
|
||||
"scope": "netdisk",
|
||||
"qrcode": "1",
|
||||
"qr_width": "460",
|
||||
"qr_height": "460",
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 保存query_token 用于后续登录
|
||||
if resp.QueryToken != "" {
|
||||
d.Addition.QueryToken = resp.QueryToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
}
|
||||
return resp.QrData, nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) getCode(ctx context.Context) (string, error) {
|
||||
// 通过query token获取code
|
||||
pathname := "/oauth/code"
|
||||
var resp struct {
|
||||
CommonRsp
|
||||
Code string `json:"code"`
|
||||
}
|
||||
_, err := d.request(ctx, pathname, "GET", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"client_id": d.conf.clientID,
|
||||
"scope": "netdisk",
|
||||
"query_token": d.Addition.QueryToken,
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Code, nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) getRefreshTokenByTV(ctx context.Context, code string, isRefresh bool) error {
|
||||
pathname := "/token"
|
||||
_, _, reqID := d.generateReqSign("POST", pathname, d.conf.signKey)
|
||||
u := d.conf.codeApi + pathname
|
||||
var resp RefreshTokenAuthResp
|
||||
body := map[string]string{
|
||||
"req_id": reqID,
|
||||
"app_ver": d.conf.appVer,
|
||||
"device_id": d.Addition.DeviceID,
|
||||
"device_brand": DeviceBrand,
|
||||
"platform": Platform,
|
||||
"device_name": DeviceName,
|
||||
"device_model": DeviceModel,
|
||||
"build_device": BuildDevice,
|
||||
"build_product": BuildProduct,
|
||||
"device_gpu": DeviceGpu,
|
||||
"activity_rect": ActivityRect,
|
||||
"channel": d.conf.channel,
|
||||
}
|
||||
if isRefresh {
|
||||
body["refresh_token"] = code
|
||||
} else {
|
||||
body["code"] = code
|
||||
}
|
||||
|
||||
_, err := base.RestyClient.R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(body).
|
||||
SetResult(&resp).
|
||||
SetContext(ctx).
|
||||
Post(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
return errors.New(resp.Message)
|
||||
}
|
||||
if resp.Data.RefreshToken != "" {
|
||||
d.Addition.RefreshToken = resp.Data.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
d.QuarkUCTVCommon.AccessToken = resp.Data.AccessToken
|
||||
} else {
|
||||
return errors.New("refresh token is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) isLogin(ctx context.Context) (bool, error) {
|
||||
_, err := d.request(ctx, "/user", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"method": "user_info",
|
||||
})
|
||||
}, nil)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (d *QuarkUCTV) generateReqSign(method string, pathname string, key string) (string, string, string) {
|
||||
//timestamp 13位时间戳
|
||||
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
|
||||
deviceID := d.Addition.DeviceID
|
||||
if deviceID == "" {
|
||||
deviceID = utils.GetMD5EncodeStr(timestamp)
|
||||
d.Addition.DeviceID = deviceID
|
||||
op.MustSaveDriverStorage(d)
|
||||
}
|
||||
// 生成req_id
|
||||
reqID := md5.Sum([]byte(deviceID + timestamp))
|
||||
reqIDHex := hex.EncodeToString(reqID[:])
|
||||
|
||||
// 生成x_pan_token
|
||||
tokenData := method + "&" + pathname + "&" + timestamp + "&" + key
|
||||
xPanToken := sha256.Sum256([]byte(tokenData))
|
||||
xPanTokenHex := hex.EncodeToString(xPanToken[:])
|
||||
|
||||
return timestamp, xPanTokenHex, reqIDHex
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"io"
|
||||
"net/url"
|
||||
stdpath "path"
|
||||
@ -95,23 +96,27 @@ func (d *S3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*mo
|
||||
input.ResponseContentDisposition = &disposition
|
||||
}
|
||||
req, _ := d.linkClient.GetObjectRequest(input)
|
||||
var link string
|
||||
var link model.Link
|
||||
var err error
|
||||
if d.CustomHost != "" {
|
||||
err = req.Build()
|
||||
link = req.HTTPRequest.URL.String()
|
||||
link.URL = req.HTTPRequest.URL.String()
|
||||
if d.RemoveBucket {
|
||||
link = strings.Replace(link, "/"+d.Bucket, "", 1)
|
||||
link.URL = strings.Replace(link.URL, "/"+d.Bucket, "", 1)
|
||||
}
|
||||
} else {
|
||||
link, err = req.Presign(time.Hour * time.Duration(d.SignURLExpire))
|
||||
if common.ShouldProxy(d, filename) {
|
||||
err = req.Sign()
|
||||
link.URL = req.HTTPRequest.URL.String()
|
||||
link.Header = req.HTTPRequest.Header
|
||||
} else {
|
||||
link.URL, err = req.Presign(time.Hour * time.Duration(d.SignURLExpire))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.Link{
|
||||
URL: link,
|
||||
}, nil
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (d *S3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"math"
|
||||
stdpath "path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
@ -23,7 +22,9 @@ import (
|
||||
type Terabox struct {
|
||||
model.Storage
|
||||
Addition
|
||||
JsToken string
|
||||
JsToken string
|
||||
url_domain_prefix string
|
||||
base_url string
|
||||
}
|
||||
|
||||
func (d *Terabox) Config() driver.Config {
|
||||
@ -36,6 +37,8 @@ func (d *Terabox) GetAddition() driver.Additional {
|
||||
|
||||
func (d *Terabox) Init(ctx context.Context) error {
|
||||
var resp CheckLoginResp
|
||||
d.base_url = "https://www.terabox.com"
|
||||
d.url_domain_prefix = "jp"
|
||||
_, err := d.get("/api/check/login", nil, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -71,7 +74,16 @@ func (d *Terabox) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
}
|
||||
|
||||
func (d *Terabox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
_, err := d.create(stdpath.Join(parentDir.GetPath(), dirName), 0, 1, "", "")
|
||||
params := map[string]string{
|
||||
"a": "commit",
|
||||
}
|
||||
data := map[string]string{
|
||||
"path": stdpath.Join(parentDir.GetPath(), dirName),
|
||||
"isdir": "1",
|
||||
"block_list": "[]",
|
||||
}
|
||||
res, err := d.post_form("/api/create", params, data, nil)
|
||||
log.Debugln(string(res))
|
||||
return err
|
||||
}
|
||||
|
||||
@ -117,63 +129,61 @@ func (d *Terabox) Remove(ctx context.Context, obj model.Obj) error {
|
||||
}
|
||||
|
||||
func (d *Terabox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
tempFile, err := stream.CacheFullInTempFile()
|
||||
resp, err := base.RestyClient.R().
|
||||
SetContext(ctx).
|
||||
Get("https://" + d.url_domain_prefix + "-data.terabox.com/rest/2.0/pcs/file?method=locateupload")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var Default int64 = 4 * 1024 * 1024
|
||||
defaultByteData := make([]byte, Default)
|
||||
count := int(math.Ceil(float64(stream.GetSize()) / float64(Default)))
|
||||
// cal md5
|
||||
h1 := md5.New()
|
||||
h2 := md5.New()
|
||||
block_list := make([]string, 0)
|
||||
left := stream.GetSize()
|
||||
for i := 0; i < count; i++ {
|
||||
byteSize := Default
|
||||
var byteData []byte
|
||||
if left < Default {
|
||||
byteSize = left
|
||||
byteData = make([]byte, byteSize)
|
||||
} else {
|
||||
byteData = defaultByteData
|
||||
}
|
||||
left -= byteSize
|
||||
_, err = io.ReadFull(tempFile, byteData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h1.Write(byteData)
|
||||
h2.Write(byteData)
|
||||
block_list = append(block_list, fmt.Sprintf("\"%s\"", hex.EncodeToString(h2.Sum(nil))))
|
||||
h2.Reset()
|
||||
}
|
||||
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
var locateupload_resp LocateUploadResp
|
||||
err = utils.Json.Unmarshal(resp.Body(), &locateupload_resp)
|
||||
if err != nil {
|
||||
log.Debugln(resp)
|
||||
return err
|
||||
}
|
||||
log.Debugln(locateupload_resp)
|
||||
|
||||
// precreate file
|
||||
rawPath := stdpath.Join(dstDir.GetPath(), stream.GetName())
|
||||
path := encodeURIComponent(rawPath)
|
||||
block_list_str := fmt.Sprintf("[%s]", strings.Join(block_list, ","))
|
||||
data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&block_list=%s",
|
||||
path, stream.GetSize(),
|
||||
block_list_str)
|
||||
params := map[string]string{}
|
||||
|
||||
var precreateBlockListStr string
|
||||
if stream.GetSize() > initialChunkSize {
|
||||
precreateBlockListStr = `["5910a591dd8fc18c32a8f3df4fdc1761","a5fc157d78e6ad1c7e114b056c92821e"]`
|
||||
} else {
|
||||
precreateBlockListStr = `["5910a591dd8fc18c32a8f3df4fdc1761"]`
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"path": rawPath,
|
||||
"autoinit": "1",
|
||||
"target_path": dstDir.GetPath(),
|
||||
"block_list": precreateBlockListStr,
|
||||
"local_mtime": strconv.FormatInt(stream.ModTime().Unix(), 10),
|
||||
"file_limit_switch_v34": "true",
|
||||
}
|
||||
var precreateResp PrecreateResp
|
||||
_, err = d.post("/api/precreate", params, data, &precreateResp)
|
||||
log.Debugln(data)
|
||||
res, err := d.post_form("/api/precreate", nil, data, &precreateResp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("%+v", precreateResp)
|
||||
if precreateResp.Errno != 0 {
|
||||
log.Debugln(string(res))
|
||||
return fmt.Errorf("[terabox] failed to precreate file, errno: %d", precreateResp.Errno)
|
||||
}
|
||||
if precreateResp.ReturnType == 2 {
|
||||
return nil
|
||||
}
|
||||
params = map[string]string{
|
||||
|
||||
// upload chunks
|
||||
tempFile, err := stream.CacheFullInTempFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"method": "upload",
|
||||
"path": path,
|
||||
"uploadid": precreateResp.Uploadid,
|
||||
@ -182,25 +192,38 @@ func (d *Terabox) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
||||
"channel": "dubox",
|
||||
"clienttype": "0",
|
||||
}
|
||||
left = stream.GetSize()
|
||||
for i, partseq := range precreateResp.BlockList {
|
||||
|
||||
streamSize := stream.GetSize()
|
||||
chunkSize := calculateChunkSize(streamSize)
|
||||
chunkByteData := make([]byte, chunkSize)
|
||||
count := int(math.Ceil(float64(streamSize) / float64(chunkSize)))
|
||||
left := streamSize
|
||||
uploadBlockList := make([]string, 0, count)
|
||||
h := md5.New()
|
||||
for partseq := 0; partseq < count; partseq++ {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
byteSize := Default
|
||||
byteSize := chunkSize
|
||||
var byteData []byte
|
||||
if left < Default {
|
||||
if left >= chunkSize {
|
||||
byteData = chunkByteData
|
||||
} else {
|
||||
byteSize = left
|
||||
byteData = make([]byte, byteSize)
|
||||
} else {
|
||||
byteData = defaultByteData
|
||||
}
|
||||
left -= byteSize
|
||||
_, err = io.ReadFull(tempFile, byteData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := "https://c-jp.terabox.com/rest/2.0/pcs/superfile2"
|
||||
|
||||
// calculate md5
|
||||
h.Write(byteData)
|
||||
uploadBlockList = append(uploadBlockList, hex.EncodeToString(h.Sum(nil)))
|
||||
h.Reset()
|
||||
|
||||
u := "https://" + locateupload_resp.Host + "/rest/2.0/pcs/superfile2"
|
||||
params["partseq"] = strconv.Itoa(partseq)
|
||||
res, err := base.RestyClient.R().
|
||||
SetContext(ctx).
|
||||
@ -212,12 +235,39 @@ func (d *Terabox) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
||||
return err
|
||||
}
|
||||
log.Debugln(res.String())
|
||||
if len(precreateResp.BlockList) > 0 {
|
||||
up(float64(i) * 100 / float64(len(precreateResp.BlockList)))
|
||||
if count > 0 {
|
||||
up(float64(partseq) * 100 / float64(count))
|
||||
}
|
||||
}
|
||||
_, err = d.create(rawPath, stream.GetSize(), 0, precreateResp.Uploadid, block_list_str)
|
||||
return err
|
||||
|
||||
// create file
|
||||
params = map[string]string{
|
||||
"isdir": "0",
|
||||
"rtype": "1",
|
||||
}
|
||||
|
||||
uploadBlockListStr, err := utils.Json.MarshalToString(uploadBlockList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = map[string]string{
|
||||
"path": rawPath,
|
||||
"size": strconv.FormatInt(stream.GetSize(), 10),
|
||||
"uploadid": precreateResp.Uploadid,
|
||||
"target_path": dstDir.GetPath(),
|
||||
"block_list": uploadBlockListStr,
|
||||
"local_mtime": strconv.FormatInt(stream.ModTime().Unix(), 10),
|
||||
}
|
||||
var createResp CreateResp
|
||||
res, err = d.post_form("/api/create", params, data, &createResp)
|
||||
log.Debugln(string(res))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if createResp.Errno != 0 {
|
||||
return fmt.Errorf("[terabox] failed to create file, errno: %d", createResp.Errno)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Terabox)(nil)
|
||||
|
@ -95,3 +95,11 @@ type PrecreateResp struct {
|
||||
type CheckLoginResp struct {
|
||||
Errno int `json:"errno"`
|
||||
}
|
||||
|
||||
type LocateUploadResp struct {
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
type CreateResp struct {
|
||||
Errno int `json:"errno"`
|
||||
}
|
||||
|
@ -14,6 +14,12 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
initialChunkSize int64 = 4 << 20 // 4MB
|
||||
initialSizeThreshold int64 = 4 << 30 // 4GB
|
||||
)
|
||||
|
||||
func getStrBetween(raw, start, end string) string {
|
||||
@ -28,11 +34,11 @@ func getStrBetween(raw, start, end string) string {
|
||||
}
|
||||
|
||||
func (d *Terabox) resetJsToken() error {
|
||||
u := "https://www.terabox.com/main"
|
||||
u := d.base_url
|
||||
res, err := base.RestyClient.R().SetHeaders(map[string]string{
|
||||
"Cookie": d.Cookie,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": "https://www.terabox.com/",
|
||||
"Referer": d.base_url,
|
||||
"User-Agent": base.UserAgent,
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
}).Get(u)
|
||||
@ -48,12 +54,12 @@ func (d *Terabox) resetJsToken() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Terabox) request(furl string, method string, callback base.ReqCallback, resp interface{}, noRetry ...bool) ([]byte, error) {
|
||||
func (d *Terabox) request(rurl string, method string, callback base.ReqCallback, resp interface{}, noRetry ...bool) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
"Cookie": d.Cookie,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": "https://www.terabox.com/",
|
||||
"Referer": d.base_url,
|
||||
"User-Agent": base.UserAgent,
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
})
|
||||
@ -70,7 +76,7 @@ func (d *Terabox) request(furl string, method string, callback base.ReqCallback,
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
res, err := req.Execute(method, furl)
|
||||
res, err := req.Execute(method, d.base_url+rurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -82,14 +88,24 @@ func (d *Terabox) request(furl string, method string, callback base.ReqCallback,
|
||||
return nil, err
|
||||
}
|
||||
if !utils.IsBool(noRetry...) {
|
||||
return d.request(furl, method, callback, resp, true)
|
||||
return d.request(rurl, method, callback, resp, true)
|
||||
}
|
||||
} else if errno == -6 {
|
||||
header := res.Header()
|
||||
log.Debugln(header)
|
||||
urlDomainPrefix := header.Get("Url-Domain-Prefix")
|
||||
if len(urlDomainPrefix) > 0 {
|
||||
d.url_domain_prefix = urlDomainPrefix
|
||||
d.base_url = "https://" + d.url_domain_prefix + ".terabox.com"
|
||||
log.Debugln("Redirect base_url to", d.base_url)
|
||||
return d.request(rurl, method, callback, resp, noRetry...)
|
||||
}
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *Terabox) get(pathname string, params map[string]string, resp interface{}) ([]byte, error) {
|
||||
return d.request("https://www.terabox.com"+pathname, http.MethodGet, func(req *resty.Request) {
|
||||
return d.request(pathname, http.MethodGet, func(req *resty.Request) {
|
||||
if params != nil {
|
||||
req.SetQueryParams(params)
|
||||
}
|
||||
@ -97,7 +113,7 @@ func (d *Terabox) get(pathname string, params map[string]string, resp interface{
|
||||
}
|
||||
|
||||
func (d *Terabox) post(pathname string, params map[string]string, data interface{}, resp interface{}) ([]byte, error) {
|
||||
return d.request("https://www.terabox.com"+pathname, http.MethodPost, func(req *resty.Request) {
|
||||
return d.request(pathname, http.MethodPost, func(req *resty.Request) {
|
||||
if params != nil {
|
||||
req.SetQueryParams(params)
|
||||
}
|
||||
@ -105,6 +121,15 @@ func (d *Terabox) post(pathname string, params map[string]string, data interface
|
||||
}, resp)
|
||||
}
|
||||
|
||||
func (d *Terabox) post_form(pathname string, params map[string]string, data map[string]string, resp interface{}) ([]byte, error) {
|
||||
return d.request(pathname, http.MethodPost, func(req *resty.Request) {
|
||||
if params != nil {
|
||||
req.SetQueryParams(params)
|
||||
}
|
||||
req.SetFormData(data)
|
||||
}, resp)
|
||||
}
|
||||
|
||||
func (d *Terabox) getFiles(dir string) ([]File, error) {
|
||||
page := 1
|
||||
num := 100
|
||||
@ -237,17 +262,24 @@ func (d *Terabox) manage(opera string, filelist interface{}) ([]byte, error) {
|
||||
return d.post("/api/filemanager", params, data, nil)
|
||||
}
|
||||
|
||||
func (d *Terabox) create(path string, size int64, isdir int, uploadid, block_list string) ([]byte, error) {
|
||||
params := map[string]string{}
|
||||
data := fmt.Sprintf("path=%s&size=%d&isdir=%d", encodeURIComponent(path), size, isdir)
|
||||
if uploadid != "" {
|
||||
data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list)
|
||||
}
|
||||
return d.post("/api/create", params, data, nil)
|
||||
}
|
||||
|
||||
func encodeURIComponent(str string) string {
|
||||
r := url.QueryEscape(str)
|
||||
r = strings.ReplaceAll(r, "+", "%20")
|
||||
return r
|
||||
}
|
||||
|
||||
func calculateChunkSize(streamSize int64) int64 {
|
||||
chunkSize := initialChunkSize
|
||||
sizeThreshold := initialSizeThreshold
|
||||
|
||||
if streamSize < chunkSize {
|
||||
return streamSize
|
||||
}
|
||||
|
||||
for streamSize > sizeThreshold {
|
||||
chunkSize <<= 1
|
||||
sizeThreshold <<= 1
|
||||
}
|
||||
|
||||
return chunkSize
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -309,7 +309,7 @@ type XunLeiBrowserCommon struct {
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
return xc.getFiles(ctx, dir.GetID(), args.ReqPath)
|
||||
return xc.getFiles(ctx, dir, args.ReqPath)
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
@ -317,17 +317,10 @@ func (xc *XunLeiBrowserCommon) Link(ctx context.Context, file model.Obj, args mo
|
||||
|
||||
params := map[string]string{
|
||||
"_magic": "2021",
|
||||
"space": "SPACE_BROWSER",
|
||||
"space": file.(*Files).GetSpace(),
|
||||
"thumbnail_size": "SIZE_LARGE",
|
||||
"with": "url",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if file.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
} else if file.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
params["space"] = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
@ -361,22 +354,9 @@ func (xc *XunLeiBrowserCommon) MakeDir(ctx context.Context, parentDir model.Obj,
|
||||
"kind": FOLDER,
|
||||
"name": dirName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
if parentDir.GetPath() == ThunderDriveFileID {
|
||||
js = base.Json{
|
||||
"kind": FOLDER,
|
||||
"name": dirName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
}
|
||||
} else if parentDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
js = base.Json{
|
||||
"kind": FOLDER,
|
||||
"name": dirName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
"space": parentDir.(*Files).GetSpace(),
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
@ -386,33 +366,14 @@ func (xc *XunLeiBrowserCommon) MakeDir(ctx context.Context, parentDir model.Obj,
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
|
||||
srcSpace := "SPACE_BROWSER"
|
||||
dstSpace := "SPACE_BROWSER"
|
||||
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
srcSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
dstSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"_from": dstSpace,
|
||||
"_from": srcObj.(*Files).GetSpace(),
|
||||
}
|
||||
js := base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID(), "space": dstSpace},
|
||||
"space": srcSpace,
|
||||
"to": base.Json{"parent_id": dstDir.GetID(), "space": dstDir.(*Files).GetSpace()},
|
||||
"space": srcObj.(*Files).GetSpace(),
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
js = base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID()},
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
@ -425,16 +386,7 @@ func (xc *XunLeiBrowserCommon) Move(ctx context.Context, srcObj, dstDir model.Ob
|
||||
func (xc *XunLeiBrowserCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
|
||||
params := map[string]string{
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
} else if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
params = map[string]string{
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
"space": srcObj.(*Files).GetSpace(),
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodPatch, func(r *resty.Request) {
|
||||
@ -448,33 +400,14 @@ func (xc *XunLeiBrowserCommon) Rename(ctx context.Context, srcObj model.Obj, new
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
|
||||
srcSpace := "SPACE_BROWSER"
|
||||
dstSpace := "SPACE_BROWSER"
|
||||
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
srcSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
dstSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"_from": dstSpace,
|
||||
"_from": srcObj.(*Files).GetSpace(),
|
||||
}
|
||||
js := base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID(), "space": dstSpace},
|
||||
"space": srcSpace,
|
||||
"to": base.Json{"parent_id": dstDir.GetID(), "space": dstDir.(*Files).GetSpace()},
|
||||
"space": srcObj.(*Files).GetSpace(),
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
js = base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID()},
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
@ -488,30 +421,17 @@ func (xc *XunLeiBrowserCommon) Remove(ctx context.Context, obj model.Obj) error
|
||||
|
||||
js := base.Json{
|
||||
"ids": []string{obj.GetID()},
|
||||
"space": "SPACE_BROWSER",
|
||||
"space": obj.(*Files).GetSpace(),
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if obj.GetPath() == ThunderDriveFileID {
|
||||
js = base.Json{
|
||||
"ids": []string{obj.GetID()},
|
||||
}
|
||||
} else if obj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
js = base.Json{
|
||||
"ids": []string{obj.GetID()},
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
}
|
||||
|
||||
// 先判断是否是特殊情况
|
||||
if obj.GetPath() == ThunderDriveFileID {
|
||||
if obj.(*Files).GetSpace() == ThunderDriveSpace {
|
||||
_, err := xc.Request(FILE_API_URL+"/{fileID}/trash", http.MethodPatch, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetPathParam("fileID", obj.GetID())
|
||||
r.SetBody("{}")
|
||||
}, nil)
|
||||
return err
|
||||
} else if obj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
} else if obj.(*Files).GetSpace() == ThunderBrowserDriveSafeSpace || obj.(*Files).GetSpace() == ThunderDriveSafeSpace {
|
||||
_, err := xc.Request(FILE_API_URL+":batchDelete", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
@ -557,29 +477,7 @@ func (xc *XunLeiBrowserCommon) Put(ctx context.Context, dstDir model.Obj, stream
|
||||
"size": stream.GetSize(),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if dstDir.GetPath() == ThunderDriveFileID {
|
||||
js = base.Json{
|
||||
"kind": FILE,
|
||||
"parent_id": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"size": stream.GetSize(),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
}
|
||||
} else if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
js = base.Json{
|
||||
"kind": FILE,
|
||||
"parent_id": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"size": stream.GetSize(),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
"space": dstDir.(*Files).GetSpace(),
|
||||
}
|
||||
|
||||
var resp UploadTaskResponse
|
||||
@ -610,58 +508,35 @@ func (xc *XunLeiBrowserCommon) Put(ctx context.Context, dstDir model.Obj, stream
|
||||
Bucket: aws.String(param.Bucket),
|
||||
Key: aws.String(param.Key),
|
||||
Expires: aws.Time(param.Expiration),
|
||||
Body: stream,
|
||||
Body: io.TeeReader(stream, driver.NewProgress(stream.GetSize(), up)),
|
||||
})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) getFiles(ctx context.Context, folderId string, path string) ([]model.Obj, error) {
|
||||
func (xc *XunLeiBrowserCommon) getFiles(ctx context.Context, dir model.Obj, path string) ([]model.Obj, error) {
|
||||
files := make([]model.Obj, 0)
|
||||
var pageToken string
|
||||
for {
|
||||
var fileList FileList
|
||||
folderSpace := "SPACE_BROWSER"
|
||||
folderSpace := ""
|
||||
switch dirF := dir.(type) {
|
||||
case *Files:
|
||||
folderSpace = dirF.GetSpace()
|
||||
default:
|
||||
// 处理 根目录的情况
|
||||
folderSpace = ThunderBrowserDriveSpace
|
||||
}
|
||||
params := map[string]string{
|
||||
"parent_id": folderId,
|
||||
"parent_id": dir.GetID(),
|
||||
"page_token": pageToken,
|
||||
"space": folderSpace,
|
||||
"filters": `{"trashed":{"eq":false}}`,
|
||||
"with": "url",
|
||||
"with_audit": "true",
|
||||
"thumbnail_size": "SIZE_LARGE",
|
||||
}
|
||||
var fileType int8
|
||||
// 处理特殊目录 “迅雷云盘” 设置特殊的 params 以便正常访问
|
||||
pattern1 := fmt.Sprintf(`^/.*/%s(/.*)?$`, ThunderDriveFolderName)
|
||||
thunderDriveMatch, _ := regexp.MatchString(pattern1, path)
|
||||
// 处理特殊目录 “超级保险箱” 设置特殊的 params 以便正常访问
|
||||
pattern2 := fmt.Sprintf(`^/.*/%s(/.*)?$`, ThunderBrowserDriveSafeFolderName)
|
||||
thunderBrowserDriveSafeMatch, _ := regexp.MatchString(pattern2, path)
|
||||
|
||||
// 如果是 "迅雷云盘" 内的
|
||||
if folderId == ThunderDriveFileID || thunderDriveMatch {
|
||||
params = map[string]string{
|
||||
"space": "",
|
||||
"__type": "drive",
|
||||
"refresh": "true",
|
||||
"__sync": "true",
|
||||
"parent_id": folderId,
|
||||
"page_token": pageToken,
|
||||
"with_audit": "true",
|
||||
"limit": "100",
|
||||
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
|
||||
}
|
||||
// 如果不是 "迅雷云盘"的"根目录"
|
||||
if folderId == ThunderDriveFileID {
|
||||
params["parent_id"] = ""
|
||||
}
|
||||
fileType = ThunderDriveType
|
||||
} else if thunderBrowserDriveSafeMatch {
|
||||
// 如果是 "超级保险箱" 内的
|
||||
fileType = ThunderBrowserDriveSafeType
|
||||
params["space"] = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
@ -670,24 +545,13 @@ func (xc *XunLeiBrowserCommon) getFiles(ctx context.Context, folderId string, pa
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 对文件夹也进行处理
|
||||
fileList.FolderType = fileType
|
||||
|
||||
for i := 0; i < len(fileList.Files); i++ {
|
||||
file := &fileList.Files[i]
|
||||
// 标记 文件夹内的文件
|
||||
file.FileType = fileList.FolderType
|
||||
for i := range fileList.Files {
|
||||
// 解决 "迅雷云盘" 重复出现问题————迅雷后端发送错误
|
||||
if file.Name == ThunderDriveFolderName && file.ID == "" && file.FolderType == ThunderDriveFolderType && folderId != "" {
|
||||
if fileList.Files[i].FolderType == ThunderDriveFolderType && fileList.Files[i].ID == "" && fileList.Files[i].Space == "" && dir.GetID() != "" {
|
||||
continue
|
||||
}
|
||||
// 处理特殊目录 “迅雷云盘” 设置特殊的文件夹ID
|
||||
if file.Name == ThunderDriveFolderName && file.ID == "" && file.FolderType == ThunderDriveFolderType {
|
||||
file.ID = ThunderDriveFileID
|
||||
} else if file.Name == ThunderBrowserDriveSafeFolderName && file.FolderType == ThunderBrowserDriveSafeFolderType {
|
||||
file.FileType = ThunderBrowserDriveSafeType
|
||||
}
|
||||
files = append(files, file)
|
||||
files = append(files, &fileList.Files[i])
|
||||
}
|
||||
|
||||
if fileList.NextPageToken == "" {
|
||||
|
@ -25,7 +25,7 @@ type ExpertAddition struct {
|
||||
SafePassword string `json:"safe_password" required:"true" help:"super safe password"` // 超级保险箱密码
|
||||
|
||||
// 签名方法1
|
||||
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"p+ExqPV,LwdwKlprzv7cQBQmxN5,vc08P1NwUBnbGsl58LzTW,VVNeXaXmZ8HH1SJEnp6YpVFSFU,pNAOJ,CNChvyDehAmUR1TDodfOusBAx,MS98NnX4Np8nxvEh6Ulv+SMMKMzKvD34C7lGWbb,9MpFF21GnVOYku0NM9Y/hzsK471UCUZ2o+,EY1QfeA06fXlw9wZNoZaXEED5zZPvNWI,,sciE,FIPqgQDUUW1e0GkiBFd5w7mCQ,zW,75XFdEO0Gi"`
|
||||
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"uWRwO7gPfdPB/0NfPtfQO+71,F93x+qPluYy6jdgNpq+lwdH1ap6WOM+nfz8/V,0HbpxvpXFsBK5CoTKam,dQhzbhzFRcawnsZqRETT9AuPAJ+wTQso82mRv,SAH98AmLZLRa6DB2u68sGhyiDh15guJpXhBzI,unqfo7Z64Rie9RNHMOB,7yxUdFADp3DOBvXdz0DPuKNVT35wqa5z0DEyEvf,RBG,ThTWPG5eC0UBqlbQ+04nZAptqGCdpv9o55A"`
|
||||
// 签名方法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"`
|
||||
@ -37,7 +37,7 @@ type ExpertAddition struct {
|
||||
DeviceID string `json:"device_id" required:"false" default:""`
|
||||
ClientID string `json:"client_id" required:"true" default:"ZUBzD9J_XPXfn7f7"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"yESVmHecEe6F0aou69vl-g"`
|
||||
ClientVersion string `json:"client_version" required:"true" default:"1.0.8.2215"`
|
||||
ClientVersion string `json:"client_version" required:"true" default:"1.10.0.2633"`
|
||||
PackageName string `json:"package_name" required:"true" default:"com.xunlei.browser"`
|
||||
|
||||
// 不影响登录,影响下载速度
|
||||
|
@ -114,8 +114,8 @@ type Files struct {
|
||||
ModifiedTime CustomTime `json:"modified_time"`
|
||||
IconLink string `json:"icon_link"`
|
||||
ThumbnailLink string `json:"thumbnail_link"`
|
||||
// Md5Checksum string `json:"md5_checksum"`
|
||||
Hash string `json:"hash"`
|
||||
Md5Checksum string `json:"md5_checksum"`
|
||||
Hash string `json:"hash"`
|
||||
// Links map[string]Link `json:"links"`
|
||||
// Phase string `json:"phase"`
|
||||
// Audit struct {
|
||||
@ -153,12 +153,22 @@ type Files struct {
|
||||
OriginalURL string `json:"original_url"`
|
||||
//Params struct{} `json:"params"`
|
||||
//OriginalFileIndex int `json:"original_file_index"`
|
||||
//Space string `json:"space"`
|
||||
Space string `json:"space"`
|
||||
//Apps []interface{} `json:"apps"`
|
||||
//Writable bool `json:"writable"`
|
||||
FolderType string `json:"folder_type"`
|
||||
//Collection interface{} `json:"collection"`
|
||||
FileType int8
|
||||
SortName string `json:"sort_name"`
|
||||
UserModifiedTime CustomTime `json:"user_modified_time"`
|
||||
//SpellName []interface{} `json:"spell_name"`
|
||||
//FileCategory string `json:"file_category"`
|
||||
//Tags []interface{} `json:"tags"`
|
||||
//ReferenceEvents []interface{} `json:"reference_events"`
|
||||
//ReferenceResource interface{} `json:"reference_resource"`
|
||||
//Params0 struct {
|
||||
// PlatformIcon string `json:"platform_icon"`
|
||||
// SmallThumbnail string `json:"small_thumbnail"`
|
||||
//} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Files) GetHash() utils.HashInfo {
|
||||
@ -172,16 +182,19 @@ func (c *Files) ModTime() time.Time { return c.ModifiedTime.Time }
|
||||
func (c *Files) IsDir() bool { return c.Kind == FOLDER }
|
||||
func (c *Files) GetID() string { return c.ID }
|
||||
func (c *Files) GetPath() string {
|
||||
// 对特殊文件进行特殊处理
|
||||
if c.FileType == ThunderDriveType {
|
||||
return ThunderDriveFileID
|
||||
} else if c.FileType == ThunderBrowserDriveSafeType {
|
||||
return ThunderBrowserDriveSafeFileID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (c *Files) Thumb() string { return c.ThumbnailLink }
|
||||
|
||||
func (c *Files) GetSpace() string {
|
||||
if c.Space != "" {
|
||||
return c.Space
|
||||
} else {
|
||||
// "迅雷云盘" 文件夹内 Space 为空
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 上传
|
||||
**/
|
||||
|
@ -23,29 +23,24 @@ const (
|
||||
)
|
||||
|
||||
var Algorithms = []string{
|
||||
"p+ExqPV",
|
||||
"LwdwKlprzv7cQBQmxN5",
|
||||
"vc08P1NwUBnbGsl58LzTW",
|
||||
"VVNeXaXmZ8HH1SJEnp6YpVFSFU",
|
||||
"pNAOJ",
|
||||
"CNChvyDehAmUR1TDodfOusBAx",
|
||||
"MS98NnX4Np8nxvEh6Ulv+SMMKMzKvD34C7lGWbb",
|
||||
"9MpFF21GnVOYku0NM9Y/hzsK471UCUZ2o+",
|
||||
"EY1QfeA06fXlw9wZNoZaXEED5zZPvNWI",
|
||||
"",
|
||||
"sciE",
|
||||
"FIPqgQDUUW1e0GkiBFd5w7mCQ",
|
||||
"zW",
|
||||
"75XFdEO0Gi",
|
||||
"uWRwO7gPfdPB/0NfPtfQO+71",
|
||||
"F93x+qPluYy6jdgNpq+lwdH1ap6WOM+nfz8/V",
|
||||
"0HbpxvpXFsBK5CoTKam",
|
||||
"dQhzbhzFRcawnsZqRETT9AuPAJ+wTQso82mRv",
|
||||
"SAH98AmLZLRa6DB2u68sGhyiDh15guJpXhBzI",
|
||||
"unqfo7Z64Rie9RNHMOB",
|
||||
"7yxUdFADp3DOBvXdz0DPuKNVT35wqa5z0DEyEvf",
|
||||
"RBG",
|
||||
"ThTWPG5eC0UBqlbQ+04nZAptqGCdpv9o55A",
|
||||
}
|
||||
|
||||
const (
|
||||
ClientID = "ZUBzD9J_XPXfn7f7"
|
||||
ClientSecret = "yESVmHecEe6F0aou69vl-g"
|
||||
ClientVersion = "1.0.8.2215"
|
||||
ClientVersion = "1.10.0.2633"
|
||||
PackageName = "com.xunlei.browser"
|
||||
DownloadUserAgent = "AndroidDownloadManager/13 (Linux; U; Android 13; M2004J7AC Build/SP1A.210812.016)"
|
||||
SdkVersion = "2.0.3.262"
|
||||
SdkVersion = "233100"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -62,12 +57,10 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
ThunderDriveFileID = "XXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
ThunderBrowserDriveSafeFileID = "YYYYYYYYYYYYYYYYYYYYYYYYYY"
|
||||
ThunderDriveFolderName = "迅雷云盘"
|
||||
ThunderBrowserDriveSafeFolderName = "超级保险箱"
|
||||
ThunderDriveType = 1
|
||||
ThunderBrowserDriveSafeType = 2
|
||||
ThunderDriveSpace = ""
|
||||
ThunderDriveSafeSpace = "SPACE_SAFE"
|
||||
ThunderBrowserDriveSpace = "SPACE_BROWSER"
|
||||
ThunderBrowserDriveSafeSpace = "SPACE_BROWSER_SAFE"
|
||||
ThunderDriveFolderType = "DEFAULT_ROOT"
|
||||
ThunderBrowserDriveSafeFolderType = "BROWSER_SAFE"
|
||||
)
|
||||
|
@ -55,7 +55,9 @@ func (d *Vtencent) Init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *Vtencent) Drop(ctx context.Context) error {
|
||||
d.cron.Stop()
|
||||
if d.cron != nil {
|
||||
d.cron.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
94
go.mod
94
go.mod
@ -3,67 +3,71 @@ module github.com/alist-org/alist/v3
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
github.com/SheltonZhu/115driver v1.0.25
|
||||
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0
|
||||
github.com/KirCute/sftpd-alist v0.0.11
|
||||
github.com/SheltonZhu/115driver v1.0.32
|
||||
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21
|
||||
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
|
||||
github.com/alist-org/gofakes3 v0.0.6
|
||||
github.com/alist-org/times v0.0.0-20240721124318-c2e3da27cc69
|
||||
github.com/alist-org/gofakes3 v0.0.7
|
||||
github.com/alist-org/times v0.0.0-20240721124654-efa0c7d3ad92
|
||||
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.54.19
|
||||
github.com/blevesearch/bleve/v2 v2.4.1
|
||||
github.com/aws/aws-sdk-go v1.55.5
|
||||
github.com/blevesearch/bleve/v2 v2.4.2
|
||||
github.com/caarlos0/env/v9 v9.0.0
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/charmbracelet/bubbletea v0.26.6
|
||||
github.com/charmbracelet/lipgloss v0.12.1
|
||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240708163039-9a9b82a0ce4d
|
||||
github.com/charmbracelet/bubbles v0.20.0
|
||||
github.com/charmbracelet/bubbletea v1.1.0
|
||||
github.com/charmbracelet/lipgloss v0.13.0
|
||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e
|
||||
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/dlclark/regexp2 v1.11.2
|
||||
github.com/dlclark/regexp2 v1.11.4
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564
|
||||
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.7.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-resty/resty/v2 v2.13.1
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/go-resty/resty/v2 v2.14.0
|
||||
github.com/go-webauthn/webauthn v0.11.1
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hekmon/transmissionrpc/v3 v3.0.0
|
||||
github.com/hirochachacha/go-smb2 v1.1.0
|
||||
github.com/ipfs/go-ipfs-api v0.7.0
|
||||
github.com/jlaffaye/ftp v0.2.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.2.8
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.3.1
|
||||
github.com/maruel/natural v1.1.1
|
||||
github.com/meilisearch/meilisearch-go v0.27.0
|
||||
github.com/meilisearch/meilisearch-go v0.27.2
|
||||
github.com/minio/sio v0.4.0
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
github.com/ncw/swift/v2 v2.0.2
|
||||
github.com/ncw/swift/v2 v2.0.3
|
||||
github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/rclone/rclone v1.67.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7
|
||||
github.com/u2takey/ffmpeg-go v0.5.0
|
||||
github.com/upyun/go-sdk/v3 v3.0.4
|
||||
github.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5
|
||||
github.com/xhofe/tache v0.1.1
|
||||
github.com/xhofe/tache v0.1.3
|
||||
github.com/xhofe/wopan-sdk-go v0.1.3
|
||||
github.com/zzzhr1990/go-common-entity v0.0.0-20221216044934-fd1c571e3a22
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/net v0.27.0
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/time v0.5.0
|
||||
golang.org/x/crypto v0.30.0
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e
|
||||
golang.org/x/image v0.19.0
|
||||
golang.org/x/net v0.28.0
|
||||
golang.org/x/oauth2 v0.22.0
|
||||
golang.org/x/time v0.8.0
|
||||
google.golang.org/appengine v1.6.8
|
||||
gopkg.in/ldap.v3 v3.1.0
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
@ -74,19 +78,20 @@ require (
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.19 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.1.4 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.20 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.1.5 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.1.4 // indirect
|
||||
github.com/charmbracelet/x/input v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.2.3 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fclairamb/go-log v0.5.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hekmon/cunits/v2 v2.1.0 // indirect
|
||||
github.com/ipfs/boxo v0.12.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@ -102,12 +107,12 @@ require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.12.0 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.1.9 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.1.10 // indirect
|
||||
github.com/blevesearch/geo v0.1.20 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.14 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.15 // indirect
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
||||
@ -125,7 +130,7 @@ require (
|
||||
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/geoffgarside/ber v1.1.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
@ -135,13 +140,13 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.9 // indirect
|
||||
github.com/go-webauthn/x v0.1.12 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/google/go-tpm v0.9.1 // 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
|
||||
@ -169,7 +174,7 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
@ -189,6 +194,7 @@ 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/otiai10/copy v1.14.0
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
@ -219,14 +225,14 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.8 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/term v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/api v0.169.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
|
||||
google.golang.org/grpc v1.66.0
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
|
208
go.sum
208
go.sum
@ -1,28 +1,33 @@
|
||||
cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
|
||||
cloud.google.com/go/compute v1.23.4 h1:EBT9Nw4q3zyE7G45Wvv3MzolIrCJEuHys5muLY0wvAw=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0 h1:ikwCzeqoqN6wvBHOB9OI6dde/jbV7EoTMpUcxtYl5Po=
|
||||
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0/go.mod h1:v0NgMtKDDi/6CM6r4P+daCljCW3eO9yS+Z+pZDTKo1E=
|
||||
github.com/KirCute/sftpd-alist v0.0.11 h1:BGInXmmLBI+v6S9WZCwvY0DRK1vDprGNcTv/57p2GSo=
|
||||
github.com/KirCute/sftpd-alist v0.0.11/go.mod h1:pPFzr6GrKqXvFXLr46ZpoqmtSpwH8DKTYloSp/ybzKQ=
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
|
||||
github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=
|
||||
github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/SheltonZhu/115driver v1.0.25 h1:i101yanLKUwV1Pi7x+vgNOwgz7Hp9JbNmo6BCZ9/4wo=
|
||||
github.com/SheltonZhu/115driver v1.0.25/go.mod h1:e3fPOBANbH/FsTya8FquJwOR3ErhCQgEab3q6CVY2k4=
|
||||
github.com/SheltonZhu/115driver v1.0.32 h1:Taw1bnfcPJZW0xTdhDvEbBS1tccif7J7DslRp2NkDyQ=
|
||||
github.com/SheltonZhu/115driver v1.0.32/go.mod h1:XXFi23pyhAgzUE8dUEKdGvIdUQKi3wv6zR7C1Do40D8=
|
||||
github.com/Unknwon/goconfig v1.0.0 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A=
|
||||
github.com/Unknwon/goconfig v1.0.0/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=
|
||||
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a h1:RenIAa2q4H8UcS/cqmwdT1WCWIAH5aumP8m8RpbqVsE=
|
||||
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a/go.mod h1:sSBbaOg90XwWKtpT56kVujF0bIeVITnPlssLclogS04=
|
||||
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21 h1:h6q5E9aMBhhdqouW81LozVPI1I+Pu6IxL2EKpfm5OjY=
|
||||
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21/go.mod h1:sSBbaOg90XwWKtpT56kVujF0bIeVITnPlssLclogS04=
|
||||
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4 h1:WnvifFgYyogPz2ZFvaVLk4gI/Co0paF92FmxSR6U1zY=
|
||||
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4/go.mod h1:8pWlL2rpusvx7Xa6yYaIWOJ8bR3gPdFBUT7OystyGOY=
|
||||
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
|
||||
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
|
||||
github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=
|
||||
github.com/aead/ecdh v0.2.0/go.mod h1:a9HHtXuSo8J1Js1MwLQx2mBhkXMT6YwUmVVEY4tTB8U=
|
||||
github.com/alist-org/gofakes3 v0.0.6 h1:kenkDSqOIJt5ZDJ9KW91YkwplFXpfToPDjP3Bd6GZRg=
|
||||
github.com/alist-org/gofakes3 v0.0.6/go.mod h1:6IyGtYGIX29fLvtXo+XZhtwX2P33KVYYj8uTgAHSu58=
|
||||
github.com/alist-org/times v0.0.0-20240721124318-c2e3da27cc69 h1:E9QJ4vVTu1KYRhelnCsQImCsbl7NlkH3Yxs3/L2ldDk=
|
||||
github.com/alist-org/times v0.0.0-20240721124318-c2e3da27cc69/go.mod h1:oPJwGY3sLmGgcJamGumz//0A35f4BwQRacyqLNcJTOU=
|
||||
github.com/alist-org/gofakes3 v0.0.7 h1:0cDGI7fLBrqumhCBto9T3ZYCL71AyGZ1l+xxJgjqe8s=
|
||||
github.com/alist-org/gofakes3 v0.0.7/go.mod h1:6IyGtYGIX29fLvtXo+XZhtwX2P33KVYYj8uTgAHSu58=
|
||||
github.com/alist-org/times v0.0.0-20240721124654-efa0c7d3ad92 h1:pIEI87zhv8ZzQcu65rTL7kqirrs8dR6HDiXrqWat2Fk=
|
||||
github.com/alist-org/times v0.0.0-20240721124654-efa0c7d3ad92/go.mod h1:oPJwGY3sLmGgcJamGumz//0A35f4BwQRacyqLNcJTOU=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andreburgaud/crypt2go v1.2.0 h1:oly/ENAodeqTYpUafgd4r3v+VKLQnmOKUyfpj+TxHbE=
|
||||
@ -32,12 +37,14 @@ 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.54.19 h1:tyWV+07jagrNiCcGRzRhdtVjQs7Vy41NwsuOcl0IbVI=
|
||||
github.com/aws/aws-sdk-go v1.54.19/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
|
||||
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@ -46,22 +53,22 @@ github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZ
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/blevesearch/bleve/v2 v2.4.1 h1:8QWqsifq693mN3h6cSigKqkKUsUfv5hu0FDgz/4bFuA=
|
||||
github.com/blevesearch/bleve/v2 v2.4.1/go.mod h1:Ezmvsouspi+uVwnDzjIsCeUIT0WuBKlicP5JZnExWzo=
|
||||
github.com/blevesearch/bleve_index_api v1.1.9 h1:Cpq0Lp3As0Gfk3+PmcoNDRKeI50C5yuFNpj0YlN/bOE=
|
||||
github.com/blevesearch/bleve_index_api v1.1.9/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
|
||||
github.com/blevesearch/bleve/v2 v2.4.2 h1:NooYP1mb3c0StkiY9/xviiq2LGSaE8BQBCc/pirMx0U=
|
||||
github.com/blevesearch/bleve/v2 v2.4.2/go.mod h1:ATNKj7Yl2oJv/lGuF4kx39bST2dveX6w0th2FFYLkc8=
|
||||
github.com/blevesearch/bleve_index_api v1.1.10 h1:PDLFhVjrjQWr6jCuU7TwlmByQVCSEURADHdCqVS9+g0=
|
||||
github.com/blevesearch/bleve_index_api v1.1.10/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
|
||||
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
|
||||
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
|
||||
github.com/blevesearch/go-faiss v1.0.19 h1:UKoP8hS7DVsVSRRloNJb4qPfe2UQ99pP4D3oXd23g2A=
|
||||
github.com/blevesearch/go-faiss v1.0.19/go.mod h1:jrxHrbl42X/RnDPI+wBoZU8joxxuRwedrxqswQ3xfU8=
|
||||
github.com/blevesearch/go-faiss v1.0.20 h1:AIkdTQFWuZ5LQmKQSebgMR4RynGNw8ZseJXaan5kvtI=
|
||||
github.com/blevesearch/go-faiss v1.0.20/go.mod h1:jrxHrbl42X/RnDPI+wBoZU8joxxuRwedrxqswQ3xfU8=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.14 h1:fgMLMpGWR7u2TdRm7XSZVWhPvMAcdYHh25Lq1fQ6Fjo=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.14/go.mod h1:B7+a7vfpY4NsjuTkpv/eY7RZ91Xr90VaJzT2t7upZN8=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.15 h1:prV17iU/o+A8FiZi9MXmqbagd8I0bCqM7OKUYPbnb5Y=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.15/go.mod h1:db0cmP03bPNadXrCDuVkKLV6ywFSiRgPFT1YVrestBc=
|
||||
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||
@ -80,8 +87,8 @@ github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz7
|
||||
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
|
||||
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
|
||||
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
|
||||
github.com/blevesearch/zapx/v16 v16.1.4 h1:TBQfG77g2UUXwfjOVcEtB9pXkg6JBmGXkeZKI67+TiA=
|
||||
github.com/blevesearch/zapx/v16 v16.1.4/go.mod h1:+Q+Z89Iv7ewhdX2jyE6Qs/RUnN4tZuokaQ0xvTaFmx8=
|
||||
github.com/blevesearch/zapx/v16 v16.1.5 h1:b0sMcarqNFxuXvjoXsF8WtwVahnxyhEvBSRJi/AUHjU=
|
||||
github.com/blevesearch/zapx/v16 v16.1.5/go.mod h1:J4mSF39w1QELc11EWRSBFkPeZuO7r/NPKkHzDCoiaI8=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
@ -94,24 +101,22 @@ github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc
|
||||
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s=
|
||||
github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk=
|
||||
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
|
||||
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
|
||||
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
|
||||
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
|
||||
github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28=
|
||||
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
|
||||
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
|
||||
github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4=
|
||||
github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
|
||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||
github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=
|
||||
github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
|
||||
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
|
||||
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
||||
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
|
||||
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
||||
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
|
||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764=
|
||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
|
||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240708163039-9a9b82a0ce4d h1:p5T6ZPvh7nihJfjI9M/W2cbcX7n766u/OGorLmE4xoQ=
|
||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240708163039-9a9b82a0ce4d/go.mod h1:akxZg8LuwOIeCPRjcDrUS1WWcIwmLNSR2lfe4y85PH4=
|
||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e h1:GLC8iDDcbt1H8+RkNao2nRGjyNTIo81e1rAJT9/uWYA=
|
||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e/go.mod h1:ln9Whp+wVY/FTbn2SK0ag+SKD2fC0yQCF/Lqowc1LmU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
@ -138,12 +143,14 @@ github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4m
|
||||
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/dlclark/regexp2 v1.11.2 h1:/u628IuisSTwri5/UKloiIsH8+qF2Pu7xEQX+yIKg68=
|
||||
github.com/dlclark/regexp2 v1.11.2/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/fclairamb/go-log v0.5.0 h1:Gz9wSamEaA6lta4IU2cjJc2xSq5sV5VYSB5w/SUHhVc=
|
||||
github.com/fclairamb/go-log v0.5.0/go.mod h1:XoRO1dYezpsGmLLkZE9I+sHqpqY65p8JA+Vqblb7k40=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/foxxorcat/mopan-sdk-go v0.1.6 h1:6J37oI4wMZLj8EPgSCcSTTIbnI5D6RCNW/srX8vQd1Y=
|
||||
@ -151,8 +158,8 @@ github.com/foxxorcat/mopan-sdk-go v0.1.6/go.mod h1:UaY6D88yBXWGrcu/PcyLWyL4lzrk5
|
||||
github.com/foxxorcat/weiyun-sdk-go v0.1.3 h1:I5c5nfGErhq9DBumyjCVCggRA74jhgriMqRRFu5jeeY=
|
||||
github.com/foxxorcat/weiyun-sdk-go v0.1.3/go.mod h1:TPxzN0d2PahweUEHlOBWlwZSA+rELSUlGYMWgXRn9ps=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gaoyb7/115drive-webdav v0.1.8 h1:EJt4PSmcbvBY4KUh2zSo5p6fN9LZFNkIzuKejipubVw=
|
||||
@ -168,6 +175,10 @@ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@ -190,14 +201,14 @@ github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
|
||||
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
|
||||
github.com/go-resty/resty/v2 v2.14.0 h1:/rhkzsAqGQkozwfKS5aFAbb6TyKd3zyFRWcdRXLPCAU=
|
||||
github.com/go-resty/resty/v2 v2.14.0/go.mod h1:IW6mekUOsElt9C7oWr0XRt9BNSD6D5rr9mhk6NjmNHg=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4=
|
||||
github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs=
|
||||
github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE=
|
||||
github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA=
|
||||
github.com/go-webauthn/webauthn v0.11.1 h1:5G/+dg91/VcaJHTtJUfwIlNJkLwbJCcnUc4W8VtkpzA=
|
||||
github.com/go-webauthn/webauthn v0.11.1/go.mod h1:YXRm1WG0OtUyDFaVAgB5KG7kVqW+6dYCJ7FTQH4SxEE=
|
||||
github.com/go-webauthn/x v0.1.12 h1:RjQ5cvApzyU/xLCiP+rub0PE4HBZsLggbxGR5ZpUf/A=
|
||||
github.com/go-webauthn/x v0.1.12/go.mod h1:XlRcGkNH8PT45TfeJYc6gqpOtiOendHhVmnOxh+5yHs=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
@ -222,8 +233,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
||||
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
|
||||
github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
@ -240,11 +251,17 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
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/hekmon/cunits/v2 v2.1.0 h1:k6wIjc4PlacNOHwKEMBgWV2/c8jyD4eRMs5mR1BBhI0=
|
||||
github.com/hekmon/cunits/v2 v2.1.0/go.mod h1:9r1TycXYXaTmEWlAIfFV8JT+Xo59U96yUJAYHxzii2M=
|
||||
github.com/hekmon/transmissionrpc/v3 v3.0.0 h1:0Fb11qE0IBh4V4GlOwHNYpqpjcYDp5GouolwrpmcUDQ=
|
||||
github.com/hekmon/transmissionrpc/v3 v3.0.0/go.mod h1:38SlNhFzinVUuY87wGj3acOmRxeYZAZfrj6Re7UgCDg=
|
||||
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
|
||||
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@ -304,8 +321,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.8 h1:elbufnS+gQVOkzX9JLkS/N9u3ay/IAIE17nE4kNoYZ4=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.2.8/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.3.1 h1:DLQQEgHUAGZB6RVlceB1f6A94O206exxW2RIMH+gMUc=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.3.1/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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
@ -332,12 +349,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/meilisearch/meilisearch-go v0.27.0 h1:lDFq8WzbsZCtt3/byr7GFqfOygWF5iy9TtDgzJo0Ds8=
|
||||
github.com/meilisearch/meilisearch-go v0.27.0/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
|
||||
github.com/meilisearch/meilisearch-go v0.27.2 h1:3G21dJ5i208shnLPDsIEZ0L0Geg/5oeXABFV7nlK94k=
|
||||
github.com/meilisearch/meilisearch-go v0.27.2/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/minio/sio v0.4.0 h1:u4SWVEm5lXSqU42ZWawV0D9I5AZ5YMmo2RXpEQ/kRhc=
|
||||
@ -381,10 +398,14 @@ github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/n
|
||||
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||
github.com/ncw/swift/v2 v2.0.2 h1:jx282pcAKFhmoZBSdMcCRFn9VWkoBIRsCpe+yZq7vEk=
|
||||
github.com/ncw/swift/v2 v2.0.2/go.mod h1:z0A9RVdYPjNjXVo2pDOPxZ4eu3oarO1P91fTItcb+Kg=
|
||||
github.com/ncw/swift/v2 v2.0.3 h1:8R9dmgFIWs+RiVlisCEfiQiik1hjuR0JnOkLxaP9ihg=
|
||||
github.com/ncw/swift/v2 v2.0.3/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
|
||||
github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831 h1:K3T3eu4h5aYIOzUtLjN08L4Qt4WGaJONMgcaD0ayBJQ=
|
||||
github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831/go.mod h1:lSHD4lC4zlMl+zcoysdJcd5KFzsWwOD8BJbyg1Ws9Ng=
|
||||
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
||||
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||
github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
@ -431,6 +452,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
|
||||
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/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIGMa7oSwp8SQPp7Hb4ZiI38K3RNBKD2LLeEM=
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||
@ -449,6 +472,8 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:s
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@ -469,10 +494,13 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7 h1:Jtcrb09q0AVWe3BGe8qtuuGxNSHWGkTWr43kHTJ+CpA=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=
|
||||
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 h1:6Y51mutOvRGRx6KqyMNo//xk8B8o6zW9/RVmy1VamOs=
|
||||
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543/go.mod h1:jpwqYA8KUVEvSUJHkCXsnBRJCSKP1BMa81QZ6kvRpow=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
|
||||
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||
@ -504,12 +532,10 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 h1:eDfebW/yfq9DtG9RO3KP7BT2dot2CvJGIvrB0NEoDXI=
|
||||
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25/go.mod h1:fH4oNm5F9NfI5dLi0oIMtsLNKQOirUDbEMCIBb/7SU0=
|
||||
github.com/xhofe/tache v0.1.1 h1:O5QY4cVjIGELx3UGh6LbVAc18MWGXgRNQjMt72x6w/8=
|
||||
github.com/xhofe/tache v0.1.1/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ=
|
||||
github.com/xhofe/tache v0.1.3 h1:MipxzlljYX29E1YI/SLC7hVomVF+51iP1OUzlsuq1wE=
|
||||
github.com/xhofe/tache v0.1.3/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ=
|
||||
github.com/xhofe/wopan-sdk-go v0.1.3 h1:J58X6v+n25ewBZjb05pKOr7AWGohb+Rdll4CThGh6+A=
|
||||
github.com/xhofe/wopan-sdk-go v0.1.3/go.mod h1:dcY9yA28fnaoZPnXZiVTFSkcd7GnIPTpTIIlfSI5z5Q=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
@ -544,19 +570,24 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
|
||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
|
||||
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
|
||||
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/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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=
|
||||
@ -570,19 +601,24 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.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=
|
||||
@ -609,20 +645,25 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
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=
|
||||
@ -632,14 +673,17 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.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=
|
||||
@ -648,8 +692,10 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
||||
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.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
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=
|
||||
@ -658,10 +704,10 @@ google.golang.org/api v0.169.0 h1:QwWPy71FgMWqJN/l6jVlFHUa29a7dcUy02I8o799nPY=
|
||||
google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
|
||||
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
|
@ -68,11 +68,7 @@ func InitConfig() {
|
||||
}
|
||||
conf.Conf.TempDir = absPath
|
||||
}
|
||||
err := os.RemoveAll(filepath.Join(conf.Conf.TempDir))
|
||||
if err != nil {
|
||||
log.Errorln("failed delete temp file:", err)
|
||||
}
|
||||
err = os.MkdirAll(conf.Conf.TempDir, 0o777)
|
||||
err := os.MkdirAll(conf.Conf.TempDir, 0o777)
|
||||
if err != nil {
|
||||
log.Fatalf("create temp dir error: %+v", err)
|
||||
}
|
||||
@ -104,3 +100,15 @@ func initURL() {
|
||||
}
|
||||
conf.URL = u
|
||||
}
|
||||
|
||||
func CleanTempDir() {
|
||||
files, err := os.ReadDir(conf.Conf.TempDir)
|
||||
if err != nil {
|
||||
log.Errorln("failed list temp file: ", err)
|
||||
}
|
||||
for _, file := range files {
|
||||
if err := os.RemoveAll(filepath.Join(conf.Conf.TempDir, file.Name())); err != nil {
|
||||
log.Errorln("failed delete temp file: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import "github.com/alist-org/alist/v3/cmd/flags"
|
||||
func InitData() {
|
||||
initUser()
|
||||
initSettings()
|
||||
initTasks()
|
||||
if flags.Dev {
|
||||
initDevData()
|
||||
initDevDo()
|
||||
|
@ -34,6 +34,7 @@ func initSettings() {
|
||||
// create or save setting
|
||||
for i := range initialSettingItems {
|
||||
item := &initialSettingItems[i]
|
||||
item.Index = uint(i)
|
||||
if item.PreDefault == "" {
|
||||
item.PreDefault = item.Value
|
||||
}
|
||||
@ -163,6 +164,7 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.SSOApplicationName, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSOEndpointName, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSOJwtPublicKey, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSOExtraScopes, Value: "", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSOAutoRegister, Value: "false", Type: conf.TypeBool, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSODefaultDir, Value: "/", Type: conf.TypeString, Group: model.SSO, Flag: model.PRIVATE},
|
||||
{Key: conf.SSODefaultPermission, Value: "0", Type: conf.TypeNumber, Group: model.SSO, Flag: model.PRIVATE},
|
||||
@ -183,6 +185,16 @@ func InitialSettings() []model.SettingItem {
|
||||
{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},
|
||||
|
||||
//ftp settings
|
||||
{Key: conf.FTPPublicHost, Value: "127.0.0.1", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPPasvPortMap, Value: "", Type: conf.TypeText, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPProxyUserAgent, Value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"Chrome/87.0.4280.88 Safari/537.36", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPMandatoryTLS, Value: "false", Type: conf.TypeBool, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPImplicitTLS, Value: "false", Type: conf.TypeBool, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPTLSPrivateKeyPath, Value: "", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
|
||||
{Key: conf.FTPTLSPublicCertPath, Value: "", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
|
||||
}
|
||||
initialSettingItems = append(initialSettingItems, tool.Tools.Items()...)
|
||||
if flags.Dev {
|
||||
|
29
internal/bootstrap/data/task.go
Normal file
29
internal/bootstrap/data/task.go
Normal file
@ -0,0 +1,29 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
var initialTaskItems []model.TaskItem
|
||||
|
||||
func initTasks() {
|
||||
InitialTasks()
|
||||
|
||||
for i := range initialTaskItems {
|
||||
item := &initialTaskItems[i]
|
||||
taskitem, _ := db.GetTaskDataByType(item.Key)
|
||||
if taskitem == nil {
|
||||
db.CreateTaskData(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InitialTasks() []model.TaskItem {
|
||||
initialTaskItems = []model.TaskItem{
|
||||
{Key: "copy", PersistData: "[]"},
|
||||
{Key: "download", PersistData: "[]"},
|
||||
{Key: "transfer", PersistData: "[]"},
|
||||
}
|
||||
return initialTaskItems
|
||||
}
|
101
internal/bootstrap/ssh.go
Normal file
101
internal/bootstrap/ssh.go
Normal file
@ -0,0 +1,101 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/cmd/flags"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func InitHostKey() {
|
||||
sshPath := filepath.Join(flags.DataDir, "ssh")
|
||||
if !utils.Exists(sshPath) {
|
||||
err := utils.CreateNestedDirectory(sshPath)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to create ssh directory: %+v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
conf.SSHSigners = make([]ssh.Signer, 0, 4)
|
||||
if rsaKey, ok := LoadOrGenerateRSAHostKey(sshPath); ok {
|
||||
conf.SSHSigners = append(conf.SSHSigners, rsaKey)
|
||||
}
|
||||
// TODO Add keys for other encryption algorithms
|
||||
}
|
||||
|
||||
func LoadOrGenerateRSAHostKey(parentDir string) (ssh.Signer, bool) {
|
||||
privateKeyPath := filepath.Join(parentDir, "ssh_host_rsa_key")
|
||||
publicKeyPath := filepath.Join(parentDir, "ssh_host_rsa_key.pub")
|
||||
privateKeyBytes, err := os.ReadFile(privateKeyPath)
|
||||
if err == nil {
|
||||
var privateKey *rsa.PrivateKey
|
||||
privateKey, err = rsaDecodePrivateKey(privateKeyBytes)
|
||||
if err == nil {
|
||||
var ret ssh.Signer
|
||||
ret, err = ssh.NewSignerFromKey(privateKey)
|
||||
if err == nil {
|
||||
return ret, true
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = os.Remove(privateKeyPath)
|
||||
_ = os.Remove(publicKeyPath)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to generate RSA private key: %+v", err)
|
||||
return nil, false
|
||||
}
|
||||
publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to generate RSA public key: %+v", err)
|
||||
return nil, false
|
||||
}
|
||||
ret, err := ssh.NewSignerFromKey(privateKey)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to generate RSA signer: %+v", err)
|
||||
return nil, false
|
||||
}
|
||||
privateBytes := rsaEncodePrivateKey(privateKey)
|
||||
publicBytes := ssh.MarshalAuthorizedKey(publicKey)
|
||||
err = os.WriteFile(privateKeyPath, privateBytes, 0600)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to write RSA private key to file: %+v", err)
|
||||
return nil, false
|
||||
}
|
||||
err = os.WriteFile(publicKeyPath, publicBytes, 0644)
|
||||
if err != nil {
|
||||
_ = os.Remove(privateKeyPath)
|
||||
utils.Log.Fatalf("failed to write RSA public key to file: %+v", err)
|
||||
return nil, false
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func rsaEncodePrivateKey(privateKey *rsa.PrivateKey) []byte {
|
||||
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
privateBlock := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Headers: nil,
|
||||
Bytes: privateKeyBytes,
|
||||
}
|
||||
return pem.EncodeToMemory(privateBlock)
|
||||
}
|
||||
|
||||
func rsaDecodePrivateKey(bytes []byte) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode(bytes)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to parse PEM block containing the key")
|
||||
}
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privateKey, nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user