Compare commits

...

266 Commits

Author SHA1 Message Date
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
cf08aa3668 feat: add doge driver (#6201)
* feat: add doge driver

* doc: 补充readme文档

* fix: 对齐meta信息

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

* perf: merge to s3

* Rename goge.go to doge.go

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-03-25 22:53:44 +08:00
9c84b6596f feat: stand-alone port s3 server (#6242)
* feat: single port s3 server

* fix: unable to PUT files if not in root dir
2024-03-24 15:16:00 +08:00
022e0ca292 fix(139): incorrect refreshTokenResp serialization (#6248) 2024-03-24 11:04:55 +08:00
88947f6676 fix(ipfs): url escape filename (#6245 close #6027)
This resolves #6027
2024-03-24 11:03:18 +08:00
Mmx
b07ddfbc13 fix(ci): replace dockerfile tag step may have no effect (#6206) 2024-03-13 15:11:21 +08:00
9a0a63d34c fix(ilanzou): add referer to request header (close #6171) 2024-03-11 20:30:22 +08:00
195c869272 feat(139): refresh token periodically (#6146)
* 139定时刷新token

* fix build fail
2024-03-11 20:10:26 +08:00
bdfc1591bd fix: webauthn logspam (#6181) 2024-03-10 16:48:25 +08:00
82222840fe fix(deps): update golang.org/x/exp digest to 814bf88 (#6144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-09 14:58:36 +08:00
45e009a22c fix(mopan): upload error (close #6158 in #6166) 2024-03-09 14:54:49 +08:00
ac68079a76 feat(seafile): improve features, support access to encrypted library, etc (#6160) 2024-03-08 15:33:42 +08:00
2a17d0c2cd fix: settings reset to default after restart if set to empty (close #6143) 2024-03-05 16:29:26 +08:00
6f6a8e6dfc fix(deps): update github.com/t3rm1n4l/go-mega digest to d494b6a (#6081)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-04 15:12:22 +08:00
7d9ecba99c fix: add m3u8 to default video types (close #6142) 2024-03-04 14:26:00 +08:00
ae6984714d fix: remove default polyfill (#6130 close #6100)
* refactor(setting): replace `polyfill.io``

* fix: remove default polyfill

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-03-02 15:36:28 +08:00
d0f88bd1cb feat: s3 server support (#6088 close #5186)
Currently tested: List, Get, Remove
2024-03-02 15:35:10 +08:00
f8b1f87a5f fix: support for Microsoft WebDAV (#6133 close #6104)
* Add support for Microsoft WebDAV

* add import
2024-03-02 14:59:55 +08:00
71e4e1ab6e fix(chaoxing): json cannot unmarshal content.uploadDate (close #6119 in #6124) 2024-03-01 13:37:09 +08:00
7e6522c81e ci: build ffmpeg image with dev version 2024-02-24 18:10:45 +08:00
94a80bccfe fix(feiji): unable to get link (close #6082) 2024-02-24 18:04:08 +08:00
e66abb3f58 fix(deps): update module github.com/aws/aws-sdk-go to v1.50.24 (#5873)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 20:56:14 +08:00
742335f80e fix: don't push docker on pr due to security 2024-02-23 15:42:52 +08:00
f1979a8bbc feat(search): search with meilisearch (#6060)
* feat(search): search with meilisearch.

* feat(search): meilisearch supports auto update.

* chores: remove utils.Log.

* fix(search): the null pointer caused by deleting non-existing file/folder indexes.

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-02-23 15:37:40 +08:00
1f835502ba feat: support customize dsn for mysql and pg (#6031)
* support for unixsocket to connect to mysql

* feat: customize dsn for mysql and pg

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-02-23 15:28:48 +08:00
424ab2d0c0 ci: remove docker latest tag on dev 2024-02-21 15:50:05 +08:00
858ba19670 ci: also push docker to hub for pr 2024-02-21 14:58:45 +08:00
Mmx
0c7e47a76c feat: add docker image with pre-installed ffmpeg (#6054)
* build: add dockerfile for ffmpeg version

* ci: add docker image with ffmpeg release

* fix: donnot push on docker build test
2024-02-21 14:04:22 +08:00
53926d5cd0 fix(search): duplicate folder on autoupdate (#6063 close #6062)
* fix(search): the problem of not returning in time when index does not support auto update.

* fix(search): the problem of duplicate indexing of folders.
2024-02-20 19:12:07 +08:00
47f4b05517 feat(sftp): allow ignore symlink error (close #6026) 2024-02-15 18:54:19 +08:00
6d85f1b0c0 fix(123): User-Agent and rate limit (#6012)
* 修复标签

* 新增接口限流器。防止云盘云端把Alist当做攻击,封禁Alist客户端

---------

Co-authored-by: 风信子 <fengxinzi@xaidc.com>
2024-02-09 14:45:44 +08:00
e49fda3e2a fix: WebDAV's creation date should use RFC3339 format (#6015 close #5878) 2024-02-08 19:22:29 +08:00
da5e35578a fix: embed all files of dist 2024-02-03 19:44:50 +08:00
812f58ae6d fix(mopan): client version is too low (#5987)
* fix(mopan): download err ` client version is too low`

* feat(mopan):support sms login

* refactor(quqi): upload use s3
2024-02-02 21:04:43 +08:00
9bd3c87bcc fix(ldap): exiting by peer exception occurred during the TLS connection(#5977) 2024-02-01 10:43:08 +08:00
c82866975e fix: error on repeated reading static (#5957)
* Update static.go

* rm initial value of static

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-01-30 21:21:53 +08:00
aef952ae68 feat(dropbox): add root_namespace_id to access teams folder (#5929)
* feat(dropbox): add root_namespace_id to access teams folder

* fix(dropbox): get_current_account API request

* feat(dropbox): extract root_namespace_id properly

* style: format code
2024-01-24 17:03:50 +08:00
9222510d8d feat(quqi): add download link with cdn (#5938)
* feat(quqi): add download link by cdn

* fix(quqi): cookie error when login with phone number
2024-01-24 16:47:49 +08:00
d88b54d98a fix(quqi): empty file link for non vip user (#5926)
* fix(quqi): error returned when uploading a file that existed

* fix empty download link for no vip user

* fix cannot parse request result

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-01-21 15:28:52 +08:00
85a28d9822 fix(quqi): error on uploading an existing file (#5920) 2024-01-20 21:22:50 +08:00
4f7761fe2c fix: set progress to 100 when it's NaN (close #5906) 2024-01-20 13:06:46 +08:00
a8c900d09e fix(quqi): file extension duplication when rename and some missing form parameters (#5910)
* feat: add `quqi` driver

* change signature of request function

* specific header for every storage

* todo: real upload

* fix upload method

* fix incorrect parameters for some request function calls

* refine some form parameters to avoid potential problems

* fix file extension duplication in rename function

* improve the error message in login function

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-01-19 13:57:31 +08:00
8bccb69e8d fix(google_photo): add support for streaming video, range requests (#5905)
* Update util.go

Return mediaMetadata

* Update driver.go

Using width and height
2024-01-19 13:02:05 +08:00
0f29a811bf fix: s3 upload exceeded total allowed configured MaxUploadParts (close #5909) 2024-01-19 12:05:10 +08:00
442c2f77ea feat: add quqi driver (#5899 close #5251)
* feat: add `quqi` driver

* change signature of request function

* specific header for every storage

* todo: real upload

* fix upload method

* fix incorrect parameters for some request function calls

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-01-19 10:59:56 +08:00
ce06f394f1 fix: missing salt of guest user (close #5737) 2024-01-17 14:15:34 +08:00
e3e790f461 feat(115): add QR code source selection (#5891)
* feat(115): add QR code source selection

closed #5386

* feat(115_share): add QR code source selection
2024-01-16 15:59:44 +08:00
f0e8c0e886 fix(chaoxing): JSON parsing error in content field (#5877)
* fix(chaoxing):fix JSON parsing error in `content` field

* fix(chaoxing): optimizing `UnmarshalJSON` implementation

* fix(chaoxing): use `objectID` when  is empty
2024-01-14 12:53:31 +08:00
86b35ae5cf fix(deps): update module golang.org/x/oauth2 to v0.16.0 (#5865)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-12 11:54:35 +08:00
4930f85b90 fix(deps): update module golang.org/x/crypto to v0.18.0 (#5863)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-11 20:56:19 +08:00
85fe65951d fix(deps): update golang.org/x/exp digest to 0dcbfd6 (#5862)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-11 20:39:16 +08:00
1381e8fb27 fix(deps): update module github.com/aws/aws-sdk-go to v1.49.18 (#5848)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-11 20:24:20 +08:00
292bbe94ee fix(feijipan): incorrect address of download link (close #5859) 2024-01-11 10:16:14 +08:00
bb6747de4e docs: add feijipan to Readme 2024-01-11 10:15:16 +08:00
555ef0eb1a feat: add feijipan driver (close #5856) 2024-01-10 16:58:10 +08:00
bff56ffd0f ci: add android target to release build (#5844)
* build: build android

Signed-off-by: lateautumn233 <lateautumn233@foxmail.com>

* ci: add `android` target to release build

Signed-off-by: lateautumn233 <lateautumn233@foxmail.com>

---------

Signed-off-by: lateautumn233 <lateautumn233@foxmail.com>
2024-01-09 19:00:11 +08:00
34b73b94f7 feat(local): allow specifying the recycle bin path (close #5832) 2024-01-09 18:51:21 +08:00
434892f135 fix(deps): update module github.com/aliyun/aliyun-oss-go-sdk to v3 (#5800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-09 17:14:31 +08:00
Mmx
e6e2d03ba1 perf: make docker release 10 times faster (#5803)
* build: improve multistage docker build

* build: add dockerfile for ci

* build: add BuildDockerMultiplatform function in build.sh for ci

* ci: change build method

* build: add missing mod download command to the Dockerfile

* build: revert changes made ffmpeg installed

* build: use musl build for docker release

* ci: apply to dev version

* fix: don't login on pr

* fix: don't build_docker_with_aria2 on pr

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-01-05 15:52:30 +08:00
28bb3f6310 fix(deps): update module golang.org/x/image to v0.15.0 (#5825)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-05 15:35:41 +08:00
fb729c1846 fix(deps): update module github.com/aws/aws-sdk-go to v1.49.15 (#5816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-05 15:34:16 +08:00
4448e08f5b fix(net): Buf use Mutex (#5823)
Co-authored-by: Andy Hsu <i@nn.ci>
2024-01-05 12:20:08 +08:00
8020d42b10 fix: panic due to send on closed channel (close #5729) 2024-01-05 11:41:53 +08:00
9d5fb7f595 feat: add ILanzou driver (#5810 close #5715)
* wip: basic request and login

* feat: impl list

* feat: impl link

* feat: impl mkdir, move, rename, delete

* feat: impl upload

* docs: add iLanzou to readme
2024-01-04 22:03:15 +08:00
126cfe9f93 fix(vtencent): only show 50 files (close #5805) 2024-01-04 21:54:39 +08:00
fd96a7ccf4 fix(deps): update golang.org/x/exp digest to be819d1 [skip ci] (#5807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-04 21:02:22 +08:00
03b9b9a119 chore(deps): update docker/setup-qemu-action action to v3 (#5798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-03 14:51:54 +08:00
03dbdfc0dd chore(deps): update docker/setup-buildx-action action to v3 (#5797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-03 14:46:37 +08:00
2683621ed7 chore(deps): update docker/metadata-action action to v5 [skip ci] (#5795)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-03 14:46:09 +08:00
be537aa49b chore(deps): update docker/login-action action to v3 [skip ci] (#5794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-03 14:45:53 +08:00
6f742a68cf chore(deps): update docker/build-push-action action to v5 [skip ci] (#5793)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-03 14:45:32 +08:00
97a4b8321d chore(deps): update actions/upload-artifact action to v4 (#5792)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-03 14:44:59 +08:00
8c432d3339 chore(deps): update actions/checkout action to v4 (#5788)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 17:43:22 +08:00
ff25e51f80 chore(deps): update actions/setup-go action to v5 (#5789)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 17:43:08 +08:00
88831b5d5a fix(deps): update module golang.org/x/oauth2 to v0.15.0 [skip ci] (#5785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 15:44:45 +08:00
b97c9173af fix(deps): update module golang.org/x/image to v0.14.0 [skip ci] (#5784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 15:42:38 +08:00
207c7e05fe fix(deps): update module github.com/spf13/cobra to v1.8.0 [skip ci] (#5783)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 15:42:17 +08:00
7db27e6da8 fix(deps): update module golang.org/x/time to v0.5.0 [skip ci] (#5786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 15:41:52 +08:00
b5cc90cb5a fix(115): support null UserAgent (#5787) 2024-01-02 15:41:32 +08:00
8a427ddc49 fix(deps): update module github.com/go-webauthn/webauthn to v0.10.0 [skip ci] (#5782)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 14:54:38 +08:00
c36644a172 fix(deps): update module github.com/go-resty/resty/v2 to v2.11.0 (#5781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 14:27:08 +08:00
45b1ff4a24 fix(deps): update module github.com/charmbracelet/bubbles to v0.17.1 [skip ci] (#5775)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 14:24:56 +08:00
a4a9675616 fix(deps): update module github.com/charmbracelet/bubbletea to v0.25.0 [skip ci] (#5776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 14:12:54 +08:00
8531b23382 fix(deps): update module github.com/deckarep/golang-set/v2 to v2.6.0 [skip ci] (#5778)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 14:12:33 +08:00
2c15349ce4 fix(deps): update module github.com/gin-contrib/cors to v1.5.0 [skip ci] (#5779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 14:12:13 +08:00
5afd65b65c fix(deps): update module github.com/aliyun/aliyun-oss-go-sdk to v2.2.10+incompatible (#5447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-01 22:19:25 +08:00
e2434029f9 fix(deps): update module github.com/maruel/natural to v1.1.1 [skip ci] (#5771)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-01 22:18:51 +08:00
bdf7abe717 fix(deps): update module github.com/aws/aws-sdk-go to v1.49.13 [skip ci] (#5774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-01 22:18:13 +08:00
2c8d003c2e fix(deps): update module github.com/djherbis/times to v1.6.0 [skip ci] (#5422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-01 22:17:44 +08:00
a006f57637 fix(deps): update module google.golang.org/appengine to v1.6.8 [skip ci] (#5772)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-01 21:42:43 +08:00
be5d94cd11 fix(deps): update module golang.org/x/crypto to v0.17.0 [security] (#5768)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-01 21:42:03 +08:00
977b3cf9ab fix(deps): update golang.org/x/exp digest to 02704c9 [skip ci] (#5769)
* fix: missing modified in validate regexp

* fix(deps): update golang.org/x/exp digest to 02704c9

---------

Co-authored-by: Andy Hsu <i@nn.ci>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-01 19:57:04 +08:00
182aacd309 fix(deps): update module github.com/gorilla/websocket to v1.5.1 [skip ci] (#5770)
* fix: missing modified in validate regexp

* fix(deps): update module github.com/gorilla/websocket to v1.5.1

---------

Co-authored-by: Andy Hsu <i@nn.ci>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-01 19:55:31 +08:00
57bac9e0d2 fix: some missing regexp lib modified 2024-01-01 18:44:59 +08:00
478470f609 feat!: replace regex package (close #5755) 2023-12-31 15:03:25 +08:00
6b8f35e7fa feat(alipan): replace domain (#5751 close #5747) 2023-12-31 14:29:14 +08:00
697a0ed2d3 feat: add ldap login support (#5706)
* feat: add ldap login support

* fix: ldap permission config group
2023-12-31 13:46:13 +08:00
299bfb4d7b feat(115): support 302 redirect (#5733) 2023-12-25 11:28:57 +08:00
3eca38e599 feat: add support for client-side discoverable WebAuthn login (#5722)
* Add support for client-side discoverable in begin login

Use `(*webauthn.WebAuthn).BeginDiscoverableLogin()` to handle client-side discoverable login.

* Upgrade github.com/go-webauthn/webauthn to v0.10.0

Upgrade [go-webauthn/webauthn](github.com/go-webauthn/webauthn) library to latest.

The convenient finish login function (as FinishDiscoverableLogin) for discoverable functions has been added in the v0.9.0. [^1]

---

[^1]: https://github.com/go-webauthn/webauthn/releases/tag/v0.9.0

* Add support for client-side discoverable in validating login

Use `(*webauthn.WebAuthn).FinishDiscoverableLogin()` to handle client-side discoverable login.

> **NOTE**:
- The first param `rawID` in this callback function is unnecessary to check, it's handled by the third-party webauthn library later.
- `userHandle` param is equal to the ID returned by (User).WebAuthnID() function.
2023-12-24 15:21:17 +08:00
ab216ed170 fix(onedrive): rename object in root folder (close #5468) 2023-12-17 22:58:26 +08:00
e91c42c9dc fix(alist_v3): timeout on upload (close #5465) 2023-12-17 15:45:27 +08:00
54f7b21a73 fix(123): api sign error (#5689 close #5083)
* fix:123 driver connect error

* feat: calculate sign with pure go

---------

Co-authored-by: tangminghao <tangminghao@hxzn.com>
Co-authored-by: Andy Hsu <i@nn.ci>
2023-12-17 15:21:32 +08:00
de56f926cf feat(139): support new personal cloud api (#5690)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-12-16 16:56:45 +08:00
6d4ab57a0e build: enable cgo for win/arm64 [skip ci] 2023-12-15 18:22:16 +08:00
734d4b0354 ci: add darwin/arm64 target to dev build 2023-12-15 17:07:02 +08:00
74b20dedc3 fix: retry multipart file reset (#5693 close #5628) 2023-12-14 21:31:36 +08:00
83c2269330 fix(qbit): seed time doesn't take effect (close #5663) 2023-12-11 15:20:29 +08:00
296be88b5f fix: incorrect key of oidc username (close #5670) 2023-12-10 13:17:56 +08:00
026e944cbb feat: add task info to resp of add task api (close #5579) 2023-12-03 14:44:20 +08:00
8bdfc7ac8e fix(offline_download): don't wait for transfer task (close #5595) 2023-12-03 14:20:01 +08:00
e4a6b758dc docs: remove jetbrains in special sponsor [skip ci] 2023-12-03 12:57:35 +08:00
66b7fe1e1b fix: task cannot be retried manually (close #5599) 2023-11-30 20:44:05 +08:00
f475eb4401 fix: incorrect go-version on auto-lang 2023-11-30 12:37:25 +08:00
b99e709bdb fix(teambition): international upload (close #5360) 2023-11-29 22:51:03 +08:00
f4dcf4599c fix: add error handling for webdav mkcol according to RFC 4918 (#5581)
* feat: add error handling for mkcol method in webdav.go

* feat: update rfc reference

* fix: fix issue with uncorrect error handling
2023-11-27 18:53:52 +08:00
54e75d7287 feat: enabled sign_all by default 2023-11-25 20:27:23 +08:00
d142fc3449 ci: upgrade golang version 2023-11-25 16:09:38 +08:00
f23567199b chore: go mod tidy 2023-11-25 15:12:25 +08:00
1420492d81 ci: go get after replacing go mod 2023-11-25 15:11:29 +08:00
b88067ea2f ci: fix docker build error: 'pread64' undeclared here 2023-11-25 14:42:33 +08:00
d5f381ef6f chore: upgrade golang version 2023-11-25 14:22:13 +08:00
68af284dad fix: task popped but not execute (close #5565) 2023-11-25 14:15:17 +08:00
d26887d211 fix: content-type conflicts with #5420 2023-11-24 19:22:19 +08:00
3f405de6a9 feat: customize allow origins, headers and methods 2023-11-24 19:18:34 +08:00
6100647310 fix: reflected XSS vulnerability plist api 2023-11-24 16:46:48 +08:00
34746e951c feat(offline_download): add simple http tool (close #4002) 2023-11-24 16:26:05 +08:00
b6134dc515 feat: allow keep files in offline download (close #4678) 2023-11-24 15:02:36 +08:00
d455a232ef fix(vtencent): hack file with size 0 but actual size is not 0
- allow use another proxy for vtencent and chaoxing
2023-11-23 22:35:07 +08:00
fe34d30d17 feat(crypt): add show hidden option (#5554) 2023-11-23 21:50:16 +08:00
0fbb986ba9 fix(aliyundrive_open): mitigation measures for 15-minute limit (#5560 close #5547)
* fix(aliyundrive_open):Mitigation measures for AliOpen's 15-minute limit.

I conducted small-scale tests, which seem to have no significant negative impact. If the 15-minute issue still occurs, further measures will be needed. Methods like local proxy can be attempted.

* chore(aliyundrive_open): change cache of the link to 1 minute

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-11-23 21:49:16 +08:00
1280070438 feat: add chaoxing and vtencent driver (#5526 close #3347)
* add chaoxing and vtencent

* add vtencent put file

* add sha1 to transfer files instantly

* simplified upload file code

* setting onlyproxy

* fix get files modifyDate bug
2023-11-23 21:40:16 +08:00
d7f66138eb docs: add sponsor VidHub [skip ci] 2023-11-22 15:09:39 +08:00
b2890f05ab feat: retry all failed task (close #5242) 2023-11-21 15:54:42 +08:00
7583c4d734 feat: customize workers and retry of task (close #5493 fix #5274) 2023-11-21 15:51:57 +08:00
11a30c5044 feat: refactor task module 2023-11-20 18:01:51 +08:00
de9647a5fa chore: remove useless code 2023-11-19 20:05:09 +08:00
8d5283604c ci: add short sha to artifact 2023-11-19 15:21:25 +08:00
867accafd1 fix(local): video file thumbnails not displaying on iOS Safari (#5420)
* perf(webdav): support for cookies on webdav drive

* fix(local): video file thumbnails not displaying on iOS Safari
2023-11-18 22:36:41 +08:00
6fc6751463 feat: support using external dist files (close #5531) 2023-11-18 19:56:22 +08:00
f904596cbc chore: remove refs to deprecated io/ioutil (#5519)
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
2023-11-16 05:16:15 -06:00
3d51845f57 feat: invalidate old token after changing the password (close #5515) 2023-11-13 15:22:42 +08:00
a7421d8fc2 fix(deps): update module github.com/aws/aws-sdk-go to v1.46.7 (#5068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-12 15:14:27 +08:00
55a14bc271 fix(mopan): 302 Redirect (#5505 close #5502)
* fix(mopan):302 Redirect

* fix(mopan): do not forget to close the body

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-11-12 15:13:55 +08:00
91f51f17d0 feat(webdav): add tls_insecure_skip_verify field (close #5490) 2023-11-10 15:38:23 +08:00
4355dae491 fix: incorrect content-type of apk files (close #5385) 2023-11-06 18:20:25 +08:00
da1c7a4c23 feat: add 115_share driver (#5481 close #5384)
This update introduces the ability to mount 115 share links.
 Currently, only listing and downloading are supported. Note that login and share link are required for this feature to work.

 Close #5384
2023-11-06 16:58:57 +08:00
769281bd40 feat: refactor offline download (#5408 close #4108)
* wip: refactor offline download (#5331)

* base tool

* working: aria2

* refactor: change type of percentage to float64

* wip: adapt aria2

* wip: use items in offline_download

* wip: use tool manager

* wip: adapt qBittorrent

* chore: fix typo

* Squashed commit of the following:

commit 4fc0a77565
Author: Andy Hsu <i@nn.ci>
Date:   Fri Oct 20 21:06:25 2023 +0800

    fix(baidu_netdisk): upload file > 4GB (close #5392)

commit aaffaee2b5
Author: gmugu <94156510@qq.com>
Date:   Thu Oct 19 19:17:53 2023 +0800

    perf(webdav): support request with cookies (#5391)

commit 8ef8023c20
Author: NewbieOrange <NewbieOrange@users.noreply.github.com>
Date:   Thu Oct 19 19:17:09 2023 +0800

    fix(aliyundrive_open): upload progress for normal upload (#5398)

commit cdfbe6dcf2
Author: foxxorcat <95907542+foxxorcat@users.noreply.github.com>
Date:   Wed Oct 18 16:27:07 2023 +0800

    fix: hash gcid empty file (#5394)

commit 94d028743a
Author: Andy Hsu <i@nn.ci>
Date:   Sat Oct 14 13:17:51 2023 +0800

    ci: remove `pr-welcome` label when close issue [skip ci]

commit 7f7335435c
Author: itsHenry <2671230065@qq.com>
Date:   Sat Oct 14 13:12:46 2023 +0800

    feat(cloudreve): support thumbnail (#5373 close #5348)

    * feat(cloudreve): support thumbnail

    * chore: remove unnecessary code

commit b9e192b29c
Author: foxxorcat <95907542+foxxorcat@users.noreply.github.com>
Date:   Thu Oct 12 20:57:12 2023 +0800

    fix(115): limit request rate (#5367 close #5275)

    * fix(115):limit request rate

    * chore(115): fix unit of `limit_rate`

    ---------

    Co-authored-by: Andy Hsu <i@nn.ci>

commit 69a98eaef6
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Wed Oct 11 22:01:55 2023 +0800

    fix(deps): update module github.com/aliyun/aliyun-oss-go-sdk to v2.2.9+incompatible (#5141)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit 1ebc96a4e5
Author: Andy Hsu <i@nn.ci>
Date:   Tue Oct 10 18:32:00 2023 +0800

    fix(wopan): fatal error concurrent map writes (close #5352)

commit 66e2324cac
Author: Andy Hsu <i@nn.ci>
Date:   Tue Oct 10 18:23:11 2023 +0800

    chore(deps): upgrade dependencies

commit 7600dc28df
Author: Andy Hsu <i@nn.ci>
Date:   Tue Oct 10 18:13:58 2023 +0800

    fix(aliyundrive_open): change default api to raw server (close #5358)

commit 8ef89ad0a4
Author: foxxorcat <95907542+foxxorcat@users.noreply.github.com>
Date:   Tue Oct 10 18:08:27 2023 +0800

    fix(baidu_netdisk): hash and `error 2` (#5356)

    * fix(baidu):hash and error:2

    * fix:invalid memory address

commit 35d672217d
Author: jeffmingup <1960588251@qq.com>
Date:   Sun Oct 8 19:29:45 2023 +0800

    fix(onedrive_app): incorrect api on `_accessToken` (#5346)

commit 1a283bb272
Author: foxxorcat <95907542+foxxorcat@users.noreply.github.com>
Date:   Fri Oct 6 16:04:39 2023 +0800

    feat(google_drive): add `hash_info`, `ctime`, `thumbnail` (#5334)

commit a008f54f4d
Author: nkh0472 <67589323+nkh0472@users.noreply.github.com>
Date:   Thu Oct 5 13:10:51 2023 +0800

    docs: minor language improvements (#5329) [skip ci]

* fix: adapt update progress type

* Squashed commit of the following:

commit 65c5ec0c34
Author: itsHenry <2671230065@qq.com>
Date:   Sat Nov 4 13:35:09 2023 +0800

    feat(cloudreve): folder size count and switch (#5457 close #5395)

commit a6325967d0
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Mon Oct 30 15:11:20 2023 +0800

    fix(deps): update module github.com/charmbracelet/lipgloss to v0.9.1 (#5234)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit 4dff49470a
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Mon Oct 30 15:10:36 2023 +0800

    fix(deps): update golang.org/x/exp digest to 7918f67 (#5366)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit cc86d6f3d1
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Sun Oct 29 14:45:55 2023 +0800

    fix(deps): update module golang.org/x/net to v0.17.0 [security] (#5370)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit c0f9c8ebaf
Author: Andy Hsu <i@nn.ci>
Date:   Thu Oct 26 19:21:09 2023 +0800

    feat: add ignore direct link params (close #5434)
2023-11-06 16:56:55 +08:00
3bbdd4fa89 fix(115): fix driver package import and variable (#5482)
names
2023-11-06 16:53:57 +08:00
68f440abdb fix(weiyun): unmarshal overflow (#5459) 2023-11-05 22:41:14 +08:00
65c5ec0c34 feat(cloudreve): folder size count and switch (#5457 close #5395) 2023-11-04 13:35:09 +08:00
a6325967d0 fix(deps): update module github.com/charmbracelet/lipgloss to v0.9.1 (#5234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 15:11:20 +08:00
4dff49470a fix(deps): update golang.org/x/exp digest to 7918f67 (#5366)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 15:10:36 +08:00
cc86d6f3d1 fix(deps): update module golang.org/x/net to v0.17.0 [security] (#5370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-29 14:45:55 +08:00
c0f9c8ebaf feat: add ignore direct link params (close #5434) 2023-10-26 19:21:09 +08:00
4fc0a77565 fix(baidu_netdisk): upload file > 4GB (close #5392) 2023-10-20 21:06:25 +08:00
aaffaee2b5 perf(webdav): support request with cookies (#5391) 2023-10-19 19:17:53 +08:00
8ef8023c20 fix(aliyundrive_open): upload progress for normal upload (#5398) 2023-10-19 19:17:09 +08:00
cdfbe6dcf2 fix: hash gcid empty file (#5394) 2023-10-18 16:27:07 +08:00
94d028743a ci: remove pr-welcome label when close issue [skip ci] 2023-10-14 13:17:51 +08:00
7f7335435c feat(cloudreve): support thumbnail (#5373 close #5348)
* feat(cloudreve): support thumbnail

* chore: remove unnecessary code
2023-10-14 13:12:46 +08:00
b9e192b29c fix(115): limit request rate (#5367 close #5275)
* fix(115):limit request rate

* chore(115): fix unit of `limit_rate`

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-10-12 20:57:12 +08:00
69a98eaef6 fix(deps): update module github.com/aliyun/aliyun-oss-go-sdk to v2.2.9+incompatible (#5141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 22:01:55 +08:00
1ebc96a4e5 fix(wopan): fatal error concurrent map writes (close #5352) 2023-10-10 18:32:00 +08:00
66e2324cac chore(deps): upgrade dependencies 2023-10-10 18:23:11 +08:00
7600dc28df fix(aliyundrive_open): change default api to raw server (close #5358) 2023-10-10 18:13:58 +08:00
8ef89ad0a4 fix(baidu_netdisk): hash and error 2 (#5356)
* fix(baidu):hash and error:2

* fix:invalid memory address
2023-10-10 18:08:27 +08:00
35d672217d fix(onedrive_app): incorrect api on _accessToken (#5346) 2023-10-08 19:29:45 +08:00
1a283bb272 feat(google_drive): add hash_info, ctime, thumbnail (#5334) 2023-10-06 16:04:39 +08:00
a008f54f4d docs: minor language improvements (#5329) [skip ci] 2023-10-05 13:10:51 +08:00
3d7f79cba8 docs: change domain of contributors image [skip ci] 2023-10-03 17:34:24 +08:00
9ff83a7950 feat: add header to meta (ref #5317) 2023-10-02 16:43:29 +08:00
e719a1a456 feat(sso): custom username key for OIDC (close #5169) 2023-10-02 14:42:40 +08:00
40a6fcbdff ci: do not stale issue with working or pr-welcome label [skip ci] 2023-10-02 14:13:11 +08:00
0fd51646f6 feat(onedrive): custom host for download link (close #5310) 2023-10-02 14:07:47 +08:00
e8958019d9 fix(115): allow use proxy directly (close #5324) 2023-10-02 14:00:13 +08:00
e1ef690784 fix(terabox): encode parameters for filemanager api (#5308) 2023-10-01 16:58:29 +08:00
4024050dd0 chore: fix typo (#5316) 2023-10-01 16:58:00 +08:00
eb918658f0 fix(deps): update module github.com/ipfs/go-ipfs-api to v0.7.0 (#5247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-30 22:58:19 +08:00
fb13dae136 feat(crypt): optional pre-generated thumbnails (#5284) 2023-09-27 13:57:10 +08:00
6b67a36d63 fix(terabox): auto refresh JsToken (close #5277) 2023-09-25 16:38:05 +08:00
a64dd4885e fix(139): fixed time zone (close #5263) 2023-09-22 16:54:16 +08:00
0f03a747d8 ci: cancel previous workflow run 2023-09-22 16:53:07 +08:00
30977cdc6d feat: sso compatibility mode (#5260) 2023-09-22 16:45:51 +08:00
106cf720c1 fix(baidu_netdisk): retry logic in request (close #5262) 2023-09-22 16:27:44 +08:00
882112ed1c feat: add hash_info field to /fs/get (close #5259) 2023-09-22 15:20:04 +08:00
2a6ab77295 fix(115): data race in Link (#5253) 2023-09-21 13:39:07 +08:00
f0981a0c8d chore(virtual): implement the driver interface with result 2023-09-20 09:02:56 +08:00
57eea4db17 fix(deps): update module github.com/go-resty/resty/v2 to v2.8.0 (#5244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-20 08:51:34 +08:00
234852ca61 fix(deps): update module github.com/pkg/sftp to v1.13.6 (#5041)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 20:02:42 +08:00
809105b67e fix(deps): update module github.com/blevesearch/bleve/v2 to v2.3.10 (#5232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-17 15:57:29 +08:00
02e8c31506 fix(deps): update golang.org/x/exp digest to 9212866 (#5205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-16 23:21:42 +08:00
19b39a5c04 fix(onedrive): overwrite upload big file (close #5217 in #5218)
See https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
2023-09-14 13:38:07 +08:00
28e2731594 fix: clear cache recursively on deleting the folder (close #5209) 2023-09-13 16:06:17 +08:00
b1a279cbcc feat(139): implement MoveResult interface (close #5130) 2023-09-13 15:56:13 +08:00
352a6a741a feat(webdav): support copy directly without task (close #5206) 2023-09-13 15:45:57 +08:00
109015567a fix(deps): update module golang.org/x/oauth2 to v0.12.0 (#5058)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 12:52:48 +08:00
9e0fa77ca2 feat: add 123 link driver (close #4924) 2023-09-10 16:50:10 +08:00
335b11c698 chore: implement the driver interface with obj return [skip ci] 2023-09-08 15:25:49 +08:00
8e433355e6 fix(terabox): missing JsToken field on request (close #5189) 2023-09-08 15:18:56 +08:00
3504f017b9 fix(upload): memory leak on form upload as task (close #5185) 2023-09-07 15:51:52 +08:00
cd2f8077fa chore: enable all pprof handle on debug 2023-09-07 14:56:50 +08:00
d5b68a91d2 fix(webdav): optimize HEAD request (close #5182) 2023-09-06 16:32:51 +08:00
623c7dcea5 fix(189pc): get real link after redirect 2023-09-06 16:02:28 +08:00
ecbd6d86cd fix(lanzou): sub file in share folder need pwd (#5184) 2023-09-06 14:48:12 +08:00
7200344ace feat: adapt hash feature for some drivers (#5180)
* feat(pikpak,thunder): adaptation gcid hash

* chore(weiyun): add note

* feat(baidu_netdisk): adaptation rapid

* feat(baidu_photo): adaptation hash

* feat(189pc): adaptation rapid

* feat(mopan):adaptation ctime

* feat(139):adaptation hash and ctime

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-09-06 14:46:35 +08:00
b313ac4daa fix(crypt): fix 139cloud hack (#5178)
(cherry picked from commit 18bf64af47e58cc69cdd2e598de9c19538a7bf78)
2023-09-06 14:12:01 +08:00
f2f312b43a fix: http response body not close on status >= 400 (close #5163) 2023-09-05 15:46:16 +08:00
6f6d20e1ba fix: force_https not take effect on noRoute (close #5167) 2023-09-05 13:05:46 +08:00
3231c3d930 perf(db): release database before exit 2023-09-05 13:04:27 +08:00
b604e21c69 feat(webdav): support http chunked request (close #5161 in #5162)
But we do not recommend not adding the content-length header when putting files
2023-09-05 13:03:29 +08:00
3c66db9845 ci: split release actions 2023-09-03 22:57:18 +08:00
f6ab1f7f61 perf(ftp): non use SIZE FTP command (close #5150) 2023-09-03 18:47:32 +08:00
8e40465e86 fix(aliyundrive_open): date format on uploading (#5151)
(cherry picked from commit 88f815979ac91caa8bc425a2ff9a18bbd8a2e736)
2023-09-03 18:12:05 +08:00
37dffd0fce feat(crypt): customize filename_encoding (#5148)
close #5109
close #5080
2023-09-03 18:06:44 +08:00
e7c0d94b44 fix: form upload when ticked As A Task (#5145) 2023-09-03 15:40:40 +08:00
8102142007 fix(deps): update github.com/orzogc/fake115uploader digest to 58f9eb7 (#5133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-02 14:50:06 +08:00
7c6dec5d47 fix(deps): update module 115driver to v1.0.16 (close #5117 in #5120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-01 14:31:47 +08:00
dd10c0c5d0 chore(aliyundrive_open): print resp content on refresh token (close #5129) 2023-08-31 18:43:25 +08:00
34fadecc2c fix(ftp): dead lock on Read (close #5128) 2023-08-31 15:10:47 +08:00
cb8867fcc1 fix(deps): update module github.com/google/uuid to v1.3.1 (#5066)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-30 19:30:41 +08:00
092ed06833 feat(uss): add AntiTheftChainToken field (#5115)
* feat(uss): add AntiTheftChainToken; fix link func

* feat(uss): optimize _upt generation
2023-08-30 15:16:26 +08:00
6308f1c35d fix: updateTime, createTime and HashInfo (#5111) 2023-08-29 13:31:24 +08:00
ce10c9f120 fix: temp file not close and incorrect WebPutAsTask 2023-08-28 18:18:02 +08:00
6c4736fc8f fix: allow no Last-Modified on upload api 2023-08-28 16:42:03 +08:00
b301b791c7 fix(local): set create and modified time for new file (close #4938) 2023-08-27 23:05:13 +08:00
19d34e2eb8 feat: receive lastModified from upload api 2023-08-27 23:03:09 +08:00
a3748af772 feat: misc improvements about upload/copy/hash (#5045)
general: add createTime/updateTime support in webdav and some drivers
general: add hash support in some drivers
general: cross-storage rapid-upload support
general: enhance upload to avoid local temp file if possible
general: replace readseekcloser with File interface to speed upstream operations
feat(aliyun_open): same as above
feat(crypt): add hack for 139cloud

Close #4934 
Close #4819 

baidu_netdisk needs to improve the upload code to support rapid-upload
2023-08-27 21:14:23 +08:00
9b765ef696 chore: remove README.md executable permission (close #5097 in #5100) 2023-08-27 14:35:03 +08:00
8f493cccc4 fix(mopan): parameter error (#5091) 2023-08-25 14:10:05 +08:00
31a033dff1 fix(lanzou): download cannot find data (#5088) 2023-08-24 21:56:20 +08:00
311 changed files with 15285 additions and 3361 deletions

44
.air.toml Normal file
View File

@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = ["server"]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

2
.github/stale.yml vendored
View File

@ -6,6 +6,8 @@ daysUntilClose: 20
exemptLabels: exemptLabels:
- accepted - accepted
- security - security
- working
- pr-welcome
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: stale staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable

View File

@ -11,27 +11,31 @@ on:
- 'cmd/lang.go' - 'cmd/lang.go'
workflow_dispatch: workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
auto_lang: auto_lang:
strategy: strategy:
matrix: matrix:
platform: [ ubuntu-latest ] platform: [ ubuntu-latest ]
go-version: [ '1.20' ] go-version: [ '1.21' ]
name: auto generate lang.json name: auto generate lang.json
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Setup go - name: Setup go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Checkout alist - name: Checkout alist
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
path: alist path: alist
- name: Checkout alist-web - name: Checkout alist-web
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: 'alist-org/alist-web' repository: 'alist-org/alist-web'
ref: main ref: main

View File

@ -6,22 +6,29 @@ on:
pull_request: pull_request:
branches: [ 'main' ] branches: [ 'main' ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
strategy: strategy:
matrix: matrix:
platform: [ubuntu-latest] platform: [ubuntu-latest]
go-version: [ '1.20' ] go-version: [ '1.21' ]
name: Build name: Build
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v2.2
id: short-sha
- name: Install dependencies - name: Install dependencies
run: | run: |
@ -35,7 +42,7 @@ jobs:
bash build.sh dev bash build.sh dev
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: alist name: alist_${{ env.SHA }}
path: dist path: dist

View File

@ -3,48 +3,100 @@ name: build_docker
on: on:
push: push:
branches: [ main ] branches: [ main ]
pull_request:
branches: [ main ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
build_docker: build_docker:
name: Build docker name: Build Docker
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: xhofe/alist images: xhofe/alist
- name: Replace release with dev
run: | - name: Docker meta with ffmpeg
sed -i 's/release/dev/g' Dockerfile id: meta-ffmpeg
uses: docker/metadata-action@v5
with:
images: xhofe/alist
flavor: |
suffix=-ffmpeg,onlatest=true
- 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
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 if: github.event_name == 'push'
uses: docker/login-action@v3
with: with:
username: xhofe username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true file: Dockerfile.ci
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
- name: Replace dockerfile tag
run: |
sed -i -e "s/latest/main/g" Dockerfile.ffmpeg
- name: Build and push with ffmpeg
id: docker_build_ffmpeg
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.ffmpeg
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
labels: ${{ steps.meta-ffmpeg.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
build_docker_with_aria2: build_docker_with_aria2:
needs: build_docker needs: build_docker
name: Build docker with aria2 name: Build docker with aria2
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push'
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: alist-org/with_aria2 repository: alist-org/with_aria2
ref: main ref: main
@ -62,4 +114,4 @@ jobs:
with: with:
github_token: ${{ secrets.MY_TOKEN }} github_token: ${{ secrets.MY_TOKEN }}
branch: main branch: main
repository: alist-org/with_aria2 repository: alist-org/with_aria2

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result - run: npx changelogithub # or changelogithub@0.12 if ensure the stable result

View File

@ -14,4 +14,4 @@ jobs:
actions: 'remove-labels' actions: 'remove-labels'
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }} issue-number: ${{ github.event.issue.number }}
labels: 'working' labels: 'working,pr-welcome'

View File

@ -9,7 +9,7 @@ jobs:
strategy: strategy:
matrix: matrix:
platform: [ ubuntu-latest ] platform: [ ubuntu-latest ]
go-version: [ '1.20' ] go-version: [ '1.21' ]
name: Release name: Release
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
@ -21,12 +21,12 @@ jobs:
prerelease: true prerelease: true
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@ -53,7 +53,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: alist-org/desktop-release repository: alist-org/desktop-release
ref: main ref: main

34
.github/workflows/release_android.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: release_android
on:
release:
types: [ published ]
jobs:
release_android:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release android
- name: Upload assets
uses: softprops/action-gh-release@v1
with:
files: build/compress/*

View File

@ -11,43 +11,82 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@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
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: xhofe/alist images: xhofe/alist
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: xhofe username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: Dockerfile.ci
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
- name: Docker meta with ffmpeg
id: meta-ffmpeg
uses: docker/metadata-action@v5
with:
images: xhofe/alist
flavor: |
latest=true
suffix=-ffmpeg,onlatest=true
- name: Build and push with ffmpeg
id: docker_build_ffmpeg
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.ffmpeg
push: true
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
labels: ${{ steps.meta-ffmpeg.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
release_docker_with_aria2: release_docker_with_aria2:
needs: release_docker needs: release_docker
name: Release docker with aria2 name: Release docker with aria2
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: alist-org/with_aria2 repository: alist-org/with_aria2
ref: main ref: main

View File

@ -0,0 +1,34 @@
name: release_linux_musl
on:
release:
types: [ published ]
jobs:
release_linux_musl:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release linux_musl
- name: Upload assets
uses: softprops/action-gh-release@v1
with:
files: build/compress/*

View File

@ -5,22 +5,22 @@ on:
types: [ published ] types: [ published ]
jobs: jobs:
release_arm: release_linux_musl_arm:
strategy: strategy:
matrix: matrix:
platform: [ ubuntu-latest ] platform: [ ubuntu-latest ]
go-version: [ '1.20' ] go-version: [ '1.21' ]
name: Release name: Release
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0

1
.gitignore vendored
View File

@ -24,6 +24,7 @@ output/
*.json *.json
/build /build
/data/ /data/
/tmp/
/log/ /log/
/lang/ /lang/
/daemon/ /daemon/

View File

@ -1,18 +1,23 @@
FROM alpine:3.18 as builder FROM alpine:edge as builder
LABEL stage=go-builder LABEL stage=go-builder
WORKDIR /app/ WORKDIR /app/
RUN apk add --no-cache bash curl gcc git go musl-dev
COPY go.mod go.sum ./
RUN go mod download
COPY ./ ./ COPY ./ ./
RUN apk add --no-cache bash curl gcc git go musl-dev; \ RUN bash build.sh release docker
bash build.sh release docker
FROM alpine:3.18 FROM alpine:edge
LABEL MAINTAINER="i@nn.ci" LABEL MAINTAINER="i@nn.ci"
VOLUME /opt/alist/data/ VOLUME /opt/alist/data/
WORKDIR /opt/alist/ WORKDIR /opt/alist/
COPY --from=builder /app/bin/alist ./ COPY --from=builder /app/bin/alist ./
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh
RUN apk add --no-cache bash ca-certificates su-exec tzdata; \ RUN apk update && \
chmod +x /entrypoint.sh apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates su-exec tzdata; \
chmod +x /entrypoint.sh && \
rm -rf /var/cache/apk/*
ENV PUID=0 PGID=0 UMASK=022 ENV PUID=0 PGID=0 UMASK=022
EXPOSE 5244 5245 EXPOSE 5244 5245
CMD [ "/entrypoint.sh" ] CMD [ "/entrypoint.sh" ]

16
Dockerfile.ci Normal file
View File

@ -0,0 +1,16 @@
FROM alpine:edge
ARG TARGETPLATFORM
LABEL MAINTAINER="i@nn.ci"
VOLUME /opt/alist/data/
WORKDIR /opt/alist/
COPY /build/${TARGETPLATFORM}/alist ./
COPY entrypoint.sh /entrypoint.sh
RUN apk update && \
apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates su-exec tzdata; \
chmod +x /entrypoint.sh && \
rm -rf /var/cache/apk/* && \
/entrypoint.sh version
ENV PUID=0 PGID=0 UMASK=022
EXPOSE 5244 5245
CMD [ "/entrypoint.sh" ]

4
Dockerfile.ffmpeg Normal file
View File

@ -0,0 +1,4 @@
FROM xhofe/alist:latest
RUN apk update && \
apk add --no-cache ffmpeg \
rm -rf /var/cache/apk/*

25
README.md Executable file → Normal file
View File

@ -1,5 +1,5 @@
<div align="center"> <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> <p><em>🗂A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>
<div> <div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3"> <a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
@ -43,9 +43,9 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
## Features ## Features
- [x] Multiple storage - [x] Multiple storages
- [x] Local storage - [x] Local storage
- [x] [Aliyundrive](https://www.aliyundrive.com/) - [x] [Aliyundrive](https://www.alipan.com/)
- [x] OneDrive / Sharepoint ([global](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us) - [x] OneDrive / Sharepoint ([global](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family) - [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com/) - [x] [GoogleDrive](https://drive.google.com/)
@ -66,7 +66,8 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] [Quark](https://pan.quark.cn) - [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com) - [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/) - [x] [Lanzou](https://www.lanzou.com/)
- [x] [Aliyundrive share](https://www.aliyundrive.com/) - [x] [ILanzou](https://www.ilanzou.com/)
- [x] [Aliyundrive share](https://www.alipan.com/)
- [x] [Google photo](https://photos.google.com/) - [x] [Google photo](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz) - [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com/) - [x] [Baidu photo](https://photo.baidu.com/)
@ -74,6 +75,8 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] [115](https://115.com/) - [x] [115](https://115.com/)
- [X] Cloudreve - [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/) - [x] [Dropbox](https://www.dropbox.com/)
- [x] [FeijiPan](https://www.feijipan.com/)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [x] Easy to deploy and out-of-the-box - [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...) - [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode - [x] Image preview in gallery mode
@ -86,7 +89,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] Protected routes (password protection and authentication) - [x] Protected routes (password protection and authentication)
- [x] WebDav (see https://alist.nn.ci/guide/webdav.html for details) - [x] WebDav (see https://alist.nn.ci/guide/webdav.html for details)
- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist) - [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist)
- [x] Cloudflare workers proxy - [x] Cloudflare Workers proxy
- [x] File/Folder package download - [x] File/Folder package download
- [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy - [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy
- [x] Offline download - [x] Offline download
@ -103,7 +106,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
## Discussion ## Discussion
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports and feature request only.** Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports and feature requests only.**
## Sponsor ## Sponsor
@ -112,22 +115,22 @@ https://alist.nn.ci/guide/sponsor.html
### Special sponsors ### Special sponsors
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (sponsored Chinese API server) - [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://zhaoziyuan.pw/) - [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/) - [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
## Contributors ## Contributors
Thanks goes to these wonderful people: Thanks goes to these wonderful people:
[![Contributors](http://contributors.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors) [![Contributors](http://contrib.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors)
## License ## License
The `AList` is open-source software licensed under the AGPL-3.0 license. The `AList` is open-source software licensed under the AGPL-3.0 license.
## Disclaimer ## Disclaimer
- This program is a free and open source project. It is designed to share files on the network disk, which is convenient for downloading and learning golang. Please abide by relevant laws and regulations when using it, and do not abuse it; - This program is a free and open source project. It is designed to share files on the network disk, which is convenient for downloading and learning Golang. Please abide by relevant laws and regulations when using it, and do not abuse it;
- This program is implemented by calling the official sdk/interface, without destroying the official interface behavior; - This program is implemented by calling the official sdk/interface, without destroying the official interface behavior;
- This program only does 302 redirect/traffic forwarding, and does not intercept, store, or tamper with any user data; - This program only does 302 redirect/traffic forwarding, and does not intercept, store, or tamper with any user data;
- Before using this program, you should understand and bear the corresponding risks, including but not limited to account ban, download speed limit, etc., which is none of this program's business; - Before using this program, you should understand and bear the corresponding risks, including but not limited to account ban, download speed limit, etc., which is none of this program's business;

View File

@ -1,5 +1,5 @@
<div align="center"> <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> <p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
<div> <div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3"> <a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
@ -45,7 +45,7 @@
- [x] 多种存储 - [x] 多种存储
- [x] 本地存储 - [x] 本地存储
- [x] [阿里云盘](https://www.aliyundrive.com/) - [x] [阿里云盘](https://www.alipan.com/)
- [x] OneDrive / Sharepoint[国际版](https://www.office.com/), [世纪互联](https://portal.partner.microsoftonline.cn),de,us - [x] OneDrive / Sharepoint[国际版](https://www.office.com/), [世纪互联](https://portal.partner.microsoftonline.cn),de,us
- [x] [天翼云盘](https://cloud.189.cn) (个人云, 家庭云) - [x] [天翼云盘](https://cloud.189.cn) (个人云, 家庭云)
- [x] [GoogleDrive](https://drive.google.com/) - [x] [GoogleDrive](https://drive.google.com/)
@ -65,7 +65,8 @@
- [x] [夸克网盘](https://pan.quark.cn) - [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷网盘](https://pan.xunlei.com) - [x] [迅雷网盘](https://pan.xunlei.com)
- [x] [蓝奏云](https://www.lanzou.com/) - [x] [蓝奏云](https://www.lanzou.com/)
- [x] [阿里云盘分享](https://www.aliyundrive.com/) - [x] [蓝奏云优享版](https://www.ilanzou.com/)
- [x] [阿里云盘分享](https://www.alipan.com/)
- [x] [谷歌相册](https://photos.google.com/) - [x] [谷歌相册](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz) - [x] [Mega.nz](https://mega.nz)
- [x] [一刻相册](https://photo.baidu.com/) - [x] [一刻相册](https://photo.baidu.com/)
@ -73,6 +74,8 @@
- [x] [115](https://115.com/) - [x] [115](https://115.com/)
- [X] Cloudreve - [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/) - [x] [Dropbox](https://www.dropbox.com/)
- [x] [飞机盘](https://www.feijipan.com/)
- [x] [多吉云](https://www.dogecloud.com/product/oss)
- [x] 部署方便,开箱即用 - [x] 部署方便,开箱即用
- [x] 文件预览PDF、markdown、代码、纯文本…… - [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览 - [x] 画廊模式下的图像预览
@ -110,15 +113,15 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
### 特别赞助 ### 特别赞助
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (国内API服务器赞助) - [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器iPhoneiPadMacApple TV全平台支持。
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.pw/) - [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/) - [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
## 贡献者 ## 贡献者
Thanks goes to these wonderful people: Thanks goes to these wonderful people:
[![Contributors](http://contributors.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors) [![Contributors](http://contrib.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors)
## 许可 ## 许可

View File

@ -1,5 +1,5 @@
<div align="center"> <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> <p><em>🗂Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p>
<div> <div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3"> <a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
@ -45,7 +45,7 @@
- [x] マルチストレージ - [x] マルチストレージ
- [x] ローカルストレージ - [x] ローカルストレージ
- [x] [Aliyundrive](https://www.aliyundrive.com/) - [x] [Aliyundrive](https://www.alipan.com/)
- [x] OneDrive / Sharepoint ([グローバル](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us) - [x] OneDrive / Sharepoint ([グローバル](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family) - [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com/) - [x] [GoogleDrive](https://drive.google.com/)
@ -66,7 +66,8 @@
- [x] [Quark](https://pan.quark.cn) - [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com) - [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/) - [x] [Lanzou](https://www.lanzou.com/)
- [x] [Aliyundrive share](https://www.aliyundrive.com/) - [x] [ILanzou](https://www.ilanzou.com/)
- [x] [Aliyundrive share](https://www.alipan.com/)
- [x] [Google photo](https://photos.google.com/) - [x] [Google photo](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz) - [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com/) - [x] [Baidu photo](https://photo.baidu.com/)
@ -74,6 +75,8 @@
- [x] [115](https://115.com/) - [x] [115](https://115.com/)
- [X] Cloudreve - [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/) - [x] [Dropbox](https://www.dropbox.com/)
- [x] [FeijiPan](https://www.feijipan.com/)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [x] デプロイが簡単で、すぐに使える - [x] デプロイが簡単で、すぐに使える
- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...) - [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)
- [x] ギャラリーモードでの画像プレビュー - [x] ギャラリーモードでの画像プレビュー
@ -112,15 +115,15 @@ https://alist.nn.ci/guide/sponsor.html
### スペシャルスポンサー ### スペシャルスポンサー
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (sponsored Chinese API server) - [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://zhaoziyuan.pw/) - [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/) - [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
## コントリビューター ## コントリビューター
これらの素晴らしい人々に感謝します: これらの素晴らしい人々に感謝します:
[![Contributors](http://contributors.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors) [![Contributors](http://contrib.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors)
## ライセンス ## ライセンス

120
build.sh
View File

@ -49,6 +49,7 @@ BuildWinArm64() {
export GOARCH=arm64 export GOARCH=arm64
export CC=$(pwd)/wrapper/zcc-arm64 export CC=$(pwd)/wrapper/zcc-arm64
export CXX=$(pwd)/wrapper/zcxx-arm64 export CXX=$(pwd)/wrapper/zcxx-arm64
export CGO_ENABLED=1
go build -o "$1" -ldflags="$ldflags" -tags=jsoniter . go build -o "$1" -ldflags="$ldflags" -tags=jsoniter .
} }
@ -75,7 +76,7 @@ BuildDev() {
export CGO_ENABLED=1 export CGO_ENABLED=1
go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter . go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
done done
xgo -targets=windows/amd64,darwin/amd64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter . xgo -targets=windows/amd64,darwin/amd64,darwin/arm64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
mv alist-* dist mv alist-* dist
cd dist cd dist
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
@ -84,11 +85,81 @@ BuildDev() {
cat md5.txt 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() { BuildDocker() {
PrepareBuildDocker
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter . go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
} }
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"
lib_tgz="build/${i}.tgz"
curl -L -o "${lib_tgz}" "${url}"
tar xf "${lib_tgz}" --strip-components 1 -C build/musl-libs
rm -f "${lib_tgz}"
done
}
BuildDockerMultiplatform() {
PrepareBuildDocker
# run PrepareBuildDockerMusl before build
export PATH=$PATH:$PWD/build/musl-libs/bin
docker_lflags="--extldflags '-static -fpic' $ldflags"
export CGO_ENABLED=1
OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-s390x)
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc s390x-linux-musl-gcc)
for i in "${!OS_ARCHES[@]}"; do
os_arch=${OS_ARCHES[$i]}
cgo_cc=${CGO_ARGS[$i]}
os=${os_arch%%-*}
arch=${os_arch##*-}
export GOOS=$os
export GOARCH=$arch
export CC=${cgo_cc}
echo "building for $os_arch"
go build -o build/$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter .
done
DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)
CGO_ARGS=(armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc)
GO_ARM=(6 7)
export GOOS=linux
export GOARCH=arm
for i in "${!DOCKER_ARM_ARCHES[@]}"; do
docker_arch=${DOCKER_ARM_ARCHES[$i]}
cgo_cc=${CGO_ARGS[$i]}
export GOARM=${GO_ARM[$i]}
export CC=${cgo_cc}
echo "building for $docker_arch"
go build -o build/${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter .
done
}
BuildRelease() { BuildRelease() {
rm -rf .git/
mkdir -p "build"
BuildWinArm64 ./build/alist-windows-arm64.exe
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
# why? Because some target platforms seem to have issues with upx compression
upx -9 ./alist-linux-amd64
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
upx -9 ./alist-windows-amd64-upx.exe
mv alist-* build
}
BuildReleaseLinuxMusl() {
rm -rf .git/ rm -rf .git/
mkdir -p "build" mkdir -p "build"
muslflags="--extldflags '-static -fpic' $ldflags" muslflags="--extldflags '-static -fpic' $ldflags"
@ -112,13 +183,6 @@ BuildRelease() {
export CGO_ENABLED=1 export CGO_ENABLED=1
go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter . go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
done done
BuildWinArm64 ./build/alist-windows-arm64.exe
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
# why? Because some target platforms seem to have issues with upx compression
upx -9 ./alist-linux-amd64
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
upx -9 ./alist-windows-amd64-upx.exe
mv alist-* build
} }
BuildReleaseLinuxMuslArm() { BuildReleaseLinuxMuslArm() {
@ -154,6 +218,27 @@ BuildReleaseLinuxMuslArm() {
done done
} }
BuildReleaseAndroid() {
rm -rf .git/
mkdir -p "build"
wget https://dl.google.com/android/repository/android-ndk-r26b-linux.zip
unzip android-ndk-r26b-linux.zip
rm android-ndk-r26b-linux.zip
OS_ARCHES=(amd64 arm64 386 arm)
CGO_ARGS=(x86_64-linux-android24-clang aarch64-linux-android24-clang i686-linux-android24-clang armv7a-linux-androideabi24-clang)
for i in "${!OS_ARCHES[@]}"; do
os_arch=${OS_ARCHES[$i]}
cgo_cc=$(realpath android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/${CGO_ARGS[$i]})
echo building for android-${os_arch}
export GOOS=android
export GOARCH=${os_arch##*-}
export CC=${cgo_cc}
export CGO_ENABLED=1
go build -o ./build/$appName-android-$os_arch -ldflags="$ldflags" -tags=jsoniter .
android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip ./build/$appName-android-$os_arch
done
}
MakeRelease() { MakeRelease() {
cd build cd build
mkdir compress mkdir compress
@ -161,6 +246,11 @@ MakeRelease() {
cp "$i" alist cp "$i" alist
tar -czvf compress/"$i".tar.gz alist tar -czvf compress/"$i".tar.gz alist
rm -f alist rm -f alist
done
for i in $(find . -type f -name "$appName-android-*"); do
cp "$i" alist
tar -czvf compress/"$i".tar.gz alist
rm -f alist
done done
for i in $(find . -type f -name "$appName-darwin-*"); do for i in $(find . -type f -name "$appName-darwin-*"); do
cp "$i" alist cp "$i" alist
@ -182,6 +272,8 @@ if [ "$1" = "dev" ]; then
FetchWebDev FetchWebDev
if [ "$2" = "docker" ]; then if [ "$2" = "docker" ]; then
BuildDocker BuildDocker
elif [ "$2" = "docker-multiplatform" ]; then
BuildDockerMultiplatform
else else
BuildDev BuildDev
fi fi
@ -189,13 +281,25 @@ elif [ "$1" = "release" ]; then
FetchWebRelease FetchWebRelease
if [ "$2" = "docker" ]; then if [ "$2" = "docker" ]; then
BuildDocker BuildDocker
elif [ "$2" = "docker-multiplatform" ]; then
BuildDockerMultiplatform
elif [ "$2" = "linux_musl_arm" ]; then elif [ "$2" = "linux_musl_arm" ]; then
BuildReleaseLinuxMuslArm BuildReleaseLinuxMuslArm
MakeRelease "md5-linux-musl-arm.txt" MakeRelease "md5-linux-musl-arm.txt"
elif [ "$2" = "linux_musl" ]; then
BuildReleaseLinuxMusl
MakeRelease "md5-linux-musl.txt"
elif [ "$2" = "android" ]; then
BuildReleaseAndroid
MakeRelease "md5-android.txt"
else else
BuildRelease BuildRelease
MakeRelease "md5.txt" MakeRelease "md5.txt"
fi fi
elif [ "$1" = "prepare" ]; then
if [ "$2" = "docker-multiplatform" ]; then
PrepareBuildDockerMusl
fi
else else
echo -e "Parameter error" echo -e "Parameter error"
fi fi

View File

@ -19,6 +19,7 @@ var AdminCmd = &cobra.Command{
Short: "Show admin user's info and some operations about admin user's password", Short: "Show admin user's info and some operations about admin user's password",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
Init() Init()
defer Release()
admin, err := op.GetAdmin() admin, err := op.GetAdmin()
if err != nil { if err != nil {
utils.Log.Errorf("failed get admin user: %+v", err) utils.Log.Errorf("failed get admin user: %+v", err)
@ -57,6 +58,7 @@ var ShowTokenCmd = &cobra.Command{
Short: "Show admin token", Short: "Show admin token",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
Init() Init()
defer Release()
token := setting.GetStr(conf.Token) token := setting.GetStr(conf.Token)
utils.Log.Infof("Admin token: %s", token) utils.Log.Infof("Admin token: %s", token)
}, },
@ -64,6 +66,7 @@ var ShowTokenCmd = &cobra.Command{
func setAdminPassword(pwd string) { func setAdminPassword(pwd string) {
Init() Init()
defer Release()
admin, err := op.GetAdmin() admin, err := op.GetAdmin()
if err != nil { if err != nil {
utils.Log.Errorf("failed get admin user: %+v", err) utils.Log.Errorf("failed get admin user: %+v", err)

View File

@ -15,6 +15,7 @@ var Cancel2FACmd = &cobra.Command{
Short: "Delete 2FA of admin user", Short: "Delete 2FA of admin user",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
Init() Init()
defer Release()
admin, err := op.GetAdmin() admin, err := op.GetAdmin()
if err != nil { if err != nil {
utils.Log.Errorf("failed to get admin user: %+v", err) utils.Log.Errorf("failed to get admin user: %+v", err)

View File

@ -7,6 +7,7 @@ import (
"github.com/alist-org/alist/v3/internal/bootstrap" "github.com/alist-org/alist/v3/internal/bootstrap"
"github.com/alist-org/alist/v3/internal/bootstrap/data" "github.com/alist-org/alist/v3/internal/bootstrap/data"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -19,6 +20,10 @@ func Init() {
bootstrap.InitIndex() bootstrap.InitIndex()
} }
func Release() {
db.Close()
}
var pid = -1 var pid = -1
var pidFile string var pidFile string

View File

@ -5,6 +5,8 @@ import (
"os" "os"
"github.com/alist-org/alist/v3/cmd/flags" "github.com/alist-org/alist/v3/cmd/flags"
_ "github.com/alist-org/alist/v3/drivers"
_ "github.com/alist-org/alist/v3/internal/offline_download"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View File

@ -2,6 +2,7 @@ package cmd
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@ -13,7 +14,6 @@ import (
"time" "time"
"github.com/alist-org/alist/v3/cmd/flags" "github.com/alist-org/alist/v3/cmd/flags"
_ "github.com/alist-org/alist/v3/drivers"
"github.com/alist-org/alist/v3/internal/bootstrap" "github.com/alist-org/alist/v3/internal/bootstrap"
"github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
@ -35,9 +35,9 @@ the address is defined in config file`,
utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart) utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second) time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
} }
bootstrap.InitAria2() bootstrap.InitOfflineDownloadTools()
bootstrap.InitQbittorrent()
bootstrap.LoadStorages() bootstrap.LoadStorages()
bootstrap.InitTaskManager()
if !flags.Debug && !flags.Dev { if !flags.Debug && !flags.Dev {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
} }
@ -51,7 +51,7 @@ the address is defined in config file`,
httpSrv = &http.Server{Addr: httpBase, Handler: r} httpSrv = &http.Server{Addr: httpBase, Handler: r}
go func() { go func() {
err := httpSrv.ListenAndServe() err := httpSrv.ListenAndServe()
if err != nil && err != http.ErrServerClosed { if err != nil && !errors.Is(err, http.ErrServerClosed) {
utils.Log.Fatalf("failed to start http: %s", err.Error()) utils.Log.Fatalf("failed to start http: %s", err.Error())
} }
}() }()
@ -62,7 +62,7 @@ the address is defined in config file`,
httpsSrv = &http.Server{Addr: httpsBase, Handler: r} httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
go func() { go func() {
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile) err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
if err != nil && err != http.ErrServerClosed { if err != nil && !errors.Is(err, http.ErrServerClosed) {
utils.Log.Fatalf("failed to start https: %s", err.Error()) utils.Log.Fatalf("failed to start https: %s", err.Error())
} }
}() }()
@ -86,11 +86,32 @@ the address is defined in config file`,
} }
} }
err = unixSrv.Serve(listener) err = unixSrv.Serve(listener)
if err != nil && err != http.ErrServerClosed { if err != nil && !errors.Is(err, http.ErrServerClosed) {
utils.Log.Fatalf("failed to start unix: %s", err.Error()) utils.Log.Fatalf("failed to start unix: %s", err.Error())
} }
}() }()
} }
if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
s3r := gin.New()
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.InitS3(s3r)
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
utils.Log.Infof("start S3 server @ %s", s3Base)
go func() {
var err error
if conf.Conf.S3.SSL {
httpsSrv = &http.Server{Addr: s3Base, Handler: s3r}
err = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
}
if !conf.Conf.S3.SSL {
httpSrv = &http.Server{Addr: s3Base, Handler: s3r}
err = httpSrv.ListenAndServe()
}
if err != nil && !errors.Is(err, http.ErrServerClosed) {
utils.Log.Fatalf("failed to start s3 server: %s", err.Error())
}
}()
}
// Wait for interrupt signal to gracefully shutdown the server with // Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 1 second. // a timeout of 1 second.
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
@ -100,7 +121,7 @@ the address is defined in config file`,
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
utils.Log.Println("Shutdown server...") utils.Log.Println("Shutdown server...")
Release()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
var wg sync.WaitGroup var wg sync.WaitGroup

View File

@ -31,6 +31,7 @@ var disableStorageCmd = &cobra.Command{
} }
mountPath := args[0] mountPath := args[0]
Init() Init()
defer Release()
storage, err := db.GetStorageByMountPath(mountPath) storage, err := db.GetStorageByMountPath(mountPath)
if err != nil { if err != nil {
utils.Log.Errorf("failed to query storage: %+v", err) utils.Log.Errorf("failed to query storage: %+v", err)
@ -89,6 +90,7 @@ var listStorageCmd = &cobra.Command{
Short: "List all storages", Short: "List all storages",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
Init() Init()
defer Release()
storages, _, err := db.GetStorages(1, -1) storages, _, err := db.GetStorages(1, -1)
if err != nil { if err != nil {
utils.Log.Errorf("failed to query storages: %+v", err) utils.Log.Errorf("failed to query storages: %+v", err)

View File

@ -2,19 +2,22 @@ package _115
import ( import (
"context" "context"
"os" "strings"
driver115 "github.com/SheltonZhu/115driver/pkg/driver" driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/time/rate"
) )
type Pan115 struct { type Pan115 struct {
model.Storage model.Storage
Addition Addition
client *driver115.Pan115Client client *driver115.Pan115Client
limiter *rate.Limiter
} }
func (d *Pan115) Config() driver.Config { func (d *Pan115) Config() driver.Config {
@ -26,29 +29,43 @@ func (d *Pan115) GetAddition() driver.Additional {
} }
func (d *Pan115) Init(ctx context.Context) error { func (d *Pan115) Init(ctx context.Context) error {
if d.LimitRate > 0 {
d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
}
return d.login() return d.login()
} }
func (d *Pan115) WaitLimit(ctx context.Context) error {
if d.limiter != nil {
return d.limiter.Wait(ctx)
}
return nil
}
func (d *Pan115) Drop(ctx context.Context) error { func (d *Pan115) Drop(ctx context.Context) error {
return nil return nil
} }
func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
files, err := d.getFiles(dir.GetID()) files, err := d.getFiles(dir.GetID())
if err != nil && !errors.Is(err, driver115.ErrNotExist) { if err != nil && !errors.Is(err, driver115.ErrNotExist) {
return nil, err return nil, err
} }
return utils.SliceConvert(files, func(src driver115.File) (model.Obj, error) { return utils.SliceConvert(files, func(src FileObj) (model.Obj, error) {
return src, nil return &src, nil
}) })
} }
func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
downloadInfo, err := d.client. if err := d.WaitLimit(ctx); err != nil {
SetUserAgent(driver115.UA115Browser). return nil, err
Download(file.(driver115.File).PickCode) }
// recover for upload var userAgent = args.Header.Get("User-Agent")
d.client.SetUserAgent(driver115.UA115Desktop) downloadInfo, err := d.
DownloadWithUA(file.(*FileObj).PickCode, userAgent)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -60,6 +77,9 @@ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
} }
func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if err := d.WaitLimit(ctx); err != nil {
return err
}
if _, err := d.client.Mkdir(parentDir.GetID(), dirName); err != nil { if _, err := d.client.Mkdir(parentDir.GetID(), dirName); err != nil {
return err return err
} }
@ -67,31 +87,99 @@ func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
} }
func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
if err := d.WaitLimit(ctx); err != nil {
return err
}
return d.client.Move(dstDir.GetID(), srcObj.GetID()) return d.client.Move(dstDir.GetID(), srcObj.GetID())
} }
func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) error { func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
if err := d.WaitLimit(ctx); err != nil {
return err
}
return d.client.Rename(srcObj.GetID(), newName) return d.client.Rename(srcObj.GetID(), newName)
} }
func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
if err := d.WaitLimit(ctx); err != nil {
return err
}
return d.client.Copy(dstDir.GetID(), srcObj.GetID()) return d.client.Copy(dstDir.GetID(), srcObj.GetID())
} }
func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error { func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {
if err := d.WaitLimit(ctx); err != nil {
return err
}
return d.client.Delete(obj.GetID()) return d.client.Delete(obj.GetID())
} }
func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
tempFile, err := utils.CreateTempFile(stream.GetReadCloser(), stream.GetSize()) if err := d.WaitLimit(ctx); err != nil {
return err
}
var (
fastInfo *driver115.UploadInitResp
dirID = dstDir.GetID()
)
if ok, err := d.client.UploadAvailable(); err != nil || !ok {
return err
}
if stream.GetSize() > d.client.UploadMetaInfo.SizeLimit {
return driver115.ErrUploadTooLarge
}
//if digest, err = d.client.GetDigestResult(stream); err != nil {
// return err
//}
const PreHashSize int64 = 128 * utils.KB
hashSize := PreHashSize
if stream.GetSize() < PreHashSize {
hashSize = stream.GetSize()
}
reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: hashSize})
if err != nil { if err != nil {
return err return err
} }
defer func() { preHash, err := utils.HashReader(utils.SHA1, reader)
_ = tempFile.Close() if err != nil {
_ = os.Remove(tempFile.Name()) return err
}() }
return d.client.UploadFastOrByMultipart(dstDir.GetID(), stream.GetName(), stream.GetSize(), tempFile) preHash = strings.ToUpper(preHash)
fullHash := stream.GetHash().GetHash(utils.SHA1)
if len(fullHash) <= 0 {
tmpF, err := stream.CacheFullInTempFile()
if err != nil {
return err
}
fullHash, err = utils.HashFile(utils.SHA1, tmpF)
if err != nil {
return err
}
}
fullHash = strings.ToUpper(fullHash)
// rapid-upload
// note that 115 add timeout for rapid-upload,
// and "sig invalid" err is thrown even when the hash is correct after timeout.
if fastInfo, err = d.rapidUpload(stream.GetSize(), stream.GetName(), dirID, preHash, fullHash, stream); err != nil {
return err
}
if matched, err := fastInfo.Ok(); err != nil {
return err
} else if matched {
return nil
}
// 闪传失败,上传
if stream.GetSize() <= utils.KB { // 文件大小小于1KB改用普通模式上传
return d.client.UploadByOSS(&fastInfo.UploadOSSParams, stream, dirID)
}
// 分片上传
return d.UploadByMultipart(&fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID)
} }
var _ driver.Driver = (*Pan115)(nil) var _ driver.Driver = (*Pan115)(nil)

