Compare commits

...

96 Commits

Author SHA1 Message Date
d4e3355f56 chore: duplicate import typo 2024-07-21 20:50:07 +08:00
94f257e557 fix(local): crush on android
closes #5874
closes #6567
2024-07-21 20:48:48 +08:00
e5f53d6dee chore: go mod tidy 2024-07-21 20:31:52 +08:00
cbd4bef814 fix(123pan): use local sort (close #6820) 2024-07-21 20:29:32 +08:00
2d57529e77 fix(123pan): add warning for mismatched file count when listing files (#6814)
Fixes an issue where using `file_name` order could result in incorrect file counts compared to response fields.
2024-07-20 12:27:18 +08:00
2b74999703 fix(deps): update module github.com/alist-org/gofakes3 to v0.0.6 (#6802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: David Hao <akang943578@gmail.com>
Co-authored-by: Ke Wang <me@ke.wang>
2024-07-17 14:31:09 +08:00
fe081d0ebc chore(deps): update softprops/action-gh-release action to v2 (#6786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-17 12:22:54 +08:00
5ef7a27be3 chore(deps): update docker/build-push-action action to v6 (#6785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-17 12:22:35 +08:00
c9a18f4de6 chore(deps): update benjlevesque/short-sha action to v3 (#6784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-17 12:22:17 +08:00
f2a24881d0 fix(deps): update module gorm.io/driver/postgres to v1.5.9 (#6783)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-17 12:21:54 +08:00
cee00005ab feat: add support for Onedrive Sharelink driver (#6793)
* feat: add support for Onedrive Sharelink driver

* fix(Onedrive Sharelink): use internal UA
2024-07-17 12:21:06 +08:00
049575b5a5 fix(pikpak): captcha_token not refreshing correctly (#6788) 2024-07-16 16:00:05 +08:00
a93937f80d fix(pikpak): add captcha_token generation function (#6775)
closes #6752 
closes #6760
2024-07-14 21:07:00 +08:00
488ebaa1af fix(deps): update module github.com/aws/aws-sdk-go to v1.54.19 (#6170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 21:04:27 +08:00
8278d3875b fix: ignore os.ErrClosed error on repeated FileStream close operations (#6762)
Also resolves the issue where S3 PutObject returns a 500 status code.
2024-07-14 20:59:24 +08:00
736ba44031 fix(deps): update module github.com/gin-gonic/gin to v1.10.0 (#6771)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 20:58:55 +08:00
a6ff6a94df fix(deps): update module golang.org/x/oauth2 to v0.21.0 (#6781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 20:58:41 +08:00
17f78b948a fix(deps): update module gorm.io/driver/mysql to v1.5.7 (#6782)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 20:58:20 +08:00
fe1040a367 chore(lark): don't use github.com/ipfs/boxo/path 2024-07-14 20:29:23 +08:00
83048e6c7c fix(deps): update module github.com/charmbracelet/lipgloss to v0.12.1 (#6768)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 20:22:50 +08:00
9128647970 fix(deps): update module github.com/rclone/rclone to v1.67.0 (#6780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 19:41:23 +08:00
9629705100 fix(deps): update module gorm.io/gorm to v1.25.11 (#6764)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 19:40:46 +08:00
cd663f78af fix(deps): update module github.com/charmbracelet/bubbletea to v0.26.6 (#6766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 19:36:10 +08:00
3c483ace4f fix(deps): update module gorm.io/driver/sqlite to v1.5.6 (#6763)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 19:09:38 +08:00
3e949fcf33 fix(deps): update module github.com/charmbracelet/bubbles to v0.18.0 (#6765)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 19:08:03 +08:00
81b0afc349 fix(deps): update module github.com/dlclark/regexp2 to v1.11.2 (#6769)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 19:07:09 +08:00
a04da3ec50 fix(deps): update module github.com/gin-contrib/cors to v1.7.2 (#6770)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 19:06:43 +08:00
9e0482afbb fix(deps): update module github.com/larksuite/oapi-sdk-go/v3 to v3.2.8 (#6756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 19:51:32 +08:00
9de40f8976 fix(deps): update module github.com/spf13/cobra to v1.8.1 (#6757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 19:50:31 +08:00
ba4df55d6e fix(deps): upgrade wopan-sdk-go (close #6663) 2024-07-13 19:49:45 +08:00
de8d2d6dc0 fix(deps): update module github.com/go-resty/resty/v2 to v2.13.1 (#6759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 19:40:57 +08:00
65b423c503 fix(deps): update github.com/city404/v6-public-rpc-proto/go digest to 9a9b82a (#6753)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:42:25 +08:00
ff20b5a6fb fix(deps): update module github.com/baidubce/bce-sdk-go to v0.9.184 (#6754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:29:05 +08:00
37d86ff55c fix(deps): update module github.com/minio/sio to v0.4.0 (#6446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:28:25 +08:00
4e1c67617f fix(deps): update module github.com/go-webauthn/webauthn to v0.10.2 (#6310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:28:13 +08:00
9bc2d340a2 fix(deps): update golang.org/x/exp digest to 46b0784 (#6486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:27:49 +08:00
60fc416d8f fix(deps): update module google.golang.org/grpc to v1.64.1 [security] (#6728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:05:55 +08:00
99c9632cdc fix(deps): update module github.com/gin-contrib/cors to v1.6.0 [security] (#6708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:05:08 +08:00
2fb772c888 fix(deps): update module github.com/meilisearch/meilisearch-go to v0.27.0 (#6436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:04:42 +08:00
87192ad07d fix(deps): update module github.com/blevesearch/bleve/v2 to v2.4.1 (#6542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:04:18 +08:00
3746831384 chore(deps): update actions-cool/issues-helper action to v3.6.0 (#6513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:03:56 +08:00
80d4fbb870 fix(deps): update module github.com/gorilla/websocket to v1.5.3 (#6653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:03:20 +08:00
92c65b450e fix(deps): update module golang.org/x/image to v0.18.0 [security] (#6658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:02:52 +08:00
213fc0232e fix(deps): update module github.com/sheltonzhu/115driver to v1.0.25 (#6447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 17:02:31 +08:00
Mmx
33be44adad chore: update polyfill URL due to service unavailability and supply chain attack risk (#6740) 2024-07-11 18:13:22 +08:00
ca0d66bd01 fix: S3 Implementation bug & Support AWS Signature V2 (#6683)
* Fix: when S3 PutObject with objectName contains /, aliyundriveopen failed due to KeyNotFound, make dir to fix this.

(cherry picked from commit eb24f45771d29a3659e75813734b290d6306cfcf)

* Upgrade gofakes3 to v0.0.5, support AWS Signature V2

(cherry picked from commit 3218d7cf2c4e1a8c51fd2414595547fd109a89ac)

---------

Co-authored-by: David Hao <akang943578@gmail.com>
2024-07-07 16:50:40 +08:00
3a3d0adfa0 feat: add pikpak offline download function (#6648)
* add pikpak offline download function

* 完善PikPak离线下载功能

* 删除多余的代码

* add task cache to avoid too many requests about API

* 优化Status函数

* 完善所有功能,目前测试无BUG

* 减少缓存时间,优化添加离线任务的参数
2024-07-07 16:50:05 +08:00
ca30849e24 feat: add support for halalcloud driver (#6696) 2024-07-07 13:20:34 +08:00
316f3569a5 feat(thunderBrowser): add automatically generate UserAgent (#6692) 2024-07-07 13:19:19 +08:00
2705877235 fix(iLanZou): resolve resource access issue (#6673)
* fix(drivers/iLanZou): resolve resource access issue on iLanZou driver mount

The driver failed to mount due to incorrect URL parameter ordering which the backend did not accept

This commit reorders the parameters to meet the backend's expectations

and ensures successful mounting of the iLanZou driver.

Closes #6271, Closes #6415

* fix(drivers/iLanZou): Fixed the error ID number returned when creating a folder

Closes #6610, Closes #6333

---------

Co-authored-by: maye174 <96584640+maye174@users.noreply.github.com>
2024-07-02 15:30:00 +08:00
432901db5a feat(thunderx): generate UserAgent automatically (#6664) 2024-07-02 14:59:07 +08:00
227d034db8 feat(sftp): add suport for passphrase of private key (#6624 close #6592)
Co-authored-by: XZB <i@1248.ink>
2024-06-28 23:50:00 +08:00
453d7da622 docs: change outdated repository link to alist-org (#6007) 2024-06-28 23:47:21 +08:00
29fe49fb87 fix(alias): Support forced refresh of file list (#6562) 2024-06-16 16:59:10 +08:00
fcf2683112 feat(ftp): custom encoding (#6528 close #1260) 2024-06-16 16:58:02 +08:00
Mmx
3a996a1a3a build: update sqlite driver (#6599)
* build: update sqlite driver

* build: remove docker build sqlite-compatible commands
2024-06-16 16:56:45 +08:00
1b14d33b9f fix(alist_v3): use net/http for uploading (#6616 close #6613) 2024-06-16 16:55:49 +08:00
639b7817bf feat: add supports for thunder_browser driver (#6529 close #6526)
* feat: add supports for thunderX driver

* fix: Fix the bug where UserID is not passed correctly

* feat: add support for thunder_browser driver
2024-05-27 21:34:26 +08:00
163af0515f fix(pikpak): refresh_token contention (#6501 close #6511) 2024-05-27 21:31:59 +08:00
8e2b9c681a fix(ilanzou): upgrade devVersion 2024-05-23 20:05:00 +08:00
0a8d710e01 fix(mopan): upgrade version (#6500) 2024-05-23 18:56:17 +08:00
d781f7127a fix: add lark to windows target 2024-05-23 11:52:37 +08:00
85d743c5d2 feat: add support for lark driver (#6475)
* feat: lark storage driver

* feat: external view mode

* limit lark targets

* fix: missing package

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-05-22 23:31:58 +08:00
5f60b51cf8 feat: add proxy_range option for 139Yun Alias AList V3 (#6496) 2024-05-22 23:31:42 +08:00
7013d1b7b8 fix: pikpak captcha_required (#6497)
* fix: pikpak captcha_required

* fix(pikpak_share):  video download
2024-05-22 23:29:29 +08:00
9eec872637 feat(mega): add 2FA support (#6473)
* feat(mega): add support for two-factor authentication in Mega driver  #6226

* feat(mega): remove debug print statement in Mega driver Init function

* feat(mega): add help message for new field
2024-05-22 23:28:14 +08:00
037850bbd5 feat(alias): support Rename and Remove (#6478)
* feat(alias): support `Rename` and `Remove`

* fix(alias): `autoFlatten` not updated after editing

* feat(alias): add `protect_same_name` option
2024-05-22 09:27:48 +08:00
bbe3d4e19f feat: add supports for thunderX driver (#6464) 2024-05-21 23:24:28 +08:00
78a9676c7c feat(alist_v3): Optional pass UA to upstream remote (#6443)
* fix(115): Support 115 302 redirect while getting link under (nested) alist_v3 remote

* chore: simplify logic

* chore: simplify logic

* use internal UA

* add option to set if the user want their ua be passed to upstream
2024-05-12 17:34:36 +08:00
8bf93562eb fix(baidu): unknown type for custom upload part size (close #6435) 2024-05-09 14:54:53 +08:00
b57afd0a98 fix(sftp): reconnect to server when connection was broken (#6416 close #6403)
* fix(sftp): reconnect to server when conn was broken (close #6403)

* fix(sftp): fix typo

---------

Co-authored-by: George Chen <gchen@isimarkets.com>
2024-05-09 14:53:25 +08:00
f261ef50cc feat: add supports for netease music driver (#6423 close #5364) 2024-05-09 14:29:35 +08:00
7e7b9b9b48 feat(s3): server support generated url request (#6431) 2024-05-09 14:28:59 +08:00
2313213f59 fix(189pc): FamilyID range overflow (#6427 close #6426) 2024-05-09 14:23:12 +08:00
5f28532423 fix(test): ensure setupStorages is executed once (#6422)
In TestGetStorageVirtualFilesByPath() and TestGetBalancedStorage(), setupStorages() was being called twice, leading to a "UNIQUE constraint failed" error.
2024-05-09 14:22:19 +08:00
4cbbda8832 fix(baidu): custom upload part size (close #5757) 2024-05-02 22:30:00 +08:00
Mmx
7bf5014417 ci: cache musl library in docker build workflow (#6392)
* ci: add musl libs into action cache

* build: update Dockerfile.ci
2024-05-02 22:28:13 +08:00
b704bba444 fix(115): disable NoOverwriteUpload (#6409 close #6251)
closed #6251
2024-05-02 22:27:55 +08:00
eecea3febd fix(onedrive): fix Ctime/Mtime (#6397) 2024-05-02 22:27:31 +08:00
0e246a7b0c chore: replace link of vidhub [skip ci] 2024-04-30 14:22:26 +08:00
Mmx
b95df1d745 perf: use io copy with buffer pool (#6389)
* feat: add io methods with buffer

* chore: move io.Copy calls to utils.CopyWithBuffer
2024-04-25 20:11:15 +08:00
ec08ecdf6c fix(baidu_netdisk): cached Ctime/Mtime (#6373 close #6370)
(cherry picked from commit 23542541e4f343d484de1f83ee5c928d2ab6753c)
2024-04-25 20:08:20 +08:00
479fc6d466 fix(webdav): make sure Mtime after Ctime (#6372 close #6371)
* fix(server/webdav) make sure Mtime >= Ctime

* fix(server/webdav) avoid variable 'stream' collides with imported package name
2024-04-24 17:13:30 +08:00
32ddab9b01 feat(123_share): add access token (#6357) 2024-04-24 14:54:01 +08:00
0c9dcec9cd fix: init storages in order (#6346) 2024-04-19 17:22:16 +08:00
793a4ea6ca fix(cloudreve): add domain to the download url if not exists (#6339 close #6265)
* fix: correct the download url got by Cloudreve driver

* fix: add an condition to the correction
2024-04-12 21:45:16 +08:00
c3c5181847 feat(Seafile): add token login (#6324 close #5302) 2024-04-10 21:50:30 +08:00
Mix
cd5a8a011d fix: typo about env of Meilisearch (#6316) 2024-04-08 18:35:23 +08:00
1756036a21 fix(authn): subfolder api is considered as a wrong origin(closes #6294 in #6301) 2024-04-03 14:33:19 +08:00
58c3cb3cf6 fix(s3): don't bind s3 port if s3 is not enabled (#6291) 2024-04-03 10:09:48 +08:00
d8e190406a feat(189pc): add family transfer upload (#6288)
* feat(189pc): add family transfer upload

* fix(189):family transfer file delete
2024-04-02 16:51:02 +08:00
2880ed70ce fix: some typos (#6283)
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
2024-04-02 16:50:30 +08:00
0e86036874 fix(doge): reget client after refresh session (#6277) 2024-03-29 14:56:49 +08:00
e37465e67e feat(crypt): force stream upload for supported drivers (#6270) 2024-03-29 14:42:01 +08:00
d517adde71 docs: use width instead of height for image in Readme (#6282)
* Update README.md

* Update README_cn.md

* Update README_ja.md
2024-03-29 14:40:43 +08:00
8a18f47e68 fix(doge): the temporary access key is only valid for two hours (#6273)
* feat: add doge driver

* doc: 补充readme文档

* fix: 对齐meta信息

* fix: 调整结构体名字,与driver保持一致

* perf: merge to s3

* Rename goge.go to doge.go

* fix: 解决多吉云临时秘钥两个小时过期的问题

* fix: 定时任务在Drop中Stop

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-03-27 14:22:26 +08:00
149 changed files with 7939 additions and 959 deletions

View File

@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions & Discussions
url: https://github.com/Xhofe/alist/discussions
url: https://github.com/alist-org/alist/discussions
about: Use GitHub discussions for message-board style questions and discussions.

View File

@ -27,7 +27,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v2.2
- uses: benjlevesque/short-sha@v3.0
id: short-sha
- name: Install dependencies

View File

@ -32,10 +32,21 @@ jobs:
flavor: |
suffix=-ffmpeg,onlatest=true
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: Cache Musl
id: cache-musl
uses: actions/cache@v4
with:
path: build/musl-libs
key: docker-musl-libs
- name: Download Musl Library
if: steps.cache-musl.outputs.cache-hit != 'true'
run: bash build.sh prepare docker-multiplatform
- name: Build go binary
run: bash build.sh dev docker-multiplatform
@ -54,7 +65,7 @@ jobs:
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ci
@ -69,7 +80,7 @@ jobs:
- name: Build and push with ffmpeg
id: docker_build_ffmpeg
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ffmpeg

View File

@ -10,7 +10,7 @@ jobs:
if: github.event.label.name == 'question'
steps:
- name: Create comment
uses: actions-cool/issues-helper@v3.5.2
uses: actions-cool/issues-helper@v3.6.0
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -42,7 +42,7 @@ jobs:
bash build.sh release
- name: Upload assets
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
prerelease: false

View File

@ -29,6 +29,6 @@ jobs:
bash build.sh release android
- name: Upload assets
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: build/compress/*

View File

@ -13,10 +13,21 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: Cache Musl
id: cache-musl
uses: actions/cache@v4
with:
path: build/musl-libs
key: docker-musl-libs
- name: Download Musl Library
if: steps.cache-musl.outputs.cache-hit != 'true'
run: bash build.sh prepare docker-multiplatform
- name: Build go binary
run: bash build.sh release docker-multiplatform
@ -40,7 +51,7 @@ jobs:
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ci
@ -60,7 +71,7 @@ jobs:
- name: Build and push with ffmpeg
id: docker_build_ffmpeg
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ffmpeg

View File

@ -29,6 +29,6 @@ jobs:
bash build.sh release linux_musl
- name: Upload assets
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: build/compress/*

View File

@ -29,6 +29,6 @@ jobs:
bash build.sh release linux_musl_arm
- name: Upload assets
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: build/compress/*

View File

@ -3,7 +3,7 @@ ARG TARGETPLATFORM
LABEL MAINTAINER="i@nn.ci"
VOLUME /opt/alist/data/
WORKDIR /opt/alist/
COPY /${TARGETPLATFORM}/alist ./
COPY /build/${TARGETPLATFORM}/alist ./
COPY entrypoint.sh /entrypoint.sh
RUN apk update && \
apk upgrade --no-cache && \

View File

@ -1,17 +1,17 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
@ -19,13 +19,13 @@
</a>
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<a href="https://github.com/alist-org/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://hub.docker.com/r/xhofe/alist">
@ -106,7 +106,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
## Discussion
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports and feature requests only.**
Please go to our [discussion forum](https://github.com/alist-org/alist/discussions) for general questions, **issues are for bug reports and feature requests only.**
## Sponsor
@ -115,7 +115,7 @@ https://alist.nn.ci/guide/sponsor.html
### Special sponsors
- [VidHub](https://okaapps.com/product/1659622164?ref=alist) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
@ -138,4 +138,4 @@ The `AList` is open-source software licensed under the AGPL-3.0 license.
---
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)

View File

@ -1,17 +1,17 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
@ -19,13 +19,13 @@
</a>
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<a href="https://github.com/alist-org/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://hub.docker.com/r/xhofe/alist">
@ -105,7 +105,7 @@
## 讨论
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) **issue仅针对错误报告和功能请求。**
一般问题请到[讨论论坛](https://github.com/alist-org/alist/discussions) **issue仅针对错误报告和功能请求。**
## 赞助
@ -113,7 +113,7 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
### 特别赞助
- [VidHub](https://zh.okaapps.com/product/1659622164?ref=alist) - 苹果生态下优雅的网盘视频播放器iPhoneiPadMacApple TV全平台支持。
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器iPhoneiPadMacApple TV全平台支持。
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
@ -136,4 +136,4 @@ Thanks goes to these wonderful people:
---
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)

View File

@ -1,17 +1,17 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
@ -19,13 +19,13 @@
</a>
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<a href="https://github.com/alist-org/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://hub.docker.com/r/xhofe/alist">
@ -106,7 +106,7 @@
## ディスカッション
一般的なご質問は[ディスカッションフォーラム](https://github.com/Xhofe/alist/discussions)をご利用ください。**問題はバグレポートと機能リクエストのみです。**
一般的なご質問は[ディスカッションフォーラム](https://github.com/alist-org/alist/discussions)をご利用ください。**問題はバグレポートと機能リクエストのみです。**
## スポンサー
@ -115,7 +115,7 @@ https://alist.nn.ci/guide/sponsor.html
### スペシャルスポンサー
- [VidHub](https://okaapps.com/product/1659622164?ref=alist) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
@ -138,4 +138,4 @@ https://alist.nn.ci/guide/sponsor.html
---
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)

View File

@ -85,28 +85,28 @@ BuildDev() {
cat md5.txt
}
PrepareBuildDocker() {
echo "replace github.com/mattn/go-sqlite3 => github.com/leso-kn/go-sqlite3 v0.0.0-20230710125852-03158dc838ed" >>go.mod
go get gorm.io/driver/sqlite@v1.4.4
go mod download
}
BuildDocker() {
PrepareBuildDocker
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
}
BuildDockerMultiplatform() {
PrepareBuildDocker
PrepareBuildDockerMusl() {
mkdir -p build/musl-libs
BASE="https://musl.cc/"
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross)
for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz"
curl -L -o "${i}.tgz" "${url}"
sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
rm -f "${i}.tgz"
lib_tgz="build/${i}.tgz"
curl -L -o "${lib_tgz}" "${url}"
tar xf "${lib_tgz}" --strip-components 1 -C build/musl-libs
rm -f "${lib_tgz}"
done
}
BuildDockerMultiplatform() {
go mod download
# run PrepareBuildDockerMusl before build
export PATH=$PATH:$PWD/build/musl-libs/bin
docker_lflags="--extldflags '-static -fpic' $ldflags"
export CGO_ENABLED=1
@ -122,7 +122,7 @@ BuildDockerMultiplatform() {
export GOARCH=$arch
export CC=${cgo_cc}
echo "building for $os_arch"
go build -o ./$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter .
go build -o build/$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter .
done
DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)
@ -136,7 +136,7 @@ BuildDockerMultiplatform() {
export GOARM=${GO_ARM[$i]}
export CC=${cgo_cc}
echo "building for $docker_arch"
go build -o ./${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter .
go build -o build/${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter .
done
}
@ -289,6 +289,10 @@ elif [ "$1" = "release" ]; then
BuildRelease
MakeRelease "md5.txt"
fi
elif [ "$1" = "prepare" ]; then
if [ "$2" = "docker-multiplatform" ]; then
PrepareBuildDockerMusl
fi
else
echo -e "Parameter error"
fi

View File

@ -91,10 +91,10 @@ the address is defined in config file`,
}
}()
}
s3r := gin.New()
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.InitS3(s3r)
if conf.Conf.S3.Port != -1 {
if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
s3r := gin.New()
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.InitS3(s3r)
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
utils.Log.Infof("start S3 server @ %s", s3Base)
go func() {

View File

@ -19,7 +19,7 @@ var config = driver.Config{
DefaultRoot: "0",
//OnlyProxy: true,
//OnlyLocal: true,
NoOverwriteUpload: true,
//NoOverwriteUpload: true,
}
func init() {

View File

@ -53,7 +53,7 @@ func (d *Pan123) Drop(ctx context.Context) error {
}
func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles(dir.GetID())
files, err := d.getFiles(dir.GetID(), dir.GetName())
if err != nil {
return nil, err
}
@ -194,7 +194,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
defer func() {
_ = tempFile.Close()
}()
if _, err = io.Copy(h, tempFile); err != nil {
if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)

View File

@ -9,14 +9,15 @@ type Addition struct {
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
driver.RootID
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
AccessToken string
//OrderBy string `json:"order_by" type:"select" options:"file_id,file_name,size,update_at" default:"file_name"`
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
AccessToken string
}
var config = driver.Config{
Name: "123Pan",
DefaultRoot: "0",
LocalSort: true,
}
func init() {

View File

@ -87,8 +87,9 @@ var _ model.Thumb = (*File)(nil)
type Files struct {
//BaseResp
Data struct {
InfoList []File `json:"InfoList"`
Next string `json:"Next"`
Total int `json:"Total"`
InfoList []File `json:"InfoList"`
} `json:"data"`
}

View File

@ -16,6 +16,7 @@ import (
"github.com/alist-org/alist/v3/pkg/utils"
resty "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
)
// do others that not defined in Driver interface
@ -232,8 +233,9 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
return body, nil
}
func (d *Pan123) getFiles(parentId string) ([]File, error) {
func (d *Pan123) getFiles(parentId string, name string) ([]File, error) {
page := 1
total := 0
res := make([]File, 0)
// 2024-02-06 fix concurrency by 123pan
for {
@ -246,8 +248,8 @@ func (d *Pan123) getFiles(parentId string) ([]File, error) {
"driveId": "0",
"limit": "100",
"next": "0",
"orderBy": d.OrderBy,
"orderDirection": d.OrderDirection,
"orderBy": "file_id",
"orderDirection": "desc",
"parentFileId": parentId,
"trashed": "false",
"SearchData": "",
@ -257,17 +259,22 @@ func (d *Pan123) getFiles(parentId string) ([]File, error) {
"operateType": "4",
"inDirectSpace": "false",
}
_, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
_res, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return nil, err
}
log.Debug(string(_res))
page++
res = append(res, resp.Data.InfoList...)
total = resp.Data.Total
if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
break
}
}
if len(res) != total {
log.Warnf("incorrect file count from remote at %s: expected %d, got %d", name, total, len(res))
}
return res, nil
}

View File

@ -4,8 +4,11 @@ import (
"context"
"encoding/base64"
"fmt"
"golang.org/x/time/rate"
"net/http"
"net/url"
"sync"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
@ -19,6 +22,7 @@ import (
type Pan123Share struct {
model.Storage
Addition
apiRateLimit sync.Map
}
func (d *Pan123Share) Config() driver.Config {
@ -146,4 +150,11 @@ func (d *Pan123Share) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
// return nil, errs.NotSupport
//}
func (d *Pan123Share) APIRateLimit(api string) bool {
limiter, _ := d.apiRateLimit.LoadOrStore(api,
rate.NewLimiter(rate.Every(time.Millisecond*700), 1))
ins := limiter.(*rate.Limiter)
return ins.Allow()
}
var _ driver.Driver = (*Pan123Share)(nil)

View File

@ -7,10 +7,11 @@ import (
type Addition struct {
ShareKey string `json:"sharekey" required:"true"`
SharePwd string `json:"sharepassword" required:"true"`
SharePwd string `json:"sharepassword"`
driver.RootID
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
//OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
AccessToken string `json:"accesstoken" type:"text"`
}
var config = driver.Config{

View File

@ -2,8 +2,15 @@ package _123Share
import (
"errors"
"fmt"
"hash/crc32"
"math"
"math/rand"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
@ -15,20 +22,45 @@ const (
Api = "https://www.123pan.com/api"
AApi = "https://www.123pan.com/a/api"
BApi = "https://www.123pan.com/b/api"
MainApi = Api
MainApi = BApi
FileList = MainApi + "/share/get"
DownloadInfo = MainApi + "/share/download/info"
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
)
func signPath(path string, os string, version string) (k string, v string) {
table := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}
random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
now := time.Now().In(time.FixedZone("CST", 8*3600))
timestamp := fmt.Sprint(now.Unix())
nowStr := []byte(now.Format("200601021504"))
for i := 0; i < len(nowStr); i++ {
nowStr[i] = table[nowStr[i]-48]
}
timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
}
func GetApi(rawUrl string) string {
u, _ := url.Parse(rawUrl)
query := u.Query()
query.Add(signPath(u.Path, "web", "3"))
u.RawQuery = query.Encode()
return u.String()
}
func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"user-agent": "Dart/2.19(dart:io)",
"platform": "android",
"app-version": "36",
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken,
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
"platform": "web",
"app-version": "3",
//"user-agent": base.UserAgent,
})
if callback != nil {
callback(req)
@ -36,7 +68,7 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
if resp != nil {
req.SetResult(resp)
}
res, err := req.Execute(method, url)
res, err := req.Execute(method, GetApi(url))
if err != nil {
return nil, err
}
@ -52,12 +84,16 @@ func (d *Pan123Share) getFiles(parentId string) ([]File, error) {
page := 1
res := make([]File, 0)
for {
if !d.APIRateLimit(FileList) {
time.Sleep(time.Millisecond * 200)
continue
}
var resp Files
query := map[string]string{
"limit": "100",
"next": "0",
"orderBy": d.OrderBy,
"orderDirection": d.OrderDirection,
"orderBy": "file_id",
"orderDirection": "desc",
"parentFileId": parentId,
"Page": strconv.Itoa(page),
"shareKey": d.ShareKey,

View File

@ -14,12 +14,15 @@ type Addition struct {
}
var config = driver.Config{
Name: "139Yun",
LocalSort: true,
Name: "139Yun",
LocalSort: true,
ProxyRangeOption: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Yun139{}
d := &Yun139{}
d.ProxyRange = true
return d
})
}

View File

@ -1,6 +1,7 @@
package _189pc
import (
"container/ring"
"context"
"net/http"
"strconv"
@ -28,6 +29,9 @@ type Cloud189PC struct {
uploadThread int
familyTransferFolder *ring.Ring
cleanFamilyTransferFile func()
storageConfig driver.Config
}
@ -52,7 +56,6 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
}
if !y.isFamily() && y.RootFolderID == "" {
y.RootFolderID = "-11"
y.FamilyID = ""
}
// 限制上传线程数
@ -79,11 +82,24 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
}
// 处理家庭云ID
if y.isFamily() && y.FamilyID == "" {
if y.FamilyID == "" {
if y.FamilyID, err = y.getFamilyID(); err != nil {
return err
}
}
// 创建中转文件夹,防止重名文件
if y.FamilyTransfer {
if y.familyTransferFolder, err = y.createFamilyTransferFolder(32); err != nil {
return err
}
}
y.cleanFamilyTransferFile = utils.NewThrottle2(time.Minute, func() {
if err := y.cleanFamilyTransfer(context.TODO()); err != nil {
utils.Log.Errorf("cleanFamilyTransferFolderError:%s", err)
}
})
return
}
@ -92,7 +108,7 @@ func (y *Cloud189PC) Drop(ctx context.Context) error {
}
func (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return y.getFiles(ctx, dir.GetID())
return y.getFiles(ctx, dir.GetID(), y.isFamily())
}
func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
@ -100,8 +116,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
URL string `json:"fileDownloadUrl"`
}
isFamily := y.isFamily()
fullUrl := API_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
}
fullUrl += "/getFileDownloadUrl.action"
@ -109,7 +126,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
_, err := y.get(fullUrl, func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParam("fileId", file.GetID())
if y.isFamily() {
if isFamily {
r.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
})
@ -119,7 +136,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
"flag": "1",
})
}
}, &downloadUrl)
}, &downloadUrl, isFamily)
if err != nil {
return nil, err
}
@ -156,8 +173,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
}
func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
isFamily := y.isFamily()
fullUrl := API_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
}
fullUrl += "/createFolder.action"
@ -169,7 +187,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
"folderName": dirName,
"relativePath": "",
})
if y.isFamily() {
if isFamily {
req.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"parentId": parentDir.GetID(),
@ -179,7 +197,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
"parentFolderId": parentDir.GetID(),
})
}
}, &newFolder)
}, &newFolder, isFamily)
if err != nil {
return nil, err
}
@ -187,27 +205,14 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
}
func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"type": "MOVE",
"taskInfos": MustString(utils.Json.MarshalToString(
[]BatchTaskInfo{
{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
},
})),
"targetFolderId": dstDir.GetID(),
})
if y.isFamily() {
req.SetFormData(map[string]string{
"familyId": y.FamilyID,
})
}
}, &resp)
isFamily := y.isFamily()
other := map[string]string{"targetFileName": dstDir.GetName()}
resp, err := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
})
if err != nil {
return nil, err
}
@ -218,10 +223,11 @@ func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.
}
func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
isFamily := y.isFamily()
queryParam := make(map[string]string)
fullUrl := API_URL
method := http.MethodPost
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
method = http.MethodGet
queryParam["familyId"] = y.FamilyID
@ -245,7 +251,7 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
_, err := y.request(fullUrl, method, func(req *resty.Request) {
req.SetContext(ctx).SetQueryParams(queryParam)
}, nil, newObj)
}, nil, newObj, isFamily)
if err != nil {
return nil, err
}
@ -253,28 +259,15 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
}
func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"type": "COPY",
"taskInfos": MustString(utils.Json.MarshalToString(
[]BatchTaskInfo{
{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
},
})),
"targetFolderId": dstDir.GetID(),
"targetFileName": dstDir.GetName(),
})
if y.isFamily() {
req.SetFormData(map[string]string{
"familyId": y.FamilyID,
})
}
}, &resp)
isFamily := y.isFamily()
other := map[string]string{"targetFileName": dstDir.GetName()}
resp, err := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
})
if err != nil {
return err
}
@ -282,27 +275,13 @@ func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
}
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"type": "DELETE",
"taskInfos": MustString(utils.Json.MarshalToString(
[]*BatchTaskInfo{
{
FileId: obj.GetID(),
FileName: obj.GetName(),
IsFolder: BoolToNumber(obj.IsDir()),
},
})),
})
isFamily := y.isFamily()
if y.isFamily() {
req.SetFormData(map[string]string{
"familyId": y.FamilyID,
})
}
}, &resp)
resp, err := y.CreateBatchTask("DELETE", IF(isFamily, y.FamilyID, ""), "", nil, BatchTaskInfo{
FileId: obj.GetID(),
FileName: obj.GetName(),
IsFolder: BoolToNumber(obj.IsDir()),
})
if err != nil {
return err
}
@ -310,25 +289,73 @@ func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
return y.WaitBatchTask("DELETE", resp.TaskID, time.Millisecond*200)
}
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (newObj model.Obj, err error) {
overwrite := true
isFamily := y.isFamily()
// 响应时间长,按需启用
if y.Addition.RapidUpload {
if newObj, err := y.RapidUpload(ctx, dstDir, stream); err == nil {
if y.Addition.RapidUpload && !stream.IsForceStreamUpload() {
if newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {
return newObj, nil
}
}
switch y.UploadMethod {
case "old":
return y.OldUpload(ctx, dstDir, stream, up)
uploadMethod := y.UploadMethod
if stream.IsForceStreamUpload() {
uploadMethod = "stream"
}
// 旧版上传家庭云也有限制
if uploadMethod == "old" {
return y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)
}
// 开启家庭云转存
if !isFamily && y.FamilyTransfer {
// 修改上传目标为家庭云文件夹
transferDstDir := dstDir
dstDir = (y.familyTransferFolder.Value).(*Cloud189Folder)
y.familyTransferFolder = y.familyTransferFolder.Next()
isFamily = true
overwrite = false
defer func() {
if newObj != nil {
// 批量任务有概率删不掉
y.cleanFamilyTransferFile()
// 转存家庭云文件到个人云
err = y.SaveFamilyFileToPersonCloud(context.TODO(), y.FamilyID, newObj, transferDstDir, true)
task := BatchTaskInfo{
FileId: newObj.GetID(),
FileName: newObj.GetName(),
IsFolder: BoolToNumber(newObj.IsDir()),
}
// 删除源文件
if resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, task); err == nil {
y.WaitBatchTask("DELETE", resp.TaskID, time.Second)
// 永久删除
if resp, err := y.CreateBatchTask("CLEAR_RECYCLE", y.FamilyID, "", nil, task); err == nil {
y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second)
}
}
newObj = nil
}
}()
}
switch uploadMethod {
case "rapid":
return y.FastUpload(ctx, dstDir, stream, up)
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
case "stream":
if stream.GetSize() == 0 {
return y.FastUpload(ctx, dstDir, stream, up)
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
}
fallthrough
default:
return y.StreamUpload(ctx, dstDir, stream, up)
return y.StreamUpload(ctx, dstDir, stream, up, isFamily, overwrite)
}
}

View File

@ -192,3 +192,19 @@ func partSize(size int64) int64 {
}
return DEFAULT
}
func isBool(bs ...bool) bool {
for _, b := range bs {
if b {
return true
}
}
return false
}
func IF[V any](o bool, t V, f V) V {
if o {
return t
}
return f
}

View File

@ -16,6 +16,7 @@ type Addition struct {
FamilyID string `json:"family_id"`
UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"`
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
FamilyTransfer bool `json:"family_transfer"`
RapidUpload bool `json:"rapid_upload"`
NoUseOcr bool `json:"no_use_ocr"`
}

View File

@ -3,10 +3,11 @@ package _189pc
import (
"encoding/xml"
"fmt"
"github.com/alist-org/alist/v3/pkg/utils"
"sort"
"strings"
"time"
"github.com/alist-org/alist/v3/pkg/utils"
)
// 居然有四种返回方式
@ -142,7 +143,7 @@ type FamilyInfoListResp struct {
type FamilyInfoResp struct {
Count int `json:"count"`
CreateTime string `json:"createTime"`
FamilyID int `json:"familyId"`
FamilyID int64 `json:"familyId"`
RemarkName string `json:"remarkName"`
Type int `json:"type"`
UseFlag int `json:"useFlag"`
@ -242,7 +243,12 @@ type BatchTaskInfo struct {
// IsFolder 是否是文件夹0-否1-是
IsFolder int `json:"isFolder"`
// SrcParentId 文件所在父目录ID
//SrcParentId string `json:"srcParentId"`
SrcParentId string `json:"srcParentId,omitempty"`
/* 冲突管理 */
// 1 -> 跳过 2 -> 保留 3 -> 覆盖
DealWay int `json:"dealWay,omitempty"`
IsConflict int `json:"isConflict,omitempty"`
}
/* 上传部分 */
@ -355,6 +361,14 @@ type BatchTaskStateResp struct {
TaskStatus int `json:"taskStatus"` //1 初始化 2 存在冲突 3 执行中4 完成
}
type BatchTaskConflictTaskInfoResp struct {
SessionKey string `json:"sessionKey"`
TargetFolderID int `json:"targetFolderId"`
TaskID string `json:"taskId"`
TaskInfos []BatchTaskInfo
TaskType int `json:"taskType"`
}
/* query 加密参数*/
type Params map[string]string

View File

@ -2,6 +2,7 @@ package _189pc
import (
"bytes"
"container/ring"
"context"
"crypto/md5"
"encoding/base64"
@ -54,11 +55,11 @@ const (
CHANNEL_ID = "web_cloud.189.cn"
)
func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]string {
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
dateOfGmt := getHttpDateStr()
sessionKey := y.tokenInfo.SessionKey
sessionSecret := y.tokenInfo.SessionSecret
if y.isFamily() {
if isFamily {
sessionKey = y.tokenInfo.FamilySessionKey
sessionSecret = y.tokenInfo.FamilySessionSecret
}
@ -72,9 +73,9 @@ func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]stri
return header
}
func (y *Cloud189PC) EncryptParams(params Params) string {
func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
sessionSecret := y.tokenInfo.SessionSecret
if y.isFamily() {
if isFamily {
sessionSecret = y.tokenInfo.FamilySessionSecret
}
if params != nil {
@ -83,17 +84,17 @@ func (y *Cloud189PC) EncryptParams(params Params) string {
return ""
}
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}) ([]byte, error) {
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) {
req := y.client.R().SetQueryParams(clientSuffix())
// 设置params
paramsData := y.EncryptParams(params)
paramsData := y.EncryptParams(params, isBool(isFamily...))
if paramsData != "" {
req.SetQueryParam("params", paramsData)
}
// Signature
req.SetHeaders(y.SignatureHeader(url, method, paramsData))
req.SetHeaders(y.SignatureHeader(url, method, paramsData, isBool(isFamily...)))
var erron RespErr
req.SetError(&erron)
@ -129,15 +130,15 @@ func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, para
return res.Body(), nil
}
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return y.request(url, http.MethodGet, callback, nil, resp)
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
return y.request(url, http.MethodGet, callback, nil, resp, isFamily...)
}
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return y.request(url, http.MethodPost, callback, nil, resp)
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
return y.request(url, http.MethodPost, callback, nil, resp, isFamily...)
}
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader) ([]byte, error) {
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader, isFamily bool) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
if err != nil {
return nil, err
@ -154,7 +155,7 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
}
if sign {
for key, value := range y.SignatureHeader(url, http.MethodPut, "") {
for key, value := range y.SignatureHeader(url, http.MethodPut, "", isFamily) {
req.Header.Add(key, value)
}
}
@ -181,9 +182,9 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
}
return body, nil
}
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, error) {
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
fullUrl := API_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
}
fullUrl += "/listFiles.action"
@ -201,7 +202,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj,
"pageNum": fmt.Sprint(pageNum),
"pageSize": "130",
})
if y.isFamily() {
if isFamily {
r.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"orderBy": toFamilyOrderBy(y.OrderBy),
@ -214,7 +215,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj,
"descending": toDesc(y.OrderDirection),
})
}
}, &resp)
}, &resp, isFamily)
if err != nil {
return nil, err
}
@ -437,7 +438,7 @@ func (y *Cloud189PC) refreshSession() (err error) {
// 普通上传
// 无法上传大小为0的文件
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
var sliceSize = partSize(file.GetSize())
count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize)))
lastPartSize := file.GetSize() % sliceSize
@ -454,7 +455,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
}
fullUrl := UPLOAD_URL
if y.isFamily() {
if isFamily {
params.Set("familyId", y.FamilyID)
fullUrl += "/family"
} else {
@ -466,7 +467,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
var initMultiUpload InitMultiUploadResp
_, err := y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
}, params, &initMultiUpload)
}, params, &initMultiUpload, isFamily)
if err != nil {
return nil, err
}
@ -502,14 +503,14 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
partInfo := fmt.Sprintf("%d-%s", i, base64.StdEncoding.EncodeToString(md5Bytes))
threadG.Go(func(ctx context.Context) error {
uploadUrls, err := y.GetMultiUploadUrls(ctx, initMultiUpload.Data.UploadFileID, partInfo)
uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, initMultiUpload.Data.UploadFileID, partInfo)
if err != nil {
return err
}
// step.4 上传切片
uploadUrl := uploadUrls[0]
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData))
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData), isFamily)
if err != nil {
return err
}
@ -538,21 +539,21 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
"sliceMd5": sliceMd5Hex,
"lazyCheck": "1",
"isLog": "0",
"opertype": "3",
}, &resp)
"opertype": IF(overwrite, "3", "1"),
}, &resp, isFamily)
if err != nil {
return nil, err
}
return resp.toFile(), nil
}
func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) (model.Obj, error) {
func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
fileMd5 := stream.GetHash().GetHash(utils.MD5)
if len(fileMd5) < utils.MD5.Width {
return nil, errors.New("invalid hash")
}
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()))
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()), isFamily)
if err != nil {
return nil, err
}
@ -561,11 +562,11 @@ func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream m
return nil, errors.New("rapid upload fail")
}
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId)
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId, isFamily, overwrite)
}
// 快传
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
tempFile, err := file.CacheFullInTempFile()
if err != nil {
return nil, err
@ -594,7 +595,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}
silceMd5.Reset()
if _, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
if _, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
return nil, err
}
md5Byte := silceMd5.Sum(nil)
@ -609,7 +610,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}
fullUrl := UPLOAD_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family"
} else {
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
@ -628,13 +629,13 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
"sliceSize": fmt.Sprint(sliceSize),
"sliceMd5": sliceMd5Hex,
}
if y.isFamily() {
if isFamily {
params.Set("familyId", y.FamilyID)
}
var uploadInfo InitMultiUploadResp
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
}, params, &uploadInfo)
}, params, &uploadInfo, isFamily)
if err != nil {
return nil, err
}
@ -659,7 +660,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
i, uploadPart := i, uploadPart
threadG.Go(func(ctx context.Context) error {
// step.3 获取上传链接
uploadUrls, err := y.GetMultiUploadUrls(ctx, uploadInfo.UploadFileID, uploadPart)
uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, uploadInfo.UploadFileID, uploadPart)
if err != nil {
return err
}
@ -671,7 +672,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}
// step.4 上传切片
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize))
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize), isFamily)
if err != nil {
return err
}
@ -698,8 +699,8 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}, Params{
"uploadFileId": uploadInfo.UploadFileID,
"isLog": "0",
"opertype": "3",
}, &resp)
"opertype": IF(overwrite, "3", "1"),
}, &resp, isFamily)
if err != nil {
return nil, err
}
@ -708,9 +709,9 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
// 获取上传切片信息
// 对http body有大小限制分片信息太多会出错
func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
fullUrl := UPLOAD_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family"
} else {
fullUrl += "/person"
@ -723,7 +724,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string
}, Params{
"uploadFileId": uploadFileId,
"partInfo": strings.Join(partInfo, ","),
}, &uploadUrlsResp)
}, &uploadUrlsResp, isFamily)
if err != nil {
return nil, err
}
@ -752,7 +753,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string
}
// 旧版本上传,家庭云不支持覆盖
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
tempFile, err := file.CacheFullInTempFile()
if err != nil {
return nil, err
@ -763,7 +764,7 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
}
// 创建上传会话
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()))
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily)
if err != nil {
return nil, err
}
@ -780,14 +781,14 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
"Expect": "100-continue",
}
if y.isFamily() {
if isFamily {
header["FamilyId"] = fmt.Sprint(y.FamilyID)
header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
} else {
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
}
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile))
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile), isFamily)
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
return nil, err
}
@ -802,10 +803,10 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
"uploadFileId": fmt.Sprint(status.UploadFileId),
"resumePolicy": "1",
})
if y.isFamily() {
if isFamily {
req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
}
}, &status)
}, &status, isFamily)
if err != nil {
return nil, err
}
@ -815,20 +816,20 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
up(float64(status.GetSize()) / float64(file.GetSize()) * 100)
}
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId)
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId, isFamily, overwrite)
}
// 创建上传会话
func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string) (*CreateUploadFileResp, error) {
func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string, isFamily bool) (*CreateUploadFileResp, error) {
var uploadInfo CreateUploadFileResp
fullUrl := API_URL + "/createUploadFile.action"
if y.isFamily() {
if isFamily {
fullUrl = API_URL + "/family/file/createFamilyFile.action"
}
_, err := y.post(fullUrl, func(req *resty.Request) {
req.SetContext(ctx)
if y.isFamily() {
if isFamily {
req.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"parentId": parentID,
@ -849,7 +850,7 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
"isLog": "0",
})
}
}, &uploadInfo)
}, &uploadInfo, isFamily)
if err != nil {
return nil, err
@ -858,11 +859,11 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
}
// 提交上传文件
func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64) (model.Obj, error) {
func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64, isFamily bool, overwrite bool) (model.Obj, error) {
var resp OldCommitUploadFileResp
_, err := y.post(fileCommitUrl, func(req *resty.Request) {
req.SetContext(ctx)
if y.isFamily() {
if isFamily {
req.SetHeaders(map[string]string{
"ResumePolicy": "1",
"UploadFileId": fmt.Sprint(uploadFileID),
@ -870,13 +871,13 @@ func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string,
})
} else {
req.SetFormData(map[string]string{
"opertype": "3",
"opertype": IF(overwrite, "3", "1"),
"resumePolicy": "1",
"uploadFileId": fmt.Sprint(uploadFileID),
"isLog": "0",
})
}
}, &resp)
}, &resp, isFamily)
if err != nil {
return nil, err
}
@ -895,10 +896,100 @@ func (y *Cloud189PC) isLogin() bool {
return err == nil
}
// 创建家庭云中转文件夹
func (y *Cloud189PC) createFamilyTransferFolder(count int) (*ring.Ring, error) {
folders := ring.New(count)
var rootFolder Cloud189Folder
_, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"folderName": "FamilyTransferFolder",
"familyId": y.FamilyID,
})
}, &rootFolder, true)
if err != nil {
return nil, err
}
folderCount := 0
// 获取已有目录
files, err := y.getFiles(context.TODO(), rootFolder.GetID(), true)
if err != nil {
return nil, err
}
for _, file := range files {
if folder, ok := file.(*Cloud189Folder); ok {
folders.Value = folder
folders = folders.Next()
folderCount++
}
}
// 创建新的目录
for folderCount < count {
var newFolder Cloud189Folder
_, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"folderName": uuid.NewString(),
"familyId": y.FamilyID,
"parentId": rootFolder.GetID(),
})
}, &newFolder, true)
if err != nil {
return nil, err
}
folders.Value = &newFolder
folders = folders.Next()
folderCount++
}
return folders, nil
}
// 清理中转文件夹
func (y *Cloud189PC) cleanFamilyTransfer(ctx context.Context) error {
var tasks []BatchTaskInfo
r := y.familyTransferFolder
for p := r.Next(); p != r; p = p.Next() {
folder := p.Value.(*Cloud189Folder)
files, err := y.getFiles(ctx, folder.GetID(), true)
if err != nil {
return err
}
for _, file := range files {
tasks = append(tasks, BatchTaskInfo{
FileId: file.GetID(),
FileName: file.GetName(),
IsFolder: BoolToNumber(file.IsDir()),
})
}
}
if len(tasks) > 0 {
// 删除
resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, tasks...)
if err != nil {
return err
}
err = y.WaitBatchTask("DELETE", resp.TaskID, time.Second)
if err != nil {
return err
}
// 永久删除
resp, err = y.CreateBatchTask("CLEAR_RECYCLE", y.FamilyID, "", nil, tasks...)
if err != nil {
return err
}
err = y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second)
return err
}
return nil
}
// 获取家庭云所有用户信息
func (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
var resp FamilyInfoListResp
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp)
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp, true)
if err != nil {
return nil, err
}
@ -922,6 +1013,73 @@ func (y *Cloud189PC) getFamilyID() (string, error) {
return fmt.Sprint(infos[0].FamilyID), nil
}
// 保存家庭云中的文件到个人云
func (y *Cloud189PC) SaveFamilyFileToPersonCloud(ctx context.Context, familyId string, srcObj, dstDir model.Obj, overwrite bool) error {
// _, err := y.post(API_URL+"/family/file/saveFileToMember.action", func(req *resty.Request) {
// req.SetQueryParams(map[string]string{
// "channelId": "home",
// "familyId": familyId,
// "destParentId": destParentId,
// "fileIdList": familyFileId,
// })
// }, nil)
// return err
task := BatchTaskInfo{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
}
resp, err := y.CreateBatchTask("COPY", familyId, dstDir.GetID(), map[string]string{
"groupId": "null",
"copyType": "2",
"shareId": "null",
}, task)
if err != nil {
return err
}
for {
state, err := y.CheckBatchTask("COPY", resp.TaskID)
if err != nil {
return err
}
switch state.TaskStatus {
case 2:
task.DealWay = IF(overwrite, 3, 2)
// 冲突时覆盖文件
if err := y.ManageBatchTask("COPY", resp.TaskID, dstDir.GetID(), task); err != nil {
return err
}
case 4:
return nil
}
time.Sleep(time.Millisecond * 400)
}
}
func (y *Cloud189PC) CreateBatchTask(aType string, familyID string, targetFolderId string, other map[string]string, taskInfos ...BatchTaskInfo) (*CreateBatchTaskResp, error) {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetFormData(map[string]string{
"type": aType,
"taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
})
if targetFolderId != "" {
req.SetFormData(map[string]string{"targetFolderId": targetFolderId})
}
if familyID != "" {
req.SetFormData(map[string]string{"familyId": familyID})
}
req.SetFormData(other)
}, &resp, familyID != "")
if err != nil {
return nil, err
}
return &resp, nil
}
// 检测任务状态
func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStateResp, error) {
var resp BatchTaskStateResp
_, err := y.post(API_URL+"/batch/checkBatchTask.action", func(req *resty.Request) {
@ -936,6 +1094,37 @@ func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStat
return &resp, nil
}
// 获取冲突的任务信息
func (y *Cloud189PC) GetConflictTaskInfo(aType string, taskID string) (*BatchTaskConflictTaskInfoResp, error) {
var resp BatchTaskConflictTaskInfoResp
_, err := y.post(API_URL+"/batch/getConflictTaskInfo.action", func(req *resty.Request) {
req.SetFormData(map[string]string{
"type": aType,
"taskId": taskID,
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
// 处理冲突
func (y *Cloud189PC) ManageBatchTask(aType string, taskID string, targetFolderId string, taskInfos ...BatchTaskInfo) error {
_, err := y.post(API_URL+"/batch/manageBatchTask.action", func(req *resty.Request) {
req.SetFormData(map[string]string{
"targetFolderId": targetFolderId,
"type": aType,
"taskId": taskID,
"taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
})
}, nil)
return err
}
var ErrIsConflict = errors.New("there is a conflict with the target object")
// 等待任务完成
func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration) error {
for {
state, err := y.CheckBatchTask(aType, taskID)
@ -944,7 +1133,7 @@ func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration)
}
switch state.TaskStatus {
case 2:
return errors.New("there is a conflict with the target object")
return ErrIsConflict
case 4:
return nil
}

View File

@ -7,6 +7,7 @@ import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
)
@ -45,6 +46,9 @@ func (d *Alias) Init(ctx context.Context) error {
d.oneKey = k
}
d.autoFlatten = true
} else {
d.oneKey = ""
d.autoFlatten = false
}
return nil
}
@ -87,8 +91,9 @@ func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
return nil, errs.ObjectNotFound
}
var objs []model.Obj
fsArgs := &fs.ListArgs{NoLog: true, Refresh: args.Refresh}
for _, dst := range dsts {
tmp, err := d.list(ctx, dst, sub)
tmp, err := d.list(ctx, dst, sub, fsArgs)
if err == nil {
objs = append(objs, tmp...)
}
@ -111,4 +116,26 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
return nil, errs.ObjectNotFound
}
func (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
reqPath, err := d.getReqPath(ctx, srcObj)
if err == nil {
return fs.Rename(ctx, *reqPath, newName)
}
if errs.IsNotImplement(err) {
return errors.New("same-name files cannot be Rename")
}
return err
}
func (d *Alias) Remove(ctx context.Context, obj model.Obj) error {
reqPath, err := d.getReqPath(ctx, obj)
if err == nil {
return fs.Remove(ctx, *reqPath)
}
if errs.IsNotImplement(err) {
return errors.New("same-name files cannot be Delete")
}
return err
}
var _ driver.Driver = (*Alias)(nil)

View File

@ -9,19 +9,25 @@ type Addition struct {
// Usually one of two
// driver.RootPath
// define other
Paths string `json:"paths" required:"true" type:"text"`
Paths string `json:"paths" required:"true" type:"text"`
ProtectSameName bool `json:"protect_same_name" default:"true" required:"false" help:"Protects same-name files from Delete or Rename"`
}
var config = driver.Config{
Name: "Alias",
LocalSort: true,
NoCache: true,
NoUpload: true,
DefaultRoot: "/",
Name: "Alias",
LocalSort: true,
NoCache: true,
NoUpload: true,
DefaultRoot: "/",
ProxyRangeOption: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Alias{}
return &Alias{
Addition: Addition{
ProtectSameName: true,
},
}
})
}

View File

@ -6,6 +6,7 @@ import (
stdpath "path"
"strings"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/sign"
@ -15,7 +16,7 @@ import (
func (d *Alias) listRoot() []model.Obj {
var objs []model.Obj
for k, _ := range d.pathMap {
for k := range d.pathMap {
obj := model.Object{
Name: k,
IsFolder: true,
@ -64,8 +65,8 @@ func (d *Alias) get(ctx context.Context, path string, dst, sub string) (model.Ob
}, nil
}
func (d *Alias) list(ctx context.Context, dst, sub string) ([]model.Obj, error) {
objs, err := fs.List(ctx, stdpath.Join(dst, sub), &fs.ListArgs{NoLog: true})
func (d *Alias) list(ctx context.Context, dst, sub string, args *fs.ListArgs) ([]model.Obj, error) {
objs, err := fs.List(ctx, stdpath.Join(dst, sub), args)
// the obj must implement the model.SetPath interface
// return objs, err
if err != nil {
@ -102,13 +103,49 @@ func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs)
return nil, err
}
if common.ShouldProxy(storage, stdpath.Base(sub)) {
return &model.Link{
link := &model.Link{
URL: fmt.Sprintf("%s/p%s?sign=%s",
common.GetApiUrl(args.HttpReq),
utils.EncodePath(reqPath, true),
sign.Sign(reqPath)),
}, nil
}
if args.HttpReq != nil && d.ProxyRange {
link.RangeReadCloser = common.NoProxyRange
}
return link, nil
}
link, _, err := fs.Link(ctx, reqPath, args)
return link, err
}
func (d *Alias) getReqPath(ctx context.Context, obj model.Obj) (*string, error) {
root, sub := d.getRootAndPath(obj.GetPath())
if sub == "" {
return nil, errs.NotSupport
}
dsts, ok := d.pathMap[root]
if !ok {
return nil, errs.ObjectNotFound
}
var reqPath *string
for _, dst := range dsts {
path := stdpath.Join(dst, sub)
_, err := fs.Get(ctx, path, &fs.GetArgs{NoLog: true})
if err != nil {
continue
}
if !d.ProtectSameName {
return &path, nil
}
if ok {
ok = false
} else {
return nil, errs.NotImplement
}
reqPath = &path
}
if reqPath == nil {
return nil, errs.ObjectNotFound
}
return reqPath, nil
}

View File

@ -6,9 +6,7 @@ import (
"io"
"net/http"
"path"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf"
@ -17,6 +15,7 @@ import (
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
type AListV3 struct {
@ -42,7 +41,7 @@ func (d *AListV3) Init(ctx context.Context) error {
return err
}
// if the username is not empty and the username is not the same as the current username, then login again
if d.Username != "" && d.Username != resp.Data.Username {
if d.Username != resp.Data.Username {
err = d.login()
if err != nil {
return err
@ -109,11 +108,19 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var resp common.Resp[FsGetResp]
// if PassUAToUpsteam is true, then pass the user-agent to the upstream
userAgent := base.UserAgent
if d.PassUAToUpsteam {
userAgent = args.Header.Get("user-agent")
if userAgent == "" {
userAgent = base.UserAgent
}
}
_, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(FsGetReq{
Path: file.GetPath(),
Password: d.MetaPassword,
})
}).SetHeader("user-agent", userAgent)
})
if err != nil {
return nil, err
@ -175,14 +182,41 @@ func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
}
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
_, err := d.requestWithTimeout("/fs/put", http.MethodPut, func(req *resty.Request) {
req.SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
SetHeader("Password", d.MetaPassword).
SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
SetContentLength(true).
SetBody(io.ReadCloser(stream))
}, time.Hour*6)
return err
req, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+"/api/fs/put", stream)
if err != nil {
return err
}
req.Header.Set("Authorization", d.Token)
req.Header.Set("File-Path", path.Join(dstDir.GetPath(), stream.GetName()))
req.Header.Set("Password", d.MetaPassword)
req.ContentLength = stream.GetSize()
// client := base.NewHttpClient()
// client.Timeout = time.Hour * 6
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
bytes, err := io.ReadAll(res.Body)
if err != nil {
return err
}
log.Debugf("[alist_v3] response body: %s", string(bytes))
if res.StatusCode >= 400 {
return fmt.Errorf("request failed, status: %s", res.Status)
}
code := utils.Json.Get(bytes, "code").ToInt()
if code != 200 {
if code == 401 || code == 403 {
err = d.login()
if err != nil {
return err
}
}
return fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(bytes, "message").ToString())
}
return nil
}
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {

View File

@ -7,18 +7,20 @@ import (
type Addition struct {
driver.RootPath
Address string `json:"url" required:"true"`
MetaPassword string `json:"meta_password"`
Username string `json:"username"`
Password string `json:"password"`
Token string `json:"token"`
Address string `json:"url" required:"true"`
MetaPassword string `json:"meta_password"`
Username string `json:"username"`
Password string `json:"password"`
Token string `json:"token"`
PassUAToUpsteam bool `json:"pass_ua_to_upsteam" default:"true"`
}
var config = driver.Config{
Name: "AList V3",
LocalSort: true,
DefaultRoot: "/",
CheckStatus: true,
Name: "AList V3",
LocalSort: true,
DefaultRoot: "/",
CheckStatus: true,
ProxyRangeOption: true,
}
func init() {

View File

@ -3,7 +3,6 @@ package alist_v3
import (
"fmt"
"net/http"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/op"
@ -14,6 +13,9 @@ import (
)
func (d *AListV3) login() error {
if d.Username == "" {
return nil
}
var resp common.Resp[LoginResp]
_, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(base.Json{
@ -57,33 +59,3 @@ func (d *AListV3) request(api, method string, callback base.ReqCallback, retry .
}
return res.Body(), nil
}
func (d *AListV3) requestWithTimeout(api, method string, callback base.ReqCallback, timeout time.Duration, retry ...bool) ([]byte, error) {
url := d.Address + "/api" + api
client := base.NewRestyClient().SetTimeout(timeout)
req := client.R()
req.SetHeader("Authorization", d.Token)
if callback != nil {
callback(req)
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
log.Debugf("[alist_v3] response body: %s", res.String())
if res.StatusCode() >= 400 {
return nil, fmt.Errorf("request failed, status: %s", res.Status())
}
code := utils.Json.Get(res.Body(), "code").ToInt()
if code != 200 {
if (code == 401 || code == 403) && !utils.IsBool(retry...) {
err = d.login()
if err != nil {
return nil, err
}
return d.requestWithTimeout(api, method, callback, timeout, true)
}
return nil, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString())
}
return res.Body(), nil
}

View File

@ -194,7 +194,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
}
if d.RapidUpload {
buf := bytes.NewBuffer(make([]byte, 0, 1024))
io.CopyN(buf, file, 1024)
utils.CopyWithBufferN(buf, file, 1024)
reqBody["pre_hash"] = utils.HashData(utils.SHA1, buf.Bytes())
if localFile != nil {
if _, err := localFile.Seek(0, io.SeekStart); err != nil {

View File

@ -136,7 +136,7 @@ func (d *AliyundriveOpen) calProofCode(stream model.FileStreamer) (string, error
if err != nil {
return "", err
}
_, err = io.CopyN(buf, reader, length)
_, err = utils.CopyWithBufferN(buf, reader, length)
if err != nil {
return "", err
}
@ -164,7 +164,7 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
count := int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
createData["part_info_list"] = makePartInfos(count)
// rapid upload
rapidUpload := stream.GetSize() > 100*utils.KB && d.RapidUpload
rapidUpload := !stream.IsForceStreamUpload() && stream.GetSize() > 100*utils.KB && d.RapidUpload
if rapidUpload {
log.Debugf("[aliyundrive_open] start cal pre_hash")
// read 1024 bytes to calculate pre hash
@ -242,13 +242,16 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
if remain := stream.GetSize() - offset; length > remain {
length = remain
}
//rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
rd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
if err != nil {
return nil, err
rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
if rapidUpload {
srd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
if err != nil {
return nil, err
}
rd = utils.NewMultiReadable(srd)
}
err = retry.Do(func() error {
//rd.Reset()
rd.Reset()
return d.uploadPart(ctx, rd, createResp.PartInfoList[i])
},
retry.Attempts(3),

View File

@ -25,6 +25,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/ftp"
_ "github.com/alist-org/alist/v3/drivers/google_drive"
_ "github.com/alist-org/alist/v3/drivers/google_photo"
_ "github.com/alist-org/alist/v3/drivers/halalcloud"
_ "github.com/alist-org/alist/v3/drivers/ilanzou"
_ "github.com/alist-org/alist/v3/drivers/ipfs_api"
_ "github.com/alist-org/alist/v3/drivers/lanzou"
@ -32,8 +33,10 @@ import (
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
_ "github.com/alist-org/alist/v3/drivers/mega"
_ "github.com/alist-org/alist/v3/drivers/mopan"
_ "github.com/alist-org/alist/v3/drivers/netease_music"
_ "github.com/alist-org/alist/v3/drivers/onedrive"
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
_ "github.com/alist-org/alist/v3/drivers/onedrive_sharelink"
_ "github.com/alist-org/alist/v3/drivers/pikpak"
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
@ -45,6 +48,8 @@ import (
_ "github.com/alist-org/alist/v3/drivers/teambition"
_ "github.com/alist-org/alist/v3/drivers/terabox"
_ "github.com/alist-org/alist/v3/drivers/thunder"
_ "github.com/alist-org/alist/v3/drivers/thunder_browser"
_ "github.com/alist-org/alist/v3/drivers/thunderx"
_ "github.com/alist-org/alist/v3/drivers/trainbit"
_ "github.com/alist-org/alist/v3/drivers/url_tree"
_ "github.com/alist-org/alist/v3/drivers/uss"

View File

@ -165,9 +165,16 @@ func (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream mo
if err != nil {
return nil, err
}
// 修复时间,具体原因见 Put 方法注释的 **注意**
newFile.Ctime = stream.CreateTime().Unix()
newFile.Mtime = stream.ModTime().Unix()
return fileToObj(newFile), nil
}
// Put
//
// **注意**: 截至 2024/04/20 百度云盘 api 接口返回的时间永远是当前时间,而不是文件时间。
// 而实际上云盘存储的时间是文件时间,所以此处需要覆盖时间,保证缓存与云盘的数据一致
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
// rapid upload
if newObj, err := d.PutRapid(ctx, dstDir, stream); err == nil {
@ -204,7 +211,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
if i == count {
byteSize = lastBlockSize
}
_, err := io.CopyN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
if err != nil && err != io.EOF {
return nil, err
}
@ -245,9 +252,9 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
log.Debugf("%+v", precreateResp)
if precreateResp.ReturnType == 2 {
//rapid upload, since got md5 match from baidu server
if err != nil {
return nil, err
}
// 修复时间,具体原因见 Put 方法注释的 **注意**
precreateResp.File.Ctime = ctime
precreateResp.File.Mtime = mtime
return fileToObj(precreateResp.File), nil
}
}
@ -298,6 +305,9 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
if err != nil {
return nil, err
}
// 修复时间,具体原因见 Put 方法注释的 **注意**
newFile.Ctime = ctime
newFile.Mtime = mtime
return fileToObj(newFile), nil
}

View File

@ -8,15 +8,16 @@ import (
type Addition struct {
RefreshToken string `json:"refresh_token" required:"true"`
driver.RootPath
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
AccessToken string
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
AccessToken string
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
}
var config = driver.Config{

View File

@ -249,6 +249,9 @@ const (
)
func (d *BaiduNetdisk) getSliceSize() int64 {
if d.CustomUploadPartSize != 0 {
return d.CustomUploadPartSize
}
switch d.vipType {
case 1:
return VipSliceSize

View File

@ -261,7 +261,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
if i == count {
byteSize = lastBlockSize
}
_, err := io.CopyN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
if err != nil && err != io.EOF {
return nil, err
}

View File

@ -229,7 +229,7 @@ func (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
if err != nil {
return err
}
_, err = io.Copy(filePart, stream)
_, err = utils.CopyWithBuffer(filePart, stream)
if err != nil {
return err
}

View File

@ -71,6 +71,9 @@ func (d *Cloudreve) Link(ctx context.Context, file model.Obj, args model.LinkArg
if err != nil {
return nil, err
}
if strings.HasPrefix(dUrl, "/api") {
dUrl = d.Address + dUrl
}
return &model.Link{
URL: dUrl,
}, nil

View File

@ -3,7 +3,6 @@ package crypt
import (
"context"
"fmt"
"github.com/alist-org/alist/v3/internal/stream"
"io"
stdpath "path"
"regexp"
@ -14,6 +13,7 @@ import (
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
@ -160,7 +160,7 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
// discarding hash as it's encrypted
}
if d.Thumbnail && thumb == "" {
thumb = utils.EncodePath(common.GetApiUrl(nil) + stdpath.Join("/d", args.ReqPath, ".thumbnails", name+".webp"), true)
thumb = utils.EncodePath(common.GetApiUrl(nil)+stdpath.Join("/d", args.ReqPath, ".thumbnails", name+".webp"), true)
}
if !ok && !d.Thumbnail {
result = append(result, &objRes)
@ -389,10 +389,11 @@ func (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileSt
Modified: streamer.ModTime(),
IsFolder: streamer.IsDir(),
},
Reader: wrappedIn,
Mimetype: "application/octet-stream",
WebPutAsTask: streamer.NeedStore(),
Exist: streamer.GetExist(),
Reader: wrappedIn,
Mimetype: "application/octet-stream",
WebPutAsTask: streamer.NeedStore(),
ForceStreamUpload: true,
Exist: streamer.GetExist(),
}
err = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false)
if err != nil {

View File

@ -39,7 +39,7 @@ func (d *FTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]m
if err := d.login(); err != nil {
return nil, err
}
entries, err := d.conn.List(dir.GetPath())
entries, err := d.conn.List(encode(dir.GetPath(), d.Encoding))
if err != nil {
return nil, err
}
@ -49,7 +49,7 @@ func (d *FTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]m
continue
}
f := model.Object{
Name: entry.Name,
Name: decode(entry.Name, d.Encoding),
Size: int64(entry.Size),
Modified: entry.Time,
IsFolder: entry.Type == ftp.EntryTypeFolder,
@ -64,7 +64,7 @@ func (d *FTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*m
return nil, err
}
r := NewFileReader(d.conn, file.GetPath(), file.GetSize())
r := NewFileReader(d.conn, encode(file.GetPath(), d.Encoding), file.GetSize())
link := &model.Link{
MFile: r,
}
@ -75,21 +75,27 @@ func (d *FTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string)
if err := d.login(); err != nil {
return err
}
return d.conn.MakeDir(stdpath.Join(parentDir.GetPath(), dirName))
return d.conn.MakeDir(encode(stdpath.Join(parentDir.GetPath(), dirName), d.Encoding))
}
func (d *FTP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
if err := d.login(); err != nil {
return err
}
return d.conn.Rename(srcObj.GetPath(), stdpath.Join(dstDir.GetPath(), srcObj.GetName()))
return d.conn.Rename(
encode(srcObj.GetPath(), d.Encoding),
encode(stdpath.Join(dstDir.GetPath(), srcObj.GetName()), d.Encoding),
)
}
func (d *FTP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
if err := d.login(); err != nil {
return err
}
return d.conn.Rename(srcObj.GetPath(), stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName))
return d.conn.Rename(
encode(srcObj.GetPath(), d.Encoding),
encode(stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName), d.Encoding),
)
}
func (d *FTP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
@ -100,10 +106,11 @@ func (d *FTP) Remove(ctx context.Context, obj model.Obj) error {
if err := d.login(); err != nil {
return err
}
path := encode(obj.GetPath(), d.Encoding)
if obj.IsDir() {
return d.conn.RemoveDirRecur(obj.GetPath())
return d.conn.RemoveDirRecur(path)
} else {
return d.conn.Delete(obj.GetPath())
return d.conn.Delete(path)
}
}
@ -112,7 +119,8 @@ func (d *FTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStream
return err
}
// TODO: support cancel
return d.conn.Stor(stdpath.Join(dstDir.GetPath(), stream.GetName()), stream)
path := stdpath.Join(dstDir.GetPath(), stream.GetName())
return d.conn.Stor(encode(path, d.Encoding), stream)
}
var _ driver.Driver = (*FTP)(nil)

View File

@ -3,10 +3,28 @@ package ftp
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
"github.com/axgle/mahonia"
)
func encode(str string, encoding string) string {
if encoding == "" {
return str
}
encoder := mahonia.NewEncoder(encoding)
return encoder.ConvertString(str)
}
func decode(str string, encoding string) string {
if encoding == "" {
return str
}
decoder := mahonia.NewDecoder(encoding)
return decoder.ConvertString(str)
}
type Addition struct {
Address string `json:"address" required:"true"`
Encoding string `json:"encoding" required:"true"`
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
driver.RootPath

View File

@ -0,0 +1,406 @@
package halalcloud
import (
"context"
"crypto/sha1"
"fmt"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/city404/v6-public-rpc-proto/go/v6/common"
pbPublicUser "github.com/city404/v6-public-rpc-proto/go/v6/user"
pubUserFile "github.com/city404/v6-public-rpc-proto/go/v6/userfile"
"github.com/rclone/rclone/lib/readers"
"github.com/zzzhr1990/go-common-entity/userfile"
"io"
"net/url"
"path"
"strconv"
"time"
)
type HalalCloud struct {
*HalalCommon
model.Storage
Addition
uploadThread int
}
func (d *HalalCloud) Config() driver.Config {
return config
}
func (d *HalalCloud) GetAddition() driver.Additional {
return &d.Addition
}
func (d *HalalCloud) Init(ctx context.Context) error {
d.uploadThread, _ = strconv.Atoi(d.UploadThread)
if d.uploadThread < 1 || d.uploadThread > 32 {
d.uploadThread, d.UploadThread = 3, "3"
}
if d.HalalCommon == nil {
d.HalalCommon = &HalalCommon{
Common: &Common{},
AuthService: &AuthService{
appID: func() string {
if d.Addition.AppID != "" {
return d.Addition.AppID
}
return AppID
}(),
appVersion: func() string {
if d.Addition.AppVersion != "" {
return d.Addition.AppVersion
}
return AppVersion
}(),
appSecret: func() string {
if d.Addition.AppSecret != "" {
return d.Addition.AppSecret
}
return AppSecret
}(),
tr: &TokenResp{
RefreshToken: d.Addition.RefreshToken,
},
},
UserInfo: &UserInfo{},
refreshTokenFunc: func(token string) error {
d.Addition.RefreshToken = token
op.MustSaveDriverStorage(d)
return nil
},
}
}
// 防止重复登录
if d.Addition.RefreshToken == "" || !d.IsLogin() {
as, err := d.NewAuthServiceWithOauth()
if err != nil {
d.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
return err
}
d.HalalCommon.AuthService = as
d.SetTokenResp(as.tr)
op.MustSaveDriverStorage(d)
}
var err error
d.HalalCommon.serv, err = d.NewAuthService(d.Addition.RefreshToken)
if err != nil {
return err
}
return nil
}
func (d *HalalCloud) Drop(ctx context.Context) error {
return nil
}
func (d *HalalCloud) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return d.getFiles(ctx, dir)
}
func (d *HalalCloud) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
return d.getLink(ctx, file, args)
}
func (d *HalalCloud) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
return d.makeDir(ctx, parentDir, dirName)
}
func (d *HalalCloud) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
return d.move(ctx, srcObj, dstDir)
}
func (d *HalalCloud) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
return d.rename(ctx, srcObj, newName)
}
func (d *HalalCloud) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
return d.copy(ctx, srcObj, dstDir)
}
func (d *HalalCloud) Remove(ctx context.Context, obj model.Obj) error {
return d.remove(ctx, obj)
}
func (d *HalalCloud) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
return d.put(ctx, dstDir, stream, up)
}
func (d *HalalCloud) IsLogin() bool {
if d.AuthService.tr == nil {
return false
}
serv, err := d.NewAuthService(d.Addition.RefreshToken)
if err != nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
result, err := pbPublicUser.NewPubUserClient(serv.GetGrpcConnection()).Get(ctx, &pbPublicUser.User{
Identity: "",
})
if result == nil || err != nil {
return false
}
d.UserInfo.Identity = result.Identity
d.UserInfo.CreateTs = result.CreateTs
d.UserInfo.Name = result.Name
d.UserInfo.UpdateTs = result.UpdateTs
return true
}
type HalalCommon struct {
*Common
*AuthService // 登录信息
*UserInfo // 用户信息
refreshTokenFunc func(token string) error
serv *AuthService
}
func (d *HalalCloud) SetTokenResp(tr *TokenResp) {
d.Addition.RefreshToken = tr.RefreshToken
}
func (d *HalalCloud) getFiles(ctx context.Context, dir model.Obj) ([]model.Obj, error) {
files := make([]model.Obj, 0)
limit := int64(100)
token := ""
client := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection())
opDir := d.GetCurrentDir(dir)
for {
result, err := client.List(ctx, &pubUserFile.FileListRequest{
Parent: &pubUserFile.File{Path: opDir},
ListInfo: &common.ScanListRequest{
Limit: limit,
Token: token,
},
})
if err != nil {
return nil, err
}
for i := 0; len(result.Files) > i; i++ {
files = append(files, (*Files)(result.Files[i]))
}
if result.ListInfo == nil || result.ListInfo.Token == "" {
break
}
token = result.ListInfo.Token
}
return files, nil
}
func (d *HalalCloud) getLink(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
client := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection())
ctx1, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
result, err := client.ParseFileSlice(ctx1, (*pubUserFile.File)(file.(*Files)))
if err != nil {
return nil, err
}
fileAddrs := []*pubUserFile.SliceDownloadInfo{}
var addressDuration int64
nodesNumber := len(result.RawNodes)
nodesIndex := nodesNumber - 1
startIndex, endIndex := 0, nodesIndex
for nodesIndex >= 0 {
if nodesIndex >= 200 {
endIndex = 200
} else {
endIndex = nodesNumber
}
for ; endIndex <= nodesNumber; endIndex += 200 {
if endIndex == 0 {
endIndex = 1
}
sliceAddress, err := client.GetSliceDownloadAddress(ctx, &pubUserFile.SliceDownloadAddressRequest{
Identity: result.RawNodes[startIndex:endIndex],
Version: 1,
})
if err != nil {
return nil, err
}
addressDuration = sliceAddress.ExpireAt
fileAddrs = append(fileAddrs, sliceAddress.Addresses...)
startIndex = endIndex
nodesIndex -= 200
}
}
size := result.FileSize
chunks := getChunkSizes(result.Sizes)
var finalClosers utils.Closers
resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
length := httpRange.Length
if httpRange.Length >= 0 && httpRange.Start+httpRange.Length >= size {
length = -1
}
if err != nil {
return nil, fmt.Errorf("open download file failed: %w", err)
}
oo := &openObject{
ctx: ctx,
d: fileAddrs,
chunk: &[]byte{},
chunks: &chunks,
skip: httpRange.Start,
sha: result.Sha1,
shaTemp: sha1.New(),
}
finalClosers.Add(oo)
return readers.NewLimitedReadCloser(oo, length), nil
}
var duration time.Duration
if addressDuration != 0 {
duration = time.Until(time.UnixMilli(addressDuration))
} else {
duration = time.Until(time.Now().Add(time.Hour))
}
resultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: finalClosers}
return &model.Link{
RangeReadCloser: resultRangeReadCloser,
Expiration: &duration,
}, nil
}
func (d *HalalCloud) makeDir(ctx context.Context, dir model.Obj, name string) (model.Obj, error) {
newDir := userfile.NewFormattedPath(d.GetCurrentOpDir(dir, []string{name}, 0)).GetPath()
_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Create(ctx, &pubUserFile.File{
Path: newDir,
})
return nil, err
}
func (d *HalalCloud) move(ctx context.Context, obj model.Obj, dir model.Obj) (model.Obj, error) {
oldDir := userfile.NewFormattedPath(d.GetCurrentDir(obj)).GetPath()
newDir := userfile.NewFormattedPath(d.GetCurrentDir(dir)).GetPath()
_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Move(ctx, &pubUserFile.BatchOperationRequest{
Source: []*pubUserFile.File{
{
Identity: obj.GetID(),
Path: oldDir,
},
},
Dest: &pubUserFile.File{
Identity: dir.GetID(),
Path: newDir,
},
})
return nil, err
}
func (d *HalalCloud) rename(ctx context.Context, obj model.Obj, name string) (model.Obj, error) {
id := obj.GetID()
newPath := userfile.NewFormattedPath(d.GetCurrentOpDir(obj, []string{name}, 0)).GetPath()
_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Rename(ctx, &pubUserFile.File{
Path: newPath,
Identity: id,
Name: name,
})
return nil, err
}
func (d *HalalCloud) copy(ctx context.Context, obj model.Obj, dir model.Obj) (model.Obj, error) {
id := obj.GetID()
sourcePath := userfile.NewFormattedPath(d.GetCurrentDir(obj)).GetPath()
if len(id) > 0 {
sourcePath = ""
}
dest := &pubUserFile.File{
Identity: dir.GetID(),
Path: userfile.NewFormattedPath(d.GetCurrentDir(dir)).GetPath(),
}
_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Copy(ctx, &pubUserFile.BatchOperationRequest{
Source: []*pubUserFile.File{
{
Path: sourcePath,
Identity: id,
},
},
Dest: dest,
})
return nil, err
}
func (d *HalalCloud) remove(ctx context.Context, obj model.Obj) error {
id := obj.GetID()
newPath := userfile.NewFormattedPath(d.GetCurrentDir(obj)).GetPath()
//if len(id) > 0 {
// newPath = ""
//}
_, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).Delete(ctx, &pubUserFile.BatchOperationRequest{
Source: []*pubUserFile.File{
{
Path: newPath,
Identity: id,
},
},
})
return err
}
func (d *HalalCloud) put(ctx context.Context, dstDir model.Obj, fileStream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
newDir := path.Join(dstDir.GetPath(), fileStream.GetName())
result, err := pubUserFile.NewPubUserFileClient(d.HalalCommon.serv.GetGrpcConnection()).CreateUploadToken(ctx, &pubUserFile.File{
Path: newDir,
})
if err != nil {
return nil, err
}
u, _ := url.Parse(result.Endpoint)
u.Host = "s3." + u.Host
result.Endpoint = u.String()
s, err := session.NewSession(&aws.Config{
HTTPClient: base.HttpClient,
Credentials: credentials.NewStaticCredentials(result.AccessKey, result.SecretKey, result.Token),
Region: aws.String(result.Region),
Endpoint: aws.String(result.Endpoint),
S3ForcePathStyle: aws.Bool(true),
})
if err != nil {
return nil, err
}
uploader := s3manager.NewUploader(s, func(u *s3manager.Uploader) {
u.Concurrency = d.uploadThread
})
if fileStream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
uploader.PartSize = fileStream.GetSize() / (s3manager.MaxUploadParts - 1)
}
_, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{
Bucket: aws.String(result.Bucket),
Key: aws.String(result.Key),
Body: io.TeeReader(fileStream, driver.NewProgress(fileStream.GetSize(), up)),
})
return nil, err
}
var _ driver.Driver = (*HalalCloud)(nil)

View File

@ -0,0 +1,38 @@
package halalcloud
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
// Usually one of two
driver.RootPath
// define other
RefreshToken string `json:"refresh_token" required:"true" help:"login type is refresh_token,this is required"`
UploadThread string `json:"upload_thread" default:"3" help:"1 <= thread <= 32"`
AppID string `json:"app_id" required:"true" default:"devDebugger/1.0"`
AppVersion string `json:"app_version" required:"true" default:"1.0.0"`
AppSecret string `json:"app_secret" required:"true" default:"Nkx3Y2xvZ2luLmNu"`
}
var config = driver.Config{
Name: "HalalCloud",
LocalSort: false,
OnlyLocal: true,
OnlyProxy: true,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "/",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &HalalCloud{}
})
}

View File

@ -0,0 +1,52 @@
package halalcloud
import "google.golang.org/grpc"
func defaultOptions() halalOptions {
return halalOptions{
// onRefreshTokenRefreshed: func(string) {},
grpcOptions: []grpc.DialOption{
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024 * 1024 * 32)),
// grpc.WithMaxMsgSize(1024 * 1024 * 1024),
},
}
}
type HalalOption interface {
apply(*halalOptions)
}
// halalOptions configure a RPC call. halalOptions are set by the HalalOption
// values passed to Dial.
type halalOptions struct {
onTokenRefreshed func(accessToken string, accessTokenExpiredAt int64, refreshToken string, refreshTokenExpiredAt int64)
grpcOptions []grpc.DialOption
}
// funcDialOption wraps a function that modifies halalOptions into an
// implementation of the DialOption interface.
type funcDialOption struct {
f func(*halalOptions)
}
func (fdo *funcDialOption) apply(do *halalOptions) {
fdo.f(do)
}
func newFuncDialOption(f func(*halalOptions)) *funcDialOption {
return &funcDialOption{
f: f,
}
}
func WithRefreshTokenRefreshedCallback(s func(accessToken string, accessTokenExpiredAt int64, refreshToken string, refreshTokenExpiredAt int64)) HalalOption {
return newFuncDialOption(func(o *halalOptions) {
o.onTokenRefreshed = s
})
}
func WithGrpcDialOptions(opts ...grpc.DialOption) HalalOption {
return newFuncDialOption(func(o *halalOptions) {
o.grpcOptions = opts
})
}

101
drivers/halalcloud/types.go Normal file
View File

@ -0,0 +1,101 @@
package halalcloud
import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/city404/v6-public-rpc-proto/go/v6/common"
pubUserFile "github.com/city404/v6-public-rpc-proto/go/v6/userfile"
"google.golang.org/grpc"
"time"
)
type AuthService struct {
appID string
appVersion string
appSecret string
grpcConnection *grpc.ClientConn
dopts halalOptions
tr *TokenResp
}
type TokenResp struct {
AccessToken string `json:"accessToken,omitempty"`
AccessTokenExpiredAt int64 `json:"accessTokenExpiredAt,omitempty"`
RefreshToken string `json:"refreshToken,omitempty"`
RefreshTokenExpiredAt int64 `json:"refreshTokenExpiredAt,omitempty"`
}
type UserInfo struct {
Identity string `json:"identity,omitempty"`
UpdateTs int64 `json:"updateTs,omitempty"`
Name string `json:"name,omitempty"`
CreateTs int64 `json:"createTs,omitempty"`
}
type OrderByInfo struct {
Field string `json:"field,omitempty"`
Asc bool `json:"asc,omitempty"`
}
type ListInfo struct {
Token string `json:"token,omitempty"`
Limit int64 `json:"limit,omitempty"`
OrderBy []*OrderByInfo `json:"order_by,omitempty"`
Version int32 `json:"version,omitempty"`
}
type FilesList struct {
Files []*Files `json:"files,omitempty"`
ListInfo *common.ScanListRequest `json:"list_info,omitempty"`
}
var _ model.Obj = (*Files)(nil)
type Files pubUserFile.File
func (f *Files) GetSize() int64 {
return f.Size
}
func (f *Files) GetName() string {
return f.Name
}
func (f *Files) ModTime() time.Time {
return time.UnixMilli(f.UpdateTs)
}
func (f *Files) CreateTime() time.Time {
return time.UnixMilli(f.UpdateTs)
}
func (f *Files) IsDir() bool {
return f.Dir
}
func (f *Files) GetHash() utils.HashInfo {
return utils.HashInfo{}
}
func (f *Files) GetID() string {
if len(f.Identity) == 0 {
f.Identity = "/"
}
return f.Identity
}
func (f *Files) GetPath() string {
return f.Path
}
type SteamFile struct {
file model.File
}
func (s *SteamFile) Read(p []byte) (n int, err error) {
return s.file.Read(p)
}
func (s *SteamFile) Close() error {
return s.file.Close()
}

385
drivers/halalcloud/util.go Normal file
View File

@ -0,0 +1,385 @@
package halalcloud
import (
"bytes"
"context"
"crypto/md5"
"crypto/tls"
"encoding/hex"
"errors"
"fmt"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
pbPublicUser "github.com/city404/v6-public-rpc-proto/go/v6/user"
pubUserFile "github.com/city404/v6-public-rpc-proto/go/v6/userfile"
"github.com/google/uuid"
"github.com/ipfs/go-cid"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"hash"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
const (
AppID = "devDebugger/1.0"
AppVersion = "1.0.0"
AppSecret = "Nkx3Y2xvZ2luLmNu"
)
const (
grpcServer = "grpcuserapi.2dland.cn:443"
grpcServerAuth = "grpcuserapi.2dland.cn"
)
func (d *HalalCloud) NewAuthServiceWithOauth(options ...HalalOption) (*AuthService, error) {
aService := &AuthService{}
err2 := errors.New("")
svc := d.HalalCommon.AuthService
for _, opt := range options {
opt.apply(&svc.dopts)
}
grpcOptions := svc.dopts.grpcOptions
grpcOptions = append(grpcOptions, grpc.WithAuthority(grpcServerAuth), grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctxx := svc.signContext(method, ctx)
err := invoker(ctxx, method, req, reply, cc, opts...) // invoking RPC method
return err
}))
grpcConnection, err := grpc.NewClient(grpcServer, grpcOptions...)
if err != nil {
return nil, err
}
defer grpcConnection.Close()
userClient := pbPublicUser.NewPubUserClient(grpcConnection)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
stateString := uuid.New().String()
// queryValues.Add("callback", oauthToken.Callback)
oauthToken, err := userClient.CreateAuthToken(ctx, &pbPublicUser.LoginRequest{
ReturnType: 2,
State: stateString,
ReturnUrl: "",
})
if err != nil {
return nil, err
}
if len(oauthToken.State) < 1 {
oauthToken.State = stateString
}
if oauthToken.Url != "" {
return nil, fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, oauthToken.Url)
}
return aService, err2
}
func (d *HalalCloud) NewAuthService(refreshToken string, options ...HalalOption) (*AuthService, error) {
svc := d.HalalCommon.AuthService
if len(refreshToken) < 1 {
refreshToken = d.Addition.RefreshToken
}
if len(d.tr.AccessToken) > 0 {
accessTokenExpiredAt := d.tr.AccessTokenExpiredAt
current := time.Now().UnixMilli()
if accessTokenExpiredAt < current {
// access token expired
d.tr.AccessToken = ""
d.tr.AccessTokenExpiredAt = 0
} else {
svc.tr.AccessTokenExpiredAt = accessTokenExpiredAt
svc.tr.AccessToken = d.tr.AccessToken
}
}
for _, opt := range options {
opt.apply(&svc.dopts)
}
grpcOptions := svc.dopts.grpcOptions
grpcOptions = append(grpcOptions, grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(10*1024*1024), grpc.MaxCallRecvMsgSize(10*1024*1024)), grpc.WithAuthority(grpcServerAuth), grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctxx := svc.signContext(method, ctx)
err := invoker(ctxx, method, req, reply, cc, opts...) // invoking RPC method
if err != nil {
grpcStatus, ok := status.FromError(err)
if ok && grpcStatus.Code() == codes.Unauthenticated && strings.Contains(grpcStatus.Err().Error(), "invalid accesstoken") && len(refreshToken) > 0 {
// refresh token
refreshResponse, err := pbPublicUser.NewPubUserClient(cc).Refresh(ctx, &pbPublicUser.Token{
RefreshToken: refreshToken,
})
if err != nil {
return err
}
if len(refreshResponse.AccessToken) > 0 {
svc.tr.AccessToken = refreshResponse.AccessToken
svc.tr.AccessTokenExpiredAt = refreshResponse.AccessTokenExpireTs
svc.OnAccessTokenRefreshed(refreshResponse.AccessToken, refreshResponse.AccessTokenExpireTs, refreshResponse.RefreshToken, refreshResponse.RefreshTokenExpireTs)
}
// retry
ctxx := svc.signContext(method, ctx)
err = invoker(ctxx, method, req, reply, cc, opts...) // invoking RPC method
if err != nil {
return err
} else {
return nil
}
}
}
return err
}))
grpcConnection, err := grpc.NewClient(grpcServer, grpcOptions...)
if err != nil {
return nil, err
}
svc.grpcConnection = grpcConnection
return svc, err
}
func (s *AuthService) OnAccessTokenRefreshed(accessToken string, accessTokenExpiredAt int64, refreshToken string, refreshTokenExpiredAt int64) {
s.tr.AccessToken = accessToken
s.tr.AccessTokenExpiredAt = accessTokenExpiredAt
s.tr.RefreshToken = refreshToken
s.tr.RefreshTokenExpiredAt = refreshTokenExpiredAt
if s.dopts.onTokenRefreshed != nil {
s.dopts.onTokenRefreshed(accessToken, accessTokenExpiredAt, refreshToken, refreshTokenExpiredAt)
}
}
func (s *AuthService) GetGrpcConnection() *grpc.ClientConn {
return s.grpcConnection
}
func (s *AuthService) Close() {
_ = s.grpcConnection.Close()
}
func (s *AuthService) signContext(method string, ctx context.Context) context.Context {
var kvString []string
currentTimeStamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
bufferedString := bytes.NewBufferString(method)
kvString = append(kvString, "timestamp", currentTimeStamp)
bufferedString.WriteString(currentTimeStamp)
kvString = append(kvString, "appid", AppID)
bufferedString.WriteString(AppID)
kvString = append(kvString, "appversion", AppVersion)
bufferedString.WriteString(AppVersion)
if s.tr != nil && len(s.tr.AccessToken) > 0 {
authorization := "Bearer " + s.tr.AccessToken
kvString = append(kvString, "authorization", authorization)
bufferedString.WriteString(authorization)
}
bufferedString.WriteString(AppSecret)
sign := GetMD5Hash(bufferedString.String())
kvString = append(kvString, "sign", sign)
return metadata.AppendToOutgoingContext(ctx, kvString...)
}
func (d *HalalCloud) GetCurrentOpDir(dir model.Obj, args []string, index int) string {
currentDir := dir.GetPath()
if len(currentDir) == 0 {
currentDir = "/"
}
opPath := currentDir + "/" + args[index]
if strings.HasPrefix(args[index], "/") {
opPath = args[index]
}
return opPath
}
func (d *HalalCloud) GetCurrentDir(dir model.Obj) string {
currentDir := dir.GetPath()
if len(currentDir) == 0 {
currentDir = "/"
}
return currentDir
}
type Common struct {
}
func getRawFiles(addr *pubUserFile.SliceDownloadInfo) ([]byte, error) {
if addr == nil {
return nil, errors.New("addr is nil")
}
client := http.Client{
Timeout: time.Duration(60 * time.Second), // Set timeout to 5 seconds
}
resp, err := client.Get(addr.DownloadAddress)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status: %s, body: %s", resp.Status, body)
}
if addr.Encrypt > 0 {
cd := uint8(addr.Encrypt)
for idx := 0; idx < len(body); idx++ {
body[idx] = body[idx] ^ cd
}
}
if addr.StoreType != 10 {
sourceCid, err := cid.Decode(addr.Identity)
if err != nil {
return nil, err
}
checkCid, err := sourceCid.Prefix().Sum(body)
if err != nil {
return nil, err
}
if !checkCid.Equals(sourceCid) {
return nil, fmt.Errorf("bad cid: %s, body: %s", checkCid.String(), body)
}
}
return body, nil
}
type openObject struct {
ctx context.Context
mu sync.Mutex
d []*pubUserFile.SliceDownloadInfo
id int
skip int64
chunk *[]byte
chunks *[]chunkSize
closed bool
sha string
shaTemp hash.Hash
}
// get the next chunk
func (oo *openObject) getChunk(ctx context.Context) (err error) {
if oo.id >= len(*oo.chunks) {
return io.EOF
}
var chunk []byte
err = utils.Retry(3, time.Second, func() (err error) {
chunk, err = getRawFiles(oo.d[oo.id])
return err
})
if err != nil {
return err
}
oo.id++
oo.chunk = &chunk
return nil
}
// Read reads up to len(p) bytes into p.
func (oo *openObject) Read(p []byte) (n int, err error) {
oo.mu.Lock()
defer oo.mu.Unlock()
if oo.closed {
return 0, fmt.Errorf("read on closed file")
}
// Skip data at the start if requested
for oo.skip > 0 {
//size := 1024 * 1024
_, size, err := oo.ChunkLocation(oo.id)
if err != nil {
return 0, err
}
if oo.skip < int64(size) {
break
}
oo.id++
oo.skip -= int64(size)
}
if len(*oo.chunk) == 0 {
err = oo.getChunk(oo.ctx)
if err != nil {
return 0, err
}
if oo.skip > 0 {
*oo.chunk = (*oo.chunk)[oo.skip:]
oo.skip = 0
}
}
n = copy(p, *oo.chunk)
*oo.chunk = (*oo.chunk)[n:]
oo.shaTemp.Write(*oo.chunk)
return n, nil
}
// Close closed the file - MAC errors are reported here
func (oo *openObject) Close() (err error) {
oo.mu.Lock()
defer oo.mu.Unlock()
if oo.closed {
return nil
}
// 校验Sha1
if string(oo.shaTemp.Sum(nil)) != oo.sha {
return fmt.Errorf("failed to finish download: %w", err)
}
oo.closed = true
return nil
}
func GetMD5Hash(text string) string {
tHash := md5.Sum([]byte(text))
return hex.EncodeToString(tHash[:])
}
// chunkSize describes a size and position of chunk
type chunkSize struct {
position int64
size int
}
func getChunkSizes(sliceSize []*pubUserFile.SliceSize) (chunks []chunkSize) {
chunks = make([]chunkSize, 0)
for _, s := range sliceSize {
// 对最后一个做特殊处理
if s.EndIndex == 0 {
s.EndIndex = s.StartIndex
}
for j := s.StartIndex; j <= s.EndIndex; j++ {
chunks = append(chunks, chunkSize{position: j, size: int(s.Size)})
}
}
return chunks
}
func (oo *openObject) ChunkLocation(id int) (position int64, size int, err error) {
if id < 0 || id >= len(*oo.chunks) {
return 0, 0, errors.New("invalid arguments")
}
return (*oo.chunks)[id].position, (*oo.chunks)[id].size, nil
}

