Compare commits

...

516 Commits

Author SHA1 Message Date
3375c26c41 perf(quark_uc&quark_uc_tv): native proxy multithreading (#8287)
* perf(quark_uc): native proxy multithreading

* perf(quark_uc_tv): native proxy multithreading

* chore(fs): file query result add id
2025-04-03 20:50:29 +08:00
ab68faef44 fix(baidu_netdisk): add another video crack api (#8275)
Co-authored-by: anobodys <anobodys@gmail.com>
2025-04-03 20:44:49 +08:00
2e21df0661 feat(driver): add Azure Blob Storage driver (#8261)
* add azure-blob driver

* fix nested folders copy

* feat(driver): add Azure Blob Storage driver

实现 Azure Blob Storage 驱动,支持以下功能:
- 使用共享密钥身份验证初始化连接
- 列出目录和文件
- 生成临时 SAS URL 进行文件访问
- 创建目录
- 移动和重命名文件/文件夹
- 复制文件/文件夹
- 删除文件/文件夹
- 上传文件并支持进度跟踪

此驱动允许用户通过 AList 平台无缝访问和管理 Azure Blob Storage 中的数据。

* feat(driver): update help doc for Azure Blob

* doc(readme): add new driver

* Update drivers/azure_blob/driver.go

fix(azure): fix name check

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update README.md

doc(readme): fix the link

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(azure): fix log and link

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-03 20:43:21 +08:00
af18cb138b feat(139): add option ReportRealSize (#8244 close #8141)
* feat(139): handle family upload errors

* feat(139): add option `ReportRealSize`

* Update drivers/139/driver.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-03 20:41:59 +08:00
31c55a2adf fix(archive): unable to preview (#8248)
* fix(archive): unable to preview

* fix bug
2025-04-03 20:41:05 +08:00
465dd1703d feat(cloudreve): s3 policy support (#8245)
* feat(cloudreve): s3 policy support

* fix(cloudreve): correct potential off-by-one error in `etags` initialization
2025-04-03 20:40:19 +08:00
a6304285b6 fix: revert "refactor(net): pass request header" (#8269)
5be50e77d9
2025-04-03 20:35:52 +08:00
affd0cecd1 fix(pikpak&pikpak_share): update algorithms (#8278) 2025-04-03 20:35:14 +08:00
37640221c0 fix(doubao): update file size type to int64 (#8289) 2025-04-03 20:34:27 +08:00
e4bd223d1c fix(deps): update 115-sdk-go to v0.1.5 2025-04-03 20:29:53 +08:00
0cde4e73d6 feat(ipfs): better ipfs support (#8225)
* feat:  better ipfs support

fixed mfs crud, added ipns support

* Update driver.go

clean up
2025-03-27 23:25:23 +08:00
7b62dcb88c fix(baidu_netdisk): deplicate retry (#8210 redo #7972, link #8180) 2025-03-27 23:22:55 +08:00
c38dc6df7c fix(115_open): support multipart upload (#8229)
Co-authored-by: neverlee <neverlea@formail.com>
2025-03-27 23:22:08 +08:00
5668e4a4ea feat(doubao): add Doubao driver (#8232 closes #8020 #8206)
* feat(doubao): implement List()

* feat(doubao): implement Link()

* feat(doubao): implement MakeDir()

* refactor(doubao): add type Object to store key

* feat(doubao): implement Move()

* feat(doubao): implement Rename()

* feat(doubao): implement Remove()
2025-03-27 23:21:42 +08:00
1335f80362 feat(archive): support multipart archives (#8184 close #8015)
* feat(archive): multipart support & sevenzip tool

* feat(archive): rardecode tool

* feat(archive): support decompress multi-selected

* fix(archive): decompress response filter internal

* feat(archive): support multipart zip

* fix: more applicable AcceptedMultipartExtensions interface
2025-03-27 23:20:44 +08:00
704d3854df feat(alist_v3): support forward archive requests (#8230)
* feat(alist_v3): support forward archive requests

* fix: encode all inner path
2025-03-27 23:18:34 +08:00
44cc71d354 fix(cloudreve): enable SetContentLength for uploading to local policy (#8228 close #8174)
* fix(cloudreve): upload failure to return error msg instead of deletion success

* fix(cloudreve): enable SetContentLength for uploading to local policy

* refactor(cloudreve): move local policy upload logic to utils for better error handling

* refactor(cloudreve): unified upload code style

* refactor(cloudreve): improve user agent handling
2025-03-27 23:18:15 +08:00
9a9aee9ac6 feat(alias): support writing to non-ambiguous paths (#8216)
* feat(alias): support writing to non-ambiguous paths

* feat(alias): support extract concurrency

* fix(alias): extract url no pass query
2025-03-27 23:17:45 +08:00
4fcc3a187e fix(traffic): duplicate semaphore release when uploading (#8211 close #8180) 2025-03-27 23:15:47 +08:00
10a76c701d fix(db): support postgres trust/peer mode (#8198 close #8066) 2025-03-27 23:15:04 +08:00
6e13923225 fix(sftp-server): postgre cannot store control characters (#8188 close #8186) 2025-03-27 23:14:36 +08:00
32890da29f fix(115_open): upgrade 115-sdk-go dependency to v0.1.4 2025-03-21 19:06:09 +08:00
758554a40f fix(115_open): upgrade 115-sdk-go dependency to v0.1.3 (close #8169) 2025-03-19 21:47:42 +08:00
4563aea47e fix(115_open): rename delay to take effect (close #8156) 2025-03-18 22:25:04 +08:00
35d6f3b8fc fix(115_open): upgrade sdk (close #8151) 2025-03-18 22:21:50 +08:00
b4e6ab12d9 refactor: FilterReadMeScripts (#8154 close #8150)
* refactor: FilterReadMeScripts

* .
2025-03-18 22:02:33 +08:00
3499c4db87 feat: 115 open driver (#8139)
* wip: 115 open

* chore(go.mod): update 115-sdk-go dependency version

* feat(115_open): implement directory management and file operations

* chore(go.mod): update 115-sdk-go dependency to v0.1.1 and adjust callback handling in driver

* chore: rename driver
2025-03-17 00:52:09 +08:00
d20f41d687 fix: missing handling of RangeReadCloser (#8146) 2025-03-16 22:14:44 +08:00
d16ba65f42 fix(lang): initialize configuration in LangCmd before generating language JSON file 2025-03-16 16:37:33 +08:00
c82e632ee1 fix: potential XSS vulnerabilities (#7923)
* fix: potential XSS vulnerabilities

* feat: support filter and render for readme.md

* chore: set ReadMeAutoRender to true

* fix attachFileName undefined

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2025-03-15 23:28:40 +08:00
04f5525f20 fix(s3): incorrectly added slash before the Bucket name (#8083 close #8001) 2025-03-15 00:21:24 +08:00
28b61a93fd feat(webdav): support oc:checksums (#8064 close #7472)
Ref: #7472
2025-03-15 00:21:07 +08:00
0126af4de0 fix(crypt): premature close of MFile (#8132 close #8119)
* fix(crypt): premature close of MFile

* refactor
2025-03-15 00:13:30 +08:00
7579d44517 fix(onedrive): set req.ContentLength (#8081)
* fix(onedrive): set req.ContentLength

* fix(onedrive_app): set req.ContentLength

* fix(cloudreve): set req.ContentLength
2025-03-15 00:12:37 +08:00
5dfea714d8 fix(cloudreve): use milliseconds timestamp in last_modified (#8133) 2025-03-15 00:12:15 +08:00
370a6c15a9 fix(baidu_netdisk): remove duplicate retry (#7972) 2025-03-01 19:00:36 +08:00
2570707a06 feat(baidu_netdisk): support dynamical slice size for low bandwith upload case (#7965)
* 动态分片尺寸

* 补充严格测试结果
2025-03-01 18:46:05 +08:00
4145734c18 refactor(net): pass request header (#8031 close #8008)
* refactor(net): pass request header

* feat(proxy): add `Etag` to response header

* refactor
2025-03-01 18:35:34 +08:00
646c7bcd21 fix(archive): use another sign for extraction (#7982) 2025-03-01 18:34:33 +08:00
cdc41595bc feat(github): support GPG verification (#7996 close #7986)
* feat(github): support GPG verification

* chore
2025-02-24 23:12:23 +08:00
79bef0be9e chore: fix build failed (#8005) 2025-02-16 15:11:48 +08:00
c230f24ebe fix(archive): decode filename when decompressing zips (#7998 close #7988) 2025-02-16 12:25:01 +08:00
30d8c20756 feat(archive): support deprioritize previewing (#7984) 2025-02-16 12:24:10 +08:00
3b71500f23 feat(traffic): support limit task worker count & file stream rate (#7948)
* feat: set task workers num & client stream rate limit

* feat: server stream rate limit

* upgrade xhofe/tache

* .
2025-02-16 12:22:11 +08:00
399336b33c fix(189pc): transfer rename (#7958)
* fix(189pc): transfer rename

* fix: OverwriteUpload

* fix: change search method

* fix

* fix
2025-02-16 12:21:34 +08:00
36b4204623 feat(github): support github proxy (#7979 close #7963) 2025-02-16 12:21:03 +08:00
f25be154c6 fix(ilanzou): add header X-Forwarded-For to solve IP ban (#7977)
* fix: warning

* feat: ip header

* fix: ip header for fs link
2025-02-16 12:20:28 +08:00
ec3fc945a3 fix(feiji): modify the request header (#7902 close #7890) 2025-02-09 18:35:39 +08:00
3f9bed3d5f feat(bootstrap): add .url to proxy types (#7928) 2025-02-09 18:33:38 +08:00
b9ad18bd0a feat(recursive-move): Advanced conflict policy for preventing unintentional overwriting (#7906) 2025-02-09 18:32:57 +08:00
0219c4e15a fix(index): fix the issue where ignored paths are not updated (#7907) 2025-02-09 18:31:43 +08:00
d983a4ebcb refactor(cmd): use std runtime package to get go version info (#7964)
* refactor(cmd): use std `runtime` package to get go version info

- Remove the `GoVersion` variable.
- Remove overriding `GoVersion` by ldflags in `build.sh`.
- Get go version, OS and arch from the constants in the std `runtime` package instead of compile time.

* chore(ci): remove `GoVersion` flag from workflows

Remove GoVersion flag from beta_release.yml and build.yml workflows.

> Reduce compile-time dependencies.
2025-02-09 18:30:56 +08:00
f795807753 feat(github_releases): support dir size for show all version (#7938)
* refactor

* 修改默认 RepoStructure

* feat: 支持使用 gh-proxy
2025-02-09 18:30:38 +08:00
6164e4577b fix: missing args when using alias driver (#7941 close #7932) 2025-02-05 19:22:10 +08:00
39bde328ee fix(lenovonas_share): the size of the directory (#7914) 2025-02-01 17:32:58 +08:00
779c293f04 fix(driver): implement canceling and updating progress for putting for some drivers (#7847)
* fix(driver): additionally implement canceling and updating progress for putting for some drivers

* refactor: add driver archive api into template

* fix(123): use built-in MD5 to avoid caching full

* .

* fix build failed
2025-02-01 17:29:55 +08:00
b9f397d29f fix(139): restore the Account handling, partially reverts #7850 (#7900 close #7784) 2025-01-30 11:25:41 +08:00
d53eecc229 fix(febbox): panic due to slice out of range (#7898 close #7889) 2025-01-30 11:24:07 +08:00
f88fd83d4a feat(ci): use go-cross/cgo-actions for dev build 2025-01-28 18:57:09 +08:00
226c34929a feat(ci): add build info for beta release 2025-01-27 21:32:59 +08:00
027edcbe53 refactor(patch): execute all patches in dev version (#7807) 2025-01-27 20:49:24 +08:00
fd51f34efa feat(misskey): add misskey driver (#7864) 2025-01-27 20:47:52 +08:00
bdd9774aa7 feat(github_releases): add support for github_releases driver (#7844 close #7842)
* feat(github_releases): 添加对 GitHub Releases 的支持

* feat(github_releases): 增加目录大小和更新时间,增加请求缓存

* Feat(github_releases): 可选填入 GitHub token 来提高速率限制或访问私有仓库

* Fix(github_releases): 修复仓库无权限或不存在时的异常

* feat(github_releases): 支持显示所有版本,开启后不显示文件夹大小

* feat(github_releases): 兼容无子目录
2025-01-27 20:28:44 +08:00
258b8f520f feat(recursive-move): add overwrite option to preventing unintentional overwriting (#7868 closes #7382,#7719)
* feat(recursive-move): add `overwrite` option to preventing unintentional overwriting

* chore: rearrange code order
2025-01-27 20:25:39 +08:00
99f39410f2 fix(s3): escape CopySource request header when copying files (#7860 close #7858) 2025-01-27 20:23:13 +08:00
267120a8c8 fix(115): fix offline download (#7845 close #7794)
* feat(115): use multi url for list files & change download url api

* fix(115): fix offline download. (close #7794)
2025-01-27 20:20:55 +08:00
5eff8cc7bf feat(upload): support rapid upload on web (#7851) 2025-01-27 20:20:09 +08:00
d5ec998699 feat(task): allow retry canceled (#7852) 2025-01-27 20:18:10 +08:00
23f3178f39 chore(README): formatting spacing in README links (#7879) [skip ci] 2025-01-27 20:13:35 +08:00
cafdb4d407 fix(139): correct path handling in groupGetFiles (#7850 closes #7848,#7603)
* fix(139): correct path handling in groupGetFiles

* perf(139): reduce the number of requests in groupGetFiles

* refactor(139): check authorization expiration (#10)

* refactor(139): check authorization expiration

* fix bug

* chore(139): update api version to 7.14.0

---------

Co-authored-by: j2rong4cn <36783515+j2rong4cn@users.noreply.github.com>
2025-01-27 20:11:21 +08:00
0d4c63e9ff feat(fs): display the existing filename in error message (#7877) 2025-01-27 20:09:17 +08:00
5c5d8378e5 fix(archive): unable to preview (#7843)
* fix(archive): unrecognition zip

* feat(archive): add tree for zip meta

* fix bug

* refactor(archive):  meta cache time use Link Expiration first

* feat(archive): return sort policy in meta (#2)

* refactor

* perf(archive): reduce new network requests

---------

Co-authored-by: KirCute_ECT <951206789@qq.com>
2025-01-27 20:08:56 +08:00
2be0c3d1a0 feat(alias): add DownloadConcurrency and DownloadPartSize option (#7829)
* fix(net): goroutine logic bug (AlistGo/alist#7215)

* Fix goroutine logic bug

* Fix bug

---------

Co-authored-by: hpy hs <hshpy.pengyu@gmail.com>

* perf(net): sequential and dynamic concurrency

* fix(net): incorrect error return

* feat(alias):  add `DownloadConcurrency` and `DownloadPartSize` option

* feat(net): add `ConcurrencyLimit`

* pref(net): create `chunk` on demand

* refactor

* refactor

* fix(net): `r.Closers.Add` has no effect

* refactor

---------

Co-authored-by: hpy hs <hshpy.pengyu@gmail.com>
2025-01-27 20:08:39 +08:00
bdcf450203 fix: resolve concurrent read/write issues in WrapObjName (#7865) 2025-01-27 20:06:18 +08:00
c2633dd443 fix(workflow): use the dev version of the web for beta releases (#7862)
* fix(workflow): use dev version of the web for beta releases

* chore(config): check version string by prefix
2025-01-23 22:49:35 +08:00
11b6a6012f fix(copy): use Link and Put when the driver does not support copying (#7834) 2025-01-18 23:52:02 +08:00
59e02287b2 feat(fs): add overwrite option to preventing unintentional overwriting (#7809) 2025-01-18 23:39:07 +08:00
bb40e2e2cd feat(archive): archive manage (#7817)
* feat(archive): archive management

* fix(ftp-server): remove duplicate ReadAtSeeker realization

* fix(archive): bad seeking of SeekableStream

* fix(archive): split internal and driver extraction api

* feat(archive): patch

* fix(shutdown): clear decompress upload tasks

* chore

* feat(archive): support .iso format

* chore
2025-01-18 23:28:12 +08:00
ab22cf8233 feat: add Reference interface to driver (#7805)
* feat: add `Reference` interface to driver

* feat(123_share): support reference 123pan
2025-01-18 23:26:58 +08:00
880cc7abca fix(139): use personal_new by default (#7836) 2025-01-18 23:24:09 +08:00
b60da9732f feat(offline-download): allow using offline download tools in any storage (#7716)
* Feat(offline-download): allow using thunder offline download tool in any storage

* Feat(offline-download): allow using 115 offline download tool in any storage

* Feat(offline-download): allow using pikpak offline download tool in any storage

* style(offline-download): unify offline download tool names

* feat(offline-download): show available offline download tools only

* Fix(offline-download): update unmodified tool names.

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2025-01-10 21:24:44 +08:00
e04114d102 feat(github): add github api driver (#7717)
* feat(github): add github api driver

* fix: filter submodule operation

* feat: rename, copy and move, but with bugs

* fix: move and copy returns 422

* fix: change TargetPath in rename msg from parent path to new self path

* fix: add non-commit mutex

* pref(github): use net/http to put blob

* chore: add a help message to `ref` addition
2025-01-10 20:59:58 +08:00
51bcf83511 feat(url-tree): support url tree driver writing (#7779 close #5166)
* feat: support url tree writing

* fix: meta writable

* feat: disable writable via addition
2025-01-10 20:50:56 +08:00
25b4b55ee1 feat(ftp-server): support resumable downloading (#7792) 2025-01-10 20:50:20 +08:00
6812ec9a6d fix(ilanzou): add accept-encoding request header (#7796 close #7759) 2025-01-10 20:49:50 +08:00
31a7470865 feat(local): support both time and percent for video thumbnail (#7802)
* feat(local): support percent for video thumbnail

The percentage determines the point in the video (as a percentage of the total duration) at which the thumbnail will be generated.

* feat(local): support both time and percent for video thumbnail
2025-01-10 20:48:45 +08:00
Mmx
687124c81d ci(build_docker): merge build_docker into release_docker workflow (#7755)
* feat(ci): merge build_docker workflow into release_docker

* fix(ci): logics of docker meta
2025-01-01 21:29:59 +08:00
e4439e66b9 fix:(baidu_photo): upload erron -6 (#7760 close #7744)
* fix:(baidu_photo): upload erron -6

* fix(baidu_photo):api add bdstoken
2025-01-01 21:13:34 +08:00
7fd4ac7851 fix(139): update familyGetFiles pagination logic (#7748 close #7711) 2024-12-30 22:55:47 +08:00
6745dcc139 feat(task): attach creator to user of the context (#7729) 2024-12-30 22:55:09 +08:00
aa1082a56c feat(sftp-server): do not generate host key until first enabled (#7734) 2024-12-30 22:54:37 +08:00
ed149be84b feat(index): add disable index option for storages (#7730) 2024-12-30 22:52:55 +08:00
040dc14ee6 fix(lenovonas_share): stoken expire (#7727) 2024-12-30 22:51:39 +08:00
Mmx
4dce53d72b feat(docker release): improve aria2 image, add aio image (#7750)
* build: add argument INSTALL_ARIA2 to dockerfile

* feat: run aria2 in main entrypoint

* feat(ci): environment matrix for docker release

* improve(ci): allow overwrite artifacts in docker release

* fix(ci): permission of alist binary in docker; entrypoint logic

* improve(aria2): move aria2 data to /opt/aria2; fix permission issues

References:

https://github.com/AlistGo/with_aria2/pull/13

Co-authored-by: GoodbyeNJN <cc@fuckwall.cc>

* fix(ci): aio image is not taking effect

* fix(build): tar command in aria2 installation process

(cherry picked from commit 647285408354807bae64df6a20fefb696ff787de)

---------

Co-authored-by: GoodbyeNJN <cc@fuckwall.cc>
2024-12-30 22:51:05 +08:00
365fc40dfe fix: static page to limit request method (#7745 close #7667) 2024-12-30 22:49:18 +08:00
5994c17b4e feat(patch): upgrade patch module (#7738)
* feat(patch): upgrade patch module

* chore(patch): add docs

* fix(patch): skip and rewrite invalid last launched version

* fix(patch): turn two functions into patches
2024-12-30 22:48:33 +08:00
42243b1517 feat(thunder): add offline download tool (#7673)
* feat(thunder): add offline download tool

* fix(thunder): improve error handling and parse file size in status response

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-12-25 21:23:58 +08:00
48916cdedf fix(permission): enhance the strictness of permissions (#7705 close #7680)
* fix(permission): enhance the strictness of permissions

* fix: add initial permissions to admin
2024-12-25 21:17:58 +08:00
5ecf5e823c fix(webauthn): handle error when removing webauthn credential (#7689) 2024-12-25 21:16:34 +08:00
c218b5701e fix(115): support float QPS (#7677) 2024-12-25 21:16:03 +08:00
77d0c78bfd feat(sftp-server): public key login (#7668) 2024-12-25 21:15:06 +08:00
db5c601cfe fix(crypt): add sign to thumbnail (#6611) 2024-12-25 21:13:54 +08:00
221cdf3611 feat(s3): support custom host presign (#7699 close #7696) 2024-12-25 21:13:23 +08:00
40b0e66efe feat(ftp-server): treat moving across file systems as copying (#7704 close #7701)
* feat(ftp-server): treat moving across file systems as copying

* fix: ensure compatibility across different fs on the same driver
2024-12-25 21:12:30 +08:00
b72e85a73a fix(ftp-server): rewrite download in a more appropriate method (#7656) 2024-12-25 21:11:45 +08:00
6aaf5975c6 fix(ftp-server): work unproperly when base url is not root (#7693)
* fix(ftp-server): work unproperly when base url is not root

* fix: avoid merge conflict
2024-12-25 21:11:36 +08:00
bb2aec20e4 fix(139): handle upload file conflicts (#7692) 2024-12-25 21:11:05 +08:00
d7aa1608ac feat(task): add speed monitor (#7655) 2024-12-25 21:09:54 +08:00
db99224126 perf: Speed ​​of database initialization (#7694)
* perf: 优化非sqlite3数据库时初始化慢的问题

* refactor
2024-12-25 21:08:22 +08:00
b8bd14f99b fix(lanzou): missing parameter (#7678 close #7210) 2024-12-17 22:05:52 +08:00
331885ed64 fix(net): close of closed channel (#7580) 2024-12-17 22:04:27 +08:00
cf58ab3a78 chore(config): disable FTP and SFTP by default 2024-12-12 21:04:14 +08:00
33ba7f1521 feat: sftp server support (#7643)
* feat: sftp server support

* fix(sftp-server): try fix build failed

* fix: sftp download lack
2024-12-12 20:51:43 +08:00
201e25c17f fix(ftp-server): large transfer leads to client timeout (#7639)
* fix(ftp-server): client timeout to wait a large file upload to netdisk

* fix(ftp-server): driver alist v3 upload failed and temp files do not be deleted
2024-12-12 20:50:00 +08:00
ecefa5e0eb ci: fix desktop beta release trigger 2024-12-10 20:21:51 +08:00
650b03aeb1 feat: ftp server support (#7634 close #1898)
* feat: ftp server support

* fix(ftp): incorrect mode for dirs in LIST returns
2024-12-10 20:17:46 +08:00
7341846499 perf(task): merge requests of operating selected (#7637) 2024-12-10 19:30:50 +08:00
a3908fd9a6 fix(139): update APIs (#7591 close #7603)
* fix(139): update family cloud API

* fix(139): update API of familyGetLink

* feat(139): support group (close #7603)

* docs: add `139 group` to Readme

* feat(139): support multipart upload (close: #7444)

* feat(139): add custom upload part size option

* fix: missing right big quote

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-12-09 23:54:21 +08:00
2a035302b2 fix(cloudreve): support upload to remote and OneDrive storage (#7632 close #6882)
- Add support for remote and OneDrive storage types
- Implement new upload methods for different storage types
- Update driver to handle various storage policies
- Add error handling and session cleanup for failed uploads
2024-12-09 23:35:44 +08:00
016e169c41 feat(139): support multipart upload (close: #7444) (#7630)
* feat(139): support multipart upload (close: #7444)

* feat(139): add custom upload part size option
2024-12-09 23:34:29 +08:00
088120df82 feat(sso): add custom extra scope support (#7577) 2024-12-09 23:33:46 +08:00
aa45a82914 fix(115): fix login bug (#7626 close #7614 close #7620) 2024-12-09 23:33:07 +08:00
5084d98398 fix(onedrive): fix timeout error (#7551 close #7506) 2024-12-08 17:06:33 +08:00
fa15c576f0 fix(pikpak): remove oauth2 method (#7567 close #7545) 2024-12-07 17:03:46 +08:00
2d3605c684 fix(baidu_photo): cookie login fix download error (#7602) 2024-12-07 17:02:52 +08:00
492b49d77a Update README.md 2024-12-07 01:00:25 +08:00
94915b2148 fix(baidu_netdisk): update fileToObj to use ServerCtime and ServerMtime (#7535) 2024-11-21 22:41:23 +08:00
2dec756f23 fix(pikpak&pikpak_share): captcha_sign error (#7530 close #7481 close #7482) 2024-11-21 22:40:39 +08:00
4c0cffd29b fix(net): close of closed channel (#7529) 2024-11-21 22:39:14 +08:00
25c5e075a9 fix(local): Preserve file owner when copying (#7528) 2024-11-21 22:38:41 +08:00
Mmx
398c04386a feat(sso): generate and verify OAuth state with go-cache (#7527) 2024-11-21 22:38:04 +08:00
Mmx
12b429584e feat(security): generating random string with crypto rand (#7525) 2024-11-21 22:37:19 +08:00
Mmx
150dcc2147 fix(sso): OIDC compatibility mode (#7524) 2024-11-21 22:36:41 +08:00
0ba754fd40 fix(release): missing installation of zig 2024-11-17 23:11:03 +08:00
28d2367a87 fix(ci): no space left on device 2024-11-17 22:24:06 +08:00
a4ad98ee3e fix(pikpak): domain block and change to NET (#7350) 2024-11-17 20:03:04 +08:00
1c01dc6839 fix(storage): delete storage fails if a panic occurred during initialization (#7501)
* fix(storage): store storages map when init storage panic

* fix(drivers): add nil check to drop method
2024-11-16 13:20:49 +08:00
c3c5843dce fix(terabox): panic due to slice out of range (#7499 close #7487) 2024-11-16 13:19:59 +08:00
6c38c5972d fix(terabox): big file upload issue (#7498 close #7490) 2024-11-16 13:18:49 +08:00
0a46979c51 feat(115): enhance cache (#7479) 2024-11-08 22:08:50 +08:00
67c93eed2b feat(baidu_netdisk,baidu_photo): add and fix hashinfo (#7469) 2024-11-08 22:08:25 +08:00
f58de9923a refactor(aliyunopen,config): Modify default properties (#7476) 2024-11-08 22:07:35 +08:00
2671c876f1 revert: "fix(115): enforce 20GB file size limit on uploadev"
This reverts commit 216e3909f3.
2024-11-02 21:08:19 +08:00
e707fa38f1 ci: remove specific tag for freebsd action 2024-11-02 17:05:00 +08:00
b803b0070e fix(115): 20GB file upload restriction (#7452)
* fix(115): multipart upload error

* feat(115): Modify default page size

* fix(115): Replace temporary repair scheme
2024-11-02 16:41:33 +08:00
64ceb5afb6 feat: support general users view and cancel own tasks (#7416 close #7398)
* feat: support general users view and cancel own tasks

Add a creator attribute to the upload, copy and offline download
tasks, so that a GENERAL task creator can view and cancel them.

BREAKING CHANGE:

1. A new internal package `task` including the struct `TaskWithCreator`
   which embeds `tache.Base` is created, and the past dependence on
   `tache.Task` will all be transferred to dependence on this package.
2. The API `/admin/task` can now also be accessed via `/task`, and the
   old endpoint is retained to ensure compatibility with legacy
   automation scripts.

Closes #7398

* fix(deps): update github.com/xhofe/tache to v0.1.3
2024-11-01 23:32:26 +08:00
10c7ebb1c0 fix(local): cross-device file move (#7430) 2024-11-01 23:31:33 +08:00
d0cda62703 ci: add freebsd release build (#7344) 2024-11-01 21:37:53 +08:00
ce0b99a510 fix(cloudreve): path not exist when moving/copying files (#7432)
Co-authored-by: 马建军 <1432318228@qq.com>
2024-11-01 21:12:29 +08:00
Mmx
34a148c83d feat(local): thumbnail token bucket smooth migration (#7425)
* feat(local): allow to migrate static token buckets

* improve(local): token bucket migration boundary handling
2024-11-01 20:58:53 +08:00
Mmx
4955d8cec8 ci(docker): support riscv64 and ppc64le (#7426)
* ci(docker): bump cache key of musl library

* build(docker): add new arches to build script

* ci(docker): add new arches to buildx platforms
2024-11-01 20:53:53 +08:00
216e3909f3 fix(115): enforce 20GB file size limit on uploadev (#7447 close #7413)
- Introduce a file size restriction to handle uploads more securely.
- Provide an informative error for uploads that exceed the new limit.
2024-11-01 20:52:19 +08:00
a701432b8b ci: add freebsd to beta release 2024-10-21 00:05:56 +08:00
a2dc45a80b fix(ilanzou): fix upload failure for small files (#7368 close #7250) 2024-10-20 23:53:56 +08:00
48ac23c8de fix(ilanzou): fix infinite loop when getting file list (#7366 close #7357) 2024-10-20 23:53:40 +08:00
2830575490 perf(123pan): change domain of login (#7325)
* Update driver.go

* 1

* Update util.go

* 123新登录接口

* Revert "Update util.go"

This reverts commit a13a58f8a86c7c36d4fd7d91137229a7667f1fb5.

* Update driver.go

* Update util.go

* Update util.go
2024-10-15 19:45:30 +08:00
e8538bd215 feat: add febbox driver (#7304 close #7293) 2024-10-14 22:44:20 +08:00
c3e43ff605 fix(115): use latest appVer for upload (close #7315) 2024-10-12 00:48:54 +08:00
5f19d73fcc fix: Terabox ( close #6961 close #6983 in #7279) 2024-10-04 15:46:10 +08:00
bdf4b52885 feat(offline_download): add transmission (close #4102 in #7232) 2024-09-28 23:15:58 +08:00
6106a2d4cc fix: dynamic update app version (close #7198 in #7220) 2024-09-18 23:30:28 +08:00
b6451451b1 fix: release version (close #7182) 2024-09-17 01:37:14 +08:00
f06d2c0348 fix(115): change ua (#7196 close #7191) 2024-09-17 01:34:47 +08:00
b7ae56b109 ci: delete beta tag before generating changelog 2024-09-14 00:50:24 +08:00
5d9167d676 fix: recover panic on storage init 2024-09-13 23:50:51 +08:00
1b42b9627c fix(google_photo): fix issue copy videos from google photo (#7160 close #7158)
#7158 During copy from google photo to aliyun, it failed consistently with 404 when copying mp4 file with =m37.

Change =m37 to =dv will fix the issue
2024-09-12 19:08:13 +08:00
bb58b94a10 fix(deps): update module github.com/charmbracelet/bubbles to v0.20.0 (#7142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-10 00:02:47 +08:00
ffce61d227 ci: add @ to trigger by comment 2024-09-10 00:02:24 +08:00
0310b70d90 fix(deps): update module golang.org/x/crypto to v0.27.0 (#7147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-10 00:01:51 +08:00
73f0b135b6 ci: split arm and non-arm target on beta release workflow 2024-09-09 00:25:35 +08:00
8316f81e41 ci: update beta tag to newest commit 2024-09-08 23:03:58 +08:00
cdbfda8921 fix(baidu_photo): change download api (#7144 close #7133) 2024-09-08 19:47:11 +08:00
9667832b32 fix(pikpak): fix nil pointer error (#7150) 2024-09-08 19:45:42 +08:00
b36d38f63f chore: go mod tidy 2024-09-08 11:12:53 +08:00
c8317250c1 fix(deps): update golang.org/x/exp digest to e7e105d (#7139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 11:10:07 +08:00
0242f36e1c fix(deps): update module google.golang.org/grpc to v1.66.0 (#7098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 10:58:52 +08:00
40a68bcee6 fix(deps): update module github.com/ncw/swift/v2 to v2.0.3 (#7107)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 10:58:04 +08:00
92713ef5c4 fix(deps): update module github.com/charmbracelet/bubbletea to v1 (#7103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 10:57:04 +08:00
716d33fddd feat(pikpak&pikpak_share): add download address delay detection (#7136)
* feat(pikpak): add download address delay detection

* feat(pikpak_share): add download address delay detection
2024-09-08 10:45:43 +08:00
c9fa3d7cd6 fix: broken file with local proxy (#7132 close #7112)
* fix: local proxy download file damage

* fix: temp dir remove
2024-09-08 10:44:34 +08:00
Mmx
4874c9e43b fix(local): thumbnails oom (#7124 close #7082)
* add my_build.sh

* Fix OOM of thumbnail generation of LoaclDrive by using a task queue to control thread count

* remove my_build.sh

* chore(local): allow ThumbConcurrency set to zero

* revert(local): changes to thumbnail generating functions

* feat(local): implement static token bucket

* feat(local): use static token bucket to limit thumbnails generating concurrent

---------

Co-authored-by: KKJas <75424880+Muione@users.noreply.github.com>
2024-09-03 20:03:30 +08:00
Mmx
34ada81582 fix(webdav): memory leak in HttpServer (#7123 close #7088)
* chore(webdav): fix warnings in HttpServe

* fix(webdav): HttpServe memory leak
2024-09-03 20:02:13 +08:00
ba716ae325 fix(pikpak): error when passing the user_id field (#7117 close #7118) 2024-09-01 23:06:51 +08:00
d4f9c4b6af ci: trigger desktop beta version 2024-08-29 23:03:54 +08:00
b910b8917f fix(deps): update golang.org/x/exp digest to 9b4947d (#7065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-24 22:24:18 +08:00
Mmx
d92744e673 chore(local): decrease mass ffmpeg logs (#7073) 2024-08-24 22:20:20 +08:00
868b0ec25c chore: replace link of zhaoziyuan [skip ci] 2024-08-23 12:27:19 +08:00
e21edf98e2 revert: 34b6785fab 2024-08-21 17:08:03 +00:00
d2514d236f feat(drivers): add kodbox storage (#7059 close #7058)
- kodbox: https://github.com/kalcaddle/kodbox
2024-08-22 00:46:38 +08:00
34b6785fab fix(deps): update module github.com/meilisearch/meilisearch-go to v0.28.0 (#7061)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 00:45:31 +08:00
48f50a2ceb fix(search): BuildIndex concurrency error (#7035) 2024-08-22 00:44:55 +08:00
74887922b4 fix(offline_download): os.create failure while the name of downloaded file is empty (#7041) 2024-08-22 00:44:23 +08:00
bcb24d61ea fix(deps): update module github.com/go-resty/resty/v2 to v2.14.0 (#6981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 00:43:35 +08:00
db1494455d fix(deps): update module github.com/charmbracelet/bubbles to v0.19.0 (#7048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 00:43:11 +08:00
d9a1809313 fix(deps): update module github.com/charmbracelet/lipgloss to v0.13.0 (#7049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 00:42:50 +08:00
0715198c7f chore: fix log format typo (#7056)
Signed-off-by: cuishuang <imcusg@gmail.com>
2024-08-22 00:42:19 +08:00
ef5e192c3b fix(pikpak): webdav upload issue (#7050) 2024-08-22 00:35:52 +08:00
489b28bdf7 fix(pikpak_share): add captcha_token generation function (#7045) 2024-08-22 00:35:14 +08:00
18176c659c ci: add beta tag to newest docker image 2024-08-20 21:36:36 +08:00
4c48a816bf fix(deps): update module github.com/charmbracelet/bubbletea to v0.27.0 (#7025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 23:30:26 +08:00
9af7aaab59 fix(deps): update github.com/city404/v6-public-rpc-proto/go digest to 90f8e24 (#7028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 23:30:08 +08:00
a54a09314f fix(deps): update module github.com/aws/aws-sdk-go to v1.55.5 (#6813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 23:29:45 +08:00
e2fcd73720 fix(deps): update module github.com/go-webauthn/webauthn to v0.11.1 (#6901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 23:29:24 +08:00
e238b90836 fix(pikpak): modify the processing logic of CaptchaToken (#7024) 2024-08-18 23:26:29 +08:00
69e5b66b50 ci: use changelogithub to generate changelog 2024-08-18 13:57:44 +08:00
e8e6d71c41 ci: only one beta release action concurrency [skip ci] 2024-08-18 00:38:27 +08:00
4ba476e25c ci: build beta release 2024-08-18 00:25:16 +08:00
e5fe9ea5f6 ci: set changelog for beta release 2024-08-17 23:03:49 +08:00
e1906c9312 ci: only release on tag with v prefix 2024-08-17 22:08:29 +08:00
51c95ee117 fix: decode body if enable gzip (#7003) 2024-08-15 22:25:53 +08:00
Mmx
1f652e2e7d ci(docker): using docker build args instead of extra dockerfile for ffmpeg (#6989)
* build: using docker build arg to determine install ffmpeg or not

* ci: pass build-args to ffmpeg image build step
2024-08-15 21:48:48 +08:00
8e6c1aa78d fix(pikpak): refresh_token cannot be obtained (#7017) 2024-08-15 21:46:55 +08:00
6bff5b6107 fix(deps): update module golang.org/x/image to v0.19.0 (#6982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 19:35:33 +08:00
Mmx
94937db491 feat(s3): using internal download method in proxy (#6988) 2024-08-14 19:34:48 +08:00
3dc250cc37 feat(115): update qrcode source list (#6996)
* remove mac, linux, window (disabled)
* add alipaymini, wechatmini, qandroid
2024-08-14 19:34:11 +08:00
9560799175 fix(189pc): InvalidSessionKey (#6994 close #6992) 2024-08-14 19:33:15 +08:00
8f3c5b1587 fix(deps): update module github.com/meilisearch/meilisearch-go to v0.27.2 (#6907)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-11 11:40:47 +08:00
285125d06a fix(deps): update module github.com/larksuite/oapi-sdk-go/v3 to v3.3.1 (#6978)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-11 11:40:26 +08:00
a26185fe05 fix(deps): update github.com/xhofe/go-cache digest to b1a7192 (#6939)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-11 11:40:05 +08:00
a7efa3a676 fix(deps): update golang.org/x/exp digest to 0cdaa3a (#6977)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-11 11:39:13 +08:00
d596ef5c38 fix(deps): update module github.com/blevesearch/bleve/v2 to v2.4.2 (#6892)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-11 11:38:56 +08:00
34e34ef564 fix(deps): update module golang.org/x/time to v0.6.0 (#6944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-11 11:38:32 +08:00
8032d0afb6 fix(deps): update module golang.org/x/oauth2 to v0.22.0 (#6943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-10 21:01:41 +08:00
d3bc8993ee fix(deps): update module github.com/dlclark/regexp2 to v1.11.4 (#6958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-10 21:01:05 +08:00
62ed169a39 feat: add support for quark tv driver and uc tv driver (#6959) 2024-08-10 21:00:43 +08:00
979d0cfeee fix(chaoxing): upload to ChaoxingxingGroupCloud failed (#6953)
change the data type on deserializing json
2024-08-10 20:59:49 +08:00
29165d8e60 feat(115): add offline download tool (close #6888 in #6954) 2024-08-10 20:59:07 +08:00
2d77db6bc2 fix(halalcloud): fix the timeout issue when logging in (#6960) 2024-08-10 20:58:10 +08:00
74f8295960 feat: persistant Task (#6925 close #5313) 2024-08-07 12:16:21 +08:00
f2727095d9 fix(thunder_browser): fix space parameter not handled correctly in some cases & update some parameters (#6952) 2024-08-06 22:14:36 +08:00
d4285b7c6c fix(halalcloud): fix some custom fields not taking effect & update appID and appSecret (#6938) 2024-08-04 19:03:24 +08:00
2e4265a778 feat: deleting folders is not allowed (close #6933) 2024-08-04 18:28:35 +08:00
81258d3e8a feat: invalidate token on logout (#6923 close #6792) 2024-08-04 12:32:39 +08:00
a6bead90d7 feat: add support for lenovonas_share driver (#6921) 2024-08-04 12:28:19 +08:00
87caaf2459 fix: out of order when database is not sqlite3 (#6560) 2024-08-03 13:11:09 +08:00
af9c6afd25 feat: update alist-org/gofakes3 to v0.0.7 to support create folder in PutObject (#6880) 2024-07-27 20:06:05 +08:00
8b5727a0aa fix(deps): update golang.org/x/exp digest to 8a7402a (#6801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 14:27:56 +08:00
aeae47c9bf fix(deps): update module github.com/larksuite/oapi-sdk-go/v3 to v3.3.0 (#6812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 20:13:01 +08:00
1aff758688 fix(deps): update github.com/alist-org/times digest to efa0c7d (#6840)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 20:11:12 +08:00
4a42bc5083 fix(lanzou): not find file page param (#6862 close #6857)
* fix(lanzou):not find file page param

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

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

(cherry picked from commit eb24f45771d29a3659e75813734b290d6306cfcf)

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

(cherry picked from commit 3218d7cf2c4e1a8c51fd2414595547fd109a89ac)

---------

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

* 完善PikPak离线下载功能

* 删除多余的代码

* add task cache to avoid too many requests about API

* 优化Status函数

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

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

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

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

and ensures successful mounting of the iLanZou driver.

Closes #6271, Closes #6415

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

Closes #6610, Closes #6333

---------

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

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

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

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

* feat: external view mode

* limit lark targets

* fix: missing package

---------

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

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

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

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

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

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

* chore: simplify logic

* chore: simplify logic

* use internal UA

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

* fix(sftp): fix typo

---------

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

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

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

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

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

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

* Update README_cn.md

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

* doc: 补充readme文档

* fix: 对齐meta信息

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

* perf: merge to s3

* Rename goge.go to doge.go

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

* fix: 定时任务在Drop中Stop

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-03-27 14:22:26 +08:00
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
443 changed files with 35224 additions and 4234 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

View File

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

2
.github/stale.yml vendored
View File

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

View File

@ -20,22 +20,22 @@ jobs:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.20' ]
go-version: [ '1.21' ]
name: auto generate lang.json
runs-on: ${{ matrix.platform }}
steps:
- name: Setup go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout alist
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: alist
- name: Checkout alist-web
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: 'alist-org/alist-web'
ref: main

138
.github/workflows/beta_release.yml vendored Normal file
View File

@ -0,0 +1,138 @@
name: beta release
on:
push:
branches: [ 'main' ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
changelog:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Beta Release Changelog
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create or update ref
id: create-or-update-ref
uses: ovsds/create-or-update-ref-action@v1
with:
ref: tags/beta
sha: ${{ github.sha }}
- name: Delete beta tag
run: git tag -d beta
continue-on-error: true
- name: changelog # or changelogithub@0.12 if ensure the stable result
id: changelog
run: |
git tag -l
npx changelogithub --output CHANGELOG.md
# npx changelogen@latest --output CHANGELOG.md
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
body_path: CHANGELOG.md
files: CHANGELOG.md
prerelease: true
tag_name: beta
release:
needs:
- changelog
strategy:
matrix:
include:
- target: '!(*musl*|*windows-arm64*|*android*|*freebsd*)' # xgo
hash: "md5"
- target: 'linux-!(arm*)-musl*' #musl-not-arm
hash: "md5-linux-musl"
- target: 'linux-arm*-musl*' #musl-arm
hash: "md5-linux-musl-arm"
- target: 'windows-arm64' #win-arm64
hash: "md5-windows-arm64"
- target: 'android-*' #android
hash: "md5-android"
- target: 'freebsd-*' #freebsd
hash: "md5-freebsd"
name: Beta Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Setup web
run: bash build.sh dev web
- name: Build
uses: go-cross/cgo-actions@v1
with:
targets: ${{ matrix.target }}
musl-target-format: $os-$musl-$arch
out-dir: build
x-flags: |
github.com/alist-org/alist/v3/internal/conf.BuiltAt=$built_at
github.com/alist-org/alist/v3/internal/conf.GitAuthor=Xhofe
github.com/alist-org/alist/v3/internal/conf.GitCommit=$git_commit
github.com/alist-org/alist/v3/internal/conf.Version=$tag
github.com/alist-org/alist/v3/internal/conf.WebVersion=dev
- name: Compress
run: |
bash build.sh zip ${{ matrix.hash }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
prerelease: true
tag_name: beta
desktop:
needs:
- release
name: Beta Release Desktop
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
repository: alist-org/desktop-release
ref: main
persist-credentials: false
fetch-depth: 0
- name: Commit
run: |
git config --local user.email "bot@nn.ci"
git config --local user.name "IlaBot"
git commit --allow-empty -m "Trigger build for ${{ github.sha }}"
- name: Push commit
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.MY_TOKEN }}
branch: main
repository: alist-org/desktop-release

View File

@ -15,31 +15,47 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [ '1.20' ]
target:
- darwin-amd64
- darwin-arm64
- windows-amd64
- linux-arm64-musl
- linux-amd64-musl
- windows-arm64
- android-arm64
name: Build
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo snap install zig --classic --beta
docker pull crazymax/xgo:latest
go install github.com/crazy-max/xgo@latest
sudo apt install upx
- uses: benjlevesque/short-sha@v3.0
id: short-sha
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Setup web
run: bash build.sh dev web
- name: Build
run: |
bash build.sh dev
uses: go-cross/cgo-actions@v1
with:
targets: ${{ matrix.target }}
musl-target-format: $os-$musl-$arch
out-dir: build
x-flags: |
github.com/alist-org/alist/v3/internal/conf.BuiltAt=$built_at
github.com/alist-org/alist/v3/internal/conf.GitAuthor=Xhofe
github.com/alist-org/alist/v3/internal/conf.GitCommit=$git_commit
github.com/alist-org/alist/v3/internal/conf.Version=$tag
github.com/alist-org/alist/v3/internal/conf.WebVersion=dev
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: alist
path: dist
name: alist_${{ env.SHA }}_${{ matrix.target }}
path: build/*

View File

@ -1,69 +0,0 @@
name: build_docker
on:
push:
branches: [ main ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build_docker:
name: Build docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: xhofe/alist
- name: Replace release with dev
run: |
sed -i 's/release/dev/g' Dockerfile
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
build_docker_with_aria2:
needs: build_docker
name: Build docker with aria2
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
repository: alist-org/with_aria2
ref: main
persist-credentials: false
fetch-depth: 0
- name: Commit
run: |
git config --local user.email "bot@nn.ci"
git config --local user.name "IlaBot"
git commit --allow-empty -m "Trigger build for ${{ github.sha }}"
- name: Push commit
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.MY_TOKEN }}
branch: main
repository: alist-org/with_aria2

View File

@ -3,7 +3,7 @@ name: auto changelog
on:
push:
tags:
- '*'
- 'v*'
jobs:
changelog:
@ -11,9 +11,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Delete beta tag
run: git tag -d beta
continue-on-error: true
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
env:
GITHUB_TOKEN: ${{secrets.MY_TOKEN}}

View File

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

View File

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

View File

@ -9,10 +9,27 @@ jobs:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.20' ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Prerelease
uses: irongut/EditRelease@v1.2.0
with:
@ -21,12 +38,12 @@ jobs:
prerelease: true
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@ -42,7 +59,7 @@ jobs:
bash build.sh release
- name: Upload assets
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
prerelease: false
@ -53,7 +70,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: alist-org/desktop-release
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@v2
with:
files: build/compress/*

View File

@ -3,66 +3,135 @@ name: release_docker
on:
push:
tags:
- '*'
- 'v*'
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
REGISTRY: 'xhofe/alist'
REGISTRY_USERNAME: 'xhofe'
REGISTRY_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
ARTIFACT_NAME: 'binaries_docker_release'
RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64'
IMAGE_PUSH: ${{ github.event_name == 'push' }}
IMAGE_IS_PROD: ${{ github.ref_type == 'tag' }}
IMAGE_TAGS_BETA: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=ref,event=pr
type=raw,value=beta,enable={{is_default_branch}}
jobs:
release_docker:
name: Release Docker
build_binary:
name: Build Binaries for Docker Release
runs-on: ubuntu-latest
steps:
- 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-v2
- name: Download Musl Library
if: steps.cache-musl.outputs.cache-hit != 'true'
run: bash build.sh prepare docker-multiplatform
- name: Build go binary (beta)
if: env.IMAGE_IS_PROD != 'true'
run: bash build.sh beta docker-multiplatform
- name: Build go binary (release)
if: env.IMAGE_IS_PROD == 'true'
run: bash build.sh release docker-multiplatform
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
overwrite: true
path: |
build/
!build/*.tgz
!build/musl-libs/**
release_docker:
needs: build_binary
name: Release Docker image
runs-on: ubuntu-latest
strategy:
matrix:
image: ["latest", "ffmpeg", "aria2", "aio"]
include:
- image: "latest"
build_arg: ""
tag_favor: ""
- image: "ffmpeg"
build_arg: INSTALL_FFMPEG=true
tag_favor: "suffix=-ffmpeg,onlatest=true"
- image: "aria2"
build_arg: INSTALL_ARIA2=true
tag_favor: "suffix=-aria2,onlatest=true"
- image: "aio"
build_arg: |
INSTALL_FFMPEG=true
INSTALL_ARIA2=true
tag_favor: "suffix=-aio,onlatest=true"
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
path: 'build/'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3
with:
logout: true
username: ${{ env.REGISTRY_USERNAME }}
password: ${{ env.REGISTRY_PASSWORD }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: xhofe/alist
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }}
images: ${{ env.REGISTRY }}
tags: ${{ env.IMAGE_IS_PROD == 'true' && '' || env.IMAGE_TAGS_BETA }}
flavor: |
${{ env.IMAGE_IS_PROD == 'true' && 'latest=true' || '' }}
${{ matrix.tag_favor }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6
with:
context: .
push: true
file: Dockerfile.ci
push: ${{ env.IMAGE_PUSH == 'true' }}
build-args: ${{ matrix.build_arg }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
release_docker_with_aria2:
needs: release_docker
name: Release docker with aria2
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
repository: alist-org/with_aria2
ref: main
persist-credentials: false
fetch-depth: 0
- name: Add tag
run: |
git config --local user.email "bot@nn.ci"
git config --local user.name "IlaBot"
git tag -a ${{ github.ref_name }} -m "release ${{ github.ref_name }}"
- name: Push tags
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.MY_TOKEN }}
branch: main
repository: alist-org/with_aria2
platforms: ${{ env.RELEASE_PLATFORMS }}

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

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

View File

@ -9,18 +9,18 @@ jobs:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.20' ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@ -29,6 +29,6 @@ jobs:
bash build.sh release linux_musl
- name: Upload assets
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: build/compress/*

View File

@ -9,18 +9,18 @@ jobs:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.20' ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@ -29,6 +29,6 @@ jobs:
bash build.sh release linux_musl_arm
- name: Upload assets
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: build/compress/*

1
.gitignore vendored
View File

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

View File

@ -1,18 +1,43 @@
FROM alpine:3.18 as builder
FROM alpine:edge as builder
LABEL stage=go-builder
WORKDIR /app/
RUN apk add --no-cache bash curl gcc git go musl-dev
COPY go.mod go.sum ./
RUN go mod download
COPY ./ ./
RUN apk add --no-cache bash curl gcc git go musl-dev; \
bash build.sh release docker
RUN bash build.sh release docker
FROM alpine:3.18
FROM alpine:edge
ARG INSTALL_FFMPEG=false
ARG INSTALL_ARIA2=false
LABEL MAINTAINER="i@nn.ci"
VOLUME /opt/alist/data/
WORKDIR /opt/alist/
RUN apk update && \
apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates su-exec tzdata; \
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \
[ "$INSTALL_ARIA2" = "true" ] && apk add --no-cache curl aria2 && \
mkdir -p /opt/aria2/.aria2 && \
wget https://github.com/P3TERX/aria2.conf/archive/refs/heads/master.tar.gz -O /tmp/aria-conf.tar.gz && \
tar -zxvf /tmp/aria-conf.tar.gz -C /opt/aria2/.aria2 --strip-components=1 && rm -f /tmp/aria-conf.tar.gz && \
sed -i 's|rpc-secret|#rpc-secret|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \
touch /opt/aria2/.aria2/aria2.session && \
/opt/aria2/.aria2/tracker.sh ; \
rm -rf /var/cache/apk/*
COPY --from=builder /app/bin/alist ./
COPY entrypoint.sh /entrypoint.sh
RUN apk add --no-cache bash ca-certificates su-exec tzdata; \
chmod +x /entrypoint.sh
ENV PUID=0 PGID=0 UMASK=022
RUN chmod +x /opt/alist/alist && \
chmod +x /entrypoint.sh && /entrypoint.sh version
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
VOLUME /opt/alist/data/
EXPOSE 5244 5245
CMD [ "/entrypoint.sh" ]
CMD [ "/entrypoint.sh" ]

35
Dockerfile.ci Normal file
View File

@ -0,0 +1,35 @@
FROM alpine:edge
ARG TARGETPLATFORM
ARG INSTALL_FFMPEG=false
ARG INSTALL_ARIA2=false
LABEL MAINTAINER="i@nn.ci"
WORKDIR /opt/alist/
RUN apk update && \
apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates su-exec tzdata; \
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \
[ "$INSTALL_ARIA2" = "true" ] && apk add --no-cache curl aria2 && \
mkdir -p /opt/aria2/.aria2 && \
wget https://github.com/P3TERX/aria2.conf/archive/refs/heads/master.tar.gz -O /tmp/aria-conf.tar.gz && \
tar -zxvf /tmp/aria-conf.tar.gz -C /opt/aria2/.aria2 --strip-components=1 && rm -f /tmp/aria-conf.tar.gz && \
sed -i 's|rpc-secret|#rpc-secret|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \
touch /opt/aria2/.aria2/aria2.session && \
/opt/aria2/.aria2/tracker.sh ; \
rm -rf /var/cache/apk/*
COPY /build/${TARGETPLATFORM}/alist ./
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /opt/alist/alist && \
chmod +x /entrypoint.sh && /entrypoint.sh version
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
VOLUME /opt/alist/data/
EXPOSE 5244 5245
CMD [ "/entrypoint.sh" ]

View File

@ -1,17 +1,17 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
@ -19,13 +19,13 @@
</a>
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<a href="https://github.com/alist-org/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://hub.docker.com/r/xhofe/alist">
@ -39,13 +39,13 @@
---
English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
## Features
- [x] Multiple storage
- [x] Multiple storages
- [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] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com/)
@ -58,7 +58,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
- [x] [Mediatrack](https://www.mediatrack.cn/)
- [x] [139yun](https://yun.139.com/) (Personal, Family)
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com/)
- [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Terabox](https://www.terabox.com/main)
@ -66,7 +66,8 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.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] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com/)
@ -74,6 +75,9 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] [115](https://115.com/)
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] [FeijiPan](https://www.feijipan.com/)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
@ -86,7 +90,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] Protected routes (password protection and authentication)
- [x] WebDav (see https://alist.nn.ci/guide/webdav.html for details)
- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist)
- [x] Cloudflare workers proxy
- [x] Cloudflare Workers proxy
- [x] File/Folder package download
- [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy
- [x] Offline download
@ -95,7 +99,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
## Document
<https://alist.nn.ci/>
<https://alistgo.com/>
## Demo
@ -103,7 +107,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
## Discussion
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports and feature request only.**
Please go to our [discussion forum](https://github.com/alist-org/alist/discussions) for general questions, **issues are for bug reports and feature requests only.**
## Sponsor
@ -112,22 +116,22 @@ https://alist.nn.ci/guide/sponsor.html
### Special sponsors
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (sponsored Chinese API server)
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.pw/)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
## Contributors
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
The `AList` is open-source software licensed under the AGPL-3.0 license.
## 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 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;
@ -135,4 +139,4 @@ The `AList` is open-source software licensed under the AGPL-3.0 license.
---
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
> [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)

View File

@ -1,17 +1,17 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
@ -19,13 +19,13 @@
</a>
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<a href="https://github.com/alist-org/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://hub.docker.com/r/xhofe/alist">
@ -45,7 +45,7 @@
- [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] [天翼云盘](https://cloud.189.cn) (个人云, 家庭云)
- [x] [GoogleDrive](https://drive.google.com/)
@ -58,14 +58,15 @@
- [x] WebDav(支持无API的OneDrive/SharePoint)
- [x] Teambition[中国](https://www.teambition.com/ )[国际](https://us.teambition.com/ )
- [x] [分秒帧](https://www.mediatrack.cn/)
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云,共享群组)
- [x] [Yandex.Disk](https://disk.yandex.com/)
- [x] [百度网盘](http://pan.baidu.com/)
- [x] [UC网盘](https://drive.uc.cn)
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷网盘](https://pan.xunlei.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] [Mega.nz](https://mega.nz)
- [x] [一刻相册](https://photo.baidu.com/)
@ -73,6 +74,8 @@
- [x] [115](https://115.com/)
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] [飞机盘](https://www.feijipan.com/)
- [x] [多吉云](https://www.dogecloud.com/product/oss)
- [x] 部署方便,开箱即用
- [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览
@ -102,7 +105,7 @@
## 讨论
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) **issue仅针对错误报告和功能请求。**
一般问题请到[讨论论坛](https://github.com/alist-org/alist/discussions) **issue仅针对错误报告和功能请求。**
## 赞助
@ -110,15 +113,15 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
### 特别赞助
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (国内API服务器赞助)
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.pw/)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器iPhoneiPadMacApple TV全平台支持。
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
## 贡献者
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)
## 许可
@ -133,4 +136,4 @@ Thanks goes to these wonderful people:
---
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)

View File

@ -1,17 +1,17 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
@ -19,13 +19,13 @@
</a>
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<a href="https://github.com/alist-org/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<a href="https://github.com/alist-org/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://hub.docker.com/r/xhofe/alist">
@ -45,7 +45,7 @@
- [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] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com/)
@ -58,7 +58,7 @@
- [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
- [x] [Mediatrack](https://www.mediatrack.cn/)
- [x] [139yun](https://yun.139.com/) (Personal, Family)
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com/)
- [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Terabox](https://www.terabox.com/main)
@ -66,7 +66,8 @@
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.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] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com/)
@ -74,6 +75,8 @@
- [x] [115](https://115.com/)
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] [FeijiPan](https://www.feijipan.com/)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [x] デプロイが簡単で、すぐに使える
- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)
- [x] ギャラリーモードでの画像プレビュー
@ -103,7 +106,7 @@
## ディスカッション
一般的なご質問は[ディスカッションフォーラム](https://github.com/Xhofe/alist/discussions)をご利用ください。**問題はバグレポートと機能リクエストのみです。**
一般的なご質問は[ディスカッションフォーラム](https://github.com/alist-org/alist/discussions)をご利用ください。**問題はバグレポートと機能リクエストのみです。**
## スポンサー
@ -112,15 +115,15 @@ https://alist.nn.ci/guide/sponsor.html
### スペシャルスポンサー
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (sponsored Chinese API server)
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.pw/)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
## コントリビューター
これらの素晴らしい人々に感謝します:
[![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)
## ライセンス
@ -135,4 +138,4 @@ https://alist.nn.ci/guide/sponsor.html
---
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)

142
build.sh
View File

@ -1,13 +1,16 @@
appName="alist"
builtAt="$(date +'%F %T %z')"
goVersion=$(go version | sed 's/go version //')
gitAuthor="Xhofe <i@nn.ci>"
gitCommit=$(git log --pretty=format:"%h" -1)
if [ "$1" = "dev" ]; then
version="dev"
webVersion="dev"
elif [ "$1" = "beta" ]; then
version="beta"
webVersion="dev"
else
git tag -d beta
version=$(git describe --abbrev=0 --tags)
webVersion=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
fi
@ -18,7 +21,6 @@ echo "frontend version: $webVersion"
ldflags="\
-w -s \
-X 'github.com/alist-org/alist/v3/internal/conf.BuiltAt=$builtAt' \
-X 'github.com/alist-org/alist/v3/internal/conf.GoVersion=$goVersion' \
-X 'github.com/alist-org/alist/v3/internal/conf.GitAuthor=$gitAuthor' \
-X 'github.com/alist-org/alist/v3/internal/conf.GitCommit=$gitCommit' \
-X 'github.com/alist-org/alist/v3/internal/conf.Version=$version' \
@ -49,6 +51,7 @@ BuildWinArm64() {
export GOARCH=arm64
export CC=$(pwd)/wrapper/zcc-arm64
export CXX=$(pwd)/wrapper/zcxx-arm64
export CGO_ENABLED=1
go build -o "$1" -ldflags="$ldflags" -tags=jsoniter .
}
@ -75,7 +78,7 @@ BuildDev() {
export CGO_ENABLED=1
go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
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
cd dist
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
@ -88,6 +91,57 @@ BuildDocker() {
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 riscv64-linux-musl-cross powerpc64le-linux-musl-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() {
go mod download
# run PrepareBuildDockerMusl before build
export PATH=$PATH:$PWD/build/musl-libs/bin
docker_lflags="--extldflags '-static -fpic' $ldflags"
export CGO_ENABLED=1
OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-s390x linux-riscv64 linux-ppc64le)
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc s390x-linux-musl-gcc riscv64-linux-musl-gcc powerpc64le-linux-musl-gcc)
for i in "${!OS_ARCHES[@]}"; do
os_arch=${OS_ARCHES[$i]}
cgo_cc=${CGO_ARGS[$i]}
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() {
rm -rf .git/
mkdir -p "build"
@ -159,6 +213,50 @@ BuildReleaseLinuxMuslArm() {
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
}
BuildReleaseFreeBSD() {
rm -rf .git/
mkdir -p "build/freebsd"
OS_ARCHES=(amd64 arm64 i386)
GO_ARCHES=(amd64 arm64 386)
CGO_ARGS=(x86_64-unknown-freebsd14.1 aarch64-unknown-freebsd14.1 i386-unknown-freebsd14.1)
for i in "${!OS_ARCHES[@]}"; do
os_arch=${OS_ARCHES[$i]}
cgo_cc="clang --target=${CGO_ARGS[$i]} --sysroot=/opt/freebsd/${os_arch}"
echo building for freebsd-${os_arch}
sudo mkdir -p "/opt/freebsd/${os_arch}"
wget -q https://download.freebsd.org/releases/${os_arch}/14.1-RELEASE/base.txz
sudo tar -xf ./base.txz -C /opt/freebsd/${os_arch}
rm base.txz
export GOOS=freebsd
export GOARCH=${GO_ARCHES[$i]}
export CC=${cgo_cc}
export CGO_ENABLED=1
export CGO_LDFLAGS="-fuse-ld=lld"
go build -o ./build/$appName-freebsd-$os_arch -ldflags="$ldflags" -tags=jsoniter .
done
}
MakeRelease() {
cd build
mkdir compress
@ -166,12 +264,22 @@ MakeRelease() {
cp "$i" alist
tar -czvf compress/"$i".tar.gz 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
for i in $(find . -type f -name "$appName-darwin-*"); do
cp "$i" alist
tar -czvf compress/"$i".tar.gz alist
rm -f alist
done
for i in $(find . -type f -name "$appName-freebsd-*"); do
cp "$i" alist
tar -czvf compress/"$i".tar.gz alist
rm -f alist
done
for i in $(find . -type f -name "$appName-windows-*"); do
cp "$i" alist.exe
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip alist.exe
@ -187,23 +295,47 @@ if [ "$1" = "dev" ]; then
FetchWebDev
if [ "$2" = "docker" ]; then
BuildDocker
elif [ "$2" = "docker-multiplatform" ]; then
BuildDockerMultiplatform
elif [ "$2" = "web" ]; then
echo "web only"
else
BuildDev
fi
elif [ "$1" = "release" ]; then
FetchWebRelease
elif [ "$1" = "release" -o "$1" = "beta" ]; then
if [ "$1" = "beta" ]; then
FetchWebDev
else
FetchWebRelease
fi
if [ "$2" = "docker" ]; then
BuildDocker
elif [ "$2" = "docker-multiplatform" ]; then
BuildDockerMultiplatform
elif [ "$2" = "linux_musl_arm" ]; then
BuildReleaseLinuxMuslArm
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"
elif [ "$2" = "freebsd" ]; then
BuildReleaseFreeBSD
MakeRelease "md5-freebsd.txt"
elif [ "$2" = "web" ]; then
echo "web only"
else
BuildRelease
MakeRelease "md5.txt"
fi
elif [ "$1" = "prepare" ]; then
if [ "$2" = "docker-multiplatform" ]; then
PrepareBuildDockerMusl
fi
elif [ "$1" = "zip" ]; then
MakeRelease "$2".txt
else
echo -e "Parameter error"
fi

View File

@ -17,7 +17,9 @@ func Init() {
bootstrap.Log()
bootstrap.InitDB()
data.InitData()
bootstrap.InitStreamLimit()
bootstrap.InitIndex()
bootstrap.InitUpgradePatch()
}
func Release() {

54
cmd/kill.go Normal file
View File

@ -0,0 +1,54 @@
package cmd
import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"os"
)
// KillCmd represents the kill command
var KillCmd = &cobra.Command{
Use: "kill",
Short: "Force kill alist server process by daemon/pid file",
Run: func(cmd *cobra.Command, args []string) {
kill()
},
}
func kill() {
initDaemon()
if pid == -1 {
log.Info("Seems not have been started. Try use `alist start` to start server.")
return
}
process, err := os.FindProcess(pid)
if err != nil {
log.Errorf("failed to find process by pid: %d, reason: %v", pid, process)
return
}
err = process.Kill()
if err != nil {
log.Errorf("failed to kill process %d: %v", pid, err)
} else {
log.Info("killed process: ", pid)
}
err = os.Remove(pidFile)
if err != nil {
log.Errorf("failed to remove pid file")
}
pid = -1
}
func init() {
RootCmd.AddCommand(KillCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// stopCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// stopCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -12,6 +12,7 @@ import (
"strings"
_ "github.com/alist-org/alist/v3/drivers"
"github.com/alist-org/alist/v3/internal/bootstrap"
"github.com/alist-org/alist/v3/internal/bootstrap/data"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/op"
@ -137,9 +138,10 @@ var LangCmd = &cobra.Command{
Use: "lang",
Short: "Generate language json file",
Run: func(cmd *cobra.Command, args []string) {
bootstrap.InitConfig()
err := os.MkdirAll("lang", 0777)
if err != nil {
utils.Log.Fatal("failed create folder: %s", err.Error())
utils.Log.Fatalf("failed create folder: %s", err.Error())
}
generateDriversJson()
generateSettingsJson()

View File

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

View File

@ -2,7 +2,11 @@ package cmd
import (
"context"
"errors"
"fmt"
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
"github.com/KirCute/sftpd-alist"
"github.com/alist-org/alist/v3/internal/fs"
"net"
"net/http"
"os"
@ -13,7 +17,6 @@ import (
"time"
"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/conf"
"github.com/alist-org/alist/v3/pkg/utils"
@ -35,9 +38,9 @@ the address is defined in config file`,
utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
}
bootstrap.InitAria2()
bootstrap.InitQbittorrent()
bootstrap.InitOfflineDownloadTools()
bootstrap.LoadStorages()
bootstrap.InitTaskManager()
if !flags.Debug && !flags.Dev {
gin.SetMode(gin.ReleaseMode)
}
@ -51,7 +54,7 @@ the address is defined in config file`,
httpSrv = &http.Server{Addr: httpBase, Handler: r}
go func() {
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())
}
}()
@ -62,7 +65,7 @@ the address is defined in config file`,
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
go func() {
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())
}
}()
@ -86,11 +89,68 @@ the address is defined in config file`,
}
}
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())
}
}()
}
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())
}
}()
}
var ftpDriver *server.FtpMainDriver
var ftpServer *ftpserver.FtpServer
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable {
var err error
ftpDriver, err = server.NewMainDriver()
if err != nil {
utils.Log.Fatalf("failed to start ftp driver: %s", err.Error())
} else {
utils.Log.Infof("start ftp server on %s", conf.Conf.FTP.Listen)
go func() {
ftpServer = ftpserver.NewFtpServer(ftpDriver)
err = ftpServer.ListenAndServe()
if err != nil {
utils.Log.Fatalf("problem ftp server listening: %s", err.Error())
}
}()
}
}
var sftpDriver *server.SftpDriver
var sftpServer *sftpd.SftpServer
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable {
var err error
sftpDriver, err = server.NewSftpDriver()
if err != nil {
utils.Log.Fatalf("failed to start sftp driver: %s", err.Error())
} else {
utils.Log.Infof("start sftp server on %s", conf.Conf.SFTP.Listen)
go func() {
sftpServer = sftpd.NewSftpServer(sftpDriver)
err = sftpServer.RunServer()
if err != nil {
utils.Log.Fatalf("problem sftp server listening: %s", err.Error())
}
}()
}
}
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 1 second.
quit := make(chan os.Signal, 1)
@ -100,6 +160,7 @@ the address is defined in config file`,
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
utils.Log.Println("Shutdown server...")
fs.ArchiveContentUploadTaskManager.RemoveAll()
Release()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
@ -131,6 +192,25 @@ the address is defined in config file`,
}
}()
}
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable && ftpServer != nil && ftpDriver != nil {
wg.Add(1)
go func() {
defer wg.Done()
ftpDriver.Stop()
if err := ftpServer.Stop(); err != nil {
utils.Log.Fatal("FTP server shutdown err: ", err)
}
}()
}
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable && sftpServer != nil && sftpDriver != nil {
wg.Add(1)
go func() {
defer wg.Done()
if err := sftpServer.Close(); err != nil {
utils.Log.Fatal("SFTP server shutdown err: ", err)
}
}()
}
wg.Wait()
utils.Log.Println("Server exit")
},

View File

@ -1,10 +1,10 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
//go:build !windows
package cmd
import (
"os"
"syscall"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -30,11 +30,11 @@ func stop() {
log.Errorf("failed to find process by pid: %d, reason: %v", pid, process)
return
}
err = process.Kill()
err = process.Signal(syscall.SIGTERM)
if err != nil {
log.Errorf("failed to kill process %d: %v", pid, err)
log.Errorf("failed to terminate process %d: %v", pid, err)
} else {
log.Info("killed process: ", pid)
log.Info("terminated process: ", pid)
}
err = os.Remove(pidFile)
if err != nil {

34
cmd/stop_windows.go Normal file
View File

@ -0,0 +1,34 @@
//go:build windows
package cmd
import (
"github.com/spf13/cobra"
)
// StopCmd represents the stop command
var StopCmd = &cobra.Command{
Use: "stop",
Short: "Same as the kill command",
Run: func(cmd *cobra.Command, args []string) {
stop()
},
}
func stop() {
kill()
}
func init() {
RootCmd.AddCommand(StopCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// stopCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// stopCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -6,6 +6,7 @@ package cmd
import (
"fmt"
"os"
"runtime"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/spf13/cobra"
@ -16,14 +17,15 @@ var VersionCmd = &cobra.Command{
Use: "version",
Short: "Show current version of AList",
Run: func(cmd *cobra.Command, args []string) {
goVersion := fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)
fmt.Printf(`Built At: %s
Go Version: %s
Author: %s
Commit ID: %s
Version: %s
WebVersion: %s
`,
conf.BuiltAt, conf.GoVersion, conf.GitAuthor, conf.GitCommit, conf.Version, conf.WebVersion)
`, conf.BuiltAt, goVersion, conf.GitAuthor, conf.GitCommit, conf.Version, conf.WebVersion)
os.Exit(0)
},
}

43
drivers/115/appver.go Normal file
View File

@ -0,0 +1,43 @@
package _115
import (
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/drivers/base"
log "github.com/sirupsen/logrus"
)
var (
md5Salt = "Qclm8MGWUv59TnrR0XPg"
appVer = "27.0.5.7"
)
func (d *Pan115) getAppVersion() ([]driver115.AppVersion, error) {
result := driver115.VersionResp{}
resp, err := base.RestyClient.R().Get(driver115.ApiGetVersion)
err = driver115.CheckErr(err, &result, resp)
if err != nil {
return nil, err
}
return result.Data.GetAppVersions(), nil
}
func (d *Pan115) getAppVer() string {
// todo add some cache
vers, err := d.getAppVersion()
if err != nil {
log.Warnf("[115] get app version failed: %v", err)
return appVer
}
for _, ver := range vers {
if ver.AppName == "win" {
return ver.Version
}
}
return appVer
}
func (d *Pan115) initAppVer() {
appVer = d.getAppVer()
}

View File

@ -2,19 +2,24 @@ package _115
import (
"context"
"strings"
"sync"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/driver"
"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/pkg/errors"
"strings"
"golang.org/x/time/rate"
)
type Pan115 struct {
model.Storage
Addition
client *driver115.Pan115Client
client *driver115.Pan115Client
limiter *rate.Limiter
appVerOnce sync.Once
}
func (d *Pan115) Config() driver.Config {
@ -26,14 +31,28 @@ func (d *Pan115) GetAddition() driver.Additional {
}
func (d *Pan115) Init(ctx context.Context) error {
d.appVerOnce.Do(d.initAppVer)
if d.LimitRate > 0 {
d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
}
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 {
return nil
}
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())
if err != nil && !errors.Is(err, driver115.ErrNotExist) {
return nil, err
@ -44,8 +63,12 @@ func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
}
func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
downloadInfo, err := d.client.
DownloadWithUA(file.(*FileObj).PickCode, driver115.UA115Browser)
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
userAgent := args.Header.Get("User-Agent")
downloadInfo, err := d.
DownloadWithUA(file.(*FileObj).PickCode, userAgent)
if err != nil {
return nil, err
}
@ -56,40 +79,91 @@ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
return link, nil
}
func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if _, err := d.client.Mkdir(parentDir.GetID(), dirName); err != nil {
return err
func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
return nil
result := driver115.MkdirResp{}
form := map[string]string{
"pid": parentDir.GetID(),
"cname": dirName,
}
req := d.client.NewRequest().
SetFormData(form).
SetResult(&result).
ForceContentType("application/json;charset=UTF-8")
resp, err := req.Post(driver115.ApiDirAdd)
err = driver115.CheckErr(err, &result, resp)
if err != nil {
return nil, err
}
f, err := d.getNewFile(result.FileID)
if err != nil {
return nil, nil
}
return f, nil
}
func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return d.client.Move(dstDir.GetID(), srcObj.GetID())
func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
if err := d.client.Move(dstDir.GetID(), srcObj.GetID()); err != nil {
return nil, err
}
f, err := d.getNewFile(srcObj.GetID())
if err != nil {
return nil, nil
}
return f, nil
}
func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return d.client.Rename(srcObj.GetID(), newName)
func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
if err := d.client.Rename(srcObj.GetID(), newName); err != nil {
return nil, err
}
f, err := d.getNewFile((srcObj.GetID()))
if err != nil {
return nil, nil
}
return f, nil
}
func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
if err := d.WaitLimit(ctx); err != nil {
return err
}
return d.client.Copy(dstDir.GetID(), srcObj.GetID())
}
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())
}
func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
var (
fastInfo *driver115.UploadInitResp
dirID = dstDir.GetID()
)
if ok, err := d.client.UploadAvailable(); err != nil || !ok {
return err
return nil, err
}
if stream.GetSize() > d.client.UploadMetaInfo.SizeLimit {
return driver115.ErrUploadTooLarge
return nil, driver115.ErrUploadTooLarge
}
//if digest, err = d.client.GetDigestResult(stream); err != nil {
// return err
@ -102,22 +176,22 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
}
reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: hashSize})
if err != nil {
return err
return nil, err
}
preHash, err := utils.HashReader(utils.SHA1, reader)
if err != nil {
return err
return nil, err
}
preHash = strings.ToUpper(preHash)
fullHash := stream.GetHash().GetHash(utils.SHA1)
if len(fullHash) <= 0 {
tmpF, err := stream.CacheFullInTempFile()
if err != nil {
return err
return nil, err
}
fullHash, err = utils.HashFile(utils.SHA1, tmpF)
if err != nil {
return err
return nil, err
}
}
fullHash = strings.ToUpper(fullHash)
@ -126,21 +200,52 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
// note that 115 add timeout for rapid-upload,
// and "sig invalid" err is thrown even when the hash is correct after timeout.
if fastInfo, err = d.rapidUpload(stream.GetSize(), stream.GetName(), dirID, preHash, fullHash, stream); err != nil {
return err
return nil, err
}
if matched, err := fastInfo.Ok(); err != nil {
return err
return nil, err
} else if matched {
return nil
f, err := d.getNewFileByPickCode(fastInfo.PickCode)
if err != nil {
return nil, nil
}
return f, nil
}
var uploadResult *UploadResult
// 闪传失败,上传
if stream.GetSize() <= utils.KB { // 文件大小小于1KB改用普通模式上传
return d.client.UploadByOSS(&fastInfo.UploadOSSParams, stream, dirID)
if stream.GetSize() <= 10*utils.MB { // 文件大小小于10MB改用普通模式上传
if uploadResult, err = d.UploadByOSS(ctx, &fastInfo.UploadOSSParams, stream, dirID, up); err != nil {
return nil, err
}
} else {
// 分片上传
if uploadResult, err = d.UploadByMultipart(ctx, &fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID, up); err != nil {
return nil, err
}
}
// 分片上传
return d.UploadByMultipart(&fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID)
file, err := d.getNewFile(uploadResult.Data.FileID)
if err != nil {
return nil, nil
}
return file, nil
}
func (d *Pan115) OfflineList(ctx context.Context) ([]*driver115.OfflineTask, error) {
resp, err := d.client.ListOfflineTask(0)
if err != nil {
return nil, err
}
return resp.Tasks, nil
}
func (d *Pan115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {
return d.client.AddOfflineTaskURIs(uris, dstDir.GetID(), driver115.WithAppVer(appVer))
}
func (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, deleteFiles bool) error {
return d.client.DeleteOfflineTasks(hashes, deleteFiles)
}
var _ driver.Driver = (*Pan115)(nil)

View File

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

View File

@ -1,10 +1,11 @@
package _115
import (
"time"
"github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"time"
)
var _ model.Obj = (*FileObj)(nil)
@ -20,3 +21,18 @@ func (f *FileObj) CreateTime() time.Time {
func (f *FileObj) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.SHA1, f.Sha1)
}
type UploadResult struct {
driver.BasicResp
Data struct {
PickCode string `json:"pick_code"`
FileSize int `json:"file_size"`
FileID string `json:"file_id"`
ThumbURL string `json:"thumb_url"`
Sha1 string `json:"sha1"`
Aid int `json:"aid"`
FileName string `json:"file_name"`
Cid string `json:"cid"`
IsVideo int `json:"is_video"`
} `json:"data"`
}

View File

@ -2,51 +2,56 @@ package _115
import (
"bytes"
"context"
"crypto/md5"
"crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/orzogc/fake115uploader/cipher"
"io"
"net/url"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/SheltonZhu/115driver/pkg/driver"
cipher "github.com/SheltonZhu/115driver/pkg/crypto/ec115"
crypto "github.com/SheltonZhu/115driver/pkg/crypto/m115"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/pkg/errors"
)
var UserAgent = driver.UA115Desktop
// var UserAgent = driver115.UA115Browser
func (d *Pan115) login() error {
var err error
opts := []driver.Option{
driver.UA(UserAgent),
func(c *driver.Pan115Client) {
opts := []driver115.Option{
driver115.UA(d.getUA()),
func(c *driver115.Pan115Client) {
c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
},
}
d.client = driver.New(opts...)
cr := &driver.Credential{}
if d.Addition.QRCodeToken != "" {
s := &driver.QRCodeSession{
UID: d.Addition.QRCodeToken,
d.client = driver115.New(opts...)
cr := &driver115.Credential{}
if d.QRCodeToken != "" {
s := &driver115.QRCodeSession{
UID: d.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")
}
d.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
d.Addition.QRCodeToken = ""
} else if d.Addition.Cookie != "" {
if err = cr.FromCookie(d.Addition.Cookie); err != nil {
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
d.QRCodeToken = ""
} else if d.Cookie != "" {
if err = cr.FromCookie(d.Cookie); err != nil {
return errors.Wrap(err, "failed to login by cookies")
}
d.client.ImportCredential(cr)
@ -59,9 +64,9 @@ func (d *Pan115) login() error {
func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
res := make([]FileObj, 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, driver115.WithMultiUrls())
if err != nil {
return nil, err
}
@ -71,9 +76,98 @@ func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
return res, nil
}
const (
appVer = "2.0.3.6"
)
func (d *Pan115) getNewFile(fileId string) (*FileObj, error) {
file, err := d.client.GetFile(fileId)
if err != nil {
return nil, err
}
return &FileObj{*file}, nil
}
func (d *Pan115) getNewFileByPickCode(pickCode string) (*FileObj, error) {
result := driver115.GetFileInfoResponse{}
req := d.client.NewRequest().
SetQueryParam("pick_code", pickCode).
ForceContentType("application/json;charset=UTF-8").
SetResult(&result)
resp, err := req.Get(driver115.ApiFileInfo)
if err := driver115.CheckErr(err, &result, resp); err != nil {
return nil, err
}
if len(result.Files) == 0 {
return nil, errors.New("not get file info")
}
fileInfo := result.Files[0]
f := &FileObj{}
f.From(fileInfo)
return f, nil
}
func (d *Pan115) getUA() string {
return fmt.Sprintf("Mozilla/5.0 115Browser/%s", appVer)
}
func (d *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
key := crypto.GenerateKey()
result := driver115.DownloadResp{}
params, err := utils.Json.Marshal(map[string]string{"pick_code": 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.AndroidApiDownloadGetUrl, driver115.Now().String())
req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cookie", d.Cookie)
req.Header.Set("User-Agent", ua)
resp, err := d.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
}
b, err := crypto.Decode(string(result.EncodedData), key)
if err != nil {
return nil, err
}
downloadInfo := struct {
Url string `json:"url"`
}{}
if err := utils.Json.Unmarshal(b, &downloadInfo); err != nil {
return nil, err
}
info := &driver115.DownloadInfo{}
info.PickCode = pickCode
info.Header = resp.Request.Header
info.Url.Url = downloadInfo.Url
return info, nil
}
func (c *Pan115) GenerateToken(fileID, preID, timeStamp, fileSize, signKey, signVal string) string {
userID := strconv.FormatInt(c.client.UserID, 10)
userIDMd5 := md5.Sum([]byte(userID))
tokenMd5 := md5.Sum([]byte(md5Salt + fileID + fileSize + signKey + signVal + userID + timeStamp + hex.EncodeToString(userIDMd5[:]) + appVer))
return hex.EncodeToString(tokenMd5[:])
}
func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) {
var (
@ -104,7 +198,7 @@ func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID stri
signKey, signVal := "", ""
for retry := true; retry; {
t := driver115.Now()
t := driver115.NowMilli()
if encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil {
return nil, err
@ -115,7 +209,7 @@ func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID stri
}
form.Set("t", t.String())
form.Set("token", d.client.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))
form.Set("token", d.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))
if signKey != "" && signVal != "" {
form.Set("sign_key", signKey)
form.Set("sign_val", signVal)
@ -168,6 +262,9 @@ func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result stri
length := end - start + 1
reader, err := stream.RangeRead(http_range.Range{Start: start, Length: length})
if err != nil {
return "", err
}
hashStr, err := utils.HashReader(utils.SHA1, reader)
if err != nil {
return "", err
@ -176,8 +273,43 @@ func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result stri
return
}
// UploadByOSS use aliyun sdk to upload
func (c *Pan115) UploadByOSS(ctx context.Context, params *driver115.UploadOSSParams, s model.FileStreamer, dirID string, up driver.UpdateProgress) (*UploadResult, error) {
ossToken, err := c.client.GetOSSToken()
if err != nil {
return nil, err
}
ossClient, err := oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)
if err != nil {
return nil, err
}
bucket, err := ossClient.Bucket(params.Bucket)
if err != nil {
return nil, err
}
var bodyBytes []byte
r := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
Reader: s,
UpdateProgress: up,
})
if err = bucket.PutObject(params.Object, r, append(
driver115.OssOption(params, ossToken),
oss.CallbackResult(&bodyBytes),
)...); err != nil {
return nil, err
}
var uploadResult UploadResult
if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
return nil, err
}
return &uploadResult, uploadResult.Err(string(bodyBytes))
}
// UploadByMultipart upload by mutipart blocks
func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize int64, stream model.FileStreamer, dirID string, opts ...driver115.UploadMultipartOption) error {
func (d *Pan115) UploadByMultipart(ctx context.Context, params *driver115.UploadOSSParams, fileSize int64, s model.FileStreamer,
dirID string, up driver.UpdateProgress, opts ...driver115.UploadMultipartOption) (*UploadResult, error) {
var (
chunks []oss.FileChunk
parts []oss.UploadPart
@ -185,12 +317,13 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
ossClient *oss.Client
bucket *oss.Bucket
ossToken *driver115.UploadOSSTokenResp
bodyBytes []byte
err error
)
tmpF, err := stream.CacheFullInTempFile()
tmpF, err := s.CacheFullInTempFile()
if err != nil {
return err
return nil, err
}
options := driver115.DefalutUploadMultipartOptions()
@ -199,17 +332,19 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
f(options)
}
}
// oss 启用Sequential必须按顺序上传
options.ThreadsNum = 1
if ossToken, err = d.client.GetOSSToken(); err != nil {
return err
return nil, err
}
if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret); err != nil {
return err
if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
return nil, err
}
if bucket, err = ossClient.Bucket(params.Bucket); err != nil {
return err
return nil, err
}
// ossToken一小时后就会失效所以每50分钟重新获取一次
@ -219,14 +354,15 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
timeout := time.NewTimer(options.Timeout)
if chunks, err = SplitFile(fileSize); err != nil {
return err
return nil, err
}
if imur, err = bucket.InitiateMultipartUpload(params.Object,
oss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken),
oss.UserAgentHeader(driver115.OSSUserAgent),
oss.EnableSha1(), oss.Sequential(),
); err != nil {
return err
return nil, err
}
wg := sync.WaitGroup{}
@ -244,37 +380,41 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
quit <- struct{}{}
}()
completedNum := atomic.Int32{}
// 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)
errCh <- fmt.Errorf("recovered in %v", r)
}
}()
for chunk := range chunksCh {
var part oss.UploadPart // 出现错误就继续尝试共尝试3次
for retry := 0; retry < 3; retry++ {
select {
case <-ctx.Done():
break
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 {
if part, err = bucket.UploadPart(imur, driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(buf)),
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))
errCh <- errors.Wrap(err, fmt.Sprintf("上传 %s 的第%d个分片时出现错误%v", s.GetName(), chunk.Number, err))
} else {
num := completedNum.Add(1)
up(float64(num) * 100.0 / float64(len(chunks)))
}
UploadedPartsCh <- part
}
@ -293,51 +433,38 @@ LOOP:
case <-ticker.C:
// 到时重新获取ossToken
if ossToken, err = d.client.GetOSSToken(); err != nil {
return err
return nil, err
}
case <-quit:
break LOOP
case <-errCh:
return err
return nil, err
case <-timeout.C:
return fmt.Errorf("time out")
return nil, fmt.Errorf("time out")
}
}
// EOF错误是xml的Unmarshal导致的响应其实是json格式所以实际上上传是成功的
if _, err = bucket.CompleteMultipartUpload(imur, parts, driver115.OssOption(params, ossToken)...); err != nil && !errors.Is(err, io.EOF) {
// 当文件名含有 &< 这两个字符之一时响应的xml解析会出现错误实际上上传是成功的
if filename := filepath.Base(stream.GetName()); !strings.ContainsAny(filename, "&<") {
return err
}
// 不知道啥原因oss那边分片上传不计算sha1导致115服务器校验错误
// params.Callback.Callback = strings.ReplaceAll(params.Callback.Callback, "${sha1}", params.SHA1)
if _, err := bucket.CompleteMultipartUpload(imur, parts, append(
driver115.OssOption(params, ossToken),
oss.CallbackResult(&bodyBytes),
)...); err != nil {
return nil, err
}
return d.checkUploadStatus(dirID, params.SHA1)
var uploadResult UploadResult
if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
return nil, err
}
return &uploadResult, uploadResult.Err(string(bodyBytes))
}
func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
for _, chunk := range chunks {
ch <- chunk
}
}
func (d *Pan115) checkUploadStatus(dirID, sha1 string) error {
// 验证上传是否成功
req := d.client.NewRequest().ForceContentType("application/json;charset=UTF-8")
opts := []driver115.GetFileOptions{
driver115.WithOrder(driver115.FileOrderByTime),
driver115.WithShowDirEnable(false),
driver115.WithAsc(false),
driver115.WithLimit(500),
}
fResp, err := driver115.GetFiles(req, dirID, opts...)
if err != nil {
return err
}
for _, fileInfo := range fResp.Files {
if fileInfo.Sha1 == sha1 {
return nil
}
}
return driver115.ErrUploadFailed
}
func SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {
for i := int64(1); i < 10; i++ {
@ -374,8 +501,8 @@ func SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {
}
var chunks []oss.FileChunk
var chunk = oss.FileChunk{}
var chunkN = (int64)(chunkNum)
chunk := oss.FileChunk{}
chunkN := (int64)(chunkNum)
for i := int64(0); i < chunkN; i++ {
chunk.Number = int(i + 1)
chunk.Offset = i * (fileSize / chunkN)
@ -397,13 +524,13 @@ func SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, erro
return nil, errors.New("chunkSize invalid")
}
var chunkN = fileSize / chunkSize
chunkN := fileSize / chunkSize
if chunkN >= 10000 {
return nil, errors.New("Too many parts, please increase part size")
}
var chunks []oss.FileChunk
var chunk = oss.FileChunk{}
chunk := oss.FileChunk{}
for i := int64(0); i < chunkN; i++ {
chunk.Number = int(i + 1)
chunk.Offset = i * chunkSize

299
drivers/115_open/driver.go Normal file
View File

@ -0,0 +1,299 @@
package _115_open
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/cmd/flags"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
sdk "github.com/xhofe/115-sdk-go"
)
type Open115 struct {
model.Storage
Addition
client *sdk.Client
}
func (d *Open115) Config() driver.Config {
return config
}
func (d *Open115) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Open115) Init(ctx context.Context) error {
d.client = sdk.New(sdk.WithRefreshToken(d.Addition.RefreshToken),
sdk.WithAccessToken(d.Addition.AccessToken),
sdk.WithOnRefreshToken(func(s1, s2 string) {
d.Addition.AccessToken = s1
d.Addition.RefreshToken = s2
op.MustSaveDriverStorage(d)
}))
if flags.Debug || flags.Dev {
d.client.SetDebug(true)
}
_, err := d.client.UserInfo(ctx)
if err != nil {
return err
}
return nil
}
func (d *Open115) Drop(ctx context.Context) error {
return nil
}
func (d *Open115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
var res []model.Obj
pageSize := int64(200)
offset := int64(0)
for {
resp, err := d.client.GetFiles(ctx, &sdk.GetFilesReq{
CID: dir.GetID(),
Limit: pageSize,
Offset: offset,
ASC: d.Addition.OrderDirection == "asc",
O: d.Addition.OrderBy,
// Cur: 1,
ShowDir: true,
})
if err != nil {
return nil, err
}
res = append(res, utils.MustSliceConvert(resp.Data, func(src sdk.GetFilesResp_File) model.Obj {
obj := Obj(src)
return &obj
})...)
if len(res) >= int(resp.Count) {
break
}
offset += pageSize
}
return res, nil
}
func (d *Open115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var ua string
if args.Header != nil {
ua = args.Header.Get("User-Agent")
}
if ua == "" {
ua = base.UserAgent
}
obj, ok := file.(*Obj)
if !ok {
return nil, fmt.Errorf("can't convert obj")
}
pc := obj.Pc
resp, err := d.client.DownURL(ctx, pc, ua)
if err != nil {
return nil, err
}
u, ok := resp[obj.GetID()]
if !ok {
return nil, fmt.Errorf("can't get link")
}
return &model.Link{
URL: u.URL.URL,
Header: http.Header{
"User-Agent": []string{ua},
},
}, nil
}
func (d *Open115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
resp, err := d.client.Mkdir(ctx, parentDir.GetID(), dirName)
if err != nil {
return nil, err
}
return &Obj{
Fid: resp.FileID,
Pid: parentDir.GetID(),
Fn: dirName,
Fc: "0",
Upt: time.Now().Unix(),
Uet: time.Now().Unix(),
UpPt: time.Now().Unix(),
}, nil
}
func (d *Open115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
_, err := d.client.Move(ctx, &sdk.MoveReq{
FileIDs: srcObj.GetID(),
ToCid: dstDir.GetID(),
})
if err != nil {
return nil, err
}
return srcObj, nil
}
func (d *Open115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
_, err := d.client.UpdateFile(ctx, &sdk.UpdateFileReq{
FileID: srcObj.GetID(),
FileNma: newName,
})
if err != nil {
return nil, err
}
obj, ok := srcObj.(*Obj)
if ok {
obj.Fn = newName
}
return srcObj, nil
}
func (d *Open115) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
_, err := d.client.Copy(ctx, &sdk.CopyReq{
PID: dstDir.GetID(),
FileID: srcObj.GetID(),
NoDupli: "1",
})
if err != nil {
return nil, err
}
return srcObj, nil
}
func (d *Open115) Remove(ctx context.Context, obj model.Obj) error {
_obj, ok := obj.(*Obj)
if !ok {
return fmt.Errorf("can't convert obj")
}
_, err := d.client.DelFile(ctx, &sdk.DelFileReq{
FileIDs: _obj.GetID(),
ParentID: _obj.Pid,
})
if err != nil {
return err
}
return nil
}
func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
tempF, err := file.CacheFullInTempFile()
if err != nil {
return err
}
// cal full sha1
sha1, err := utils.HashReader(utils.SHA1, tempF)
if err != nil {
return err
}
_, err = tempF.Seek(0, io.SeekStart)
if err != nil {
return err
}
// pre 128k sha1
sha1128k, err := utils.HashReader(utils.SHA1, io.LimitReader(tempF, 128*1024))
if err != nil {
return err
}
_, err = tempF.Seek(0, io.SeekStart)
if err != nil {
return err
}
// 1. Init
resp, err := d.client.UploadInit(ctx, &sdk.UploadInitReq{
FileName: file.GetName(),
FileSize: file.GetSize(),
Target: dstDir.GetID(),
FileID: strings.ToUpper(sha1),
PreID: strings.ToUpper(sha1128k),
})
if err != nil {
return err
}
if resp.Status == 2 {
return nil
}
// 2. two way verify
if utils.SliceContains([]int{6, 7, 8}, resp.Status) {
signCheck := strings.Split(resp.SignCheck, "-") //"sign_check": "2392148-2392298" 取2392148-2392298之间的内容(包含2392148、2392298)的sha1
start, err := strconv.ParseInt(signCheck[0], 10, 64)
if err != nil {
return err
}
end, err := strconv.ParseInt(signCheck[1], 10, 64)
if err != nil {
return err
}
_, err = tempF.Seek(start, io.SeekStart)
if err != nil {
return err
}
signVal, err := utils.HashReader(utils.SHA1, io.LimitReader(tempF, end-start+1))
if err != nil {
return err
}
_, err = tempF.Seek(0, io.SeekStart)
if err != nil {
return err
}
resp, err = d.client.UploadInit(ctx, &sdk.UploadInitReq{
FileName: file.GetName(),
FileSize: file.GetSize(),
Target: dstDir.GetID(),
FileID: strings.ToUpper(sha1),
PreID: strings.ToUpper(sha1128k),
SignKey: resp.SignKey,
SignVal: strings.ToUpper(signVal),
})
if err != nil {
return err
}
if resp.Status == 2 {
return nil
}
}
// 3. get upload token
tokenResp, err := d.client.UploadGetToken(ctx)
if err != nil {
return err
}
// 4. upload
err = d.multpartUpload(ctx, tempF, file, up, tokenResp, resp)
if err != nil {
return err
}
return nil
}
// func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
// // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
// return nil, errs.NotImplement
// }
// func (d *Open115) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
// // TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
// return nil, errs.NotImplement
// }
// func (d *Open115) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
// // TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
// return nil, errs.NotImplement
// }
// func (d *Open115) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
// // TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional
// // a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir
// // return errs.NotImplement to use an internal archive tool
// return nil, errs.NotImplement
// }
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*Open115)(nil)

36
drivers/115_open/meta.go Normal file
View File

@ -0,0 +1,36 @@
package _115_open
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
// Usually one of two
driver.RootID
// define other
RefreshToken string `json:"refresh_token" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"file_name,file_size,user_utime,file_type"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"`
AccessToken string
}
var config = driver.Config{
Name: "115 Open",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "0",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Open115{}
})
}

59
drivers/115_open/types.go Normal file
View File

@ -0,0 +1,59 @@
package _115_open
import (
"time"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
sdk "github.com/xhofe/115-sdk-go"
)
type Obj sdk.GetFilesResp_File
// Thumb implements model.Thumb.
func (o *Obj) Thumb() string {
return o.Thumbnail
}
// CreateTime implements model.Obj.
func (o *Obj) CreateTime() time.Time {
return time.Unix(o.UpPt, 0)
}
// GetHash implements model.Obj.
func (o *Obj) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.SHA1, o.Sha1)
}
// GetID implements model.Obj.
func (o *Obj) GetID() string {
return o.Fid
}
// GetName implements model.Obj.
func (o *Obj) GetName() string {
return o.Fn
}
// GetPath implements model.Obj.
func (o *Obj) GetPath() string {
return ""
}
// GetSize implements model.Obj.
func (o *Obj) GetSize() int64 {
return o.FS
}
// IsDir implements model.Obj.
func (o *Obj) IsDir() bool {
return o.Fc == "0"
}
// ModTime implements model.Obj.
func (o *Obj) ModTime() time.Time {
return time.Unix(o.Upt, 0)
}
var _ model.Obj = (*Obj)(nil)
var _ model.Thumb = (*Obj)(nil)

140
drivers/115_open/upload.go Normal file
View File

@ -0,0 +1,140 @@
package _115_open
import (
"context"
"encoding/base64"
"io"
"time"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/avast/retry-go"
sdk "github.com/xhofe/115-sdk-go"
)
func calPartSize(fileSize int64) int64 {
var partSize int64 = 20 * utils.MB
if fileSize > partSize {
if fileSize > 1*utils.TB { // file Size over 1TB
partSize = 5 * utils.GB // file part size 5GB
} else if fileSize > 768*utils.GB { // over 768GB
partSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part
} else if fileSize > 512*utils.GB { // over 512GB
partSize = 82463373 // ≈ 78.6432MB
} else if fileSize > 384*utils.GB { // over 384GB
partSize = 54975582 // ≈ 52.4288MB
} else if fileSize > 256*utils.GB { // over 256GB
partSize = 41231687 // ≈ 39.3216MB
} else if fileSize > 128*utils.GB { // over 128GB
partSize = 27487791 // ≈ 26.2144MB
}
}
return partSize
}
func (d *Open115) singleUpload(ctx context.Context, tempF model.File, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {
ossClient, err := oss.New(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
if err != nil {
return err
}
bucket, err := ossClient.Bucket(initResp.Bucket)
if err != nil {
return err
}
err = bucket.PutObject(initResp.Object, tempF,
oss.Callback(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.Callback))),
oss.CallbackVar(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.CallbackVar))),
)
return err
}
// type CallbackResult struct {
// State bool `json:"state"`
// Code int `json:"code"`
// Message string `json:"message"`
// Data struct {
// PickCode string `json:"pick_code"`
// FileName string `json:"file_name"`
// FileSize int64 `json:"file_size"`
// FileID string `json:"file_id"`
// ThumbURL string `json:"thumb_url"`
// Sha1 string `json:"sha1"`
// Aid int `json:"aid"`
// Cid string `json:"cid"`
// } `json:"data"`
// }
func (d *Open115) multpartUpload(ctx context.Context, tempF model.File, stream model.FileStreamer, up driver.UpdateProgress, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {
fileSize := stream.GetSize()
chunkSize := calPartSize(fileSize)
ossClient, err := oss.New(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
if err != nil {
return err
}
bucket, err := ossClient.Bucket(initResp.Bucket)
if err != nil {
return err
}
imur, err := bucket.InitiateMultipartUpload(initResp.Object, oss.Sequential())
if err != nil {
return err
}
partNum := (stream.GetSize() + chunkSize - 1) / chunkSize
parts := make([]oss.UploadPart, partNum)
offset := int64(0)
for i := int64(1); i <= partNum; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
partSize := chunkSize
if i == partNum {
partSize = fileSize - (i-1)*chunkSize
}
rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
err = retry.Do(func() error {
_ = rd.Reset()
rateLimitedRd := driver.NewLimitedUploadStream(ctx, rd)
part, err := bucket.UploadPart(imur, rateLimitedRd, partSize, int(i))
if err != nil {
return err
}
parts[i-1] = part
return nil
},
retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Second))
if err != nil {
return err
}
if i == partNum {
offset = fileSize
} else {
offset += partSize
}
up(float64(offset) / float64(fileSize))
}
// callbackRespBytes := make([]byte, 1024)
_, err = bucket.CompleteMultipartUpload(
imur,
parts,
oss.Callback(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.Callback))),
oss.CallbackVar(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.CallbackVar))),
// oss.CallbackResult(&callbackRespBytes),
)
if err != nil {
return err
}
return nil
}

3
drivers/115_open/util.go Normal file
View File

@ -0,0 +1,3 @@
package _115_open
// do others that not defined in Driver interface

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,tv,alipaymini,wechatmini,qandroid" default:"linux" help:"select the QR code device, default linux"`
PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
LimitRate float64 `json:"limit_rate" type:"float" 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: "0",
// 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;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
d.QRCodeToken = ""
} else if d.Cookie != "" {
if err = cr.FromCookie(d.Cookie); err != nil {
return errors.Wrap(err, "failed to login by cookies")
}
d.client.ImportCredential(cr)
} else {
return errors.New("missing cookie or qrcode account")
}
return d.client.LoginCheck()
}

View File

@ -6,6 +6,14 @@ import (
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"sync"
"time"
"golang.org/x/time/rate"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
@ -17,14 +25,12 @@ import (
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"io"
"net/http"
"net/url"
)
type Pan123 struct {
model.Storage
Addition
apiRateLimit sync.Map
}
func (d *Pan123) Config() driver.Config {
@ -36,19 +42,19 @@ func (d *Pan123) GetAddition() driver.Additional {
}
func (d *Pan123) Init(ctx context.Context) error {
_, err := d.request(UserInfo, http.MethodGet, nil, nil)
_, err := d.Request(UserInfo, http.MethodGet, nil, nil)
return err
}
func (d *Pan123) Drop(ctx context.Context) error {
_, _ = d.request(Logout, http.MethodPost, func(req *resty.Request) {
_, _ = d.Request(Logout, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{})
}, nil)
return nil
}
func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles(dir.GetID())
files, err := d.getFiles(ctx, dir.GetID(), dir.GetName())
if err != nil {
return nil, err
}
@ -76,7 +82,8 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
"size": f.Size,
"type": f.Type,
}
resp, err := d.request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
resp, err := d.Request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetHeaders(headers)
}, nil)
if err != nil {
@ -129,7 +136,7 @@ func (d *Pan123) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
"size": 0,
"type": 1,
}
_, err := d.request(Mkdir, http.MethodPost, func(req *resty.Request) {
_, err := d.Request(Mkdir, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
@ -140,7 +147,7 @@ func (d *Pan123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
"fileIdList": []base.Json{{"FileId": srcObj.GetID()}},
"parentFileId": dstDir.GetID(),
}
_, err := d.request(Move, http.MethodPost, func(req *resty.Request) {
_, err := d.Request(Move, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
@ -152,7 +159,7 @@ func (d *Pan123) Rename(ctx context.Context, srcObj model.Obj, newName string) e
"fileId": srcObj.GetID(),
"fileName": newName,
}
_, err := d.request(Rename, http.MethodPost, func(req *resty.Request) {
_, err := d.Request(Rename, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
@ -169,7 +176,7 @@ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
"operation": true,
"fileTrashInfoList": []File{f},
}
_, err := d.request(Trash, http.MethodPost, func(req *resty.Request) {
_, err := d.Request(Trash, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
@ -178,36 +185,39 @@ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
}
}
func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
// const DEFAULT int64 = 10485760
h := md5.New()
// need to calculate md5 of the full content
tempFile, err := stream.CacheFullInTempFile()
if err != nil {
return err
func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
etag := file.GetHash().GetHash(utils.MD5)
if len(etag) < utils.MD5.Width {
// const DEFAULT int64 = 10485760
h := md5.New()
// need to calculate md5 of the full content
tempFile, err := file.CacheFullInTempFile()
if err != nil {
return err
}
defer func() {
_ = tempFile.Close()
}()
if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
etag = hex.EncodeToString(h.Sum(nil))
}
defer func() {
_ = tempFile.Close()
}()
if _, err = io.Copy(h, tempFile); err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
etag := hex.EncodeToString(h.Sum(nil))
data := base.Json{
"driveId": 0,
"duplicate": 2, // 2->覆盖 1->重命名 0->默认
"etag": etag,
"fileName": stream.GetName(),
"fileName": file.GetName(),
"parentFileId": dstDir.GetID(),
"size": stream.GetSize(),
"size": file.GetSize(),
"type": 0,
}
var resp UploadResp
res, err := d.request(UploadRequest, http.MethodPost, func(req *resty.Request) {
res, err := d.Request(UploadRequest, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, &resp)
if err != nil {
@ -218,7 +228,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return nil
}
if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" {
err = d.newUpload(ctx, &resp, stream, tempFile, up)
err = d.newUpload(ctx, &resp, file, up)
return err
} else {
cfg := &aws.Config{
@ -232,17 +242,23 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err
}
uploader := s3manager.NewUploader(s)
if file.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
uploader.PartSize = file.GetSize() / (s3manager.MaxUploadParts - 1)
}
input := &s3manager.UploadInput{
Bucket: &resp.Data.Bucket,
Key: &resp.Data.Key,
Body: tempFile,
Body: driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
Reader: file,
UpdateProgress: up,
}),
}
_, err = uploader.UploadWithContext(ctx, input)
if err != nil {
return err
}
}
if err != nil {
return err
}
_, err = d.request(UploadComplete, http.MethodPost, func(req *resty.Request) {
_, err = d.Request(UploadComplete, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"fileId": resp.Data.FileId,
}).SetContext(ctx)
@ -250,4 +266,12 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err
}
func (d *Pan123) APIRateLimit(ctx context.Context, api string) error {
value, _ := d.apiRateLimit.LoadOrStore(api,
rate.NewLimiter(rate.Every(700*time.Millisecond), 1))
limiter := value.(*rate.Limiter)
return limiter.Wait(ctx)
}
var _ driver.Driver = (*Pan123)(nil)

View File

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

View File

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

View File

@ -25,7 +25,7 @@ func (d *Pan123) getS3PreSignedUrls(ctx context.Context, upReq *UploadResp, star
"StorageNode": upReq.Data.StorageNode,
}
var s3PreSignedUrls S3PreSignedURLs
_, err := d.request(S3PreSignedUrls, http.MethodPost, func(req *resty.Request) {
_, err := d.Request(S3PreSignedUrls, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, &s3PreSignedUrls)
if err != nil {
@ -44,7 +44,7 @@ func (d *Pan123) getS3Auth(ctx context.Context, upReq *UploadResp, start, end in
"uploadId": upReq.Data.UploadId,
}
var s3PreSignedUrls S3PreSignedURLs
_, err := d.request(S3Auth, http.MethodPost, func(req *resty.Request) {
_, err := d.Request(S3Auth, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, &s3PreSignedUrls)
if err != nil {
@ -63,13 +63,13 @@ func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp, file model.F
"key": upReq.Data.Key,
"uploadId": upReq.Data.UploadId,
}
_, err := d.request(UploadCompleteV2, http.MethodPost, func(req *resty.Request) {
_, err := d.Request(UploadCompleteV2, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, nil)
return err
}
func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, reader io.Reader, up driver.UpdateProgress) error {
func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, up driver.UpdateProgress) error {
chunkSize := int64(1024 * 1024 * 16)
// fetch s3 pre signed urls
chunkCount := int(math.Ceil(float64(file.GetSize()) / float64(chunkSize)))
@ -81,6 +81,7 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
batchSize = 10
getS3UploadUrl = d.getS3PreSignedUrls
}
limited := driver.NewLimitedUploadStream(ctx, file)
for i := 1; i <= chunkCount; i += batchSize {
if utils.IsCanceled(ctx) {
return ctx.Err()
@ -103,11 +104,11 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
if j == chunkCount {
curSize = file.GetSize() - (int64(chunkCount)-1)*chunkSize
}
err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.LimitReader(reader, chunkSize), curSize, false, getS3UploadUrl)
err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.LimitReader(limited, chunkSize), curSize, false, getS3UploadUrl)
if err != nil {
return err
}
up(j * 100 / chunkCount)
up(float64(j) * 100 / float64(chunkCount))
}
}
// complete s3 upload

View File

@ -1,15 +1,23 @@
package _123
import (
"context"
"errors"
"fmt"
"hash/crc32"
"math"
"math/rand"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
)
// do others that not defined in Driver interface
@ -18,8 +26,9 @@ const (
Api = "https://www.123pan.com/api"
AApi = "https://www.123pan.com/a/api"
BApi = "https://www.123pan.com/b/api"
MainApi = Api
SignIn = MainApi + "/user/sign_in"
LoginApi = "https://login.123pan.com/api"
MainApi = BApi
SignIn = LoginApi + "/user/sign_in"
Logout = MainApi + "/user/logout"
UserInfo = MainApi + "/user/info"
FileList = MainApi + "/file/list/new"
@ -37,6 +46,104 @@ const (
//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 {
var body base.Json
if utils.IsEmailFormat(d.Username) {
@ -56,9 +163,9 @@ func (d *Pan123) login() error {
SetHeaders(map[string]string{
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"user-agent": "Dart/2.19(dart:io)",
"platform": "android",
"app-version": "36",
"user-agent": "Dart/2.19(dart:io)-alist",
"platform": "web",
"app-version": "3",
//"user-agent": base.UserAgent,
}).
SetBody(body).Post(SignIn)
@ -87,15 +194,17 @@ func (d *Pan123) login() error {
// return &authKey, nil
//}
func (d *Pan123) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
func (d *Pan123) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
isRetry := false
do:
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken,
"user-agent": "Dart/2.19(dart:io)",
"platform": "android",
"app-version": "36",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
"platform": "web",
"app-version": "3",
//"user-agent": base.UserAgent,
})
if callback != nil {
@ -109,51 +218,67 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
// return nil, err
//}
//req.SetQueryParam("auth-key", *authKey)
res, err := req.Execute(method, url)
res, err := req.Execute(method, GetApi(url))
if err != nil {
return nil, err
}
body := res.Body()
code := utils.Json.Get(body, "code").ToInt()
if code != 0 {
if code == 401 {
if !isRetry && code == 401 {
err := d.login()
if err != nil {
return nil, err
}
return d.request(url, method, callback, resp)
isRetry = true
goto do
}
return nil, errors.New(jsoniter.Get(body, "message").ToString())
}
return body, nil
}
func (d *Pan123) getFiles(parentId string) ([]File, error) {
func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]File, error) {
page := 1
total := 0
res := make([]File, 0)
// 2024-02-06 fix concurrency by 123pan
for {
if err := d.APIRateLimit(ctx, FileList); err != nil {
return nil, err
}
var resp Files
query := map[string]string{
"driveId": "0",
"limit": "100",
"next": "0",
"orderBy": d.OrderBy,
"orderDirection": d.OrderDirection,
"parentFileId": parentId,
"trashed": "false",
"Page": strconv.Itoa(page),
"driveId": "0",
"limit": "100",
"next": "0",
"orderBy": "file_id",
"orderDirection": "desc",
"parentFileId": parentId,
"trashed": "false",
"SearchData": "",
"Page": strconv.Itoa(page),
"OnlyLookAbnormalFile": "0",
"event": "homeListFile",
"operateType": "4",
"inDirectSpace": "false",
}
_, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
_res, err := d.Request(FileList, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return nil, err
}
log.Debug(string(_res))
page++
res = append(res, resp.Data.InfoList...)
total = resp.Data.Total
if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
break
}
}
if len(res) != total {
log.Warnf("incorrect file count from remote at %s: expected %d, got %d", name, total, len(res))
}
return res, nil
}

View File

@ -6,7 +6,12 @@ import (
"fmt"
"net/http"
"net/url"
"sync"
"time"
"golang.org/x/time/rate"
_123 "github.com/alist-org/alist/v3/drivers/123"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
@ -19,6 +24,8 @@ import (
type Pan123Share struct {
model.Storage
Addition
apiRateLimit sync.Map
ref *_123.Pan123
}
func (d *Pan123Share) Config() driver.Config {
@ -35,13 +42,23 @@ func (d *Pan123Share) Init(ctx context.Context) error {
return nil
}
func (d *Pan123Share) InitReference(storage driver.Driver) error {
refStorage, ok := storage.(*_123.Pan123)
if ok {
d.ref = refStorage
return nil
}
return fmt.Errorf("ref: storage is not 123Pan")
}
func (d *Pan123Share) Drop(ctx context.Context) error {
d.ref = nil
return nil
}
func (d *Pan123Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
// TODO return the files list, required
files, err := d.getFiles(dir.GetID())
files, err := d.getFiles(ctx, dir.GetID())
if err != nil {
return nil, err
}
@ -146,4 +163,12 @@ func (d *Pan123Share) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
// return nil, errs.NotSupport
//}
func (d *Pan123Share) APIRateLimit(ctx context.Context, api string) error {
value, _ := d.apiRateLimit.LoadOrStore(api,
rate.NewLimiter(rate.Every(700*time.Millisecond), 1))
limiter := value.(*rate.Limiter)
return limiter.Wait(ctx)
}
var _ driver.Driver = (*Pan123Share)(nil)

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -9,17 +9,22 @@ type Addition struct {
//Account string `json:"account" required:"true"`
Authorization string `json:"authorization" type:"text" required:"true"`
driver.RootID
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
CloudID string `json:"cloud_id"`
Type string `json:"type" type:"select" options:"personal_new,family,group,personal" default:"personal_new"`
CloudID string `json:"cloud_id"`
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
ReportRealSize bool `json:"report_real_size" type:"bool" default:"true" help:"Enable to report the real file size during upload"`
}
var config = driver.Config{
Name: "139Yun",
LocalSort: true,
Name: "139Yun",
LocalSort: true,
ProxyRangeOption: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Yun139{}
d := &Yun139{}
d.ProxyRange = true
return d
})
}

View File

@ -1,5 +1,16 @@
package _139
import (
"encoding/xml"
)
const (
MetaPersonal string = "personal"
MetaFamily string = "family"
MetaGroup string = "group"
MetaPersonalNew string = "personal_new"
)
type BaseResp struct {
Success bool `json:"success"`
Code string `json:"code"`
@ -44,6 +55,7 @@ type Content struct {
//ContentDesc string `json:"contentDesc"`
//ContentType int `json:"contentType"`
//ContentOrigin int `json:"contentOrigin"`
CreateTime string `json:"createTime"`
UpdateTime string `json:"updateTime"`
//CommentCount int `json:"commentCount"`
ThumbnailURL string `json:"thumbnailURL"`
@ -131,6 +143,13 @@ type UploadResp struct {
} `json:"data"`
}
type InterLayerUploadResult struct {
XMLName xml.Name `xml:"result"`
Text string `xml:",chardata"`
ResultCode int `xml:"resultCode"`
Msg string `xml:"msg"`
}
type CloudContent struct {
ContentID string `json:"contentID"`
//Modifier string `json:"modifier"`
@ -185,3 +204,92 @@ type QueryContentListResp struct {
RecallContent interface{} `json:"recallContent"`
} `json:"data"`
}
type QueryGroupContentListResp struct {
BaseResp
Data struct {
Result struct {
ResultCode string `json:"resultCode"`
ResultDesc string `json:"resultDesc"`
} `json:"result"`
GetGroupContentResult struct {
ParentCatalogID string `json:"parentCatalogID"` // 根目录是"0"
CatalogList []struct {
Catalog
Path string `json:"path"`
} `json:"catalogList"`
ContentList []Content `json:"contentList"`
NodeCount int `json:"nodeCount"` // 文件+文件夹数量
CtlgCnt int `json:"ctlgCnt"` // 文件夹数量
ContCnt int `json:"contCnt"` // 文件数量
} `json:"getGroupContentResult"`
} `json:"data"`
}
type ParallelHashCtx struct {
PartOffset int64 `json:"partOffset"`
}
type PartInfo struct {
PartNumber int64 `json:"partNumber"`
PartSize int64 `json:"partSize"`
ParallelHashCtx ParallelHashCtx `json:"parallelHashCtx"`
}
type PersonalThumbnail struct {
Style string `json:"style"`
Url string `json:"url"`
}
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"`
FileName string `json:"fileName"`
PartInfos []PersonalPartInfo `json:"partInfos"`
Exist bool `json:"exist"`
RapidUpload bool `json:"rapidUpload"`
UploadId string `json:"uploadId"`
}
}
type PersonalUploadUrlResp struct {
BaseResp
Data struct {
FileId string `json:"fileId"`
UploadId string `json:"uploadId"`
PartInfos []PersonalPartInfo `json:"partInfos"`
}
}
type RefreshTokenResp struct {
XMLName xml.Name `xml:"root"`
Return string `xml:"return"`
Token string `xml:"token"`
Expiretime int32 `xml:"expiretime"`
AccessToken string `xml:"accessToken"`
Desc string `xml:"desc"`
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"net/url"
"path"
"sort"
"strconv"
"strings"
@ -13,6 +14,7 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/pkg/utils/random"
"github.com/go-resty/resty/v2"
@ -52,6 +54,55 @@ func getTime(t string) time.Time {
return stamp
}
func (d *Yun139) refreshToken() error {
if d.ref != nil {
return d.ref.refreshToken()
}
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
if err != nil {
return fmt.Errorf("authorization decode failed: %s", err)
}
decodeStr := string(decode)
splits := strings.Split(decodeStr, ":")
if len(splits) < 3 {
return fmt.Errorf("authorization is invalid, splits < 3")
}
strs := strings.Split(splits[2], "|")
if len(strs) < 4 {
return fmt.Errorf("authorization is invalid, strs < 4")
}
expiration, err := strconv.ParseInt(strs[3], 10, 64)
if err != nil {
return fmt.Errorf("authorization is invalid")
}
expiration -= time.Now().UnixMilli()
if expiration > 1000*60*60*24*15 {
// Authorization有效期大于15天无需刷新
return nil
}
if expiration < 0 {
return fmt.Errorf("authorization has expired")
}
url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do"
var resp RefreshTokenResp
reqBody := "<root><token>" + splits[2] + "</token><account>" + splits[1] + "</account><clienttype>656</clienttype></root>"
_, err = base.RestyClient.R().
ForceContentType("application/xml").
SetBody(reqBody).
SetResult(&resp).
Post(url)
if err != nil {
return err
}
if resp.Return != "0" {
return fmt.Errorf("failed to refresh token: %s", resp.Desc)
}
d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token))
op.MustSaveDriverStorage(d)
return nil
}
func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
url := "https://yun.139.com" + pathname
req := base.RestyClient.R()
@ -72,21 +123,22 @@ func (d *Yun139) request(pathname string, method string, callback base.ReqCallba
req.SetHeaders(map[string]string{
"Accept": "application/json, text/plain, */*",
"CMS-DEVICE": "default",
"Authorization": "Basic " + d.Authorization,
"Authorization": "Basic " + d.getAuthorization(),
"mcloud-channel": "1000101",
"mcloud-client": "10701",
//"mcloud-route": "001",
"mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
//"mcloud-skey":"",
"mcloud-version": "6.6.0",
"Origin": "https://yun.139.com",
"Referer": "https://yun.139.com/w/",
"x-DeviceInfo": "||9|6.6.0|chrome|95.0.4638.69|uwIy75obnsRPIwlJSd7D9GhUvFwG96ce||macos 10.15.2||zh-CN|||",
"x-huawei-channelSrc": "10000034",
"x-inner-ntwk": "2",
"x-m4c-caller": "PC",
"x-m4c-src": "10002",
"x-SvcType": svcType,
"mcloud-version": "7.14.0",
"Origin": "https://yun.139.com",
"Referer": "https://yun.139.com/w/",
"x-DeviceInfo": "||9|7.14.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,
"Inner-Hcy-Router-Https": "1",
})
var e BaseResp
@ -124,7 +176,7 @@ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
"catalogSortType": 0,
"contentSortType": 0,
"commonAccountInfo": base.Json{
"account": d.Account,
"account": d.getAccount(),
"accountType": 1,
},
}
@ -172,7 +224,7 @@ func (d *Yun139) newJson(data map[string]interface{}) base.Json {
"cloudID": d.CloudID,
"cloudType": 1,
"commonAccountInfo": base.Json{
"account": d.Account,
"account": d.getAccount(),
"accountType": 1,
},
}
@ -193,10 +245,11 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
"sortDirection": 1,
})
var resp QueryContentListResp
_, err := d.post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp)
_, err := d.post("/orchestration/familyCloud-rebuild/content/v1.2/queryContentList", data, &resp)
if err != nil {
return nil, err
}
path := resp.Data.Path
for _, catalog := range resp.Data.CloudCatalogList {
f := model.Object{
ID: catalog.CatalogID,
@ -205,6 +258,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
IsFolder: true,
Modified: getTime(catalog.LastUpdateTime),
Ctime: getTime(catalog.CreateTime),
Path: path, // 文件夹上一级的Path
}
files = append(files, &f)
}
@ -216,13 +270,14 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
Size: content.ContentSize,
Modified: getTime(content.LastUpdateTime),
Ctime: getTime(content.CreateTime),
Path: path, // 文件所在目录的Path
},
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
//Thumbnail: content.BigthumbnailURL,
}
files = append(files, &f)
}
if 100*pageNum > resp.Data.TotalCount {
if resp.Data.TotalCount == 0 {
break
}
pageNum++
@ -230,12 +285,67 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
return files, nil
}
func (d *Yun139) groupGetFiles(catalogID string) ([]model.Obj, error) {
pageNum := 1
files := make([]model.Obj, 0)
for {
data := d.newJson(base.Json{
"groupID": d.CloudID,
"catalogID": path.Base(catalogID),
"contentSortType": 0,
"sortDirection": 1,
"startNumber": pageNum,
"endNumber": pageNum + 99,
"path": path.Join(d.RootFolderID, catalogID),
})
var resp QueryGroupContentListResp
_, err := d.post("/orchestration/group-rebuild/content/v1.0/queryGroupContentList", data, &resp)
if err != nil {
return nil, err
}
path := resp.Data.GetGroupContentResult.ParentCatalogID
for _, catalog := range resp.Data.GetGroupContentResult.CatalogList {
f := model.Object{
ID: catalog.CatalogID,
Name: catalog.CatalogName,
Size: 0,
IsFolder: true,
Modified: getTime(catalog.UpdateTime),
Ctime: getTime(catalog.CreateTime),
Path: catalog.Path, // 文件夹的真实Path root:/开头
}
files = append(files, &f)
}
for _, content := range resp.Data.GetGroupContentResult.ContentList {
f := model.ObjThumb{
Object: model.Object{
ID: content.ContentID,
Name: content.ContentName,
Size: content.ContentSize,
Modified: getTime(content.UpdateTime),
Ctime: getTime(content.CreateTime),
Path: path, // 文件所在目录的Path
},
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
//Thumbnail: content.BigthumbnailURL,
}
files = append(files, &f)
}
if (pageNum + 99) > resp.Data.GetGroupContentResult.NodeCount {
break
}
pageNum = pageNum + 100
}
return files, nil
}
func (d *Yun139) getLink(contentId string) (string, error) {
data := base.Json{
"appName": "",
"contentID": contentId,
"commonAccountInfo": base.Json{
"account": d.Account,
"account": d.getAccount(),
"accountType": 1,
},
}
@ -246,9 +356,199 @@ func (d *Yun139) getLink(contentId string) (string, error) {
}
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
}
func (d *Yun139) familyGetLink(contentId string, path string) (string, error) {
data := d.newJson(base.Json{
"contentID": contentId,
"path": path,
})
res, err := d.post("/orchestration/familyCloud-rebuild/content/v1.0/getFileDownLoadURL",
data, nil)
if err != nil {
return "", err
}
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
}
func (d *Yun139) groupGetLink(contentId string, path string) (string, error) {
data := d.newJson(base.Json{
"contentID": contentId,
"groupID": d.CloudID,
"path": path,
})
res, err := d.post("/orchestration/group-rebuild/groupManage/v1.0/getGroupFileDownLoadURL",
data, nil)
if err != nil {
return "", err
}
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
}
func unicode(str string) string {
textQuoted := strconv.QuoteToASCII(str)
textUnquoted := textQuoted[1 : len(textQuoted)-1]
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.getAuthorization(),
"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.14.0",
"Origin": "https://yun.139.com",
"Referer": "https://yun.139.com/w/",
"x-DeviceInfo": "||9|7.14.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.14.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
}
}
func (d *Yun139) getAuthorization() string {
if d.ref != nil {
return d.ref.getAuthorization()
}
return d.Authorization
}
func (d *Yun139) getAccount() string {
if d.ref != nil {
return d.ref.getAccount()
}
return d.Account
}

View File

@ -365,7 +365,7 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
log.Debugf("uploadData: %+v", uploadData)
requestURL := uploadData.RequestURL
uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&")
req, err := http.NewRequest(http.MethodPut, requestURL, bytes.NewReader(byteData))
req, err := http.NewRequest(http.MethodPut, requestURL, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
if err != nil {
return err
}
@ -375,12 +375,12 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
req.Header.Set(v[0:i], v[i+1:])
}
r, err := base.HttpClient.Do(req)
log.Debugf("%+v %+v", r, r.Request.Header)
r.Body.Close()
if err != nil {
return err
}
up(int(i * 100 / count))
log.Debugf("%+v %+v", r, r.Request.Header)
_ = r.Body.Close()
up(float64(i) * 100 / float64(count))
}
fileMd5 := hex.EncodeToString(md5Sum.Sum(nil))
sliceMd5 := fileMd5

View File

@ -2,6 +2,7 @@ package _189pc
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
@ -13,6 +14,7 @@ import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
)
type Cloud189PC struct {
@ -28,7 +30,11 @@ type Cloud189PC struct {
uploadThread int
familyTransferFolder *Cloud189Folder
cleanFamilyTransferFile func()
storageConfig driver.Config
ref *Cloud189PC
}
func (y *Cloud189PC) Config() driver.Config {
@ -43,16 +49,24 @@ func (y *Cloud189PC) GetAddition() driver.Additional {
}
func (y *Cloud189PC) Init(ctx context.Context) (err error) {
// 兼容旧上传接口
y.storageConfig.NoOverwriteUpload = y.isFamily() && (y.Addition.RapidUpload || y.Addition.UploadMethod == "old")
y.storageConfig = config
if y.isFamily() {
// 兼容旧上传接口
if y.Addition.RapidUpload || y.Addition.UploadMethod == "old" {
y.storageConfig.NoOverwriteUpload = true
}
} else {
// 家庭云转存,不支持覆盖上传
if y.Addition.FamilyTransfer {
y.storageConfig.NoOverwriteUpload = true
}
}
// 处理个人云和家庭云参数
if y.isFamily() && y.RootFolderID == "-11" {
y.RootFolderID = ""
}
if !y.isFamily() && y.RootFolderID == "" {
y.RootFolderID = "-11"
y.FamilyID = ""
}
// 限制上传线程数
@ -61,38 +75,64 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
y.uploadThread, y.UploadThread = 3, "3"
}
// 初始化请求客户端
if y.client == nil {
y.client = base.NewRestyClient().SetHeaders(map[string]string{
"Accept": "application/json;charset=UTF-8",
"Referer": WEB_URL,
})
}
if y.ref == nil {
// 初始化请求客户端
if y.client == nil {
y.client = base.NewRestyClient().SetHeaders(map[string]string{
"Accept": "application/json;charset=UTF-8",
"Referer": WEB_URL,
})
}
// 避免重复登陆
identity := utils.GetMD5EncodeStr(y.Username + y.Password)
if !y.isLogin() || y.identity != identity {
y.identity = identity
if err = y.login(); err != nil {
return
// 避免重复登陆
identity := utils.GetMD5EncodeStr(y.Username + y.Password)
if !y.isLogin() || y.identity != identity {
y.identity = identity
if err = y.login(); err != nil {
return
}
}
}
// 处理家庭云ID
if y.isFamily() && y.FamilyID == "" {
if y.FamilyID == "" {
if y.FamilyID, err = y.getFamilyID(); err != nil {
return err
}
}
// 创建中转文件夹
if y.FamilyTransfer {
if err := y.createFamilyTransferFolder(); 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
}
func (d *Cloud189PC) InitReference(storage driver.Driver) error {
refStorage, ok := storage.(*Cloud189PC)
if ok {
d.ref = refStorage
return nil
}
return errs.NotSupport
}
func (y *Cloud189PC) Drop(ctx context.Context) error {
y.ref = nil
return nil
}
func (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return y.getFiles(ctx, dir.GetID())
return y.getFiles(ctx, dir.GetID(), y.isFamily())
}
func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
@ -100,8 +140,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
URL string `json:"fileDownloadUrl"`
}
isFamily := y.isFamily()
fullUrl := API_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
}
fullUrl += "/getFileDownloadUrl.action"
@ -109,7 +150,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
_, err := y.get(fullUrl, func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParam("fileId", file.GetID())
if y.isFamily() {
if isFamily {
r.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
})
@ -119,7 +160,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
"flag": "1",
})
}
}, &downloadUrl)
}, &downloadUrl, isFamily)
if err != nil {
return nil, err
}
@ -156,8 +197,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
}
func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
isFamily := y.isFamily()
fullUrl := API_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
}
fullUrl += "/createFolder.action"
@ -169,7 +211,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
"folderName": dirName,
"relativePath": "",
})
if y.isFamily() {
if isFamily {
req.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"parentId": parentDir.GetID(),
@ -179,7 +221,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
"parentFolderId": parentDir.GetID(),
})
}
}, &newFolder)
}, &newFolder, isFamily)
if err != nil {
return nil, err
}
@ -187,27 +229,14 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
}
func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"type": "MOVE",
"taskInfos": MustString(utils.Json.MarshalToString(
[]BatchTaskInfo{
{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
},
})),
"targetFolderId": dstDir.GetID(),
})
if y.isFamily() {
req.SetFormData(map[string]string{
"familyId": y.FamilyID,
})
}
}, &resp)
isFamily := y.isFamily()
other := map[string]string{"targetFileName": dstDir.GetName()}
resp, err := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
})
if err != nil {
return nil, err
}
@ -218,10 +247,11 @@ func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.
}
func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
isFamily := y.isFamily()
queryParam := make(map[string]string)
fullUrl := API_URL
method := http.MethodPost
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
method = http.MethodGet
queryParam["familyId"] = y.FamilyID
@ -245,7 +275,7 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
_, err := y.request(fullUrl, method, func(req *resty.Request) {
req.SetContext(ctx).SetQueryParams(queryParam)
}, nil, newObj)
}, nil, newObj, isFamily)
if err != nil {
return nil, err
}
@ -253,28 +283,15 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
}
func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"type": "COPY",
"taskInfos": MustString(utils.Json.MarshalToString(
[]BatchTaskInfo{
{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
},
})),
"targetFolderId": dstDir.GetID(),
"targetFileName": dstDir.GetName(),
})
if y.isFamily() {
req.SetFormData(map[string]string{
"familyId": y.FamilyID,
})
}
}, &resp)
isFamily := y.isFamily()
other := map[string]string{"targetFileName": dstDir.GetName()}
resp, err := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
})
if err != nil {
return err
}
@ -282,27 +299,13 @@ func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
}
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"type": "DELETE",
"taskInfos": MustString(utils.Json.MarshalToString(
[]*BatchTaskInfo{
{
FileId: obj.GetID(),
FileName: obj.GetName(),
IsFolder: BoolToNumber(obj.IsDir()),
},
})),
})
isFamily := y.isFamily()
if y.isFamily() {
req.SetFormData(map[string]string{
"familyId": y.FamilyID,
})
}
}, &resp)
resp, err := y.CreateBatchTask("DELETE", IF(isFamily, y.FamilyID, ""), "", nil, BatchTaskInfo{
FileId: obj.GetID(),
FileName: obj.GetName(),
IsFolder: BoolToNumber(obj.IsDir()),
})
if err != nil {
return err
}
@ -310,25 +313,87 @@ func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
return y.WaitBatchTask("DELETE", resp.TaskID, time.Millisecond*200)
}
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (newObj model.Obj, err error) {
overwrite := true
isFamily := y.isFamily()
// 响应时间长,按需启用
if y.Addition.RapidUpload {
if newObj, err := y.RapidUpload(ctx, dstDir, stream); err == nil {
if y.Addition.RapidUpload && !stream.IsForceStreamUpload() {
if newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {
return newObj, nil
}
}
switch y.UploadMethod {
case "old":
return y.OldUpload(ctx, dstDir, stream, up)
uploadMethod := y.UploadMethod
if stream.IsForceStreamUpload() {
uploadMethod = "stream"
}
// 旧版上传家庭云也有限制
if uploadMethod == "old" {
return y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)
}
// 开启家庭云转存
if !isFamily && y.FamilyTransfer {
// 修改上传目标为家庭云文件夹
transferDstDir := dstDir
dstDir = y.familyTransferFolder
// 使用临时文件名
srcName := stream.GetName()
stream = &WrapFileStreamer{
FileStreamer: stream,
Name: fmt.Sprintf("0%s.transfer", uuid.NewString()),
}
// 使用家庭云上传
isFamily = true
overwrite = false
defer func() {
if newObj != nil {
// 转存家庭云文件到个人云
err = y.SaveFamilyFileToPersonCloud(context.TODO(), y.FamilyID, newObj, transferDstDir, true)
// 删除家庭云源文件
go y.Delete(context.TODO(), y.FamilyID, newObj)
// 批量任务有概率删不掉
go y.cleanFamilyTransferFile()
// 转存失败返回错误
if err != nil {
return
}
// 查找转存文件
var file *Cloud189File
file, err = y.findFileByName(context.TODO(), newObj.GetName(), transferDstDir.GetID(), false)
if err != nil {
if err == errs.ObjectNotFound {
err = fmt.Errorf("unknown error: No transfer file obtained %s", newObj.GetName())
}
return
}
// 重命名转存文件
newObj, err = y.Rename(context.TODO(), file, srcName)
if err != nil {
// 重命名失败删除源文件
_ = y.Delete(context.TODO(), "", file)
}
return
}
}()
}
switch uploadMethod {
case "rapid":
return y.FastUpload(ctx, dstDir, stream, up)
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
case "stream":
if stream.GetSize() == 0 {
return y.FastUpload(ctx, dstDir, stream, up)
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
}
fallthrough
default:
return y.StreamUpload(ctx, dstDir, stream, up)
return y.StreamUpload(ctx, dstDir, stream, up, isFamily, overwrite)
}
}

View File

@ -18,6 +18,7 @@ import (
"strings"
"time"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils/random"
)
@ -192,3 +193,28 @@ func partSize(size int64) int64 {
}
return DEFAULT
}
func isBool(bs ...bool) bool {
for _, b := range bs {
if b {
return true
}
}
return false
}
func IF[V any](o bool, t V, f V) V {
if o {
return t
}
return f
}
type WrapFileStreamer struct {
model.FileStreamer
Name string
}
func (w *WrapFileStreamer) GetName() string {
return w.Name
}

View File

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

View File

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

View File

@ -19,9 +19,12 @@ import (
"strings"
"time"
"golang.org/x/sync/semaphore"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/setting"
@ -54,13 +57,13 @@ const (
CHANNEL_ID = "web_cloud.189.cn"
)
func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]string {
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
dateOfGmt := getHttpDateStr()
sessionKey := y.tokenInfo.SessionKey
sessionSecret := y.tokenInfo.SessionSecret
if y.isFamily() {
sessionKey = y.tokenInfo.FamilySessionKey
sessionSecret = y.tokenInfo.FamilySessionSecret
sessionKey := y.getTokenInfo().SessionKey
sessionSecret := y.getTokenInfo().SessionSecret
if isFamily {
sessionKey = y.getTokenInfo().FamilySessionKey
sessionSecret = y.getTokenInfo().FamilySessionSecret
}
header := map[string]string{
@ -72,10 +75,10 @@ func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]stri
return header
}
func (y *Cloud189PC) EncryptParams(params Params) string {
sessionSecret := y.tokenInfo.SessionSecret
if y.isFamily() {
sessionSecret = y.tokenInfo.FamilySessionSecret
func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
sessionSecret := y.getTokenInfo().SessionSecret
if isFamily {
sessionSecret = y.getTokenInfo().FamilySessionSecret
}
if params != nil {
return AesECBEncrypt(params.Encode(), sessionSecret[:16])
@ -83,17 +86,17 @@ func (y *Cloud189PC) EncryptParams(params Params) string {
return ""
}
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}) ([]byte, error) {
req := y.client.R().SetQueryParams(clientSuffix())
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) {
req := y.getClient().R().SetQueryParams(clientSuffix())
// 设置params
paramsData := y.EncryptParams(params)
paramsData := y.EncryptParams(params, isBool(isFamily...))
if paramsData != "" {
req.SetQueryParam("params", paramsData)
}
// Signature
req.SetHeaders(y.SignatureHeader(url, method, paramsData))
req.SetHeaders(y.SignatureHeader(url, method, paramsData, isBool(isFamily...)))
var erron RespErr
req.SetError(&erron)
@ -113,31 +116,33 @@ func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, para
if err = y.refreshSession(); err != nil {
return nil, err
}
return y.request(url, method, callback, params, resp)
return y.request(url, method, callback, params, resp, isFamily...)
}
// if erron.ErrorCode == "InvalidSessionKey" || erron.Code == "InvalidSessionKey" {
if strings.Contains(res.String(), "InvalidSessionKey") {
if err = y.refreshSession(); err != nil {
return nil, err
}
return y.request(url, method, callback, params, resp, isFamily...)
}
// 处理错误
if erron.HasError() {
if erron.ErrorCode == "InvalidSessionKey" {
if err = y.refreshSession(); err != nil {
return nil, err
}
return y.request(url, method, callback, params, resp)
}
return nil, &erron
}
return res.Body(), nil
}
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return y.request(url, http.MethodGet, callback, nil, resp)
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
return y.request(url, http.MethodGet, callback, nil, resp, isFamily...)
}
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return y.request(url, http.MethodPost, callback, nil, resp)
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
return y.request(url, http.MethodPost, callback, nil, resp, isFamily...)
}
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader) ([]byte, error) {
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader, isFamily bool) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
if err != nil {
return nil, err
@ -154,7 +159,7 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
}
if sign {
for key, value := range y.SignatureHeader(url, http.MethodPut, "") {
for key, value := range y.SignatureHeader(url, http.MethodPut, "", isFamily) {
req.Header.Add(key, value)
}
}
@ -171,8 +176,8 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
}
var erron RespErr
jsoniter.Unmarshal(body, &erron)
xml.Unmarshal(body, &erron)
_ = jsoniter.Unmarshal(body, &erron)
_ = xml.Unmarshal(body, &erron)
if erron.HasError() {
return nil, &erron
}
@ -181,40 +186,10 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
}
return body, nil
}
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, error) {
fullUrl := API_URL
if y.isFamily() {
fullUrl += "/family/file"
}
fullUrl += "/listFiles.action"
res := make([]model.Obj, 0, 130)
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
res := make([]model.Obj, 0, 100)
for pageNum := 1; ; pageNum++ {
var resp Cloud189FilesResp
_, err := y.get(fullUrl, func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"folderId": fileId,
"fileType": "0",
"mediaAttr": "0",
"iconOption": "5",
"pageNum": fmt.Sprint(pageNum),
"pageSize": "130",
})
if y.isFamily() {
r.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"orderBy": toFamilyOrderBy(y.OrderBy),
"descending": toDesc(y.OrderDirection),
})
} else {
r.SetQueryParams(map[string]string{
"recursive": "0",
"orderBy": y.OrderBy,
"descending": toDesc(y.OrderDirection),
})
}
}, &resp)
resp, err := y.getFilesWithPage(ctx, fileId, isFamily, pageNum, 1000, y.OrderBy, y.OrderDirection)
if err != nil {
return nil, err
}
@ -233,6 +208,63 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj,
return res, nil
}
func (y *Cloud189PC) getFilesWithPage(ctx context.Context, fileId string, isFamily bool, pageNum int, pageSize int, orderBy string, orderDirection string) (*Cloud189FilesResp, error) {
fullUrl := API_URL
if isFamily {
fullUrl += "/family/file"
}
fullUrl += "/listFiles.action"
var resp Cloud189FilesResp
_, err := y.get(fullUrl, func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"folderId": fileId,
"fileType": "0",
"mediaAttr": "0",
"iconOption": "5",
"pageNum": fmt.Sprint(pageNum),
"pageSize": fmt.Sprint(pageSize),
})
if isFamily {
r.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"orderBy": toFamilyOrderBy(orderBy),
"descending": toDesc(orderDirection),
})
} else {
r.SetQueryParams(map[string]string{
"recursive": "0",
"orderBy": orderBy,
"descending": toDesc(orderDirection),
})
}
}, &resp, isFamily)
if err != nil {
return nil, err
}
return &resp, nil
}
func (y *Cloud189PC) findFileByName(ctx context.Context, searchName string, folderId string, isFamily bool) (*Cloud189File, error) {
for pageNum := 1; ; pageNum++ {
resp, err := y.getFilesWithPage(ctx, folderId, isFamily, pageNum, 10, "filename", "asc")
if err != nil {
return nil, err
}
// 获取完毕跳出
if resp.FileListAO.Count == 0 {
return nil, errs.ObjectNotFound
}
for i := 0; i < len(resp.FileListAO.FileList); i++ {
file := resp.FileListAO.FileList[i]
if file.Name == searchName {
return &file, nil
}
}
}
}
func (y *Cloud189PC) login() (err error) {
// 初始化登陆所需参数
if y.loginParam == nil {
@ -400,6 +432,9 @@ func (y *Cloud189PC) initLoginParam() error {
// 刷新会话
func (y *Cloud189PC) refreshSession() (err error) {
if y.ref != nil {
return y.ref.refreshSession()
}
var erron RespErr
var userSessionResp UserSessionResp
_, err = y.client.R().
@ -437,7 +472,7 @@ func (y *Cloud189PC) refreshSession() (err error) {
// 普通上传
// 无法上传大小为0的文件
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
var sliceSize = partSize(file.GetSize())
count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize)))
lastPartSize := file.GetSize() % sliceSize
@ -454,7 +489,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
}
fullUrl := UPLOAD_URL
if y.isFamily() {
if isFamily {
params.Set("familyId", y.FamilyID)
fullUrl += "/family"
} else {
@ -466,7 +501,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
var initMultiUpload InitMultiUploadResp
_, err := y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
}, params, &initMultiUpload)
}, params, &initMultiUpload, isFamily)
if err != nil {
return nil, err
}
@ -475,6 +510,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
retry.Attempts(3),
retry.Delay(time.Second),
retry.DelayType(retry.BackOffDelay))
sem := semaphore.NewWeighted(3)
fileMd5 := md5.New()
silceMd5 := md5.New()
@ -484,7 +520,6 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
if utils.IsCanceled(upCtx) {
break
}
byteData := make([]byte, sliceSize)
if i == count {
byteData = byteData[:lastPartSize]
@ -493,6 +528,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
// 读取块
silceMd5.Reset()
if _, err := io.ReadFull(io.TeeReader(file, io.MultiWriter(fileMd5, silceMd5)), byteData); err != io.EOF && err != nil {
sem.Release(1)
return nil, err
}
@ -502,18 +538,23 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
partInfo := fmt.Sprintf("%d-%s", i, base64.StdEncoding.EncodeToString(md5Bytes))
threadG.Go(func(ctx context.Context) error {
uploadUrls, err := y.GetMultiUploadUrls(ctx, initMultiUpload.Data.UploadFileID, partInfo)
if err = sem.Acquire(ctx, 1); err != nil {
return err
}
defer sem.Release(1)
uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, initMultiUpload.Data.UploadFileID, partInfo)
if err != nil {
return err
}
// step.4 上传切片
uploadUrl := uploadUrls[0]
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData))
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false,
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)), isFamily)
if err != nil {
return err
}
up(int(threadG.Success()) * 100 / count)
up(float64(threadG.Success()) * 100 / float64(count))
return nil
})
}
@ -538,21 +579,21 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
"sliceMd5": sliceMd5Hex,
"lazyCheck": "1",
"isLog": "0",
"opertype": "3",
}, &resp)
"opertype": IF(overwrite, "3", "1"),
}, &resp, isFamily)
if err != nil {
return nil, err
}
return resp.toFile(), nil
}
func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) (model.Obj, error) {
func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
fileMd5 := stream.GetHash().GetHash(utils.MD5)
if len(fileMd5) < utils.MD5.Width {
return nil, errors.New("invalid hash")
}
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()))
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()), isFamily)
if err != nil {
return nil, err
}
@ -561,11 +602,11 @@ func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream m
return nil, errors.New("rapid upload fail")
}
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId)
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId, isFamily, overwrite)
}
// 快传
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
tempFile, err := file.CacheFullInTempFile()
if err != nil {
return nil, err
@ -594,7 +635,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}
silceMd5.Reset()
if _, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
if _, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
return nil, err
}
md5Byte := silceMd5.Sum(nil)
@ -609,7 +650,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}
fullUrl := UPLOAD_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family"
} else {
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
@ -617,7 +658,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}
// 尝试恢复进度
uploadProgress, ok := base.GetUploadProgress[*UploadProgress](y, y.tokenInfo.SessionKey, fileMd5Hex)
uploadProgress, ok := base.GetUploadProgress[*UploadProgress](y, y.getTokenInfo().SessionKey, fileMd5Hex)
if !ok {
//step.2 预上传
params := Params{
@ -628,13 +669,13 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
"sliceSize": fmt.Sprint(sliceSize),
"sliceMd5": sliceMd5Hex,
}
if y.isFamily() {
if isFamily {
params.Set("familyId", y.FamilyID)
}
var uploadInfo InitMultiUploadResp
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
}, params, &uploadInfo)
}, params, &uploadInfo, isFamily)
if err != nil {
return nil, err
}
@ -659,7 +700,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
i, uploadPart := i, uploadPart
threadG.Go(func(ctx context.Context) error {
// step.3 获取上传链接
uploadUrls, err := y.GetMultiUploadUrls(ctx, uploadInfo.UploadFileID, uploadPart)
uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, uploadInfo.UploadFileID, uploadPart)
if err != nil {
return err
}
@ -671,12 +712,12 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}
// step.4 上传切片
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize))
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize), isFamily)
if err != nil {
return err
}
up(int(threadG.Success()) * 100 / len(uploadUrls))
up(float64(threadG.Success()) * 100 / float64(len(uploadUrls)))
uploadProgress.UploadParts[i] = ""
return nil
})
@ -684,7 +725,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
if err = threadG.Wait(); err != nil {
if errors.Is(err, context.Canceled) {
uploadProgress.UploadParts = utils.SliceFilter(uploadProgress.UploadParts, func(s string) bool { return s != "" })
base.SaveUploadProgress(y, uploadProgress, y.tokenInfo.SessionKey, fileMd5Hex)
base.SaveUploadProgress(y, uploadProgress, y.getTokenInfo().SessionKey, fileMd5Hex)
}
return nil, err
}
@ -698,8 +739,8 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}, Params{
"uploadFileId": uploadInfo.UploadFileID,
"isLog": "0",
"opertype": "3",
}, &resp)
"opertype": IF(overwrite, "3", "1"),
}, &resp, isFamily)
if err != nil {
return nil, err
}
@ -708,9 +749,9 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
// 获取上传切片信息
// 对http body有大小限制分片信息太多会出错
func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
fullUrl := UPLOAD_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family"
} else {
fullUrl += "/person"
@ -723,7 +764,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string
}, Params{
"uploadFileId": uploadFileId,
"partInfo": strings.Join(partInfo, ","),
}, &uploadUrlsResp)
}, &uploadUrlsResp, isFamily)
if err != nil {
return nil, err
}
@ -752,7 +793,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string
}
// 旧版本上传,家庭云不支持覆盖
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
tempFile, err := file.CacheFullInTempFile()
if err != nil {
return nil, err
@ -761,9 +802,10 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
if err != nil {
return nil, err
}
rateLimited := driver.NewLimitedUploadStream(ctx, io.NopCloser(tempFile))
// 创建上传会话
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()))
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily)
if err != nil {
return nil, err
}
@ -780,14 +822,14 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
"Expect": "100-continue",
}
if y.isFamily() {
if isFamily {
header["FamilyId"] = fmt.Sprint(y.FamilyID)
header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
} else {
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
}
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile))
_, err := y.put(ctx, status.FileUploadUrl, header, true, rateLimited, isFamily)
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
return nil, err
}
@ -802,33 +844,33 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
"uploadFileId": fmt.Sprint(status.UploadFileId),
"resumePolicy": "1",
})
if y.isFamily() {
if isFamily {
req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
}
}, &status)
}, &status, isFamily)
if err != nil {
return nil, err
}
if _, err := tempFile.Seek(status.GetSize(), io.SeekStart); err != nil {
return nil, err
}
up(int(status.GetSize()/file.GetSize()) * 100)
up(float64(status.GetSize()) / float64(file.GetSize()) * 100)
}
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId)
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId, isFamily, overwrite)
}
// 创建上传会话
func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string) (*CreateUploadFileResp, error) {
func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string, isFamily bool) (*CreateUploadFileResp, error) {
var uploadInfo CreateUploadFileResp
fullUrl := API_URL + "/createUploadFile.action"
if y.isFamily() {
if isFamily {
fullUrl = API_URL + "/family/file/createFamilyFile.action"
}
_, err := y.post(fullUrl, func(req *resty.Request) {
req.SetContext(ctx)
if y.isFamily() {
if isFamily {
req.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"parentId": parentID,
@ -849,7 +891,7 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
"isLog": "0",
})
}
}, &uploadInfo)
}, &uploadInfo, isFamily)
if err != nil {
return nil, err
@ -858,11 +900,11 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
}
// 提交上传文件
func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64) (model.Obj, error) {
func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64, isFamily bool, overwrite bool) (model.Obj, error) {
var resp OldCommitUploadFileResp
_, err := y.post(fileCommitUrl, func(req *resty.Request) {
req.SetContext(ctx)
if y.isFamily() {
if isFamily {
req.SetHeaders(map[string]string{
"ResumePolicy": "1",
"UploadFileId": fmt.Sprint(uploadFileID),
@ -870,13 +912,13 @@ func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string,
})
} else {
req.SetFormData(map[string]string{
"opertype": "3",
"opertype": IF(overwrite, "3", "1"),
"resumePolicy": "1",
"uploadFileId": fmt.Sprint(uploadFileID),
"isLog": "0",
})
}
}, &resp)
}, &resp, isFamily)
if err != nil {
return nil, err
}
@ -895,10 +937,79 @@ func (y *Cloud189PC) isLogin() bool {
return err == nil
}
// 创建家庭云中转文件夹
func (y *Cloud189PC) createFamilyTransferFolder() error {
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 err
}
y.familyTransferFolder = &rootFolder
return nil
}
// 清理中转文件夹
func (y *Cloud189PC) cleanFamilyTransfer(ctx context.Context) error {
transferFolderId := y.familyTransferFolder.GetID()
for pageNum := 1; ; pageNum++ {
resp, err := y.getFilesWithPage(ctx, transferFolderId, true, pageNum, 100, "lastOpTime", "asc")
if err != nil {
return err
}
// 获取完毕跳出
if resp.FileListAO.Count == 0 {
break
}
var tasks []BatchTaskInfo
for i := 0; i < len(resp.FileListAO.FolderList); i++ {
folder := resp.FileListAO.FolderList[i]
tasks = append(tasks, BatchTaskInfo{
FileId: folder.GetID(),
FileName: folder.GetName(),
IsFolder: BoolToNumber(folder.IsDir()),
})
}
for i := 0; i < len(resp.FileListAO.FileList); i++ {
file := resp.FileListAO.FileList[i]
tasks = append(tasks, BatchTaskInfo{
FileId: file.GetID(),
FileName: file.GetName(),
IsFolder: BoolToNumber(file.IsDir()),
})
}
if len(tasks) > 0 {
// 删除
resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, tasks...)
if err != nil {
return err
}
err = y.WaitBatchTask("DELETE", resp.TaskID, time.Second)
if err != nil {
return err
}
// 永久删除
resp, err = y.CreateBatchTask("CLEAR_RECYCLE", y.FamilyID, "", nil, tasks...)
if err != nil {
return err
}
err = y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second)
return err
}
}
return nil
}
// 获取家庭云所有用户信息
func (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
var resp FamilyInfoListResp
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp)
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp, true)
if err != nil {
return nil, err
}
@ -915,13 +1026,108 @@ func (y *Cloud189PC) getFamilyID() (string, error) {
return "", fmt.Errorf("cannot get automatically,please input family_id")
}
for _, info := range infos {
if strings.Contains(y.tokenInfo.LoginName, info.RemarkName) {
if strings.Contains(y.getTokenInfo().LoginName, info.RemarkName) {
return fmt.Sprint(info.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) Delete(ctx context.Context, familyId string, srcObj model.Obj) error {
task := BatchTaskInfo{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
}
// 删除源文件
resp, err := y.CreateBatchTask("DELETE", familyId, "", nil, task)
if err != nil {
return err
}
err = y.WaitBatchTask("DELETE", resp.TaskID, time.Second)
if err != nil {
return err
}
// 清除回收站
resp, err = y.CreateBatchTask("CLEAR_RECYCLE", familyId, "", nil, task)
if err != nil {
return err
}
err = y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second)
if err != nil {
return err
}
return nil
}
func (y *Cloud189PC) CreateBatchTask(aType string, familyID string, targetFolderId string, other map[string]string, taskInfos ...BatchTaskInfo) (*CreateBatchTaskResp, error) {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetFormData(map[string]string{
"type": aType,
"taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
})
if targetFolderId != "" {
req.SetFormData(map[string]string{"targetFolderId": targetFolderId})
}
if familyID != "" {
req.SetFormData(map[string]string{"familyId": familyID})
}
req.SetFormData(other)
}, &resp, familyID != "")
if err != nil {
return nil, err
}
return &resp, nil
}
// 检测任务状态
func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStateResp, error) {
var resp BatchTaskStateResp
_, err := y.post(API_URL+"/batch/checkBatchTask.action", func(req *resty.Request) {
@ -936,6 +1142,37 @@ func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStat
return &resp, nil
}
// 获取冲突的任务信息
func (y *Cloud189PC) GetConflictTaskInfo(aType string, taskID string) (*BatchTaskConflictTaskInfoResp, error) {
var resp BatchTaskConflictTaskInfoResp
_, err := y.post(API_URL+"/batch/getConflictTaskInfo.action", func(req *resty.Request) {
req.SetFormData(map[string]string{
"type": aType,
"taskId": taskID,
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
// 处理冲突
func (y *Cloud189PC) ManageBatchTask(aType string, taskID string, targetFolderId string, taskInfos ...BatchTaskInfo) error {
_, err := y.post(API_URL+"/batch/manageBatchTask.action", func(req *resty.Request) {
req.SetFormData(map[string]string{
"targetFolderId": targetFolderId,
"type": aType,
"taskId": taskID,
"taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
})
}, nil)
return err
}
var ErrIsConflict = errors.New("there is a conflict with the target object")
// 等待任务完成
func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration) error {
for {
state, err := y.CheckBatchTask(aType, taskID)
@ -944,10 +1181,24 @@ func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration)
}
switch state.TaskStatus {
case 2:
return errors.New("there is a conflict with the target object")
return ErrIsConflict
case 4:
return nil
}
time.Sleep(t)
}
}
func (y *Cloud189PC) getTokenInfo() *AppSessionResp {
if y.ref != nil {
return y.ref.getTokenInfo()
}
return y.tokenInfo
}
func (y *Cloud189PC) getClient() *resty.Client {
if y.ref != nil {
return y.ref.getClient()
}
return y.client
}

View File

@ -3,10 +3,12 @@ package alias
import (
"context"
"errors"
stdpath "path"
"strings"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
)
@ -45,6 +47,9 @@ func (d *Alias) Init(ctx context.Context) error {
d.oneKey = k
}
d.autoFlatten = true
} else {
d.oneKey = ""
d.autoFlatten = false
}
return nil
}
@ -87,8 +92,9 @@ func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
return nil, errs.ObjectNotFound
}
var objs []model.Obj
fsArgs := &fs.ListArgs{NoLog: true, Refresh: args.Refresh}
for _, dst := range dsts {
tmp, err := d.list(ctx, dst, sub)
tmp, err := d.list(ctx, dst, sub, fsArgs)
if err == nil {
objs = append(objs, tmp...)
}
@ -105,10 +111,211 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
for _, dst := range dsts {
link, err := d.link(ctx, dst, sub, args)
if err == nil {
if !args.Redirect && len(link.URL) > 0 {
// 正常情况下 多并发 仅支持返回URL的驱动
// alias套娃alias 可以让crypt、mega等驱动(不返回URL的) 支持并发
if d.DownloadConcurrency > 0 {
link.Concurrency = d.DownloadConcurrency
}
if d.DownloadPartSize > 0 {
link.PartSize = d.DownloadPartSize * utils.KB
}
}
return link, nil
}
}
return nil, errs.ObjectNotFound
}
func (d *Alias) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if !d.Writable {
return errs.PermissionDenied
}
reqPath, err := d.getReqPath(ctx, parentDir, true)
if err == nil {
return fs.MakeDir(ctx, stdpath.Join(*reqPath, dirName))
}
if errs.IsNotImplement(err) {
return errors.New("same-name dirs cannot make sub-dir")
}
return err
}
func (d *Alias) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
if !d.Writable {
return errs.PermissionDenied
}
srcPath, err := d.getReqPath(ctx, srcObj, false)
if errs.IsNotImplement(err) {
return errors.New("same-name files cannot be moved")
}
if err != nil {
return err
}
dstPath, err := d.getReqPath(ctx, dstDir, true)
if errs.IsNotImplement(err) {
return errors.New("same-name dirs cannot be moved to")
}
if err != nil {
return err
}
return fs.Move(ctx, *srcPath, *dstPath)
}
func (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
if !d.Writable {
return errs.PermissionDenied
}
reqPath, err := d.getReqPath(ctx, srcObj, false)
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) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
if !d.Writable {
return errs.PermissionDenied
}
srcPath, err := d.getReqPath(ctx, srcObj, false)
if errs.IsNotImplement(err) {
return errors.New("same-name files cannot be copied")
}
if err != nil {
return err
}
dstPath, err := d.getReqPath(ctx, dstDir, true)
if errs.IsNotImplement(err) {
return errors.New("same-name dirs cannot be copied to")
}
if err != nil {
return err
}
_, err = fs.Copy(ctx, *srcPath, *dstPath)
return err
}
func (d *Alias) Remove(ctx context.Context, obj model.Obj) error {
if !d.Writable {
return errs.PermissionDenied
}
reqPath, err := d.getReqPath(ctx, obj, false)
if err == nil {
return fs.Remove(ctx, *reqPath)
}
if errs.IsNotImplement(err) {
return errors.New("same-name files cannot be Delete")
}
return err
}
func (d *Alias) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {
if !d.Writable {
return errs.PermissionDenied
}
reqPath, err := d.getReqPath(ctx, dstDir, true)
if err == nil {
return fs.PutDirectly(ctx, *reqPath, s)
}
if errs.IsNotImplement(err) {
return errors.New("same-name dirs cannot be Put")
}
return err
}
func (d *Alias) PutURL(ctx context.Context, dstDir model.Obj, name, url string) error {
if !d.Writable {
return errs.PermissionDenied
}
reqPath, err := d.getReqPath(ctx, dstDir, true)
if err == nil {
return fs.PutURL(ctx, *reqPath, name, url)
}
if errs.IsNotImplement(err) {
return errors.New("same-name files cannot offline download")
}
return err
}
func (d *Alias) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
root, sub := d.getRootAndPath(obj.GetPath())
dsts, ok := d.pathMap[root]
if !ok {
return nil, errs.ObjectNotFound
}
for _, dst := range dsts {
meta, err := d.getArchiveMeta(ctx, dst, sub, args)
if err == nil {
return meta, nil
}
}
return nil, errs.NotImplement
}
func (d *Alias) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
root, sub := d.getRootAndPath(obj.GetPath())
dsts, ok := d.pathMap[root]
if !ok {
return nil, errs.ObjectNotFound
}
for _, dst := range dsts {
l, err := d.listArchive(ctx, dst, sub, args)
if err == nil {
return l, nil
}
}
return nil, errs.NotImplement
}
func (d *Alias) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
// alias的两个驱动一个支持驱动提取一个不支持如何兼容
// 如果访问的是不支持驱动提取的驱动内的压缩文件GetArchiveMeta就会返回errs.NotImplement提取URL前缀就会是/aeExtract就不会被调用
// 如果访问的是支持驱动提取的驱动内的压缩文件GetArchiveMeta就会返回有效值提取URL前缀就会是/adExtract就会被调用
root, sub := d.getRootAndPath(obj.GetPath())
dsts, ok := d.pathMap[root]
if !ok {
return nil, errs.ObjectNotFound
}
for _, dst := range dsts {
link, err := d.extract(ctx, dst, sub, args)
if err == nil {
if !args.Redirect && len(link.URL) > 0 {
if d.DownloadConcurrency > 0 {
link.Concurrency = d.DownloadConcurrency
}
if d.DownloadPartSize > 0 {
link.PartSize = d.DownloadPartSize * utils.KB
}
}
return link, nil
}
}
return nil, errs.NotImplement
}
func (d *Alias) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error {
if !d.Writable {
return errs.PermissionDenied
}
srcPath, err := d.getReqPath(ctx, srcObj, false)
if errs.IsNotImplement(err) {
return errors.New("same-name files cannot be decompressed")
}
if err != nil {
return err
}
dstPath, err := d.getReqPath(ctx, dstDir, true)
if errs.IsNotImplement(err) {
return errors.New("same-name dirs cannot be decompressed to")
}
if err != nil {
return err
}
_, err = fs.ArchiveDecompress(ctx, *srcPath, *dstPath, args)
return err
}
var _ driver.Driver = (*Alias)(nil)

View File

@ -9,19 +9,28 @@ type Addition struct {
// Usually one of two
// driver.RootPath
// define other
Paths string `json:"paths" required:"true" type:"text"`
Paths string `json:"paths" required:"true" type:"text"`
ProtectSameName bool `json:"protect_same_name" default:"true" required:"false" help:"Protects same-name files from Delete or Rename"`
DownloadConcurrency int `json:"download_concurrency" default:"0" required:"false" type:"number" help:"Need to enable proxy"`
DownloadPartSize int `json:"download_part_size" default:"0" type:"number" required:"false" help:"Need to enable proxy. Unit: KB"`
Writable bool `json:"writable" type:"bool" default:"false"`
}
var config = driver.Config{
Name: "Alias",
LocalSort: true,
NoCache: true,
NoUpload: true,
DefaultRoot: "/",
Name: "Alias",
LocalSort: true,
NoCache: true,
NoUpload: false,
DefaultRoot: "/",
ProxyRangeOption: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Alias{}
return &Alias{
Addition: Addition{
ProtectSameName: true,
},
}
})
}

View File

@ -3,11 +3,15 @@ package alias
import (
"context"
"fmt"
"net/url"
stdpath "path"
"strings"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/sign"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
@ -15,7 +19,7 @@ import (
func (d *Alias) listRoot() []model.Obj {
var objs []model.Obj
for k, _ := range d.pathMap {
for k := range d.pathMap {
obj := model.Object{
Name: k,
IsFolder: true,
@ -61,11 +65,12 @@ func (d *Alias) get(ctx context.Context, path string, dst, sub string) (model.Ob
Size: obj.GetSize(),
Modified: obj.ModTime(),
IsFolder: obj.IsDir(),
HashInfo: obj.GetHash(),
}, nil
}
func (d *Alias) list(ctx context.Context, dst, sub string) ([]model.Obj, error) {
objs, err := fs.List(ctx, stdpath.Join(dst, sub), &fs.ListArgs{NoLog: true})
func (d *Alias) list(ctx context.Context, dst, sub string, args *fs.ListArgs) ([]model.Obj, error) {
objs, err := fs.List(ctx, stdpath.Join(dst, sub), args)
// the obj must implement the model.SetPath interface
// return objs, err
if err != nil {
@ -93,22 +98,128 @@ func (d *Alias) list(ctx context.Context, dst, sub string) ([]model.Obj, error)
func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) {
reqPath := stdpath.Join(dst, sub)
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
// 参考 crypt 驱动
storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)
if err != nil {
return nil, err
}
if _, ok := storage.(*Alias); !ok && !args.Redirect {
link, _, err := op.Link(ctx, storage, reqActualPath, args)
return link, err
}
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
if err != nil {
return nil, err
}
if common.ShouldProxy(storage, stdpath.Base(sub)) {
return &model.Link{
link := &model.Link{
URL: fmt.Sprintf("%s/p%s?sign=%s",
common.GetApiUrl(args.HttpReq),
utils.EncodePath(reqPath, true),
sign.Sign(reqPath)),
}, nil
}
if args.HttpReq != nil && d.ProxyRange {
link.RangeReadCloser = common.NoProxyRange
}
return link, nil
}
link, _, err := fs.Link(ctx, reqPath, args)
link, _, err := op.Link(ctx, storage, reqActualPath, args)
return link, err
}
func (d *Alias) getReqPath(ctx context.Context, obj model.Obj, isParent bool) (*string, error) {
root, sub := d.getRootAndPath(obj.GetPath())
if sub == "" && !isParent {
return nil, errs.NotSupport
}
dsts, ok := d.pathMap[root]
if !ok {
return nil, errs.ObjectNotFound
}
var reqPath *string
for _, dst := range dsts {
path := stdpath.Join(dst, sub)
_, err := fs.Get(ctx, path, &fs.GetArgs{NoLog: true})
if err != nil {
continue
}
if !d.ProtectSameName {
return &path, nil
}
if ok {
ok = false
} else {
return nil, errs.NotImplement
}
reqPath = &path
}
if reqPath == nil {
return nil, errs.ObjectNotFound
}
return reqPath, nil
}
func (d *Alias) getArchiveMeta(ctx context.Context, dst, sub string, args model.ArchiveArgs) (model.ArchiveMeta, error) {
reqPath := stdpath.Join(dst, sub)
storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)
if err != nil {
return nil, err
}
if _, ok := storage.(driver.ArchiveReader); ok {
return op.GetArchiveMeta(ctx, storage, reqActualPath, model.ArchiveMetaArgs{
ArchiveArgs: args,
Refresh: true,
})
}
return nil, errs.NotImplement
}
func (d *Alias) listArchive(ctx context.Context, dst, sub string, args model.ArchiveInnerArgs) ([]model.Obj, error) {
reqPath := stdpath.Join(dst, sub)
storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)
if err != nil {
return nil, err
}
if _, ok := storage.(driver.ArchiveReader); ok {
return op.ListArchive(ctx, storage, reqActualPath, model.ArchiveListArgs{
ArchiveInnerArgs: args,
Refresh: true,
})
}
return nil, errs.NotImplement
}
func (d *Alias) extract(ctx context.Context, dst, sub string, args model.ArchiveInnerArgs) (*model.Link, error) {
reqPath := stdpath.Join(dst, sub)
storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)
if err != nil {
return nil, err
}
if _, ok := storage.(driver.ArchiveReader); ok {
if _, ok := storage.(*Alias); !ok && !args.Redirect {
link, _, err := op.DriverExtract(ctx, storage, reqActualPath, args)
return link, err
}
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
if err != nil {
return nil, err
}
if common.ShouldProxy(storage, stdpath.Base(sub)) {
link := &model.Link{
URL: fmt.Sprintf("%s/ap%s?inner=%s&pass=%s&sign=%s",
common.GetApiUrl(args.HttpReq),
utils.EncodePath(reqPath, true),
utils.EncodePath(args.InnerPath, true),
url.QueryEscape(args.Password),
sign.SignArchive(reqPath)),
}
if args.HttpReq != nil && d.ProxyRange {
link.RangeReadCloser = common.NoProxyRange
}
return link, nil
}
link, _, err := op.DriverExtract(ctx, storage, reqActualPath, args)
return link, err
}
return nil, errs.NotImplement
}

View File

@ -5,17 +5,19 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
type AListV3 struct {
@ -34,29 +36,29 @@ func (d *AListV3) GetAddition() driver.Additional {
func (d *AListV3) Init(ctx context.Context) error {
d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/")
var resp common.Resp[MeResp]
_, err := d.request("/me", http.MethodGet, func(req *resty.Request) {
_, _, err := d.request("/me", http.MethodGet, func(req *resty.Request) {
req.SetResult(&resp)
})
if err != nil {
return err
}
// if the username is not empty and the username is not the same as the current username, then login again
if d.Username != "" && d.Username != resp.Data.Username {
if d.Username != resp.Data.Username {
err = d.login()
if err != nil {
return err
}
}
// re-get the user info
_, err = d.request("/me", http.MethodGet, func(req *resty.Request) {
_, _, err = d.request("/me", http.MethodGet, func(req *resty.Request) {
req.SetResult(&resp)
})
if err != nil {
return err
}
if resp.Data.Role == model.GUEST {
url := d.Address + "/api/public/settings"
res, err := base.RestyClient.R().Get(url)
u := d.Address + "/api/public/settings"
res, err := base.RestyClient.R().Get(u)
if err != nil {
return err
}
@ -74,7 +76,7 @@ func (d *AListV3) Drop(ctx context.Context) error {
func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
var resp common.Resp[FsListResp]
_, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) {
_, _, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(ListReq{
PageReq: model.PageReq{
Page: 1,
@ -108,11 +110,19 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var resp common.Resp[FsGetResp]
_, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
// if PassUAToUpsteam is true, then pass the user-agent to the upstream
userAgent := base.UserAgent
if d.PassUAToUpsteam {
userAgent = args.Header.Get("user-agent")
if userAgent == "" {
userAgent = base.UserAgent
}
}
_, _, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(FsGetReq{
Path: file.GetPath(),
Password: d.MetaPassword,
})
}).SetHeader("user-agent", userAgent)
})
if err != nil {
return nil, err
@ -123,7 +133,7 @@ func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
}
func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
_, err := d.request("/fs/mkdir", http.MethodPost, func(req *resty.Request) {
_, _, err := d.request("/fs/mkdir", http.MethodPost, func(req *resty.Request) {
req.SetBody(MkdirOrLinkReq{
Path: path.Join(parentDir.GetPath(), dirName),
})
@ -132,7 +142,7 @@ func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
}
func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := d.request("/fs/move", http.MethodPost, func(req *resty.Request) {
_, _, err := d.request("/fs/move", http.MethodPost, func(req *resty.Request) {
req.SetBody(MoveCopyReq{
SrcDir: path.Dir(srcObj.GetPath()),
DstDir: dstDir.GetPath(),
@ -143,7 +153,7 @@ func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
}
func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
_, err := d.request("/fs/rename", http.MethodPost, func(req *resty.Request) {
_, _, err := d.request("/fs/rename", http.MethodPost, func(req *resty.Request) {
req.SetBody(RenameReq{
Path: srcObj.GetPath(),
Name: newName,
@ -153,7 +163,7 @@ func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string)
}
func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := d.request("/fs/copy", http.MethodPost, func(req *resty.Request) {
_, _, err := d.request("/fs/copy", http.MethodPost, func(req *resty.Request) {
req.SetBody(MoveCopyReq{
SrcDir: path.Dir(srcObj.GetPath()),
DstDir: dstDir.GetPath(),
@ -164,7 +174,7 @@ func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
}
func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
_, err := d.request("/fs/remove", http.MethodPost, func(req *resty.Request) {
_, _, err := d.request("/fs/remove", http.MethodPost, func(req *resty.Request) {
req.SetBody(RemoveReq{
Dir: path.Dir(obj.GetPath()),
Names: []string{obj.GetName()},
@ -173,13 +183,174 @@ func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
return err
}
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) {
req.SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
SetHeader("Password", d.MetaPassword).
SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
SetContentLength(true).
SetBody(io.ReadCloser(stream))
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {
reader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
Reader: s,
UpdateProgress: up,
})
req, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+"/api/fs/put", reader)
if err != nil {
return err
}
req.Header.Set("Authorization", d.Token)
req.Header.Set("File-Path", path.Join(dstDir.GetPath(), s.GetName()))
req.Header.Set("Password", d.MetaPassword)
if md5 := s.GetHash().GetHash(utils.MD5); len(md5) > 0 {
req.Header.Set("X-File-Md5", md5)
}
if sha1 := s.GetHash().GetHash(utils.SHA1); len(sha1) > 0 {
req.Header.Set("X-File-Sha1", sha1)
}
if sha256 := s.GetHash().GetHash(utils.SHA256); len(sha256) > 0 {
req.Header.Set("X-File-Sha256", sha256)
}
req.ContentLength = s.GetSize()
// client := base.NewHttpClient()
// client.Timeout = time.Hour * 6
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
bytes, err := io.ReadAll(res.Body)
if err != nil {
return err
}
log.Debugf("[alist_v3] response body: %s", string(bytes))
if res.StatusCode >= 400 {
return fmt.Errorf("request failed, status: %s", res.Status)
}
code := utils.Json.Get(bytes, "code").ToInt()
if code != 200 {
if code == 401 || code == 403 {
err = d.login()
if err != nil {
return err
}
}
return fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(bytes, "message").ToString())
}
return nil
}
func (d *AListV3) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
if !d.ForwardArchiveReq {
return nil, errs.NotImplement
}
var resp common.Resp[ArchiveMetaResp]
_, code, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(ArchiveMetaReq{
ArchivePass: args.Password,
Password: d.MetaPassword,
Path: obj.GetPath(),
Refresh: false,
})
})
if code == 202 {
return nil, errs.WrongArchivePassword
}
if err != nil {
return nil, err
}
var tree []model.ObjTree
if resp.Data.Content != nil {
tree = make([]model.ObjTree, 0, len(resp.Data.Content))
for _, content := range resp.Data.Content {
tree = append(tree, &content)
}
}
return &model.ArchiveMetaInfo{
Comment: resp.Data.Comment,
Encrypted: resp.Data.Encrypted,
Tree: tree,
}, nil
}
func (d *AListV3) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
if !d.ForwardArchiveReq {
return nil, errs.NotImplement
}
var resp common.Resp[ArchiveListResp]
_, code, err := d.request("/fs/archive/list", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(ArchiveListReq{
ArchiveMetaReq: ArchiveMetaReq{
ArchivePass: args.Password,
Password: d.MetaPassword,
Path: obj.GetPath(),
Refresh: false,
},
PageReq: model.PageReq{
Page: 1,
PerPage: 0,
},
InnerPath: args.InnerPath,
})
})
if code == 202 {
return nil, errs.WrongArchivePassword
}
if err != nil {
return nil, err
}
var files []model.Obj
for _, f := range resp.Data.Content {
file := model.ObjThumb{
Object: model.Object{
Name: f.Name,
Modified: f.Modified,
Ctime: f.Created,
Size: f.Size,
IsFolder: f.IsDir,
HashInfo: utils.FromString(f.HashInfo),
},
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
}
files = append(files, &file)
}
return files, nil
}
func (d *AListV3) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
if !d.ForwardArchiveReq {
return nil, errs.NotSupport
}
var resp common.Resp[ArchiveMetaResp]
_, _, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(ArchiveMetaReq{
ArchivePass: args.Password,
Password: d.MetaPassword,
Path: obj.GetPath(),
Refresh: false,
})
})
if err != nil {
return nil, err
}
return &model.Link{
URL: fmt.Sprintf("%s?inner=%s&pass=%s&sign=%s",
resp.Data.RawURL,
utils.EncodePath(args.InnerPath, true),
url.QueryEscape(args.Password),
resp.Data.Sign),
}, nil
}
func (d *AListV3) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error {
if !d.ForwardArchiveReq {
return errs.NotImplement
}
dir, name := path.Split(srcObj.GetPath())
_, _, err := d.request("/fs/archive/decompress", http.MethodPost, func(req *resty.Request) {
req.SetBody(DecompressReq{
ArchivePass: args.Password,
CacheFull: args.CacheFull,
DstDir: dstDir.GetPath(),
InnerPath: args.InnerPath,
Name: []string{name},
PutIntoNewDir: args.PutIntoNewDir,
SrcDir: dir,
})
})
return err
}

View File

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

View File

@ -4,6 +4,7 @@ import (
"time"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
)
type ListReq struct {
@ -81,3 +82,89 @@ type MeResp struct {
SsoId string `json:"sso_id"`
Otp bool `json:"otp"`
}
type ArchiveMetaReq struct {
ArchivePass string `json:"archive_pass"`
Password string `json:"password"`
Path string `json:"path"`
Refresh bool `json:"refresh"`
}
type TreeResp struct {
ObjResp
Children []TreeResp `json:"children"`
hashCache *utils.HashInfo
}
func (t *TreeResp) GetSize() int64 {
return t.Size
}
func (t *TreeResp) GetName() string {
return t.Name
}
func (t *TreeResp) ModTime() time.Time {
return t.Modified
}
func (t *TreeResp) CreateTime() time.Time {
return t.Created
}
func (t *TreeResp) IsDir() bool {
return t.ObjResp.IsDir
}
func (t *TreeResp) GetHash() utils.HashInfo {
return utils.FromString(t.HashInfo)
}
func (t *TreeResp) GetID() string {
return ""
}
func (t *TreeResp) GetPath() string {
return ""
}
func (t *TreeResp) GetChildren() []model.ObjTree {
ret := make([]model.ObjTree, 0, len(t.Children))
for _, child := range t.Children {
ret = append(ret, &child)
}
return ret
}
func (t *TreeResp) Thumb() string {
return t.ObjResp.Thumb
}
type ArchiveMetaResp struct {
Comment string `json:"comment"`
Encrypted bool `json:"encrypted"`
Content []TreeResp `json:"content"`
RawURL string `json:"raw_url"`
Sign string `json:"sign"`
}
type ArchiveListReq struct {
model.PageReq
ArchiveMetaReq
InnerPath string `json:"inner_path"`
}
type ArchiveListResp struct {
Content []ObjResp `json:"content"`
Total int64 `json:"total"`
}
type DecompressReq struct {
ArchivePass string `json:"archive_pass"`
CacheFull bool `json:"cache_full"`
DstDir string `json:"dst_dir"`
InnerPath string `json:"inner_path"`
Name []string `json:"name"`
PutIntoNewDir bool `json:"put_into_new_dir"`
SrcDir string `json:"src_dir"`
}

View File

@ -13,8 +13,11 @@ import (
)
func (d *AListV3) login() error {
if d.Username == "" {
return nil
}
var resp common.Resp[LoginResp]
_, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) {
_, _, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(base.Json{
"username": d.Username,
"password": d.Password,
@ -28,7 +31,7 @@ func (d *AListV3) login() error {
return nil
}
func (d *AListV3) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
func (d *AListV3) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, int, error) {
url := d.Address + "/api" + api
req := base.RestyClient.R()
req.SetHeader("Authorization", d.Token)
@ -37,22 +40,26 @@ func (d *AListV3) request(api, method string, callback base.ReqCallback, retry .
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
code := 0
if res != nil {
code = res.StatusCode()
}
return nil, code, err
}
log.Debugf("[alist_v3] response body: %s", res.String())
if res.StatusCode() >= 400 {
return nil, fmt.Errorf("request failed, status: %s", res.Status())
return nil, res.StatusCode(), 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 nil, code, err
}
return d.request(api, method, callback, true)
}
return nil, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString())
return nil, code, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString())
}
return res.Body(), nil
return res.Body(), 200, nil
}

View File

@ -7,7 +7,6 @@ import (
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/alist-org/alist/v3/internal/stream"
"io"
"math"
"math/big"
@ -20,6 +19,7 @@ import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/cron"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
@ -51,7 +51,7 @@ func (d *AliDrive) Init(ctx context.Context) error {
return err
}
// 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 {
return err
}
@ -105,7 +105,7 @@ func (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs
"file_id": file.GetID(),
"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)
}, nil)
if err != nil {
@ -113,14 +113,14 @@ func (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs
}
return &model.Link{
Header: http.Header{
"Referer": []string{"https://www.aliyundrive.com/"},
"Referer": []string{"https://www.alipan.com/"},
},
URL: utils.Json.Get(res, "url").ToString(),
}, nil
}
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{
"check_name_mode": "refuse",
"drive_id": d.DriveId,
@ -138,7 +138,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 {
_, 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{
"check_name_mode": "refuse",
"drive_id": d.DriveId,
@ -155,7 +155,7 @@ func (d *AliDrive) Copy(ctx context.Context, srcObj, dstDir 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{
"drive_id": d.DriveId,
"file_id": obj.GetID(),
@ -193,7 +193,10 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
}
if d.RapidUpload {
buf := bytes.NewBuffer(make([]byte, 0, 1024))
io.CopyN(buf, file, 1024)
_, err := utils.CopyWithBufferN(buf, file, 1024)
if err != nil {
return err
}
reqBody["pre_hash"] = utils.HashData(utils.SHA1, buf.Bytes())
if localFile != nil {
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
@ -215,7 +218,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
}
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)
}, &resp)
@ -269,7 +272,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
n, _ := io.NewSectionReader(localFile, o.Int64(), 8).Read(buf[:8])
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
_, err, e := d.request("https://api.aliyundrive.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
_, err, e := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
req.SetBody(reqBody)
}, &resp)
if err != nil && e.Code != "PreHashMatched" {
@ -285,6 +288,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
file.Reader = localFile
}
rateLimited := driver.NewLimitedUploadStream(ctx, file)
for i, partInfo := range resp.PartInfoList {
if utils.IsCanceled(ctx) {
return ctx.Err()
@ -293,7 +297,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
if d.InternalUpload {
url = partInfo.InternalUploadUrl
}
req, err := http.NewRequest("PUT", url, io.LimitReader(file, DEFAULT))
req, err := http.NewRequest("PUT", url, io.LimitReader(rateLimited, DEFAULT))
if err != nil {
return err
}
@ -302,13 +306,13 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
if err != nil {
return err
}
res.Body.Close()
_ = res.Body.Close()
if count > 0 {
up(i * 100 / count)
up(float64(i) * 100 / float64(count))
}
}
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{
"drive_id": d.DriveId,
"file_id": resp.FileId,
@ -333,10 +337,10 @@ func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}
}
switch args.Method {
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
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["url_expire_sec"] = 14400
default:

View File

@ -26,7 +26,7 @@ func (d *AliDrive) createSession() error {
state.retry = 0
return fmt.Errorf("createSession failed after three retries")
}
_, err, _ := d.request("https://api.aliyundrive.com/users/v1/users/device/create_session", http.MethodPost, func(req *resty.Request) {
_, err, _ := d.request("https://api.alipan.com/users/v1/users/device/create_session", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"deviceName": "samsung",
"modelName": "SM-G9810",
@ -42,7 +42,7 @@ func (d *AliDrive) createSession() 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
// }
@ -58,7 +58,7 @@ func (d *AliDrive) sign() {
// do others that not defined in Driver interface
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 e RespErr
_, err := base.RestyClient.R().
@ -85,7 +85,7 @@ func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp i
req := base.RestyClient.R()
state, ok := global.Load(d.UserID)
if !ok {
if url == "https://api.aliyundrive.com/v2/user/get" {
if url == "https://api.alipan.com/v2/user/get" {
state = &State{}
} else {
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{
"Authorization": "Bearer\t" + d.AccessToken,
"content-type": "application/json",
"origin": "https://www.aliyundrive.com",
"Referer": "https://aliyundrive.com/",
"origin": "https://www.alipan.com",
"Referer": "https://alipan.com/",
"X-Signature": state.signature,
"x-request-id": uuid.NewString(),
"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",
"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)
}, &resp)
@ -172,7 +172,7 @@ func (d *AliDrive) getFiles(fileId string) ([]File, 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{
"requests": []base.Json{
{

View File

@ -19,12 +19,12 @@ import (
type AliyundriveOpen struct {
model.Storage
Addition
base string
DriveId string
limitList func(ctx context.Context, data base.Json) (*Files, error)
limitLink func(ctx context.Context, file model.Obj) (*model.Link, error)
ref *AliyundriveOpen
}
func (d *AliyundriveOpen) Config() driver.Config {
@ -58,7 +58,17 @@ func (d *AliyundriveOpen) Init(ctx context.Context) error {
return nil
}
func (d *AliyundriveOpen) InitReference(storage driver.Driver) error {
refStorage, ok := storage.(*AliyundriveOpen)
if ok {
d.ref = refStorage
return nil
}
return errs.NotSupport
}
func (d *AliyundriveOpen) Drop(ctx context.Context) error {
d.ref = nil
return nil
}
@ -93,7 +103,7 @@ func (d *AliyundriveOpen) link(ctx context.Context, file model.Obj) (*model.Link
}
url = utils.Json.Get(res, "streamsUrl", d.LIVPDownloadFormat).ToString()
}
exp := time.Hour
exp := time.Minute
return &model.Link{
URL: url,
Expiration: &exp,

View File

@ -6,12 +6,12 @@ import (
)
type Addition struct {
DriveType string `json:"drive_type" type:"select" options:"default,resource,backup" default:"default"`
DriveType string `json:"drive_type" type:"select" options:"default,resource,backup" default:"resource"`
driver.RootID
RefreshToken string `json:"refresh_token" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
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"`
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"`
@ -32,11 +32,10 @@ var config = driver.Config{
DefaultRoot: "root",
NoOverwriteUpload: true,
}
var API_URL = "https://openapi.alipan.com"
func init() {
op.RegisterDriver(func() driver.Driver {
return &AliyundriveOpen{
base: "https://openapi.aliyundrive.com",
}
return &AliyundriveOpen{}
})
}

View File

@ -5,7 +5,6 @@ import (
"context"
"encoding/base64"
"fmt"
"github.com/alist-org/alist/v3/pkg/http_range"
"io"
"math"
"net/http"
@ -16,6 +15,7 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/avast/retry-go"
"github.com/go-resty/resty/v2"
@ -77,7 +77,7 @@ func (d *AliyundriveOpen) uploadPart(ctx context.Context, r io.Reader, partInfo
if err != nil {
return err
}
res.Body.Close()
_ = res.Body.Close()
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusConflict {
return fmt.Errorf("upload status: %d", res.StatusCode)
}
@ -126,7 +126,7 @@ func getProofRange(input string, size int64) (*ProofRange, error) {
}
func (d *AliyundriveOpen) calProofCode(stream model.FileStreamer) (string, error) {
proofRange, err := getProofRange(d.AccessToken, stream.GetSize())
proofRange, err := getProofRange(d.getAccessToken(), stream.GetSize())
if err != nil {
return "", err
}
@ -136,7 +136,7 @@ func (d *AliyundriveOpen) calProofCode(stream model.FileStreamer) (string, error
if err != nil {
return "", err
}
_, err = io.CopyN(buf, reader, length)
_, err = utils.CopyWithBufferN(buf, reader, length)
if err != nil {
return "", err
}
@ -164,7 +164,7 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
count := int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
createData["part_info_list"] = makePartInfos(count)
// rapid upload
rapidUpload := stream.GetSize() > 100*utils.KB && d.RapidUpload
rapidUpload := !stream.IsForceStreamUpload() && stream.GetSize() > 100*utils.KB && d.RapidUpload
if rapidUpload {
log.Debugf("[aliyundrive_open] start cal pre_hash")
// read 1024 bytes to calculate pre hash
@ -242,14 +242,18 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
if remain := stream.GetSize() - offset; length > remain {
length = remain
}
//rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
rd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
if err != nil {
return nil, err
rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
if rapidUpload {
srd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
if err != nil {
return nil, err
}
rd = utils.NewMultiReadable(srd)
}
err = retry.Do(func() error {
//rd.Reset()
return d.uploadPart(ctx, rd, createResp.PartInfoList[i])
_ = rd.Reset()
rateLimitedRd := driver.NewLimitedUploadStream(ctx, rd)
return d.uploadPart(ctx, rateLimitedRd, createResp.PartInfoList[i])
},
retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
@ -258,6 +262,7 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
return nil, err
}
offset += partSize
up(float64(i*100) / float64(count))
}
} else {
log.Debugf("[aliyundrive_open] rapid upload success, file id: %s", createResp.FileId)

View File

@ -19,7 +19,7 @@ import (
// do others that not defined in Driver interface
func (d *AliyundriveOpen) _refreshToken() (string, string, error) {
url := d.base + "/oauth/access_token"
url := API_URL + "/oauth/access_token"
if d.OauthTokenURL != "" && d.ClientID == "" {
url = d.OauthTokenURL
}
@ -74,6 +74,9 @@ func getSub(token string) (string, error) {
}
func (d *AliyundriveOpen) refreshToken() error {
if d.ref != nil {
return d.ref.refreshToken()
}
refresh, access, err := d._refreshToken()
for i := 0; i < 3; i++ {
if err == nil {
@ -86,7 +89,7 @@ func (d *AliyundriveOpen) refreshToken() error {
if err != nil {
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
op.MustSaveDriverStorage(d)
return nil
@ -100,7 +103,7 @@ func (d *AliyundriveOpen) request(uri, method string, callback base.ReqCallback,
func (d *AliyundriveOpen) requestReturnErrResp(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error, *ErrResp) {
req := base.RestyClient.R()
// TODO check whether access_token is expired
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
req.SetHeader("Authorization", "Bearer "+d.getAccessToken())
if method == http.MethodPost {
req.SetHeader("Content-Type", "application/json")
}
@ -109,7 +112,7 @@ func (d *AliyundriveOpen) requestReturnErrResp(uri, method string, callback base
}
var e ErrResp
req.SetError(&e)
res, err := req.Execute(method, d.base+uri)
res, err := req.Execute(method, API_URL+uri)
if err != nil {
if res != nil {
log.Errorf("[aliyundrive_open] request error: %s", res.String())
@ -118,7 +121,7 @@ func (d *AliyundriveOpen) requestReturnErrResp(uri, method string, callback base
}
isRetry := len(retry) > 0 && retry[0]
if e.Code != "" {
if !isRetry && (utils.SliceContains([]string{"AccessTokenInvalid", "AccessTokenExpired", "I400JD"}, e.Code) || d.AccessToken == "") {
if !isRetry && (utils.SliceContains([]string{"AccessTokenInvalid", "AccessTokenExpired", "I400JD"}, e.Code) || d.getAccessToken() == "") {
err = d.refreshToken()
if err != nil {
return nil, err, nil
@ -176,3 +179,10 @@ func getNowTime() (time.Time, string) {
nowTimeStr := nowTime.Format("2006-01-02T15:04:05.000Z")
return nowTime, nowTimeStr
}
func (d *AliyundriveOpen) getAccessToken() string {
if d.ref != nil {
return d.ref.getAccessToken()
}
return d.AccessToken
}

View File

@ -105,7 +105,7 @@ func (d *AliyundriveShare) link(ctx context.Context, file model.Obj) (*model.Lin
"share_id": d.ShareId,
}
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)
})
if err != nil {
@ -113,7 +113,7 @@ func (d *AliyundriveShare) link(ctx context.Context, file model.Obj) (*model.Lin
}
return &model.Link{
Header: http.Header{
"Referer": []string{"https://www.aliyundrive.com/"},
"Referer": []string{"https://www.alipan.com/"},
},
URL: resp.DownloadUrl,
}, nil
@ -128,9 +128,9 @@ func (d *AliyundriveShare) Other(ctx context.Context, args model.OtherArgs) (int
}
switch args.Method {
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":
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"
default:
return nil, errs.NotSupport

View File

@ -16,7 +16,7 @@ const (
)
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 e ErrorResp
_, err := base.RestyClient.R().
@ -47,7 +47,7 @@ func (d *AliyundriveShare) getShareToken() error {
var resp ShareTokenResp
_, err := base.RestyClient.R().
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 {
return err
}
@ -116,7 +116,7 @@ func (d *AliyundriveShare) getFiles(fileId string) ([]File, error) {
SetHeader("x-share-token", d.ShareToken).
SetHeader(CanaryHeaderKey, CanaryHeaderValue).
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 {
return nil, err
}

View File

@ -2,6 +2,8 @@ package drivers
import (
_ "github.com/alist-org/alist/v3/drivers/115"
_ "github.com/alist-org/alist/v3/drivers/115_open"
_ "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_link"
_ "github.com/alist-org/alist/v3/drivers/123_share"
@ -14,26 +16,41 @@ import (
_ "github.com/alist-org/alist/v3/drivers/aliyundrive"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_open"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_share"
_ "github.com/alist-org/alist/v3/drivers/azure_blob"
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
_ "github.com/alist-org/alist/v3/drivers/baidu_share"
_ "github.com/alist-org/alist/v3/drivers/chaoxing"
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
_ "github.com/alist-org/alist/v3/drivers/crypt"
_ "github.com/alist-org/alist/v3/drivers/doubao"
_ "github.com/alist-org/alist/v3/drivers/dropbox"
_ "github.com/alist-org/alist/v3/drivers/febbox"
_ "github.com/alist-org/alist/v3/drivers/ftp"
_ "github.com/alist-org/alist/v3/drivers/github"
_ "github.com/alist-org/alist/v3/drivers/github_releases"
_ "github.com/alist-org/alist/v3/drivers/google_drive"
_ "github.com/alist-org/alist/v3/drivers/google_photo"
_ "github.com/alist-org/alist/v3/drivers/halalcloud"
_ "github.com/alist-org/alist/v3/drivers/ilanzou"
_ "github.com/alist-org/alist/v3/drivers/ipfs_api"
_ "github.com/alist-org/alist/v3/drivers/kodbox"
_ "github.com/alist-org/alist/v3/drivers/lanzou"
_ "github.com/alist-org/alist/v3/drivers/lenovonas_share"
_ "github.com/alist-org/alist/v3/drivers/local"
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
_ "github.com/alist-org/alist/v3/drivers/mega"
_ "github.com/alist-org/alist/v3/drivers/misskey"
_ "github.com/alist-org/alist/v3/drivers/mopan"
_ "github.com/alist-org/alist/v3/drivers/netease_music"
_ "github.com/alist-org/alist/v3/drivers/onedrive"
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
_ "github.com/alist-org/alist/v3/drivers/onedrive_sharelink"
_ "github.com/alist-org/alist/v3/drivers/pikpak"
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
_ "github.com/alist-org/alist/v3/drivers/quark_uc_tv"
_ "github.com/alist-org/alist/v3/drivers/quqi"
_ "github.com/alist-org/alist/v3/drivers/s3"
_ "github.com/alist-org/alist/v3/drivers/seafile"
_ "github.com/alist-org/alist/v3/drivers/sftp"
@ -41,10 +58,13 @@ import (
_ "github.com/alist-org/alist/v3/drivers/teambition"
_ "github.com/alist-org/alist/v3/drivers/terabox"
_ "github.com/alist-org/alist/v3/drivers/thunder"
_ "github.com/alist-org/alist/v3/drivers/thunder_browser"
_ "github.com/alist-org/alist/v3/drivers/thunderx"
_ "github.com/alist-org/alist/v3/drivers/trainbit"
_ "github.com/alist-org/alist/v3/drivers/url_tree"
_ "github.com/alist-org/alist/v3/drivers/uss"
_ "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/weiyun"
_ "github.com/alist-org/alist/v3/drivers/wopan"

View File

@ -0,0 +1,313 @@
package azure_blob
import (
"context"
"fmt"
"io"
"path"
"regexp"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
)
// Azure Blob Storage based on the blob APIs
// Link: https://learn.microsoft.com/rest/api/storageservices/blob-service-rest-api
type AzureBlob struct {
model.Storage
Addition
client *azblob.Client
containerClient *container.Client
config driver.Config
}
// Config returns the driver configuration.
func (d *AzureBlob) Config() driver.Config {
return d.config
}
// GetAddition returns additional settings specific to Azure Blob Storage.
func (d *AzureBlob) GetAddition() driver.Additional {
return &d.Addition
}
// Init initializes the Azure Blob Storage client using shared key authentication.
func (d *AzureBlob) Init(ctx context.Context) error {
// Validate the endpoint URL
accountName := extractAccountName(d.Addition.Endpoint)
if !regexp.MustCompile(`^[a-z0-9]+$`).MatchString(accountName) {
return fmt.Errorf("invalid storage account name: must be chars of lowercase letters or numbers only")
}
credential, err := azblob.NewSharedKeyCredential(accountName, d.Addition.AccessKey)
if err != nil {
return fmt.Errorf("failed to create credential: %w", err)
}
// Check if Endpoint is just account name
endpoint := d.Addition.Endpoint
if accountName == endpoint {
endpoint = fmt.Sprintf("https://%s.blob.core.windows.net/", accountName)
}
// Initialize Azure Blob client with retry policy
client, err := azblob.NewClientWithSharedKeyCredential(endpoint, credential,
&azblob.ClientOptions{ClientOptions: azcore.ClientOptions{
Retry: policy.RetryOptions{
MaxRetries: MaxRetries,
RetryDelay: RetryDelay,
},
}})
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
d.client = client
// Ensure container exists or create it
containerName := strings.Trim(d.Addition.ContainerName, "/ \\")
if containerName == "" {
return fmt.Errorf("container name cannot be empty")
}
return d.createContainerIfNotExists(ctx, containerName)
}
// Drop releases resources associated with the Azure Blob client.
func (d *AzureBlob) Drop(ctx context.Context) error {
d.client = nil
return nil
}
// List retrieves blobs and directories under the specified path.
func (d *AzureBlob) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
prefix := ensureTrailingSlash(dir.GetPath())
pager := d.containerClient.NewListBlobsHierarchyPager("/", &container.ListBlobsHierarchyOptions{
Prefix: &prefix,
})
var objs []model.Obj
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list blobs: %w", err)
}
// Process directories
for _, blobPrefix := range page.Segment.BlobPrefixes {
objs = append(objs, &model.Object{
Name: path.Base(strings.TrimSuffix(*blobPrefix.Name, "/")),
Path: *blobPrefix.Name,
Modified: *blobPrefix.Properties.LastModified,
Ctime: *blobPrefix.Properties.CreationTime,
IsFolder: true,
})
}
// Process files
for _, blob := range page.Segment.BlobItems {
if strings.HasSuffix(*blob.Name, "/") {
continue
}
objs = append(objs, &model.Object{
Name: path.Base(*blob.Name),
Path: *blob.Name,
Size: *blob.Properties.ContentLength,
Modified: *blob.Properties.LastModified,
Ctime: *blob.Properties.CreationTime,
IsFolder: false,
})
}
}
return objs, nil
}
// Link generates a temporary SAS URL for accessing a blob.
func (d *AzureBlob) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
blobClient := d.containerClient.NewBlobClient(file.GetPath())
expireDuration := time.Hour * time.Duration(d.SignURLExpire)
sasURL, err := blobClient.GetSASURL(sas.BlobPermissions{Read: true}, time.Now().Add(expireDuration), nil)
if err != nil {
return nil, fmt.Errorf("failed to generate SAS URL: %w", err)
}
return &model.Link{URL: sasURL}, nil
}
// MakeDir creates a virtual directory by uploading an empty blob as a marker.
func (d *AzureBlob) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
dirPath := path.Join(parentDir.GetPath(), dirName)
if err := d.mkDir(ctx, dirPath); err != nil {
return nil, fmt.Errorf("failed to create directory marker: %w", err)
}
return &model.Object{
Path: dirPath,
Name: dirName,
IsFolder: true,
}, nil
}
// Move relocates an object (file or directory) to a new directory.
func (d *AzureBlob) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
srcPath := srcObj.GetPath()
dstPath := path.Join(dstDir.GetPath(), srcObj.GetName())
if err := d.moveOrRename(ctx, srcPath, dstPath, srcObj.IsDir(), srcObj.GetSize()); err != nil {
return nil, fmt.Errorf("move operation failed: %w", err)
}
return &model.Object{
Path: dstPath,
Name: srcObj.GetName(),
Modified: time.Now(),
IsFolder: srcObj.IsDir(),
Size: srcObj.GetSize(),
}, nil
}
// Rename changes the name of an existing object.
func (d *AzureBlob) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
srcPath := srcObj.GetPath()
dstPath := path.Join(path.Dir(srcPath), newName)
if err := d.moveOrRename(ctx, srcPath, dstPath, srcObj.IsDir(), srcObj.GetSize()); err != nil {
return nil, fmt.Errorf("rename operation failed: %w", err)
}
return &model.Object{
Path: dstPath,
Name: newName,
Modified: time.Now(),
IsFolder: srcObj.IsDir(),
Size: srcObj.GetSize(),
}, nil
}
// Copy duplicates an object (file or directory) to a specified destination directory.
func (d *AzureBlob) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
dstPath := path.Join(dstDir.GetPath(), srcObj.GetName())
// Handle directory copying using flat listing
if srcObj.IsDir() {
srcPrefix := srcObj.GetPath()
srcPrefix = ensureTrailingSlash(srcPrefix)
// Get all blobs under the source directory
blobs, err := d.flattenListBlobs(ctx, srcPrefix)
if err != nil {
return nil, fmt.Errorf("failed to list source directory contents: %w", err)
}
// Process each blob - copy to destination
for _, blob := range blobs {
// Skip the directory marker itself
if *blob.Name == srcPrefix {
continue
}
// Calculate relative path from source
relPath := strings.TrimPrefix(*blob.Name, srcPrefix)
itemDstPath := path.Join(dstPath, relPath)
if strings.HasSuffix(itemDstPath, "/") || (blob.Metadata["hdi_isfolder"] != nil && *blob.Metadata["hdi_isfolder"] == "true") {
// Create directory marker at destination
err := d.mkDir(ctx, itemDstPath)
if err != nil {
return nil, fmt.Errorf("failed to create directory marker [%s]: %w", itemDstPath, err)
}
} else {
// Copy the blob
if err := d.copyFile(ctx, *blob.Name, itemDstPath); err != nil {
return nil, fmt.Errorf("failed to copy %s: %w", *blob.Name, err)
}
}
}
// Create directory marker at destination if needed
if len(blobs) == 0 {
err := d.mkDir(ctx, dstPath)
if err != nil {
return nil, fmt.Errorf("failed to create directory [%s]: %w", dstPath, err)
}
}
return &model.Object{
Path: dstPath,
Name: srcObj.GetName(),
Modified: time.Now(),
IsFolder: true,
}, nil
}
// Copy a single file
if err := d.copyFile(ctx, srcObj.GetPath(), dstPath); err != nil {
return nil, fmt.Errorf("failed to copy blob: %w", err)
}
return &model.Object{
Path: dstPath,
Name: srcObj.GetName(),
Size: srcObj.GetSize(),
Modified: time.Now(),
IsFolder: false,
}, nil
}
// Remove deletes a specified blob or recursively deletes a directory and its contents.
func (d *AzureBlob) Remove(ctx context.Context, obj model.Obj) error {
path := obj.GetPath()
// Handle recursive directory deletion
if obj.IsDir() {
return d.deleteFolder(ctx, path)
}
// Delete single file
return d.deleteFile(ctx, path, false)
}
// Put uploads a file stream to Azure Blob Storage with progress tracking.
func (d *AzureBlob) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
blobPath := path.Join(dstDir.GetPath(), stream.GetName())
blobClient := d.containerClient.NewBlockBlobClient(blobPath)
// Determine optimal upload options based on file size
options := optimizedUploadOptions(stream.GetSize())
// Track upload progress
progressTracker := &progressTracker{
total: stream.GetSize(),
updateProgress: up,
}
// Wrap stream to handle context cancellation and progress tracking
limitedStream := driver.NewLimitedUploadStream(ctx, io.TeeReader(stream, progressTracker))
// Upload the stream to Azure Blob Storage
_, err := blobClient.UploadStream(ctx, limitedStream, options)
if err != nil {
return nil, fmt.Errorf("failed to upload file: %w", err)
}
return &model.Object{
Path: blobPath,
Name: stream.GetName(),
Size: stream.GetSize(),
Modified: time.Now(),
IsFolder: false,
}, nil
}
// The following methods related to archive handling are not implemented yet.
// func (d *AzureBlob) GetArchiveMeta(...) {...}
// func (d *AzureBlob) ListArchive(...) {...}
// func (d *AzureBlob) Extract(...) {...}
// func (d *AzureBlob) ArchiveDecompress(...) {...}
// Ensure AzureBlob implements the driver.Driver interface.
var _ driver.Driver = (*AzureBlob)(nil)

View File

@ -0,0 +1,27 @@
package azure_blob
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
Endpoint string `json:"endpoint" required:"true" default:"https://<accountname>.blob.core.windows.net/" help:"e.g. https://accountname.blob.core.windows.net/. The full endpoint URL for Azure Storage, including the unique storage account name (3 ~ 24 numbers and lowercase letters only)."`
AccessKey string `json:"access_key" required:"true" help:"The access key for Azure Storage, used for authentication. https://learn.microsoft.com/azure/storage/common/storage-account-keys-manage"`
ContainerName string `json:"container_name" required:"true" help:"The name of the container in Azure Storage (created in the Azure portal). https://learn.microsoft.com/azure/storage/blobs/blob-containers-portal"`
SignURLExpire int `json:"sign_url_expire" type:"number" default:"4" help:"The expiration time for SAS URLs, in hours."`
}
var config = driver.Config{
Name: "Azure Blob Storage",
LocalSort: true,
CheckStatus: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &AzureBlob{
config: config,
}
})
}

View File

@ -0,0 +1,20 @@
package azure_blob
import "github.com/alist-org/alist/v3/internal/driver"
// progressTracker is used to track upload progress
type progressTracker struct {
total int64
current int64
updateProgress driver.UpdateProgress
}
// Write implements io.Writer to track progress
func (pt *progressTracker) Write(p []byte) (n int, err error) {
n = len(p)
pt.current += int64(n)
if pt.updateProgress != nil && pt.total > 0 {
pt.updateProgress(float64(pt.current) * 100 / float64(pt.total))
}
return n, nil
}

401
drivers/azure_blob/util.go Normal file
View File

@ -0,0 +1,401 @@
package azure_blob
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"path"
"sort"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/service"
log "github.com/sirupsen/logrus"
)
const (
// MaxRetries defines the maximum number of retry attempts for Azure operations
MaxRetries = 3
// RetryDelay defines the base delay between retries
RetryDelay = 3 * time.Second
// MaxBatchSize defines the maximum number of operations in a single batch request
MaxBatchSize = 128
)
// extractAccountName 从 Azure 存储 Endpoint 中提取账户名
func extractAccountName(endpoint string) string {
// 移除协议前缀
endpoint = strings.TrimPrefix(endpoint, "https://")
endpoint = strings.TrimPrefix(endpoint, "http://")
// 获取第一个点之前的部分(即账户名)
parts := strings.Split(endpoint, ".")
if len(parts) > 0 {
// to lower case
return strings.ToLower(parts[0])
}
return ""
}
// isNotFoundError checks if the error is a "not found" type error
func isNotFoundError(err error) bool {
var storageErr *azcore.ResponseError
if errors.As(err, &storageErr) {
return storageErr.StatusCode == 404
}
// Fallback to string matching for backwards compatibility
return err != nil && strings.Contains(err.Error(), "BlobNotFound")
}
// flattenListBlobs - Optimize blob listing to handle pagination better
func (d *AzureBlob) flattenListBlobs(ctx context.Context, prefix string) ([]container.BlobItem, error) {
// Standardize prefix format
prefix = ensureTrailingSlash(prefix)
var blobItems []container.BlobItem
pager := d.containerClient.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{
Prefix: &prefix,
Include: container.ListBlobsInclude{
Metadata: true,
},
})
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list blobs: %w", err)
}
for _, blob := range page.Segment.BlobItems {
blobItems = append(blobItems, *blob)
}
}
return blobItems, nil
}
// batchDeleteBlobs - Simplify batch deletion logic
func (d *AzureBlob) batchDeleteBlobs(ctx context.Context, blobPaths []string) error {
if len(blobPaths) == 0 {
return nil
}
// Process in batches of MaxBatchSize
for i := 0; i < len(blobPaths); i += MaxBatchSize {
end := min(i+MaxBatchSize, len(blobPaths))
currentBatch := blobPaths[i:end]
// Create batch builder
batchBuilder, err := d.containerClient.NewBatchBuilder()
if err != nil {
return fmt.Errorf("failed to create batch builder: %w", err)
}
// Add delete operations
for _, blobPath := range currentBatch {
if err := batchBuilder.Delete(blobPath, nil); err != nil {
return fmt.Errorf("failed to add delete operation for %s: %w", blobPath, err)
}
}
// Submit batch
responses, err := d.containerClient.SubmitBatch(ctx, batchBuilder, nil)
if err != nil {
return fmt.Errorf("batch delete request failed: %w", err)
}
// Check responses
for _, resp := range responses.Responses {
if resp.Error != nil && !isNotFoundError(resp.Error) {
// 获取 blob 名称以提供更好的错误信息
blobName := "unknown"
if resp.BlobName != nil {
blobName = *resp.BlobName
}
return fmt.Errorf("failed to delete blob %s: %v", blobName, resp.Error)
}
}
}
return nil
}
// deleteFolder recursively deletes a directory and all its contents
func (d *AzureBlob) deleteFolder(ctx context.Context, prefix string) error {
// Ensure directory path ends with slash
prefix = ensureTrailingSlash(prefix)
// Get all blobs under the directory using flattenListBlobs
globs, err := d.flattenListBlobs(ctx, prefix)
if err != nil {
return fmt.Errorf("failed to list blobs for deletion: %w", err)
}
// If there are blobs in the directory, delete them
if len(globs) > 0 {
// 分离文件和目录标记
var filePaths []string
var dirPaths []string
for _, blob := range globs {
blobName := *blob.Name
if isDirectory(blob) {
// remove trailing slash for directory names
dirPaths = append(dirPaths, strings.TrimSuffix(blobName, "/"))
} else {
filePaths = append(filePaths, blobName)
}
}
// 先删除文件,再删除目录
if len(filePaths) > 0 {
if err := d.batchDeleteBlobs(ctx, filePaths); err != nil {
return err
}
}
if len(dirPaths) > 0 {
// 按路径深度分组
depthMap := make(map[int][]string)
for _, dir := range dirPaths {
depth := strings.Count(dir, "/") // 计算目录深度
depthMap[depth] = append(depthMap[depth], dir)
}
// 按深度从大到小排序
var depths []int
for depth := range depthMap {
depths = append(depths, depth)
}
sort.Sort(sort.Reverse(sort.IntSlice(depths)))
// 按深度逐层批量删除
for _, depth := range depths {
batch := depthMap[depth]
if err := d.batchDeleteBlobs(ctx, batch); err != nil {
return err
}
}
}
}
// 最后删除目录标记本身
return d.deleteEmptyDirectory(ctx, prefix)
}
// deleteFile deletes a single file or blob with better error handling
func (d *AzureBlob) deleteFile(ctx context.Context, path string, isDir bool) error {
blobClient := d.containerClient.NewBlobClient(path)
_, err := blobClient.Delete(ctx, nil)
if err != nil && !(isDir && isNotFoundError(err)) {
return err
}
return nil
}
// copyFile copies a single blob from source path to destination path
func (d *AzureBlob) copyFile(ctx context.Context, srcPath, dstPath string) error {
srcBlob := d.containerClient.NewBlobClient(srcPath)
dstBlob := d.containerClient.NewBlobClient(dstPath)
// Use configured expiration time for SAS URL
expireDuration := time.Hour * time.Duration(d.SignURLExpire)
srcURL, err := srcBlob.GetSASURL(sas.BlobPermissions{Read: true}, time.Now().Add(expireDuration), nil)
if err != nil {
return fmt.Errorf("failed to generate source SAS URL: %w", err)
}
_, err = dstBlob.StartCopyFromURL(ctx, srcURL, nil)
return err
}
// createContainerIfNotExists - Create container if not exists
// Clean up commented code
func (d *AzureBlob) createContainerIfNotExists(ctx context.Context, containerName string) error {
serviceClient := d.client.ServiceClient()
containerClient := serviceClient.NewContainerClient(containerName)
var options = service.CreateContainerOptions{}
_, err := containerClient.Create(ctx, &options)
if err != nil {
var responseErr *azcore.ResponseError
if errors.As(err, &responseErr) && responseErr.ErrorCode != "ContainerAlreadyExists" {
return fmt.Errorf("failed to create or access container [%s]: %w", containerName, err)
}
}
d.containerClient = containerClient
return nil
}
// mkDir creates a virtual directory marker by uploading an empty blob with metadata.
func (d *AzureBlob) mkDir(ctx context.Context, fullDirName string) error {
dirPath := ensureTrailingSlash(fullDirName)
blobClient := d.containerClient.NewBlockBlobClient(dirPath)
// Upload an empty blob with metadata indicating it's a directory
_, err := blobClient.Upload(ctx, struct {
*bytes.Reader
io.Closer
}{
Reader: bytes.NewReader([]byte{}),
Closer: io.NopCloser(nil),
}, &blockblob.UploadOptions{
Metadata: map[string]*string{
"hdi_isfolder": to.Ptr("true"),
},
})
return err
}
// ensureTrailingSlash ensures the provided path ends with a trailing slash.
func ensureTrailingSlash(path string) string {
if !strings.HasSuffix(path, "/") {
return path + "/"
}
return path
}
// moveOrRename moves or renames blobs or directories from source to destination.
func (d *AzureBlob) moveOrRename(ctx context.Context, srcPath, dstPath string, isDir bool, srcSize int64) error {
if isDir {
// Normalize paths for directory operations
srcPath = ensureTrailingSlash(srcPath)
dstPath = ensureTrailingSlash(dstPath)
// List all blobs under the source directory
blobs, err := d.flattenListBlobs(ctx, srcPath)
if err != nil {
return fmt.Errorf("failed to list blobs: %w", err)
}
// Iterate and copy each blob to the destination
for _, item := range blobs {
srcBlobName := *item.Name
relPath := strings.TrimPrefix(srcBlobName, srcPath)
itemDstPath := path.Join(dstPath, relPath)
if isDirectory(item) {
// Create directory marker at destination
if err := d.mkDir(ctx, itemDstPath); err != nil {
return fmt.Errorf("failed to create directory marker [%s]: %w", itemDstPath, err)
}
} else {
// Copy file blob to destination
if err := d.copyFile(ctx, srcBlobName, itemDstPath); err != nil {
return fmt.Errorf("failed to copy blob [%s]: %w", srcBlobName, err)
}
}
}
// Handle empty directories by creating a marker at destination
if len(blobs) == 0 {
if err := d.mkDir(ctx, dstPath); err != nil {
return fmt.Errorf("failed to create directory [%s]: %w", dstPath, err)
}
}
// Delete source directory and its contents
if err := d.deleteFolder(ctx, srcPath); err != nil {
log.Warnf("failed to delete source directory [%s]: %v\n, and try again", srcPath, err)
// Retry deletion once more and ignore the result
if err := d.deleteFolder(ctx, srcPath); err != nil {
log.Errorf("Retry deletion of source directory [%s] failed: %v", srcPath, err)
}
}
return nil
}
// Single file move or rename operation
if err := d.copyFile(ctx, srcPath, dstPath); err != nil {
return fmt.Errorf("failed to copy file: %w", err)
}
// Delete source file after successful copy
if err := d.deleteFile(ctx, srcPath, false); err != nil {
log.Errorf("Error deleting source file [%s]: %v", srcPath, err)
}
return nil
}
// optimizedUploadOptions returns the optimal upload options based on file size
func optimizedUploadOptions(fileSize int64) *azblob.UploadStreamOptions {
options := &azblob.UploadStreamOptions{
BlockSize: 4 * 1024 * 1024, // 4MB block size
Concurrency: 4, // Default concurrency
}
// For large files, increase block size and concurrency
if fileSize > 256*1024*1024 { // For files larger than 256MB
options.BlockSize = 8 * 1024 * 1024 // 8MB blocks
options.Concurrency = 8 // More concurrent uploads
}
// For very large files (>1GB)
if fileSize > 1024*1024*1024 {
options.BlockSize = 16 * 1024 * 1024 // 16MB blocks
options.Concurrency = 16 // Higher concurrency
}
return options
}
// isDirectory determines if a blob represents a directory
// Checks multiple indicators: path suffix, metadata, and content type
func isDirectory(blob container.BlobItem) bool {
// Check path suffix
if strings.HasSuffix(*blob.Name, "/") {
return true
}
// Check metadata for directory marker
if blob.Metadata != nil {
if val, ok := blob.Metadata["hdi_isfolder"]; ok && val != nil && *val == "true" {
return true
}
// Azure Storage Explorer and other tools may use different metadata keys
if val, ok := blob.Metadata["is_directory"]; ok && val != nil && strings.ToLower(*val) == "true" {
return true
}
}
// Check content type (some tools mark directories with specific content types)
if blob.Properties != nil && blob.Properties.ContentType != nil {
contentType := strings.ToLower(*blob.Properties.ContentType)
if blob.Properties.ContentLength != nil && *blob.Properties.ContentLength == 0 && (contentType == "application/directory" || contentType == "directory") {
return true
}
}
return false
}
// deleteEmptyDirectory deletes a directory only if it's empty
func (d *AzureBlob) deleteEmptyDirectory(ctx context.Context, dirPath string) error {
// Directory is empty, delete the directory marker
blobClient := d.containerClient.NewBlobClient(strings.TrimSuffix(dirPath, "/"))
_, err := blobClient.Delete(ctx, nil)
// Also try deleting with trailing slash (for different directory marker formats)
if err != nil && isNotFoundError(err) {
blobClient = d.containerClient.NewBlobClient(dirPath)
_, err = blobClient.Delete(ctx, nil)
}
// Ignore not found errors
if err != nil && isNotFoundError(err) {
log.Infof("Directory [%s] not found during deletion: %v", dirPath, err)
return nil
}
return err
}

View File

@ -5,7 +5,6 @@ import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"math"
"net/url"
@ -13,6 +12,8 @@ import (
"strconv"
"time"
"golang.org/x/sync/semaphore"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
@ -28,10 +29,9 @@ type BaiduNetdisk struct {
Addition
uploadThread int
vipType int // 会员类型0普通用户(4G/4M)、1普通会员(10G/16M)、2超级会员(20G/32M)
}
const DefaultSliceSize int64 = 4 * utils.MB
func (d *BaiduNetdisk) Config() driver.Config {
return config
}
@ -54,7 +54,11 @@ func (d *BaiduNetdisk) Init(ctx context.Context) error {
"method": "uinfo",
}, nil)
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 {
@ -74,6 +78,8 @@ func (d *BaiduNetdisk) List(ctx context.Context, dir model.Obj, args model.ListA
func (d *BaiduNetdisk) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if d.DownloadAPI == "crack" {
return d.linkCrack(file, args)
} else if d.DownloadAPI == "crack_video" {
return d.linkCrackVideo(file, args)
}
return d.linkOfficial(file, args)
}
@ -153,26 +159,26 @@ func (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream mo
}
streamSize := stream.GetSize()
rawPath := stdpath.Join(dstDir.GetPath(), stream.GetName())
path := encodeURIComponent(rawPath)
path := stdpath.Join(dstDir.GetPath(), stream.GetName())
mtime := stream.ModTime().Unix()
ctime := stream.CreateTime().Unix()
blockList, _ := utils.Json.MarshalToString([]string{contentMd5})
data := fmt.Sprintf("path=%s&size=%d&isdir=0&rtype=3&block_list=%s&local_mtime=%d&local_ctime=%d",
path, streamSize, blockList, mtime, ctime)
params := map[string]string{
"method": "create",
}
log.Debugf("[baidu_netdisk] precreate data: %s", data)
var newFile File
_, err := d.post("/xpan/file", params, data, &newFile)
_, 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 {
@ -185,17 +191,18 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
}
streamSize := stream.GetSize()
count := int(math.Max(math.Ceil(float64(streamSize)/float64(DefaultSliceSize)), 1))
lastBlockSize := streamSize % DefaultSliceSize
sliceSize := d.getSliceSize(streamSize)
count := int(math.Max(math.Ceil(float64(streamSize)/float64(sliceSize)), 1))
lastBlockSize := streamSize % sliceSize
if streamSize > 0 && lastBlockSize == 0 {
lastBlockSize = DefaultSliceSize
lastBlockSize = sliceSize
}
//cal md5 for first 256k data
const SliceSize int64 = 256 * 1024
const SliceSize int64 = 256 * utils.KB
// cal md5
blockList := make([]string, 0, count)
byteSize := DefaultSliceSize
byteSize := sliceSize
fileMd5H := md5.New()
sliceMd5H := md5.New()
sliceMd5H2 := md5.New()
@ -208,7 +215,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
if i == count {
byteSize = lastBlockSize
}
_, err := io.CopyN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
if err != nil && err != io.EOF {
return nil, err
}
@ -218,9 +225,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
contentMd5 := hex.EncodeToString(fileMd5H.Sum(nil))
sliceMd5 := hex.EncodeToString(sliceMd5H2.Sum(nil))
blockListStr, _ := utils.Json.MarshalToString(blockList)
rawPath := stdpath.Join(dstDir.GetPath(), stream.GetName())
path := encodeURIComponent(rawPath)
path := stdpath.Join(dstDir.GetPath(), stream.GetName())
mtime := stream.ModTime().Unix()
ctime := stream.CreateTime().Unix()
@ -228,40 +233,55 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
// 尝试获取之前的进度
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5)
if !ok {
data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&rtype=3&block_list=%s&content-md5=%s&slice-md5=%s&local_mtime=%d&local_ctime=%d",
path, streamSize, blockListStr, contentMd5, sliceMd5, mtime, ctime)
params := map[string]string{
"method": "precreate",
}
log.Debugf("[baidu_netdisk] precreate data: %s", data)
_, err = d.post("/xpan/file", params, data, &precreateResp)
form := map[string]string{
"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 {
return nil, err
}
log.Debugf("%+v", precreateResp)
if precreateResp.ReturnType == 2 {
//rapid upload, since got md5 match from baidu server
if err != nil {
return nil, err
}
// 修复时间,具体原因见 Put 方法注释的 **注意**
precreateResp.File.Ctime = ctime
precreateResp.File.Mtime = mtime
return fileToObj(precreateResp.File), nil
}
}
// step.2 上传分片
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
retry.Attempts(3),
retry.Attempts(1),
retry.Delay(time.Second),
retry.DelayType(retry.BackOffDelay))
sem := semaphore.NewWeighted(3)
for i, partseq := range precreateResp.BlockList {
if utils.IsCanceled(upCtx) {
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 {
byteSize = lastBlockSize
}
threadG.Go(func(ctx context.Context) error {
if err = sem.Acquire(ctx, 1); err != nil {
return err
}
defer sem.Release(1)
params := map[string]string{
"method": "upload",
"access_token": d.AccessToken,
@ -270,11 +290,12 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
"uploadid": precreateResp.Uploadid,
"partseq": strconv.Itoa(partseq),
}
err := d.uploadSlice(ctx, params, stream.GetName(), io.NewSectionReader(tempFile, offset, byteSize))
err := d.uploadSlice(ctx, params, stream.GetName(),
driver.NewLimitedUploadStream(ctx, io.NewSectionReader(tempFile, offset, byteSize)))
if err != nil {
return err
}
up(int(threadG.Success()) * 100 / len(precreateResp.BlockList))
up(float64(threadG.Success()) * 100 / float64(len(precreateResp.BlockList)))
precreateResp.BlockList[i] = -1
return nil
})
@ -290,10 +311,13 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
// step.3 创建文件
var newFile File
_, err = d.create(rawPath, streamSize, 0, precreateResp.Uploadid, blockListStr, &newFile, mtime, ctime)
_, err = d.create(path, streamSize, 0, precreateResp.Uploadid, blockListStr, &newFile, mtime, ctime)
if err != nil {
return nil, err
}
// 修复时间,具体原因见 Put 方法注释的 **注意**
newFile.Ctime = ctime
newFile.Mtime = mtime
return fileToObj(newFile), nil
}

View File

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

View File

@ -1,12 +1,12 @@
package baidu_netdisk
import (
"github.com/alist-org/alist/v3/pkg/utils"
"path"
"strconv"
"time"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
)
type TokenErrResp struct {
@ -17,7 +17,7 @@ type TokenErrResp struct {
type File struct {
//TkbindId int `json:"tkbind_id"`
//OwnerType int `json:"owner_type"`
//Category int `json:"category"`
Category int `json:"category"`
//RealCategory string `json:"real_category"`
FsId int64 `json:"fs_id"`
//OperId int `json:"oper_id"`
@ -56,11 +56,11 @@ func fileToObj(f File) *model.ObjThumb {
if f.ServerFilename == "" {
f.ServerFilename = path.Base(f.Path)
}
if f.LocalCtime == 0 {
f.LocalCtime = f.Ctime
if f.ServerCtime == 0 {
f.ServerCtime = f.Ctime
}
if f.LocalMtime == 0 {
f.LocalMtime = f.Mtime
if f.ServerMtime == 0 {
f.ServerMtime = f.Mtime
}
return &model.ObjThumb{
Object: model.Object{
@ -68,10 +68,12 @@ func fileToObj(f File) *model.ObjThumb {
Path: f.Path,
Name: f.ServerFilename,
Size: f.Size,
Modified: time.Unix(f.LocalMtime, 0),
Ctime: time.Unix(f.LocalCtime, 0),
Modified: time.Unix(f.ServerMtime, 0),
Ctime: time.Unix(f.ServerCtime, 0),
IsFolder: f.Isdir == 1,
HashInfo: utils.NewHashInfo(utils.MD5, f.Md5),
// 直接获取的MD5是错误的
HashInfo: utils.NewHashInfo(utils.MD5, DecryptMd5(f.Md5)),
},
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbs.Url3},
}

View File

@ -1,13 +1,14 @@
package baidu_netdisk
import (
"encoding/hex"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"unicode"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/errs"
@ -78,6 +79,12 @@ func (d *BaiduNetdisk) request(furl string, method string, callback base.ReqCall
return retry.Unrecoverable(err2)
}
}
if 31023 == errno && d.DownloadAPI == "crack_video" {
result = res.Body()
return nil
}
return fmt.Errorf("req: [%s] ,errno: %d, refer to https://pan.baidu.com/union/doc/", furl, errno)
}
result = res.Body()
@ -96,10 +103,10 @@ func (d *BaiduNetdisk) get(pathname string, params map[string]string, resp inter
}, 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) {
req.SetQueryParams(params)
req.SetBody(data)
req.SetFormData(form)
}, resp)
}
@ -130,12 +137,21 @@ func (d *BaiduNetdisk) getFiles(dir string) ([]File, error) {
if len(resp.List) == 0 {
break
}
res = append(res, resp.List...)
if d.OnlyListVideoFile {
for _, file := range resp.List {
if file.Isdir == 1 || file.Category == 1 {
res = append(res, file)
}
}
} else {
res = append(res, resp.List...)
}
}
return res, nil
}
func (d *BaiduNetdisk) linkOfficial(file model.Obj, args model.LinkArgs) (*model.Link, error) {
func (d *BaiduNetdisk) linkOfficial(file model.Obj, _ model.LinkArgs) (*model.Link, error) {
var resp DownloadResp
params := map[string]string{
"method": "filemetas",
@ -154,6 +170,7 @@ func (d *BaiduNetdisk) linkOfficial(file model.Obj, args model.LinkArgs) (*model
//if res.StatusCode() == 302 {
u = res.Header().Get("location")
//}
return &model.Link{
URL: u,
Header: http.Header{
@ -162,7 +179,7 @@ func (d *BaiduNetdisk) linkOfficial(file model.Obj, args model.LinkArgs) (*model
}, nil
}
func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Link, error) {
func (d *BaiduNetdisk) linkCrack(file model.Obj, _ model.LinkArgs) (*model.Link, error) {
var resp DownloadResp2
param := map[string]string{
"target": fmt.Sprintf("[\"%s\"]", file.GetPath()),
@ -176,6 +193,7 @@ func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Li
if err != nil {
return nil, err
}
return &model.Link{
URL: resp.Info[0].Dlink,
Header: http.Header{
@ -184,35 +202,184 @@ func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Li
}, nil
}
func (d *BaiduNetdisk) linkCrackVideo(file model.Obj, _ model.LinkArgs) (*model.Link, error) {
param := map[string]string{
"type": "VideoURL",
"path": fmt.Sprintf("%s", file.GetPath()),
"fs_id": file.GetID(),
"devuid": "0%1",
"clienttype": "1",
"channel": "android_15_25010PN30C_bd-netdisk_1523a",
"nom3u8": "1",
"dlink": "1",
"media": "1",
"origin": "dlna",
}
resp, err := d.request("https://pan.baidu.com/api/mediainfo", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(param)
}, nil)
if err != nil {
return nil, err
}
return &model.Link{
URL: utils.Json.Get(resp, "info", "dlink").ToString(),
Header: http.Header{
"User-Agent": []string{d.CustomCrackUA},
},
}, nil
}
func (d *BaiduNetdisk) manage(opera string, filelist any) ([]byte, error) {
params := map[string]string{
"method": "filemanager",
"opera": opera,
}
marshal, _ := utils.Json.MarshalToString(filelist)
data := fmt.Sprintf("async=0&filelist=%s&ondup=fail", marshal)
return d.post("/xpan/file", params, data, nil)
return d.postForm("/xpan/file", params, map[string]string{
"async": "0",
"filelist": marshal,
"ondup": "fail",
}, nil)
}
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{
"method": "create",
}
data := ""
if mtime == 0 || ctime == 0 {
data = fmt.Sprintf("path=%s&size=%d&isdir=%d&rtype=3", encodeURIComponent(path), size, isdir)
} else {
data = fmt.Sprintf("path=%s&size=%d&isdir=%d&rtype=3&local_mtime=%d&local_ctime=%d", encodeURIComponent(path), size, isdir, mtime, ctime)
form := map[string]string{
"path": path,
"size": strconv.FormatInt(size, 10),
"isdir": strconv.Itoa(isdir),
"rtype": "3",
}
if mtime != 0 && ctime != 0 {
joinTime(form, ctime, mtime)
}
if uploadid != "" {
data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list)
form["uploadid"] = uploadid
}
return d.post("/xpan/file", params, data, resp)
if block_list != "" {
form["block_list"] = block_list
}
return d.postForm("/xpan/file", params, form, resp)
}
func encodeURIComponent(str string) string {
r := url.QueryEscape(str)
r = strings.ReplaceAll(r, "+", "%20")
return r
func joinTime(form map[string]string, ctime, mtime int64) {
form["local_mtime"] = strconv.FormatInt(mtime, 10)
form["local_ctime"] = strconv.FormatInt(ctime, 10)
}
const (
DefaultSliceSize int64 = 4 * utils.MB
VipSliceSize int64 = 16 * utils.MB
SVipSliceSize int64 = 32 * utils.MB
MaxSliceNum = 2048 // 文档写的是 1024/没写 ,但实际测试是 2048
SliceStep int64 = 1 * utils.MB
)
func (d *BaiduNetdisk) getSliceSize(filesize int64) int64 {
// 非会员固定为 4MB
if d.vipType == 0 {
if d.CustomUploadPartSize != 0 {
log.Warnf("CustomUploadPartSize is not supported for non-vip user, use DefaultSliceSize")
}
if filesize > MaxSliceNum*DefaultSliceSize {
log.Warnf("File size(%d) is too large, may cause upload failure", filesize)
}
return DefaultSliceSize
}
if d.CustomUploadPartSize != 0 {
if d.CustomUploadPartSize < DefaultSliceSize {
log.Warnf("CustomUploadPartSize(%d) is less than DefaultSliceSize(%d), use DefaultSliceSize", d.CustomUploadPartSize, DefaultSliceSize)
return DefaultSliceSize
}
if d.vipType == 1 && d.CustomUploadPartSize > VipSliceSize {
log.Warnf("CustomUploadPartSize(%d) is greater than VipSliceSize(%d), use VipSliceSize", d.CustomUploadPartSize, VipSliceSize)
return VipSliceSize
}
if d.vipType == 2 && d.CustomUploadPartSize > SVipSliceSize {
log.Warnf("CustomUploadPartSize(%d) is greater than SVipSliceSize(%d), use SVipSliceSize", d.CustomUploadPartSize, SVipSliceSize)
return SVipSliceSize
}
return d.CustomUploadPartSize
}
maxSliceSize := DefaultSliceSize
switch d.vipType {
case 1:
maxSliceSize = VipSliceSize
case 2:
maxSliceSize = SVipSliceSize
}
// upload on low bandwidth
if d.LowBandwithUploadMode {
size := DefaultSliceSize
for size <= maxSliceSize {
if filesize <= MaxSliceNum*size {
return size
}
size += SliceStep
}
}
if filesize > MaxSliceNum*maxSliceSize {
log.Warnf("File size(%d) is too large, may cause upload failure", filesize)
}
return maxSliceSize
}
// func encodeURIComponent(str string) string {
// r := url.QueryEscape(str)
// r = strings.ReplaceAll(r, "+", "%20")
// return r
// }
func DecryptMd5(encryptMd5 string) string {
if _, err := hex.DecodeString(encryptMd5); err == nil {
return encryptMd5
}
var out strings.Builder
out.Grow(len(encryptMd5))
for i, n := 0, int64(0); i < len(encryptMd5); i++ {
if i == 9 {
n = int64(unicode.ToLower(rune(encryptMd5[i])) - 'g')
} else {
n, _ = strconv.ParseInt(encryptMd5[i:i+1], 16, 64)
}
out.WriteString(strconv.FormatInt(n^int64(15&i), 16))
}
encryptMd5 = out.String()
return encryptMd5[8:16] + encryptMd5[:8] + encryptMd5[24:32] + encryptMd5[16:24]
}
func EncryptMd5(originalMd5 string) string {
reversed := originalMd5[8:16] + originalMd5[:8] + originalMd5[24:32] + originalMd5[16:24]
var out strings.Builder
out.Grow(len(reversed))
for i, n := 0, int64(0); i < len(reversed); i++ {
n, _ = strconv.ParseInt(reversed[i:i+1], 16, 64)
n ^= int64(15 & i)
if i == 9 {
out.WriteRune(rune(n) + 'g')
} else {
out.WriteString(strconv.FormatInt(n, 16))
}
}
return out.String()
}

View File

@ -13,6 +13,8 @@ import (
"strings"
"time"
"golang.org/x/sync/semaphore"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
@ -27,9 +29,10 @@ type BaiduPhoto struct {
model.Storage
Addition
AccessToken string
Uk int64
root model.Obj
// AccessToken string
Uk int64
bdstoken string
root model.Obj
uploadThread int
}
@ -48,9 +51,9 @@ func (d *BaiduPhoto) Init(ctx context.Context) error {
d.uploadThread, d.UploadThread = 3, "3"
}
if err := d.refreshToken(); err != nil {
return err
}
// if err := d.refreshToken(); err != nil {
// return err
// }
// root
if d.AlbumID != "" {
@ -73,6 +76,10 @@ func (d *BaiduPhoto) Init(ctx context.Context) error {
if err != nil {
return err
}
d.bdstoken, err = d.getBDStoken()
if err != nil {
return err
}
d.Uk, err = strconv.ParseInt(info.YouaID, 10, 64)
return err
}
@ -82,7 +89,7 @@ func (d *BaiduPhoto) GetRoot(ctx context.Context) (model.Obj, error) {
}
func (d *BaiduPhoto) Drop(ctx context.Context) error {
d.AccessToken = ""
// d.AccessToken = ""
d.Uk = 0
d.root = nil
return nil
@ -137,13 +144,18 @@ func (d *BaiduPhoto) Link(ctx context.Context, file model.Obj, args model.LinkAr
case *File:
return d.linkFile(ctx, file, args)
case *AlbumFile:
f, err := d.CopyAlbumFile(ctx, file)
if err != nil {
return nil, err
// 处理共享相册
if d.Uk != file.Uk {
// 有概率无法获取到链接
// return d.linkAlbum(ctx, file, args)
f, err := d.CopyAlbumFile(ctx, file)
if err != nil {
return nil, err
}
return d.linkFile(ctx, f, args)
}
return d.linkFile(ctx, f, args)
// 有概率无法获取到链接
//return d.linkAlbum(ctx, file, args)
return d.linkFile(ctx, &file.File, args)
}
return nil, errs.NotFile
}
@ -261,7 +273,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
if i == count {
byteSize = lastBlockSize
}
_, err := io.CopyN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
if err != nil && err != io.EOF {
return nil, err
}
@ -286,11 +298,12 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
}
// 尝试获取之前的进度
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5)
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, strconv.FormatInt(d.Uk, 10), contentMd5)
if !ok {
_, err = d.Post(FILE_API_URL_V1+"/precreate", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(params)
r.SetQueryParam("bdstoken", d.bdstoken)
}, &precreateResp)
if err != nil {
return nil, err
@ -303,6 +316,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
retry.Attempts(3),
retry.Delay(time.Second),
retry.DelayType(retry.BackOffDelay))
sem := semaphore.NewWeighted(3)
for i, partseq := range precreateResp.BlockList {
if utils.IsCanceled(upCtx) {
break
@ -314,22 +328,27 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
}
threadG.Go(func(ctx context.Context) error {
if err = sem.Acquire(ctx, 1); err != nil {
return err
}
defer sem.Release(1)
uploadParams := map[string]string{
"method": "upload",
"path": params["path"],
"partseq": fmt.Sprint(partseq),
"uploadid": precreateResp.UploadID,
"app_id": "16051585",
}
_, err = d.Post("https://c3.pcs.baidu.com/rest/2.0/pcs/superfile2", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(uploadParams)
r.SetFileReader("file", stream.GetName(), io.NewSectionReader(tempFile, offset, byteSize))
r.SetFileReader("file", stream.GetName(),
driver.NewLimitedUploadStream(ctx, io.NewSectionReader(tempFile, offset, byteSize)))
}, nil)
if err != nil {
return err
}
up(int(threadG.Success()) * 100 / len(precreateResp.BlockList))
up(float64(threadG.Success()) * 100 / float64(len(precreateResp.BlockList)))
precreateResp.BlockList[i] = -1
return nil
})
@ -337,7 +356,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
if err = threadG.Wait(); err != nil {
if errors.Is(err, context.Canceled) {
precreateResp.BlockList = utils.SliceFilter(precreateResp.BlockList, func(s int) bool { return s >= 0 })
base.SaveUploadProgress(d, precreateResp, d.AccessToken, contentMd5)
base.SaveUploadProgress(d, strconv.FormatInt(d.Uk, 10), contentMd5)
}
return nil, err
}
@ -347,6 +366,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
_, err = d.Post(FILE_API_URL_V1+"/create", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(params)
r.SetQueryParam("bdstoken", d.bdstoken)
}, &precreateResp)
if err != nil {
return nil, err

View File

@ -6,13 +6,14 @@ import (
)
type Addition struct {
RefreshToken string `json:"refresh_token" required:"true"`
ShowType string `json:"show_type" type:"select" options:"root,root_only_album,root_only_file" default:"root"`
AlbumID string `json:"album_id"`
// RefreshToken string `json:"refresh_token" required:"true"`
Cookie string `json:"cookie" required:"true"`
ShowType string `json:"show_type" type:"select" options:"root,root_only_album,root_only_file" default:"root"`
AlbumID string `json:"album_id"`
//AlbumPassword string `json:"album_password"`
DeleteOrigin bool `json:"delete_origin"`
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
DeleteOrigin bool `json:"delete_origin"`
// ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
// ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
}

View File

@ -72,7 +72,7 @@ func (c *File) Thumb() string {
}
func (c *File) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.MD5, c.Md5)
return utils.NewHashInfo(utils.MD5, DecryptMd5(c.Md5))
}
/*相册部分*/

View File

@ -2,13 +2,15 @@ package baiduphoto
import (
"context"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"strings"
"unicode"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
@ -21,9 +23,10 @@ const (
FILE_API_URL_V2 = API_URL + "/file/v2"
)
func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
req := base.RestyClient.R().
SetQueryParam("access_token", d.AccessToken)
func (d *BaiduPhoto) Request(client *resty.Client, furl string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
req := client.R().
// SetQueryParam("access_token", d.AccessToken)
SetHeader("Cookie", d.Cookie)
if callback != nil {
callback(req)
}
@ -45,10 +48,10 @@ func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallba
return nil, fmt.Errorf("no shared albums found")
case 50100:
return nil, fmt.Errorf("illegal title, only supports 50 characters")
case -6:
if err = d.refreshToken(); err != nil {
return nil, err
}
// case -6:
// if err = d.refreshToken(); err != nil {
// return nil, err
// }
default:
return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron)
}
@ -63,36 +66,36 @@ func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallba
// return res.Body(), nil
//}
func (d *BaiduPhoto) refreshToken() error {
u := "https://openapi.baidu.com/oauth/2.0/token"
var resp base.TokenResp
var e TokenErrResp
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
"grant_type": "refresh_token",
"refresh_token": d.RefreshToken,
"client_id": d.ClientID,
"client_secret": d.ClientSecret,
}).Get(u)
if err != nil {
return err
}
if e.ErrorMsg != "" {
return &e
}
if resp.RefreshToken == "" {
return errs.EmptyToken
}
d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
op.MustSaveDriverStorage(d)
return nil
}
// func (d *BaiduPhoto) refreshToken() error {
// u := "https://openapi.baidu.com/oauth/2.0/token"
// var resp base.TokenResp
// var e TokenErrResp
// _, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
// "grant_type": "refresh_token",
// "refresh_token": d.RefreshToken,
// "client_id": d.ClientID,
// "client_secret": d.ClientSecret,
// }).Get(u)
// if err != nil {
// return err
// }
// if e.ErrorMsg != "" {
// return &e
// }
// if resp.RefreshToken == "" {
// return errs.EmptyToken
// }
// d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
// op.MustSaveDriverStorage(d)
// return nil
// }
func (d *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
return d.Request(furl, http.MethodGet, callback, resp)
return d.Request(base.RestyClient, furl, http.MethodGet, callback, resp)
}
func (d *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
return d.Request(furl, http.MethodPost, callback, resp)
return d.Request(base.RestyClient, furl, http.MethodPost, callback, resp)
}
// 获取所有文件
@ -338,24 +341,29 @@ func (d *BaiduPhoto) linkAlbum(ctx context.Context, file *AlbumFile, args model.
headers["X-Forwarded-For"] = args.IP
}
res, err := base.NoRedirectClient.R().
SetContext(ctx).
SetHeaders(headers).
SetQueryParams(map[string]string{
"access_token": d.AccessToken,
"fsid": fmt.Sprint(file.Fsid),
"album_id": file.AlbumID,
"tid": fmt.Sprint(file.Tid),
"uk": fmt.Sprint(file.Uk),
}).
Head(ALBUM_API_URL + "/download")
resp, err := d.Request(base.NoRedirectClient, ALBUM_API_URL+"/download", http.MethodHead, func(r *resty.Request) {
r.SetContext(ctx)
r.SetHeaders(headers)
r.SetQueryParams(map[string]string{
"fsid": fmt.Sprint(file.Fsid),
"album_id": file.AlbumID,
"tid": fmt.Sprint(file.Tid),
"uk": fmt.Sprint(file.Uk),
})
}, nil)
if err != nil {
return nil, err
}
if resp.StatusCode() != 302 {
return nil, fmt.Errorf("not found 302 redirect")
}
location := resp.Header().Get("Location")
link := &model.Link{
URL: res.Header().Get("location"),
URL: location,
Header: http.Header{
"User-Agent": []string{headers["User-Agent"]},
"Referer": []string{"https://photo.baidu.com/"},
@ -385,10 +393,24 @@ func (d *BaiduPhoto) linkFile(ctx context.Context, file *File, args model.LinkAr
"fsid": fmt.Sprint(file.Fsid),
})
}, &downloadUrl)
// resp, err := d.Request(base.NoRedirectClient, FILE_API_URL_V1+"/download", http.MethodHead, func(r *resty.Request) {
// r.SetContext(ctx)
// r.SetHeaders(headers)
// r.SetQueryParams(map[string]string{
// "fsid": fmt.Sprint(file.Fsid),
// })
// }, nil)
if err != nil {
return nil, err
}
// if resp.StatusCode() != 302 {
// return nil, fmt.Errorf("not found 302 redirect")
// }
// location := resp.Header().Get("Location")
link := &model.Link{
URL: downloadUrl.Dlink,
Header: http.Header{
@ -453,3 +475,55 @@ func (d *BaiduPhoto) uInfo() (*UInfo, error) {
}
return &info, nil
}
func (d *BaiduPhoto) getBDStoken() (string, error) {
var info struct {
Result struct {
Bdstoken string `json:"bdstoken"`
Token string `json:"token"`
Uk int64 `json:"uk"`
} `json:"result"`
}
_, err := d.Get("https://pan.baidu.com/api/gettemplatevariable?fields=[%22bdstoken%22,%22token%22,%22uk%22]", nil, &info)
if err != nil {
return "", err
}
return info.Result.Bdstoken, nil
}
func DecryptMd5(encryptMd5 string) string {
if _, err := hex.DecodeString(encryptMd5); err == nil {
return encryptMd5
}
var out strings.Builder
out.Grow(len(encryptMd5))
for i, n := 0, int64(0); i < len(encryptMd5); i++ {
if i == 9 {
n = int64(unicode.ToLower(rune(encryptMd5[i])) - 'g')
} else {
n, _ = strconv.ParseInt(encryptMd5[i:i+1], 16, 64)
}
out.WriteString(strconv.FormatInt(n^int64(15&i), 16))
}
encryptMd5 = out.String()
return encryptMd5[8:16] + encryptMd5[:8] + encryptMd5[24:32] + encryptMd5[16:24]
}
func EncryptMd5(originalMd5 string) string {
reversed := originalMd5[8:16] + originalMd5[:8] + originalMd5[24:32] + originalMd5[16:24]
var out strings.Builder
out.Grow(len(reversed))
for i, n := 0, int64(0); i < len(reversed); i++ {
n, _ = strconv.ParseInt(reversed[i:i+1], 16, 64)
n ^= int64(15 & i)
if i == 9 {
out.WriteRune(rune(n) + 'g')
} else {
out.WriteString(strconv.FormatInt(n, 16))
}
}
return out.String()
}

View File

@ -6,6 +6,7 @@ import (
"time"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/net"
"github.com/go-resty/resty/v2"
)
@ -26,24 +27,15 @@ func InitClient() {
NoRedirectClient.SetHeader("user-agent", UserAgent)
RestyClient = NewRestyClient()
HttpClient = NewHttpClient()
HttpClient = net.NewHttpClient()
}
func NewRestyClient() *resty.Client {
client := resty.New().
SetHeader("user-agent", UserAgent).
SetRetryCount(3).
SetRetryResetReaders(true).
SetTimeout(DefaultTimeout).
SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
return client
}
func NewHttpClient() *http.Client {
return &http.Client{
Timeout: time.Hour * 48,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify},
},
}
}

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

@ -0,0 +1,306 @@
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 {
if d.cron != nil {
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, file 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", file.GetName())
if err != nil {
return err
}
_, err = utils.CopyWithBuffer(filePart, file)
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
}
r := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
Reader: &driver.SimpleReaderWithSize{
Reader: body,
Size: int64(body.Len()),
},
UpdateProgress: up,
})
req, err := http.NewRequestWithContext(ctx, "POST", "https://pan-yz.chaoxing.com/upload", r)
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 int64 `json:"uploadDate"`
ModifyDate int64 `json:"modifyDate"`
UploadDateFormat string `json:"uploadDateFormat"`
Residstr string `json:"residstr"`
Suffix string `json:"suffix"`
Preview string `json:"preview"`
Thumbnail string `json:"thumbnail"`
Creator int `json:"creator"`
Duration int `json:"duration"`
IsImg bool `json:"isImg"`
PreviewURL string `json:"previewUrl"`
Filetype string `json:"filetype"`
Filepath string `json:"filepath"`
Sort int `json:"sort"`
Topsort int `json:"topsort"`
ResTypeValue int `json:"resTypeValue"`
Extinfo string `json:"extinfo"`
} `json:"data"`
}
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 int64 `json:"uploadDate"`
ModifyDate int64 `json:"modifyDate"`
UploadDateFormat string `json:"uploadDateFormat"`
Residstr string `json:"residstr"`
Suffix string `json:"suffix"`
Preview string `json:"preview"`
Thumbnail string `json:"thumbnail"`
Creator int `json:"creator"`
Duration int `json:"duration"`
IsImg bool `json:"isImg"`
PreviewURL string `json:"previewUrl"`
Filetype string `json:"filetype"`
Filepath string `json:"filepath"`
Sort int `json:"sort"`
Topsort int `json:"topsort"`
ResTypeValue int `json:"resTypeValue"`
Extinfo string `json:"extinfo"`
} `json:"param"`
}
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

@ -4,11 +4,12 @@ import (
"context"
"io"
"net/http"
"strconv"
"path"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
@ -49,7 +50,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 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 +72,9 @@ func (d *Cloudreve) Link(ctx context.Context, file model.Obj, args model.LinkArg
if err != nil {
return nil, err
}
if strings.HasPrefix(dUrl, "/api") {
dUrl = d.Address + dUrl
}
return &model.Link{
URL: dUrl,
}, nil
@ -75,7 +91,7 @@ func (d *Cloudreve) MakeDir(ctx context.Context, parentDir model.Obj, dirName st
func (d *Cloudreve) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
body := base.Json{
"action": "move",
"src_dir": srcObj.GetPath(),
"src_dir": path.Dir(srcObj.GetPath()),
"dst": dstDir.GetPath(),
"src": convertSrc(srcObj),
}
@ -97,7 +113,7 @@ func (d *Cloudreve) Rename(ctx context.Context, srcObj model.Obj, newName string
func (d *Cloudreve) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
body := base.Json{
"src_dir": srcObj.GetPath(),
"src_dir": path.Dir(srcObj.GetPath()),
"dst": dstDir.GetPath(),
"src": convertSrc(srcObj),
}
@ -118,6 +134,8 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
if io.ReadCloser(stream) == http.NoBody {
return d.create(ctx, dstDir, stream)
}
// 获取存储策略
var r DirectoryResp
err := d.request(http.MethodGet, "/directory"+dstDir.GetPath(), nil, &r)
if err != nil {
@ -128,8 +146,10 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
"size": stream.GetSize(),
"name": stream.GetName(),
"policy_id": r.Policy.Id,
"last_modified": stream.ModTime().Unix(),
"last_modified": stream.ModTime().UnixMilli(),
}
// 获取上传会话信息
var u UploadInfo
err = d.request(http.MethodPut, "/file/upload", func(req *resty.Request) {
req.SetBody(uploadBody)
@ -137,36 +157,26 @@ func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.File
if err != nil {
return err
}
var chunkSize = u.ChunkSize
var buf []byte
var chunk int
for {
var n int
buf = make([]byte, chunkSize)
n, err = io.ReadAtLeast(stream, buf, chunkSize)
if err != nil && err != io.ErrUnexpectedEOF {
if err == io.EOF {
return nil
}
return err
}
if n == 0 {
break
}
buf = buf[:n]
err = d.request(http.MethodPost, "/file/upload/"+u.SessionID+"/"+strconv.Itoa(chunk), func(req *resty.Request) {
req.SetHeader("Content-Type", "application/octet-stream")
req.SetHeader("Content-Length", strconv.Itoa(n))
req.SetBody(buf)
}, nil)
if err != nil {
break
}
chunk++
// 根据存储方式选择分片上传的方法
switch r.Policy.Type {
case "onedrive":
err = d.upOneDrive(ctx, stream, u, up)
case "s3":
err = d.upS3(ctx, stream, u, up)
case "remote": // 从机存储
err = d.upRemote(ctx, stream, u, up)
case "local": // 本机存储
err = d.upLocal(ctx, stream, u, up)
default:
err = errs.NotImplement
}
return err
if err != nil {
// 删除失败的会话
_ = d.request(http.MethodDelete, "/file/upload/"+u.SessionID, nil, nil)
return err
}
return nil
}
func (d *Cloudreve) create(ctx context.Context, dir model.Obj, file model.Obj) error {

View File

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

View File

@ -21,9 +21,12 @@ type Policy struct {
}
type UploadInfo struct {
SessionID string `json:"sessionID"`
ChunkSize int `json:"chunkSize"`
Expires int `json:"expires"`
SessionID string `json:"sessionID"`
ChunkSize int `json:"chunkSize"`
Expires int `json:"expires"`
UploadURLs []string `json:"uploadURLs"`
Credential string `json:"credential,omitempty"` // local
CompleteURL string `json:"completeURL,omitempty"` // s3
}
type DirectoryResp struct {
@ -44,13 +47,20 @@ type Object struct {
SourceEnabled bool `json:"source_enabled"`
}
func objectToObj(f Object) *model.Object {
return &model.Object{
ID: f.Id,
Name: f.Name,
Size: int64(f.Size),
Modified: f.Date,
IsFolder: f.Type == "dir",
type DirectoryProp struct {
Size int `json:"size"`
}
func objectToObj(f Object, t model.Thumbnail) *model.ObjThumb {
return &model.ObjThumb{
Object: model.Object{
ID: f.Id,
Name: f.Name,
Size: int64(f.Size),
Modified: f.Date,
IsFolder: f.Type == "dir",
},
Thumbnail: t,
}
}

View File

@ -1,16 +1,23 @@
package cloudreve
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/cookie"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
json "github.com/json-iterator/go"
jsoniter "github.com/json-iterator/go"
@ -20,17 +27,20 @@ import (
const loginPath = "/user/session"
func (d *Cloudreve) getUA() string {
if d.CustomUA != "" {
return d.CustomUA
}
return base.UserAgent
}
func (d *Cloudreve) request(method string, path string, callback base.ReqCallback, out interface{}) error {
u := d.Address + "/api/v3" + path
ua := d.CustomUA
if ua == "" {
ua = base.UserAgent
}
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"Cookie": "cloudreve-session=" + d.Cookie,
"Accept": "application/json, text/plain, */*",
"User-Agent": ua,
"User-Agent": d.getUA(),
})
var r Resp
@ -93,7 +103,7 @@ func (d *Cloudreve) login() error {
if err == nil {
break
}
if err != nil && err.Error() != "CAPTCHA not match." {
if err.Error() != "CAPTCHA not match." {
break
}
}
@ -149,3 +159,235 @@ func convertSrc(obj model.Obj) map[string]interface{} {
m["items"] = items
return m
}
func (d *Cloudreve) GetThumb(file Object) (model.Thumbnail, error) {
if !d.Addition.EnableThumbAndFolderSize {
return model.Thumbnail{}, nil
}
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": d.getUA(),
})
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
}
func (d *Cloudreve) upLocal(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {
var finish int64 = 0
var chunk int = 0
DEFAULT := int64(u.ChunkSize)
for finish < stream.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
utils.Log.Debugf("[Cloudreve-Local] upload: %d", finish)
var byteSize = DEFAULT
left := stream.GetSize() - finish
if left < DEFAULT {
byteSize = left
}
byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
err = d.request(http.MethodPost, "/file/upload/"+u.SessionID+"/"+strconv.Itoa(chunk), func(req *resty.Request) {
req.SetHeader("Content-Type", "application/octet-stream")
req.SetContentLength(true)
req.SetHeader("Content-Length", strconv.FormatInt(byteSize, 10))
req.SetHeader("User-Agent", d.getUA())
req.SetBody(driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
}, nil)
if err != nil {
break
}
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
chunk++
}
return nil
}
func (d *Cloudreve) upRemote(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {
uploadUrl := u.UploadURLs[0]
credential := u.Credential
var finish int64 = 0
var chunk int = 0
DEFAULT := int64(u.ChunkSize)
for finish < stream.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
utils.Log.Debugf("[Cloudreve-Remote] upload: %d", finish)
var byteSize = DEFAULT
left := stream.GetSize() - finish
if left < DEFAULT {
byteSize = left
}
byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequest("POST", uploadUrl+"?chunk="+strconv.Itoa(chunk),
driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Authorization", fmt.Sprint(credential))
req.Header.Set("User-Agent", d.getUA())
finish += byteSize
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
_ = res.Body.Close()
up(float64(finish) * 100 / float64(stream.GetSize()))
chunk++
}
return nil
}
func (d *Cloudreve) upOneDrive(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {
uploadUrl := u.UploadURLs[0]
var finish int64 = 0
DEFAULT := int64(u.ChunkSize)
for finish < stream.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
utils.Log.Debugf("[Cloudreve-OneDrive] upload: %d", finish)
var byteSize = DEFAULT
left := stream.GetSize() - finish
if left < DEFAULT {
byteSize = left
}
byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequest("PUT", uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
req.Header.Set("User-Agent", d.getUA())
finish += byteSize
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 {
data, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
return errors.New(string(data))
}
_ = res.Body.Close()
up(float64(finish) * 100 / float64(stream.GetSize()))
}
// 上传成功发送回调请求
err := d.request(http.MethodPost, "/callback/onedrive/finish/"+u.SessionID, func(req *resty.Request) {
req.SetBody("{}")
}, nil)
if err != nil {
return err
}
return nil
}
func (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {
var finish int64 = 0
var chunk int = 0
var etags []string
DEFAULT := int64(u.ChunkSize)
for finish < stream.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
utils.Log.Debugf("[Cloudreve-S3] upload: %d", finish)
var byteSize = DEFAULT
left := stream.GetSize() - finish
if left < DEFAULT {
byteSize = left
}
byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequest("PUT", u.UploadURLs[chunk],
driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
finish += byteSize
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
_ = res.Body.Close()
etags = append(etags, res.Header.Get("ETag"))
up(float64(finish) * 100 / float64(stream.GetSize()))
chunk++
}
// s3LikeFinishUpload
// https://github.com/cloudreve/frontend/blob/b485bf297974cbe4834d2e8e744ae7b7e5b2ad39/src/component/Uploader/core/api/index.ts#L204-L252
bodyBuilder := &strings.Builder{}
bodyBuilder.WriteString("<CompleteMultipartUpload>")
for i, etag := range etags {
bodyBuilder.WriteString(fmt.Sprintf(
`<Part><PartNumber>%d</PartNumber><ETag>%s</ETag></Part>`,
i+1, // PartNumber 从 1 开始
etag,
))
}
bodyBuilder.WriteString("</CompleteMultipartUpload>")
req, err := http.NewRequest(
"POST",
u.CompleteURL,
strings.NewReader(bodyBuilder.String()),
)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/xml")
req.Header.Set("User-Agent", d.getUA())
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
return fmt.Errorf("up status: %d, error: %s", res.StatusCode, string(body))
}
// 上传成功发送回调请求
err = d.request(http.MethodGet, "/callback/s3/"+u.SessionID, nil, nil)
if err != nil {
return err
}
return nil
}

View File

@ -3,7 +3,6 @@ package crypt
import (
"context"
"fmt"
"github.com/alist-org/alist/v3/internal/stream"
"io"
stdpath "path"
"regexp"
@ -14,8 +13,11 @@ import (
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/sign"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
rcCrypt "github.com/rclone/rclone/backend/crypt"
"github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/config/obscure"
@ -123,6 +125,9 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
//filter illegal files
continue
}
if !d.ShowHidden && strings.HasPrefix(name, ".") {
continue
}
objRes := model.Object{
Name: name,
Size: 0,
@ -144,6 +149,9 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
//filter illegal files
continue
}
if !d.ShowHidden && strings.HasPrefix(name, ".") {
continue
}
objRes := model.Object{
Name: name,
Size: size,
@ -152,7 +160,14 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
Ctime: obj.CreateTime(),
// discarding hash as it's encrypted
}
if !ok {
if d.Thumbnail && thumb == "" {
thumbPath := stdpath.Join(args.ReqPath, ".thumbnails", name+".webp")
thumb = fmt.Sprintf("%s/d%s?sign=%s",
common.GetApiUrl(common.GetHttpReq(ctx)),
utils.EncodePath(thumbPath, true),
sign.Sign(thumbPath))
}
if !ok && !d.Thumbnail {
result = append(result, &objRes)
} else {
objWithThumb := model.ObjThumb{
@ -248,19 +263,13 @@ func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
}
rrc := remoteLink.RangeReadCloser
if len(remoteLink.URL) > 0 {
rangedRemoteLink := &model.Link{
URL: remoteLink.URL,
Header: remoteLink.Header,
}
var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, rangedRemoteLink)
var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, remoteLink)
if err != nil {
return nil, err
}
rrc = converted
}
if rrc != nil {
//remoteRangeReader, err :=
remoteReader, err := rrc.RangeRead(ctx, http_range.Range{Start: underlyingOffset, Length: length})
remoteClosers.AddClosers(rrc.GetClosers())
if err != nil {
@ -273,7 +282,6 @@ func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
if err != nil {
return nil, err
}
//remoteClosers.Add(remoteLink.MFile)
//keep reuse same MFile and close at last.
remoteClosers.Add(remoteLink.MFile)
return io.NopCloser(remoteLink.MFile), nil
@ -292,7 +300,6 @@ func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
resultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers}
resultLink := &model.Link{
Header: remoteLink.Header,
RangeReadCloser: resultRangeReadCloser,
Expiration: remoteLink.Expiration,
}
@ -379,10 +386,11 @@ func (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileSt
Modified: streamer.ModTime(),
IsFolder: streamer.IsDir(),
},
Reader: wrappedIn,
Mimetype: "application/octet-stream",
WebPutAsTask: streamer.NeedStore(),
Exist: streamer.GetExist(),
Reader: wrappedIn,
Mimetype: "application/octet-stream",
WebPutAsTask: streamer.NeedStore(),
ForceStreamUpload: true,
Exist: streamer.GetExist(),
}
err = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false)
if err != nil {

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