View File

@ -6,18 +6,20 @@ import (
) )
type Addition struct { type Addition struct {
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"` Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"` QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
PageSize int64 `json:"page_size" type:"number" default:"56" help:"list api per page size of 115 driver"` QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,linux,mac,windows,tv" default:"linux" help:"select the QR code device, default linux"`
PageSize int64 `json:"page_size" type:"number" default:"56" help:"list api per page size of 115 driver"`
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
driver.RootID driver.RootID
} }
var config = driver.Config{ var config = driver.Config{
Name: "115 Cloud", Name: "115 Cloud",
DefaultRoot: "0", DefaultRoot: "0",
OnlyProxy: true, //OnlyProxy: true,
OnlyLocal: true, //OnlyLocal: true,
NoOverwriteUpload: true, //NoOverwriteUpload: true,
} }
func init() { func init() {

View File

@ -3,6 +3,20 @@ package _115
import ( import (
"github.com/SheltonZhu/115driver/pkg/driver" "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"time"
) )
var _ model.Obj = (*driver.File)(nil) var _ model.Obj = (*FileObj)(nil)
type FileObj struct {
driver.File
}
func (f *FileObj) CreateTime() time.Time {
return f.File.CreateTime
}
func (f *FileObj) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.SHA1, f.Sha1)
}