View File

@ -66,18 +66,18 @@ func (d *ILanZou) Drop(ctx context.Context) error {
}
func (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
offset := 1
limit := 60
var res []ListItem
for {
var resp ListResp
_, err := d.proved("/record/file/list", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"type": "0",
"folderId": dir.GetID(),
"offset": strconv.Itoa(offset),
"limit": strconv.Itoa(limit),
}).SetResult(&resp)
params := []string{
"offset=1",
"limit=60",
"folderId=" + dir.GetID(),
"type=0",
}
queryString := strings.Join(params, "&")
req.SetQueryString(queryString).SetResult(&resp)
})
if err != nil {
return nil, err
@ -86,7 +86,6 @@ func (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs)
if resp.TotalPage <= resp.Offset {
break
}
offset++
}
return utils.SliceConvert(res, func(f ListItem) (model.Obj, error) {
updTime, err := time.ParseInLocation("2006-01-02 15:04:05", f.UpdTime, time.Local)
@ -118,36 +117,39 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
if err != nil {
return nil, err
}
query := u.Query()
query.Set("uuid", d.UUID)
query.Set("devType", "6")
query.Set("devCode", d.UUID)
query.Set("devModel", "chrome")
query.Set("devVersion", d.conf.devVersion)
query.Set("appVersion", "")
ts, err := getTimestamp(d.conf.secret)
if err != nil {
return nil, err
ts, ts_str, err := getTimestamp(d.conf.secret)
params := []string{
"uuid=" + url.QueryEscape(d.UUID),
"devType=6",
"devCode=" + url.QueryEscape(d.UUID),
"devModel=chrome",
"devVersion=" + url.QueryEscape(d.conf.devVersion),
"appVersion=",
"timestamp=" + ts_str,
"appToken=" + url.QueryEscape(d.Token),
"enable=0",
}
query.Set("timestamp", ts)
query.Set("appToken", d.Token)
query.Set("enable", "1")
downloadId, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%s", file.GetID(), d.userID)), d.conf.secret)
if err != nil {
return nil, err
}
query.Set("downloadId", hex.EncodeToString(downloadId))
auth, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%d", file.GetID(), time.Now().UnixMilli())), d.conf.secret)
params = append(params, "downloadId="+url.QueryEscape(hex.EncodeToString(downloadId)))
auth, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%d", file.GetID(), ts)), d.conf.secret)
if err != nil {
return nil, err
}
query.Set("auth", hex.EncodeToString(auth))
u.RawQuery = query.Encode()
params = append(params, "auth="+url.QueryEscape(hex.EncodeToString(auth)))
u.RawQuery = strings.Join(params, "&")
realURL := u.String()
// get the url after redirect
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
//"Origin": d.conf.site,
"Referer": d.conf.site + "/",
"Referer": d.conf.site + "/",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
}).Get(realURL)
if err != nil {
return nil, err
@ -155,7 +157,7 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
if res.StatusCode() == 302 {
realURL = res.Header().Get("location")
} else {
return nil, fmt.Errorf("redirect failed, status: %d", res.StatusCode())
return nil, fmt.Errorf("redirect failed, status: %d, msg: %s", res.StatusCode(), utils.Json.Get(res.Body(), "msg").ToString())
}
link := model.Link{URL: realURL}
return &link, nil
@ -173,7 +175,7 @@ func (d *ILanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
return nil, err
}
return &model.Object{
ID: utils.Json.Get(res, "list", "0", "id").ToString(),
ID: utils.Json.Get(res, "list", 0, "id").ToString(),
//Path: "",
Name: dirName,
Size: 0,
@ -271,7 +273,7 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
defer func() {
_ = tempFile.Close()
}()
if _, err = io.Copy(h, tempFile); err != nil {
if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
return nil, err
}
_, err = tempFile.Seek(0, io.SeekStart)
@ -342,10 +344,12 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
var resp UploadResultResp
for i := 0; i < 10; i++ {
_, err = d.unproved("/7n/results", http.MethodPost, func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"tokenList": token,
"tokenTime": time.Now().Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)"),
}).SetResult(&resp)
params := []string{
"tokenList=" + token,
"tokenTime=" + time.Now().Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)"),
}
queryString := strings.Join(params, "&")
req.SetQueryString(queryString).SetResult(&resp)
})
if err != nil {
return nil, err

View File

@ -46,7 +46,7 @@ func init() {
bucket: "wpanstore-lanzou",
unproved: "unproved",
proved: "proved",
devVersion: "122",
devVersion: "125",
site: "https://www.ilanzou.com",
},
}
@ -72,7 +72,7 @@ func init() {
bucket: "wpanstore",
unproved: "ws",
proved: "app",
devVersion: "121",
devVersion: "125",
site: "https://www.feijipan.com",
},
}

View File

@ -4,7 +4,9 @@ import (
"encoding/hex"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
@ -31,44 +33,52 @@ func (d *ILanZou) login() error {
return nil
}
func getTimestamp(secret []byte) (string, error) {
func getTimestamp(secret []byte) (int64, string, error) {
ts := time.Now().UnixMilli()
tsStr := strconv.FormatInt(ts, 10)
res, err := mopan.AesEncrypt([]byte(tsStr), secret)
if err != nil {
return "", err
return 0, "", err
}
return hex.EncodeToString(res), nil
return ts, hex.EncodeToString(res), nil
}
func (d *ILanZou) request(pathname, method string, callback base.ReqCallback, proved bool, retry ...bool) ([]byte, error) {
req := base.RestyClient.R()
ts, err := getTimestamp(d.conf.secret)
_, ts_str, err := getTimestamp(d.conf.secret)
if err != nil {
return nil, err
}
req.SetQueryParams(map[string]string{
"uuid": d.UUID,
"devType": "6",
"devCode": d.UUID,
"devModel": "chrome",
"devVersion": d.conf.devVersion,
"appVersion": "",
"timestamp": ts,
//"appToken": d.Token,
"extra": "2",
})
req.SetHeaders(map[string]string{
"Origin": d.conf.site,
"Referer": d.conf.site + "/",
})
if proved {
req.SetQueryParam("appToken", d.Token)
params := []string{
"uuid=" + url.QueryEscape(d.UUID),
"devType=6",
"devCode=" + url.QueryEscape(d.UUID),
"devModel=chrome",
"devVersion=" + url.QueryEscape(d.conf.devVersion),
"appVersion=",
"timestamp=" + ts_str,
}
if proved {
params = append(params, "appToken="+url.QueryEscape(d.Token))
}
params = append(params, "extra=2")
queryString := strings.Join(params, "&")
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"Origin": d.conf.site,
"Referer": d.conf.site + "/",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
})
if callback != nil {
callback(req)
}
res, err := req.Execute(method, d.conf.base+pathname)
res, err := req.Execute(method, d.conf.base+pathname+"?"+queryString)
if err != nil {
if res != nil {
log.Errorf("[iLanZou] request error: %s", res.String())

8
drivers/lark.go Normal file
View File

@ -0,0 +1,8 @@
// +build linux darwin windows
// +build amd64 arm64
package drivers
import (
_ "github.com/alist-org/alist/v3/drivers/lark"
)

397
drivers/lark/driver.go Normal file
View File

@ -0,0 +1,397 @@
package lark
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
lark "github.com/larksuite/oapi-sdk-go/v3"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1"
"golang.org/x/time/rate"
)
type Lark struct {
model.Storage
Addition
client *lark.Client
rootFolderToken string
}
func (c *Lark) Config() driver.Config {
return config
}
func (c *Lark) GetAddition() driver.Additional {
return &c.Addition
}
func (c *Lark) Init(ctx context.Context) error {
c.client = lark.NewClient(c.AppId, c.AppSecret, lark.WithTokenCache(newTokenCache()))
paths := strings.Split(c.RootFolderPath, "/")
token := ""
var ok bool
var file *larkdrive.File
for _, p := range paths {
if p == "" {
token = ""
continue
}
resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())
if err != nil {
return err
}
for {
ok, file, err = resp.Next()
if !ok {
return errs.ObjectNotFound
}
if err != nil {
return err
}
if *file.Type == "folder" && *file.Name == p {
token = *file.Token
break
}
}
}
c.rootFolderToken = token
return nil
}
func (c *Lark) Drop(ctx context.Context) error {
return nil
}
func (c *Lark) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
token, ok := c.getObjToken(ctx, dir.GetPath())
if !ok {
return nil, errs.ObjectNotFound
}
if token == emptyFolderToken {
return nil, nil
}
resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())
if err != nil {
return nil, err
}
ok = false
var file *larkdrive.File
var res []model.Obj
for {
ok, file, err = resp.Next()
if !ok {
break
}
if err != nil {
return nil, err
}
modifiedUnix, _ := strconv.ParseInt(*file.ModifiedTime, 10, 64)
createdUnix, _ := strconv.ParseInt(*file.CreatedTime, 10, 64)
f := model.Object{
ID: *file.Token,
Path: strings.Join([]string{c.RootFolderPath, dir.GetPath(), *file.Name}, "/"),
Name: *file.Name,
Size: 0,
Modified: time.Unix(modifiedUnix, 0),
Ctime: time.Unix(createdUnix, 0),
IsFolder: *file.Type == "folder",
}
res = append(res, &f)
}
return res, nil
}
func (c *Lark) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
token, ok := c.getObjToken(ctx, file.GetPath())
if !ok {
return nil, errs.ObjectNotFound
}
resp, err := c.client.GetTenantAccessTokenBySelfBuiltApp(ctx, &larkcore.SelfBuiltTenantAccessTokenReq{
AppID: c.AppId,
AppSecret: c.AppSecret,
})
if err != nil {
return nil, err
}
if !c.ExternalMode {
accessToken := resp.TenantAccessToken
url := fmt.Sprintf("https://open.feishu.cn/open-apis/drive/v1/files/%s/download", token)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
req.Header.Set("Range", "bytes=0-1")
ar, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if ar.StatusCode != http.StatusPartialContent {
return nil, errors.New("failed to get download link")
}
return &model.Link{
URL: url,
Header: http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", accessToken)},
},
}, nil
} else {
url := strings.Join([]string{c.TenantUrlPrefix, "file", token}, "/")
return &model.Link{
URL: url,
}, nil
}
}
func (c *Lark) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
token, ok := c.getObjToken(ctx, parentDir.GetPath())
if !ok {
return nil, errs.ObjectNotFound
}
body, err := larkdrive.NewCreateFolderFilePathReqBodyBuilder().FolderToken(token).Name(dirName).Build()
if err != nil {
return nil, err
}
resp, err := c.client.Drive.File.CreateFolder(ctx,
larkdrive.NewCreateFolderFileReqBuilder().Body(body).Build())
if err != nil {
return nil, err
}
if !resp.Success() {
return nil, errors.New(resp.Error())
}
return &model.Object{
ID: *resp.Data.Token,
Path: strings.Join([]string{c.RootFolderPath, parentDir.GetPath(), dirName}, "/"),
Name: dirName,
Size: 0,
IsFolder: true,
}, nil
}
func (c *Lark) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
srcToken, ok := c.getObjToken(ctx, srcObj.GetPath())
if !ok {
return nil, errs.ObjectNotFound
}
dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())
if !ok {
return nil, errs.ObjectNotFound
}
req := larkdrive.NewMoveFileReqBuilder().
Body(larkdrive.NewMoveFileReqBodyBuilder().
Type("file").
FolderToken(dstDirToken).
Build()).FileToken(srcToken).
Build()
// 发起请求
resp, err := c.client.Drive.File.Move(ctx, req)
if err != nil {
return nil, err
}
if !resp.Success() {
return nil, errors.New(resp.Error())
}
return nil, nil
}
func (c *Lark) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
// TODO rename obj, optional
return nil, errs.NotImplement
}
func (c *Lark) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
srcToken, ok := c.getObjToken(ctx, srcObj.GetPath())
if !ok {
return nil, errs.ObjectNotFound
}
dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())
if !ok {
return nil, errs.ObjectNotFound
}
req := larkdrive.NewCopyFileReqBuilder().
Body(larkdrive.NewCopyFileReqBodyBuilder().
Name(srcObj.GetName()).
Type("file").
FolderToken(dstDirToken).
Build()).FileToken(srcToken).
Build()
// 发起请求
resp, err := c.client.Drive.File.Copy(ctx, req)
if err != nil {
return nil, err
}
if !resp.Success() {
return nil, errors.New(resp.Error())
}
return nil, nil
}
func (c *Lark) Remove(ctx context.Context, obj model.Obj) error {
token, ok := c.getObjToken(ctx, obj.GetPath())
if !ok {
return errs.ObjectNotFound
}
req := larkdrive.NewDeleteFileReqBuilder().
FileToken(token).
Type("file").
Build()
// 发起请求
resp, err := c.client.Drive.File.Delete(ctx, req)
if err != nil {
return err
}
if !resp.Success() {
return errors.New(resp.Error())
}
return nil
}
var uploadLimit = rate.NewLimiter(rate.Every(time.Second), 5)
func (c *Lark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
token, ok := c.getObjToken(ctx, dstDir.GetPath())
if !ok {
return nil, errs.ObjectNotFound
}
// prepare
req := larkdrive.NewUploadPrepareFileReqBuilder().
FileUploadInfo(larkdrive.NewFileUploadInfoBuilder().
FileName(stream.GetName()).
ParentType(`explorer`).
ParentNode(token).
Size(int(stream.GetSize())).
Build()).
Build()
// 发起请求
uploadLimit.Wait(ctx)
resp, err := c.client.Drive.File.UploadPrepare(ctx, req)
if err != nil {
return nil, err
}
if !resp.Success() {
return nil, errors.New(resp.Error())
}
uploadId := *resp.Data.UploadId
blockSize := *resp.Data.BlockSize
blockCount := *resp.Data.BlockNum
// upload
for i := 0; i < blockCount; i++ {
length := int64(blockSize)
if i == blockCount-1 {
length = stream.GetSize() - int64(i*blockSize)
}
reader := io.LimitReader(stream, length)
req := larkdrive.NewUploadPartFileReqBuilder().
Body(larkdrive.NewUploadPartFileReqBodyBuilder().
UploadId(uploadId).
Seq(i).
Size(int(length)).
File(reader).
Build()).
Build()
// 发起请求
uploadLimit.Wait(ctx)
resp, err := c.client.Drive.File.UploadPart(ctx, req)
if err != nil {
return nil, err
}
if !resp.Success() {
return nil, errors.New(resp.Error())
}
up(float64(i) / float64(blockCount))
}
//close
closeReq := larkdrive.NewUploadFinishFileReqBuilder().
Body(larkdrive.NewUploadFinishFileReqBodyBuilder().
UploadId(uploadId).
BlockNum(blockCount).
Build()).
Build()
// 发起请求
closeResp, err := c.client.Drive.File.UploadFinish(ctx, closeReq)
if err != nil {
return nil, err
}
if !closeResp.Success() {
return nil, errors.New(closeResp.Error())
}
return &model.Object{
ID: *closeResp.Data.FileToken,
}, nil
}
//func (d *Lark) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*Lark)(nil)