View File

@ -1,31 +1,48 @@
package _115 package _115
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
crypto "github.com/gaoyb7/115drive-webdav/115"
"github.com/orzogc/fake115uploader/cipher"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
var UserAgent = driver.UA115Desktop var UserAgent = driver115.UA115Desktop
func (d *Pan115) login() error { func (d *Pan115) login() error {
var err error var err error
opts := []driver.Option{ opts := []driver115.Option{
driver.UA(UserAgent), driver115.UA(UserAgent),
func(c *driver.Pan115Client) { func(c *driver115.Pan115Client) {
c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify}) c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
}, },
} }
d.client = driver.New(opts...) d.client = driver115.New(opts...)
cr := &driver.Credential{} cr := &driver115.Credential{}
if d.Addition.QRCodeToken != "" { if d.Addition.QRCodeToken != "" {
s := &driver.QRCodeSession{ s := &driver115.QRCodeSession{
UID: d.Addition.QRCodeToken, UID: d.Addition.QRCodeToken,
} }
if cr, err = d.client.QRCodeLogin(s); err != nil { if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
return errors.Wrap(err, "failed to login by qrcode") return errors.Wrap(err, "failed to login by qrcode")
} }
d.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID) d.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
@ -41,17 +58,422 @@ func (d *Pan115) login() error {
return d.client.LoginCheck() return d.client.LoginCheck()
} }
func (d *Pan115) getFiles(fileId string) ([]driver.File, error) { func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
res := make([]driver.File, 0) res := make([]FileObj, 0)
if d.PageSize <= 0 { if d.PageSize <= 0 {
d.PageSize = driver.FileListLimit d.PageSize = driver115.FileListLimit
} }
files, err := d.client.ListWithLimit(fileId, d.PageSize) files, err := d.client.ListWithLimit(fileId, d.PageSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, file := range *files { for _, file := range *files {
res = append(res, file) res = append(res, FileObj{file})
} }
return res, nil return res, nil
} }
const (
appVer = "2.0.3.6"
)
func (c *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
key := crypto.GenerateKey()
result := driver115.DownloadResp{}
params, err := utils.Json.Marshal(map[string]string{"pickcode": pickCode})
if err != nil {
return nil, err
}
data := crypto.Encode(params, key)
bodyReader := strings.NewReader(url.Values{"data": []string{data}}.Encode())
reqUrl := fmt.Sprintf("%s?t=%s", driver115.ApiDownloadGetUrl, driver115.Now().String())
req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cookie", c.Cookie)
req.Header.Set("User-Agent", ua)
resp, err := c.client.Client.GetClient().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err := utils.Json.Unmarshal(body, &result); err != nil {
return nil, err
}
if err = result.Err(string(body)); err != nil {
return nil, err
}
bytes, err := crypto.Decode(string(result.EncodedData), key)
if err != nil {
return nil, err
}
downloadInfo := driver115.DownloadData{}
if err := utils.Json.Unmarshal(bytes, &downloadInfo); err != nil {
return nil, err
}
for _, info := range downloadInfo {
if info.FileSize < 0 {
return nil, driver115.ErrDownloadEmpty
}
info.Header = resp.Request.Header
return info, nil
}
return nil, driver115.ErrUnexpected
}
func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) {
var (
ecdhCipher *cipher.EcdhCipher
encrypted []byte
decrypted []byte
encodedToken string
err error
target = "U_1_" + dirID
bodyBytes []byte
result = driver115.UploadInitResp{}
fileSizeStr = strconv.FormatInt(fileSize, 10)
)
if ecdhCipher, err = cipher.NewEcdhCipher(); err != nil {
return nil, err
}
userID := strconv.FormatInt(d.client.UserID, 10)
form := url.Values{}
form.Set("appid", "0")
form.Set("appversion", appVer)
form.Set("userid", userID)
form.Set("filename", fileName)
form.Set("filesize", fileSizeStr)
form.Set("fileid", fileID)
form.Set("target", target)
form.Set("sig", d.client.GenerateSignature(fileID, target))
signKey, signVal := "", ""
for retry := true; retry; {
t := driver115.Now()
if encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil {
return nil, err
}
params := map[string]string{
"k_ec": encodedToken,
}
form.Set("t", t.String())
form.Set("token", d.client.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))
if signKey != "" && signVal != "" {
form.Set("sign_key", signKey)
form.Set("sign_val", signVal)
}
if encrypted, err = ecdhCipher.Encrypt([]byte(form.Encode())); err != nil {
return nil, err
}
req := d.client.NewRequest().
SetQueryParams(params).
SetBody(encrypted).
SetHeaderVerbatim("Content-Type", "application/x-www-form-urlencoded").
SetDoNotParseResponse(true)
resp, err := req.Post(driver115.ApiUploadInit)
if err != nil {
return nil, err
}
data := resp.RawBody()
defer data.Close()
if bodyBytes, err = io.ReadAll(data); err != nil {
return nil, err
}
if decrypted, err = ecdhCipher.Decrypt(bodyBytes); err != nil {
return nil, err
}
if err = driver115.CheckErr(json.Unmarshal(decrypted, &result), &result, resp); err != nil {
return nil, err
}
if result.Status == 7 {
// Update signKey & signVal
signKey = result.SignKey
signVal, err = UploadDigestRange(stream, result.SignCheck)
if err != nil {
return nil, err
}
} else {
retry = false
}
result.SHA1 = fileID
}
return &result, nil
}
func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result string, err error) {
var start, end int64
if _, err = fmt.Sscanf(rangeSpec, "%d-%d", &start, &end); err != nil {
return
}
length := end - start + 1
reader, err := stream.RangeRead(http_range.Range{Start: start, Length: length})
hashStr, err := utils.HashReader(utils.SHA1, reader)
if err != nil {
return "", err
}
result = strings.ToUpper(hashStr)
return
}
// UploadByMultipart upload by mutipart blocks
func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize int64, stream model.FileStreamer, dirID string, opts ...driver115.UploadMultipartOption) error {
var (
chunks []oss.FileChunk
parts []oss.UploadPart
imur oss.InitiateMultipartUploadResult
ossClient *oss.Client
bucket *oss.Bucket
ossToken *driver115.UploadOSSTokenResp
err error
)
tmpF, err := stream.CacheFullInTempFile()
if err != nil {
return err
}
options := driver115.DefalutUploadMultipartOptions()
if len(opts) > 0 {
for _, f := range opts {
f(options)
}
}
if ossToken, err = d.client.GetOSSToken(); err != nil {
return err
}
if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret); err != nil {
return err
}
if bucket, err = ossClient.Bucket(params.Bucket); err != nil {
return err
}
// ossToken一小时后就会失效所以每50分钟重新获取一次
ticker := time.NewTicker(options.TokenRefreshTime)
defer ticker.Stop()
// 设置超时
timeout := time.NewTimer(options.Timeout)
if chunks, err = SplitFile(fileSize); err != nil {
return err
}
if imur, err = bucket.InitiateMultipartUpload(params.Object,
oss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken),
oss.UserAgentHeader(driver115.OSSUserAgent),
); err != nil {
return err
}
wg := sync.WaitGroup{}
wg.Add(len(chunks))
chunksCh := make(chan oss.FileChunk)
errCh := make(chan error)
UploadedPartsCh := make(chan oss.UploadPart)
quit := make(chan struct{})
// producer
go chunksProducer(chunksCh, chunks)
go func() {
wg.Wait()
quit <- struct{}{}
}()
// consumers
for i := 0; i < options.ThreadsNum; i++ {
go func(threadId int) {
defer func() {
if r := recover(); r != nil {
errCh <- fmt.Errorf("recovered in %v", r)
}
}()
for chunk := range chunksCh {
var part oss.UploadPart // 出现错误就继续尝试共尝试3次
for retry := 0; retry < 3; retry++ {
select {
case <-ticker.C:
if ossToken, err = d.client.GetOSSToken(); err != nil { // 到时重新获取ossToken
errCh <- errors.Wrap(err, "刷新token时出现错误")
}
default:
}
buf := make([]byte, chunk.Size)
if _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) {
continue
}
b := bytes.NewBuffer(buf)
if part, err = bucket.UploadPart(imur, b, chunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {
break
}
}
if err != nil {
errCh <- errors.Wrap(err, fmt.Sprintf("上传 %s 的第%d个分片时出现错误%v", stream.GetName(), chunk.Number, err))
}
UploadedPartsCh <- part
}
}(i)
}
go func() {
for part := range UploadedPartsCh {
parts = append(parts, part)
wg.Done()
}
}()
LOOP:
for {
select {
case <-ticker.C:
// 到时重新获取ossToken
if ossToken, err = d.client.GetOSSToken(); err != nil {
return err
}
case <-quit:
break LOOP
case <-errCh:
return err
case <-timeout.C:
return fmt.Errorf("time out")
}
}
// EOF错误是xml的Unmarshal导致的响应其实是json格式所以实际上上传是成功的
if _, err = bucket.CompleteMultipartUpload(imur, parts, driver115.OssOption(params, ossToken)...); err != nil && !errors.Is(err, io.EOF) {
// 当文件名含有 &< 这两个字符之一时响应的xml解析会出现错误实际上上传是成功的
if filename := filepath.Base(stream.GetName()); !strings.ContainsAny(filename, "&<") {
return err
}
}
return d.checkUploadStatus(dirID, params.SHA1)
}
func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
for _, chunk := range chunks {
ch <- chunk
}
}
func (d *Pan115) checkUploadStatus(dirID, sha1 string) error {
// 验证上传是否成功
req := d.client.NewRequest().ForceContentType("application/json;charset=UTF-8")
opts := []driver115.GetFileOptions{
driver115.WithOrder(driver115.FileOrderByTime),
driver115.WithShowDirEnable(false),
driver115.WithAsc(false),
driver115.WithLimit(500),
}
fResp, err := driver115.GetFiles(req, dirID, opts...)
if err != nil {
return err
}
for _, fileInfo := range fResp.Files {
if fileInfo.Sha1 == sha1 {
return nil
}
}
return driver115.ErrUploadFailed
}
func SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {
for i := int64(1); i < 10; i++ {
if fileSize < i*utils.GB { // 文件大小小于iGB时分为i*1000片
if chunks, err = SplitFileByPartNum(fileSize, int(i*1000)); err != nil {
return
}
break
}
}
if fileSize > 9*utils.GB { // 文件大小大于9GB时分为10000片
if chunks, err = SplitFileByPartNum(fileSize, 10000); err != nil {
return
}
}
// 单个分片大小不能小于100KB
if chunks[0].Size < 100*utils.KB {
if chunks, err = SplitFileByPartSize(fileSize, 100*utils.KB); err != nil {
return
}
}
return
}
// SplitFileByPartNum splits big file into parts by the num of parts.
// Split the file with specified parts count, returns the split result when error is nil.
func SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {
if chunkNum <= 0 || chunkNum > 10000 {
return nil, errors.New("chunkNum invalid")
}
if int64(chunkNum) > fileSize {
return nil, errors.New("oss: chunkNum invalid")
}
var chunks []oss.FileChunk
var chunk = oss.FileChunk{}
var chunkN = (int64)(chunkNum)
for i := int64(0); i < chunkN; i++ {
chunk.Number = int(i + 1)
chunk.Offset = i * (fileSize / chunkN)
if i == chunkN-1 {
chunk.Size = fileSize/chunkN + fileSize%chunkN
} else {
chunk.Size = fileSize / chunkN
}
chunks = append(chunks, chunk)
}
return chunks, nil
}
// SplitFileByPartSize splits big file into parts by the size of parts.
// Splits the file by the part size. Returns the FileChunk when error is nil.
func SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, error) {
if chunkSize <= 0 {
return nil, errors.New("chunkSize invalid")
}
var chunkN = fileSize / chunkSize
if chunkN >= 10000 {
return nil, errors.New("Too many parts, please increase part size")
}
var chunks []oss.FileChunk
var chunk = oss.FileChunk{}
for i := int64(0); i < chunkN; i++ {
chunk.Number = int(i + 1)
chunk.Offset = i * chunkSize
chunk.Size = chunkSize
chunks = append(chunks, chunk)
}
if fileSize%chunkSize > 0 {
chunk.Number = len(chunks) + 1
chunk.Offset = int64(len(chunks)) * chunkSize
chunk.Size = fileSize % chunkSize
chunks = append(chunks, chunk)
}
return chunks, nil
}

112
drivers/115_share/driver.go Normal file
View File

@ -0,0 +1,112 @@
package _115_share
import (
"context"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"golang.org/x/time/rate"
)
type Pan115Share struct {
model.Storage
Addition
client *driver115.Pan115Client
limiter *rate.Limiter
}
func (d *Pan115Share) Config() driver.Config {
return config
}
func (d *Pan115Share) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Pan115Share) Init(ctx context.Context) error {
if d.LimitRate > 0 {
d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
}
return d.login()
}
func (d *Pan115Share) WaitLimit(ctx context.Context) error {
if d.limiter != nil {
return d.limiter.Wait(ctx)
}
return nil
}
func (d *Pan115Share) Drop(ctx context.Context) error {
return nil
}
func (d *Pan115Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
files := make([]driver115.ShareFile, 0)
fileResp, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, dir.GetID(), driver115.QueryLimit(int(d.PageSize)))
if err != nil {
return nil, err
}
files = append(files, fileResp.Data.List...)
total := fileResp.Data.Count
count := len(fileResp.Data.List)
for total > count {
fileResp, err := d.client.GetShareSnap(
d.ShareCode, d.ReceiveCode, dir.GetID(),
driver115.QueryLimit(int(d.PageSize)), driver115.QueryOffset(count),
)
if err != nil {
return nil, err
}
files = append(files, fileResp.Data.List...)
count += len(fileResp.Data.List)
}
return utils.SliceConvert(files, transFunc)
}
func (d *Pan115Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
downloadInfo, err := d.client.DownloadByShareCode(d.ShareCode, d.ReceiveCode, file.GetID())
if err != nil {
return nil, err
}
return &model.Link{URL: downloadInfo.URL.URL}, nil
}
func (d *Pan115Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotSupport
}
func (d *Pan115Share) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *Pan115Share) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return errs.NotSupport
}
func (d *Pan115Share) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *Pan115Share) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotSupport
}
func (d *Pan115Share) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
return errs.NotSupport
}
var _ driver.Driver = (*Pan115Share)(nil)

34
drivers/115_share/meta.go Normal file
View File

@ -0,0 +1,34 @@
package _115_share
import (
"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" help:"one of QR code token and cookie required"`
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,linux,mac,windows,tv" default:"linux" help:"select the QR code device, default linux"`
PageSize int64 `json:"page_size" type:"number" default:"20" help:"list api per page size of 115 driver"`
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
ShareCode string `json:"share_code" type:"text" required:"true" help:"share code of 115 share link"`
ReceiveCode string `json:"receive_code" type:"text" required:"true" help:"receive code of 115 share link"`
driver.RootID
}
var config = driver.Config{
Name: "115 Share",
DefaultRoot: "",
// OnlyProxy: true,
// OnlyLocal: true,
CheckStatus: false,
Alert: "",
NoOverwriteUpload: true,
NoUpload: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Pan115Share{}
})
}

111
drivers/115_share/utils.go Normal file
View File

@ -0,0 +1,111 @@
package _115_share
import (
"fmt"
"strconv"
"time"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
)
var _ model.Obj = (*FileObj)(nil)
type FileObj struct {
Size int64
Sha1 string
Utm time.Time
FileName string
isDir bool
FileID string
}
func (f *FileObj) CreateTime() time.Time {
return f.Utm
}
func (f *FileObj) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.SHA1, f.Sha1)
}
func (f *FileObj) GetSize() int64 {
return f.Size
}
func (f *FileObj) GetName() string {
return f.FileName
}
func (f *FileObj) ModTime() time.Time {
return f.Utm
}
func (f *FileObj) IsDir() bool {
return f.isDir
}
func (f *FileObj) GetID() string {
return f.FileID
}
func (f *FileObj) GetPath() string {
return ""
}
func transFunc(sf driver115.ShareFile) (model.Obj, error) {
timeInt, err := strconv.ParseInt(sf.UpdateTime, 10, 64)
if err != nil {
return nil, err
}
var (
utm = time.Unix(timeInt, 0)
isDir = (sf.IsFile == 0)
fileID = string(sf.FileID)
)
if isDir {
fileID = string(sf.CategoryID)
}
return &FileObj{
Size: int64(sf.Size),
Sha1: sf.Sha1,
Utm: utm,
FileName: string(sf.FileName),
isDir: isDir,
FileID: fileID,
}, nil
}
var UserAgent = driver115.UA115Browser
func (d *Pan115Share) login() error {
var err error
opts := []driver115.Option{
driver115.UA(UserAgent),
}
d.client = driver115.New(opts...)
if _, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, ""); err != nil {
return errors.Wrap(err, "failed to get share snap")
}
cr := &driver115.Credential{}
if d.QRCodeToken != "" {
s := &driver115.QRCodeSession{
UID: d.QRCodeToken,
}
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
return errors.Wrap(err, "failed to login by qrcode")
}
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
d.QRCodeToken = ""
} else if d.Cookie != "" {
if err = cr.FromCookie(d.Cookie); err != nil {
return errors.Wrap(err, "failed to login by cookies")
}
d.client.ImportCredential(cr)
} else {
return errors.New("missing cookie or qrcode account")
}
return d.client.LoginCheck()
}

View File

@ -6,10 +6,12 @@ import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"golang.org/x/time/rate"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os" "sync"
"time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
@ -27,6 +29,7 @@ import (
type Pan123 struct { type Pan123 struct {
model.Storage model.Storage
Addition Addition
apiRateLimit sync.Map
} }
func (d *Pan123) Config() driver.Config { func (d *Pan123) Config() driver.Config {
@ -184,15 +187,14 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
// const DEFAULT int64 = 10485760 // const DEFAULT int64 = 10485760
h := md5.New() h := md5.New()
// need to calculate md5 of the full content // need to calculate md5 of the full content
tempFile, err := utils.CreateTempFile(stream.GetReadCloser(), stream.GetSize()) tempFile, err := stream.CacheFullInTempFile()
if err != nil { if err != nil {
return err return err
} }
defer func() { defer func() {
_ = tempFile.Close() _ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}() }()
if _, err = io.Copy(h, tempFile); err != nil { if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
return err return err
} }
_, err = tempFile.Seek(0, io.SeekStart) _, err = tempFile.Seek(0, io.SeekStart)
@ -235,6 +237,9 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err return err
} }
uploader := s3manager.NewUploader(s) uploader := s3manager.NewUploader(s)
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
}
input := &s3manager.UploadInput{ input := &s3manager.UploadInput{
Bucket: &resp.Data.Bucket, Bucket: &resp.Data.Bucket,
Key: &resp.Data.Key, Key: &resp.Data.Key,
@ -253,4 +258,11 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err return err
} }
func (d *Pan123) APIRateLimit(api string) bool {
limiter, _ := d.apiRateLimit.LoadOrStore(api,
rate.NewLimiter(rate.Every(time.Millisecond*700), 1))
ins := limiter.(*rate.Limiter)
return ins.Allow()
}
var _ driver.Driver = (*Pan123)(nil) var _ driver.Driver = (*Pan123)(nil)

View File

@ -1,6 +1,7 @@
package _123 package _123
import ( import (
"github.com/alist-org/alist/v3/pkg/utils"
"net/url" "net/url"
"path" "path"
"strconv" "strconv"
@ -21,6 +22,14 @@ type File struct {
DownloadUrl string `json:"DownloadUrl"` DownloadUrl string `json:"DownloadUrl"`
} }
func (f File) CreateTime() time.Time {
return f.UpdateAt
}
func (f File) GetHash() utils.HashInfo {
return utils.HashInfo{}
}
func (f File) GetPath() string { func (f File) GetPath() string {
return "" return ""
} }

View File

@ -107,7 +107,7 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
if err != nil { if err != nil {
return err return err
} }
up(j * 100 / chunkCount) up(float64(j) * 100 / float64(chunkCount))
} }
} }
// complete s3 upload // complete s3 upload

View File