36
drivers/lark/meta.go Normal file
View File

@ -0,0 +1,36 @@
package lark
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
// Usually one of two
driver.RootPath
// define other
AppId string `json:"app_id" type:"text" help:"app id"`
AppSecret string `json:"app_secret" type:"text" help:"app secret"`
ExternalMode bool `json:"external_mode" type:"bool" help:"external mode"`
TenantUrlPrefix string `json:"tenant_url_prefix" type:"text" help:"tenant url prefix"`
}
var config = driver.Config{
Name: "Lark",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "/",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Lark{}
})
}

32
drivers/lark/types.go Normal file
View File

@ -0,0 +1,32 @@
package lark
import (
"context"
"github.com/Xhofe/go-cache"
"time"
)
type TokenCache struct {
cache.ICache[string]
}
func (t *TokenCache) Set(_ context.Context, key string, value string, expireTime time.Duration) error {
t.ICache.Set(key, value, cache.WithEx[string](expireTime))
return nil
}
func (t *TokenCache) Get(_ context.Context, key string) (string, error) {
v, ok := t.ICache.Get(key)
if ok {
return v, nil
}
return "", nil
}
func newTokenCache() *TokenCache {
c := cache.NewMemCache[string]()
return &TokenCache{c}
}

66
drivers/lark/util.go Normal file
View File

@ -0,0 +1,66 @@
package lark
import (
"context"
"github.com/Xhofe/go-cache"
larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1"
log "github.com/sirupsen/logrus"
"path"
"time"
)
const objTokenCacheDuration = 5 * time.Minute
const emptyFolderToken = "empty"
var objTokenCache = cache.NewMemCache[string]()
var exOpts = cache.WithEx[string](objTokenCacheDuration)
func (c *Lark) getObjToken(ctx context.Context, folderPath string) (string, bool) {
if token, ok := objTokenCache.Get(folderPath); ok {
return token, true
}
dir, name := path.Split(folderPath)
// strip the last slash of dir if it exists
if len(dir) > 0 && dir[len(dir)-1] == '/' {
dir = dir[:len(dir)-1]
}
if name == "" {
return c.rootFolderToken, true
}
var parentToken string
var found bool
parentToken, found = c.getObjToken(ctx, dir)
if !found {
return emptyFolderToken, false
}
req := larkdrive.NewListFileReqBuilder().FolderToken(parentToken).Build()
resp, err := c.client.Drive.File.ListByIterator(ctx, req)
if err != nil {
log.WithError(err).Error("failed to list files")
return emptyFolderToken, false
}
var file *larkdrive.File
for {
found, file, err = resp.Next()
if !found {
break
}
if err != nil {
log.WithError(err).Error("failed to get next file")
break
}
if *file.Name == name {
objTokenCache.Set(folderPath, *file.Token, exOpts)
return *file.Token, true
}
}
return emptyFolderToken, false
}

View File

@ -21,7 +21,7 @@ import (
"github.com/alist-org/alist/v3/internal/sign"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/djherbis/times"
"github.com/alist-org/times"
log "github.com/sirupsen/logrus"
_ "golang.org/x/image/webp"
)

View File

@ -206,7 +206,7 @@ func (d *MediaTrack) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
return err
}
h := md5.New()
_, err = io.Copy(h, tempFile)
_, err = utils.CopyWithBuffer(h, tempFile)
if err != nil {
return err
}

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/pquerna/otp/totp"
"github.com/rclone/rclone/lib/readers"
"github.com/alist-org/alist/v3/internal/driver"
@ -33,8 +34,16 @@ func (d *Mega) GetAddition() driver.Additional {
}
func (d *Mega) Init(ctx context.Context) error {
var twoFACode = d.TwoFACode
d.c = mega.New()
return d.c.Login(d.Email, d.Password)
if d.TwoFASecret != "" {
code, err := totp.GenerateCode(d.TwoFASecret, time.Now())
if err != nil {
return fmt.Errorf("generate totp code failed: %w", err)
}
twoFACode = code
}
return d.c.MultiFactorLogin(d.Email, d.Password, twoFACode)
}
func (d *Mega) Drop(ctx context.Context) error {

View File

@ -9,8 +9,10 @@ type Addition struct {
// Usually one of two
//driver.RootPath
//driver.RootID
Email string `json:"email" required:"true"`
Password string `json:"password" required:"true"`
Email string `json:"email" required:"true"`
Password string `json:"password" required:"true"`
TwoFACode string `json:"two_fa_code" required:"false" help:"2FA 6-digit code, filling in the 2FA code alone will not support reloading driver"`
TwoFASecret string `json:"two_fa_secret" required:"false" help:"2FA secret"`
}
var config = driver.Config{

View File

@ -0,0 +1,135 @@
package netease_music
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"math/big"
"strings"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/pkg/utils/random"
)
var (
linuxapiKey = []byte("rFgB&h#%2?^eDg:Q")
eapiKey = []byte("e82ckenh8dichen8")
iv = []byte("0102030405060708")
presetKey = []byte("0CoJUm6Qyw8W8jud")
publicKey = []byte("-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----")
stdChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
)
func aesKeyPending(key []byte) []byte {
k := len(key)
count := 0
switch true {
case k <= 16:
count = 16 - k
case k <= 24:
count = 24 - k
case k <= 32:
count = 32 - k
default:
return key[:32]
}
if count == 0 {
return key
}
return append(key, bytes.Repeat([]byte{0}, count)...)
}
func pkcs7Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}
func aesCBCEncrypt(src, key, iv []byte) []byte {
block, _ := aes.NewCipher(aesKeyPending(key))
src = pkcs7Padding(src, block.BlockSize())
dst := make([]byte, len(src))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(dst, src)
return dst
}
func aesECBEncrypt(src, key []byte) []byte {
block, _ := aes.NewCipher(aesKeyPending(key))
src = pkcs7Padding(src, block.BlockSize())
dst := make([]byte, len(src))
ecbCryptBlocks(block, dst, src)
return dst
}
func ecbCryptBlocks(block cipher.Block, dst, src []byte) {
bs := block.BlockSize()
for len(src) > 0 {
block.Encrypt(dst, src[:bs])
src = src[bs:]
dst = dst[bs:]
}
}
func rsaEncrypt(buffer, key []byte) []byte {
buffers := make([]byte, 128-16, 128)
buffers = append(buffers, buffer...)
block, _ := pem.Decode(key)
pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
pub := pubInterface.(*rsa.PublicKey)
c := new(big.Int).SetBytes([]byte(buffers))
return c.Exp(c, big.NewInt(int64(pub.E)), pub.N).Bytes()
}
func getSecretKey() ([]byte, []byte) {
key := make([]byte, 16)
reversed := make([]byte, 16)
for i := 0; i < 16; i++ {
result := stdChars[random.RangeInt64(0, 62)]
key[i] = result
reversed[15-i] = result
}
return key, reversed
}
func weapi(data map[string]string) map[string]string {
text, _ := utils.Json.Marshal(data)
secretKey, reversedKey := getSecretKey()
params := []byte(base64.StdEncoding.EncodeToString(aesCBCEncrypt(text, presetKey, iv)))
return map[string]string{
"params": base64.StdEncoding.EncodeToString(aesCBCEncrypt(params, reversedKey, iv)),
"encSecKey": hex.EncodeToString(rsaEncrypt(secretKey, publicKey)),
}
}
func eapi(url string, data map[string]interface{}) map[string]string {
text, _ := utils.Json.Marshal(data)
msg := "nobody" + url + "use" + string(text) + "md5forencrypt"
h := md5.New()
h.Write([]byte(msg))
digest := hex.EncodeToString(h.Sum(nil))
params := []byte(url + "-36cd479b6b5-" + string(text) + "-36cd479b6b5-" + digest)
return map[string]string{
"params": hex.EncodeToString(aesECBEncrypt(params, eapiKey)),
}
}
func linuxapi(data map[string]interface{}) map[string]string {
text, _ := utils.Json.Marshal(data)
return map[string]string{
"eparams": strings.ToUpper(hex.EncodeToString(aesECBEncrypt(text, linuxapiKey))),
}
}

View File

@ -0,0 +1,110 @@
package netease_music
import (
"context"
"strings"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
_ "golang.org/x/image/webp"
)
type NeteaseMusic struct {
model.Storage
Addition
csrfToken string
musicU string
fileMapByName map[string]model.Obj
}
func (d *NeteaseMusic) Config() driver.Config {
return config
}
func (d *NeteaseMusic) GetAddition() driver.Additional {
return &d.Addition
}
func (d *NeteaseMusic) Init(ctx context.Context) error {
d.csrfToken = d.Addition.getCookie("__csrf")
d.musicU = d.Addition.getCookie("MUSIC_U")
if d.csrfToken == "" || d.musicU == "" {
return errs.EmptyToken
}
return nil
}
func (d *NeteaseMusic) Drop(ctx context.Context) error {
return nil
}
func (d *NeteaseMusic) Get(ctx context.Context, path string) (model.Obj, error) {
if path == "/" {
return &model.Object{
IsFolder: true,
Path: path,
}, nil
}
fragments := strings.Split(path, "/")
if len(fragments) > 1 {
fileName := fragments[1]
if strings.HasSuffix(fileName, ".lrc") {
lrc := d.fileMapByName[fileName]
return d.getLyricObj(lrc)
}
if song, ok := d.fileMapByName[fileName]; ok {
return song, nil
} else {
return nil, errs.ObjectNotFound
}
}
return nil, errs.ObjectNotFound
}
func (d *NeteaseMusic) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return d.getSongObjs(args)
}
func (d *NeteaseMusic) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if lrc, ok := file.(*LyricObj); ok {
if args.Type == "parsed" {
return lrc.getLyricLink(), nil
} else {
return lrc.getProxyLink(args), nil
}
}
return d.getSongLink(file)
}
func (d *NeteaseMusic) Remove(ctx context.Context, obj model.Obj) error {
return d.removeSongObj(obj)
}
func (d *NeteaseMusic) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
return d.putSongStream(stream)
}
func (d *NeteaseMusic) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *NeteaseMusic) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *NeteaseMusic) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotSupport
}
func (d *NeteaseMusic) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return errs.NotSupport
}
var _ driver.Driver = (*NeteaseMusic)(nil)