@ -3,12 +3,18 @@ package _123
import ( import (
"errors" "errors"
"fmt" "fmt"
"hash/crc32"
"math"
"math/rand"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" resty "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@ -18,7 +24,7 @@ const (
Api = "https://www.123pan.com/api" Api = "https://www.123pan.com/api"
AApi = "https://www.123pan.com/a/api" AApi = "https://www.123pan.com/a/api"
BApi = "https://www.123pan.com/b/api" BApi = "https://www.123pan.com/b/api"
MainApi = Api MainApi = BApi
SignIn = MainApi + "/user/sign_in" SignIn = MainApi + "/user/sign_in"
Logout = MainApi + "/user/logout" Logout = MainApi + "/user/logout"
UserInfo = MainApi + "/user/info" UserInfo = MainApi + "/user/info"
@ -37,6 +43,104 @@ const (
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY" //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 GetApi(url string) string {
// vm := js.New()
// vm.Set("url", url[22:])
// r, err := vm.RunString(`
// (function(e){
// function A(t, e) {
// e = 1 < arguments.length && void 0 !== e ? e : 10;
// for (var n = function() {
// for (var t = [], e = 0; e < 256; e++) {
// for (var n = e, r = 0; r < 8; r++)
// n = 1 & n ? 3988292384 ^ n >>> 1 : n >>> 1;
// t[e] = n
// }
// return t
// }(), r = function(t) {
// t = t.replace(/\\r\\n/g, "\\n");
// for (var e = "", n = 0; n < t.length; n++) {
// var r = t.charCodeAt(n);
// r < 128 ? e += String.fromCharCode(r) : e = 127 < r && r < 2048 ? (e += String.fromCharCode(r >> 6 | 192)) + String.fromCharCode(63 & r | 128) : (e = (e += String.fromCharCode(r >> 12 | 224)) + String.fromCharCode(r >> 6 & 63 | 128)) + String.fromCharCode(63 & r | 128)
// }
// return e
// }(t), a = -1, i = 0; i < r.length; i++)
// a = a >>> 8 ^ n[255 & (a ^ r.charCodeAt(i))];
// return (a = (-1 ^ a) >>> 0).toString(e)
// }
//
// function v(t) {
// return (v = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(t) {
// return typeof t
// }
// : function(t) {
// return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t
// }
// )(t)
// }
//
// for (p in a = Math.round(1e7 * Math.random()),
// o = Math.round(((new Date).getTime() + 60 * (new Date).getTimezoneOffset() * 1e3 + 288e5) / 1e3).toString(),
// m = ["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"],
// u = function(t, e, n) {
// var r;
// n = 2 < arguments.length && void 0 !== n ? n : 8;
// return 0 === arguments.length ? null : (r = "object" === v(t) ? t : (10 === "".concat(t).length && (t = 1e3 * Number.parseInt(t)),
// new Date(t)),
// t += 6e4 * new Date(t).getTimezoneOffset(),
// {
// y: (r = new Date(t + 36e5 * n)).getFullYear(),
// m: r.getMonth() + 1 < 10 ? "0".concat(r.getMonth() + 1) : r.getMonth() + 1,
// d: r.getDate() < 10 ? "0".concat(r.getDate()) : r.getDate(),
// h: r.getHours() < 10 ? "0".concat(r.getHours()) : r.getHours(),
// f: r.getMinutes() < 10 ? "0".concat(r.getMinutes()) : r.getMinutes()
// })
// }(o),
// h = u.y,
// g = u.m,
// l = u.d,
// c = u.h,
// u = u.f,
// d = [h, g, l, c, u].join(""),
// f = [],
// d)
// f.push(m[Number(d[p])]);
// return h = A(f.join("")),
// g = A("".concat(o, "|").concat(a, "|").concat(e, "|").concat("web", "|").concat("3", "|").concat(h)),
// "".concat(h, "=").concat(o, "-").concat(a, "-").concat(g);
// })(url)
// `)
// if err != nil {
// fmt.Println(err)
// return url
// }
// v, _ := r.Export().(string)
// return url + "?" + v
//}
func (d *Pan123) login() error { func (d *Pan123) login() error {
var body base.Json var body base.Json
if utils.IsEmailFormat(d.Username) { if utils.IsEmailFormat(d.Username) {
@ -56,9 +160,9 @@ func (d *Pan123) login() error {
SetHeaders(map[string]string{ SetHeaders(map[string]string{
"origin": "https://www.123pan.com", "origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/", "referer": "https://www.123pan.com/",
"user-agent": "Dart/2.19(dart:io)", "user-agent": "Dart/2.19(dart:io)-alist",
"platform": "android", "platform": "web",
"app-version": "36", "app-version": "3",
//"user-agent": base.UserAgent, //"user-agent": base.UserAgent,
}). }).
SetBody(body).Post(SignIn) SetBody(body).Post(SignIn)
@ -93,9 +197,9 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
"origin": "https://www.123pan.com", "origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/", "referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken, "authorization": "Bearer " + d.AccessToken,
"user-agent": "Dart/2.19(dart:io)", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
"platform": "android", "platform": "web",
"app-version": "36", "app-version": "3",
//"user-agent": base.UserAgent, //"user-agent": base.UserAgent,
}) })
if callback != nil { if callback != nil {
@ -109,7 +213,7 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
// return nil, err // return nil, err
//} //}
//req.SetQueryParam("auth-key", *authKey) //req.SetQueryParam("auth-key", *authKey)
res, err := req.Execute(method, url) res, err := req.Execute(method, GetApi(url))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -131,17 +235,27 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
func (d *Pan123) getFiles(parentId string) ([]File, error) { func (d *Pan123) getFiles(parentId string) ([]File, error) {
page := 1 page := 1
res := make([]File, 0) res := make([]File, 0)
// 2024-02-06 fix concurrency by 123pan
for { for {
if !d.APIRateLimit(FileList) {
time.Sleep(time.Millisecond * 200)
continue
}
var resp Files var resp Files
query := map[string]string{ query := map[string]string{
"driveId": "0", "driveId": "0",
"limit": "100", "limit": "100",
"next": "0", "next": "0",
"orderBy": d.OrderBy, "orderBy": d.OrderBy,
"orderDirection": d.OrderDirection, "orderDirection": d.OrderDirection,
"parentFileId": parentId, "parentFileId": parentId,
"trashed": "false", "trashed": "false",
"Page": strconv.Itoa(page), "SearchData": "",
"Page": strconv.Itoa(page),
"OnlyLookAbnormalFile": "0",
"event": "homeListFile",
"operateType": "4",
"inDirectSpace": "false",
} }
_, err := d.request(FileList, http.MethodGet, func(req *resty.Request) { _, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query) req.SetQueryParams(query)

View File

@ -0,0 +1,77 @@
package _123Link
import (
"context"
stdpath "path"
"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/utils"
)
type Pan123Link struct {
model.Storage
Addition
root *Node
}
func (d *Pan123Link) Config() driver.Config {
return config
}
func (d *Pan123Link) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Pan123Link) Init(ctx context.Context) error {
node, err := BuildTree(d.OriginURLs)
if err != nil {
return err
}
node.calSize()
d.root = node
return nil
}
func (d *Pan123Link) Drop(ctx context.Context) error {
return nil
}
func (d *Pan123Link) Get(ctx context.Context, path string) (model.Obj, error) {
node := GetNodeFromRootByPath(d.root, path)
return nodeToObj(node, path)
}
func (d *Pan123Link) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
node := GetNodeFromRootByPath(d.root, dir.GetPath())
if node == nil {
return nil, errs.ObjectNotFound
}
if node.isFile() {
return nil, errs.NotFolder
}
return utils.SliceConvert(node.Children, func(node *Node) (model.Obj, error) {
return nodeToObj(node, stdpath.Join(dir.GetPath(), node.Name))
})
}
func (d *Pan123Link) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
node := GetNodeFromRootByPath(d.root, file.GetPath())
if node == nil {
return nil, errs.ObjectNotFound
}
if node.isFile() {
signUrl, err := SignURL(node.Url, d.PrivateKey, d.UID, time.Duration(d.ValidDuration)*time.Minute)
if err != nil {
return nil, err
}
return &model.Link{
URL: signUrl,
}, nil
}
return nil, errs.NotFile
}
var _ driver.Driver = (*Pan123Link)(nil)

23
drivers/123_link/meta.go Normal file
View File

@ -0,0 +1,23 @@
package _123Link
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
OriginURLs string `json:"origin_urls" type:"text" required:"true" default:"https://vip.123pan.com/29/folder/file.mp3" help:"structure:FolderName:\n [FileSize:][Modified:]Url"`
PrivateKey string `json:"private_key"`
UID uint64 `json:"uid" type:"number"`
ValidDuration int64 `json:"valid_duration" type:"number" default:"30" help:"minutes"`
}
var config = driver.Config{
Name: "123PanLink",
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Pan123Link{}
})
}

152
drivers/123_link/parse.go Normal file
View File

@ -0,0 +1,152 @@
package _123Link
import (
"fmt"
url2 "net/url"
stdpath "path"
"strconv"
"strings"
"time"
)
// build tree from text, text structure definition:
/**
* FolderName:
* [FileSize:][Modified:]Url
*/
/**
* For example:
* folder1:
* name1:url1
* url2
* folder2:
* url3
* url4
* url5
* folder3:
* url6
* url7
* url8
*/
// if there are no name, use the last segment of url as name
func BuildTree(text string) (*Node, error) {
lines := strings.Split(text, "\n")
var root = &Node{Level: -1, Name: "root"}
stack := []*Node{root}
for _, line := range lines {
// calculate indent
indent := 0
for i := 0; i < len(line); i++ {
if line[i] != ' ' {
break
}
indent++
}
// if indent is not a multiple of 2, it is an error
if indent%2 != 0 {
return nil, fmt.Errorf("the line '%s' is not a multiple of 2", line)
}
// calculate level
level := indent / 2
line = strings.TrimSpace(line[indent:])
// if the line is empty, skip
if line == "" {
continue
}
// if level isn't greater than the level of the top of the stack
// it is not the child of the top of the stack
for level <= stack[len(stack)-1].Level {
// pop the top of the stack
stack = stack[:len(stack)-1]
}
// if the line is a folder
if isFolder(line) {
// create a new node
node := &Node{
Level: level,
Name: strings.TrimSuffix(line, ":"),
}
// add the node to the top of the stack
stack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)
// push the node to the stack
stack = append(stack, node)
} else {
// if the line is a file
// create a new node
node, err := parseFileLine(line)
if err != nil {
return nil, err
}
node.Level = level
// add the node to the top of the stack
stack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)
}
}
return root, nil
}
func isFolder(line string) bool {
return strings.HasSuffix(line, ":")
}
// line definition:
// [FileSize:][Modified:]Url
func parseFileLine(line string) (*Node, error) {
// if there is no url, it is an error
if !strings.Contains(line, "http://") && !strings.Contains(line, "https://") {
return nil, fmt.Errorf("invalid line: %s, because url is required for file", line)
}
index := strings.Index(line, "http://")
if index == -1 {
index = strings.Index(line, "https://")
}
url := line[index:]
info := line[:index]
node := &Node{
Url: url,
}
name := stdpath.Base(url)
unescape, err := url2.PathUnescape(name)
if err == nil {
name = unescape
}
node.Name = name
if index > 0 {
if !strings.HasSuffix(info, ":") {
return nil, fmt.Errorf("invalid line: %s, because file info must end with ':'", line)
}
info = info[:len(info)-1]
if info == "" {
return nil, fmt.Errorf("invalid line: %s, because file name can't be empty", line)
}
infoParts := strings.Split(info, ":")
size, err := strconv.ParseInt(infoParts[0], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid line: %s, because file size must be an integer", line)
}
node.Size = size
if len(infoParts) > 1 {
modified, err := strconv.ParseInt(infoParts[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid line: %s, because file modified must be an unix timestamp", line)
}
node.Modified = modified
} else {
node.Modified = time.Now().Unix()
}
}
return node, nil
}
func splitPath(path string) []string {
if path == "/" {
return []string{"root"}
}
parts := strings.Split(path, "/")
parts[0] = "root"
return parts
}
func GetNodeFromRootByPath(root *Node, path string) *Node {
return root.getByPath(splitPath(path))
}

66
drivers/123_link/types.go Normal file
View File

@ -0,0 +1,66 @@
package _123Link
import (
"time"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
)
// Node is a node in the folder tree
type Node struct {
Url string
Name string
Level int
Modified int64
Size int64
Children []*Node
}
func (node *Node) getByPath(paths []string) *Node {
if len(paths) == 0 || node == nil {
return nil
}
if node.Name != paths[0] {
return nil
}
if len(paths) == 1 {
return node
}
for _, child := range node.Children {
tmp := child.getByPath(paths[1:])
if tmp != nil {
return tmp
}
}
return nil
}
func (node *Node) isFile() bool {
return node.Url != ""
}
func (node *Node) calSize() int64 {
if node.isFile() {
return node.Size
}
var size int64 = 0
for _, child := range node.Children {
size += child.calSize()
}
node.Size = size
return size
}
func nodeToObj(node *Node, path string) (model.Obj, error) {
if node == nil {
return nil, errs.ObjectNotFound
}
return &model.Object{
Name: node.Name,
Size: node.Size,
Modified: time.Unix(node.Modified, 0),
IsFolder: !node.isFile(),
Path: path,
}, nil
}

30
drivers/123_link/util.go Normal file
View File

@ -0,0 +1,30 @@
package _123Link
import (
"crypto/md5"
"fmt"
"math/rand"
"net/url"
"time"
)
func SignURL(originURL, privateKey string, uid uint64, validDuration time.Duration) (newURL string, err error) {
if privateKey == "" {
return originURL, nil
}
var (
ts = time.Now().Add(validDuration).Unix() // 有效时间戳
rInt = rand.Int() // 随机正整数
objURL *url.URL
)
objURL, err = url.Parse(originURL)
if err != nil {
return "", err
}
authKey := fmt.Sprintf("%d-%d-%d-%x", ts, rInt, uid, md5.Sum([]byte(fmt.Sprintf("%s-%d-%d-%d-%s",
objURL.Path, ts, rInt, uid, privateKey))))
v := objURL.Query()
v.Add("auth_key", authKey)
objURL.RawQuery = v.Encode()
return objURL.String(), nil
}

View File

@ -4,8 +4,11 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"golang.org/x/time/rate"
"net/http" "net/http"
"net/url" "net/url"
"sync"
"time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
@ -19,6 +22,7 @@ import (
type Pan123Share struct { type Pan123Share struct {
model.Storage model.Storage
Addition Addition
apiRateLimit sync.Map
} }
func (d *Pan123Share) Config() driver.Config { 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 // 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) var _ driver.Driver = (*Pan123Share)(nil)

View File

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

View File

@ -1,6 +1,7 @@
package _123Share package _123Share
import ( import (
"github.com/alist-org/alist/v3/pkg/utils"
"net/url" "net/url"
"path" "path"
"strconv" "strconv"
@ -21,6 +22,10 @@ type File struct {
DownloadUrl string `json:"DownloadUrl"` DownloadUrl string `json:"DownloadUrl"`
} }
func (f File) GetHash() utils.HashInfo {
return utils.HashInfo{}
}
func (f File) GetPath() string { func (f File) GetPath() string {
return "" return ""
} }
@ -36,6 +41,9 @@ func (f File) GetName() string {
func (f File) ModTime() time.Time { func (f File) ModTime() time.Time {
return f.UpdateAt return f.UpdateAt
} }
func (f File) CreateTime() time.Time {
return f.UpdateAt
}
func (f File) IsDir() bool { func (f File) IsDir() bool {
return f.Type == 1 return f.Type == 1

View File

@ -2,8 +2,15 @@ package _123Share
import ( import (
"errors" "errors"
"fmt"
"hash/crc32"
"math"
"math/rand"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
@ -15,20 +22,45 @@ const (
Api = "https://www.123pan.com/api" Api = "https://www.123pan.com/api"
AApi = "https://www.123pan.com/a/api" AApi = "https://www.123pan.com/a/api"
BApi = "https://www.123pan.com/b/api" BApi = "https://www.123pan.com/b/api"
MainApi = Api MainApi = BApi
FileList = MainApi + "/share/get" FileList = MainApi + "/share/get"
DownloadInfo = MainApi + "/share/download/info" DownloadInfo = MainApi + "/share/download/info"
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY" //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) { func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R() req := base.RestyClient.R()
req.SetHeaders(map[string]string{ req.SetHeaders(map[string]string{
"origin": "https://www.123pan.com", "origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/", "referer": "https://www.123pan.com/",
"user-agent": "Dart/2.19(dart:io)", "authorization": "Bearer " + d.AccessToken,
"platform": "android", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
"app-version": "36", "platform": "web",
"app-version": "3",
//"user-agent": base.UserAgent,
}) })
if callback != nil { if callback != nil {
callback(req) callback(req)
@ -36,7 +68,7 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
if resp != nil { if resp != nil {
req.SetResult(resp) req.SetResult(resp)
} }
res, err := req.Execute(method, url) res, err := req.Execute(method, GetApi(url))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -52,6 +84,10 @@ func (d *Pan123Share) getFiles(parentId string) ([]File, error) {
page := 1 page := 1
res := make([]File, 0) res := make([]File, 0)
for { for {
if !d.APIRateLimit(FileList) {
time.Sleep(time.Millisecond * 200)
continue
}
var resp Files var resp Files
query := map[string]string{ query := map[string]string{
"limit": "100", "limit": "100",

View File

@ -8,18 +8,21 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/pkg/cron"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type Yun139 struct { type Yun139 struct {
model.Storage model.Storage
Addition Addition
cron *cron.Cron
Account string Account string
} }
@ -35,61 +38,116 @@ func (d *Yun139) Init(ctx context.Context) error {
if d.Authorization == "" { if d.Authorization == "" {
return fmt.Errorf("authorization is empty") return fmt.Errorf("authorization is empty")
} }
decode, err := base64.StdEncoding.DecodeString(d.Authorization) d.cron = cron.NewCron(time.Hour * 24 * 7)
if err != nil { d.cron.Do(func() {
return err err := d.refreshToken()
} if err != nil {
decodeStr := string(decode) log.Errorf("%+v", err)
splits := strings.Split(decodeStr, ":") }
if len(splits) < 2 { })
return fmt.Errorf("authorization is invalid, splits < 2") switch d.Addition.Type {
} case MetaPersonalNew:
d.Account = splits[1] if len(d.Addition.RootFolderID) == 0 {
_, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{ d.RootFolderID = "/"
"qryUserExternInfoReq": base.Json{ }
"commonAccountInfo": base.Json{ return nil
"account": d.Account, case MetaPersonal:
"accountType": 1, if len(d.Addition.RootFolderID) == 0 {
d.RootFolderID = "root"
}
fallthrough
case MetaFamily:
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
if err != nil {
return err
}
decodeStr := string(decode)
splits := strings.Split(decodeStr, ":")
if len(splits) < 2 {
return fmt.Errorf("authorization is invalid, splits < 2")
}
d.Account = splits[1]
_, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{
"qryUserExternInfoReq": base.Json{
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
}, },
}, }, nil)
}, nil) return err
return err default:
return errs.NotImplement
}
} }
func (d *Yun139) Drop(ctx context.Context) error { func (d *Yun139) Drop(ctx context.Context) error {
if d.cron != nil {
d.cron.Stop()
}
return nil return nil
} }
func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if d.isFamily() { switch d.Addition.Type {
return d.familyGetFiles(dir.GetID()) case MetaPersonalNew:
} else { return d.personalGetFiles(dir.GetID())
case MetaPersonal:
return d.getFiles(dir.GetID()) return d.getFiles(dir.GetID())
case MetaFamily:
return d.familyGetFiles(dir.GetID())
default:
return nil, errs.NotImplement
} }
} }
func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
u, err := d.getLink(file.GetID()) var url string
var err error
switch d.Addition.Type {
case MetaPersonalNew:
url, err = d.personalGetLink(file.GetID())
case MetaPersonal:
fallthrough
case MetaFamily:
url, err = d.getLink(file.GetID())
default:
return nil, errs.NotImplement
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &model.Link{URL: u}, nil return &model.Link{URL: url}, nil
} }
func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
data := base.Json{ var err error
"createCatalogExtReq": base.Json{ switch d.Addition.Type {
"parentCatalogID": parentDir.GetID(), case MetaPersonalNew:
"newCatalogName": dirName, data := base.Json{
"commonAccountInfo": base.Json{ "parentFileId": parentDir.GetID(),
"account": d.Account, "name": dirName,
"accountType": 1, "description": "",
"type": "folder",
"fileRenameMode": "force_rename",
}
pathname := "/hcy/file/create"
_, err = d.personalPost(pathname, data, nil)
case MetaPersonal:
data := base.Json{
"createCatalogExtReq": base.Json{
"parentCatalogID": parentDir.GetID(),
"newCatalogName": dirName,
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
}, },
}, }
} pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt"
pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt" _, err = d.post(pathname, data, nil)
if d.isFamily() { case MetaFamily:
data = base.Json{ data := base.Json{
"cloudID": d.CloudID, "cloudID": d.CloudID,
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.Account,
@ -97,144 +155,198 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
}, },
"docLibName": dirName, "docLibName": dirName,
} }
pathname = "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc" pathname := "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc"
_, err = d.post(pathname, data, nil)
default:
err = errs.NotImplement
} }
_, err := d.post(pathname, data, nil)
return err return err
} }
func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
if d.isFamily() { switch d.Addition.Type {
return errs.NotImplement case MetaPersonalNew:
} data := base.Json{
var contentInfoList []string "fileIds": []string{srcObj.GetID()},
var catalogInfoList []string "toParentFileId": dstDir.GetID(),
if srcObj.IsDir() { }
catalogInfoList = append(catalogInfoList, srcObj.GetID()) pathname := "/hcy/file/batchMove"
} else { _, err := d.personalPost(pathname, data, nil)
contentInfoList = append(contentInfoList, srcObj.GetID()) if err != nil {
} return nil, err
data := base.Json{ }
"createBatchOprTaskReq": base.Json{ return srcObj, nil
"taskType": 3, case MetaPersonal:
"actionType": "304", var contentInfoList []string
"taskInfo": base.Json{ var catalogInfoList []string
"contentInfoList": contentInfoList, if srcObj.IsDir() {
"catalogInfoList": catalogInfoList, catalogInfoList = append(catalogInfoList, srcObj.GetID())
"newCatalogID": dstDir.GetID(), } else {
contentInfoList = append(contentInfoList, srcObj.GetID())
}
data := base.Json{
"createBatchOprTaskReq": base.Json{
"taskType": 3,
"actionType": "304",
"taskInfo": base.Json{
"contentInfoList": contentInfoList,
"catalogInfoList": catalogInfoList,
"newCatalogID": dstDir.GetID(),
},
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
}, },
"commonAccountInfo": base.Json{ }
"account": d.Account, pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
"accountType": 1, _, err := d.post(pathname, data, nil)
}, if err != nil {
}, return nil, err
}
return srcObj, nil
default:
return nil, errs.NotImplement
} }
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
_, err := d.post(pathname, data, nil)
return err
} }
func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) error { func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
if d.isFamily() { var err error
return errs.NotImplement switch d.Addition.Type {
} case MetaPersonalNew:
var data base.Json data := base.Json{
var pathname string "fileId": srcObj.GetID(),
if srcObj.IsDir() { "name": newName,
data = base.Json{ "description": "",
"catalogID": srcObj.GetID(),
"catalogName": newName,
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
} }
pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo" pathname := "/hcy/file/update"
} else { _, err = d.personalPost(pathname, data, nil)
data = base.Json{ case MetaPersonal:
"contentID": srcObj.GetID(), var data base.Json
"contentName": newName, var pathname string
"commonAccountInfo": base.Json{ if srcObj.IsDir() {
"account": d.Account, data = base.Json{
"accountType": 1, "catalogID": srcObj.GetID(),
}, "catalogName": newName,
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
}
pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo"
} else {
data = base.Json{
"contentID": srcObj.GetID(),
"contentName": newName,
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
}
pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo"
} }
pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo" _, err = d.post(pathname, data, nil)
default:
err = errs.NotImplement
} }
_, err := d.post(pathname, data, nil)
return err return err
} }
func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
if d.isFamily() { var err error
return errs.NotImplement switch d.Addition.Type {
} case MetaPersonalNew:
var contentInfoList []string data := base.Json{
var catalogInfoList []string "fileIds": []string{srcObj.GetID()},
if srcObj.IsDir() { "toParentFileId": dstDir.GetID(),
catalogInfoList = append(catalogInfoList, srcObj.GetID()) }
} else { pathname := "/hcy/file/batchCopy"
contentInfoList = append(contentInfoList, srcObj.GetID()) _, err := d.personalPost(pathname, data, nil)
} return err
data := base.Json{ case MetaPersonal:
"createBatchOprTaskReq": base.Json{ var contentInfoList []string
"taskType": 3, var catalogInfoList []string
"actionType": 309, if srcObj.IsDir() {
"taskInfo": base.Json{ catalogInfoList = append(catalogInfoList, srcObj.GetID())
"contentInfoList": contentInfoList, } else {
"catalogInfoList": catalogInfoList, contentInfoList = append(contentInfoList, srcObj.GetID())
"newCatalogID": dstDir.GetID(), }
data := base.Json{
"createBatchOprTaskReq": base.Json{
"taskType": 3,
"actionType": 309,
"taskInfo": base.Json{
"contentInfoList": contentInfoList,
"catalogInfoList": catalogInfoList,
"newCatalogID": dstDir.GetID(),
},
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
}, },
"commonAccountInfo": base.Json{ }
"account": d.Account, pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
"accountType": 1, _, err = d.post(pathname, data, nil)
}, default:
}, err = errs.NotImplement
} }
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
_, err := d.post(pathname, data, nil)
return err return err
} }
func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error { func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
var contentInfoList []string switch d.Addition.Type {
var catalogInfoList []string case MetaPersonalNew:
if obj.IsDir() { data := base.Json{
catalogInfoList = append(catalogInfoList, obj.GetID()) "fileIds": []string{obj.GetID()},
} else {
contentInfoList = append(contentInfoList, obj.GetID())
}
data := base.Json{
"createBatchOprTaskReq": base.Json{
"taskType": 2,
"actionType": 201,
"taskInfo": base.Json{
"newCatalogID": "",
"contentInfoList": contentInfoList,
"catalogInfoList": catalogInfoList,
},
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
},
}
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
if d.isFamily() {
data = base.Json{
"catalogList": catalogInfoList,
"contentList": contentInfoList,
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
"sourceCatalogType": 1002,
"taskType": 2,
} }
pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask" pathname := "/hcy/recyclebin/batchTrash"
_, err := d.personalPost(pathname, data, nil)
return err
case MetaPersonal:
fallthrough
case MetaFamily:
var contentInfoList []string
var catalogInfoList []string
if obj.IsDir() {
catalogInfoList = append(catalogInfoList, obj.GetID())
} else {
contentInfoList = append(contentInfoList, obj.GetID())
}
data := base.Json{
"createBatchOprTaskReq": base.Json{
"taskType": 2,
"actionType": 201,
"taskInfo": base.Json{
"newCatalogID": "",
"contentInfoList": contentInfoList,
"catalogInfoList": catalogInfoList,
},
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
},
}
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
if d.isFamily() {
data = base.Json{
"catalogList": catalogInfoList,
"contentList": contentInfoList,
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
"sourceCatalogType": 1002,
"taskType": 2,
}
pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask"
}
_, err := d.post(pathname, data, nil)
return err
default:
return errs.NotImplement
} }
_, err := d.post(pathname, data, nil)
return err
} }
const ( const (
@ -254,94 +366,208 @@ func getPartSize(size int64) int64 {
} }
func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
data := base.Json{ switch d.Addition.Type {
"manualRename": 2, case MetaPersonalNew:
"operation": 0, var err error
"fileCount": 1, fullHash := stream.GetHash().GetHash(utils.SHA256)
"totalSize": 0, // 去除上传大小限制 if len(fullHash) <= 0 {
"uploadContentList": []base.Json{{ tmpF, err := stream.CacheFullInTempFile()
"contentName": stream.GetName(), if err != nil {
"contentSize": 0, // 去除上传大小限制 return err
// "digest": "5a3231986ce7a6b46e408612d385bafa" }
}}, fullHash, err = utils.HashFile(utils.SHA256, tmpF)
"parentCatalogID": dstDir.GetID(), if err != nil {
"newCatalogName": "", return err
"commonAccountInfo": base.Json{ }
"account": d.Account, }
"accountType": 1, // return errs.NotImplement
}, data := base.Json{
} "contentHash": fullHash,
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest" "contentHashAlgorithm": "SHA256",
if d.isFamily() { "contentType": "application/octet-stream",
data = d.newJson(base.Json{ "parallelUpload": false,
"fileCount": 1, "partInfos": []base.Json{{
"manualRename": 2, "parallelHashCtx": base.Json{
"operation": 0, "partOffset": 0,
"path": "", },
"seqNo": "", "partNumber": 1,
"totalSize": 0, "partSize": stream.GetSize(),
"uploadContentList": []base.Json{{
"contentName": stream.GetName(),
"contentSize": 0,
// "digest": "5a3231986ce7a6b46e408612d385bafa"
}}, }},
}) "size": stream.GetSize(),
pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL" "parentFileId": dstDir.GetID(),
return errs.NotImplement "name": stream.GetName(),
} "type": "file",
var resp UploadResp "fileRenameMode": "auto_rename",
_, err := d.post(pathname, data, &resp)
if err != nil {
return err
}
// Progress
p := driver.NewProgress(stream.GetSize(), up)
var partSize = getPartSize(stream.GetSize())
part := (stream.GetSize() + partSize - 1) / partSize
if part == 0 {
part = 1
}
for i := int64(0); i < part; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
} }
pathname := "/hcy/file/create"
start := i * partSize var resp PersonalUploadResp
byteSize := stream.GetSize() - start _, err = d.personalPost(pathname, data, &resp)
if byteSize > partSize {
byteSize = partSize
}
limitReader := io.LimitReader(stream, byteSize)
// Update Progress
r := io.TeeReader(limitReader, p)
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r)
if err != nil { if err != nil {
return err return err
} }
if resp.Data.Exist || resp.Data.RapidUpload {
return nil
}
// Progress
p := driver.NewProgress(stream.GetSize(), up)
// Update Progress
r := io.TeeReader(stream, p)
req, err := http.NewRequest("PUT", resp.Data.PartInfos[0].UploadUrl, r)
if err != nil {
return err
}
req = req.WithContext(ctx) req = req.WithContext(ctx)
req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName())) req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10)) req.Header.Set("Content-Length", fmt.Sprint(stream.GetSize()))
req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1)) req.Header.Set("Origin", "https://yun.139.com")
req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID) req.Header.Set("Referer", "https://yun.139.com/")
req.Header.Set("rangeType", "0") req.ContentLength = stream.GetSize()
req.ContentLength = byteSize
res, err := base.HttpClient.Do(req) res, err := base.HttpClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
_ = res.Body.Close() _ = res.Body.Close()
log.Debugf("%+v", res) log.Debugf("%+v", res)
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", res.StatusCode) return fmt.Errorf("unexpected status code: %d", res.StatusCode)
} }
}
return nil data = base.Json{
"contentHash": fullHash,
"contentHashAlgorithm": "SHA256",
"fileId": resp.Data.FileId,
"uploadId": resp.Data.UploadId,
}
_, err = d.personalPost("/hcy/file/complete", data, nil)
if err != nil {
return err
}
return nil
case MetaPersonal:
fallthrough
case MetaFamily:
data := base.Json{
"manualRename": 2,
"operation": 0,
"fileCount": 1,
"totalSize": 0, // 去除上传大小限制
"uploadContentList": []base.Json{{
"contentName": stream.GetName(),
"contentSize": 0, // 去除上传大小限制
// "digest": "5a3231986ce7a6b46e408612d385bafa"
}},
"parentCatalogID": dstDir.GetID(),
"newCatalogName": "",
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
}
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
if d.isFamily() {
// data = d.newJson(base.Json{
// "fileCount": 1,
// "manualRename": 2,
// "operation": 0,
// "path": "",
// "seqNo": "",
// "totalSize": 0,
// "uploadContentList": []base.Json{{
// "contentName": stream.GetName(),
// "contentSize": 0,
// // "digest": "5a3231986ce7a6b46e408612d385bafa"
// }},
// })
// pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL"
return errs.NotImplement
}
var resp UploadResp
_, err := d.post(pathname, data, &resp)
if err != nil {
return err
}
// Progress
p := driver.NewProgress(stream.GetSize(), up)
var partSize = getPartSize(stream.GetSize())
part := (stream.GetSize() + partSize - 1) / partSize
if part == 0 {
part = 1
}
for i := int64(0); i < part; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
start := i * partSize
byteSize := stream.GetSize() - start
if byteSize > partSize {
byteSize = partSize
}
limitReader := io.LimitReader(stream, byteSize)
// Update Progress
r := io.TeeReader(limitReader, p)
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r)
if err != nil {
return err
}
req = req.WithContext(ctx)
req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName()))
req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10))
req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1))
req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID)
req.Header.Set("rangeType", "0")
req.ContentLength = byteSize
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
_ = res.Body.Close()
log.Debugf("%+v", res)
if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
return nil
default:
return errs.NotImplement
}
}
func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
switch d.Addition.Type {
case MetaPersonalNew:
var resp base.Json
var uri string
data := base.Json{
"category": "video",
"fileId": args.Obj.GetID(),
}
switch args.Method {
case "video_preview":
uri = "/hcy/videoPreview/getPreviewInfo"
default:
return nil, errs.NotSupport
}
_, err := d.personalPost(uri, data, &resp)
if err != nil {
return nil, err
}
return resp["data"], nil
default:
return nil, errs.NotImplement
}
} }
var _ driver.Driver = (*Yun139)(nil) var _ driver.Driver = (*Yun139)(nil)

View File

@ -9,17 +9,20 @@ type Addition struct {
//Account string `json:"account" required:"true"` //Account string `json:"account" required:"true"`
Authorization string `json:"authorization" type:"text" required:"true"` Authorization string `json:"authorization" type:"text" required:"true"`
driver.RootID driver.RootID
Type string `json:"type" type:"select" options:"personal,family" default:"personal"` Type string `json:"type" type:"select" options:"personal,family,personal_new" default:"personal"`
CloudID string `json:"cloud_id"` CloudID string `json:"cloud_id"`
} }
var config = driver.Config{ var config = driver.Config{
Name: "139Yun", Name: "139Yun",
LocalSort: true, LocalSort: true,
ProxyRangeOption: true,
} }
func init() { func init() {
op.RegisterDriver(func() driver.Driver { op.RegisterDriver(func() driver.Driver {
return &Yun139{} d := &Yun139{}
d.ProxyRange = true
return d
}) })
} }

View File

@ -1,5 +1,15 @@
package _139 package _139
import (
"encoding/xml"
)
const (
MetaPersonal string = "personal"
MetaFamily string = "family"
MetaPersonalNew string = "personal_new"
)
type BaseResp struct { type BaseResp struct {
Success bool `json:"success"` Success bool `json:"success"`
Code string `json:"code"` Code string `json:"code"`
@ -10,7 +20,7 @@ type Catalog struct {
CatalogID string `json:"catalogID"` CatalogID string `json:"catalogID"`
CatalogName string `json:"catalogName"` CatalogName string `json:"catalogName"`
//CatalogType int `json:"catalogType"` //CatalogType int `json:"catalogType"`
//CreateTime string `json:"createTime"` CreateTime string `json:"createTime"`
UpdateTime string `json:"updateTime"` UpdateTime string `json:"updateTime"`
//IsShared bool `json:"isShared"` //IsShared bool `json:"isShared"`
//CatalogLevel int `json:"catalogLevel"` //CatalogLevel int `json:"catalogLevel"`
@ -63,7 +73,7 @@ type Content struct {
//ParentCatalogID string `json:"parentCatalogId"` //ParentCatalogID string `json:"parentCatalogId"`
//Channel string `json:"channel"` //Channel string `json:"channel"`
//GeoLocFlag string `json:"geoLocFlag"` //GeoLocFlag string `json:"geoLocFlag"`
//Digest string `json:"digest"` Digest string `json:"digest"`
//Version string `json:"version"` //Version string `json:"version"`
//FileEtag string `json:"fileEtag"` //FileEtag string `json:"fileEtag"`
//FileVersion string `json:"fileVersion"` //FileVersion string `json:"fileVersion"`
@ -141,7 +151,7 @@ type CloudContent struct {
//ContentSuffix string `json:"contentSuffix"` //ContentSuffix string `json:"contentSuffix"`
ContentSize int64 `json:"contentSize"` ContentSize int64 `json:"contentSize"`
//ContentDesc string `json:"contentDesc"` //ContentDesc string `json:"contentDesc"`
//CreateTime string `json:"createTime"` CreateTime string `json:"createTime"`
//Shottime interface{} `json:"shottime"` //Shottime interface{} `json:"shottime"`
LastUpdateTime string `json:"lastUpdateTime"` LastUpdateTime string `json:"lastUpdateTime"`
ThumbnailURL string `json:"thumbnailURL"` ThumbnailURL string `json:"thumbnailURL"`
@ -165,7 +175,7 @@ type CloudCatalog struct {
CatalogID string `json:"catalogID"` CatalogID string `json:"catalogID"`
CatalogName string `json:"catalogName"` CatalogName string `json:"catalogName"`
//CloudID string `json:"cloudID"` //CloudID string `json:"cloudID"`
//CreateTime string `json:"createTime"` CreateTime string `json:"createTime"`
LastUpdateTime string `json:"lastUpdateTime"` LastUpdateTime string `json:"lastUpdateTime"`
//Creator string `json:"creator"` //Creator string `json:"creator"`
//CreatorNickname string `json:"creatorNickname"` //CreatorNickname string `json:"creatorNickname"`
@ -185,3 +195,51 @@ type QueryContentListResp struct {
RecallContent interface{} `json:"recallContent"` RecallContent interface{} `json:"recallContent"`
} `json:"data"` } `json:"data"`
} }
type PersonalThumbnail struct {
Style string `json:"style"`
Url string `json:"url"`
}
type PersonalFileItem struct {
FileId string `json:"fileId"`
Name string `json:"name"`
Size int64 `json:"size"`
Type string `json:"type"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Thumbnails []PersonalThumbnail `json:"thumbnailUrls"`
}
type PersonalListResp struct {
BaseResp
Data struct {
Items []PersonalFileItem `json:"items"`
NextPageCursor string `json:"nextPageCursor"`
}
}
type PersonalPartInfo struct {
PartNumber int `json:"partNumber"`
UploadUrl string `json:"uploadUrl"`
}
type PersonalUploadResp struct {
BaseResp
Data struct {
FileId string `json:"fileId"`
PartInfos []PersonalPartInfo `json:"partInfos"`
Exist bool `json:"exist"`
RapidUpload bool `json:"rapidUpload"`
UploadId string `json:"uploadId"`
}
}
type RefreshTokenResp struct {
XMLName xml.Name `xml:"root"`
Return string `xml:"return"`
Token string `xml:"token"`
Expiretime int32 `xml:"expiretime"`
AccessToken string `xml:"accessToken"`
Desc string `xml:"desc"`
}

View File

@ -15,6 +15,7 @@ import (
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/pkg/utils/random" "github.com/alist-org/alist/v3/pkg/utils/random"
"github.com/alist-org/alist/v3/internal/op"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -48,10 +49,36 @@ func calSign(body, ts, randStr string) string {
} }
func getTime(t string) time.Time { func getTime(t string) time.Time {
stamp, _ := time.ParseInLocation("20060102150405", t, time.Local) stamp, _ := time.ParseInLocation("20060102150405", t, utils.CNLoc)
return stamp return stamp
} }
func (d *Yun139) refreshToken() error {
url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do"
var resp RefreshTokenResp
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
if err != nil {
return err
}
decodeStr := string(decode)
splits := strings.Split(decodeStr, ":")
reqBody := "<root><token>" + splits[2] + "</token><account>" + splits[1] + "</account><clienttype>656</clienttype></root>"
_, err = base.RestyClient.R().
ForceContentType("application/xml").
SetBody(reqBody).
SetResult(&resp).
Post(url)
if err != nil {
return err
}
if resp.Return != "0" {
return fmt.Errorf("failed to refresh token: %s", resp.Desc)
}
d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token))
op.MustSaveDriverStorage(d)
return nil
}
func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
url := "https://yun.139.com" + pathname url := "https://yun.139.com" + pathname
req := base.RestyClient.R() req := base.RestyClient.R()
@ -139,6 +166,7 @@ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
Name: catalog.CatalogName, Name: catalog.CatalogName,
Size: 0, Size: 0,
Modified: getTime(catalog.UpdateTime), Modified: getTime(catalog.UpdateTime),
Ctime: getTime(catalog.CreateTime),
IsFolder: true, IsFolder: true,
} }
files = append(files, &f) files = append(files, &f)
@ -150,6 +178,7 @@ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
Name: content.ContentName, Name: content.ContentName,
Size: content.ContentSize, Size: content.ContentSize,
Modified: getTime(content.UpdateTime), Modified: getTime(content.UpdateTime),
HashInfo: utils.NewHashInfo(utils.MD5, content.Digest),
}, },
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL}, Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
//Thumbnail: content.BigthumbnailURL, //Thumbnail: content.BigthumbnailURL,
@ -202,6 +231,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
Size: 0, Size: 0,
IsFolder: true, IsFolder: true,
Modified: getTime(catalog.LastUpdateTime), Modified: getTime(catalog.LastUpdateTime),
Ctime: getTime(catalog.CreateTime),
} }
files = append(files, &f) files = append(files, &f)
} }
@ -212,6 +242,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
Name: content.ContentName, Name: content.ContentName,
Size: content.ContentSize, Size: content.ContentSize,
Modified: getTime(content.LastUpdateTime), Modified: getTime(content.LastUpdateTime),
Ctime: getTime(content.CreateTime),
}, },
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL}, Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
//Thumbnail: content.BigthumbnailURL, //Thumbnail: content.BigthumbnailURL,
@ -248,3 +279,154 @@ func unicode(str string) string {
textUnquoted := textQuoted[1 : len(textQuoted)-1] textUnquoted := textQuoted[1 : len(textQuoted)-1]
return textUnquoted return textUnquoted
} }
func (d *Yun139) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
url := "https://personal-kd-njs.yun.139.com" + pathname
req := base.RestyClient.R()
randStr := random.String(16)
ts := time.Now().Format("2006-01-02 15:04:05")
if callback != nil {
callback(req)
}
body, err := utils.Json.Marshal(req.Body)
if err != nil {
return nil, err
}
sign := calSign(string(body), ts, randStr)
svcType := "1"
if d.isFamily() {
svcType = "2"
}
req.SetHeaders(map[string]string{
"Accept": "application/json, text/plain, */*",
"Authorization": "Basic " + d.Authorization,
"Caller": "web",
"Cms-Device": "default",
"Mcloud-Channel": "1000101",
"Mcloud-Client": "10701",
"Mcloud-Route": "001",
"Mcloud-Sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
"Mcloud-Version": "7.13.0",
"Origin": "https://yun.139.com",
"Referer": "https://yun.139.com/w/",
"x-DeviceInfo": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||",
"x-huawei-channelSrc": "10000034",
"x-inner-ntwk": "2",
"x-m4c-caller": "PC",
"x-m4c-src": "10002",
"x-SvcType": svcType,
"X-Yun-Api-Version": "v1",
"X-Yun-App-Channel": "10000034",
"X-Yun-Channel-Source": "10000034",
"X-Yun-Client-Info": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||dW5kZWZpbmVk||",
"X-Yun-Module-Type": "100",
"X-Yun-Svc-Type": "1",
})
var e BaseResp
req.SetResult(&e)
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
log.Debugln(res.String())
if !e.Success {
return nil, errors.New(e.Message)
}
if resp != nil {
err = utils.Json.Unmarshal(res.Body(), resp)
if err != nil {
return nil, err
}
}
return res.Body(), nil
}
func (d *Yun139) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) {
return d.personalRequest(pathname, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, resp)
}
func getPersonalTime(t string) time.Time {
stamp, err := time.ParseInLocation("2006-01-02T15:04:05.999-07:00", t, utils.CNLoc)
if err != nil {
panic(err)
}
return stamp
}
func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
files := make([]model.Obj, 0)
nextPageCursor := ""
for {
data := base.Json{
"imageThumbnailStyleList": []string{"Small", "Large"},
"orderBy": "updated_at",
"orderDirection": "DESC",
"pageInfo": base.Json{
"pageCursor": nextPageCursor,
"pageSize": 100,
},
"parentFileId": fileId,
}
var resp PersonalListResp
_, err := d.personalPost("/hcy/file/list", data, &resp)
if err != nil {
return nil, err
}
nextPageCursor = resp.Data.NextPageCursor
for _, item := range resp.Data.Items {
var isFolder = (item.Type == "folder")
var f model.Obj
if isFolder {
f = &model.Object{
ID: item.FileId,
Name: item.Name,
Size: 0,
Modified: getPersonalTime(item.UpdatedAt),
Ctime: getPersonalTime(item.CreatedAt),
IsFolder: isFolder,
}
} else {
var Thumbnails = item.Thumbnails
var ThumbnailUrl string
if len(Thumbnails) > 0 {
ThumbnailUrl = Thumbnails[len(Thumbnails)-1].Url
}
f = &model.ObjThumb{
Object: model.Object{
ID: item.FileId,
Name: item.Name,
Size: item.Size,
Modified: getPersonalTime(item.UpdatedAt),
Ctime: getPersonalTime(item.CreatedAt),
IsFolder: isFolder,
},
Thumbnail: model.Thumbnail{Thumbnail: ThumbnailUrl},
}
}
files = append(files, f)
}
if len(nextPageCursor) == 0 {
break
}
}
return files, nil
}
func (d *Yun139) personalGetLink(fileId string) (string, error) {
data := base.Json{
"fileId": fileId,
}
res, err := d.personalPost("/hcy/file/getDownloadUrl",
data, nil)
if err != nil {
return "", err
}
var cdnUrl = jsoniter.Get(res, "data", "cdnUrl").ToString()
if cdnUrl != "" {
return cdnUrl, nil
} else {
return jsoniter.Get(res, "data", "url").ToString(), nil
}
}

View File

@ -380,7 +380,7 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
if err != nil { if err != nil {
return err return err
} }
up(int(i * 100 / count)) up(float64(i) * 100 / float64(count))
} }
fileMd5 := hex.EncodeToString(md5Sum.Sum(nil)) fileMd5 := hex.EncodeToString(md5Sum.Sum(nil))
sliceMd5 := fileMd5 sliceMd5 := fileMd5

View File

@ -1,6 +1,7 @@
package _189pc package _189pc
import ( import (
"container/ring"
"context" "context"
"net/http" "net/http"
"strconv" "strconv"
@ -27,10 +28,18 @@ type Cloud189PC struct {
tokenInfo *AppSessionResp tokenInfo *AppSessionResp
uploadThread int uploadThread int
familyTransferFolder *ring.Ring
cleanFamilyTransferFile func()
storageConfig driver.Config
} }
func (y *Cloud189PC) Config() driver.Config { func (y *Cloud189PC) Config() driver.Config {
return config if y.storageConfig.Name == "" {
y.storageConfig = config
}
return y.storageConfig
} }
func (y *Cloud189PC) GetAddition() driver.Additional { func (y *Cloud189PC) GetAddition() driver.Additional {
@ -38,13 +47,15 @@ func (y *Cloud189PC) GetAddition() driver.Additional {
} }
func (y *Cloud189PC) Init(ctx context.Context) (err error) { func (y *Cloud189PC) Init(ctx context.Context) (err error) {
// 兼容旧上传接口
y.storageConfig.NoOverwriteUpload = y.isFamily() && (y.Addition.RapidUpload || y.Addition.UploadMethod == "old")
// 处理个人云和家庭云参数 // 处理个人云和家庭云参数
if y.isFamily() && y.RootFolderID == "-11" { if y.isFamily() && y.RootFolderID == "-11" {
y.RootFolderID = "" y.RootFolderID = ""
} }
if !y.isFamily() && y.RootFolderID == "" { if !y.isFamily() && y.RootFolderID == "" {
y.RootFolderID = "-11" y.RootFolderID = "-11"
y.FamilyID = ""
} }
// 限制上传线程数 // 限制上传线程数
@ -71,11 +82,24 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
} }
// 处理家庭云ID // 处理家庭云ID
if y.isFamily() && y.FamilyID == "" { if y.FamilyID == "" {
if y.FamilyID, err = y.getFamilyID(); err != nil { if y.FamilyID, err = y.getFamilyID(); err != nil {
return err 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 return
} }
@ -84,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) { 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) { func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
@ -92,8 +116,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
URL string `json:"fileDownloadUrl"` URL string `json:"fileDownloadUrl"`
} }
isFamily := y.isFamily()
fullUrl := API_URL fullUrl := API_URL
if y.isFamily() { if isFamily {
fullUrl += "/family/file" fullUrl += "/family/file"
} }
fullUrl += "/getFileDownloadUrl.action" fullUrl += "/getFileDownloadUrl.action"
@ -101,7 +126,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
_, err := y.get(fullUrl, func(r *resty.Request) { _, err := y.get(fullUrl, func(r *resty.Request) {
r.SetContext(ctx) r.SetContext(ctx)
r.SetQueryParam("fileId", file.GetID()) r.SetQueryParam("fileId", file.GetID())
if y.isFamily() { if isFamily {
r.SetQueryParams(map[string]string{ r.SetQueryParams(map[string]string{
"familyId": y.FamilyID, "familyId": y.FamilyID,
}) })
@ -111,17 +136,18 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
"flag": "1", "flag": "1",
}) })
} }
}, &downloadUrl) }, &downloadUrl, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 重定向获取真实链接 // 重定向获取真实链接
downloadUrl.URL = strings.Replace(strings.ReplaceAll(downloadUrl.URL, "&amp;", "&"), "http://", "https://", 1) downloadUrl.URL = strings.Replace(strings.ReplaceAll(downloadUrl.URL, "&amp;", "&"), "http://", "https://", 1)
res, err := base.NoRedirectClient.R().SetContext(ctx).Get(downloadUrl.URL) res, err := base.NoRedirectClient.R().SetContext(ctx).SetDoNotParseResponse(true).Get(downloadUrl.URL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer res.RawBody().Close()
if res.StatusCode() == 302 { if res.StatusCode() == 302 {
downloadUrl.URL = res.Header().Get("location") downloadUrl.URL = res.Header().Get("location")
} }
@ -147,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) { func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
isFamily := y.isFamily()
fullUrl := API_URL fullUrl := API_URL
if y.isFamily() { if isFamily {
fullUrl += "/family/file" fullUrl += "/family/file"
} }
fullUrl += "/createFolder.action" fullUrl += "/createFolder.action"
@ -160,7 +187,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
"folderName": dirName, "folderName": dirName,
"relativePath": "", "relativePath": "",
}) })
if y.isFamily() { if isFamily {
req.SetQueryParams(map[string]string{ req.SetQueryParams(map[string]string{
"familyId": y.FamilyID, "familyId": y.FamilyID,
"parentId": parentDir.GetID(), "parentId": parentDir.GetID(),
@ -170,7 +197,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
"parentFolderId": parentDir.GetID(), "parentFolderId": parentDir.GetID(),
}) })
} }
}, &newFolder) }, &newFolder, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -178,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) { func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
var resp CreateBatchTaskResp isFamily := y.isFamily()
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) { other := map[string]string{"targetFileName": dstDir.GetName()}
req.SetContext(ctx)
req.SetFormData(map[string]string{ resp, err := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
"type": "MOVE", FileId: srcObj.GetID(),
"taskInfos": MustString(utils.Json.MarshalToString( FileName: srcObj.GetName(),
[]BatchTaskInfo{ IsFolder: BoolToNumber(srcObj.IsDir()),
{ })
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)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -209,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) { func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
isFamily := y.isFamily()
queryParam := make(map[string]string) queryParam := make(map[string]string)
fullUrl := API_URL fullUrl := API_URL
method := http.MethodPost method := http.MethodPost
if y.isFamily() { if isFamily {
fullUrl += "/family/file" fullUrl += "/family/file"
method = http.MethodGet method = http.MethodGet
queryParam["familyId"] = y.FamilyID queryParam["familyId"] = y.FamilyID
@ -236,7 +251,7 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
_, err := y.request(fullUrl, method, func(req *resty.Request) { _, err := y.request(fullUrl, method, func(req *resty.Request) {
req.SetContext(ctx).SetQueryParams(queryParam) req.SetContext(ctx).SetQueryParams(queryParam)
}, nil, newObj) }, nil, newObj, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -244,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 { func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
var resp CreateBatchTaskResp isFamily := y.isFamily()
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) { other := map[string]string{"targetFileName": dstDir.GetName()}
req.SetContext(ctx)
req.SetFormData(map[string]string{ resp, err := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
"type": "COPY", FileId: srcObj.GetID(),
"taskInfos": MustString(utils.Json.MarshalToString( FileName: srcObj.GetName(),
[]BatchTaskInfo{ IsFolder: BoolToNumber(srcObj.IsDir()),
{ })
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)
if err != nil { if err != nil {
return err return err
} }
@ -273,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 { func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
var resp CreateBatchTaskResp isFamily := y.isFamily()
_, 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()),
},
})),
})
if y.isFamily() { resp, err := y.CreateBatchTask("DELETE", IF(isFamily, y.FamilyID, ""), "", nil, BatchTaskInfo{
req.SetFormData(map[string]string{ FileId: obj.GetID(),
"familyId": y.FamilyID, FileName: obj.GetName(),
}) IsFolder: BoolToNumber(obj.IsDir()),
} })
}, &resp)
if err != nil { if err != nil {
return err return err
} }
@ -301,18 +289,73 @@ func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
return y.WaitBatchTask("DELETE", resp.TaskID, time.Millisecond*200) 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) {
switch y.UploadMethod { overwrite := true
case "old": isFamily := y.isFamily()
return y.OldUpload(ctx, dstDir, stream, up)
// 响应时间长,按需启用
if y.Addition.RapidUpload && !stream.IsForceStreamUpload() {
if newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {
return newObj, nil
}
}
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": case "rapid":
return y.FastUpload(ctx, dstDir, stream, up) return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
case "stream": case "stream":
if stream.GetSize() == 0 { if stream.GetSize() == 0 {
return y.FastUpload(ctx, dstDir, stream, up) return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
} }
fallthrough fallthrough
default: 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 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,8 @@ type Addition struct {
FamilyID string `json:"family_id"` FamilyID string `json:"family_id"`
UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"` UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"`
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"` 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"` NoUseOcr bool `json:"no_use_ocr"`
} }

View File

@ -6,6 +6,8 @@ import (
"sort" "sort"
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/pkg/utils"
) )
// 居然有四种返回方式 // 居然有四种返回方式
@ -141,7 +143,7 @@ type FamilyInfoListResp struct {
type FamilyInfoResp struct { type FamilyInfoResp struct {
Count int `json:"count"` Count int `json:"count"`
CreateTime string `json:"createTime"` CreateTime string `json:"createTime"`
FamilyID int `json:"familyId"` FamilyID int64 `json:"familyId"`
RemarkName string `json:"remarkName"` RemarkName string `json:"remarkName"`
Type int `json:"type"` Type int `json:"type"`
UseFlag int `json:"useFlag"` UseFlag int `json:"useFlag"`
@ -175,6 +177,14 @@ type Cloud189File struct {
// StarLabel int64 `json:"starLabel"` // StarLabel int64 `json:"starLabel"`
} }
func (c *Cloud189File) CreateTime() time.Time {
return time.Time(c.CreateDate)
}
func (c *Cloud189File) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.MD5, c.Md5)
}
func (c *Cloud189File) GetSize() int64 { return c.Size } func (c *Cloud189File) GetSize() int64 { return c.Size }
func (c *Cloud189File) GetName() string { return c.Name } func (c *Cloud189File) GetName() string { return c.Name }
func (c *Cloud189File) ModTime() time.Time { return time.Time(c.LastOpTime) } func (c *Cloud189File) ModTime() time.Time { return time.Time(c.LastOpTime) }
@ -199,6 +209,14 @@ type Cloud189Folder struct {
// StarLabel int64 `json:"starLabel"` // StarLabel int64 `json:"starLabel"`
} }
func (c *Cloud189Folder) CreateTime() time.Time {
return time.Time(c.CreateDate)
}
func (c *Cloud189Folder) GetHash() utils.HashInfo {
return utils.HashInfo{}
}
func (c *Cloud189Folder) GetSize() int64 { return 0 } func (c *Cloud189Folder) GetSize() int64 { return 0 }
func (c *Cloud189Folder) GetName() string { return c.Name } func (c *Cloud189Folder) GetName() string { return c.Name }
func (c *Cloud189Folder) ModTime() time.Time { return time.Time(c.LastOpTime) } func (c *Cloud189Folder) ModTime() time.Time { return time.Time(c.LastOpTime) }
@ -225,7 +243,12 @@ type BatchTaskInfo struct {
// IsFolder 是否是文件夹0-否1-是 // IsFolder 是否是文件夹0-否1-是
IsFolder int `json:"isFolder"` IsFolder int `json:"isFolder"`
// SrcParentId 文件所在父目录ID // SrcParentId 文件所在父目录ID
//SrcParentId string `json:"srcParentId"` SrcParentId string `json:"srcParentId,omitempty"`
/* 冲突管理 */
// 1 -> 跳过 2 -> 保留 3 -> 覆盖
DealWay int `json:"dealWay,omitempty"`
IsConflict int `json:"isConflict,omitempty"`
} }
/* 上传部分 */ /* 上传部分 */
@ -338,6 +361,14 @@ type BatchTaskStateResp struct {
TaskStatus int `json:"taskStatus"` //1 初始化 2 存在冲突 3 执行中4 完成 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 加密参数*/ /* query 加密参数*/
type Params map[string]string type Params map[string]string

View File

@ -2,6 +2,7 @@ package _189pc
import ( import (
"bytes" "bytes"
"container/ring"
"context" "context"
"crypto/md5" "crypto/md5"
"encoding/base64" "encoding/base64"
@ -13,7 +14,6 @@ import (
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"os"
"regexp" "regexp"
"sort" "sort"
"strconv" "strconv"
@ -55,11 +55,11 @@ const (
CHANNEL_ID = "web_cloud.189.cn" 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() dateOfGmt := getHttpDateStr()
sessionKey := y.tokenInfo.SessionKey sessionKey := y.tokenInfo.SessionKey
sessionSecret := y.tokenInfo.SessionSecret sessionSecret := y.tokenInfo.SessionSecret
if y.isFamily() { if isFamily {
sessionKey = y.tokenInfo.FamilySessionKey sessionKey = y.tokenInfo.FamilySessionKey
sessionSecret = y.tokenInfo.FamilySessionSecret sessionSecret = y.tokenInfo.FamilySessionSecret
} }
@ -73,9 +73,9 @@ func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]stri
return header return header
} }
func (y *Cloud189PC) EncryptParams(params Params) string { func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
sessionSecret := y.tokenInfo.SessionSecret sessionSecret := y.tokenInfo.SessionSecret
if y.isFamily() { if isFamily {
sessionSecret = y.tokenInfo.FamilySessionSecret sessionSecret = y.tokenInfo.FamilySessionSecret
} }
if params != nil { if params != nil {
@ -84,17 +84,17 @@ func (y *Cloud189PC) EncryptParams(params Params) string {
return "" 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()) req := y.client.R().SetQueryParams(clientSuffix())
// 设置params // 设置params
paramsData := y.EncryptParams(params) paramsData := y.EncryptParams(params, isBool(isFamily...))
if paramsData != "" { if paramsData != "" {
req.SetQueryParam("params", paramsData) req.SetQueryParam("params", paramsData)
} }
// Signature // Signature
req.SetHeaders(y.SignatureHeader(url, method, paramsData)) req.SetHeaders(y.SignatureHeader(url, method, paramsData, isBool(isFamily...)))
var erron RespErr var erron RespErr
req.SetError(&erron) req.SetError(&erron)
@ -130,15 +130,15 @@ func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, para
return res.Body(), nil return res.Body(), nil
} }
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) { func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
return y.request(url, http.MethodGet, callback, nil, resp) return y.request(url, http.MethodGet, callback, nil, resp, isFamily...)
} }
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) { func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
return y.request(url, http.MethodPost, callback, nil, resp) 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) req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
if err != nil { if err != nil {
return nil, err return nil, err
@ -155,7 +155,7 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
} }
if sign { 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) req.Header.Add(key, value)
} }
} }
@ -182,9 +182,9 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
} }
return body, nil 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 fullUrl := API_URL
if y.isFamily() { if isFamily {
fullUrl += "/family/file" fullUrl += "/family/file"
} }
fullUrl += "/listFiles.action" fullUrl += "/listFiles.action"
@ -202,7 +202,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj,
"pageNum": fmt.Sprint(pageNum), "pageNum": fmt.Sprint(pageNum),
"pageSize": "130", "pageSize": "130",
}) })
if y.isFamily() { if isFamily {
r.SetQueryParams(map[string]string{ r.SetQueryParams(map[string]string{
"familyId": y.FamilyID, "familyId": y.FamilyID,
"orderBy": toFamilyOrderBy(y.OrderBy), "orderBy": toFamilyOrderBy(y.OrderBy),
@ -215,7 +215,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj,
"descending": toDesc(y.OrderDirection), "descending": toDesc(y.OrderDirection),
}) })
} }
}, &resp) }, &resp, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -438,7 +438,7 @@ func (y *Cloud189PC) refreshSession() (err error) {
// 普通上传 // 普通上传
// 无法上传大小为0的文件 // 无法上传大小为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()) var sliceSize = partSize(file.GetSize())
count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize))) count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize)))
lastPartSize := file.GetSize() % sliceSize lastPartSize := file.GetSize() % sliceSize
@ -455,7 +455,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
} }
fullUrl := UPLOAD_URL fullUrl := UPLOAD_URL
if y.isFamily() { if isFamily {
params.Set("familyId", y.FamilyID) params.Set("familyId", y.FamilyID)
fullUrl += "/family" fullUrl += "/family"
} else { } else {
@ -467,7 +467,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
var initMultiUpload InitMultiUploadResp var initMultiUpload InitMultiUploadResp
_, err := y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) { _, err := y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
}, params, &initMultiUpload) }, params, &initMultiUpload, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -503,18 +503,18 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
partInfo := fmt.Sprintf("%d-%s", i, base64.StdEncoding.EncodeToString(md5Bytes)) partInfo := fmt.Sprintf("%d-%s", i, base64.StdEncoding.EncodeToString(md5Bytes))
threadG.Go(func(ctx context.Context) error { 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 { if err != nil {
return err return err
} }
// step.4 上传切片 // step.4 上传切片
uploadUrl := uploadUrls[0] 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 { if err != nil {
return err return err
} }
up(int(threadG.Success()) * 100 / count) up(float64(threadG.Success()) * 100 / float64(count))
return nil return nil
}) })
} }
@ -539,25 +539,38 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
"sliceMd5": sliceMd5Hex, "sliceMd5": sliceMd5Hex,
"lazyCheck": "1", "lazyCheck": "1",
"isLog": "0", "isLog": "0",
"opertype": "3", "opertype": IF(overwrite, "3", "1"),
}, &resp) }, &resp, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.toFile(), nil return resp.toFile(), nil
} }
// 快传 func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { fileMd5 := stream.GetHash().GetHash(utils.MD5)
// 需要获取完整文件md5,必须支持 io.Seek if len(fileMd5) < utils.MD5.Width {
tempFile, err := utils.CreateTempFile(file.GetReadCloser(), file.GetSize()) return nil, errors.New("invalid hash")
}
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()), isFamily)
if err != nil {
return nil, err
}
if uploadInfo.FileDataExists != 1 {
return nil, errors.New("rapid upload fail")
}
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, isFamily bool, overwrite bool) (model.Obj, error) {
tempFile, err := file.CacheFullInTempFile()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
var sliceSize = partSize(file.GetSize()) var sliceSize = partSize(file.GetSize())
count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize))) count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize)))
@ -582,7 +595,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
} }
silceMd5.Reset() 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 return nil, err
} }
md5Byte := silceMd5.Sum(nil) md5Byte := silceMd5.Sum(nil)
@ -597,7 +610,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
} }
fullUrl := UPLOAD_URL fullUrl := UPLOAD_URL
if y.isFamily() { if isFamily {
fullUrl += "/family" fullUrl += "/family"
} else { } else {
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`) //params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
@ -616,13 +629,13 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
"sliceSize": fmt.Sprint(sliceSize), "sliceSize": fmt.Sprint(sliceSize),
"sliceMd5": sliceMd5Hex, "sliceMd5": sliceMd5Hex,
} }
if y.isFamily() { if isFamily {
params.Set("familyId", y.FamilyID) params.Set("familyId", y.FamilyID)
} }
var uploadInfo InitMultiUploadResp var uploadInfo InitMultiUploadResp
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) { _, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
}, params, &uploadInfo) }, params, &uploadInfo, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -647,7 +660,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
i, uploadPart := i, uploadPart i, uploadPart := i, uploadPart
threadG.Go(func(ctx context.Context) error { threadG.Go(func(ctx context.Context) error {
// step.3 获取上传链接 // step.3 获取上传链接
uploadUrls, err := y.GetMultiUploadUrls(ctx, uploadInfo.UploadFileID, uploadPart) uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, uploadInfo.UploadFileID, uploadPart)
if err != nil { if err != nil {
return err return err
} }
@ -659,12 +672,12 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
} }
// step.4 上传切片 // 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 { if err != nil {
return err return err
} }
up(int(threadG.Success()) * 100 / len(uploadUrls)) up(float64(threadG.Success()) * 100 / float64(len(uploadUrls)))
uploadProgress.UploadParts[i] = "" uploadProgress.UploadParts[i] = ""
return nil return nil
}) })
@ -686,8 +699,8 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}, Params{ }, Params{
"uploadFileId": uploadInfo.UploadFileID, "uploadFileId": uploadInfo.UploadFileID,
"isLog": "0", "isLog": "0",
"opertype": "3", "opertype": IF(overwrite, "3", "1"),
}, &resp) }, &resp, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -696,9 +709,9 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
// 获取上传切片信息 // 获取上传切片信息
// 对http body有大小限制分片信息太多会出错 // 对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 fullUrl := UPLOAD_URL
if y.isFamily() { if isFamily {
fullUrl += "/family" fullUrl += "/family"
} else { } else {
fullUrl += "/person" fullUrl += "/person"
@ -711,7 +724,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string
}, Params{ }, Params{
"uploadFileId": uploadFileId, "uploadFileId": uploadFileId,
"partInfo": strings.Join(partInfo, ","), "partInfo": strings.Join(partInfo, ","),
}, &uploadUrlsResp) }, &uploadUrlsResp, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -740,70 +753,25 @@ 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) {
// 需要获取完整文件md5,必须支持 io.Seek tempFile, err := file.CacheFullInTempFile()
tempFile, err := utils.CreateTempFile(file.GetReadCloser(), file.GetSize())
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() { fileMd5, err := utils.HashFile(utils.MD5, tempFile)
_ = tempFile.Close() if err != nil {
_ = os.Remove(tempFile.Name())
}()
// 计算md5
fileMd5 := md5.New()
if _, err := io.Copy(fileMd5, tempFile); err != nil {
return nil, err return nil, err
} }
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return nil, err
}
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
// 创建上传会话 // 创建上传会话
var uploadInfo CreateUploadFileResp uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily)
fullUrl := API_URL + "/createUploadFile.action"
if y.isFamily() {
fullUrl = API_URL + "/family/file/createFamilyFile.action"
}
_, err = y.post(fullUrl, func(req *resty.Request) {
req.SetContext(ctx)
if y.isFamily() {
req.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"fileMd5": fileMd5Hex,
"fileName": file.GetName(),
"fileSize": fmt.Sprint(file.GetSize()),
"parentId": dstDir.GetID(),
"resumePolicy": "1",
})
} else {
req.SetFormData(map[string]string{
"parentFolderId": dstDir.GetID(),
"fileName": file.GetName(),
"size": fmt.Sprint(file.GetSize()),
"md5": fileMd5Hex,
"opertype": "3",
"flag": "1",
"resumePolicy": "1",
"isLog": "0",
// "baseFileId": "",
// "lastWrite":"",
// "localPath": strings.ReplaceAll(param.LocalPath, "\\", "/"),
// "fileExt": "",
})
}
}, &uploadInfo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 网盘中不存在该文件,开始上传 // 网盘中不存在该文件,开始上传
status := GetUploadFileStatusResp{CreateUploadFileResp: uploadInfo} status := GetUploadFileStatusResp{CreateUploadFileResp: *uploadInfo}
for status.Size < file.GetSize() && status.FileDataExists != 1 { for status.GetSize() < file.GetSize() && status.FileDataExists != 1 {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return nil, ctx.Err() return nil, ctx.Err()
} }
@ -813,14 +781,14 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
"Expect": "100-continue", "Expect": "100-continue",
} }
if y.isFamily() { if isFamily {
header["FamilyId"] = fmt.Sprint(y.FamilyID) header["FamilyId"] = fmt.Sprint(y.FamilyID)
header["UploadFileId"] = fmt.Sprint(status.UploadFileId) header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
} else { } else {
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId) 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" { if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
return nil, err return nil, err
} }
@ -835,39 +803,81 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
"uploadFileId": fmt.Sprint(status.UploadFileId), "uploadFileId": fmt.Sprint(status.UploadFileId),
"resumePolicy": "1", "resumePolicy": "1",
}) })
if y.isFamily() { if isFamily {
req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID)) req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
} }
}, &status) }, &status, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, err := tempFile.Seek(status.GetSize(), io.SeekStart); err != nil { if _, err := tempFile.Seek(status.GetSize(), io.SeekStart); err != nil {
return nil, err return nil, err
} }
up(int(status.Size / file.GetSize())) up(float64(status.GetSize()) / float64(file.GetSize()) * 100)
} }
// 提交 return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId, isFamily, overwrite)
var resp OldCommitUploadFileResp }
_, err = y.post(status.FileCommitUrl, func(req *resty.Request) {
// 创建上传会话
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 isFamily {
fullUrl = API_URL + "/family/file/createFamilyFile.action"
}
_, err := y.post(fullUrl, func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
if y.isFamily() { if isFamily {
req.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"parentId": parentID,
"fileMd5": fileMd5,
"fileName": fileName,
"fileSize": fileSize,
"resumePolicy": "1",
})
} else {
req.SetFormData(map[string]string{
"parentFolderId": parentID,
"fileName": fileName,
"size": fileSize,
"md5": fileMd5,
"opertype": "3",
"flag": "1",
"resumePolicy": "1",
"isLog": "0",
})
}
}, &uploadInfo, isFamily)
if err != nil {
return nil, err
}
return &uploadInfo, nil
}
// 提交上传文件
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 isFamily {
req.SetHeaders(map[string]string{ req.SetHeaders(map[string]string{
"ResumePolicy": "1", "ResumePolicy": "1",
"UploadFileId": fmt.Sprint(status.UploadFileId), "UploadFileId": fmt.Sprint(uploadFileID),
"FamilyId": fmt.Sprint(y.FamilyID), "FamilyId": fmt.Sprint(y.FamilyID),
}) })
} else { } else {
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
"opertype": "3", "opertype": IF(overwrite, "3", "1"),
"resumePolicy": "1", "resumePolicy": "1",
"uploadFileId": fmt.Sprint(status.UploadFileId), "uploadFileId": fmt.Sprint(uploadFileID),
"isLog": "0", "isLog": "0",
}) })
} }
}, &resp) }, &resp, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -886,10 +896,100 @@ func (y *Cloud189PC) isLogin() bool {
return err == nil 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) { func (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
var resp FamilyInfoListResp 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 { if err != nil {
return nil, err return nil, err
} }
@ -913,6 +1013,73 @@ func (y *Cloud189PC) getFamilyID() (string, error) {
return fmt.Sprint(infos[0].FamilyID), nil 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) { func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStateResp, error) {
var resp BatchTaskStateResp var resp BatchTaskStateResp
_, err := y.post(API_URL+"/batch/checkBatchTask.action", func(req *resty.Request) { _, err := y.post(API_URL+"/batch/checkBatchTask.action", func(req *resty.Request) {
@ -927,6 +1094,37 @@ func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStat
return &resp, nil 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 { func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration) error {
for { for {
state, err := y.CheckBatchTask(aType, taskID) state, err := y.CheckBatchTask(aType, taskID)
@ -935,7 +1133,7 @@ func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration)
} }
switch state.TaskStatus { switch state.TaskStatus {
case 2: case 2:
return errors.New("there is a conflict with the target object") return ErrIsConflict
case 4: case 4:
return nil return nil
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "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/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
) )
@ -45,6 +46,9 @@ func (d *Alias) Init(ctx context.Context) error {
d.oneKey = k d.oneKey = k
} }
d.autoFlatten = true d.autoFlatten = true
} else {
d.oneKey = ""
d.autoFlatten = false
} }
return nil return nil
} }
@ -111,4 +115,26 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
return nil, errs.ObjectNotFound 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) var _ driver.Driver = (*Alias)(nil)

View File

@ -9,19 +9,25 @@ type Addition struct {
// Usually one of two // Usually one of two
// driver.RootPath // driver.RootPath
// define other // 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{ var config = driver.Config{
Name: "Alias", Name: "Alias",
LocalSort: true, LocalSort: true,
NoCache: true, NoCache: true,
NoUpload: true, NoUpload: true,
DefaultRoot: "/", DefaultRoot: "/",
ProxyRangeOption: true,
} }
func init() { func init() {
op.RegisterDriver(func() driver.Driver { op.RegisterDriver(func() driver.Driver {
return &Alias{} return &Alias{
Addition: Addition{
ProtectSameName: true,
},
}
}) })
} }

View File

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

View File