View File

@ -0,0 +1,32 @@
package netease_music
import (
"regexp"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
Cookie string `json:"cookie" type:"text" required:"true" help:""`
SongLimit uint64 `json:"song_limit" default:"200" type:"number" help:"only get 200 songs by default"`
}
func (ad *Addition) getCookie(name string) string {
re := regexp.MustCompile(name + "=([^(;|$)]+)")
matches := re.FindStringSubmatch(ad.Cookie)
if len(matches) < 2 {
return ""
}
return matches[1]
}
var config = driver.Config{
Name: "NeteaseMusic",
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &NeteaseMusic{}
})
}

View File

@ -0,0 +1,116 @@
package netease_music
import (
"context"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/sign"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/pkg/utils/random"
"github.com/alist-org/alist/v3/server/common"
)
type HostsResp struct {
Upload []string `json:"upload"`
}
type SongResp struct {
Data []struct {
Url string `json:"url"`
} `json:"data"`
}
type ListResp struct {
Size string `json:"size"`
MaxSize string `json:"maxSize"`
Data []struct {
AddTime int64 `json:"addTime"`
FileName string `json:"fileName"`
FileSize int64 `json:"fileSize"`
SongId int64 `json:"songId"`
SimpleSong struct {
Al struct {
PicUrl string `json:"picUrl"`
} `json:"al"`
} `json:"simpleSong"`
} `json:"data"`
}
type LyricObj struct {
model.Object
lyric string
}
func (lrc *LyricObj) getProxyLink(args model.LinkArgs) *model.Link {
rawURL := common.GetApiUrl(args.HttpReq) + "/p" + lrc.Path
rawURL = utils.EncodePath(rawURL, true) + "?type=parsed&sign=" + sign.Sign(lrc.Path)
return &model.Link{URL: rawURL}
}
func (lrc *LyricObj) getLyricLink() *model.Link {
reader := strings.NewReader(lrc.lyric)
return &model.Link{
RangeReadCloser: &model.RangeReadCloser{
RangeReader: func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
if httpRange.Length < 0 {
return io.NopCloser(reader), nil
}
sr := io.NewSectionReader(reader, httpRange.Start, httpRange.Length)
return io.NopCloser(sr), nil
},
Closers: utils.EmptyClosers(),
},
}
}
type ReqOption struct {
crypto string
stream model.FileStreamer
data map[string]string
headers map[string]string
cookies []*http.Cookie
url string
}
type Characteristic map[string]string
func (ch *Characteristic) fromDriver(d *NeteaseMusic) *Characteristic {
*ch = map[string]string{
"osver": "",
"deviceId": "",
"mobilename": "",
"appver": "6.1.1",
"versioncode": "140",
"buildver": strconv.FormatInt(time.Now().Unix(), 10),
"resolution": "1920x1080",
"os": "android",
"channel": "",
"requestId": strconv.FormatInt(time.Now().Unix()*1000, 10) + strconv.Itoa(int(random.RangeInt64(0, 1000))),
"MUSIC_U": d.musicU,
}
return ch
}
func (ch Characteristic) toCookies() []*http.Cookie {
cookies := make([]*http.Cookie, 0)
for k, v := range ch {
cookies = append(cookies, &http.Cookie{Name: k, Value: v})
}
return cookies
}
func (ch *Characteristic) merge(data map[string]string) map[string]interface{} {
body := map[string]interface{}{
"header": ch,
}
for k, v := range data {
body[k] = v
}
return body
}

View File

@ -0,0 +1,208 @@
package netease_music
import (
"crypto/md5"
"encoding/hex"
"io"
"net/http"
"strconv"
"strings"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/dhowden/tag"
)
type token struct {
resourceId string
objectKey string
token string
}
type songmeta struct {
needUpload bool
songId string
name string
artist string
album string
}
type uploader struct {
driver *NeteaseMusic
file model.File
meta songmeta
md5 string
ext string
size string
filename string
}
func (u *uploader) init(stream model.FileStreamer) error {
u.filename = stream.GetName()
u.size = strconv.FormatInt(stream.GetSize(), 10)
u.ext = "mp3"
if strings.HasSuffix(stream.GetMimetype(), "flac") {
u.ext = "flac"
}
h := md5.New()
io.Copy(h, stream)
u.md5 = hex.EncodeToString(h.Sum(nil))
_, err := u.file.Seek(0, io.SeekStart)
if err != nil {
return err
}
if m, err := tag.ReadFrom(u.file); err != nil {
u.meta = songmeta{}
} else {
u.meta = songmeta{
name: m.Title(),
artist: m.Artist(),
album: m.Album(),
}
}
if u.meta.name == "" {
u.meta.name = u.filename
}
if u.meta.album == "" {
u.meta.album = "未知专辑"
}
if u.meta.artist == "" {
u.meta.artist = "未知艺术家"
}
_, err = u.file.Seek(0, io.SeekStart)
if err != nil {
return err
}
return nil
}
func (u *uploader) checkIfExisted() error {
body, err := u.driver.request("https://interface.music.163.com/api/cloud/upload/check", http.MethodPost,
ReqOption{
crypto: "weapi",
data: map[string]string{
"ext": "",
"songId": "0",
"version": "1",
"bitrate": "999000",
"length": u.size,
"md5": u.md5,
},
cookies: []*http.Cookie{
{Name: "os", Value: "pc"},
{Name: "appver", Value: "2.9.7"},
},
},
)
if err != nil {
return err
}
u.meta.songId = utils.Json.Get(body, "songId").ToString()
u.meta.needUpload = utils.Json.Get(body, "needUpload").ToBool()
return nil
}
func (u *uploader) allocToken(bucket ...string) (token, error) {
if len(bucket) == 0 {
bucket = []string{""}
}
body, err := u.driver.request("https://music.163.com/weapi/nos/token/alloc", http.MethodPost, ReqOption{
crypto: "weapi",
data: map[string]string{
"bucket": bucket[0],
"local": "false",
"type": "audio",
"nos_product": "3",
"filename": u.filename,
"md5": u.md5,
"ext": u.ext,
},
})
if err != nil {
return token{}, err
}
return token{
resourceId: utils.Json.Get(body, "result", "resourceId").ToString(),
objectKey: utils.Json.Get(body, "result", "objectKey").ToString(),
token: utils.Json.Get(body, "result", "token").ToString(),
}, nil
}
func (u *uploader) publishInfo(resourceId string) error {
body, err := u.driver.request("https://music.163.com/api/upload/cloud/info/v2", http.MethodPost, ReqOption{
crypto: "weapi",
data: map[string]string{
"md5": u.md5,
"filename": u.filename,
"song": u.meta.name,
"album": u.meta.album,
"artist": u.meta.artist,
"songid": u.meta.songId,
"resourceId": resourceId,
"bitrate": "999000",
},
})
if err != nil {
return err
}
_, err = u.driver.request("https://interface.music.163.com/api/cloud/pub/v2", http.MethodPost, ReqOption{
crypto: "weapi",
data: map[string]string{
"songid": utils.Json.Get(body, "songId").ToString(),
},
})
if err != nil {
return err
}
return nil
}
func (u *uploader) upload(stream model.FileStreamer) error {
bucket := "jd-musicrep-privatecloud-audio-public"
token, err := u.allocToken(bucket)
if err != nil {
return err
}
body, err := u.driver.request("https://wanproxy.127.net/lbs?version=1.0&bucketname="+bucket, http.MethodGet,
ReqOption{},
)
if err != nil {
return err
}
var resp HostsResp
err = utils.Json.Unmarshal(body, &resp)
if err != nil {
return err
}
objectKey := strings.ReplaceAll(token.objectKey, "/", "%2F")
_, err = u.driver.request(
resp.Upload[0]+"/"+bucket+"/"+objectKey+"?offset=0&complete=true&version=1.0",
http.MethodPost,
ReqOption{
stream: stream,
headers: map[string]string{
"x-nos-token": token.token,
"Content-Type": "audio/mpeg",
"Content-Length": u.size,
"Content-MD5": u.md5,
},
},
)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,246 @@
package netease_music
import (
"io"
"net/http"
"path"
"regexp"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
)
func (d *NeteaseMusic) request(url, method string, opt ReqOption) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeader("Cookie", d.Addition.Cookie)
if strings.Contains(url, "music.163.com") {
req.SetHeader("Referer", "https://music.163.com")
}
if opt.cookies != nil {
for _, cookie := range opt.cookies {
req.SetCookie(cookie)
}
}
if opt.headers != nil {
for header, value := range opt.headers {
req.SetHeader(header, value)
}
}
data := opt.data
if opt.crypto == "weapi" {
data = weapi(data)
re, _ := regexp.Compile(`/\w*api/`)
url = re.ReplaceAllString(url, "/weapi/")
} else if opt.crypto == "eapi" {
ch := new(Characteristic).fromDriver(d)
req.SetCookies(ch.toCookies())
data = eapi(opt.url, ch.merge(data))
re, _ := regexp.Compile(`/\w*api/`)
url = re.ReplaceAllString(url, "/eapi/")
} else if opt.crypto == "linuxapi" {
re, _ := regexp.Compile(`/\w*api/`)
data = linuxapi(map[string]interface{}{
"url": re.ReplaceAllString(url, "/api/"),
"method": method,
"params": data,
})
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36")
url = "https://music.163.com/api/linux/forward"
}
if method == http.MethodPost {
if opt.stream != nil {
req.SetContentLength(true)
req.SetBody(io.ReadCloser(opt.stream))
} else {
req.SetFormData(data)
}
res, err := req.Post(url)
return res.Body(), err
}
if method == http.MethodGet {
res, err := req.Get(url)
return res.Body(), err
}
return nil, errs.NotImplement
}
func (d *NeteaseMusic) getSongObjs(args model.ListArgs) ([]model.Obj, error) {
body, err := d.request("https://music.163.com/weapi/v1/cloud/get", http.MethodPost, ReqOption{
crypto: "weapi",
data: map[string]string{
"limit": strconv.FormatUint(d.Addition.SongLimit, 10),
"offset": "0",
},
cookies: []*http.Cookie{
{Name: "os", Value: "pc"},
},
})
if err != nil {
return nil, err
}
var resp ListResp
err = utils.Json.Unmarshal(body, &resp)
if err != nil {
return nil, err
}
d.fileMapByName = make(map[string]model.Obj)
files := make([]model.Obj, 0, len(resp.Data))
for _, f := range resp.Data {
song := &model.ObjThumb{
Object: model.Object{
IsFolder: false,
Size: f.FileSize,
Name: f.FileName,
Modified: time.UnixMilli(f.AddTime),
ID: strconv.FormatInt(f.SongId, 10),
},
Thumbnail: model.Thumbnail{Thumbnail: f.SimpleSong.Al.PicUrl},
}
d.fileMapByName[song.Name] = song
files = append(files, song)
// map song id for lyric
lrcName := strings.Split(f.FileName, ".")[0] + ".lrc"
lrc := &model.Object{
IsFolder: false,
Name: lrcName,
Path: path.Join(args.ReqPath, lrcName),
ID: strconv.FormatInt(f.SongId, 10),
}
d.fileMapByName[lrc.Name] = lrc
}
return files, nil
}
func (d *NeteaseMusic) getSongLink(file model.Obj) (*model.Link, error) {
body, err := d.request(
"https://music.163.com/api/song/enhance/player/url", http.MethodPost, ReqOption{
crypto: "linuxapi",
data: map[string]string{
"ids": "[" + file.GetID() + "]",
"br": "999000",
},
cookies: []*http.Cookie{
{Name: "os", Value: "pc"},
},
},
)
if err != nil {
return nil, err
}
var resp SongResp
err = utils.Json.Unmarshal(body, &resp)
if err != nil {
return nil, err
}
if len(resp.Data) < 1 {
return nil, errs.ObjectNotFound
}
return &model.Link{URL: resp.Data[0].Url}, nil
}
func (d *NeteaseMusic) getLyricObj(file model.Obj) (model.Obj, error) {
if lrc, ok := file.(*LyricObj); ok {
return lrc, nil
}
body, err := d.request(
"https://music.163.com/api/song/lyric?_nmclfl=1", http.MethodPost, ReqOption{
data: map[string]string{
"id": file.GetID(),
"tv": "-1",
"lv": "-1",
"rv": "-1",
"kv": "-1",
},
cookies: []*http.Cookie{
{Name: "os", Value: "ios"},
},
},
)
if err != nil {
return nil, err
}
lyric := utils.Json.Get(body, "lrc", "lyric").ToString()
return &LyricObj{
lyric: lyric,
Object: model.Object{
IsFolder: false,
ID: file.GetID(),
Name: file.GetName(),
Path: file.GetPath(),
Size: int64(len(lyric)),
},
}, nil
}
func (d *NeteaseMusic) removeSongObj(file model.Obj) error {
_, err := d.request("http://music.163.com/weapi/cloud/del", http.MethodPost, ReqOption{
crypto: "weapi",
data: map[string]string{
"songIds": "[" + file.GetID() + "]",
},
})
return err
}
func (d *NeteaseMusic) putSongStream(stream model.FileStreamer) error {
tmp, err := stream.CacheFullInTempFile()
if err != nil {
return err
}
defer tmp.Close()
u := uploader{driver: d, file: tmp}
err = u.init(stream)
if err != nil {
return err
}
err = u.checkIfExisted()
if err != nil {
return err
}
token, err := u.allocToken()
if err != nil {
return err
}
if u.meta.needUpload {
err = u.upload(stream)
if err != nil {
return err
}
}
err = u.publishInfo(token.resourceId)
if err != nil {
return err
}
return nil
}

View File

@ -118,6 +118,7 @@ func (d *Onedrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName str
"folder": base.Json{},
"@microsoft.graph.conflictBehavior": "rename",
}
// todo 修复文件夹 ctime/mtime, onedrive 可在 data 里设置 fileSystemInfo 字段, 但是此接口未提供 ctime/mtime
_, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)

View File

@ -24,12 +24,12 @@ type RespErr struct {
}
type File struct {
Id string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
LastModifiedDateTime time.Time `json:"lastModifiedDateTime"`
Url string `json:"@microsoft.graph.downloadUrl"`
File *struct {
Id string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
FileSystemInfo *FileSystemInfoFacet `json:"fileSystemInfo"`
Url string `json:"@microsoft.graph.downloadUrl"`
File *struct {
MimeType string `json:"mimeType"`
} `json:"file"`
Thumbnails []struct {
@ -58,7 +58,7 @@ func fileToObj(f File, parentID string) *Object {
ID: f.Id,
Name: f.Name,
Size: f.Size,
Modified: f.LastModifiedDateTime,
Modified: f.FileSystemInfo.LastModifiedDateTime,
IsFolder: f.File == nil,
},
Thumbnail: model.Thumbnail{Thumbnail: thumb},
@ -72,3 +72,20 @@ type Files struct {
Value []File `json:"value"`
NextLink string `json:"@odata.nextLink"`
}
// Metadata represents a request to update Metadata.
// It includes only the writeable properties.
// omitempty is intentionally included for all, per https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update?view=odsp-graph-online#request-body
type Metadata struct {
Description string `json:"description,omitempty"` // Provides a user-visible description of the item. Read-write. Only on OneDrive Personal. Undocumented limit of 1024 characters.
FileSystemInfo *FileSystemInfoFacet `json:"fileSystemInfo,omitempty"` // File system information on client. Read-write.
}
// FileSystemInfoFacet contains properties that are reported by the
// device's local file system for the local version of an item. This
// facet can be used to specify the last modified date or created date
// of the item as it was on the local device.
type FileSystemInfoFacet struct {
CreatedDateTime time.Time `json:"createdDateTime,omitempty"` // The UTC date and time the file was created on a client.
LastModifiedDateTime time.Time `json:"lastModifiedDateTime,omitempty"` // The UTC date and time the file was last modified on a client.
}

View File

@ -127,7 +127,7 @@ func (d *Onedrive) Request(url string, method string, callback base.ReqCallback,
func (d *Onedrive) getFiles(path string) ([]File, error) {
var res []File
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,lastModifiedDateTime,content.downloadUrl,file,parentReference"
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,fileSystemInfo,content.downloadUrl,file,parentReference"
for nextLink != "" {
var files Files
_, err := d.Request(nextLink, http.MethodGet, nil, &files)
@ -148,7 +148,10 @@ func (d *Onedrive) GetFile(path string) (*File, error) {
}
func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) error {
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/content"
filepath := stdpath.Join(dstDir.GetPath(), stream.GetName())
// 1. upload new file
// ApiDoc: https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online
url := d.GetMetaUrl(false, filepath) + "/content"
data, err := io.ReadAll(stream)
if err != nil {
return err
@ -156,12 +159,50 @@ func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.F
_, err = d.Request(url, http.MethodPut, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, nil)
if err != nil {
return fmt.Errorf("onedrive: Failed to upload new file(path=%v): %w", filepath, err)
}
// 2. update metadata
err = d.updateMetadata(ctx, stream, filepath)
if err != nil {
return fmt.Errorf("onedrive: Failed to update file(path=%v) metadata: %w", filepath, err)
}
return nil
}
func (d *Onedrive) updateMetadata(ctx context.Context, stream model.FileStreamer, filepath string) error {
url := d.GetMetaUrl(false, filepath)
metadata := toAPIMetadata(stream)
// ApiDoc: https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update?view=odsp-graph-online
_, err := d.Request(url, http.MethodPatch, func(req *resty.Request) {
req.SetBody(metadata).SetContext(ctx)
}, nil)
return err
}
func toAPIMetadata(stream model.FileStreamer) Metadata {
metadata := Metadata{
FileSystemInfo: &FileSystemInfoFacet{},
}
if !stream.ModTime().IsZero() {
metadata.FileSystemInfo.LastModifiedDateTime = stream.ModTime()
}
if !stream.CreateTime().IsZero() {
metadata.FileSystemInfo.CreatedDateTime = stream.CreateTime()
}
if stream.CreateTime().IsZero() && !stream.ModTime().IsZero() {
metadata.FileSystemInfo.CreatedDateTime = stream.CreateTime()
}
return metadata
}
func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/createUploadSession"
res, err := d.Request(url, http.MethodPost, nil, nil)
metadata := map[string]interface{}{"item": toAPIMetadata(stream)}
res, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(metadata).SetContext(ctx)
}, nil)
if err != nil {
return err
}

View File

@ -0,0 +1,131 @@
package onedrive_sharelink
import (
"context"
"strings"
"time"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/cron"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
)
type OnedriveSharelink struct {
model.Storage
cron *cron.Cron
Addition
}
func (d *OnedriveSharelink) Config() driver.Config {
return config
}
func (d *OnedriveSharelink) GetAddition() driver.Additional {
return &d.Addition
}
func (d *OnedriveSharelink) Init(ctx context.Context) error {
// Initialize error variable
var err error
// If there is "-my" in the URL, it is NOT a SharePoint link
d.IsSharepoint = !strings.Contains(d.ShareLinkURL, "-my")
// Initialize cron job to run every hour
d.cron = cron.NewCron(time.Hour * 1)
d.cron.Do(func() {
var err error
d.Headers, err = d.getHeaders()
if err != nil {
log.Errorf("%+v", err)
}
})
// Get initial headers
d.Headers, err = d.getHeaders()
if err != nil {
return err
}
return nil
}
func (d *OnedriveSharelink) Drop(ctx context.Context) error {
return nil
}
func (d *OnedriveSharelink) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
path := dir.GetPath()
files, err := d.getFiles(path)
if err != nil {
return nil, err
}
// Convert the slice of files to the required model.Obj format
return utils.SliceConvert(files, func(src Item) (model.Obj, error) {
return fileToObj(src), nil
})
}
func (d *OnedriveSharelink) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
// Get the unique ID of the file
uniqueId := file.GetID()
// Cut the first char and the last char
uniqueId = uniqueId[1 : len(uniqueId)-1]
url := d.downloadLinkPrefix + uniqueId
header := d.Headers
// If the headers are older than 30 minutes, get new headers
if d.HeaderTime < time.Now().Unix()-1800 {
var err error
log.Debug("headers are older than 30 minutes, get new headers")
header, err = d.getHeaders()
if err != nil {
return nil, err
}
}
return &model.Link{
URL: url,
Header: header,
}, nil
}
func (d *OnedriveSharelink) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
// TODO create folder, optional
return errs.NotImplement
}
func (d *OnedriveSharelink) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
// TODO move obj, optional
return errs.NotImplement
}
func (d *OnedriveSharelink) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
// TODO rename obj, optional
return errs.NotImplement
}
func (d *OnedriveSharelink) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
// TODO copy obj, optional
return errs.NotImplement
}
func (d *OnedriveSharelink) Remove(ctx context.Context, obj model.Obj) error {
// TODO remove obj, optional
return errs.NotImplement
}
func (d *OnedriveSharelink) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
// TODO upload file, optional
return errs.NotImplement
}
//func (d *OnedriveSharelink) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*OnedriveSharelink)(nil)

View File

@ -0,0 +1,32 @@
package onedrive_sharelink
import (
"net/http"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootPath
ShareLinkURL string `json:"url" required:"true"`
ShareLinkPassword string `json:"password"`
IsSharepoint bool
downloadLinkPrefix string
Headers http.Header
HeaderTime int64
}
var config = driver.Config{
Name: "Onedrive Sharelink",
OnlyProxy: true,
NoUpload: true,
DefaultRoot: "/",
CheckStatus: false,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &OnedriveSharelink{}
})
}

View File

@ -0,0 +1,77 @@
package onedrive_sharelink
import (
"strconv"
"time"
"github.com/alist-org/alist/v3/internal/model"
)
// FolderResp represents the structure of the folder response from the OneDrive API.
type FolderResp struct {
// Data holds the nested structure of the response.
Data struct {
Legacy struct {
RenderListData struct {
ListData struct {
Items []Item `json:"Row"` // Items contains the list of items in the folder.
} `json:"ListData"`
} `json:"renderListDataAsStream"`
} `json:"legacy"`
} `json:"data"`
}
// Item represents an individual item in the folder.
type Item struct {
ObjType string `json:"FSObjType"` // ObjType indicates if the item is a file or folder.
Name string `json:"FileLeafRef"` // Name is the name of the item.
ModifiedTime time.Time `json:"Modified."` // ModifiedTime is the last modified time of the item.
Size string `json:"File_x0020_Size"` // Size is the size of the item in string format.
Id string `json:"UniqueId"` // Id is the unique identifier of the item.
}
// fileToObj converts an Item to an ObjThumb.
func fileToObj(f Item) *model.ObjThumb {
// Convert Size from string to int64.
size, _ := strconv.ParseInt(f.Size, 10, 64)
// Convert ObjType from string to int.
objtype, _ := strconv.Atoi(f.ObjType)
// Create a new ObjThumb with the converted values.
file := &model.ObjThumb{
Object: model.Object{
Name: f.Name,
Modified: f.ModifiedTime,
Size: size,
IsFolder: objtype == 1, // Check if the item is a folder.
ID: f.Id,
},
Thumbnail: model.Thumbnail{},
}
return file
}
// GraphQLNEWRequest represents the structure of a new GraphQL request.
type GraphQLNEWRequest struct {
ListData struct {
NextHref string `json:"NextHref"` // NextHref is the link to the next set of data.
Row []Item `json:"Row"` // Row contains the list of items.
} `json:"ListData"`
}
// GraphQLRequest represents the structure of a GraphQL request.
type GraphQLRequest struct {
Data struct {
Legacy struct {
RenderListDataAsStream struct {
ListData struct {
NextHref string `json:"NextHref"` // NextHref is the link to the next set of data.
Row []Item `json:"Row"` // Row contains the list of items.
} `json:"ListData"`
ViewMetadata struct {
ListViewXml string `json:"ListViewXml"` // ListViewXml contains the XML of the list view.
} `json:"ViewMetadata"`
} `json:"renderListDataAsStream"`
} `json:"legacy"`
} `json:"data"`
}

View File

@ -0,0 +1,363 @@
package onedrive_sharelink
import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf"
log "github.com/sirupsen/logrus"
"golang.org/x/net/html"
)
// NewNoRedirectClient creates an HTTP client that doesn't follow redirects
func NewNoRedirectCLient() *http.Client {
return &http.Client{
Timeout: time.Hour * 48,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify},
},
// Prevent following redirects
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
// getCookiesWithPassword fetches cookies required for authenticated access using the provided password
func getCookiesWithPassword(link, password string) (string, error) {
// Send GET request
resp, err := http.Get(link)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Parse the HTML response
doc, err := html.Parse(resp.Body)
if err != nil {
return "", err
}
// Initialize variables to store form data
var viewstate, eventvalidation, postAction string
// Recursive function to find input fields by their IDs
var findInputFields func(*html.Node)
findInputFields = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "input" {
for _, attr := range n.Attr {
if attr.Key == "id" {
switch attr.Val {
case "__VIEWSTATE":
viewstate = getAttrValue(n, "value")
case "__EVENTVALIDATION":
eventvalidation = getAttrValue(n, "value")
}
}
}
}
if n.Type == html.ElementNode && n.Data == "form" {
for _, attr := range n.Attr {
if attr.Key == "id" && attr.Val == "inputForm" {
postAction = getAttrValue(n, "action")
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
findInputFields(c)
}
}
findInputFields(doc)
// Prepare the new URL for the POST request
linkParts, err := url.Parse(link)
if err != nil {
return "", err
}
newURL := fmt.Sprintf("%s://%s%s", linkParts.Scheme, linkParts.Host, postAction)
// Prepare the request body
data := url.Values{
"txtPassword": []string{password},
"__EVENTVALIDATION": []string{eventvalidation},
"__VIEWSTATE": []string{viewstate},
"__VIEWSTATEENCRYPTED": []string{""},
}
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// Send the POST request, preventing redirects
resp, err = client.PostForm(newURL, data)
if err != nil {
return "", err
}
// Extract the desired cookie value
cookie := resp.Cookies()
var fedAuthCookie string
for _, c := range cookie {
if c.Name == "FedAuth" {
fedAuthCookie = c.Value
break
}
}
if fedAuthCookie == "" {
return "", fmt.Errorf("wrong password")
}
return fmt.Sprintf("FedAuth=%s;", fedAuthCookie), nil
}
// getAttrValue retrieves the value of the specified attribute from an HTML node
func getAttrValue(n *html.Node, key string) string {
for _, attr := range n.Attr {
if attr.Key == key {
return attr.Val
}
}
return ""
}
// getHeaders constructs and returns the necessary HTTP headers for accessing the OneDrive share link
func (d *OnedriveSharelink) getHeaders() (http.Header, error) {
header := http.Header{}
header.Set("User-Agent", base.UserAgent)
header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
// Save current timestamp to d.HeaderTime
d.HeaderTime = time.Now().Unix()
if d.ShareLinkPassword == "" {
// Create a no-redirect client
clientNoDirect := NewNoRedirectCLient()
req, err := http.NewRequest("GET", d.ShareLinkURL, nil)
if err != nil {
return nil, err
}
// Set headers for the request
req.Header = header
answerNoRedirect, err := clientNoDirect.Do(req)
if err != nil {
return nil, err
}
redirectUrl := answerNoRedirect.Header.Get("Location")
log.Debugln("redirectUrl:", redirectUrl)
if redirectUrl == "" {
return nil, fmt.Errorf("password protected link. Please provide password")
}
header.Set("Cookie", answerNoRedirect.Header.Get("Set-Cookie"))
header.Set("Referer", redirectUrl)
// Extract the host part of the redirect URL and set it as the authority
u, err := url.Parse(redirectUrl)
if err != nil {
return nil, err
}
header.Set("authority", u.Host)
return header, nil
} else {
cookie, err := getCookiesWithPassword(d.ShareLinkURL, d.ShareLinkPassword)
if err != nil {
return nil, err
}
header.Set("Cookie", cookie)
header.Set("Referer", d.ShareLinkURL)
header.Set("authority", strings.Split(strings.Split(d.ShareLinkURL, "//")[1], "/")[0])
return header, nil
}
}
// getFiles retrieves the files from the OneDrive share link at the specified path
func (d *OnedriveSharelink) getFiles(path string) ([]Item, error) {
clientNoDirect := NewNoRedirectCLient()
req, err := http.NewRequest("GET", d.ShareLinkURL, nil)
if err != nil {
return nil, err
}
header := req.Header
redirectUrl := ""
if d.ShareLinkPassword == "" {
header.Set("User-Agent", base.UserAgent)
header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
req.Header = header
answerNoRedirect, err := clientNoDirect.Do(req)
if err != nil {
return nil, err
}
redirectUrl = answerNoRedirect.Header.Get("Location")
} else {
header = d.Headers
req.Header = header
answerNoRedirect, err := clientNoDirect.Do(req)
if err != nil {
return nil, err
}
redirectUrl = answerNoRedirect.Header.Get("Location")
}
redirectSplitURL := strings.Split(redirectUrl, "/")
req.Header = d.Headers
downloadLinkPrefix := ""
rootFolderPre := ""
// Determine the appropriate URL and root folder based on whether the link is SharePoint
if d.IsSharepoint {
// update req url
req.URL, err = url.Parse(redirectUrl)
if err != nil {
return nil, err
}
// Get redirectUrl
answer, err := clientNoDirect.Do(req)
if err != nil {
d.Headers, err = d.getHeaders()
if err != nil {
return nil, err
}
return d.getFiles(path)
}
defer answer.Body.Close()
re := regexp.MustCompile(`templateUrl":"(.*?)"`)
body, err := io.ReadAll(answer.Body)
if err != nil {
return nil, err
}
template := re.FindString(string(body))
template = template[strings.Index(template, "templateUrl\":\"")+len("templateUrl\":\""):]
template = template[:strings.Index(template, "?id=")]
template = template[:strings.LastIndex(template, "/")]
downloadLinkPrefix = template + "/download.aspx?UniqueId="
params, err := url.ParseQuery(redirectUrl[strings.Index(redirectUrl, "?")+1:])
if err != nil {
return nil, err
}
rootFolderPre = params.Get("id")
} else {
redirectUrlCut := redirectUrl[:strings.LastIndex(redirectUrl, "/")]
downloadLinkPrefix = redirectUrlCut + "/download.aspx?UniqueId="
params, err := url.ParseQuery(redirectUrl[strings.Index(redirectUrl, "?")+1:])
if err != nil {
return nil, err
}
rootFolderPre = params.Get("id")
}
d.downloadLinkPrefix = downloadLinkPrefix
rootFolder, err := url.QueryUnescape(rootFolderPre)
if err != nil {
return nil, err
}
log.Debugln("rootFolder:", rootFolder)
// Extract the relative path up to and including "Documents"
relativePath := strings.Split(rootFolder, "Documents")[0] + "Documents"
// URL encode the relative path
relativeUrl := url.QueryEscape(relativePath)
// Replace underscores and hyphens in the encoded relative path
relativeUrl = strings.Replace(relativeUrl, "_", "%5F", -1)
relativeUrl = strings.Replace(relativeUrl, "-", "%2D", -1)
// If the path is not the root, append the path to the root folder
if path != "/" {
rootFolder = rootFolder + path
}
// URL encode the full root folder path
rootFolderUrl := url.QueryEscape(rootFolder)
// Replace underscores and hyphens in the encoded root folder URL
rootFolderUrl = strings.Replace(rootFolderUrl, "_", "%5F", -1)
rootFolderUrl = strings.Replace(rootFolderUrl, "-", "%2D", -1)
log.Debugln("relativePath:", relativePath, "relativeUrl:", relativeUrl, "rootFolder:", rootFolder, "rootFolderUrl:", rootFolderUrl)
// Construct the GraphQL query with the encoded paths
graphqlVar := fmt.Sprintf(`{"query":"query (\n $listServerRelativeUrl: String!,$renderListDataAsStreamParameters: RenderListDataAsStreamParameters!,$renderListDataAsStreamQueryString: String!\n )\n {\n \n legacy {\n \n renderListDataAsStream(\n listServerRelativeUrl: $listServerRelativeUrl,\n parameters: $renderListDataAsStreamParameters,\n queryString: $renderListDataAsStreamQueryString\n )\n }\n \n \n perf {\n executionTime\n overheadTime\n parsingTime\n queryCount\n validationTime\n resolvers {\n name\n queryCount\n resolveTime\n waitTime\n }\n }\n }","variables":{"listServerRelativeUrl":"%s","renderListDataAsStreamParameters":{"renderOptions":5707527,"allowMultipleValueFilterForTaxonomyFields":true,"addRequiredFields":true,"folderServerRelativeUrl":"%s"},"renderListDataAsStreamQueryString":"@a1=\'%s\'&RootFolder=%s&TryNewExperienceSingle=TRUE"}}`, relativePath, rootFolder, relativeUrl, rootFolderUrl)
tempHeader := make(http.Header)
for k, v := range d.Headers {
tempHeader[k] = v
}
tempHeader["Content-Type"] = []string{"application/json;odata=verbose"}
client := &http.Client{}
postUrl := strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], "/") + "/_api/v2.1/graphql"
req, err = http.NewRequest("POST", postUrl, strings.NewReader(graphqlVar))
if err != nil {
return nil, err
}
req.Header = tempHeader
resp, err := client.Do(req)
if err != nil {
d.Headers, err = d.getHeaders()
if err != nil {
return nil, err
}
return d.getFiles(path)
}
defer resp.Body.Close()
var graphqlReq GraphQLRequest
json.NewDecoder(resp.Body).Decode(&graphqlReq)
log.Debugln("graphqlReq:", graphqlReq)
filesData := graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row
if graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.NextHref != "" {
nextHref := graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.NextHref + "&@a1=REPLACEME&TryNewExperienceSingle=TRUE"
nextHref = strings.Replace(nextHref, "REPLACEME", "%27"+relativeUrl+"%27", -1)
log.Debugln("nextHref:", nextHref)
filesData = append(filesData, graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row...)
listViewXml := graphqlReq.Data.Legacy.RenderListDataAsStream.ViewMetadata.ListViewXml
log.Debugln("listViewXml:", listViewXml)
renderListDataAsStreamVar := `{"parameters":{"__metadata":{"type":"SP.RenderListDataParameters"},"RenderOptions":1216519,"ViewXml":"REPLACEME","AllowMultipleValueFilterForTaxonomyFields":true,"AddRequiredFields":true}}`
listViewXml = strings.Replace(listViewXml, `"`, `\"`, -1)
renderListDataAsStreamVar = strings.Replace(renderListDataAsStreamVar, "REPLACEME", listViewXml, -1)
graphqlReqNEW := GraphQLNEWRequest{}
postUrl = strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], "/") + "/_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream" + nextHref
req, _ = http.NewRequest("POST", postUrl, strings.NewReader(renderListDataAsStreamVar))
req.Header = tempHeader
resp, err := client.Do(req)
if err != nil {
d.Headers, err = d.getHeaders()
if err != nil {
return nil, err
}
return d.getFiles(path)
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(&graphqlReqNEW)
for graphqlReqNEW.ListData.NextHref != "" {
graphqlReqNEW = GraphQLNEWRequest{}
postUrl = strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], "/") + "/_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream" + nextHref
req, _ = http.NewRequest("POST", postUrl, strings.NewReader(renderListDataAsStreamVar))
req.Header = tempHeader
resp, err := client.Do(req)
if err != nil {
d.Headers, err = d.getHeaders()
if err != nil {
return nil, err
}
return d.getFiles(path)
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(&graphqlReqNEW)
nextHref = graphqlReqNEW.ListData.NextHref + "&@a1=REPLACEME&TryNewExperienceSingle=TRUE"
nextHref = strings.Replace(nextHref, "REPLACEME", "%27"+relativeUrl+"%27", -1)
filesData = append(filesData, graphqlReqNEW.ListData.Row...)
}
filesData = append(filesData, graphqlReqNEW.ListData.Row...)
} else {
filesData = append(filesData, graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row...)
}
return filesData, nil
}

View File

@ -2,8 +2,11 @@ package pikpak
import (
"context"
"encoding/json"
"fmt"
"github.com/alist-org/alist/v3/internal/op"
"net/http"
"strconv"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
@ -17,13 +20,14 @@ import (
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)
type PikPak struct {
model.Storage
Addition
RefreshToken string
AccessToken string
*Common
oauth2Token oauth2.TokenSource
}
func (d *PikPak) Config() driver.Config {
@ -34,8 +38,52 @@ func (d *PikPak) GetAddition() driver.Additional {
return &d.Addition
}
func (d *PikPak) Init(ctx context.Context) error {
return d.login()
func (d *PikPak) Init(ctx context.Context) (err error) {
if d.ClientID == "" || d.ClientSecret == "" {
d.ClientID = "YNxT9w7GMdWvEOKa"
d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
}
if d.Common == nil {
d.Common = &Common{
client: base.NewRestyClient(),
CaptchaToken: "",
UserID: "",
DeviceID: utils.GetMD5EncodeStr(d.Username + d.Password),
UserAgent: BuildCustomUserAgent(utils.GetMD5EncodeStr(d.Username+d.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, ""),
RefreshCTokenCk: func(token string) {
d.Common.CaptchaToken = token
op.MustSaveDriverStorage(d)
},
}
}
oauth2Config := &oauth2.Config{
ClientID: d.ClientID,
ClientSecret: d.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: "https://user.mypikpak.com/v1/auth/signin",
TokenURL: "https://user.mypikpak.com/v1/auth/token",
AuthStyle: oauth2.AuthStyleInParams,
},
}
d.oauth2Token = oauth2.ReuseTokenSource(nil, utils.TokenSource(func() (*oauth2.Token, error) {
return oauth2Config.PasswordCredentialsToken(
context.WithValue(context.Background(), oauth2.HTTPClient, base.HttpClient),
d.Username,
d.Password,
)
}))
// 获取用户ID
_ = d.GetUserID()
// 获取CaptchaToken
_ = d.RefreshCaptchaTokenAtLogin(GetAction(http.MethodGet, "https://api-drive.mypikpak.com/drive/v1/files"), d.Common.UserID)
// 更新UserAgent
d.Common.UserAgent = BuildCustomUserAgent(d.Common.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, d.Common.UserID)
return nil
}
func (d *PikPak) Drop(ctx context.Context) error {
@ -54,7 +102,7 @@ func (d *PikPak) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
func (d *PikPak) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var resp File
_, err := d.request(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.GetID()),
_, err := d.requestWithCaptchaToken(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.GetID()),
http.MethodGet, nil, &resp)
if err != nil {
return nil, err
@ -184,4 +232,107 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err
}
// 离线下载文件
func (d *PikPak) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
requestBody := base.Json{
"kind": "drive#file",
"name": fileName,
"upload_type": "UPLOAD_TYPE_URL",
"url": base.Json{
"url": fileUrl,
},
"parent_id": parentDir.GetID(),
"folder_type": "",
}
var resp OfflineDownloadResp
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files", http.MethodPost, func(req *resty.Request) {
req.SetBody(requestBody)
}, &resp)
if err != nil {
return nil, err
}
return &resp.Task, err
}
/*
获取离线下载任务列表
phase 可能的取值:
PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING
*/
func (d *PikPak) OfflineList(ctx context.Context, nextPageToken string, phase []string) ([]OfflineTask, error) {
res := make([]OfflineTask, 0)
url := "https://api-drive.mypikpak.com/drive/v1/tasks"
if len(phase) == 0 {
phase = []string{"PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR", "PHASE_TYPE_COMPLETE", "PHASE_TYPE_PENDING"}
}
params := map[string]string{
"type": "offline",
"thumbnail_size": "SIZE_SMALL",
"limit": "10000",
"page_token": nextPageToken,
"with": "reference_resource",
}
// 处理 phase 参数
if len(phase) > 0 {
filters := base.Json{
"phase": map[string]string{
"in": strings.Join(phase, ","),
},
}
filtersJSON, err := json.Marshal(filters)
if err != nil {
return nil, fmt.Errorf("failed to marshal filters: %w", err)
}
params["filters"] = string(filtersJSON)
}
var resp OfflineListResp
_, err := d.request(url, http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx).
SetQueryParams(params)
}, &resp)
if err != nil {
return nil, fmt.Errorf("failed to get offline list: %w", err)
}
res = append(res, resp.Tasks...)
return res, nil
}
func (d *PikPak) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {
url := "https://api-drive.mypikpak.com/drive/v1/tasks"
params := map[string]string{
"task_ids": strings.Join(taskIDs, ","),
"delete_files": strconv.FormatBool(deleteFiles),
}
_, err := d.request(url, http.MethodDelete, func(req *resty.Request) {
req.SetContext(ctx).
SetQueryParams(params)
}, nil)
if err != nil {
return fmt.Errorf("failed to delete tasks %v: %w", taskIDs, err)
}
return nil
}
func (d *PikPak) GetUserID() error {
token, err := d.oauth2Token.Token()
if err != nil {
return err
}
userID := token.Extra("sub").(string)
if userID != "" {
d.Common.SetUserID(userID)
}
return nil
}
var _ driver.Driver = (*PikPak)(nil)

View File

@ -9,6 +9,8 @@ type Addition struct {
driver.RootID
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
DisableMediaLink bool `json:"disable_media_link"`
}

View File