@ -3,10 +3,12 @@ package alist_v3
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"net/http" "net/http"
"path" "path"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/conf"
@ -93,8 +95,10 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
Object: model.Object{ Object: model.Object{
Name: f.Name, Name: f.Name,
Modified: f.Modified, Modified: f.Modified,
Ctime: f.Created,
Size: f.Size, Size: f.Size,
IsFolder: f.IsDir, IsFolder: f.IsDir,
HashInfo: utils.FromString(f.HashInfo),
}, },
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb}, Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
} }
@ -105,11 +109,19 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var resp common.Resp[FsGetResp] 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) { _, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(FsGetReq{ req.SetResult(&resp).SetBody(FsGetReq{
Path: file.GetPath(), Path: file.GetPath(),
Password: d.MetaPassword, Password: d.MetaPassword,
}) }).SetHeader("user-agent", userAgent)
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -171,13 +183,13 @@ 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 { func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
_, err := d.request("/fs/put", http.MethodPut, func(req *resty.Request) { _, err := d.requestWithTimeout("/fs/put", http.MethodPut, func(req *resty.Request) {
req.SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())). req.SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
SetHeader("Password", d.MetaPassword). SetHeader("Password", d.MetaPassword).
SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)). SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
SetContentLength(true). SetContentLength(true).
SetBody(stream.GetReadCloser()) SetBody(io.ReadCloser(stream))
}) }, time.Hour*6)
return err return err
} }

View File

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

View File

@ -18,9 +18,11 @@ type ObjResp struct {
Size int64 `json:"size"` Size int64 `json:"size"`
IsDir bool `json:"is_dir"` IsDir bool `json:"is_dir"`
Modified time.Time `json:"modified"` Modified time.Time `json:"modified"`
Created time.Time `json:"created"`
Sign string `json:"sign"` Sign string `json:"sign"`
Thumb string `json:"thumb"` Thumb string `json:"thumb"`
Type int `json:"type"` Type int `json:"type"`
HashInfo string `json:"hashinfo"`
} }
type FsListResp struct { type FsListResp struct {

View File

@ -3,6 +3,7 @@ package alist_v3
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/op"
@ -56,3 +57,33 @@ func (d *AListV3) request(api, method string, callback base.ReqCallback, retry .
} }
return res.Body(), nil 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

@ -14,6 +14,8 @@ import (
"os" "os"
"time" "time"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
@ -50,7 +52,7 @@ func (d *AliDrive) Init(ctx context.Context) error {
return err return err
} }
// get driver id // get driver id
res, err, _ := d.request("https://api.aliyundrive.com/v2/user/get", http.MethodPost, nil, nil) res, err, _ := d.request("https://api.alipan.com/v2/user/get", http.MethodPost, nil, nil)
if err != nil { if err != nil {
return err return err
} }
@ -67,7 +69,7 @@ func (d *AliDrive) Init(ctx context.Context) error {
return nil return nil
} }
// init deviceID // init deviceID
deviceID := utils.GetSHA256Encode([]byte(d.UserID)) deviceID := utils.HashData(utils.SHA256, []byte(d.UserID))
// init privateKey // init privateKey
privateKey, _ := NewPrivateKeyFromHex(deviceID) privateKey, _ := NewPrivateKeyFromHex(deviceID)
state := State{ state := State{
@ -104,7 +106,7 @@ func (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs
"file_id": file.GetID(), "file_id": file.GetID(),
"expire_sec": 14400, "expire_sec": 14400,
} }
res, err, _ := d.request("https://api.aliyundrive.com/v2/file/get_download_url", http.MethodPost, func(req *resty.Request) { res, err, _ := d.request("https://api.alipan.com/v2/file/get_download_url", http.MethodPost, func(req *resty.Request) {
req.SetBody(data) req.SetBody(data)
}, nil) }, nil)
if err != nil { if err != nil {
@ -112,14 +114,14 @@ func (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs
} }
return &model.Link{ return &model.Link{
Header: http.Header{ Header: http.Header{
"Referer": []string{"https://www.aliyundrive.com/"}, "Referer": []string{"https://www.alipan.com/"},
}, },
URL: utils.Json.Get(res, "url").ToString(), URL: utils.Json.Get(res, "url").ToString(),
}, nil }, nil
} }
func (d *AliDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { func (d *AliDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
_, err, _ := d.request("https://api.aliyundrive.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) { _, err, _ := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{ req.SetBody(base.Json{
"check_name_mode": "refuse", "check_name_mode": "refuse",
"drive_id": d.DriveId, "drive_id": d.DriveId,
@ -137,7 +139,7 @@ func (d *AliDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
} }
func (d *AliDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error { func (d *AliDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
_, err, _ := d.request("https://api.aliyundrive.com/v3/file/update", http.MethodPost, func(req *resty.Request) { _, err, _ := d.request("https://api.alipan.com/v3/file/update", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{ req.SetBody(base.Json{
"check_name_mode": "refuse", "check_name_mode": "refuse",
"drive_id": d.DriveId, "drive_id": d.DriveId,
@ -154,7 +156,7 @@ func (d *AliDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
} }
func (d *AliDrive) Remove(ctx context.Context, obj model.Obj) error { func (d *AliDrive) Remove(ctx context.Context, obj model.Obj) error {
_, err, _ := d.request("https://api.aliyundrive.com/v2/recyclebin/trash", http.MethodPost, func(req *resty.Request) { _, err, _ := d.request("https://api.alipan.com/v2/recyclebin/trash", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{ req.SetBody(base.Json{
"drive_id": d.DriveId, "drive_id": d.DriveId,
"file_id": obj.GetID(), "file_id": obj.GetID(),
@ -163,14 +165,14 @@ func (d *AliDrive) Remove(ctx context.Context, obj model.Obj) error {
return err return err
} }
func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error {
file := model.FileStream{ file := stream.FileStream{
Obj: stream, Obj: streamer,
ReadCloser: stream, Reader: streamer,
Mimetype: stream.GetMimetype(), Mimetype: streamer.GetMimetype(),
} }
const DEFAULT int64 = 10485760 const DEFAULT int64 = 10485760
var count = int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT))) var count = int(math.Ceil(float64(streamer.GetSize()) / float64(DEFAULT)))
partInfoList := make([]base.Json, 0, count) partInfoList := make([]base.Json, 0, count)
for i := 1; i <= count; i++ { for i := 1; i <= count; i++ {
@ -187,25 +189,25 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
} }
var localFile *os.File var localFile *os.File
if fileStream, ok := file.ReadCloser.(*model.FileStream); ok { if fileStream, ok := file.Reader.(*stream.FileStream); ok {
localFile, _ = fileStream.ReadCloser.(*os.File) localFile, _ = fileStream.Reader.(*os.File)
} }
if d.RapidUpload { if d.RapidUpload {
buf := bytes.NewBuffer(make([]byte, 0, 1024)) buf := bytes.NewBuffer(make([]byte, 0, 1024))
io.CopyN(buf, file, 1024) utils.CopyWithBufferN(buf, file, 1024)
reqBody["pre_hash"] = utils.GetSHA1Encode(buf.Bytes()) reqBody["pre_hash"] = utils.HashData(utils.SHA1, buf.Bytes())
if localFile != nil { if localFile != nil {
if _, err := localFile.Seek(0, io.SeekStart); err != nil { if _, err := localFile.Seek(0, io.SeekStart); err != nil {
return err return err
} }
} else { } else {
// 把头部拼接回去 // 把头部拼接回去
file.ReadCloser = struct { file.Reader = struct {
io.Reader io.Reader
io.Closer io.Closer
}{ }{
Reader: io.MultiReader(buf, file), Reader: io.MultiReader(buf, file),
Closer: file, Closer: &file,
} }
} }
} else { } else {
@ -214,7 +216,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
} }
var resp UploadResp var resp UploadResp
_, err, e := d.request("https://api.aliyundrive.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) { _, err, e := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
req.SetBody(reqBody) req.SetBody(reqBody)
}, &resp) }, &resp)
@ -268,7 +270,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
n, _ := io.NewSectionReader(localFile, o.Int64(), 8).Read(buf[:8]) n, _ := io.NewSectionReader(localFile, o.Int64(), 8).Read(buf[:8])
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n]) reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
_, err, e := d.request("https://api.aliyundrive.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) { _, err, e := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
req.SetBody(reqBody) req.SetBody(reqBody)
}, &resp) }, &resp)
if err != nil && e.Code != "PreHashMatched" { if err != nil && e.Code != "PreHashMatched" {
@ -281,7 +283,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
if _, err = localFile.Seek(0, io.SeekStart); err != nil { if _, err = localFile.Seek(0, io.SeekStart); err != nil {
return err return err
} }
file.ReadCloser = localFile file.Reader = localFile
} }
for i, partInfo := range resp.PartInfoList { for i, partInfo := range resp.PartInfoList {
@ -303,11 +305,11 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
} }
res.Body.Close() res.Body.Close()
if count > 0 { if count > 0 {
up(i * 100 / count) up(float64(i) * 100 / float64(count))
} }
} }
var resp2 base.Json var resp2 base.Json
_, err, e = d.request("https://api.aliyundrive.com/v2/file/complete", http.MethodPost, func(req *resty.Request) { _, err, e = d.request("https://api.alipan.com/v2/file/complete", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{ req.SetBody(base.Json{
"drive_id": d.DriveId, "drive_id": d.DriveId,
"file_id": resp.FileId, "file_id": resp.FileId,
@ -332,10 +334,10 @@ func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}
} }
switch args.Method { switch args.Method {
case "doc_preview": case "doc_preview":
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url" url = "https://api.alipan.com/v2/file/get_office_preview_url"
data["access_token"] = d.AccessToken data["access_token"] = d.AccessToken
case "video_preview": case "video_preview":
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info" url = "https://api.alipan.com/v2/file/get_video_preview_play_info"
data["category"] = "live_transcoding" data["category"] = "live_transcoding"
data["url_expire_sec"] = 14400 data["url_expire_sec"] = 14400
default: default:

View File

@ -26,7 +26,7 @@ func (d *AliDrive) createSession() error {
state.retry = 0 state.retry = 0
return fmt.Errorf("createSession failed after three retries") return fmt.Errorf("createSession failed after three retries")
} }
_, err, _ := d.request("https://api.aliyundrive.com/users/v1/users/device/create_session", http.MethodPost, func(req *resty.Request) { _, err, _ := d.request("https://api.alipan.com/users/v1/users/device/create_session", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{ req.SetBody(base.Json{
"deviceName": "samsung", "deviceName": "samsung",
"modelName": "SM-G9810", "modelName": "SM-G9810",
@ -42,7 +42,7 @@ func (d *AliDrive) createSession() error {
} }
// func (d *AliDrive) renewSession() error { // func (d *AliDrive) renewSession() error {
// _, err, _ := d.request("https://api.aliyundrive.com/users/v1/users/device/renew_session", http.MethodPost, nil, nil) // _, err, _ := d.request("https://api.alipan.com/users/v1/users/device/renew_session", http.MethodPost, nil, nil)
// return err // return err
// } // }
@ -58,7 +58,7 @@ func (d *AliDrive) sign() {
// do others that not defined in Driver interface // do others that not defined in Driver interface
func (d *AliDrive) refreshToken() error { func (d *AliDrive) refreshToken() error {
url := "https://auth.aliyundrive.com/v2/account/token" url := "https://auth.alipan.com/v2/account/token"
var resp base.TokenResp var resp base.TokenResp
var e RespErr var e RespErr
_, err := base.RestyClient.R(). _, err := base.RestyClient.R().
@ -85,7 +85,7 @@ func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp i
req := base.RestyClient.R() req := base.RestyClient.R()
state, ok := global.Load(d.UserID) state, ok := global.Load(d.UserID)
if !ok { if !ok {
if url == "https://api.aliyundrive.com/v2/user/get" { if url == "https://api.alipan.com/v2/user/get" {
state = &State{} state = &State{}
} else { } else {
return nil, fmt.Errorf("can't load user state, user_id: %s", d.UserID), RespErr{} return nil, fmt.Errorf("can't load user state, user_id: %s", d.UserID), RespErr{}
@ -94,8 +94,8 @@ func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp i
req.SetHeaders(map[string]string{ req.SetHeaders(map[string]string{
"Authorization": "Bearer\t" + d.AccessToken, "Authorization": "Bearer\t" + d.AccessToken,
"content-type": "application/json", "content-type": "application/json",
"origin": "https://www.aliyundrive.com", "origin": "https://www.alipan.com",
"Referer": "https://aliyundrive.com/", "Referer": "https://alipan.com/",
"X-Signature": state.signature, "X-Signature": state.signature,
"x-request-id": uuid.NewString(), "x-request-id": uuid.NewString(),
"X-Canary": "client=Android,app=adrive,version=v4.1.0", "X-Canary": "client=Android,app=adrive,version=v4.1.0",
@ -158,7 +158,7 @@ func (d *AliDrive) getFiles(fileId string) ([]File, error) {
"video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300", "video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300",
"url_expire_sec": 14400, "url_expire_sec": 14400,
} }
_, err, _ := d.request("https://api.aliyundrive.com/v2/file/list", http.MethodPost, func(req *resty.Request) { _, err, _ := d.request("https://api.alipan.com/v2/file/list", http.MethodPost, func(req *resty.Request) {
req.SetBody(data) req.SetBody(data)
}, &resp) }, &resp)
@ -172,7 +172,7 @@ func (d *AliDrive) getFiles(fileId string) ([]File, error) {
} }
func (d *AliDrive) batch(srcId, dstId string, url string) error { func (d *AliDrive) batch(srcId, dstId string, url string) error {
res, err, _ := d.request("https://api.aliyundrive.com/v3/batch", http.MethodPost, func(req *resty.Request) { res, err, _ := d.request("https://api.alipan.com/v3/batch", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{ req.SetBody(base.Json{
"requests": []base.Json{ "requests": []base.Json{
{ {

View File

@ -93,7 +93,7 @@ func (d *AliyundriveOpen) link(ctx context.Context, file model.Obj) (*model.Link
} }
url = utils.Json.Get(res, "streamsUrl", d.LIVPDownloadFormat).ToString() url = utils.Json.Get(res, "streamsUrl", d.LIVPDownloadFormat).ToString()
} }
exp := time.Hour exp := time.Minute
return &model.Link{ return &model.Link{
URL: url, URL: url,
Expiration: &exp, Expiration: &exp,

View File

@ -11,7 +11,7 @@ type Addition struct {
RefreshToken string `json:"refresh_token" required:"true"` RefreshToken string `json:"refresh_token" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"` OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"` OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
OauthTokenURL string `json:"oauth_token_url" default:"https://api.xhofe.top/alist/ali_open/token"` OauthTokenURL string `json:"oauth_token_url" default:"https://api.nn.ci/alist/ali_open/token"`
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"` ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"` ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"` RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
@ -36,7 +36,7 @@ var config = driver.Config{
func init() { func init() {
op.RegisterDriver(func() driver.Driver { op.RegisterDriver(func() driver.Driver {
return &AliyundriveOpen{ return &AliyundriveOpen{
base: "https://openapi.aliyundrive.com", base: "https://openapi.alipan.com",
} }
}) })
} }

View File

@ -1,6 +1,7 @@
package aliyundrive_open package aliyundrive_open
import ( import (
"github.com/alist-org/alist/v3/pkg/utils"
"time" "time"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
@ -46,6 +47,8 @@ func fileToObj(f File) *model.ObjThumb {
Size: f.Size, Size: f.Size,
Modified: f.UpdatedAt, Modified: f.UpdatedAt,
IsFolder: f.Type == "folder", IsFolder: f.Type == "folder",
Ctime: f.CreatedAt,
HashInfo: utils.NewHashInfo(utils.SHA1, f.ContentHash),
}, },
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail}, Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
} }

View File

@ -3,14 +3,11 @@ package aliyundrive_open
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/sha1"
"encoding/base64" "encoding/base64"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"math" "math"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -18,6 +15,7 @@ import (
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/avast/retry-go" "github.com/avast/retry-go"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
@ -33,19 +31,19 @@ func makePartInfos(size int) []base.Json {
} }
func calPartSize(fileSize int64) int64 { func calPartSize(fileSize int64) int64 {
var partSize int64 = 20 * 1024 * 1024 var partSize int64 = 20 * utils.MB
if fileSize > partSize { if fileSize > partSize {
if fileSize > 1*1024*1024*1024*1024 { // file Size over 1TB if fileSize > 1*utils.TB { // file Size over 1TB
partSize = 5 * 1024 * 1024 * 1024 // file part size 5GB partSize = 5 * utils.GB // file part size 5GB
} else if fileSize > 768*1024*1024*1024 { // over 768GB } else if fileSize > 768*utils.GB { // over 768GB
partSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part partSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part
} else if fileSize > 512*1024*1024*1024 { // over 512GB } else if fileSize > 512*utils.GB { // over 512GB
partSize = 82463373 // ≈ 78.6432MB partSize = 82463373 // ≈ 78.6432MB
} else if fileSize > 384*1024*1024*1024 { // over 384GB } else if fileSize > 384*utils.GB { // over 384GB
partSize = 54975582 // ≈ 52.4288MB partSize = 54975582 // ≈ 52.4288MB
} else if fileSize > 256*1024*1024*1024 { // over 256GB } else if fileSize > 256*utils.GB { // over 256GB
partSize = 41231687 // ≈ 39.3216MB partSize = 41231687 // ≈ 39.3216MB
} else if fileSize > 128*1024*1024*1024 { // over 128GB } else if fileSize > 128*utils.GB { // over 128GB
partSize = 27487791 // ≈ 26.2144MB partSize = 27487791 // ≈ 26.2144MB
} }
} }
@ -127,17 +125,22 @@ func getProofRange(input string, size int64) (*ProofRange, error) {
return pr, nil return pr, nil
} }
func (d *AliyundriveOpen) calProofCode(file *os.File, fileSize int64) (string, error) { func (d *AliyundriveOpen) calProofCode(stream model.FileStreamer) (string, error) {
proofRange, err := getProofRange(d.AccessToken, fileSize) proofRange, err := getProofRange(d.AccessToken, stream.GetSize())
if err != nil { if err != nil {
return "", err return "", err
} }
buf := make([]byte, proofRange.End-proofRange.Start) length := proofRange.End - proofRange.Start
_, err = file.ReadAt(buf, proofRange.Start) buf := bytes.NewBuffer(make([]byte, 0, length))
reader, err := stream.RangeRead(http_range.Range{Start: proofRange.Start, Length: length})
if err != nil { if err != nil {
return "", err return "", err
} }
return base64.StdEncoding.EncodeToString(buf), nil _, err = utils.CopyWithBufferN(buf, reader, length)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
} }
func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
@ -145,70 +148,67 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
// Part Size Unit: Bytes, Default: 20MB, // Part Size Unit: Bytes, Default: 20MB,
// Maximum number of slices 10,000, ≈195.3125GB // Maximum number of slices 10,000, ≈195.3125GB
var partSize = calPartSize(stream.GetSize()) var partSize = calPartSize(stream.GetSize())
const dateFormat = "2006-01-02T15:04:05.000Z"
mtimeStr := stream.ModTime().UTC().Format(dateFormat)
ctimeStr := stream.CreateTime().UTC().Format(dateFormat)
createData := base.Json{ createData := base.Json{
"drive_id": d.DriveId, "drive_id": d.DriveId,
"parent_file_id": dstDir.GetID(), "parent_file_id": dstDir.GetID(),
"name": stream.GetName(), "name": stream.GetName(),
"type": "file", "type": "file",
"check_name_mode": "ignore", "check_name_mode": "ignore",
"local_modified_at": mtimeStr,
"local_created_at": ctimeStr,
} }
count := int(math.Ceil(float64(stream.GetSize()) / float64(partSize))) count := int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
createData["part_info_list"] = makePartInfos(count) createData["part_info_list"] = makePartInfos(count)
// rapid upload // rapid upload
rapidUpload := stream.GetSize() > 100*1024 && d.RapidUpload rapidUpload := !stream.IsForceStreamUpload() && stream.GetSize() > 100*utils.KB && d.RapidUpload
if rapidUpload { if rapidUpload {
log.Debugf("[aliyundrive_open] start cal pre_hash") log.Debugf("[aliyundrive_open] start cal pre_hash")
// read 1024 bytes to calculate pre hash // read 1024 bytes to calculate pre hash
buf := bytes.NewBuffer(make([]byte, 0, 1024)) reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: 1024})
_, err := io.CopyN(buf, stream, 1024) if err != nil {
return nil, err
}
hash, err := utils.HashReader(utils.SHA1, reader)
if err != nil { if err != nil {
return nil, err return nil, err
} }
createData["size"] = stream.GetSize() createData["size"] = stream.GetSize()
createData["pre_hash"] = utils.GetSHA1Encode(buf.Bytes()) createData["pre_hash"] = hash
// if support seek, seek to start
if localFile, ok := stream.(io.Seeker); ok {
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
return nil, err
}
} else {
// Put spliced head back to stream
stream.SetReadCloser(struct {
io.Reader
io.Closer
}{
Reader: io.MultiReader(buf, stream.GetReadCloser()),
Closer: stream.GetReadCloser(),
})
}
} }
var createResp CreateResp var createResp CreateResp
_, err, e := d.requestReturnErrResp("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) { _, err, e := d.requestReturnErrResp("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
req.SetBody(createData).SetResult(&createResp) req.SetBody(createData).SetResult(&createResp)
}) })
var tmpF model.File
if err != nil { if err != nil {
if e.Code != "PreHashMatched" || !rapidUpload { if e.Code != "PreHashMatched" || !rapidUpload {
return nil, err return nil, err
} }
log.Debugf("[aliyundrive_open] pre_hash matched, start rapid upload") log.Debugf("[aliyundrive_open] pre_hash matched, start rapid upload")
// convert to local file
file, err := utils.CreateTempFile(stream, stream.GetSize()) hi := stream.GetHash()
if err != nil { hash := hi.GetHash(utils.SHA1)
return nil, err if len(hash) <= 0 {
} tmpF, err = stream.CacheFullInTempFile()
_ = stream.GetReadCloser().Close() if err != nil {
stream.SetReadCloser(file) return nil, err
// calculate full hash }
h := sha1.New() hash, err = utils.HashFile(utils.SHA1, tmpF)
_, err = io.Copy(h, file) if err != nil {
if err != nil { return nil, err
return nil, err }
} }
delete(createData, "pre_hash") delete(createData, "pre_hash")
createData["proof_version"] = "v1" createData["proof_version"] = "v1"
createData["content_hash_name"] = "sha1" createData["content_hash_name"] = "sha1"
createData["content_hash"] = hex.EncodeToString(h.Sum(nil)) createData["content_hash"] = hash
createData["proof_code"], err = d.calProofCode(file, stream.GetSize()) createData["proof_code"], err = d.calProofCode(stream)
if err != nil { if err != nil {
return nil, fmt.Errorf("cal proof code error: %s", err.Error()) return nil, fmt.Errorf("cal proof code error: %s", err.Error())
} }
@ -218,17 +218,15 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
if err != nil { if err != nil {
return nil, err return nil, err
} }
// seek to start
if _, err = file.Seek(0, io.SeekStart); err != nil {
return nil, err
}
} }
if !createResp.RapidUpload { if !createResp.RapidUpload {
// 2. upload // 2. normal upload
log.Debugf("[aliyundive_open] normal upload") log.Debugf("[aliyundive_open] normal upload")
preTime := time.Now() preTime := time.Now()
var offset, length int64 = 0, partSize
//var length
for i := 0; i < len(createResp.PartInfoList); i++ { for i := 0; i < len(createResp.PartInfoList); i++ {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return nil, ctx.Err() return nil, ctx.Err()
@ -241,7 +239,17 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
} }
preTime = time.Now() preTime = time.Now()
} }
if remain := stream.GetSize() - offset; length > remain {
length = remain
}
rd := utils.NewMultiReadable(io.LimitReader(stream, partSize)) 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 { err = retry.Do(func() error {
rd.Reset() rd.Reset()
return d.uploadPart(ctx, rd, createResp.PartInfoList[i]) return d.uploadPart(ctx, rd, createResp.PartInfoList[i])
@ -252,6 +260,8 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
if err != nil { if err != nil {
return nil, err return nil, err
} }
offset += partSize
up(float64(i*100) / float64(count))
} }
} else { } else {
log.Debugf("[aliyundrive_open] rapid upload success, file id: %s", createResp.FileId) log.Debugf("[aliyundrive_open] rapid upload success, file id: %s", createResp.FileId)

View File

@ -26,7 +26,7 @@ func (d *AliyundriveOpen) _refreshToken() (string, string, error) {
//var resp base.TokenResp //var resp base.TokenResp
var e ErrResp var e ErrResp
res, err := base.RestyClient.R(). res, err := base.RestyClient.R().
ForceContentType("application/json"). //ForceContentType("application/json").
SetBody(base.Json{ SetBody(base.Json{
"client_id": d.ClientID, "client_id": d.ClientID,
"client_secret": d.ClientSecret, "client_secret": d.ClientSecret,
@ -45,7 +45,7 @@ func (d *AliyundriveOpen) _refreshToken() (string, string, error) {
} }
refresh, access := utils.Json.Get(res.Body(), "refresh_token").ToString(), utils.Json.Get(res.Body(), "access_token").ToString() refresh, access := utils.Json.Get(res.Body(), "refresh_token").ToString(), utils.Json.Get(res.Body(), "access_token").ToString()
if refresh == "" { if refresh == "" {
return "", "", errors.New("failed to refresh token: refresh token is empty") return "", "", fmt.Errorf("failed to refresh token: refresh token is empty, resp: %s", res.String())
} }
curSub, err := getSub(d.RefreshToken) curSub, err := getSub(d.RefreshToken)
if err != nil { if err != nil {
@ -86,7 +86,7 @@ func (d *AliyundriveOpen) refreshToken() error {
if err != nil { if err != nil {
return err return err
} }
log.Infof("[ali_open] toekn exchange: %s -> %s", d.RefreshToken, refresh) log.Infof("[ali_open] token exchange: %s -> %s", d.RefreshToken, refresh)
d.RefreshToken, d.AccessToken = refresh, access d.RefreshToken, d.AccessToken = refresh, access
op.MustSaveDriverStorage(d) op.MustSaveDriverStorage(d)
return nil return nil

View File

@ -105,7 +105,7 @@ func (d *AliyundriveShare) link(ctx context.Context, file model.Obj) (*model.Lin
"share_id": d.ShareId, "share_id": d.ShareId,
} }
var resp ShareLinkResp var resp ShareLinkResp
_, err := d.request("https://api.aliyundrive.com/v2/file/get_share_link_download_url", http.MethodPost, func(req *resty.Request) { _, err := d.request("https://api.alipan.com/v2/file/get_share_link_download_url", http.MethodPost, func(req *resty.Request) {
req.SetHeader(CanaryHeaderKey, CanaryHeaderValue).SetBody(data).SetResult(&resp) req.SetHeader(CanaryHeaderKey, CanaryHeaderValue).SetBody(data).SetResult(&resp)
}) })
if err != nil { if err != nil {
@ -113,7 +113,7 @@ func (d *AliyundriveShare) link(ctx context.Context, file model.Obj) (*model.Lin
} }
return &model.Link{ return &model.Link{
Header: http.Header{ Header: http.Header{
"Referer": []string{"https://www.aliyundrive.com/"}, "Referer": []string{"https://www.alipan.com/"},
}, },
URL: resp.DownloadUrl, URL: resp.DownloadUrl,
}, nil }, nil
@ -128,9 +128,9 @@ func (d *AliyundriveShare) Other(ctx context.Context, args model.OtherArgs) (int
} }
switch args.Method { switch args.Method {
case "doc_preview": case "doc_preview":
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url" url = "https://api.alipan.com/v2/file/get_office_preview_url"
case "video_preview": case "video_preview":
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info" url = "https://api.alipan.com/v2/file/get_video_preview_play_info"
data["category"] = "live_transcoding" data["category"] = "live_transcoding"
default: default:
return nil, errs.NotSupport return nil, errs.NotSupport

View File

@ -44,6 +44,7 @@ func fileToObj(f File) *model.ObjThumb {
Name: f.Name, Name: f.Name,
Size: f.Size, Size: f.Size,
Modified: f.UpdatedAt, Modified: f.UpdatedAt,
Ctime: f.CreatedAt,
IsFolder: f.Type == "folder", IsFolder: f.Type == "folder",
}, },
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail}, Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},

View File

@ -16,7 +16,7 @@ const (
) )
func (d *AliyundriveShare) refreshToken() error { func (d *AliyundriveShare) refreshToken() error {
url := "https://auth.aliyundrive.com/v2/account/token" url := "https://auth.alipan.com/v2/account/token"
var resp base.TokenResp var resp base.TokenResp
var e ErrorResp var e ErrorResp
_, err := base.RestyClient.R(). _, err := base.RestyClient.R().
@ -47,7 +47,7 @@ func (d *AliyundriveShare) getShareToken() error {
var resp ShareTokenResp var resp ShareTokenResp
_, err := base.RestyClient.R(). _, err := base.RestyClient.R().
SetResult(&resp).SetError(&e).SetBody(data). SetResult(&resp).SetError(&e).SetBody(data).
Post("https://api.aliyundrive.com/v2/share_link/get_share_token") Post("https://api.alipan.com/v2/share_link/get_share_token")
if err != nil { if err != nil {
return err return err
} }
@ -116,7 +116,7 @@ func (d *AliyundriveShare) getFiles(fileId string) ([]File, error) {
SetHeader("x-share-token", d.ShareToken). SetHeader("x-share-token", d.ShareToken).
SetHeader(CanaryHeaderKey, CanaryHeaderValue). SetHeader(CanaryHeaderKey, CanaryHeaderValue).
SetResult(&resp).SetError(&e).SetBody(data). SetResult(&resp).SetError(&e).SetBody(data).
Post("https://api.aliyundrive.com/adrive/v3/file/list") Post("https://api.alipan.com/adrive/v3/file/list")
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,7 +2,9 @@ package drivers
import ( import (
_ "github.com/alist-org/alist/v3/drivers/115" _ "github.com/alist-org/alist/v3/drivers/115"
_ "github.com/alist-org/alist/v3/drivers/115_share"
_ "github.com/alist-org/alist/v3/drivers/123" _ "github.com/alist-org/alist/v3/drivers/123"
_ "github.com/alist-org/alist/v3/drivers/123_link"
_ "github.com/alist-org/alist/v3/drivers/123_share" _ "github.com/alist-org/alist/v3/drivers/123_share"
_ "github.com/alist-org/alist/v3/drivers/139" _ "github.com/alist-org/alist/v3/drivers/139"
_ "github.com/alist-org/alist/v3/drivers/189" _ "github.com/alist-org/alist/v3/drivers/189"
@ -16,23 +18,27 @@ import (
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk" _ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
_ "github.com/alist-org/alist/v3/drivers/baidu_photo" _ "github.com/alist-org/alist/v3/drivers/baidu_photo"
_ "github.com/alist-org/alist/v3/drivers/baidu_share" _ "github.com/alist-org/alist/v3/drivers/baidu_share"
_ "github.com/alist-org/alist/v3/drivers/chaoxing"
_ "github.com/alist-org/alist/v3/drivers/cloudreve" _ "github.com/alist-org/alist/v3/drivers/cloudreve"
_ "github.com/alist-org/alist/v3/drivers/crypt" _ "github.com/alist-org/alist/v3/drivers/crypt"
_ "github.com/alist-org/alist/v3/drivers/dropbox" _ "github.com/alist-org/alist/v3/drivers/dropbox"
_ "github.com/alist-org/alist/v3/drivers/ftp" _ "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_drive"
_ "github.com/alist-org/alist/v3/drivers/google_photo" _ "github.com/alist-org/alist/v3/drivers/google_photo"
_ "github.com/alist-org/alist/v3/drivers/ilanzou"
_ "github.com/alist-org/alist/v3/drivers/ipfs_api" _ "github.com/alist-org/alist/v3/drivers/ipfs_api"
_ "github.com/alist-org/alist/v3/drivers/lanzou" _ "github.com/alist-org/alist/v3/drivers/lanzou"
_ "github.com/alist-org/alist/v3/drivers/local" _ "github.com/alist-org/alist/v3/drivers/local"
_ "github.com/alist-org/alist/v3/drivers/mediatrack" _ "github.com/alist-org/alist/v3/drivers/mediatrack"
_ "github.com/alist-org/alist/v3/drivers/mega" _ "github.com/alist-org/alist/v3/drivers/mega"
_ "github.com/alist-org/alist/v3/drivers/mopan" _ "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"
_ "github.com/alist-org/alist/v3/drivers/onedrive_app" _ "github.com/alist-org/alist/v3/drivers/onedrive_app"
_ "github.com/alist-org/alist/v3/drivers/pikpak" _ "github.com/alist-org/alist/v3/drivers/pikpak"
_ "github.com/alist-org/alist/v3/drivers/pikpak_share" _ "github.com/alist-org/alist/v3/drivers/pikpak_share"
_ "github.com/alist-org/alist/v3/drivers/quark_uc" _ "github.com/alist-org/alist/v3/drivers/quark_uc"
_ "github.com/alist-org/alist/v3/drivers/quqi"
_ "github.com/alist-org/alist/v3/drivers/s3" _ "github.com/alist-org/alist/v3/drivers/s3"
_ "github.com/alist-org/alist/v3/drivers/seafile" _ "github.com/alist-org/alist/v3/drivers/seafile"
_ "github.com/alist-org/alist/v3/drivers/sftp" _ "github.com/alist-org/alist/v3/drivers/sftp"
@ -40,10 +46,12 @@ import (
_ "github.com/alist-org/alist/v3/drivers/teambition" _ "github.com/alist-org/alist/v3/drivers/teambition"
_ "github.com/alist-org/alist/v3/drivers/terabox" _ "github.com/alist-org/alist/v3/drivers/terabox"
_ "github.com/alist-org/alist/v3/drivers/thunder" _ "github.com/alist-org/alist/v3/drivers/thunder"
_ "github.com/alist-org/alist/v3/drivers/thunderx"
_ "github.com/alist-org/alist/v3/drivers/trainbit" _ "github.com/alist-org/alist/v3/drivers/trainbit"
_ "github.com/alist-org/alist/v3/drivers/url_tree" _ "github.com/alist-org/alist/v3/drivers/url_tree"
_ "github.com/alist-org/alist/v3/drivers/uss" _ "github.com/alist-org/alist/v3/drivers/uss"
_ "github.com/alist-org/alist/v3/drivers/virtual" _ "github.com/alist-org/alist/v3/drivers/virtual"
_ "github.com/alist-org/alist/v3/drivers/vtencent"
_ "github.com/alist-org/alist/v3/drivers/webdav" _ "github.com/alist-org/alist/v3/drivers/webdav"
_ "github.com/alist-org/alist/v3/drivers/weiyun" _ "github.com/alist-org/alist/v3/drivers/weiyun"
_ "github.com/alist-org/alist/v3/drivers/wopan" _ "github.com/alist-org/alist/v3/drivers/wopan"

View File

@ -5,11 +5,9 @@ import (
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt"
"io" "io"
"math" "math"
"net/url" "net/url"
"os"
stdpath "path" stdpath "path"
"strconv" "strconv"
"time" "time"
@ -29,10 +27,9 @@ type BaiduNetdisk struct {
Addition Addition
uploadThread int uploadThread int
vipType int // 会员类型0普通用户(4G/4M)、1普通会员(10G/16M)、2超级会员(20G/32M)
} }
const DefaultSliceSize int64 = 4 * 1024 * 1024
func (d *BaiduNetdisk) Config() driver.Config { func (d *BaiduNetdisk) Config() driver.Config {
return config return config
} }
@ -55,7 +52,11 @@ func (d *BaiduNetdisk) Init(ctx context.Context) error {
"method": "uinfo", "method": "uinfo",
}, nil) }, nil)
log.Debugf("[baidu] get uinfo: %s", string(res)) log.Debugf("[baidu] get uinfo: %s", string(res))
return err if err != nil {
return err
}
d.vipType = utils.Json.Get(res, "vip_type").ToInt()
return nil
} }
func (d *BaiduNetdisk) Drop(ctx context.Context) error { func (d *BaiduNetdisk) Drop(ctx context.Context) error {
@ -81,7 +82,7 @@ func (d *BaiduNetdisk) Link(ctx context.Context, file model.Obj, args model.Link
func (d *BaiduNetdisk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { func (d *BaiduNetdisk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
var newDir File var newDir File
_, err := d.create(stdpath.Join(parentDir.GetPath(), dirName), 0, 1, "", "", &newDir) _, err := d.create(stdpath.Join(parentDir.GetPath(), dirName), 0, 1, "", "", &newDir, 0, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -147,28 +148,57 @@ func (d *BaiduNetdisk) Remove(ctx context.Context, obj model.Obj) error {
return err return err
} }
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { func (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) (model.Obj, error) {
tempFile, err := utils.CreateTempFile(stream.GetReadCloser(), stream.GetSize()) contentMd5 := stream.GetHash().GetHash(utils.MD5)
if len(contentMd5) < utils.MD5.Width {
return nil, errors.New("invalid hash")
}
streamSize := stream.GetSize()
path := stdpath.Join(dstDir.GetPath(), stream.GetName())
mtime := stream.ModTime().Unix()
ctime := stream.CreateTime().Unix()
blockList, _ := utils.Json.MarshalToString([]string{contentMd5})
var newFile File
_, err := d.create(path, streamSize, 0, "", blockList, &newFile, mtime, ctime)
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 {
return newObj, nil
}
tempFile, err := stream.CacheFullInTempFile()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
streamSize := stream.GetSize() streamSize := stream.GetSize()
count := int(math.Max(math.Ceil(float64(streamSize)/float64(DefaultSliceSize)), 1)) sliceSize := d.getSliceSize()
lastBlockSize := streamSize % DefaultSliceSize count := int(math.Max(math.Ceil(float64(streamSize)/float64(sliceSize)), 1))
lastBlockSize := streamSize % sliceSize
if streamSize > 0 && lastBlockSize == 0 { if streamSize > 0 && lastBlockSize == 0 {
lastBlockSize = DefaultSliceSize lastBlockSize = sliceSize
} }
//cal md5 for first 256k data //cal md5 for first 256k data
const SliceSize int64 = 256 * 1024 const SliceSize int64 = 256 * 1024
// cal md5 // cal md5
blockList := make([]string, 0, count) blockList := make([]string, 0, count)
byteSize := DefaultSliceSize byteSize := sliceSize
fileMd5H := md5.New() fileMd5H := md5.New()
sliceMd5H := md5.New() sliceMd5H := md5.New()
sliceMd5H2 := md5.New() sliceMd5H2 := md5.New()
@ -181,7 +211,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
if i == count { if i == count {
byteSize = lastBlockSize 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 { if err != nil && err != io.EOF {
return nil, err return nil, err
} }
@ -191,32 +221,40 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
contentMd5 := hex.EncodeToString(fileMd5H.Sum(nil)) contentMd5 := hex.EncodeToString(fileMd5H.Sum(nil))
sliceMd5 := hex.EncodeToString(sliceMd5H2.Sum(nil)) sliceMd5 := hex.EncodeToString(sliceMd5H2.Sum(nil))
blockListStr, _ := utils.Json.MarshalToString(blockList) blockListStr, _ := utils.Json.MarshalToString(blockList)
path := stdpath.Join(dstDir.GetPath(), stream.GetName())
rawPath := stdpath.Join(dstDir.GetPath(), stream.GetName()) mtime := stream.ModTime().Unix()
path := encodeURIComponent(rawPath) ctime := stream.CreateTime().Unix()
// step.1 预上传 // step.1 预上传
// 尝试获取之前的进度 // 尝试获取之前的进度
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5) precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5)
if !ok { if !ok {
data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&rtype=3&block_list=%s&content-md5=%s&slice-md5=%s",
path, streamSize,
blockListStr,
contentMd5, sliceMd5)
params := map[string]string{ params := map[string]string{
"method": "precreate", "method": "precreate",
} }
log.Debugf("[baidu_netdisk] precreate data: %s", data) form := map[string]string{
_, err = d.post("/xpan/file", params, data, &precreateResp) "path": path,
"size": strconv.FormatInt(streamSize, 10),
"isdir": "0",
"autoinit": "1",
"rtype": "3",
"block_list": blockListStr,
"content-md5": contentMd5,
"slice-md5": sliceMd5,
}
joinTime(form, ctime, mtime)
log.Debugf("[baidu_netdisk] precreate data: %s", form)
_, err = d.postForm("/xpan/file", params, form, &precreateResp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debugf("%+v", precreateResp) log.Debugf("%+v", precreateResp)
if precreateResp.ReturnType == 2 { if precreateResp.ReturnType == 2 {
//rapid upload, since got md5 match from baidu server //rapid upload, since got md5 match from baidu server
if err != nil { // 修复时间,具体原因见 Put 方法注释的 **注意**
return nil, err precreateResp.File.Ctime = ctime
} precreateResp.File.Mtime = mtime
return fileToObj(precreateResp.File), nil return fileToObj(precreateResp.File), nil
} }
} }
@ -230,7 +268,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
break break
} }
i, partseq, offset, byteSize := i, partseq, int64(partseq)*DefaultSliceSize, DefaultSliceSize i, partseq, offset, byteSize := i, partseq, int64(partseq)*sliceSize, sliceSize
if partseq+1 == count { if partseq+1 == count {
byteSize = lastBlockSize byteSize = lastBlockSize
} }
@ -247,7 +285,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
if err != nil { if err != nil {
return err return err
} }
up(int(threadG.Success()) * 100 / len(precreateResp.BlockList)) up(float64(threadG.Success()) * 100 / float64(len(precreateResp.BlockList)))
precreateResp.BlockList[i] = -1 precreateResp.BlockList[i] = -1
return nil return nil
}) })
@ -263,12 +301,16 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
// step.3 创建文件 // step.3 创建文件
var newFile File var newFile File
_, err = d.create(rawPath, streamSize, 0, precreateResp.Uploadid, blockListStr, &newFile) _, err = d.create(path, streamSize, 0, precreateResp.Uploadid, blockListStr, &newFile, mtime, ctime)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 修复时间,具体原因见 Put 方法注释的 **注意**
newFile.Ctime = ctime
newFile.Mtime = mtime
return fileToObj(newFile), nil return fileToObj(newFile), nil
} }
func (d *BaiduNetdisk) uploadSlice(ctx context.Context, params map[string]string, fileName string, file io.Reader) error { func (d *BaiduNetdisk) uploadSlice(ctx context.Context, params map[string]string, fileName string, file io.Reader) error {
res, err := base.RestyClient.R(). res, err := base.RestyClient.R().
SetContext(ctx). SetContext(ctx).

View File

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

View File

@ -40,11 +40,11 @@ type File struct {
Isdir int `json:"isdir"` Isdir int `json:"isdir"`
// list resp // list resp
//ServerCtime int64 `json:"server_ctime"` ServerCtime int64 `json:"server_ctime"`
ServerMtime int64 `json:"server_mtime"` ServerMtime int64 `json:"server_mtime"`
//ServerAtime int64 `json:"server_atime"` LocalMtime int64 `json:"local_mtime"`
//LocalCtime int64 `json:"local_ctime"` LocalCtime int64 `json:"local_ctime"`
//LocalMtime int64 `json:"local_mtime"` //ServerAtime int64 `json:"server_atime"` `
// only create and precreate resp // only create and precreate resp
Ctime int64 `json:"ctime"` Ctime int64 `json:"ctime"`
@ -55,8 +55,11 @@ func fileToObj(f File) *model.ObjThumb {
if f.ServerFilename == "" { if f.ServerFilename == "" {
f.ServerFilename = path.Base(f.Path) f.ServerFilename = path.Base(f.Path)
} }
if f.ServerMtime == 0 { if f.LocalCtime == 0 {
f.ServerMtime = int64(f.Mtime) f.LocalCtime = f.Ctime
}
if f.LocalMtime == 0 {
f.LocalMtime = f.Mtime
} }
return &model.ObjThumb{ return &model.ObjThumb{
Object: model.Object{ Object: model.Object{
@ -64,8 +67,12 @@ func fileToObj(f File) *model.ObjThumb {
Path: f.Path, Path: f.Path,
Name: f.ServerFilename, Name: f.ServerFilename,
Size: f.Size, Size: f.Size,
Modified: time.Unix(f.ServerMtime, 0), Modified: time.Unix(f.LocalMtime, 0),
Ctime: time.Unix(f.LocalCtime, 0),
IsFolder: f.Isdir == 1, IsFolder: f.Isdir == 1,
// 直接获取的MD5是错误的
// HashInfo: utils.NewHashInfo(utils.MD5, f.Md5),
}, },
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbs.Url3}, Thumbnail: model.Thumbnail{Thumbnail: f.Thumbs.Url3},
} }

View File

@ -1,11 +1,10 @@
package baidu_netdisk package baidu_netdisk
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
@ -22,7 +21,7 @@ import (
func (d *BaiduNetdisk) refreshToken() error { func (d *BaiduNetdisk) refreshToken() error {
err := d._refreshToken() err := d._refreshToken()
if err != nil && err == errs.EmptyToken { if err != nil && errors.Is(err, errs.EmptyToken) {
err = d._refreshToken() err = d._refreshToken()
} }
return err return err
@ -74,21 +73,16 @@ func (d *BaiduNetdisk) request(furl string, method string, callback base.ReqCall
log.Info("refreshing baidu_netdisk token.") log.Info("refreshing baidu_netdisk token.")
err2 := d.refreshToken() err2 := d.refreshToken()
if err2 != nil { if err2 != nil {
return err2 return retry.Unrecoverable(err2)
} }
} }
return fmt.Errorf("req: [%s] ,errno: %d, refer to https://pan.baidu.com/union/doc/", furl, errno)
err2 := fmt.Errorf("req: [%s] ,errno: %d, refer to https://pan.baidu.com/union/doc/", furl, errno)
if !utils.SliceContains([]int{2}, errno) {
err2 = retry.Unrecoverable(err2)
}
return err2
} }
result = res.Body() result = res.Body()
return nil return nil
}, },
retry.LastErrorOnly(true), retry.LastErrorOnly(true),
retry.Attempts(5), retry.Attempts(3),
retry.Delay(time.Second), retry.Delay(time.Second),
retry.DelayType(retry.BackOffDelay)) retry.DelayType(retry.BackOffDelay))
return result, err return result, err
@ -100,10 +94,10 @@ func (d *BaiduNetdisk) get(pathname string, params map[string]string, resp inter
}, resp) }, resp)
} }
func (d *BaiduNetdisk) post(pathname string, params map[string]string, data interface{}, resp interface{}) ([]byte, error) { func (d *BaiduNetdisk) postForm(pathname string, params map[string]string, form map[string]string, resp interface{}) ([]byte, error) {
return d.request("https://pan.baidu.com/rest/2.0"+pathname, http.MethodPost, func(req *resty.Request) { return d.request("https://pan.baidu.com/rest/2.0"+pathname, http.MethodPost, func(req *resty.Request) {
req.SetQueryParams(params) req.SetQueryParams(params)
req.SetBody(data) req.SetFormData(form)
}, resp) }, resp)
} }
@ -158,6 +152,9 @@ func (d *BaiduNetdisk) linkOfficial(file model.Obj, args model.LinkArgs) (*model
//if res.StatusCode() == 302 { //if res.StatusCode() == 302 {
u = res.Header().Get("location") u = res.Header().Get("location")
//} //}
updateObjMd5(file, "pan.baidu.com", u)
return &model.Link{ return &model.Link{
URL: u, URL: u,
Header: http.Header{ Header: http.Header{
@ -180,6 +177,9 @@ func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Li
if err != nil { if err != nil {
return nil, err return nil, err
} }
updateObjMd5(file, d.CustomCrackUA, resp.Info[0].Dlink)
return &model.Link{ return &model.Link{
URL: resp.Info[0].Dlink, URL: resp.Info[0].Dlink,
Header: http.Header{ Header: http.Header{
@ -194,23 +194,76 @@ func (d *BaiduNetdisk) manage(opera string, filelist any) ([]byte, error) {
"opera": opera, "opera": opera,
} }
marshal, _ := utils.Json.MarshalToString(filelist) marshal, _ := utils.Json.MarshalToString(filelist)
data := fmt.Sprintf("async=0&filelist=%s&ondup=fail", marshal) return d.postForm("/xpan/file", params, map[string]string{
return d.post("/xpan/file", params, data, nil) "async": "0",
"filelist": marshal,
"ondup": "fail",
}, nil)
} }
func (d *BaiduNetdisk) create(path string, size int64, isdir int, uploadid, block_list string, resp any) ([]byte, error) { func (d *BaiduNetdisk) create(path string, size int64, isdir int, uploadid, block_list string, resp any, mtime, ctime int64) ([]byte, error) {
params := map[string]string{ params := map[string]string{
"method": "create", "method": "create",
} }
data := fmt.Sprintf("path=%s&size=%d&isdir=%d&rtype=3", encodeURIComponent(path), size, isdir) form := map[string]string{
if uploadid != "" { "path": path,
data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list) "size": strconv.FormatInt(size, 10),
"isdir": strconv.Itoa(isdir),
"rtype": "3",
} }
return d.post("/xpan/file", params, data, resp) if mtime != 0 && ctime != 0 {
joinTime(form, ctime, mtime)
}
if uploadid != "" {
form["uploadid"] = uploadid
}
if block_list != "" {
form["block_list"] = block_list
}
return d.postForm("/xpan/file", params, form, resp)
} }
func encodeURIComponent(str string) string { func joinTime(form map[string]string, ctime, mtime int64) {
r := url.QueryEscape(str) form["local_mtime"] = strconv.FormatInt(mtime, 10)
r = strings.ReplaceAll(r, "+", "%20") form["local_ctime"] = strconv.FormatInt(ctime, 10)
return r
} }
func updateObjMd5(obj model.Obj, userAgent, u string) {
object := model.GetRawObject(obj)
if object != nil {
req, _ := http.NewRequest(http.MethodHead, u, nil)
req.Header.Add("User-Agent", userAgent)
resp, _ := base.HttpClient.Do(req)
if resp != nil {
contentMd5 := resp.Header.Get("Content-Md5")
object.HashInfo = utils.NewHashInfo(utils.MD5, contentMd5)
}
}
}
const (
DefaultSliceSize int64 = 4 * utils.MB
VipSliceSize = 16 * utils.MB
SVipSliceSize = 32 * utils.MB
)
func (d *BaiduNetdisk) getSliceSize() int64 {
if d.CustomUploadPartSize != 0 {
return d.CustomUploadPartSize
}
switch d.vipType {
case 1:
return VipSliceSize
case 2:
return SVipSliceSize
default:
return DefaultSliceSize
}
}
// func encodeURIComponent(str string) string {
// r := url.QueryEscape(str)
// r = strings.ReplaceAll(r, "+", "%20")
// return r
// }

View File

@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"io" "io"
"math" "math"
"os"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -228,15 +227,14 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
return nil, fmt.Errorf("file size cannot be zero") return nil, fmt.Errorf("file size cannot be zero")
} }
// TODO:
// 暂时没有找到妙传方式
// 需要获取完整文件md5,必须支持 io.Seek // 需要获取完整文件md5,必须支持 io.Seek
tempFile, err := utils.CreateTempFile(stream.GetReadCloser(), stream.GetSize()) tempFile, err := stream.CacheFullInTempFile()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
const DEFAULT int64 = 1 << 22 const DEFAULT int64 = 1 << 22
const SliceSize int64 = 1 << 18 const SliceSize int64 = 1 << 18
@ -263,7 +261,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
if i == count { if i == count {
byteSize = lastBlockSize 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 { if err != nil && err != io.EOF {
return nil, err return nil, err
} }
@ -331,7 +329,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
if err != nil { if err != nil {
return err return err
} }
up(int(threadG.Success()) * 100 / len(precreateResp.BlockList)) up(float64(threadG.Success()) * 100 / float64(len(precreateResp.BlockList)))
precreateResp.BlockList[i] = -1 precreateResp.BlockList[i] = -1
return nil return nil
}) })

View File

@ -61,12 +61,12 @@ func moveFileToAlbumFile(file *File, album *Album, uk int64) *AlbumFile {
func renameAlbum(album *Album, newName string) *Album { func renameAlbum(album *Album, newName string) *Album {
return &Album{ return &Album{
AlbumID: album.AlbumID, AlbumID: album.AlbumID,
Tid: album.Tid, Tid: album.Tid,
JoinTime: album.JoinTime, JoinTime: album.JoinTime,
CreateTime: album.CreateTime, CreationTime: album.CreationTime,
Title: newName, Title: newName,
Mtime: time.Now().Unix(), Mtime: time.Now().Unix(),
} }
} }

View File

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
) )
@ -51,22 +53,17 @@ type (
Ctime int64 `json:"ctime"` // 创建时间 s Ctime int64 `json:"ctime"` // 创建时间 s
Mtime int64 `json:"mtime"` // 修改时间 s Mtime int64 `json:"mtime"` // 修改时间 s
Thumburl []string `json:"thumburl"` Thumburl []string `json:"thumburl"`
Md5 string `json:"md5"`
parseTime *time.Time
} }
) )
func (c *File) GetSize() int64 { return c.Size } func (c *File) GetSize() int64 { return c.Size }
func (c *File) GetName() string { return getFileName(c.Path) } func (c *File) GetName() string { return getFileName(c.Path) }
func (c *File) ModTime() time.Time { func (c *File) CreateTime() time.Time { return time.Unix(c.Ctime, 0) }
if c.parseTime == nil { func (c *File) ModTime() time.Time { return time.Unix(c.Mtime, 0) }
c.parseTime = toTime(c.Mtime) func (c *File) IsDir() bool { return false }
} func (c *File) GetID() string { return "" }
return *c.parseTime func (c *File) GetPath() string { return "" }
}
func (c *File) IsDir() bool { return false }
func (c *File) GetID() string { return "" }
func (c *File) GetPath() string { return "" }
func (c *File) Thumb() string { func (c *File) Thumb() string {
if len(c.Thumburl) > 0 { if len(c.Thumburl) > 0 {
return c.Thumburl[0] return c.Thumburl[0]
@ -74,6 +71,10 @@ func (c *File) Thumb() string {
return "" return ""
} }
func (c *File) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.MD5, c.Md5)
}
/*相册部分*/ /*相册部分*/
type ( type (
AlbumListResp struct { AlbumListResp struct {
@ -84,12 +85,12 @@ type (
} }
Album struct { Album struct {
AlbumID string `json:"album_id"` AlbumID string `json:"album_id"`
Tid int64 `json:"tid"` Tid int64 `json:"tid"`
Title string `json:"title"` Title string `json:"title"`
JoinTime int64 `json:"join_time"` JoinTime int64 `json:"join_time"`
CreateTime int64 `json:"create_time"` CreationTime int64 `json:"create_time"`
Mtime int64 `json:"mtime"` Mtime int64 `json:"mtime"`
parseTime *time.Time parseTime *time.Time
} }
@ -109,17 +110,17 @@ type (
} }
) )
func (a *Album) GetSize() int64 { return 0 } func (a *Album) GetHash() utils.HashInfo {
func (a *Album) GetName() string { return a.Title } return utils.HashInfo{}
func (a *Album) ModTime() time.Time {
if a.parseTime == nil {
a.parseTime = toTime(a.Mtime)
}
return *a.parseTime
} }
func (a *Album) IsDir() bool { return true }
func (a *Album) GetID() string { return "" } func (a *Album) GetSize() int64 { return 0 }
func (a *Album) GetPath() string { return "" } func (a *Album) GetName() string { return a.Title }
func (a *Album) CreateTime() time.Time { return time.Unix(a.CreationTime, 0) }
func (a *Album) ModTime() time.Time { return time.Unix(a.Mtime, 0) }
func (a *Album) IsDir() bool { return true }
func (a *Album) GetID() string { return "" }
func (a *Album) GetPath() string { return "" }
type ( type (
CopyFileResp struct { CopyFileResp struct {

View File

@ -33,6 +33,7 @@ func NewRestyClient() *resty.Client {
client := resty.New(). client := resty.New().
SetHeader("user-agent", UserAgent). SetHeader("user-agent", UserAgent).
SetRetryCount(3). SetRetryCount(3).
SetRetryResetReaders(true).
SetTimeout(DefaultTimeout). SetTimeout(DefaultTimeout).
SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify}) SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
return client return client

297
drivers/chaoxing/driver.go Normal file
View File

@ -0,0 +1,297 @@
package chaoxing
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"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/internal/op"
"github.com/alist-org/alist/v3/pkg/cron"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
"google.golang.org/appengine/log"
)
type ChaoXing struct {
model.Storage
Addition
cron *cron.Cron
config driver.Config
conf Conf
}
func (d *ChaoXing) Config() driver.Config {
return d.config
}
func (d *ChaoXing) GetAddition() driver.Additional {
return &d.Addition
}
func (d *ChaoXing) refreshCookie() error {
cookie, err := d.Login()
if err != nil {
d.Status = err.Error()
op.MustSaveDriverStorage(d)
return nil
}
d.Addition.Cookie = cookie
op.MustSaveDriverStorage(d)
return nil
}
func (d *ChaoXing) Init(ctx context.Context) error {
err := d.refreshCookie()
if err != nil {
log.Errorf(ctx, err.Error())
}
d.cron = cron.NewCron(time.Hour * 12)
d.cron.Do(func() {
err = d.refreshCookie()
if err != nil {
log.Errorf(ctx, err.Error())
}
})
return nil
}
func (d *ChaoXing) Drop(ctx context.Context) error {
d.cron.Stop()
return nil
}
func (d *ChaoXing) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.GetFiles(dir.GetID())
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
return fileToObj(src), nil
})
}
func (d *ChaoXing) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var resp DownResp
ua := d.conf.ua
fileId := strings.Split(file.GetID(), "$")[1]
_, err := d.requestDownload("/screen/note_note/files/status/"+fileId, http.MethodPost, func(req *resty.Request) {
req.SetHeader("User-Agent", ua)
}, &resp)
if err != nil {
return nil, err
}
u := resp.Download
return &model.Link{
URL: u,
Header: http.Header{
"Cookie": []string{d.Cookie},
"Referer": []string{d.conf.referer},
"User-Agent": []string{ua},
},
Concurrency: 2,
PartSize: 10 * utils.MB,
}, nil
}
func (d *ChaoXing) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"name": dirName,
"pid": parentDir.GetID(),
}
var resp ListFileResp
_, err := d.request("/pc/resource/addResourceFolder", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return err
}
if resp.Result != 1 {
msg := fmt.Sprintf("error:%s", resp.Msg)
return errors.New(msg)
}
return nil
}
func (d *ChaoXing) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"folderIds": srcObj.GetID(),
"targetId": dstDir.GetID(),
}
if !srcObj.IsDir() {
query = map[string]string{
"bbsid": d.Addition.Bbsid,
"recIds": strings.Split(srcObj.GetID(), "$")[0],
"targetId": dstDir.GetID(),
}
}
var resp ListFileResp
_, err := d.request("/pc/resource/moveResource", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return err
}
if !resp.Status {
msg := fmt.Sprintf("error:%s", resp.Msg)
return errors.New(msg)
}
return nil
}
func (d *ChaoXing) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"folderId": srcObj.GetID(),
"name": newName,
}
path := "/pc/resource/updateResourceFolderName"
if !srcObj.IsDir() {
// path = "/pc/resource/updateResourceFileName"
// query = map[string]string{
// "bbsid": d.Addition.Bbsid,
// "recIds": strings.Split(srcObj.GetID(), "$")[0],
// "name": newName,
// }
return errors.New("此网盘不支持修改文件名")
}
var resp ListFileResp
_, err := d.request(path, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return err
}
if resp.Result != 1 {
msg := fmt.Sprintf("error:%s", resp.Msg)
return errors.New(msg)
}
return nil
}
func (d *ChaoXing) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
// TODO copy obj, optional
return errs.NotImplement
}
func (d *ChaoXing) Remove(ctx context.Context, obj model.Obj) error {
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"folderIds": obj.GetID(),
}
path := "/pc/resource/deleteResourceFolder"
var resp ListFileResp
if !obj.IsDir() {
path = "/pc/resource/deleteResourceFile"
query = map[string]string{
"bbsid": d.Addition.Bbsid,
"recIds": strings.Split(obj.GetID(), "$")[0],
}
}
_, err := d.request(path, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return err
}
if resp.Result != 1 {
msg := fmt.Sprintf("error:%s", resp.Msg)
return errors.New(msg)
}
return nil
}
func (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
var resp UploadDataRsp
_, err := d.request("https://noteyd.chaoxing.com/pc/files/getUploadConfig", http.MethodGet, func(req *resty.Request) {
}, &resp)
if err != nil {
return err
}
if resp.Result != 1 {
return errors.New("get upload data error")
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
filePart, err := writer.CreateFormFile("file", stream.GetName())
if err != nil {
return err
}
_, err = utils.CopyWithBuffer(filePart, stream)
if err != nil {
return err
}
err = writer.WriteField("_token", resp.Msg.Token)
if err != nil {
return err
}
err = writer.WriteField("puid", fmt.Sprintf("%d", resp.Msg.Puid))
if err != nil {
fmt.Println("Error writing param2 to request body:", err)
return err
}
err = writer.Close()
if err != nil {
return err
}
req, err := http.NewRequest("POST", "https://pan-yz.chaoxing.com/upload", body)
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Content-Length", fmt.Sprintf("%d", body.Len()))
resps, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resps.Body.Close()
bodys, err := io.ReadAll(resps.Body)
if err != nil {
return err
}
var fileRsp UploadFileDataRsp
err = json.Unmarshal(bodys, &fileRsp)
if err != nil {
return err
}
if fileRsp.Msg != "success" {
return errors.New(fileRsp.Msg)
}
uploadDoneParam := UploadDoneParam{Key: fileRsp.ObjectID, Cataid: "100000019", Param: fileRsp.Data}
params, err := json.Marshal(uploadDoneParam)
if err != nil {
return err
}
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"pid": dstDir.GetID(),
"type": "yunpan",
"params": url.QueryEscape("[" + string(params) + "]"),
}
var respd ListFileResp
_, err = d.request("/pc/resource/addResource", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &respd)
if err != nil {
return err
}
if respd.Result != 1 {
msg := fmt.Sprintf("error:%v", resp.Msg)
return errors.New(msg)
}
return nil
}
var _ driver.Driver = (*ChaoXing)(nil)

47
drivers/chaoxing/meta.go Normal file
View File

@ -0,0 +1,47 @@
package chaoxing
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
// 此程序挂载的是超星小组网盘,需要代理才能使用;
// 登录超星后进入个人空间,进入小组,新建小组,点击进去。
// url中就有bbsid的参数系统限制单文件大小2G没有总容量限制
type Addition struct {
// 超星用户名及密码
UserName string `json:"user_name" required:"true"`
Password string `json:"password" required:"true"`
// 从自己新建的小组url里获取
Bbsid string `json:"bbsid" required:"true"`
driver.RootID
// 可不填,程序会自动登录获取
Cookie string `json:"cookie"`
}
type Conf struct {
ua string
referer string
api string
DowloadApi string
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &ChaoXing{
config: driver.Config{
Name: "ChaoXingGroupDrive",
OnlyProxy: true,
OnlyLocal: false,
DefaultRoot: "-1",
NoOverwriteUpload: true,
},
conf: Conf{
ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch",
referer: "https://chaoxing.com/",
api: "https://groupweb.chaoxing.com",
DowloadApi: "https://noteyd.chaoxing.com",
},
}
})
}

276
drivers/chaoxing/types.go Normal file
View File

@ -0,0 +1,276 @@
package chaoxing
import (
"bytes"
"fmt"
"strconv"
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type Resp struct {
Result int `json:"result"`
}
type UserAuth struct {
GroupAuth struct {
AddData int `json:"addData"`
AddDataFolder int `json:"addDataFolder"`
AddLebel int `json:"addLebel"`
AddManager int `json:"addManager"`
AddMem int `json:"addMem"`
AddTopicFolder int `json:"addTopicFolder"`
AnonymousAddReply int `json:"anonymousAddReply"`
AnonymousAddTopic int `json:"anonymousAddTopic"`
BatchOperation int `json:"batchOperation"`
DelData int `json:"delData"`
DelDataFolder int `json:"delDataFolder"`
DelMem int `json:"delMem"`
DelTopicFolder int `json:"delTopicFolder"`
Dismiss int `json:"dismiss"`
ExamEnc string `json:"examEnc"`
GroupChat int `json:"groupChat"`
IsShowCircleChatButton int `json:"isShowCircleChatButton"`
IsShowCircleCloudButton int `json:"isShowCircleCloudButton"`
IsShowCompanyButton int `json:"isShowCompanyButton"`
Join int `json:"join"`
MemberShowRankSet int `json:"memberShowRankSet"`
ModifyDataFolder int `json:"modifyDataFolder"`
ModifyExpose int `json:"modifyExpose"`
ModifyName int `json:"modifyName"`
ModifyShowPic int `json:"modifyShowPic"`
ModifyTopicFolder int `json:"modifyTopicFolder"`
ModifyVisibleState int `json:"modifyVisibleState"`
OnlyMgrScoreSet int `json:"onlyMgrScoreSet"`
Quit int `json:"quit"`
SendNotice int `json:"sendNotice"`
ShowActivityManage int `json:"showActivityManage"`
ShowActivitySet int `json:"showActivitySet"`
ShowAttentionSet int `json:"showAttentionSet"`
ShowAutoClearStatus int `json:"showAutoClearStatus"`
ShowBarcode int `json:"showBarcode"`
ShowChatRoomSet int `json:"showChatRoomSet"`
ShowCircleActivitySet int `json:"showCircleActivitySet"`
ShowCircleSet int `json:"showCircleSet"`
ShowCmem int `json:"showCmem"`
ShowDataFolder int `json:"showDataFolder"`
ShowDelReason int `json:"showDelReason"`
ShowForward int `json:"showForward"`
ShowGroupChat int `json:"showGroupChat"`
ShowGroupChatSet int `json:"showGroupChatSet"`
ShowGroupSquareSet int `json:"showGroupSquareSet"`
ShowLockAddSet int `json:"showLockAddSet"`
ShowManager int `json:"showManager"`
ShowManagerIdentitySet int `json:"showManagerIdentitySet"`
ShowNeedDelReasonSet int `json:"showNeedDelReasonSet"`
ShowNotice int `json:"showNotice"`
ShowOnlyManagerReplySet int `json:"showOnlyManagerReplySet"`
ShowRank int `json:"showRank"`
ShowRank2 int `json:"showRank2"`
ShowRecycleBin int `json:"showRecycleBin"`
ShowReplyByClass int `json:"showReplyByClass"`
ShowReplyNeedCheck int `json:"showReplyNeedCheck"`
ShowSignbanSet int `json:"showSignbanSet"`
ShowSpeechSet int `json:"showSpeechSet"`
ShowTopicCheck int `json:"showTopicCheck"`
ShowTopicNeedCheck int `json:"showTopicNeedCheck"`
ShowTransferSet int `json:"showTransferSet"`
} `json:"groupAuth"`
OperationAuth struct {
Add int `json:"add"`
AddTopicToFolder int `json:"addTopicToFolder"`
ChoiceSet int `json:"choiceSet"`
DelTopicFromFolder int `json:"delTopicFromFolder"`
Delete int `json:"delete"`
Reply int `json:"reply"`
ScoreSet int `json:"scoreSet"`
TopSet int `json:"topSet"`
Update int `json:"update"`
} `json:"operationAuth"`
}
// 手机端学习通上传的文件的json内容(content字段)与网页端上传的有所不同
// 网页端json `"puid": 54321, "size": 12345`
// 手机端json `"puid": "54321". "size": "12345"`
type int_str int
// json 字符串数字和纯数字解析
func (ios *int_str) UnmarshalJSON(data []byte) error {
intValue, err := strconv.Atoi(string(bytes.Trim(data, "\"")))
if err != nil {
return err
}
*ios = int_str(intValue)
return nil
}
type File struct {
Cataid int `json:"cataid"`
Cfid int `json:"cfid"`
Content struct {
Cfid int `json:"cfid"`
Pid int `json:"pid"`
FolderName string `json:"folderName"`
ShareType int `json:"shareType"`
Preview string `json:"preview"`
Filetype string `json:"filetype"`
PreviewURL string `json:"previewUrl"`
IsImg bool `json:"isImg"`
ParentPath string `json:"parentPath"`
Icon string `json:"icon"`
Suffix string `json:"suffix"`
Duration int `json:"duration"`
Pantype string `json:"pantype"`
Puid int_str `json:"puid"`
Filepath string `json:"filepath"`
Crc string `json:"crc"`
Isfile bool `json:"isfile"`
Residstr string `json:"residstr"`
ObjectID string `json:"objectId"`
Extinfo string `json:"extinfo"`
Thumbnail string `json:"thumbnail"`
Creator int `json:"creator"`
ResTypeValue int `json:"resTypeValue"`
UploadDateFormat string `json:"uploadDateFormat"`
DisableOpt bool `json:"disableOpt"`
DownPath string `json:"downPath"`
Sort int `json:"sort"`
Topsort int `json:"topsort"`
Restype string `json:"restype"`
Size int_str `json:"size"`
UploadDate int64 `json:"uploadDate"`
FileSize string `json:"fileSize"`
Name string `json:"name"`
FileID string `json:"fileId"`
} `json:"content"`
CreatorID int `json:"creatorId"`
DesID string `json:"des_id"`
ID int `json:"id"`
Inserttime int64 `json:"inserttime"`
Key string `json:"key"`
Norder int `json:"norder"`
OwnerID int `json:"ownerId"`
OwnerType int `json:"ownerType"`
Path string `json:"path"`
Rid int `json:"rid"`
Status int `json:"status"`
Topsign int `json:"topsign"`
}
type ListFileResp struct {
Msg string `json:"msg"`
Result int `json:"result"`
Status bool `json:"status"`
UserAuth UserAuth `json:"userAuth"`
List []File `json:"list"`
}
type DownResp struct {
Msg string `json:"msg"`
Duration int `json:"duration"`
Download string `json:"download"`
FileStatus string `json:"fileStatus"`
URL string `json:"url"`
Status bool `json:"status"`
}
type UploadDataRsp struct {
Result int `json:"result"`
Msg struct {
Puid int `json:"puid"`
Token string `json:"token"`
} `json:"msg"`
}
type UploadFileDataRsp struct {
Result bool `json:"result"`
Msg string `json:"msg"`
Crc string `json:"crc"`
ObjectID string `json:"objectId"`
Resid int64 `json:"resid"`
Puid int `json:"puid"`
Data struct {
DisableOpt bool `json:"disableOpt"`
Resid int64 `json:"resid"`
Crc string `json:"crc"`
Puid int `json:"puid"`
Isfile bool `json:"isfile"`
Pantype string `json:"pantype"`
Size int `json:"size"`
Name string `json:"name"`
ObjectID string `json:"objectId"`
Restype string `json:"restype"`
UploadDate time.Time `json:"uploadDate"`
ModifyDate time.Time `json:"modifyDate"`
UploadDateFormat string `json:"uploadDateFormat"`
Residstr string `json:"residstr"`
Suffix string `json:"suffix"`
Preview string `json:"preview"`
Thumbnail string `json:"thumbnail"`
Creator int `json:"creator"`
Duration int `json:"duration"`
IsImg bool `json:"isImg"`
PreviewURL string `json:"previewUrl"`
Filetype string `json:"filetype"`
Filepath string `json:"filepath"`
Sort int `json:"sort"`
Topsort int `json:"topsort"`
ResTypeValue int `json:"resTypeValue"`
Extinfo string `json:"extinfo"`
} `json:"data"`
}
type UploadDoneParam struct {
Cataid string `json:"cataid"`
Key string `json:"key"`
Param struct {
DisableOpt bool `json:"disableOpt"`
Resid int64 `json:"resid"`
Crc string `json:"crc"`
Puid int `json:"puid"`
Isfile bool `json:"isfile"`
Pantype string `json:"pantype"`
Size int `json:"size"`
Name string `json:"name"`
ObjectID string `json:"objectId"`
Restype string `json:"restype"`
UploadDate time.Time `json:"uploadDate"`
ModifyDate time.Time `json:"modifyDate"`
UploadDateFormat string `json:"uploadDateFormat"`
Residstr string `json:"residstr"`
Suffix string `json:"suffix"`
Preview string `json:"preview"`
Thumbnail string `json:"thumbnail"`
Creator int `json:"creator"`
Duration int `json:"duration"`
IsImg bool `json:"isImg"`
PreviewURL string `json:"previewUrl"`
Filetype string `json:"filetype"`
Filepath string `json:"filepath"`
Sort int `json:"sort"`
Topsort int `json:"topsort"`
ResTypeValue int `json:"resTypeValue"`
Extinfo string `json:"extinfo"`
} `json:"param"`
}
func fileToObj(f File) *model.Object {
if len(f.Content.FolderName) > 0 {
return &model.Object{
ID: fmt.Sprintf("%d", f.ID),
Name: f.Content.FolderName,
Size: 0,
Modified: time.UnixMilli(f.Inserttime),
IsFolder: true,
}
}
paserTime := time.UnixMilli(f.Content.UploadDate)
return &model.Object{
ID: fmt.Sprintf("%d$%s", f.ID, f.Content.FileID),
Name: f.Content.Name,
Size: int64(f.Content.Size),
Modified: paserTime,
IsFolder: false,
}
}

183
drivers/chaoxing/util.go Normal file
View File

@ -0,0 +1,183 @@
package chaoxing
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"fmt"
"mime/multipart"
"net/http"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/go-resty/resty/v2"
)
func (d *ChaoXing) requestDownload(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
u := d.conf.DowloadApi + pathname
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"Cookie": d.Cookie,
"Accept": "application/json, text/plain, */*",
"Referer": d.conf.referer,
})
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
var e Resp
req.SetError(&e)
res, err := req.Execute(method, u)
if err != nil {
return nil, err
}
return res.Body(), nil
}
func (d *ChaoXing) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
u := d.conf.api + pathname
if strings.Contains(pathname, "getUploadConfig") {
u = pathname
}
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"Cookie": d.Cookie,
"Accept": "application/json, text/plain, */*",
"Referer": d.conf.referer,
})
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
var e Resp
req.SetError(&e)
res, err := req.Execute(method, u)
if err != nil {
return nil, err
}
return res.Body(), nil
}
func (d *ChaoXing) GetFiles(parent string) ([]File, error) {
files := make([]File, 0)
query := map[string]string{
"bbsid": d.Addition.Bbsid,
"folderId": parent,
"recType": "1",
}
var resp ListFileResp
_, err := d.request("/pc/resource/getResourceList", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return nil, err
}
if resp.Result != 1 {
msg := fmt.Sprintf("error code is:%d", resp.Result)
return nil, errors.New(msg)
}
if len(resp.List) > 0 {
files = append(files, resp.List...)
}
querys := map[string]string{
"bbsid": d.Addition.Bbsid,
"folderId": parent,
"recType": "2",
}
var resps ListFileResp
_, err = d.request("/pc/resource/getResourceList", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(querys)
}, &resps)
if err != nil {
return nil, err
}
for _, file := range resps.List {
// 手机端超星上传的文件没有fileID字段但ObjectID与fileID相同可代替
if file.Content.FileID == "" {
file.Content.FileID = file.Content.ObjectID
}
files = append(files, file)
}
return files, nil
}
func EncryptByAES(message, key string) (string, error) {
aesKey := []byte(key)
plainText := []byte(message)
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}
iv := aesKey[:aes.BlockSize]
mode := cipher.NewCBCEncrypter(block, iv)
padding := aes.BlockSize - len(plainText)%aes.BlockSize
paddedText := append(plainText, byte(padding))
for i := 0; i < padding-1; i++ {
paddedText = append(paddedText, byte(padding))
}
ciphertext := make([]byte, len(paddedText))
mode.CryptBlocks(ciphertext, paddedText)
encrypted := base64.StdEncoding.EncodeToString(ciphertext)
return encrypted, nil
}
func CookiesToString(cookies []*http.Cookie) string {
var cookieStr string
for _, cookie := range cookies {
cookieStr += cookie.Name + "=" + cookie.Value + "; "
}
if len(cookieStr) > 2 {
cookieStr = cookieStr[:len(cookieStr)-2]
}
return cookieStr
}
func (d *ChaoXing) Login() (string, error) {
transferKey := "u2oh6Vu^HWe4_AES"
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
uname, err := EncryptByAES(d.Addition.UserName, transferKey)
if err != nil {
return "", err
}
password, err := EncryptByAES(d.Addition.Password, transferKey)
if err != nil {
return "", err
}
err = writer.WriteField("uname", uname)
if err != nil {
return "", err
}
err = writer.WriteField("password", password)
if err != nil {
return "", err
}
err = writer.WriteField("t", "true")
if err != nil {
return "", err
}
err = writer.Close()
if err != nil {
return "", err
}
// Create the request
req, err := http.NewRequest("POST", "https://passport2.chaoxing.com/fanyalogin", body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Content-Length", fmt.Sprintf("%d", body.Len()))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
return CookiesToString(resp.Cookies()), nil
}

View File

@ -49,7 +49,19 @@ func (d *Cloudreve) List(ctx context.Context, dir model.Obj, args model.ListArgs
} }
return utils.SliceConvert(r.Objects, func(src Object) (model.Obj, error) { return utils.SliceConvert(r.Objects, func(src Object) (model.Obj, error) {
return objectToObj(src), nil thumb, err := d.GetThumb(src)
if err != nil {
return nil, err
}
if src.Type == "dir" && d.EnableThumbAndFolderSize {
var dprop DirectoryProp
err = d.request(http.MethodGet, "/object/property/"+src.Id+"?is_folder=true", nil, &dprop)
if err != nil {
return nil, err
}
src.Size = dprop.Size
}
return objectToObj(src, thumb), nil
}) })
} }
@ -59,6 +71,9 @@ func (d *Cloudreve) Link(ctx context.Context, file model.Obj, args model.LinkArg
if err != nil { if err != nil {
return nil, err return nil, err
} }
if strings.HasPrefix(dUrl, "/api") {
dUrl = d.Address + dUrl
}
return &model.Link{ return &model.Link{
URL: dUrl, URL: dUrl,
}, nil }, nil
@ -115,7 +130,7 @@ func (d *Cloudreve) Remove(ctx context.Context, obj model.Obj) error {
} }
func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
if stream.GetReadCloser() == http.NoBody { if io.ReadCloser(stream) == http.NoBody {
return d.create(ctx, dstDir, stream) return d.create(ctx, dstDir, stream)
} }
var r DirectoryResp var r DirectoryResp

View File

@ -9,11 +9,12 @@ type Addition struct {
// Usually one of two // Usually one of two
driver.RootPath driver.RootPath
// define other // define other
Address string `json:"address" required:"true"` Address string `json:"address" required:"true"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Cookie string `json:"cookie"` Cookie string `json:"cookie"`
CustomUA string `json:"custom_ua"` CustomUA string `json:"custom_ua"`
EnableThumbAndFolderSize bool `json:"enable_thumb_and_folder_size"`
} }
var config = driver.Config{ var config = driver.Config{

View File

@ -44,13 +44,20 @@ type Object struct {
SourceEnabled bool `json:"source_enabled"` SourceEnabled bool `json:"source_enabled"`
} }
func objectToObj(f Object) *model.Object { type DirectoryProp struct {
return &model.Object{ Size int `json:"size"`
ID: f.Id, }
Name: f.Name,
Size: int64(f.Size), func objectToObj(f Object, t model.Thumbnail) *model.ObjThumb {
Modified: f.Date, return &model.ObjThumb{
IsFolder: f.Type == "dir", Object: model.Object{
ID: f.Id,
Name: f.Name,
Size: int64(f.Size),
Modified: f.Date,
IsFolder: f.Type == "dir",
},
Thumbnail: t,
} }
} }

View File

@ -149,3 +149,26 @@ func convertSrc(obj model.Obj) map[string]interface{} {
m["items"] = items m["items"] = items
return m return m
} }
func (d *Cloudreve) GetThumb(file Object) (model.Thumbnail, error) {
if !d.Addition.EnableThumbAndFolderSize {
return model.Thumbnail{}, nil
}
ua := d.CustomUA
if ua == "" {
ua = base.UserAgent
}
req := base.NoRedirectClient.R()
req.SetHeaders(map[string]string{
"Cookie": "cloudreve-session=" + d.Cookie,
"Accept": "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
"User-Agent": ua,
})
resp, err := req.Execute(http.MethodGet, d.Address+"/api/v3/file/thumb/"+file.Id)
if err != nil {
return model.Thumbnail{}, err
}
return model.Thumbnail{
Thumbnail: resp.Header().Get("Location"),
}, nil
}

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http"
stdpath "path" stdpath "path"
"regexp" "regexp"
"strings" "strings"
@ -13,10 +12,11 @@ import (
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/fs" "github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/net"
"github.com/alist-org/alist/v3/internal/op" "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/http_range"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
rcCrypt "github.com/rclone/rclone/backend/crypt" rcCrypt "github.com/rclone/rclone/backend/crypt"
"github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/config/obscure" "github.com/rclone/rclone/fs/config/obscure"
@ -55,6 +55,8 @@ func (d *Crypt) Init(ctx context.Context) error {
if !isCryptExt(d.EncryptedSuffix) { if !isCryptExt(d.EncryptedSuffix) {
return fmt.Errorf("EncryptedSuffix is Illegal") return fmt.Errorf("EncryptedSuffix is Illegal")
} }
d.FileNameEncoding = utils.GetNoneEmpty(d.FileNameEncoding, "base64")
d.EncryptedSuffix = utils.GetNoneEmpty(d.EncryptedSuffix, ".bin")
op.MustSaveDriverStorage(d) op.MustSaveDriverStorage(d)
@ -72,7 +74,7 @@ func (d *Crypt) Init(ctx context.Context) error {
"password2": p2, "password2": p2,
"filename_encryption": d.FileNameEnc, "filename_encryption": d.FileNameEnc,
"directory_name_encryption": d.DirNameEnc, "directory_name_encryption": d.DirNameEnc,
"filename_encoding": "base64", "filename_encoding": d.FileNameEncoding,
"suffix": d.EncryptedSuffix, "suffix": d.EncryptedSuffix,
"pass_bad_blocks": "", "pass_bad_blocks": "",
} }
@ -82,7 +84,6 @@ func (d *Crypt) Init(ctx context.Context) error {
} }
d.cipher = c d.cipher = c
//c, err := rcCrypt.newCipher(rcCrypt.NameEncryptionStandard, "", "", true, nil)
return nil return nil
} }
@ -123,11 +124,16 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
//filter illegal files //filter illegal files
continue continue
} }
if !d.ShowHidden && strings.HasPrefix(name, ".") {
continue
}
objRes := model.Object{ objRes := model.Object{
Name: name, Name: name,
Size: 0, Size: 0,
Modified: obj.ModTime(), Modified: obj.ModTime(),
IsFolder: obj.IsDir(), IsFolder: obj.IsDir(),
Ctime: obj.CreateTime(),
// discarding hash as it's encrypted
} }
result = append(result, &objRes) result = append(result, &objRes)
} else { } else {
@ -142,13 +148,21 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
//filter illegal files //filter illegal files
continue continue
} }
if !d.ShowHidden && strings.HasPrefix(name, ".") {
continue
}
objRes := model.Object{ objRes := model.Object{
Name: name, Name: name,
Size: size, Size: size,
Modified: obj.ModTime(), Modified: obj.ModTime(),
IsFolder: obj.IsDir(), IsFolder: obj.IsDir(),
Ctime: obj.CreateTime(),
// discarding hash as it's encrypted
} }
if !ok { if d.Thumbnail && thumb == "" {
thumb = utils.EncodePath(common.GetApiUrl(nil)+stdpath.Join("/d", args.ReqPath, ".thumbnails", name+".webp"), true)
}
if !ok && !d.Thumbnail {
result = append(result, &objRes) result = append(result, &objRes)
} else { } else {
objWithThumb := model.ObjThumb{ objWithThumb := model.ObjThumb{
@ -232,70 +246,53 @@ func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
return nil, err return nil, err
} }
if remoteLink.RangeReadCloser.RangeReader == nil && remoteLink.ReadSeekCloser == nil && len(remoteLink.URL) == 0 { if remoteLink.RangeReadCloser == nil && remoteLink.MFile == nil && len(remoteLink.URL) == 0 {
return nil, fmt.Errorf("the remote storage driver need to be enhanced to support encrytion") return nil, fmt.Errorf("the remote storage driver need to be enhanced to support encrytion")
} }
remoteFileSize := remoteFile.GetSize() remoteFileSize := remoteFile.GetSize()
remoteClosers := utils.NewClosers() remoteClosers := utils.EmptyClosers()
rangeReaderFunc := func(ctx context.Context, underlyingOffset, underlyingLength int64) (io.ReadCloser, error) { rangeReaderFunc := func(ctx context.Context, underlyingOffset, underlyingLength int64) (io.ReadCloser, error) {
length := underlyingLength length := underlyingLength
if underlyingLength >= 0 && underlyingOffset+underlyingLength >= remoteFileSize { if underlyingLength >= 0 && underlyingOffset+underlyingLength >= remoteFileSize {
length = -1 length = -1
} }
if remoteLink.RangeReadCloser.RangeReader != nil { rrc := remoteLink.RangeReadCloser
if len(remoteLink.URL) > 0 {
rangedRemoteLink := &model.Link{
URL: remoteLink.URL,
Header: remoteLink.Header,
}
var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, rangedRemoteLink)
if err != nil {
return nil, err
}
rrc = converted
}
if rrc != nil {
//remoteRangeReader, err := //remoteRangeReader, err :=
remoteReader, err := remoteLink.RangeReadCloser.RangeReader(http_range.Range{Start: underlyingOffset, Length: length}) remoteReader, err := rrc.RangeRead(ctx, http_range.Range{Start: underlyingOffset, Length: length})
remoteClosers.Add(remoteLink.RangeReadCloser.Closers) remoteClosers.AddClosers(rrc.GetClosers())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return remoteReader, nil return remoteReader, nil
} }
if remoteLink.ReadSeekCloser != nil { if remoteLink.MFile != nil {
_, err := remoteLink.ReadSeekCloser.Seek(underlyingOffset, io.SeekStart) _, err := remoteLink.MFile.Seek(underlyingOffset, io.SeekStart)
if err != nil { if err != nil {
return nil, err return nil, err
} }
//remoteClosers.Add(remoteLink.ReadSeekCloser) //remoteClosers.Add(remoteLink.MFile)
//keep reuse same ReadSeekCloser and close at last. //keep reuse same MFile and close at last.
return io.NopCloser(remoteLink.ReadSeekCloser), nil remoteClosers.Add(remoteLink.MFile)
return io.NopCloser(remoteLink.MFile), nil
} }
if len(remoteLink.URL) > 0 {
rangedRemoteLink := &model.Link{
URL: remoteLink.URL,
Header: remoteLink.Header,
}
response, err := RequestRangedHttp(args.HttpReq, rangedRemoteLink, underlyingOffset, length)
//remoteClosers.Add(response.Body)
if err != nil {
return nil, fmt.Errorf("remote storage http request failure,status: %d err:%s", response.StatusCode, err)
}
if underlyingOffset == 0 && length == -1 || response.StatusCode == http.StatusPartialContent {
return response.Body, nil
} else if response.StatusCode == http.StatusOK {
log.Warnf("remote http server not supporting range request, expect low perfromace!")
readCloser, err := net.GetRangedHttpReader(response.Body, underlyingOffset, length)
if err != nil {
return nil, err
}
return readCloser, nil
}
return response.Body, nil
}
//if remoteLink.Data != nil {
// log.Warnf("remote storage not supporting range request, expect low perfromace!")
// readCloser, err := net.GetRangedHttpReader(remoteLink.Data, underlyingOffset, length)
// remoteCloser = remoteLink.Data
// if err != nil {
// return nil, err
// }
// return readCloser, nil
//}
return nil, errs.NotSupport return nil, errs.NotSupport
} }
resultRangeReader := func(httpRange http_range.Range) (io.ReadCloser, error) { resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
readSeeker, err := d.cipher.DecryptDataSeek(ctx, rangeReaderFunc, httpRange.Start, httpRange.Length) readSeeker, err := d.cipher.DecryptDataSeek(ctx, rangeReaderFunc, httpRange.Start, httpRange.Length)
if err != nil { if err != nil {
return nil, err return nil, err
@ -306,7 +303,7 @@ func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
resultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers} resultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers}
resultLink := &model.Link{ resultLink := &model.Link{
Header: remoteLink.Header, Header: remoteLink.Header,
RangeReadCloser: *resultRangeReadCloser, RangeReadCloser: resultRangeReadCloser,
Expiration: remoteLink.Expiration, Expiration: remoteLink.Expiration,
} }
@ -370,32 +367,33 @@ func (d *Crypt) Remove(ctx context.Context, obj model.Obj) error {
return op.Remove(ctx, d.remoteStorage, remoteActualPath) return op.Remove(ctx, d.remoteStorage, remoteActualPath)
} }
func (d *Crypt) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error {
dstDirActualPath, err := d.getActualPathForRemote(dstDir.GetPath(), true) dstDirActualPath, err := d.getActualPathForRemote(dstDir.GetPath(), true)
if err != nil { if err != nil {
return fmt.Errorf("failed to convert path to remote path: %w", err) return fmt.Errorf("failed to convert path to remote path: %w", err)
} }
in := stream.GetReadCloser()
// Encrypt the data into wrappedIn // Encrypt the data into wrappedIn
wrappedIn, err := d.cipher.EncryptData(in) wrappedIn, err := d.cipher.EncryptData(streamer)
if err != nil { if err != nil {
return fmt.Errorf("failed to EncryptData: %w", err) return fmt.Errorf("failed to EncryptData: %w", err)
} }
streamOut := &model.FileStream{ // doesn't support seekableStream, since rapid-upload is not working for encrypted data
streamOut := &stream.FileStream{
Obj: &model.Object{ Obj: &model.Object{
ID: stream.GetID(), ID: streamer.GetID(),
Path: stream.GetPath(), Path: streamer.GetPath(),
Name: d.cipher.EncryptFileName(stream.GetName()), Name: d.cipher.EncryptFileName(streamer.GetName()),
Size: d.cipher.EncryptedSize(stream.GetSize()), Size: d.cipher.EncryptedSize(streamer.GetSize()),
Modified: stream.ModTime(), Modified: streamer.ModTime(),
IsFolder: stream.IsDir(), IsFolder: streamer.IsDir(),
}, },
ReadCloser: io.NopCloser(wrappedIn), Reader: wrappedIn,
Mimetype: "application/octet-stream", Mimetype: "application/octet-stream",
WebPutAsTask: stream.NeedStore(), WebPutAsTask: streamer.NeedStore(),
Old: stream.GetOld(), ForceStreamUpload: true,
Exist: streamer.GetExist(),
} }
err = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false) err = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false)
if err != nil { if err != nil {

View File

@ -15,16 +15,15 @@ type Addition struct {
DirNameEnc string `json:"directory_name_encryption" type:"select" required:"true" options:"false,true" default:"false"` DirNameEnc string `json:"directory_name_encryption" type:"select" required:"true" options:"false,true" default:"false"`
RemotePath string `json:"remote_path" required:"true" help:"This is where the encrypted data stores"` RemotePath string `json:"remote_path" required:"true" help:"This is where the encrypted data stores"`
Password string `json:"password" required:"true" confidential:"true" help:"the main password"` Password string `json:"password" required:"true" confidential:"true" help:"the main password"`
Salt string `json:"salt" confidential:"true" help:"If you don't know what is salt, treat it as a second password'. Optional but recommended"` Salt string `json:"salt" confidential:"true" help:"If you don't know what is salt, treat it as a second password. Optional but recommended"`
EncryptedSuffix string `json:"encrypted_suffix" required:"true" default:".bin" help:"encrypted files will have this suffix"` EncryptedSuffix string `json:"encrypted_suffix" required:"true" default:".bin" help:"for advanced user only! encrypted files will have this suffix"`
} FileNameEncoding string `json:"filename_encoding" type:"select" required:"true" options:"base64,base32,base32768" default:"base64" help:"for advanced user only!"`
/*// inMemory contains decrypted confidential info and other temp data. will not persist these info anywhere Thumbnail bool `json:"thumbnail" required:"true" default:"false" help:"enable thumbnail which pre-generated under .thumbnails folder"`
type inMemory struct {
password string ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
salt string }
}*/
var config = driver.Config{ var config = driver.Config{
Name: "Crypt", Name: "Crypt",

View File

@ -1,24 +1,13 @@
package crypt package crypt
import ( import (
"net/http"
stdpath "path" stdpath "path"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/net"
"github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/http_range"
) )
func RequestRangedHttp(r *http.Request, link *model.Link, offset, length int64) (*http.Response, error) {
header := net.ProcessHeader(http.Header{}, link.Header)
header = http_range.ApplyRangeToHttpHeader(http_range.Range{Start: offset, Length: length}, header)
return net.RequestHttp("GET", header, link.URL)
}
// will give the best guessing based on the path // will give the best guessing based on the path
func guessPath(path string) (isFolder, secondTry bool) { func guessPath(path string) (isFolder, secondTry bool) {
if strings.HasSuffix(path, "/") { if strings.HasSuffix(path, "/") {

View File

@ -45,7 +45,25 @@ func (d *Dropbox) Init(ctx context.Context) error {
if result != query { if result != query {
return fmt.Errorf("failed to check user: %s", string(res)) return fmt.Errorf("failed to check user: %s", string(res))
} }
return nil d.RootNamespaceId, err = d.GetRootNamespaceId(ctx)
return err
}
func (d *Dropbox) GetRootNamespaceId(ctx context.Context) (string, error) {
res, err := d.request("/2/users/get_current_account", http.MethodPost, func(req *resty.Request) {
req.SetBody(nil)
})
if err != nil {
return "", err
}
var currentAccountResp CurrentAccountResp
err = utils.Json.Unmarshal(res, &currentAccountResp)
if err != nil {
return "", err
}
rootNamespaceId := currentAccountResp.RootInfo.RootNamespaceId
return rootNamespaceId, nil
} }
func (d *Dropbox) Drop(ctx context.Context) error { func (d *Dropbox) Drop(ctx context.Context) error {
@ -203,7 +221,7 @@ func (d *Dropbox) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
_ = res.Body.Close() _ = res.Body.Close()
if count > 0 { if count > 0 {
up((i + 1) * 100 / count) up(float64(i+1) * 100 / float64(count))
} }
offset += byteSize offset += byteSize

View File

@ -17,7 +17,8 @@ type Addition struct {
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"` ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"` ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
AccessToken string AccessToken string
RootNamespaceId string
} }
var config = driver.Config{ var config = driver.Config{

View File

@ -23,6 +23,13 @@ type RefreshTokenErrorResp struct {
ErrorDescription string `json:"error_description"` ErrorDescription string `json:"error_description"`
} }
type CurrentAccountResp struct {
RootInfo struct {
RootNamespaceId string `json:"root_namespace_id"`
HomeNamespaceId string `json:"home_namespace_id"`
} `json:"root_info"`
}
type File struct { type File struct {
Tag string `json:".tag"` Tag string `json:".tag"`
Name string `json:"name"` Name string `json:"name"`

View File

@ -46,12 +46,22 @@ func (d *Dropbox) refreshToken() error {
func (d *Dropbox) request(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) { func (d *Dropbox) request(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
req := base.RestyClient.R() req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken) req.SetHeader("Authorization", "Bearer "+d.AccessToken)
if method == http.MethodPost { if d.RootNamespaceId != "" {
req.SetHeader("Content-Type", "application/json") apiPathRootJson, err := utils.Json.MarshalToString(map[string]interface{}{
".tag": "root",
"root": d.RootNamespaceId,
})
if err != nil {
return nil, err
}
req.SetHeader("Dropbox-API-Path-Root", apiPathRootJson)
} }
if callback != nil { if callback != nil {
callback(req) callback(req)
} }
if method == http.MethodPost && req.Body != nil {
req.SetHeader("Content-Type", "application/json")
}
var e ErrorResp var e ErrorResp
req.SetError(&e) req.SetError(&e)
res, err := req.Execute(method, d.base+uri) res, err := req.Execute(method, d.base+uri)

View File

@ -64,9 +64,9 @@ func (d *FTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*m
return nil, err return nil, err
} }
r := NewFTPFileReader(d.conn, file.GetPath()) r := NewFileReader(d.conn, file.GetPath(), file.GetSize())
link := &model.Link{ link := &model.Link{
ReadSeekCloser: r, MFile: r,
} }
return link, nil return link, nil
} }

View File

@ -4,6 +4,7 @@ import (
"io" "io"
"os" "os"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/jlaffaye/ftp" "github.com/jlaffaye/ftp"
@ -30,43 +31,59 @@ func (d *FTP) login() error {
return nil return nil
} }
// An FTP file reader that implements io.ReadSeekCloser for seeking. // FileReader An FTP file reader that implements io.MFile for seeking.
type FTPFileReader struct { type FileReader struct {
conn *ftp.ServerConn conn *ftp.ServerConn
resp *ftp.Response resp *ftp.Response
offset int64 offset atomic.Int64
mu sync.Mutex readAtOffset int64
path string mu sync.Mutex
path string
size int64
} }
func NewFTPFileReader(conn *ftp.ServerConn, path string) *FTPFileReader { func NewFileReader(conn *ftp.ServerConn, path string, size int64) *FileReader {
return &FTPFileReader{ return &FileReader{
conn: conn, conn: conn,
path: path, path: path,
size: size,
} }
} }
func (r *FTPFileReader) Read(buf []byte) (n int, err error) { func (r *FileReader) Read(buf []byte) (n int, err error) {
n, err = r.ReadAt(buf, r.offset.Load())
r.offset.Add(int64(n))
return
}
func (r *FileReader) ReadAt(buf []byte, off int64) (n int, err error) {
if off < 0 {
return -1, os.ErrInvalid
}
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
if off != r.readAtOffset {
//have to restart the connection, to correct offset
_ = r.resp.Close()
r.resp = nil
}
if r.resp == nil { if r.resp == nil {
r.resp, err = r.conn.RetrFrom(r.path, uint64(r.offset)) r.resp, err = r.conn.RetrFrom(r.path, uint64(off))
r.readAtOffset = off
if err != nil { if err != nil {
return 0, err return 0, err
} }
} }
n, err = r.resp.Read(buf) n, err = r.resp.Read(buf)
r.offset += int64(n) r.readAtOffset += int64(n)
return return
} }
func (r *FTPFileReader) Seek(offset int64, whence int) (int64, error) { func (r *FileReader) Seek(offset int64, whence int) (int64, error) {
r.mu.Lock() oldOffset := r.offset.Load()
defer r.mu.Unlock()
oldOffset := r.offset
var newOffset int64 var newOffset int64
switch whence { switch whence {
case io.SeekStart: case io.SeekStart:
@ -74,11 +91,7 @@ func (r *FTPFileReader) Seek(offset int64, whence int) (int64, error) {
case io.SeekCurrent: case io.SeekCurrent:
newOffset = oldOffset + offset newOffset = oldOffset + offset
case io.SeekEnd: case io.SeekEnd:
size, err := r.conn.FileSize(r.path) return r.size, nil
if err != nil {
return oldOffset, err
}
newOffset = offset + int64(size)
default: default:
return -1, os.ErrInvalid return -1, os.ErrInvalid
} }
@ -91,17 +104,11 @@ func (r *FTPFileReader) Seek(offset int64, whence int) (int64, error) {
// offset not changed, so return directly // offset not changed, so return directly
return oldOffset, nil return oldOffset, nil
} }
r.offset = newOffset r.offset.Store(newOffset)
if r.resp != nil {
// close the existing ftp data connection, otherwise the next read will be blocked
_ = r.resp.Close() // we do not care about whether it returns an error
r.resp = nil
}
return newOffset, nil return newOffset, nil
} }
func (r *FTPFileReader) Close() error { func (r *FileReader) Close() error {
if r.resp != nil { if r.resp != nil {
return r.resp.Close() return r.resp.Close()
} }

View File

@ -112,7 +112,7 @@ func (d *GoogleDrive) Remove(ctx context.Context, obj model.Obj) error {
} }
func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
obj := stream.GetOld() obj := stream.GetExist()
var ( var (
e Error e Error
url string url string
@ -158,7 +158,7 @@ func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
putUrl := res.Header().Get("location") putUrl := res.Header().Get("location")
if stream.GetSize() < d.ChunkSize*1024*1024 { if stream.GetSize() < d.ChunkSize*1024*1024 {
_, err = d.request(putUrl, http.MethodPut, func(req *resty.Request) { _, err = d.request(putUrl, http.MethodPut, func(req *resty.Request) {
req.SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).SetBody(stream.GetReadCloser()) req.SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).SetBody(stream)
}, nil) }, nil)
} else { } else {
err = d.chunkUpload(ctx, stream, putUrl) err = d.chunkUpload(ctx, stream, putUrl)

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