@ -1,6 +1,7 @@
package pikpak
import (
"fmt"
"strconv"
"time"
@ -9,11 +10,6 @@ import (
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
)
type RespErr struct {
ErrorCode int `json:"error_code"`
Error string `json:"error"`
}
type Files struct {
Files []File `json:"files"`
NextPageToken string `json:"next_page_token"`
@ -99,3 +95,101 @@ type UploadTaskData struct {
File File `json:"file"`
}
// 添加离线下载响应
type OfflineDownloadResp struct {
File *string `json:"file"`
Task OfflineTask `json:"task"`
UploadType string `json:"upload_type"`
URL struct {
Kind string `json:"kind"`
} `json:"url"`
}
// 离线下载列表
type OfflineListResp struct {
ExpiresIn int64 `json:"expires_in"`
NextPageToken string `json:"next_page_token"`
Tasks []OfflineTask `json:"tasks"`
}
// offlineTask
type OfflineTask struct {
Callback string `json:"callback"`
CreatedTime string `json:"created_time"`
FileID string `json:"file_id"`
FileName string `json:"file_name"`
FileSize string `json:"file_size"`
IconLink string `json:"icon_link"`
ID string `json:"id"`
Kind string `json:"kind"`
Message string `json:"message"`
Name string `json:"name"`
Params Params `json:"params"`
Phase string `json:"phase"` // PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING
Progress int64 `json:"progress"`
ReferenceResource ReferenceResource `json:"reference_resource"`
Space string `json:"space"`
StatusSize int64 `json:"status_size"`
Statuses []string `json:"statuses"`
ThirdTaskID string `json:"third_task_id"`
Type string `json:"type"`
UpdatedTime string `json:"updated_time"`
UserID string `json:"user_id"`
}
type Params struct {
Age string `json:"age"`
MIMEType *string `json:"mime_type,omitempty"`
PredictType string `json:"predict_type"`
URL string `json:"url"`
}
type ReferenceResource struct {
Type string `json:"@type"`
Audit interface{} `json:"audit"`
Hash string `json:"hash"`
IconLink string `json:"icon_link"`
ID string `json:"id"`
Kind string `json:"kind"`
Medias []Media `json:"medias"`
MIMEType string `json:"mime_type"`
Name string `json:"name"`
Params map[string]interface{} `json:"params"`
ParentID string `json:"parent_id"`
Phase string `json:"phase"`
Size string `json:"size"`
Space string `json:"space"`
Starred bool `json:"starred"`
Tags []string `json:"tags"`
ThumbnailLink string `json:"thumbnail_link"`
}
type ErrResp struct {
ErrorCode int64 `json:"error_code"`
ErrorMsg string `json:"error"`
ErrorDescription string `json:"error_description"`
}
func (e *ErrResp) IsError() bool {
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
}
func (e *ErrResp) Error() string {
return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)
}
type CaptchaTokenRequest struct {
Action string `json:"action"`
CaptchaToken string `json:"captcha_token"`
ClientID string `json:"client_id"`
DeviceID string `json:"device_id"`
Meta map[string]string `json:"meta"`
RedirectUri string `json:"redirect_uri"`
}
type CaptchaTokenResponse struct {
CaptchaToken string `json:"captcha_token"`
ExpiresIn int64 `json:"expires_in"`
Url string `json:"url"`
}

View File

@ -1,104 +1,108 @@
package pikpak
import (
"crypto/md5"
"crypto/sha1"
"encoding/hex"
"errors"
"io"
"fmt"
"github.com/alist-org/alist/v3/pkg/utils"
"net/http"
"regexp"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/op"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
)
// do others that not defined in Driver interface
func (d *PikPak) login() error {
url := "https://user.mypikpak.com/v1/auth/signin"
var e RespErr
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
"captcha_token": "",
"client_id": "YNxT9w7GMdWvEOKa",
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
"username": d.Username,
"password": d.Password,
}).Post(url)
if err != nil {
return err
}
if e.ErrorCode != 0 {
return errors.New(e.Error)
}
data := res.Body()
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
return nil
var Algorithms = []string{
"PAe56I7WZ6FCSkFy77A96jHWcQA27ui80Qy4",
"SUbmk67TfdToBAEe2cZyP8vYVeN",
"1y3yFSZVWiGN95fw/2FQlRuH/Oy6WnO",
"8amLtHJpGzHPz4m9hGz7r+i+8dqQiAk",
"tmIEq5yl2g/XWwM3sKZkY4SbL8YUezrvxPksNabUJ",
"4QvudeJwgJuSf/qb9/wjC21L5aib",
"D1RJd+FZ+LBbt+dAmaIyYrT9gxJm0BB",
"1If",
"iGZr/SJPUFRkwvC174eelKy",
}
func (d *PikPak) refreshToken() error {
url := "https://user.mypikpak.com/v1/auth/token"
var e RespErr
res, err := base.RestyClient.R().SetError(&e).
SetHeader("user-agent", "").SetBody(base.Json{
"client_id": "YNxT9w7GMdWvEOKa",
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
"grant_type": "refresh_token",
"refresh_token": d.RefreshToken,
}).Post(url)
if err != nil {
d.Status = err.Error()
op.MustSaveDriverStorage(d)
return err
}
if e.ErrorCode != 0 {
if e.ErrorCode == 4126 {
// refresh_token invalid, re-login
return d.login()
}
d.Status = e.Error
op.MustSaveDriverStorage(d)
return errors.New(e.Error)
}
data := res.Body()
d.Status = "work"
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
op.MustSaveDriverStorage(d)
return nil
}
const (
ClientID = "YNxT9w7GMdWvEOKa"
ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
ClientVersion = "1.46.2"
PackageName = "com.pikcloud.pikpak"
SdkVersion = "2.0.4.204000 "
)
func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
token, err := d.oauth2Token.Token()
if err != nil {
return nil, err
}
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
var e RespErr
var e ErrResp
req.SetError(&e)
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
if e.ErrorCode != 0 {
if e.ErrorCode == 16 {
// login / refresh token
err = d.refreshToken()
if err != nil {
return nil, err
}
return d.request(url, method, callback, resp)
} else {
return nil, errors.New(e.Error)
}
if e.IsError() {
return nil, &e
}
return res.Body(), nil
}
func (d *PikPak) requestWithCaptchaToken(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
data, err := d.request(url, method, func(req *resty.Request) {
req.SetHeaders(map[string]string{
"User-Agent": d.GetUserAgent(),
"X-Device-ID": d.GetDeviceID(),
"X-Captcha-Token": d.GetCaptchaToken(),
})
if callback != nil {
callback(req)
}
}, resp)
errResp, ok := err.(*ErrResp)
if !ok {
return nil, err
}
switch errResp.ErrorCode {
case 0:
return data, nil
//case 4122, 4121, 10, 16:
// if d.refreshTokenFunc != nil {
// if err = xc.refreshTokenFunc(); err == nil {
// break
// }
// }
// return nil, err
case 9: // 验证码token过期
if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.Common.UserID); err != nil {
return nil, err
}
default:
return nil, err
}
return d.requestWithCaptchaToken(url, method, callback, resp)
}
func (d *PikPak) getFiles(id string) ([]File, error) {
res := make([]File, 0)
pageToken := "first"
@ -127,27 +131,173 @@ func (d *PikPak) getFiles(id string) ([]File, error) {
return res, nil
}
func getGcid(r io.Reader, size int64) (string, error) {
calcBlockSize := func(j int64) int64 {
var psize int64 = 0x40000
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
psize = psize << 1
}
return psize
func GetAction(method string, url string) string {
urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(url)[1]
return method + ":" + urlpath
}
type Common struct {
client *resty.Client
CaptchaToken string
UserID string
// 必要值,签名相关
DeviceID string
UserAgent string
// 验证码token刷新成功回调
RefreshCTokenCk func(token string)
}
func generateDeviceSign(deviceID, packageName string) string {
signatureBase := fmt.Sprintf("%s%s%s%s", deviceID, packageName, "1", "appkey")
sha1Hash := sha1.New()
sha1Hash.Write([]byte(signatureBase))
sha1Result := sha1Hash.Sum(nil)
sha1String := hex.EncodeToString(sha1Result)
md5Hash := md5.New()
md5Hash.Write([]byte(sha1String))
md5Result := md5Hash.Sum(nil)
md5String := hex.EncodeToString(md5Result)
deviceSign := fmt.Sprintf("div101.%s%s", deviceID, md5String)
return deviceSign
}
func BuildCustomUserAgent(deviceID, clientID, appName, sdkVersion, clientVersion, packageName, userID string) string {
deviceSign := generateDeviceSign(deviceID, packageName)
var sb strings.Builder
sb.WriteString(fmt.Sprintf("ANDROID-%s/%s ", appName, clientVersion))
sb.WriteString("protocolVersion/200 ")
sb.WriteString("accesstype/ ")
sb.WriteString(fmt.Sprintf("clientid/%s ", clientID))
sb.WriteString(fmt.Sprintf("clientversion/%s ", clientVersion))
sb.WriteString("action_type/ ")
sb.WriteString("networktype/WIFI ")
sb.WriteString("sessionid/ ")
sb.WriteString(fmt.Sprintf("deviceid/%s ", deviceID))
sb.WriteString("providername/NONE ")
sb.WriteString(fmt.Sprintf("devicesign/%s ", deviceSign))
sb.WriteString("refresh_token/ ")
sb.WriteString(fmt.Sprintf("sdkversion/%s ", sdkVersion))
sb.WriteString(fmt.Sprintf("datetime/%d ", time.Now().UnixMilli()))
sb.WriteString(fmt.Sprintf("usrno/%s ", userID))
sb.WriteString(fmt.Sprintf("appname/android-%s ", appName))
sb.WriteString(fmt.Sprintf("session_origin/ "))
sb.WriteString(fmt.Sprintf("grant_type/ "))
sb.WriteString(fmt.Sprintf("appid/ "))
sb.WriteString(fmt.Sprintf("clientip/ "))
sb.WriteString(fmt.Sprintf("devicename/Xiaomi_M2004j7ac "))
sb.WriteString(fmt.Sprintf("osversion/13 "))
sb.WriteString(fmt.Sprintf("platformversion/10 "))
sb.WriteString(fmt.Sprintf("accessmode/ "))
sb.WriteString(fmt.Sprintf("devicemodel/M2004J7AC "))
return sb.String()
}
func (c *Common) SetDeviceID(deviceID string) {
c.DeviceID = deviceID
}
func (c *Common) SetUserID(userID string) {
c.UserID = userID
}
func (c *Common) SetUserAgent(userAgent string) {
c.UserAgent = userAgent
}
func (c *Common) SetCaptchaToken(captchaToken string) {
c.CaptchaToken = captchaToken
}
func (c *Common) GetCaptchaToken() string {
return c.CaptchaToken
}
func (c *Common) GetUserAgent() string {
return c.UserAgent
}
func (c *Common) GetDeviceID() string {
return c.DeviceID
}
// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
func (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error {
metas := map[string]string{
"client_version": ClientVersion,
"package_name": PackageName,
"user_id": userID,
}
metas["timestamp"], metas["captcha_sign"] = d.Common.GetCaptchaSign()
return d.refreshCaptchaToken(action, metas)
}
// RefreshCaptchaTokenInLogin 刷新验证码token(登录时)
func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {
metas := make(map[string]string)
if ok, _ := regexp.MatchString(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`, username); ok {
metas["email"] = username
} else if len(username) >= 11 && len(username) <= 18 {
metas["phone_number"] = username
} else {
metas["username"] = username
}
return d.refreshCaptchaToken(action, metas)
}
// GetCaptchaSign 获取验证码签名
func (c *Common) GetCaptchaSign() (timestamp, sign string) {
timestamp = fmt.Sprint(time.Now().UnixMilli())
str := fmt.Sprint(ClientID, ClientVersion, PackageName, c.DeviceID, timestamp)
for _, algorithm := range Algorithms {
str = utils.GetMD5EncodeStr(str + algorithm)
}
sign = "1." + str
return
}
// refreshCaptchaToken 刷新CaptchaToken
func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) error {
param := CaptchaTokenRequest{
Action: action,
CaptchaToken: d.Common.CaptchaToken,
ClientID: ClientID,
DeviceID: d.Common.DeviceID,
Meta: metas,
RedirectUri: "xlaccsdk01://xbase.cloud/callback?state=harbor",
}
var e ErrResp
var resp CaptchaTokenResponse
_, err := d.request("https://user.mypikpak.com/v1/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
req.SetError(&e).SetBody(param)
}, &resp)
if err != nil {
return err
}
hash1 := sha1.New()
hash2 := sha1.New()
readSize := calcBlockSize(size)
for {
hash2.Reset()
if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
if err != io.EOF {
return "", err
}
break
}
hash1.Write(hash2.Sum(nil))
if e.IsError() {
return &e
}
return hex.EncodeToString(hash1.Sum(nil)), nil
if resp.Url != "" {
return fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
}
if resp.CaptchaToken == "" {
return fmt.Errorf("empty captchaToken")
}
if d.Common.RefreshCTokenCk != nil {
d.Common.RefreshCTokenCk(resp.CaptchaToken)
}
d.Common.SetCaptchaToken(resp.CaptchaToken)
return nil
}

View File

@ -4,17 +4,18 @@ import (
"context"
"net/http"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
"golang.org/x/oauth2"
)
type PikPakShare struct {
model.Storage
Addition
RefreshToken string
AccessToken string
oauth2Token oauth2.TokenSource
PassCodeToken string
}
@ -27,15 +28,31 @@ func (d *PikPakShare) GetAddition() driver.Additional {
}
func (d *PikPakShare) Init(ctx context.Context) error {
err := d.login()
if err != nil {
return err
if d.ClientID == "" || d.ClientSecret == "" {
d.ClientID = "YNxT9w7GMdWvEOKa"
d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
}
oauth2Config := &oauth2.Config{
ClientID: d.ClientID,
ClientSecret: d.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: "https://user.mypikpak.com/v1/auth/signin",
TokenURL: "https://user.mypikpak.com/v1/auth/token",
AuthStyle: oauth2.AuthStyleInParams,
},
}
d.oauth2Token = oauth2.ReuseTokenSource(nil, utils.TokenSource(func() (*oauth2.Token, error) {
return oauth2Config.PasswordCredentialsToken(
context.WithValue(context.Background(), oauth2.HTTPClient, base.HttpClient),
d.Username,
d.Password,
)
}))
if d.SharePwd != "" {
err = d.getSharePassToken()
if err != nil {
return err
}
return d.getSharePassToken()
}
return nil
}
@ -67,8 +84,14 @@ func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkA
if err != nil {
return nil, err
}
downloadUrl := resp.FileInfo.WebContentLink
if downloadUrl == "" && len(resp.FileInfo.Medias) > 0 {
downloadUrl = resp.FileInfo.Medias[0].Link.Url
}
link := model.Link{
URL: resp.FileInfo.WebContentLink,
URL: downloadUrl,
}
return &link, nil
}

View File

@ -7,10 +7,12 @@ import (
type Addition struct {
driver.RootID
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
ShareId string `json:"share_id" required:"true"`
SharePwd string `json:"share_pwd"`
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
ShareId string `json:"share_id" required:"true"`
SharePwd string `json:"share_pwd"`
ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
}
var config = driver.Config{

View File

@ -5,70 +5,18 @@ import (
"net/http"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/op"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
)
// do others that not defined in Driver interface
func (d *PikPakShare) login() error {
url := "https://user.mypikpak.com/v1/auth/signin"
var e RespErr
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
"captcha_token": "",
"client_id": "YNxT9w7GMdWvEOKa",
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
"username": d.Username,
"password": d.Password,
}).Post(url)
if err != nil {
return err
}
if e.ErrorCode != 0 {
return errors.New(e.Error)
}
data := res.Body()
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
return nil
}
func (d *PikPakShare) refreshToken() error {
url := "https://user.mypikpak.com/v1/auth/token"
var e RespErr
res, err := base.RestyClient.R().SetError(&e).
SetHeader("user-agent", "").SetBody(base.Json{
"client_id": "YNxT9w7GMdWvEOKa",
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
"grant_type": "refresh_token",
"refresh_token": d.RefreshToken,
}).Post(url)
if err != nil {
d.Status = err.Error()
op.MustSaveDriverStorage(d)
return err
}
if e.ErrorCode != 0 {
if e.ErrorCode == 4126 {
// refresh_token invalid, re-login
return d.login()
}
d.Status = e.Error
op.MustSaveDriverStorage(d)
return errors.New(e.Error)
}
data := res.Body()
d.Status = "work"
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
op.MustSaveDriverStorage(d)
return nil
}
func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
token, err := d.oauth2Token.Token()
if err != nil {
return nil, err
}
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
if callback != nil {
callback(req)
}
@ -82,14 +30,6 @@ func (d *PikPakShare) request(url string, method string, callback base.ReqCallba
return nil, err
}
if e.ErrorCode != 0 {
if e.ErrorCode == 16 {
// login / refresh token
err = d.refreshToken()
if err != nil {
return nil, err
}
return d.request(url, method, callback, resp)
}
return nil, errors.New(e.Error)
}
return res.Body(), nil

View File

@ -143,7 +143,7 @@ func (d *QuarkOrUC) Put(ctx context.Context, dstDir model.Obj, stream model.File
_ = tempFile.Close()
}()
m := md5.New()
_, err = io.Copy(m, tempFile)
_, err = utils.CopyWithBuffer(m, tempFile)
if err != nil {
return err
}
@ -153,7 +153,7 @@ func (d *QuarkOrUC) Put(ctx context.Context, dstDir model.Obj, stream model.File
}
md5Str := hex.EncodeToString(m.Sum(nil))
s := sha1.New()
_, err = io.Copy(s, tempFile)
_, err = utils.CopyWithBuffer(s, tempFile)
if err != nil {
return err
}

View File

@ -17,6 +17,7 @@ type TmpTokenResponse struct {
}
type TmpTokenResponseData struct {
Credentials Credentials `json:"Credentials"`
ExpiredAt int `json:"ExpiredAt"`
}
type Credentials struct {
AccessKeyId string `json:"accessKeyId,omitempty"`

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/cron"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
@ -28,6 +29,7 @@ type S3 struct {
linkClient *s3.S3
config driver.Config
cron *cron.Cron
}
func (d *S3) Config() driver.Config {
@ -42,6 +44,18 @@ func (d *S3) Init(ctx context.Context) error {
if d.Region == "" {
d.Region = "alist"
}
if d.config.Name == "Doge" {
// 多吉云每次临时生成的秘钥有效期为 2h所以这里设置为 118 分钟重新生成一次
d.cron = cron.NewCron(time.Minute * 118)
d.cron.Do(func() {
err := d.initSession()
if err != nil {
log.Errorln("Doge init session error:", err)
}
d.client = d.getClient(false)
d.linkClient = d.getClient(true)
})
}
err := d.initSession()
if err != nil {
return err
@ -52,6 +66,9 @@ func (d *S3) Init(ctx context.Context) error {
}
func (d *S3) Drop(ctx context.Context) error {
if d.cron != nil {
d.cron.Stop()
}
return nil
}

View File

@ -9,8 +9,9 @@ type Addition struct {
driver.RootPath
Address string `json:"address" required:"true"`
UserName string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
UserName string `json:"username" required:"false"`
Password string `json:"password" required:"false"`
Token string `json:"token" required:"false"`
RepoId string `json:"repoId" required:"false"`
RepoPwd string `json:"repoPwd" required:"false"`
}

View File

@ -14,6 +14,10 @@ import (
)
func (d *Seafile) getToken() error {
if d.Token != "" {
d.authorization = fmt.Sprintf("Token %s", d.Token)
return nil
}
var authResp AuthTokenResp
res, err := base.RestyClient.R().
SetResult(&authResp).

View File

@ -16,7 +16,8 @@ import (
type SFTP struct {
model.Storage
Addition
client *sftp.Client
client *sftp.Client
clientConnectionError error
}
func (d *SFTP) Config() driver.Config {
@ -39,6 +40,9 @@ func (d *SFTP) Drop(ctx context.Context) error {
}
func (d *SFTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if err := d.clientReconnectOnConnectionError(); err != nil {
return nil, err
}
log.Debugf("[sftp] list dir: %s", dir.GetPath())
files, err := d.client.ReadDir(dir.GetPath())
if err != nil {
@ -51,6 +55,9 @@ func (d *SFTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
}
func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if err := d.clientReconnectOnConnectionError(); err != nil {
return nil, err
}
remoteFile, err := d.client.Open(file.GetPath())
if err != nil {
return nil, err
@ -62,14 +69,23 @@ func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
}
func (d *SFTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if err := d.clientReconnectOnConnectionError(); err != nil {
return err
}
return d.client.MkdirAll(path.Join(parentDir.GetPath(), dirName))
}
func (d *SFTP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
if err := d.clientReconnectOnConnectionError(); err != nil {
return err
}
return d.client.Rename(srcObj.GetPath(), path.Join(dstDir.GetPath(), srcObj.GetName()))
}
func (d *SFTP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
if err := d.clientReconnectOnConnectionError(); err != nil {
return err
}
return d.client.Rename(srcObj.GetPath(), path.Join(path.Dir(srcObj.GetPath()), newName))
}
@ -78,10 +94,16 @@ func (d *SFTP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
}
func (d *SFTP) Remove(ctx context.Context, obj model.Obj) error {
if err := d.clientReconnectOnConnectionError(); err != nil {
return err
}
return d.remove(obj.GetPath())
}
func (d *SFTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
if err := d.clientReconnectOnConnectionError(); err != nil {
return err
}
dstFile, err := d.client.Create(path.Join(dstDir.GetPath(), stream.GetName()))
if err != nil {
return err

View File

@ -10,6 +10,7 @@ type Addition struct {
Username string `json:"username" required:"true"`
PrivateKey string `json:"private_key" type:"text"`
Password string `json:"password"`
Passphrase string `json:"passphrase"`
driver.RootPath
IgnoreSymlinkError bool `json:"ignore_symlink_error" default:"false" info:"Ignore symlink error"`
}

View File

@ -4,6 +4,7 @@ import (
"path"
"github.com/pkg/sftp"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
@ -11,8 +12,14 @@ import (
func (d *SFTP) initClient() error {
var auth ssh.AuthMethod
if d.PrivateKey != "" {
signer, err := ssh.ParsePrivateKey([]byte(d.PrivateKey))
if len(d.PrivateKey) > 0 {
var err error
var signer ssh.Signer
if len(d.Passphrase) > 0 {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(d.PrivateKey), []byte(d.Passphrase))
} else {
signer, err = ssh.ParsePrivateKey([]byte(d.PrivateKey))
}
if err != nil {
return err
}
@ -30,6 +37,23 @@ func (d *SFTP) initClient() error {
return err
}
d.client, err = sftp.NewClient(conn)
if err == nil {
d.clientConnectionError = nil
go func(d *SFTP) {
d.clientConnectionError = d.client.Wait()
}(d)
}
return err
}
func (d *SFTP) clientReconnectOnConnectionError() error {
err := d.clientConnectionError
if err == nil {
return nil
}
log.Debugf("[sftp] discarding closed sftp connection: %v", err)
_ = d.client.Close()
err = d.initClient()
return err
}

View File

@ -1,7 +1,7 @@
package smb
import (
"io"
"github.com/alist-org/alist/v3/pkg/utils"
"io/fs"
"net"
"os"
@ -74,7 +74,7 @@ func (d *SMB) CopyFile(src, dst string) error {
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
if _, err = utils.CopyWithBuffer(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = d.fs.Stat(src); err != nil {

View File

@ -190,7 +190,7 @@ func getGcid(r io.Reader, size int64) (string, error) {
readSize := calcBlockSize(size)
for {
hash2.Reset()
if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
if n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {
if err != io.EOF {
return "", err
}

View File

@ -0,0 +1,837 @@
package thunder_browser
import (
"context"
"errors"
"fmt"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/go-resty/resty/v2"
"net/http"
"regexp"
"strings"
)
type ThunderBrowser struct {
*XunLeiBrowserCommon
model.Storage
Addition
identity string
}
func (x *ThunderBrowser) Config() driver.Config {
return config
}
func (x *ThunderBrowser) GetAddition() driver.Additional {
return &x.Addition
}
func (x *ThunderBrowser) Init(ctx context.Context) (err error) {
spaceTokenFunc := func() error {
// 如果用户未设置 "超级保险柜" 密码 则直接返回
if x.SafePassword == "" {
return nil
}
// 通过 GetSafeAccessToken 获取
token, err := x.GetSafeAccessToken(x.SafePassword)
x.SetSpaceTokenResp(token)
return err
}
// 初始化所需参数
if x.XunLeiBrowserCommon == nil {
x.XunLeiBrowserCommon = &XunLeiBrowserCommon{
Common: &Common{
client: base.NewRestyClient(),
Algorithms: Algorithms,
DeviceID: utils.GetMD5EncodeStr(x.Username + x.Password),
ClientID: ClientID,
ClientSecret: ClientSecret,
ClientVersion: ClientVersion,
PackageName: PackageName,
UserAgent: BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), PackageName, SdkVersion, ClientVersion, PackageName),
DownloadUserAgent: DownloadUserAgent,
UseVideoUrl: x.UseVideoUrl,
RemoveWay: x.Addition.RemoveWay,
refreshCTokenCk: func(token string) {
x.CaptchaToken = token
op.MustSaveDriverStorage(x)
},
},
refreshTokenFunc: func() error {
// 通过RefreshToken刷新
token, err := x.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
// 重新登录
token, err = x.Login(x.Username, x.Password)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
op.MustSaveDriverStorage(x)
}
}
x.SetTokenResp(token)
return err
},
}
}
// 自定义验证码token
ctoekn := strings.TrimSpace(x.CaptchaToken)
if ctoekn != "" {
x.SetCaptchaToken(ctoekn)
}
if x.DeviceID == "" {
x.SetDeviceID(utils.GetMD5EncodeStr(x.Username + x.Password))
}
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
x.Addition.RootFolderID = x.RootFolderID
// 防止重复登录
identity := x.GetIdentity()
if x.identity != identity || !x.IsLogin() {
x.identity = identity
// 登录
token, err := x.Login(x.Username, x.Password)
if err != nil {
return err
}
x.SetTokenResp(token)
}
// 获取 spaceToken
err = spaceTokenFunc()
if err != nil {
return err
}
return nil
}
func (x *ThunderBrowser) Drop(ctx context.Context) error {
return nil
}
type ThunderBrowserExpert struct {
*XunLeiBrowserCommon
model.Storage
ExpertAddition
identity string
}
func (x *ThunderBrowserExpert) Config() driver.Config {
return configExpert
}
func (x *ThunderBrowserExpert) GetAddition() driver.Additional {
return &x.ExpertAddition
}
func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {
spaceTokenFunc := func() error {
// 如果用户未设置 "超级保险柜" 密码 则直接返回
if x.SafePassword == "" {
return nil
}
// 通过 GetSafeAccessToken 获取
token, err := x.GetSafeAccessToken(x.SafePassword)
x.SetSpaceTokenResp(token)
return err
}
// 防止重复登录
identity := x.GetIdentity()
if identity != x.identity || !x.IsLogin() {
x.identity = identity
x.XunLeiBrowserCommon = &XunLeiBrowserCommon{
Common: &Common{
client: base.NewRestyClient(),
DeviceID: func() string {
if len(x.DeviceID) != 32 {
if x.LoginType == "user" {
return utils.GetMD5EncodeStr(x.Username + x.Password)
}
return utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken)
}
return x.DeviceID
}(),
ClientID: x.ClientID,
ClientSecret: x.ClientSecret,
ClientVersion: x.ClientVersion,
PackageName: x.PackageName,
UserAgent: func() string {
if x.ExpertAddition.UserAgent != "" {
return x.ExpertAddition.UserAgent
}
if x.LoginType == "user" {
return BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), x.PackageName, SdkVersion, x.ClientVersion, x.PackageName)
}
return BuildCustomUserAgent(utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken), x.PackageName, SdkVersion, x.ClientVersion, x.PackageName)
}(),
DownloadUserAgent: func() string {
if x.ExpertAddition.DownloadUserAgent != "" {
return x.ExpertAddition.DownloadUserAgent
}
return DownloadUserAgent
}(),
UseVideoUrl: x.UseVideoUrl,
RemoveWay: x.ExpertAddition.RemoveWay,
refreshCTokenCk: func(token string) {
x.CaptchaToken = token
op.MustSaveDriverStorage(x)
},
},
}
if x.ExpertAddition.CaptchaToken != "" {
x.SetCaptchaToken(x.ExpertAddition.CaptchaToken)
op.MustSaveDriverStorage(x)
}
if x.Common.DeviceID != "" {
x.ExpertAddition.DeviceID = x.Common.DeviceID
op.MustSaveDriverStorage(x)
}
if x.Common.UserAgent != "" {
x.ExpertAddition.UserAgent = x.Common.UserAgent
op.MustSaveDriverStorage(x)
}
if x.Common.DownloadUserAgent != "" {
x.ExpertAddition.DownloadUserAgent = x.Common.DownloadUserAgent
op.MustSaveDriverStorage(x)
}
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
x.ExpertAddition.RootFolderID = x.RootFolderID
// 签名方法
if x.SignType == "captcha_sign" {
x.Common.Timestamp = x.Timestamp
x.Common.CaptchaSign = x.CaptchaSign
} else {
x.Common.Algorithms = strings.Split(x.Algorithms, ",")
}
// 登录方式
if x.LoginType == "refresh_token" {
// 通过RefreshToken登录
token, err := x.XunLeiBrowserCommon.RefreshToken(x.ExpertAddition.RefreshToken)
if err != nil {
return err
}
x.SetTokenResp(token)
// 刷新token方法
x.SetRefreshTokenFunc(func() error {
token, err := x.XunLeiBrowserCommon.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
}
x.SetTokenResp(token)
op.MustSaveDriverStorage(x)
return err
})
err = spaceTokenFunc()
if err != nil {
return err
}
} else {
// 通过用户密码登录
token, err := x.Login(x.Username, x.Password)
if err != nil {
return err
}
x.SetTokenResp(token)
x.SetRefreshTokenFunc(func() error {
token, err := x.XunLeiBrowserCommon.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
token, err = x.Login(x.Username, x.Password)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
}
}
x.SetTokenResp(token)
op.MustSaveDriverStorage(x)
return err
})
err = spaceTokenFunc()
if err != nil {
return err
}
}
} else {
// 仅修改验证码token
if x.CaptchaToken != "" {
x.SetCaptchaToken(x.CaptchaToken)
}
err = spaceTokenFunc()
if err != nil {
return err
}
x.XunLeiBrowserCommon.UserAgent = x.UserAgent
x.XunLeiBrowserCommon.DownloadUserAgent = x.DownloadUserAgent
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
x.ExpertAddition.RootFolderID = x.RootFolderID
}
return nil
}
func (x *ThunderBrowserExpert) Drop(ctx context.Context) error {
return nil
}
func (x *ThunderBrowserExpert) SetTokenResp(token *TokenResp) {
x.XunLeiBrowserCommon.SetTokenResp(token)
if token != nil {
x.ExpertAddition.RefreshToken = token.RefreshToken
}
}
type XunLeiBrowserCommon struct {
*Common
*TokenResp // 登录信息
refreshTokenFunc func() error
}
func (xc *XunLeiBrowserCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return xc.getFiles(ctx, dir.GetID(), args.ReqPath)
}
func (xc *XunLeiBrowserCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var lFile Files
params := map[string]string{
"_magic": "2021",
"space": "SPACE_BROWSER",
"thumbnail_size": "SIZE_LARGE",
"with": "url",
}
// 对 "迅雷云盘" 内的文件 特殊处理
if file.GetPath() == ThunderDriveFileID {
params = map[string]string{}
} else if file.GetPath() == ThunderBrowserDriveSafeFileID {
// 对 "超级保险箱" 内的文件 特殊处理
params["space"] = "SPACE_BROWSER_SAFE"
}
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", file.GetID())
r.SetQueryParams(params)
//r.SetQueryParam("space", "")
}, &lFile)
if err != nil {
return nil, err
}
link := &model.Link{
URL: lFile.WebContentLink,
Header: http.Header{
"User-Agent": {xc.DownloadUserAgent},
},
}
if xc.UseVideoUrl {
for _, media := range lFile.Medias {
if media.Link.URL != "" {
link.URL = media.Link.URL
break
}
}
}
return link, nil
}
func (xc *XunLeiBrowserCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
js := base.Json{
"kind": FOLDER,
"name": dirName,
"parent_id": parentDir.GetID(),
"space": "SPACE_BROWSER",
}
if parentDir.GetPath() == ThunderDriveFileID {
js = base.Json{
"kind": FOLDER,
"name": dirName,
"parent_id": parentDir.GetID(),
}
} else if parentDir.GetPath() == ThunderBrowserDriveSafeFileID {
js = base.Json{
"kind": FOLDER,
"name": dirName,
"parent_id": parentDir.GetID(),
"space": "SPACE_BROWSER_SAFE",
}
}
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&js)
}, nil)
return err
}
func (xc *XunLeiBrowserCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
srcSpace := "SPACE_BROWSER"
dstSpace := "SPACE_BROWSER"
// 对 "超级保险箱" 内的文件 特殊处理
if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
srcSpace = "SPACE_BROWSER_SAFE"
}
if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
dstSpace = "SPACE_BROWSER_SAFE"
}
params := map[string]string{
"_from": dstSpace,
}
js := base.Json{
"to": base.Json{"parent_id": dstDir.GetID(), "space": dstSpace},
"space": srcSpace,
"ids": []string{srcObj.GetID()},
}
// 对 "迅雷云盘" 内的文件 特殊处理
if srcObj.GetPath() == ThunderDriveFileID {
params = map[string]string{}
js = base.Json{
"to": base.Json{"parent_id": dstDir.GetID()},
"ids": []string{srcObj.GetID()},
}
}
_, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&js)
r.SetQueryParams(params)
}, nil)
return err
}
func (xc *XunLeiBrowserCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
params := map[string]string{
"space": "SPACE_BROWSER",
}
// 对 "迅雷云盘" 内的文件 特殊处理
if srcObj.GetPath() == ThunderDriveFileID {
params = map[string]string{}
} else if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
// 对 "超级保险箱" 内的文件 特殊处理
params = map[string]string{
"space": "SPACE_BROWSER_SAFE",
}
}
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodPatch, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", srcObj.GetID())
r.SetBody(&base.Json{"name": newName})
r.SetQueryParams(params)
}, nil)
return err
}
func (xc *XunLeiBrowserCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
srcSpace := "SPACE_BROWSER"
dstSpace := "SPACE_BROWSER"
// 对 "超级保险箱" 内的文件 特殊处理
if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
srcSpace = "SPACE_BROWSER_SAFE"
}
if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
dstSpace = "SPACE_BROWSER_SAFE"
}
params := map[string]string{
"_from": dstSpace,
}
js := base.Json{
"to": base.Json{"parent_id": dstDir.GetID(), "space": dstSpace},
"space": srcSpace,
"ids": []string{srcObj.GetID()},
}
// 对 "迅雷云盘" 内的文件 特殊处理
if srcObj.GetPath() == ThunderDriveFileID {
params = map[string]string{}
js = base.Json{
"to": base.Json{"parent_id": dstDir.GetID()},
"ids": []string{srcObj.GetID()},
}
}
_, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&js)
r.SetQueryParams(params)
}, nil)
return err
}
func (xc *XunLeiBrowserCommon) Remove(ctx context.Context, obj model.Obj) error {
js := base.Json{
"ids": []string{obj.GetID()},
"space": "SPACE_BROWSER",
}
// 对 "迅雷云盘" 内的文件 特殊处理
if obj.GetPath() == ThunderDriveFileID {
js = base.Json{
"ids": []string{obj.GetID()},
}
} else if obj.GetPath() == ThunderBrowserDriveSafeFileID {
// 对 "超级保险箱" 内的文件 特殊处理
js = base.Json{
"ids": []string{obj.GetID()},
"space": "SPACE_BROWSER_SAFE",
}
}
// 先判断是否是特殊情况
if obj.GetPath() == ThunderDriveFileID {
_, err := xc.Request(FILE_API_URL+"/{fileID}/trash", http.MethodPatch, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", obj.GetID())
r.SetBody("{}")
}, nil)
return err
} else if obj.GetPath() == ThunderBrowserDriveSafeFileID {
_, err := xc.Request(FILE_API_URL+":batchDelete", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&js)
}, nil)
return err
}
// 根据用户选择的删除方式进行删除
if xc.RemoveWay == "delete" {
_, err := xc.Request(FILE_API_URL+":batchDelete", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&js)
}, nil)
return err
} else {
_, err := xc.Request(FILE_API_URL+":batchTrash", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&js)
}, nil)
return err
}
}
func (xc *XunLeiBrowserCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
hi := stream.GetHash()
gcid := hi.GetHash(hash_extend.GCID)
if len(gcid) < hash_extend.GCID.Width {
tFile, err := stream.CacheFullInTempFile()
if err != nil {
return err
}
gcid, err = utils.HashFile(hash_extend.GCID, tFile, stream.GetSize())
if err != nil {
return err
}
}
js := base.Json{
"kind": FILE,
"parent_id": dstDir.GetID(),
"name": stream.GetName(),
"size": stream.GetSize(),
"hash": gcid,
"upload_type": UPLOAD_TYPE_RESUMABLE,
"space": "SPACE_BROWSER",
}
// 对 "迅雷云盘" 内的文件 特殊处理
if dstDir.GetPath() == ThunderDriveFileID {
js = base.Json{
"kind": FILE,
"parent_id": dstDir.GetID(),
"name": stream.GetName(),
"size": stream.GetSize(),
"hash": gcid,
"upload_type": UPLOAD_TYPE_RESUMABLE,
}
} else if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
// 对 "超级保险箱" 内的文件 特殊处理
js = base.Json{
"kind": FILE,
"parent_id": dstDir.GetID(),
"name": stream.GetName(),
"size": stream.GetSize(),
"hash": gcid,
"upload_type": UPLOAD_TYPE_RESUMABLE,
"space": "SPACE_BROWSER_SAFE",
}
}
var resp UploadTaskResponse
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&js)
}, &resp)
if err != nil {
return err
}
param := resp.Resumable.Params
if resp.UploadType == UPLOAD_TYPE_RESUMABLE {
param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".")
s, err := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),
Region: aws.String("xunlei"),
Endpoint: aws.String(param.Endpoint),
})
if err != nil {
return err
}
uploader := s3manager.NewUploader(s)
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
}
_, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{
Bucket: aws.String(param.Bucket),
Key: aws.String(param.Key),
Expires: aws.Time(param.Expiration),
Body: stream,
})
return err
}
return nil
}
func (xc *XunLeiBrowserCommon) getFiles(ctx context.Context, folderId string, path string) ([]model.Obj, error) {
files := make([]model.Obj, 0)
var pageToken string
for {
var fileList FileList
folderSpace := "SPACE_BROWSER"
params := map[string]string{
"parent_id": folderId,
"page_token": pageToken,
"space": folderSpace,
"filters": `{"trashed":{"eq":false}}`,
"with_audit": "true",
"thumbnail_size": "SIZE_LARGE",
}
var fileType int8
// 处理特殊目录 “迅雷云盘” 设置特殊的 params 以便正常访问
pattern1 := fmt.Sprintf(`^/.*/%s(/.*)?$`, ThunderDriveFolderName)
thunderDriveMatch, _ := regexp.MatchString(pattern1, path)
// 处理特殊目录 “超级保险箱” 设置特殊的 params 以便正常访问
pattern2 := fmt.Sprintf(`^/.*/%s(/.*)?$`, ThunderBrowserDriveSafeFolderName)
thunderBrowserDriveSafeMatch, _ := regexp.MatchString(pattern2, path)
// 如果是 "迅雷云盘" 内的
if folderId == ThunderDriveFileID || thunderDriveMatch {
params = map[string]string{
"space": "",
"__type": "drive",
"refresh": "true",
"__sync": "true",
"parent_id": folderId,
"page_token": pageToken,
"with_audit": "true",
"limit": "100",
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
}
// 如果不是 "迅雷云盘"的"根目录"
if folderId == ThunderDriveFileID {
params["parent_id"] = ""
}
fileType = ThunderDriveType
} else if thunderBrowserDriveSafeMatch {
// 如果是 "超级保险箱" 内的
fileType = ThunderBrowserDriveSafeType
params["space"] = "SPACE_BROWSER_SAFE"
}
_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(params)
}, &fileList)
if err != nil {
return nil, err
}
// 对文件夹也进行处理
fileList.FolderType = fileType
for i := 0; i < len(fileList.Files); i++ {
file := &fileList.Files[i]
// 标记 文件夹内的文件
file.FileType = fileList.FolderType
// 解决 "迅雷云盘" 重复出现问题————迅雷后端发送错误
if file.Name == ThunderDriveFolderName && file.ID == "" && file.FolderType == ThunderDriveFolderType && folderId != "" {
continue
}
// 处理特殊目录 “迅雷云盘” 设置特殊的文件夹ID
if file.Name == ThunderDriveFolderName && file.ID == "" && file.FolderType == ThunderDriveFolderType {
file.ID = ThunderDriveFileID
} else if file.Name == ThunderBrowserDriveSafeFolderName && file.FolderType == ThunderBrowserDriveSafeFolderType {
file.FileType = ThunderBrowserDriveSafeType
}
files = append(files, file)
}
if fileList.NextPageToken == "" {
break
}
pageToken = fileList.NextPageToken
}
return files, nil
}
// SetRefreshTokenFunc 设置刷新Token的方法
func (xc *XunLeiBrowserCommon) SetRefreshTokenFunc(fn func() error) {
xc.refreshTokenFunc = fn
}
// SetTokenResp 设置Token
func (xc *XunLeiBrowserCommon) SetTokenResp(tr *TokenResp) {
xc.TokenResp = tr
}
// SetSpaceTokenResp 设置Token
func (xc *XunLeiBrowserCommon) SetSpaceTokenResp(spaceToken string) {
xc.TokenResp.Token = spaceToken
}
// Request 携带Authorization和CaptchaToken的请求
func (xc *XunLeiBrowserCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
data, err := xc.Common.Request(url, method, func(req *resty.Request) {
req.SetHeaders(map[string]string{
"Authorization": xc.GetToken(),
"X-Captcha-Token": xc.GetCaptchaToken(),
"X-Space-Authorization": xc.GetSpaceToken(),
})
if callback != nil {
callback(req)
}
}, resp)
errResp, ok := err.(*ErrResp)
if !ok {
return nil, err
}
switch errResp.ErrorCode {
case 0:
return data, nil
case 4122, 4121, 10, 16:
if xc.refreshTokenFunc != nil {
if err = xc.refreshTokenFunc(); err == nil {
break
}
}
return nil, err
case 9:
// space_token 获取失败
if errResp.ErrorMsg == "space_token_invalid" {
if token, err := xc.GetSafeAccessToken(xc.Token); err != nil {
return nil, err
} else {
xc.SetSpaceTokenResp(token)
}
}
if errResp.ErrorMsg == "captcha_invalid" {
// 验证码token过期
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {
return nil, err
}
}
return nil, err
default:
return nil, err
}
return xc.Request(url, method, callback, resp)
}
// RefreshToken 刷新Token
func (xc *XunLeiBrowserCommon) RefreshToken(refreshToken string) (*TokenResp, error) {
var resp TokenResp
_, err := xc.Common.Request(XLUSER_API_URL+"/auth/token", http.MethodPost, func(req *resty.Request) {
req.SetBody(&base.Json{
"grant_type": "refresh_token",
"refresh_token": refreshToken,
"client_id": xc.ClientID,
"client_secret": xc.ClientSecret,
})
}, &resp)
if err != nil {
return nil, err
}
if resp.RefreshToken == "" {
return nil, errors.New("refresh token is empty")
}
return &resp, nil
}
// GetSafeAccessToken 获取 超级保险柜 AccessToken
func (xc *XunLeiBrowserCommon) GetSafeAccessToken(safePassword string) (string, error) {
var resp TokenResp
_, err := xc.Request(XLUSER_API_URL+"/password/check", http.MethodPost, func(req *resty.Request) {
req.SetBody(&base.Json{
"scene": "box",
"password": EncryptPassword(safePassword),
})
}, &resp)
if err != nil {
return "", err
}
if resp.Token == "" {
return "", errors.New("SafePassword is incorrect ")
}
return resp.Token, nil
}
// Login 登录
func (xc *XunLeiBrowserCommon) Login(username, password string) (*TokenResp, error) {
url := XLUSER_API_URL + "/auth/signin"
err := xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username)
if err != nil {
return nil, err
}
var resp TokenResp
_, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(&SignInRequest{
CaptchaToken: xc.GetCaptchaToken(),
ClientID: xc.ClientID,
ClientSecret: xc.ClientSecret,
Username: username,
Password: password,
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (xc *XunLeiBrowserCommon) IsLogin() bool {
if xc.TokenResp == nil {
return false
}
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
return err == nil
}

View File

@ -0,0 +1,108 @@
package thunder_browser
import (
"crypto/md5"
"encoding/hex"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
)
// ExpertAddition 高级设置
type ExpertAddition struct {
driver.RootID
LoginType string `json:"login_type" type:"select" options:"user,refresh_token" default:"user"`
SignType string `json:"sign_type" type:"select" options:"algorithms,captcha_sign" default:"algorithms"`
// 登录方式1
Username string `json:"username" required:"true" help:"login type is user,this is required"`
Password string `json:"password" required:"true" help:"login type is user,this is required"`
// 登录方式2
RefreshToken string `json:"refresh_token" required:"true" help:"login type is refresh_token,this is required"`
SafePassword string `json:"safe_password" required:"true" help:"super safe password"` // 超级保险箱密码
// 签名方法1
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"p+ExqPV,LwdwKlprzv7cQBQmxN5,vc08P1NwUBnbGsl58LzTW,VVNeXaXmZ8HH1SJEnp6YpVFSFU,pNAOJ,CNChvyDehAmUR1TDodfOusBAx,MS98NnX4Np8nxvEh6Ulv+SMMKMzKvD34C7lGWbb,9MpFF21GnVOYku0NM9Y/hzsK471UCUZ2o+,EY1QfeA06fXlw9wZNoZaXEED5zZPvNWI,,sciE,FIPqgQDUUW1e0GkiBFd5w7mCQ,zW,75XFdEO0Gi"`
// 签名方法2
CaptchaSign string `json:"captcha_sign" required:"true" help:"sign type is captcha_sign,this is required"`
Timestamp string `json:"timestamp" required:"true" help:"sign type is captcha_sign,this is required"`
// 验证码
CaptchaToken string `json:"captcha_token"`
// 必要且影响登录,由签名决定
DeviceID string `json:"device_id" required:"false" default:""`
ClientID string `json:"client_id" required:"true" default:"ZUBzD9J_XPXfn7f7"`
ClientSecret string `json:"client_secret" required:"true" default:"yESVmHecEe6F0aou69vl-g"`
ClientVersion string `json:"client_version" required:"true" default:"1.0.8.2215"`
PackageName string `json:"package_name" required:"true" default:"com.xunlei.browser"`
// 不影响登录,影响下载速度
UserAgent string `json:"user_agent" required:"false" default:""`
DownloadUserAgent string `json:"download_user_agent" required:"false" default:""`
// 优先使用视频链接代替下载链接
UseVideoUrl bool `json:"use_video_url"`
// 移除方式
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
}
// GetIdentity 登录特征,用于判断是否重新登录
func (i *ExpertAddition) GetIdentity() string {
hash := md5.New()
if i.LoginType == "refresh_token" {
hash.Write([]byte(i.RefreshToken))
} else {
hash.Write([]byte(i.Username + i.Password))
}
if i.SignType == "captcha_sign" {
hash.Write([]byte(i.CaptchaSign + i.Timestamp))
} else {
hash.Write([]byte(i.Algorithms))
}
hash.Write([]byte(i.DeviceID))
hash.Write([]byte(i.ClientID))
hash.Write([]byte(i.ClientSecret))
hash.Write([]byte(i.ClientVersion))
hash.Write([]byte(i.PackageName))
return hex.EncodeToString(hash.Sum(nil))
}
type Addition struct {
driver.RootID
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
SafePassword string `json:"safe_password" required:"true"` // 超级保险箱密码
CaptchaToken string `json:"captcha_token"`
UseVideoUrl bool `json:"use_video_url" default:"false"`
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
}
// GetIdentity 登录特征,用于判断是否重新登录
func (i *Addition) GetIdentity() string {
return utils.GetMD5EncodeStr(i.Username + i.Password)
}
var config = driver.Config{
Name: "ThunderBrowser",
LocalSort: true,
}
var configExpert = driver.Config{
Name: "ThunderBrowserExpert",
LocalSort: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &ThunderBrowser{}
})
op.RegisterDriver(func() driver.Driver {
return &ThunderBrowserExpert{}
})
}

View File

@ -0,0 +1,223 @@
package thunder_browser
import (
"fmt"
"strconv"
"time"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
)
type ErrResp struct {
ErrorCode int64 `json:"error_code"`
ErrorMsg string `json:"error"`
ErrorDescription string `json:"error_description"`
// ErrorDetails interface{} `json:"error_details"`
}
func (e *ErrResp) IsError() bool {
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
}
func (e *ErrResp) Error() string {
return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)
}
/*
* 验证码Token
**/
type CaptchaTokenRequest struct {
Action string `json:"action"`
CaptchaToken string `json:"captcha_token"`
ClientID string `json:"client_id"`
DeviceID string `json:"device_id"`
Meta map[string]string `json:"meta"`
RedirectUri string `json:"redirect_uri"`
}
type CaptchaTokenResponse struct {
CaptchaToken string `json:"captcha_token"`
ExpiresIn int64 `json:"expires_in"`
Url string `json:"url"`
}
/*
* 登录
**/
type TokenResp struct {
TokenType string `json:"token_type"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
Sub string `json:"sub"`
UserID string `json:"user_id"`
Token string `json:"token"` // "超级保险箱" 访问Token
}
func (t *TokenResp) GetToken() string {
return fmt.Sprint(t.TokenType, " ", t.AccessToken)
}
// GetSpaceToken 获取"超级保险箱" 访问Token
func (t *TokenResp) GetSpaceToken() string {
return t.Token
}
type SignInRequest struct {
CaptchaToken string `json:"captcha_token"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Username string `json:"username"`
Password string `json:"password"`
}
/*
* 文件
**/
type FileList struct {
Kind string `json:"kind"`
NextPageToken string `json:"next_page_token"`
Files []Files `json:"files"`
Version string `json:"version"`
VersionOutdated bool `json:"version_outdated"`
FolderType int8
}
type Link struct {
URL string `json:"url"`
Token string `json:"token"`
Expire time.Time `json:"expire"`
Type string `json:"type"`
}
var _ model.Obj = (*Files)(nil)
type Files struct {
Kind string `json:"kind"`
ID string `json:"id"`
ParentID string `json:"parent_id"`
Name string `json:"name"`
//UserID string `json:"user_id"`
Size string `json:"size"`
//Revision string `json:"revision"`
//FileExtension string `json:"file_extension"`
//MimeType string `json:"mime_type"`
//Starred bool `json:"starred"`
WebContentLink string `json:"web_content_link"`
CreatedTime CustomTime `json:"created_time"`
ModifiedTime CustomTime `json:"modified_time"`
IconLink string `json:"icon_link"`
ThumbnailLink string `json:"thumbnail_link"`
// Md5Checksum string `json:"md5_checksum"`
Hash string `json:"hash"`
// Links map[string]Link `json:"links"`
// Phase string `json:"phase"`
// Audit struct {
// Status string `json:"status"`
// Message string `json:"message"`
// Title string `json:"title"`
// } `json:"audit"`
Medias []struct {
//Category string `json:"category"`
//IconLink string `json:"icon_link"`
//IsDefault bool `json:"is_default"`
//IsOrigin bool `json:"is_origin"`
//IsVisible bool `json:"is_visible"`
Link Link `json:"link"`
//MediaID string `json:"media_id"`
//MediaName string `json:"media_name"`
//NeedMoreQuota bool `json:"need_more_quota"`
//Priority int `json:"priority"`
//RedirectLink string `json:"redirect_link"`
//ResolutionName string `json:"resolution_name"`
// Video struct {
// AudioCodec string `json:"audio_codec"`
// BitRate int `json:"bit_rate"`
// Duration int `json:"duration"`
// FrameRate int `json:"frame_rate"`
// Height int `json:"height"`
// VideoCodec string `json:"video_codec"`
// VideoType string `json:"video_type"`
// Width int `json:"width"`
// } `json:"video"`
// VipTypes []string `json:"vip_types"`
} `json:"medias"`
Trashed bool `json:"trashed"`
DeleteTime string `json:"delete_time"`
OriginalURL string `json:"original_url"`
//Params struct{} `json:"params"`
//OriginalFileIndex int `json:"original_file_index"`
//Space string `json:"space"`
//Apps []interface{} `json:"apps"`
//Writable bool `json:"writable"`
FolderType string `json:"folder_type"`
//Collection interface{} `json:"collection"`
FileType int8
}
func (c *Files) GetHash() utils.HashInfo {
return utils.NewHashInfo(hash_extend.GCID, c.Hash)
}
func (c *Files) GetSize() int64 { size, _ := strconv.ParseInt(c.Size, 10, 64); return size }
func (c *Files) GetName() string { return c.Name }
func (c *Files) CreateTime() time.Time { return c.CreatedTime.Time }
func (c *Files) ModTime() time.Time { return c.ModifiedTime.Time }
func (c *Files) IsDir() bool { return c.Kind == FOLDER }
func (c *Files) GetID() string { return c.ID }
func (c *Files) GetPath() string {
// 对特殊文件进行特殊处理
if c.FileType == ThunderDriveType {
return ThunderDriveFileID
} else if c.FileType == ThunderBrowserDriveSafeType {
return ThunderBrowserDriveSafeFileID
}
return ""
}
func (c *Files) Thumb() string { return c.ThumbnailLink }
/*
* 上传
**/
type UploadTaskResponse struct {
UploadType string `json:"upload_type"`
/*//UPLOAD_TYPE_FORM
Form struct {
//Headers struct{} `json:"headers"`
Kind string `json:"kind"`
Method string `json:"method"`
MultiParts struct {
OSSAccessKeyID string `json:"OSSAccessKeyId"`
Signature string `json:"Signature"`
Callback string `json:"callback"`
Key string `json:"key"`
Policy string `json:"policy"`
XUserData string `json:"x:user_data"`
} `json:"multi_parts"`
URL string `json:"url"`
} `json:"form"`*/
//UPLOAD_TYPE_RESUMABLE
Resumable struct {
Kind string `json:"kind"`
Params struct {
AccessKeyID string `json:"access_key_id"`
AccessKeySecret string `json:"access_key_secret"`
Bucket string `json:"bucket"`
Endpoint string `json:"endpoint"`
Expiration time.Time `json:"expiration"`
Key string `json:"key"`
SecurityToken string `json:"security_token"`
} `json:"params"`
Provider string `json:"provider"`
} `json:"resumable"`
File Files `json:"file"`
}

View File

@ -0,0 +1,318 @@
package thunder_browser
import (
"crypto/md5"
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
const (
API_URL = "https://x-api-pan.xunlei.com/drive/v1"
FILE_API_URL = API_URL + "/files"
XLUSER_API_URL = "https://xluser-ssl.xunlei.com/v1"
)
var Algorithms = []string{
"p+ExqPV",
"LwdwKlprzv7cQBQmxN5",
"vc08P1NwUBnbGsl58LzTW",
"VVNeXaXmZ8HH1SJEnp6YpVFSFU",
"pNAOJ",
"CNChvyDehAmUR1TDodfOusBAx",
"MS98NnX4Np8nxvEh6Ulv+SMMKMzKvD34C7lGWbb",
"9MpFF21GnVOYku0NM9Y/hzsK471UCUZ2o+",
"EY1QfeA06fXlw9wZNoZaXEED5zZPvNWI",
"",
"sciE",
"FIPqgQDUUW1e0GkiBFd5w7mCQ",
"zW",
"75XFdEO0Gi",
}
const (
ClientID = "ZUBzD9J_XPXfn7f7"
ClientSecret = "yESVmHecEe6F0aou69vl-g"
ClientVersion = "1.0.8.2215"
PackageName = "com.xunlei.browser"
DownloadUserAgent = "AndroidDownloadManager/13 (Linux; U; Android 13; M2004J7AC Build/SP1A.210812.016)"
SdkVersion = "2.0.3.262"
)
const (
FOLDER = "drive#folder"
FILE = "drive#file"
RESUMABLE = "drive#resumable"
)
const (
UPLOAD_TYPE_UNKNOWN = "UPLOAD_TYPE_UNKNOWN"
//UPLOAD_TYPE_FORM = "UPLOAD_TYPE_FORM"
UPLOAD_TYPE_RESUMABLE = "UPLOAD_TYPE_RESUMABLE"
UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL"
)
const (
ThunderDriveFileID = "XXXXXXXXXXXXXXXXXXXXXXXXXX"
ThunderBrowserDriveSafeFileID = "YYYYYYYYYYYYYYYYYYYYYYYYYY"
ThunderDriveFolderName = "迅雷云盘"
ThunderBrowserDriveSafeFolderName = "超级保险箱"
ThunderDriveType = 1
ThunderBrowserDriveSafeType = 2
ThunderDriveFolderType = "DEFAULT_ROOT"
ThunderBrowserDriveSafeFolderType = "BROWSER_SAFE"
)
func GetAction(method string, url string) string {
urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(url)[1]
return method + ":" + urlpath
}
type Common struct {
client *resty.Client
captchaToken string
// 签名相关,二选一
Algorithms []string
Timestamp, CaptchaSign string
// 必要值,签名相关
DeviceID string
ClientID string
ClientSecret string
ClientVersion string
PackageName string
UserAgent string
DownloadUserAgent string
UseVideoUrl bool
RemoveWay string
// 验证码token刷新成功回调
refreshCTokenCk func(token string)
}
func (c *Common) SetDeviceID(deviceID string) {
c.DeviceID = deviceID
}
func (c *Common) SetCaptchaToken(captchaToken string) {
c.captchaToken = captchaToken
}
func (c *Common) GetCaptchaToken() string {
return c.captchaToken
}
// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
func (c *Common) RefreshCaptchaTokenAtLogin(action, userID string) error {
metas := map[string]string{
"client_version": c.ClientVersion,
"package_name": c.PackageName,
"user_id": userID,
}
metas["timestamp"], metas["captcha_sign"] = c.GetCaptchaSign()
return c.refreshCaptchaToken(action, metas)
}
// RefreshCaptchaTokenInLogin 刷新验证码token(登录时)
func (c *Common) RefreshCaptchaTokenInLogin(action, username string) error {
metas := make(map[string]string)
if ok, _ := regexp.MatchString(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`, username); ok {
metas["email"] = username
} else if len(username) >= 11 && len(username) <= 18 {
metas["phone_number"] = username
} else {
metas["username"] = username
}
return c.refreshCaptchaToken(action, metas)
}
// GetCaptchaSign 获取验证码签名
func (c *Common) GetCaptchaSign() (timestamp, sign string) {
if len(c.Algorithms) == 0 {
return c.Timestamp, c.CaptchaSign
}
timestamp = fmt.Sprint(time.Now().UnixMilli())
str := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)
for _, algorithm := range c.Algorithms {
str = utils.GetMD5EncodeStr(str + algorithm)
}
sign = "1." + str
return
}
// 刷新验证码token
func (c *Common) refreshCaptchaToken(action string, metas map[string]string) error {
param := CaptchaTokenRequest{
Action: action,
CaptchaToken: c.captchaToken,
ClientID: c.ClientID,
DeviceID: c.DeviceID,
Meta: metas,
RedirectUri: "xlaccsdk01://xunlei.com/callback?state=harbor",
}
var e ErrResp
var resp CaptchaTokenResponse
_, err := c.Request(XLUSER_API_URL+"/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
req.SetError(&e).SetBody(param)
}, &resp)
if err != nil {
return err
}
if e.IsError() {
return &e
}
if resp.Url != "" {
return fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
}
if resp.CaptchaToken == "" {
return fmt.Errorf("empty captchaToken")
}
if c.refreshCTokenCk != nil {
c.refreshCTokenCk(resp.CaptchaToken)
}
c.SetCaptchaToken(resp.CaptchaToken)
return nil
}
// Request 只有基础信息的请求
func (c *Common) Request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := c.client.R().SetHeaders(map[string]string{
"user-agent": c.UserAgent,
"accept": "application/json;charset=UTF-8",
"x-device-id": c.DeviceID,
"x-client-id": c.ClientID,
"x-client-version": c.ClientVersion,
})
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
var erron ErrResp
utils.Json.Unmarshal(res.Body(), &erron)
if erron.IsError() {
return nil, &erron
}
return res.Body(), nil
}
// 计算文件Gcid
func getGcid(r io.Reader, size int64) (string, error) {
calcBlockSize := func(j int64) int64 {
var psize int64 = 0x40000
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
psize = psize << 1
}
return psize
}
hash1 := sha1.New()
hash2 := sha1.New()
readSize := calcBlockSize(size)
for {
hash2.Reset()
if n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {
if err != io.EOF {
return "", err
}
break
}
hash1.Write(hash2.Sum(nil))
}
return hex.EncodeToString(hash1.Sum(nil)), nil
}
type CustomTime struct {
time.Time
}
const timeFormat = time.RFC3339
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
str := string(b)
if str == `""` {
*ct = CustomTime{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}
return nil
}
t, err := time.Parse(`"`+timeFormat+`"`, str)
if err != nil {
return err
}
*ct = CustomTime{Time: t}
return nil
}
// EncryptPassword 超级保险箱 加密
func EncryptPassword(password string) string {
if password == "" {
return ""
}
// 将字符串转换为字节数组
byteData := []byte(password)
// 计算MD5哈希值
hash := md5.Sum(byteData)
// 将哈希值转换为十六进制字符串
return hex.EncodeToString(hash[:])
}
func generateDeviceSign(deviceID, packageName string) string {
signatureBase := fmt.Sprintf("%s%s%s%s", deviceID, packageName, "22062", "a5d7416858147a4ab99573872ffccef8")
sha1Hash := sha1.New()
sha1Hash.Write([]byte(signatureBase))
sha1Result := sha1Hash.Sum(nil)
sha1String := hex.EncodeToString(sha1Result)
md5Hash := md5.New()
md5Hash.Write([]byte(sha1String))
md5Result := md5Hash.Sum(nil)
md5String := hex.EncodeToString(md5Result)
deviceSign := fmt.Sprintf("div101.%s%s", deviceID, md5String)
return deviceSign
}
func BuildCustomUserAgent(deviceID, appName, sdkVersion, clientVersion, packageName string) string {
//deviceSign := generateDeviceSign(deviceID, packageName)
var sb strings.Builder
sb.WriteString(fmt.Sprintf("ANDROID-%s/%s ", appName, clientVersion))
sb.WriteString("networkType/WIFI ")
sb.WriteString(fmt.Sprintf("appid/%s ", "22062"))
sb.WriteString(fmt.Sprintf("deviceName/Xiaomi_M2004j7ac "))
sb.WriteString(fmt.Sprintf("deviceModel/M2004J7AC "))
sb.WriteString(fmt.Sprintf("OSVersion/13 "))
sb.WriteString(fmt.Sprintf("protocolVersion/301 "))
sb.WriteString(fmt.Sprintf("platformversion/10 "))
sb.WriteString(fmt.Sprintf("sdkVersion/%s ", sdkVersion))
sb.WriteString(fmt.Sprintf("Oauth2Client/0.9 (Linux 4_9_337-perf-sn-uotan-gd9d488809c3d) (JAVA 0) "))
return sb.String()
}

558
drivers/thunderx/driver.go Normal file
View File

@ -0,0 +1,558 @@
package thunderx
import (
"context"
"fmt"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/go-resty/resty/v2"
"net/http"
"strings"
)
type ThunderX struct {
*XunLeiXCommon
model.Storage
Addition
identity string
}
func (x *ThunderX) Config() driver.Config {
return config
}
func (x *ThunderX) GetAddition() driver.Additional {
return &x.Addition
}
func (x *ThunderX) Init(ctx context.Context) (err error) {
// 初始化所需参数
if x.XunLeiXCommon == nil {
x.XunLeiXCommon = &XunLeiXCommon{
Common: &Common{
client: base.NewRestyClient(),
Algorithms: Algorithms,
DeviceID: utils.GetMD5EncodeStr(x.Username + x.Password),
ClientID: ClientID,
ClientSecret: ClientSecret,
ClientVersion: ClientVersion,
PackageName: PackageName,
UserAgent: BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, ""),
DownloadUserAgent: DownloadUserAgent,
UseVideoUrl: x.UseVideoUrl,
refreshCTokenCk: func(token string) {
x.CaptchaToken = token
op.MustSaveDriverStorage(x)
},
},
refreshTokenFunc: func() error {
// 通过RefreshToken刷新
token, err := x.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
// 重新登录
token, err = x.Login(x.Username, x.Password)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
if token.UserID != "" {
x.SetUserID(token.UserID)
x.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, token.UserID)
}
op.MustSaveDriverStorage(x)
}
}
x.SetTokenResp(token)
return err
},
}
}
// 自定义验证码token
ctoken := strings.TrimSpace(x.CaptchaToken)
if ctoken != "" {
x.SetCaptchaToken(ctoken)
}
if x.DeviceID == "" {
x.SetDeviceID(utils.GetMD5EncodeStr(x.Username + x.Password))
}
x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
x.Addition.RootFolderID = x.RootFolderID
// 防止重复登录
identity := x.GetIdentity()
if x.identity != identity || !x.IsLogin() {
x.identity = identity
// 登录
token, err := x.Login(x.Username, x.Password)
if err != nil {
return err
}
x.SetTokenResp(token)
if token.UserID != "" {
x.SetUserID(token.UserID)
x.UserAgent = BuildCustomUserAgent(x.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, token.UserID)
}
}
return nil
}
func (x *ThunderX) Drop(ctx context.Context) error {
return nil
}
type ThunderXExpert struct {
*XunLeiXCommon
model.Storage
ExpertAddition
identity string
}
func (x *ThunderXExpert) Config() driver.Config {
return configExpert
}
func (x *ThunderXExpert) GetAddition() driver.Additional {
return &x.ExpertAddition
}
func (x *ThunderXExpert) Init(ctx context.Context) (err error) {
// 防止重复登录
identity := x.GetIdentity()
if identity != x.identity || !x.IsLogin() {
x.identity = identity
x.XunLeiXCommon = &XunLeiXCommon{
Common: &Common{
client: base.NewRestyClient(),
DeviceID: func() string {
if len(x.DeviceID) != 32 {
if x.LoginType == "user" {
return utils.GetMD5EncodeStr(x.Username + x.Password)
}
return utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken)
}
return x.DeviceID
}(),
ClientID: x.ClientID,
ClientSecret: x.ClientSecret,
ClientVersion: x.ClientVersion,
PackageName: x.PackageName,
UserAgent: func() string {
if x.ExpertAddition.UserAgent != "" {
return x.ExpertAddition.UserAgent
}
if x.LoginType == "user" {
return BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, "")
}
return BuildCustomUserAgent(utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, "")
}(),
DownloadUserAgent: func() string {
if x.ExpertAddition.DownloadUserAgent != "" {
return x.ExpertAddition.DownloadUserAgent
}
return DownloadUserAgent
}(),
UseVideoUrl: x.UseVideoUrl,
refreshCTokenCk: func(token string) {
x.CaptchaToken = token
op.MustSaveDriverStorage(x)
},
},
}
if x.ExpertAddition.CaptchaToken != "" {
x.SetCaptchaToken(x.ExpertAddition.CaptchaToken)
op.MustSaveDriverStorage(x)
}
if x.Common.DeviceID != "" {
x.ExpertAddition.DeviceID = x.Common.DeviceID
op.MustSaveDriverStorage(x)
}
if x.Common.DownloadUserAgent != "" {
x.ExpertAddition.DownloadUserAgent = x.Common.DownloadUserAgent
op.MustSaveDriverStorage(x)
}
x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
x.ExpertAddition.RootFolderID = x.RootFolderID
// 签名方法
if x.SignType == "captcha_sign" {
x.Common.Timestamp = x.Timestamp
x.Common.CaptchaSign = x.CaptchaSign
} else {
x.Common.Algorithms = strings.Split(x.Algorithms, ",")
}
// 登录方式
if x.LoginType == "refresh_token" {
// 通过RefreshToken登录
token, err := x.XunLeiXCommon.RefreshToken(x.ExpertAddition.RefreshToken)
if err != nil {
return err
}
x.SetTokenResp(token)
// 刷新token方法
x.SetRefreshTokenFunc(func() error {
token, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
}
x.SetTokenResp(token)
op.MustSaveDriverStorage(x)
return err
})
} else {
// 通过用户密码登录
token, err := x.Login(x.Username, x.Password)
if err != nil {
return err
}
x.SetTokenResp(token)
x.SetRefreshTokenFunc(func() error {
token, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
token, err = x.Login(x.Username, x.Password)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
}
}
x.SetTokenResp(token)
op.MustSaveDriverStorage(x)
return err
})
}
// 更新 UserAgent
if x.TokenResp.UserID != "" {
x.ExpertAddition.UserAgent = BuildCustomUserAgent(x.ExpertAddition.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, x.TokenResp.UserID)
x.SetUserAgent(x.ExpertAddition.UserAgent)
op.MustSaveDriverStorage(x)
}
} else {
// 仅修改验证码token
if x.CaptchaToken != "" {
x.SetCaptchaToken(x.CaptchaToken)
}
x.XunLeiXCommon.UserAgent = x.ExpertAddition.UserAgent
x.XunLeiXCommon.DownloadUserAgent = x.ExpertAddition.UserAgent
x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
x.ExpertAddition.RootFolderID = x.RootFolderID
}
return nil
}
func (x *ThunderXExpert) Drop(ctx context.Context) error {
return nil
}
func (x *ThunderXExpert) SetTokenResp(token *TokenResp) {
x.XunLeiXCommon.SetTokenResp(token)
if token != nil {
x.ExpertAddition.RefreshToken = token.RefreshToken
}
}
type XunLeiXCommon struct {
*Common
*TokenResp // 登录信息
refreshTokenFunc func() error
}
func (xc *XunLeiXCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return xc.getFiles(ctx, dir.GetID())
}
func (xc *XunLeiXCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var lFile Files
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", file.GetID())
//r.SetQueryParam("space", "")
}, &lFile)
if err != nil {
return nil, err
}
link := &model.Link{
URL: lFile.WebContentLink,
Header: http.Header{
"User-Agent": {xc.DownloadUserAgent},
},
}
if xc.UseVideoUrl {
for _, media := range lFile.Medias {
if media.Link.URL != "" {
link.URL = media.Link.URL
break
}
}
}
/*
strs := regexp.MustCompile(`e=([0-9]*)`).FindStringSubmatch(lFile.WebContentLink)
if len(strs) == 2 {
timestamp, err := strconv.ParseInt(strs[1], 10, 64)
if err == nil {
expired := time.Duration(timestamp-time.Now().Unix()) * time.Second
link.Expiration = &expired
}
}
*/
return link, nil
}
func (xc *XunLeiXCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"kind": FOLDER,
"name": dirName,
"parent_id": parentDir.GetID(),
})
}, nil)
return err
}
func (xc *XunLeiXCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"to": base.Json{"parent_id": dstDir.GetID()},
"ids": []string{srcObj.GetID()},
})
}, nil)
return err
}
func (xc *XunLeiXCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodPatch, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", srcObj.GetID())
r.SetBody(&base.Json{"name": newName})
}, nil)
return err
}
func (xc *XunLeiXCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"to": base.Json{"parent_id": dstDir.GetID()},
"ids": []string{srcObj.GetID()},
})
}, nil)
return err
}
func (xc *XunLeiXCommon) Remove(ctx context.Context, obj model.Obj) error {
_, err := xc.Request(FILE_API_URL+"/{fileID}/trash", http.MethodPatch, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", obj.GetID())
r.SetBody("{}")
}, nil)
return err
}
func (xc *XunLeiXCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
hi := stream.GetHash()
gcid := hi.GetHash(hash_extend.GCID)
if len(gcid) < hash_extend.GCID.Width {
tFile, err := stream.CacheFullInTempFile()
if err != nil {
return err
}
gcid, err = utils.HashFile(hash_extend.GCID, tFile, stream.GetSize())
if err != nil {
return err
}
}
var resp UploadTaskResponse
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"kind": FILE,
"parent_id": dstDir.GetID(),
"name": stream.GetName(),
"size": stream.GetSize(),
"hash": gcid,
"upload_type": UPLOAD_TYPE_RESUMABLE,
})
}, &resp)
if err != nil {
return err
}
param := resp.Resumable.Params
if resp.UploadType == UPLOAD_TYPE_RESUMABLE {
param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".")
s, err := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),
Region: aws.String("xunlei"),
Endpoint: aws.String(param.Endpoint),
})
if err != nil {
return err
}
uploader := s3manager.NewUploader(s)
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
}
_, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{
Bucket: aws.String(param.Bucket),
Key: aws.String(param.Key),
Expires: aws.Time(param.Expiration),
Body: stream,
})
return err
}
return nil
}
func (xc *XunLeiXCommon) getFiles(ctx context.Context, folderId string) ([]model.Obj, error) {
files := make([]model.Obj, 0)
var pageToken string
for {
var fileList FileList
_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"space": "",
"__type": "drive",
"refresh": "true",
"__sync": "true",
"parent_id": folderId,
"page_token": pageToken,
"with_audit": "true",
"limit": "100",
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
})
}, &fileList)
if err != nil {
return nil, err
}
for i := 0; i < len(fileList.Files); i++ {
files = append(files, &fileList.Files[i])
}
if fileList.NextPageToken == "" {
break
}
pageToken = fileList.NextPageToken
}
return files, nil
}
// SetRefreshTokenFunc 设置刷新Token的方法
func (xc *XunLeiXCommon) SetRefreshTokenFunc(fn func() error) {
xc.refreshTokenFunc = fn
}
// SetTokenResp 设置Token
func (xc *XunLeiXCommon) SetTokenResp(tr *TokenResp) {
xc.TokenResp = tr
}
// Request 携带Authorization和CaptchaToken的请求
func (xc *XunLeiXCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
data, err := xc.Common.Request(url, method, func(req *resty.Request) {
req.SetHeaders(map[string]string{
"Authorization": xc.Token(),
"X-Captcha-Token": xc.GetCaptchaToken(),
})
if callback != nil {
callback(req)
}
}, resp)
errResp, ok := err.(*ErrResp)
if !ok {
return nil, err
}
switch errResp.ErrorCode {
case 0:
return data, nil
case 4122, 4121, 10, 16:
if xc.refreshTokenFunc != nil {
if err = xc.refreshTokenFunc(); err == nil {
break
}
}
return nil, err
case 9: // 验证码token过期
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {
return nil, err
}
default:
return nil, err
}
return xc.Request(url, method, callback, resp)
}
// RefreshToken 刷新Token
func (xc *XunLeiXCommon) RefreshToken(refreshToken string) (*TokenResp, error) {
var resp TokenResp
_, err := xc.Common.Request(XLUSER_API_URL+"/auth/token", http.MethodPost, func(req *resty.Request) {
req.SetBody(&base.Json{
"grant_type": "refresh_token",
"refresh_token": refreshToken,
"client_id": xc.ClientID,
"client_secret": xc.ClientSecret,
})
}, &resp)
if err != nil {
return nil, err
}
if resp.RefreshToken == "" {
return nil, errs.EmptyToken
}
resp.UserID = resp.Sub
return &resp, nil
}
// Login 登录
func (xc *XunLeiXCommon) Login(username, password string) (*TokenResp, error) {
url := XLUSER_API_URL + "/auth/signin"
err := xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username)
if err != nil {
return nil, err
}
var resp TokenResp
_, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(&SignInRequest{
CaptchaToken: xc.GetCaptchaToken(),
ClientID: xc.ClientID,
ClientSecret: xc.ClientSecret,
Username: username,
Password: password,
})
}, &resp)
if err != nil {
return nil, err
}
resp.UserID = resp.Sub
return &resp, nil
}
func (xc *XunLeiXCommon) IsLogin() bool {
if xc.TokenResp == nil {
return false
}
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
return err == nil
}

103
drivers/thunderx/meta.go Normal file
View File

@ -0,0 +1,103 @@
package thunderx
import (
"crypto/md5"
"encoding/hex"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
)
// 高级设置
type ExpertAddition struct {
driver.RootID
LoginType string `json:"login_type" type:"select" options:"user,refresh_token" default:"user"`
SignType string `json:"sign_type" type:"select" options:"algorithms,captcha_sign" default:"algorithms"`
// 登录方式1
Username string `json:"username" required:"true" help:"login type is user,this is required"`
Password string `json:"password" required:"true" help:"login type is user,this is required"`
// 登录方式2
RefreshToken string `json:"refresh_token" required:"true" help:"login type is refresh_token,this is required"`
// 签名方法1
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"kVy0WbPhiE4v6oxXZ88DvoA3Q,lON/AUoZKj8/nBtcE85mVbkOaVdVa,rLGffQrfBKH0BgwQ33yZofvO3Or,FO6HWqw,GbgvyA2,L1NU9QvIQIH7DTRt,y7llk4Y8WfYflt6,iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe,8C28RTXmVcco0,X5Xh,7xe25YUgfGgD0xW3ezFS,,CKCR,8EmDjBo6h3eLaK7U6vU2Qys0NsMx,t2TeZBXKqbdP09Arh9C3"`
// 签名方法2
CaptchaSign string `json:"captcha_sign" required:"true" help:"sign type is captcha_sign,this is required"`
Timestamp string `json:"timestamp" required:"true" help:"sign type is captcha_sign,this is required"`
// 验证码
CaptchaToken string `json:"captcha_token"`
// 必要且影响登录,由签名决定
DeviceID string `json:"device_id" required:"false" default:""`
ClientID string `json:"client_id" required:"true" default:"ZQL_zwA4qhHcoe_2"`
ClientSecret string `json:"client_secret" required:"true" default:"Og9Vr1L8Ee6bh0olFxFDRg"`
ClientVersion string `json:"client_version" required:"true" default:"1.06.0.2132"`
PackageName string `json:"package_name" required:"true" default:"com.thunder.downloader"`
////不影响登录,影响下载速度
UserAgent string `json:"user_agent" required:"false" default:""`
DownloadUserAgent string `json:"download_user_agent" required:"false" default:""`
//优先使用视频链接代替下载链接
UseVideoUrl bool `json:"use_video_url"`
}
// 登录特征,用于判断是否重新登录
func (i *ExpertAddition) GetIdentity() string {
hash := md5.New()
if i.LoginType == "refresh_token" {
hash.Write([]byte(i.RefreshToken))
} else {
hash.Write([]byte(i.Username + i.Password))
}
if i.SignType == "captcha_sign" {
hash.Write([]byte(i.CaptchaSign + i.Timestamp))
} else {
hash.Write([]byte(i.Algorithms))
}
hash.Write([]byte(i.DeviceID))
hash.Write([]byte(i.ClientID))
hash.Write([]byte(i.ClientSecret))
hash.Write([]byte(i.ClientVersion))
hash.Write([]byte(i.PackageName))
return hex.EncodeToString(hash.Sum(nil))
}
type Addition struct {
driver.RootID
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
CaptchaToken string `json:"captcha_token"`
UseVideoUrl bool `json:"use_video_url" default:"true"`
}
// 登录特征,用于判断是否重新登录
func (i *Addition) GetIdentity() string {
return utils.GetMD5EncodeStr(i.Username + i.Password)
}
var config = driver.Config{
Name: "ThunderX",
LocalSort: true,
OnlyProxy: false,
}
var configExpert = driver.Config{
Name: "ThunderXExpert",
LocalSort: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &ThunderX{}
})
op.RegisterDriver(func() driver.Driver {
return &ThunderXExpert{}
})
}

Some files were not shown because too many files have changed in this diff Show More