Compare commits

..

262 Commits

Author SHA1 Message Date
49213c1321 fix(setting): update PDF and EPUB viewer URLs (#297)
- Change PDF.js viewer URL from protocol-relative to HTTPS and updates parameter from "url" to "file" for proper document loading
- Also standardize EPUB.js viewer to use HTTPS protocol for consistency
2025-06-23 11:32:22 +08:00
64dd3cb047 fix(ci):fixed changelog ci (#302) 2025-06-22 20:48:10 +08:00
12fd52b6b7 docs(README_cn): format document links as list to sync with other languages. (#279)
Fix #272
2025-06-22 19:06:00 +08:00
27533d0e20 fixed(drive):Delete old Dropbox renewapi (#296)
* add dropbox api

* fixed(api):Delete old dropbox renew api

---------

Signed-off-by: Suyunmeng <69945917+Suyunmeng@users.noreply.github.com>
Co-authored-by: pikachuim <pikachuim@qq.com>
2025-06-22 18:52:55 +08:00
Ray
34a2eeb4a9 删除曲奇云盘驱动 (#294)
* Update all.go 删除quqi

Signed-off-by: Ray <eiauo.ray@gmail.com>

* Delete drivers/quqi directory删除quqi驱动

Signed-off-by: Ray <eiauo.ray@gmail.com>

---------

Signed-off-by: Ray <eiauo.ray@gmail.com>
2025-06-22 18:48:26 +08:00
652e4ba1cb add dropbox api (#295) 2025-06-22 18:28:35 +08:00
639b5cf7c2 fix(net):empty file download error (#282) 2025-06-22 14:21:45 +08:00
b5c1386645 fix: typo and outdated - compose yaml (#263)
* Update docker-compose.yml

Image not exist

Signed-off-by:  Jimmy Alexander <142508054+integer2bit@users.noreply.github.com>

* chore: update docker-compose.yml image to docker hub

Signed-off-by:  Jimmy Alexander <142508054+integer2bit@users.noreply.github.com>

---------

Signed-off-by: Jimmy Alexander <142508054+integer2bit@users.noreply.github.com>
2025-06-22 12:22:21 +08:00
041868dfb8 docs: add channel and update compose config (#272)
* fix:remove-compose-version

Signed-off-by: SenkjM <112735335+SenkjM@users.noreply.github.com>

* mod : add Channel

* docs:update README

---------

Signed-off-by: SenkjM <112735335+SenkjM@users.noreply.github.com>
2025-06-22 00:41:25 +08:00
cfbc157477 chore(ci): remove issue-related automation workflows (#257) 2025-06-21 15:14:24 +08:00
5d44806064 fix(upload): revert #79 (#248) 2025-06-21 00:16:19 +08:00
fc8b99c862 chore:fixed issue translate permissions
Signed-off-by: Suyunmeng <sumengjing@outlook.com>
2025-06-20 23:14:28 +08:00
24560b43c0 chore:fixed issue translate
Signed-off-by: Suyunmeng <sumengjing@outlook.com>
2025-06-20 23:08:50 +08:00
39ca385778 chore:Fixed docker release CI
Update release.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

Update release.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

chore(ci):Fixed CI bugs

Update release_linux_musl_arm.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

Update release_linux_musl.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

Update release_freebsd.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

Update release_linux_musl_arm.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

Update release_linux_musl.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

Update release_freebsd.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

Update release.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

Update release_android.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

Update release_docker.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

Update release_docker.yml

Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

chore:Fixed docker ci
2025-06-20 22:44:39 +08:00
ef0531ad40 feat(ci): add changelog content to release content (#233) 2025-06-20 19:24:29 +08:00
12540a8abc fix: http2 content-length (#224) 2025-06-20 17:57:14 +08:00
0f5ed14fe2 feat(fs): add cross-storage move support (#211)
* feat(fs): add cross-storage move support

* fix(fs): add check before moving files

* fix(fs): changed error detect method

---------

Co-authored-by: ShenLin <773933146@qq.com>
2025-06-20 17:54:24 +08:00
ca55b89322 remove alist from repo (#230)
* remove alist from repo

* remove alist from repo

* remove alist from repo
2025-06-20 17:41:16 +08:00
a3c7cb059d chore:Change Logo URL and fixed aliyundrive open bugs (#208)
* Fix Logo URL

* fixed aliyunpan_open

* fixed aliyundrive bugs

* fixed onlineapi bugs

* fixed onlineapi bugs

* Fixed Bugs

* Rollback

* fixed

* fixed onlineapi

* fixed driver

---------

Signed-off-by: Suyunmeng <sumengjing@outlook.com>
2025-06-19 21:20:29 +08:00
0f8545133b add text output for error message (#210)
* mod rank for AccessToken

* del alist_v2

* add error message from remote

---------

Co-authored-by: Suyunmeng <sumengjing@outlook.com>
2025-06-19 20:25:45 +08:00
72fad1be2e Delete Lark Drive (#201) 2025-06-19 16:38:51 +08:00
b7ce7f172b Dev pika (#202)
* mod rank for AccessToken

* del alist_v2

---------

Co-authored-by: Suyunmeng <sumengjing@outlook.com>
2025-06-19 16:25:07 +08:00
248c041711 fix(setting): update preview url (#198) 2025-06-19 15:05:29 +08:00
70b937e031 Revert "feat(log): add error logging middleware for improved error handling (#182)"
This reverts commit 5e8d8d070a.
2025-06-19 09:56:45 +08:00
79521db8e0 fix(ci): add workflow_dispatch for beta_release and build 2025-06-18 23:27:23 +08:00
015d3ecd00 fix(ci): add auth header when access GitHub CI 2025-06-18 23:27:23 +08:00
89451b6d98 fix(ci): use OpenListTeam/cgo-actions@v1.1.2 2025-06-18 23:27:23 +08:00
681cb6c8a4 fix(ci): freebsd 14.1 is deprecated (#187) 2025-06-18 22:17:21 +08:00
c2d1316f65 fix(115open) fixed rate_limit bugs (#161)
* fixed 115 bugs

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Fixed 115 open bugs

* fixed bugs

---------

Signed-off-by: Suyunmeng <sumengjing@outlook.com>
2025-06-18 21:32:23 +08:00
5e8d8d070a feat(log): add error logging middleware for improved error handling (#182) 2025-06-18 20:37:16 +08:00
c7c0bfe810 mod rank for AccessToken (#181) 2025-06-18 18:09:36 +08:00
e9c73b52db fix(Dockerfile): add jq package (#168) 2025-06-18 14:26:42 +08:00
7d24a5d45f feat: add file visibility checks for windows (#39)
* feat: add file visibility checks for windows

* fix: fix build error

* refactor: optimize thie ishidden

---------

Co-authored-by: Hantong Chen <70561268+cxw620@users.noreply.github.com>
2025-06-17 23:25:02 +08:00
3ab309e00e refactor: adjust CI process (#125)
* feat(cmd/lang): allow setting frontend path when generate lang files

* chore(ci): remove ci for auto_lang (done by frontend ci)
2025-06-17 22:35:02 +08:00
8822eef97e chore(api):Add online api refresh method (#143)
* Add Official API Refresh Interface(Baiduyun)

* add UseOnlineAPI & APIAddress
add _refreshToken using APIAddress

* fix return

* Modify the frontend display using the default API refresh method

* Fixed display and operation related issues

* fixed aliyundrive_open old refresh

---------

Co-authored-by: Suyunmeng <sumengjing@outlook.com>
2025-06-17 22:13:28 +08:00
7613f886d0 fix(123open): add rate limit (#144) 2025-06-17 18:49:32 +08:00
fe02a989bd feat(123pan): support 123Open (#93) 2025-06-17 18:38:25 +08:00
2bed40cfce chore(Dockerfile): add jq package (#142)
Add jq package

Signed-off-by: Suyunmeng <sumengjing@outlook.com>
2025-06-17 18:38:22 +08:00
87ca1b96ae fix(189pc): crashes when upload cancelled (#79)
* fix(189pc): crashes when upload cancelled

Signed-off-by: XZB-1248 <28593573+XZB-1248@users.noreply.github.com>

* fix(189pc): replace semaphore with errgroup.Group.SetLimit

---------

Signed-off-by: XZB-1248 <28593573+XZB-1248@users.noreply.github.com>
Co-authored-by: KirCute <951206789@qq.com>
2025-06-17 00:13:31 +08:00
5a4649c929 feat(alias): support parallel write (#69)
* feat(alias): support parallel write

* fix(alias): lack `err` in `errors.Join()`
2025-06-17 00:13:01 +08:00
2e2cec05fd fix(cloudreve): remove unnecessary finish increment in upload functions (#62)
* fix(cloudreve): remove unnecessary finish increment in upload functions

* fix(cloudreve_v4): remove unnecessary finish increment in upload functions
2025-06-17 00:12:45 +08:00
b1afadd129 chore: update project meta (#51)
* chore: update project meta (partial)

* chore: update README

* chore: update pdf preview

* revert: use old hash

* chore: update logo file url
2025-06-16 16:29:45 +08:00
a59ad9a84e fix(lanzou): fix removing JavaScript comments from response data (#37)
* 修复清理js中块注释的bug

移除块注释,目前只将开始“/*”和结束的“*/”移除,未将注释中内容移除

Signed-off-by: 410680876f1 <71364356+410680876f1@users.noreply.github.com>

* 修复清理js中注释的bug

移注释,目前只将注释标识符号清楚,注释中内容被遗留了下来
感谢大佬@Kuingsmile的代码审核

Signed-off-by: 410680876f1 <71364356+410680876f1@users.noreply.github.com>

* 代码格式化

Signed-off-by: 410680876f1 <71364356+410680876f1@users.noreply.github.com>

---------

Signed-off-by: 410680876f1 <71364356+410680876f1@users.noreply.github.com>
Co-authored-by: Hantong Chen <70561268+cxw620@users.noreply.github.com>
2025-06-16 16:28:08 +08:00
2e889fb07d chore(ci): fixed dockerhub ci process (#97)
* add DockerHub

* fixed dockerhub

* Update dockerhub CI

* Update DockerHub

* fixed bugs

* fixed CI Bugs

* Update auto_lang.yml

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* fixed release

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Update test_docker.yml

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Delete hub

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Delete hub

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* test build

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Simplify actions

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Simplify actions test

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Simplify actions test Successful ,Rollback Environments

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Update release_docker.yml

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Update test_docker.yml

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Update auto_lang.yml

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Update auto_lang.yml

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Update auto_lang.yml

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Update test_docker.yml

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

* Update release_docker.yml

Signed-off-by: Suyunmeng <sumengjing@outlook.com>

---------

Signed-off-by: Suyunmeng <sumengjing@outlook.com>
2025-06-16 14:16:45 +08:00
d95c4f0127 chore: change the CDN link of the logo and modify the OCR port (#84)
* Change the CDN link of the logo and modify the OCR port

* Update

* Rollback OCR interface
2025-06-15 23:19:25 +08:00
1c58d11d62 chore: update build.sh & add loongarch64 support (#28)
* chore: update build.sh

* chore: update ci build

* chore: update ci build GitAuthor

* feat(build.sh): fix web release download & add loongarch64 build (#63)

chore(ci): fix web download & add loongarch64 & update cgo-actions to v1.1.1

* chore: temporary build test

* revert(build.sh): change to release used url

* feat(build.sh): add set -e for build script

* fix(build.sh): fix web release download logic

* fix(build.sh): remove TODO comment

* feat(build.sh): update GitAuthor to bot@openlist.team

* feat(build.sh): update GitAuthor

* fix: using fsSL in curl & add info of desktop client back

* chore: refine beta release workflow comments

---------

Co-authored-by: Yinan Qin <39023210+elysia-best@users.noreply.github.com>
2025-06-14 23:16:42 +08:00
e11c390c4d fix(cli): Fixed links to documentation in the CLI (#71)
Co-authored-by: reschen <reschen@126.com>
2025-06-14 20:21:14 +08:00
2965915bed chore(deps): switch the dependency from KirCute/sftpd-alist to OpenListTeam/sftpd-openlist (#64) 2025-06-14 16:04:13 +08:00
da1cfd1945 chore(deps): remove dependency KirCute/ftpserverlib-pasvportmap (#61) 2025-06-14 15:56:51 +08:00
8a29790327 chore(ci): using latest musl-compilers (#23)
* chore(ci): using `latest` musl-compilers

https://github.com/OpenListTeam/OpenList/pull/3#pullrequestreview-2921194381

Signed-off-by: YaoSiQian <admin@yaosiqian.cn>

* chore(ci): using `latest` musl-compilers in beta_release CI

---------

Signed-off-by: YaoSiQian <admin@yaosiqian.cn>
Co-authored-by: Hantong Chen <cxwdyx620@gmail.com>
2025-06-13 17:01:24 +08:00
7cd8f648c8 fix: change app name in cmd (#36) 2025-06-13 15:19:25 +08:00
b8e6083e19 chore: remove deprecated vtencent drive (#33) 2025-06-13 12:55:52 +08:00
3f821bdcd1 revert: using old salt value (#29)
Signed-off-by: Yinan Qin <39023210+elysia-best@users.noreply.github.com>
2025-06-13 12:47:51 +08:00
9e05c81d9c chore: update project logo (#26) 2025-06-12 23:29:35 +08:00
f1552b67a0 chore(setting): update repo name in default announcement (#27)
chore(SETTING)/update repo name in default announcement
2025-06-12 23:28:33 +08:00
20d1d5b479 chore(README): update README, add progress intro (#21)
* Update README_cn.md

* Update README_cn.md

* Update README.md

* Update README_ja.md

* Update README_ja.md

* Update README_ja.md

* Update README_ja.md

---------

Co-authored-by: Hantong Chen <70561268+cxw620@users.noreply.github.com>
2025-06-12 22:03:18 +08:00
fdcc2f136e chore: change module name to OpenListTeam/OpenList (#2)
* Enable blank issue

* chore(README.md): update docs (temporally)

* Update FUNDING.yml

* chore: purge README.md

* chore: change module name to OpenListTeam/OpenList

* fix: fix link errors

* chore: remove v3 in module name

* fix: resolve some conficts

* fix: resolve conficts

* docs: update with latest file

---------

Co-authored-by: ShenLin <773933146@qq.com>
Co-authored-by: Hantong Chen <cxwdyx620@gmail.com>
Co-authored-by: joshua <i@joshua.su>
Co-authored-by: Hantong Chen <70561268+cxw620@users.noreply.github.com>
2025-06-12 22:02:46 +08:00
5feb86ceee chore(docs&ci): change links in files & fix github ci and docker ci (#3)
* Enable blank issue

* chore(README.md): update docs (temporally)

* Update FUNDING.yml

* chore: purge README.md

* Update README.md

Alist改为OpenList

* Update README_cn.md

Alist改为OpenList

* Update README.md

漏了一处

* Update README_ja.md

Alist改为OpenList

* Update README_cn.md

漏了一处

* Update CODE_OF_CONDUCT.md

更改链接

* Update README.md

更新tg链接

* Update README_cn.md

更新tg链接

* Update README_ja.md

更新tg链接

* chore(build&docs): use new links in build and github templates

* Update README.md

更新团队名

* chore: disable translation update, change beta release into artifacts

* fix: disable docker build and name the uploaded artifacts

* fix typo

* Update README_cn.md

更新团队名称

* Update README_ja.md

更新

* Update project name in CONTRIBUTING.md

* Update README_cn.md

更新

* Update README.md

更新

* Update README_ja.md

* fix: fix artifact name

* chore(build.sh): use original musl.cc

* fix(ci): fix action artifacts upload

* Update CODE_OF_CONDUCT.md

TG更改为Telegram

* Update README_cn.md

更新论坛链接

* Update README.md

更新论坛链接

* Update README_ja.md

更新论坛链接

* feat: update community based call back for onedrive

* chore(ci): update musl.cc link

* chore: use openlist as name instead of default OpenList

* Update user.go

* chore: fix artifact name

* feat(ci): add docker build test

* fix: add more platforms

* fix: explicitly use docker.io

* fix: fix typo

* fix(docker): fix test build push platform

* chore: change to OpenListTeam

* Update CODE_OF_CONDUCT.md

* doc: update org name

* docs: change repo urls

* feat: release docker image to ghcr.io on tagging

* fix: fix the name of test_docker

* build: update the names in docker-compose and docker file

* chore: rename

---------

Co-authored-by: ShenLin <773933146@qq.com>
Co-authored-by: Hantong Chen <cxwdyx620@gmail.com>
Co-authored-by: joshua <i@joshua.su>
Co-authored-by: 绎泽 <yize@tencent.to>
Co-authored-by: zyk2507 <93830642+zyk2507@users.noreply.github.com>
2025-06-12 21:29:43 +08:00
ee783fa1be chore(CONTRIBUTING): update to new OpenList-Frontend 2025-06-12 17:58:55 +08:00
0bcb4fe16d chore(README): update project name
Update FUNDING.yml

chore: purge README.md

Update project name in CONTRIBUTING.md

Update README.md

Alist改为OpenList

Update README_cn.md

Alist改为OpenList

Update README.md

漏了一处

Update README_ja.md

Alist改为OpenList

Update README_cn.md

漏了一处

Update CODE_OF_CONDUCT.md

更改链接

Update README.md

更新tg链接

Update README_cn.md

更新tg链接

Update README_ja.md

更新tg链接

Update README.md

更新团队名

Update README_cn.md

更新团队名称

Update README_ja.md

更新

Update README_cn.md

更新

Update README.md

更新

Update README_ja.md

Update CODE_OF_CONDUCT.md

TG更改为Telegram

Update README_cn.md

更新论坛链接

Update README.md

更新论坛链接

Update README_ja.md

更新论坛链接
2025-06-12 16:56:22 +08:00
4f57bd3ae6 chore(README.md): update docs (temporally) 2025-06-12 16:56:22 +08:00
cf42fe6a40 chore: allow blank issue 2025-06-12 16:56:18 +08:00
c4775521c6 chore(README.md): reminder of fork
fix: remove `alistgo.com`
2025-06-11 16:14:44 +08:00
ffa03bfda1 feat(cloudreve_v4): add Cloudreve V4 driver (#8470 closes #8328 #8467)
* feat(cloudreve_v4): add Cloudreve V4 driver implementation

* fix(cloudreve_v4): update request handling to prevent token refresh loop

* feat(onedrive): implement retry logic for upload failures

* feat(cloudreve): implement retry logic for upload failures

* feat(cloudreve_v4): support cloud sorting

* fix(cloudreve_v4): improve token handling in Init method

* feat(cloudreve_v4): support share

* feat(cloudreve): support reference

* feat(cloudreve_v4): support version upload

* fix(cloudreve_v4): add SetBody in upLocal

* fix(cloudreve_v4): update URL structure in Link and FileUrlResp
2025-05-24 13:38:43 +08:00
630cf30af5 feat(115_open): implement rate limiting for API requests 2025-05-11 13:39:32 +08:00
bc5117fa4f fix(115_open): add delay in MakeDir function to handle rate limiting 2025-05-02 16:53:39 +08:00
11e7284824 fix: prevent guest user from updating profile (#8447) 2025-04-29 23:14:16 +08:00
b2b91a9281 feat(doubao): add get_download_info API and download_api option (#8428) 2025-04-27 20:00:25 +08:00
f541489d7d fix(netease_music): change ListResp size fields from string to int64 (#8417) 2025-04-27 19:59:30 +08:00
6d9c554f6f feat: add UseLargeThumbnail for 139 (#8424) 2025-04-27 19:58:45 +08:00
Mmx
e532ab31ef fix: remove auth middleware for authn login (#8407) 2025-04-27 19:58:09 +08:00
Mmx
bf0705ec17 fix: shebang of entrypoint.sh (#8408) 2025-04-27 19:56:34 +08:00
17b42b9fa4 fix(mega): use newest file for same filename (#8422 close #8344)
Mega supports duplicate names but alist does not support.
In `List()` method, driver will return multiple files with same name.
That makes alist to use oldest version file for listing/downloading.
So it is necessary to filter old same name files in a folder.
After fixes, all CRUD work normally.

Refs #8344
2025-04-27 19:56:04 +08:00
41bdab49aa fix(139): incorrect host (#8368)
* fix: correct new personal cloud path for 139Driver

* Update drivers/139/driver.go

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

* fix bug

---------

Co-authored-by: panshaosen <19802021493@139.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: j2rong4cn <253551464@qq.com>
2025-04-19 14:29:12 +08:00
8f89c55aca perf(local): avoid duplicate parsing of VideoThumbPos (#7812)
* 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

* refactor(local): avoid duplicate parsing of VideoThumbPos
2025-04-19 14:27:13 +08:00
b449312da8 fix(docker_release): avoid duplicate occupation in docker image (#8393 close #8388)
* fix(ci): modify the method of adding permissions

* fix(build): modify the method of adding permissions(to keep up with ci)
2025-04-19 14:26:19 +08:00
52d4e8ec47 fix(lanzou): remove JavaScript comments from response data (#8386)
* feat(lanzou): add RemoveJSComment function to clean JavaScript comments from HTML

* feat(lanzou): remove comments from share page data in getFilesByShareUrl function

* fix(lanzou): optimize RemoveJSComment function to improve comment removal logic
2025-04-19 14:24:43 +08:00
28e5b5759e feat(azure_blob): implement GetRootId interface in Addition struct (#8389)
fix failed get dir
2025-04-19 14:23:48 +08:00
477c43971f feat(doubao_share): support doubao_share link (#8376)
Co-authored-by: anobodys <anobodys@gmail.com>
2025-04-19 14:22:43 +08:00
0a9921fa79 fix(aliyundrive_open): resolve file duplication issues and improve path handling (#8358)
* fix(aliyundrive_open): resolve file duplication issues and improve path handling

1. Fix file duplication by implementing a new removeDuplicateFiles method that cleans up duplicate files after operations
2. Change Move operation to use "ignore" for check_name_mode instead of "refuse" to allow moves when destination has same filename
3. Set Copy operation to handle duplicates by removing them after successful copy
4. Improve path handling for all file operations (Move, Rename, Put, MakeDir) by properly maintaining the full path of objects
5. Implement GetRoot interface for proper root object initialization with correct path
6. Add proper path management in List operation to ensure objects have correct paths
7. Fix path handling in error cases and improve logging of failures

* refactor(aliyundrive_open): change error logging to warnings for duplicate file removal

Updated the Move, Rename, and Copy methods to log warnings instead of errors when duplicate file removal fails, as the primary operations have already completed successfully. This improves the clarity of logs without affecting the functionality.

* Update drivers/aliyundrive_open/util.go

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-19 14:22:12 +08:00
88abb323cb feat(url-tree): implement the Put interface to support adding links directly to the UrlTree on the web side (#8312)
* feat(url-tree)支持PUT

* feat(url-tree) UrlTree更新时,需要将路径和内容分割 #8303

* fix: stdpath.Join call

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

---------

Co-authored-by: Andy Hsu <i@nn.ci>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-12 17:27:56 +08:00
f0b1aeaf8d feat(doubao): support upload (#8302 close #8335)
* feat(doubao): support upload

* fix(doubao): fix file list cursor

* fix: handle strconv.Atoi err

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

---------

Co-authored-by: anobodys <anobodys@gmail.com>
Co-authored-by: Andy Hsu <i@nn.ci>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-12 17:12:40 +08:00
c8470b9a2a fix(fs): remove old target object from cache before updating (#8352) 2025-04-12 17:09:46 +08:00
Dgs
d0ee90cd11 fix(thunder): fix login issue (#8342 close #8288) 2025-04-12 17:05:58 +08:00
Dgs
544a7ea022 fix(pikpak&pikpak_share): fix WebPackageName (#8305) 2025-04-12 17:03:58 +08:00
4f5cabc725 feat: add h2c for http server (#8294)
* feat: add h2c for http server

* chore(config): add EnableH2c option
2025-04-12 17:02:51 +08:00
a2f266277c fix(net): unexpected write (#8291 close #8281) 2025-04-12 17:01:52 +08:00
a4bfbf8a83 fix(ipfs): fix problems (#8252)
* fix: 🐛 (ipfs): fix the list error caused by not proper join path function

使用更加规范的路径拼接,修复了有中文或符号的路径无法正常访问的问题

* refactor: 命名规范

* 删除多余的条件判断

* fix: 使用withresult方法重构代码,添加get方法,提高性能

* fix: 允许get方法获取目录

去除多余的判断

* fix: 允许copy,rename,move进行覆写

* fix: 修复move方法导致的目录被删除

* refactor: 整理关于返回Path的代码

* fix: 修复由于get方法导致的ipfs路径无法访问

* fix: 修复path处理错误的get方法

修复get方法,删除意外加入的目录

* fix: fix path join

use path join instead of filepath join to avoid os problem

* fix: rm filepath ref

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2025-04-12 17:01:30 +08:00
ddffacf07b perf: optimize IO read/write usage (#8243)
* perf: optimize IO read/write usage

* .

* Update drivers/139/driver.go

Co-authored-by: MadDogOwner <xiaoran@xrgzs.top>

---------

Co-authored-by: MadDogOwner <xiaoran@xrgzs.top>
2025-04-12 16:55:31 +08:00
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
545 changed files with 25538 additions and 7865 deletions

4
.github/FUNDING.yml vendored
View File

@ -3,11 +3,11 @@
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: xhofe # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://alist.nn.ci/guide/sponsor.html'] custom: []

View File

@ -12,18 +12,18 @@ body:
attributes: attributes:
label: Please make sure of the following things label: Please make sure of the following things
description: | description: |
You must check all the following, otherwise your issue may be closed directly. Or you can go to the [discussions](https://github.com/alist-org/alist/discussions) You must check all the following, otherwise your issue may be closed directly. Or you can go to the [discussions](https://github.com/OpenListTeam/OpenList/discussions)
您必须勾选以下所有内容否则您的issue可能会被直接关闭。或者您可以去[讨论区](https://github.com/alist-org/alist/discussions) 您必须勾选以下所有内容否则您的issue可能会被直接关闭。或者您可以去[讨论区](https://github.com/OpenListTeam/OpenList/discussions)
options: options:
- label: | - label: |
I have read the [documentation](https://alist.nn.ci). I have read the [documentation](https://openlistteam.github.io/docs).
我已经阅读了[文档](https://alist.nn.ci)。 我已经阅读了[文档](https://openlistteam.github.io/docs)。
- label: | - label: |
I'm sure there are no duplicate issues or discussions. I'm sure there are no duplicate issues or discussions.
我确定没有重复的issue或讨论。 我确定没有重复的issue或讨论。
- label: | - label: |
I'm sure it's due to `AList` and not something else(such as [Network](https://alist.nn.ci/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host) ,`Dependencies` or `Operational`). I'm sure it's due to `OpenList` and not something else(such as [Network](https://openlistteam.github.io/docs/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host) ,`Dependencies` or `Operational`).
我确定是`AList`的问题,而不是其他原因(例如[网络](https://alist.nn.ci/zh/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host)`依赖`或`操作`)。 我确定是`OpenList`的问题,而不是其他原因(例如[网络](https://openlistteam.github.io/docs/zh/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host)`依赖`或`操作`)。
- label: | - label: |
I'm sure this issue is not fixed in the latest version. I'm sure this issue is not fixed in the latest version.
我确定这个问题在最新版本中没有被修复。 我确定这个问题在最新版本中没有被修复。
@ -31,7 +31,7 @@ body:
- type: input - type: input
id: version id: version
attributes: attributes:
label: AList Version / AList 版本 label: OpenList Version / OpenList 版本
description: | description: |
What version of our software are you running? Do not use `latest` or `master` as an answer. What version of our software are you running? Do not use `latest` or `master` as an answer.
您使用的是哪个版本的软件?请不要使用`latest`或`master`作为答案。 您使用的是哪个版本的软件?请不要使用`latest`或`master`作为答案。
@ -68,8 +68,8 @@ body:
attributes: attributes:
label: Config / 配置 label: Config / 配置
description: | description: |
Please provide the configuration file of your `AList` application and take a screenshot of the relevant storage configuration. (hide privacy field) Please provide the configuration file of your `OpenList` application and take a screenshot of the relevant storage configuration. (hide privacy field)
请提供您的`AList`应用的配置文件,并截图相关存储配置。(隐藏隐私字段) 请提供您的`OpenList`应用的配置文件,并截图相关存储配置。(隐藏隐私字段)
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

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

View File

@ -7,7 +7,7 @@ body:
label: Please make sure of the following things label: Please make sure of the following things
description: You may select more than one, even select all. description: You may select more than one, even select all.
options: options:
- label: I have read the [documentation](https://alist.nn.ci). - label: I have read the [documentation](https://openlistteam.github.io/docs).
- label: I'm sure there are no duplicate issues or discussions. - label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure this feature is not implemented. - label: I'm sure this feature is not implemented.
- label: I'm sure it's a reasonable and popular requirement. - label: I'm sure it's a reasonable and popular requirement.

View File

@ -1,71 +0,0 @@
name: auto_lang
on:
push:
branches:
- 'main'
paths:
- 'drivers/**'
- 'internal/bootstrap/data/setting.go'
- 'internal/conf/const.go'
- 'cmd/lang.go'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
auto_lang:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: auto generate lang.json
runs-on: ${{ matrix.platform }}
steps:
- name: Setup go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout alist
uses: actions/checkout@v4
with:
path: alist
- name: Checkout alist-web
uses: actions/checkout@v4
with:
repository: 'alist-org/alist-web'
ref: main
persist-credentials: false
fetch-depth: 0
path: alist-web
- name: Generate lang
run: |
cd alist
go run ./main.go lang
cd ..
- name: Copy lang file
run: |
cp -f ./alist/lang/*.json ./alist-web/src/lang/en/ 2>/dev/null || :
- name: Commit git
run: |
cd alist-web
git add .
git config --local user.email "bot@nn.ci"
git config --local user.name "IlaBot"
git commit -m "chore: auto update i18n file" -a 2>/dev/null || :
cd ..
- name: Push lang files
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.MY_TOKEN }}
branch: main
directory: alist-web
repository: alist-org/alist-web

View File

@ -3,11 +3,15 @@ name: beta release
on: on:
push: push:
branches: [ 'main' ] branches: [ 'main' ]
workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions:
contents: write
jobs: jobs:
changelog: changelog:
strategy: strategy:
@ -22,15 +26,24 @@ jobs:
with: with:
fetch-depth: 0 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 - name: changelog # or changelogithub@0.12 if ensure the stable result
id: changelog id: changelog
run: | run: |
git tag -l git tag -l
npx changelogithub --output CHANGELOG.md npx changelogithub --output CHANGELOG.md
# npx changelogen@latest --output CHANGELOG.md
- name: Upload assets to beta release
- name: Upload assets
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
body_path: CHANGELOG.md body_path: CHANGELOG.md
@ -38,20 +51,33 @@ jobs:
prerelease: true prerelease: true
tag_name: beta tag_name: beta
- name: Upload assets to github artifact
uses: actions/upload-artifact@v4
with:
name: beta changelog
path: ${{ github.workspace }}/CHANGELOG.md
compression-level: 0
if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn`
release: release:
needs: needs:
- changelog - changelog
strategy: strategy:
matrix: matrix:
include: include:
- target: '!(*musl*|*windows-arm64*|*android*)' # xgo - target: '!(*musl*|*windows-arm64*|*android*|*freebsd*)' # xgo
hash: "md5" hash: "md5"
- target: 'linux-*-musl*' #musl - target: 'linux-!(arm*)-musl*' #musl-not-arm
hash: "md5-linux-musl" hash: "md5-linux-musl"
- target: 'linux-arm*-musl*' #musl-arm
hash: "md5-linux-musl-arm"
- target: 'windows-arm64' #win-arm64 - target: 'windows-arm64' #win-arm64
hash: "md5-windows-arm64" hash: "md5-windows-arm64"
- target: 'android-*' #android - target: 'android-*' #android
hash: "md5-android" hash: "md5-android"
- target: 'freebsd-*' #freebsd
hash: "md5-freebsd"
name: Beta Release name: Beta Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -67,34 +93,81 @@ jobs:
- name: Setup web - name: Setup web
run: bash build.sh dev web run: bash build.sh dev web
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build - name: Build
id: test-action uses: OpenListTeam/cgo-actions@v1.1.2
uses: go-cross/cgo-actions@v1
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
musl-target-format: $os-$musl-$arch musl-target-format: $os-$musl-$arch
out-dir: build out-dir: build
output: openlist-$target$ext
musl-base-url: "https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
x-flags: |
github.com/OpenListTeam/OpenList/internal/conf.BuiltAt=$built_at
github.com/OpenListTeam/OpenList/internal/conf.GitAuthor=OpenList
github.com/OpenListTeam/OpenList/internal/conf.GitCommit=$git_commit
github.com/OpenListTeam/OpenList/internal/conf.Version=$tag
github.com/OpenListTeam/OpenList/internal/conf.WebVersion=dev
- name: Compress - name: Compress
run: | run: |
bash build.sh zip ${{ matrix.hash }} bash build.sh zip ${{ matrix.hash }}
env:
- name: Upload assets GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# See above
- name: Upload assets to beta release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: build/compress/* files: build/compress/*
prerelease: true prerelease: true
tag_name: beta tag_name: beta
desktop: - name: Clean illegal characters from matrix.target
needs: id: clean_target_name
- release run: |
name: Beta Release Desktop ILLEGAL_CHARS_REGEX='[":<>|*?\\/\r\n]'
runs-on: ubuntu-latest CLEANED_TARGET=$(echo "${{ matrix.target }}" | sed -E "s/$ILLEGAL_CHARS_REGEX//g")
steps: echo "Original target: ${{ matrix.target }}"
- uses: dusansimic/trigger-workflow-action@v0 echo "Cleaned target: $CLEANED_TARGET"
echo "cleaned_target=$CLEANED_TARGET" >> $GITHUB_ENV
- name: Upload assets to github artifact
uses: actions/upload-artifact@v4
with: with:
name: release_beta.yml name: beta builds for ${{ env.cleaned_target }}
owner: alist-org path: ${{ github.workspace }}/build/compress/*
repo: desktop-release compression-level: 0
if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn`
# TODO: We do not have desktop clients right now. We may need a better way to
# trigger the build of desktop client when we actually have it.
# desktop:
# needs:
# - release
# name: Beta Release Desktop
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repo
# uses: actions/checkout@v4
# with:
# repository: openlistteam/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: openlistteam/desktop-release

View File

@ -5,6 +5,7 @@ on:
branches: [ 'main' ] branches: [ 'main' ]
pull_request: pull_request:
branches: [ 'main' ] branches: [ 'main' ]
workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@ -15,14 +16,17 @@ jobs:
strategy: strategy:
matrix: matrix:
platform: [ubuntu-latest] platform: [ubuntu-latest]
go-version: [ '1.21' ] target:
- darwin-amd64
- darwin-arm64
- windows-amd64
- linux-arm64-musl
- linux-amd64-musl
- windows-arm64
- android-arm64
name: Build name: Build
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -30,19 +34,31 @@ jobs:
- uses: benjlevesque/short-sha@v3.0 - uses: benjlevesque/short-sha@v3.0
id: short-sha id: short-sha
- name: Install dependencies - name: Setup Go
run: | uses: actions/setup-go@v5
sudo snap install zig --classic --beta with:
docker pull crazymax/xgo:latest go-version: '1.22'
go install github.com/crazy-max/xgo@latest
sudo apt install upx - name: Setup web
run: bash build.sh dev web
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build - name: Build
run: | uses: OpenListTeam/cgo-actions@v1.1.2
bash build.sh dev with:
targets: ${{ matrix.target }}
musl-target-format: $os-$musl-$arch
out-dir: build
x-flags: |
github.com/OpenListTeam/OpenList/internal/conf.BuiltAt=$built_at
github.com/OpenListTeam/OpenList/internal/conf.GitAuthor=OpenList
github.com/OpenListTeam/OpenList/internal/conf.GitCommit=$git_commit
github.com/OpenListTeam/OpenList/internal/conf.Version=$tag
github.com/OpenListTeam/OpenList/internal/conf.WebVersion=dev
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: alist_${{ env.SHA }} name: openlist_${{ env.SHA }}_${{ matrix.target }}
path: dist path: build/*

View File

@ -1,126 +0,0 @@
name: build_docker
on:
push:
branches: [ main ]
pull_request:
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@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: xhofe/alist
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=ref,event=pr
type=raw,value=beta,enable={{is_default_branch}}
- name: Docker meta with ffmpeg
id: meta-ffmpeg
uses: docker/metadata-action@v5
with:
images: xhofe/alist
flavor: |
suffix=-ffmpeg
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=ref,event=pr
type=raw,value=beta,enable={{is_default_branch}}
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: Cache Musl
id: cache-musl
uses: actions/cache@v4
with:
path: build/musl-libs
key: docker-musl-libs
- name: Download Musl Library
if: steps.cache-musl.outputs.cache-hit != 'true'
run: bash build.sh prepare docker-multiplatform
- name: Build go binary
run: bash build.sh dev docker-multiplatform
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ci
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
- name: Build and push with ffmpeg
id: docker_build_ffmpeg
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ci
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
labels: ${{ steps.meta-ffmpeg.outputs.labels }}
build-args: INSTALL_FFMPEG=true
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
build_docker_with_aria2:
needs: build_docker
name: Build docker with aria2
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- name: Checkout repo
uses: actions/checkout@v4
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

@ -15,6 +15,10 @@ jobs:
with: with:
fetch-depth: 0 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 - run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
env: env:
GITHUB_TOKEN: ${{secrets.MY_TOKEN}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

View File

@ -1,22 +0,0 @@
name: Close need info
on:
schedule:
- cron: "0 0 */1 * *"
workflow_dispatch:
jobs:
close-need-info:
runs-on: ubuntu-latest
steps:
- name: close-issues
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
token: ${{ secrets.GITHUB_TOKEN }}
labels: 'question'
inactive-day: 3
close-reason: 'not_planned'
body: |
Hello @${{ github.event.issue.user.login }}, this issue was closed due to no activities in 3 days.
你好 @${{ github.event.issue.user.login }}此issue因超过3天未回复被关闭。

View File

@ -1,21 +0,0 @@
name: Close inactive
on:
schedule:
- cron: "0 0 */7 * *"
workflow_dispatch:
jobs:
close-inactive:
runs-on: ubuntu-latest
steps:
- name: close-issues
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
token: ${{ secrets.GITHUB_TOKEN }}
labels: 'stale'
inactive-day: 8
close-reason: 'not_planned'
body: |
Hello @${{ github.event.issue.user.login }}, this issue was closed due to inactive more than 52 days. You can reopen or recreate it if you think it should continue. Thank you for your contributions again.

View File

@ -1,25 +0,0 @@
name: Issue Duplicate
on:
issues:
types: [labeled]
jobs:
create-comment:
runs-on: ubuntu-latest
if: github.event.label.name == 'duplicate'
steps:
- name: Create comment
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hello @${{ github.event.issue.user.login }}, your issue is a duplicate and will be closed.
你好 @${{ github.event.issue.user.login }}你的issue是重复的将被关闭。
- name: Close issue
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issue'
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,25 +0,0 @@
name: Issue Invalid
on:
issues:
types: [labeled]
jobs:
create-comment:
runs-on: ubuntu-latest
if: github.event.label.name == 'invalid'
steps:
- name: Create comment
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hello @${{ github.event.issue.user.login }}, your issue is invalid and will be closed.
你好 @${{ github.event.issue.user.login }}你的issue无效将被关闭。
- name: Close issue
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issue'
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,17 +0,0 @@
name: Remove working label when issue closed
on:
issues:
types: [closed]
jobs:
rm-working:
runs-on: ubuntu-latest
steps:
- name: Remove working label
uses: actions-cool/issues-helper@v3
with:
actions: 'remove-labels'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: 'working,pr-welcome'

View File

@ -1,20 +0,0 @@
name: Issue Question
on:
issues:
types: [labeled]
jobs:
create-comment:
runs-on: ubuntu-latest
if: github.event.label.name == 'question'
steps:
- name: Create comment
uses: actions-cool/issues-helper@v3.6.0
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hello @${{ github.event.issue.user.login }}, please input issue by template and add detail. Issues labeled by `question` will be closed if no activities in 3 days.
你好 @${{ github.event.issue.user.login }}请按照issue模板填写, 并详细说明问题/日志记录/复现步骤/复现链接/实现思路或提供更多信息等, 3天内未回复issue自动关闭。

View File

@ -1,19 +0,0 @@
name: Issues Similarity Analysis
on:
issues:
types: [opened, edited]
jobs:
similarity-analysis:
runs-on: ubuntu-latest
steps:
- name: analysis
uses: actions-cool/issues-similarity-analysis@v1
with:
filter-threshold: 0.5
comment-title: '### See'
comment-body: '${index}. ${similarity} #${number}'
show-footer: false
show-mentioned: true
since-days: 730

View File

@ -1,13 +0,0 @@
name: Translation Helper
on:
pull_request_target:
types: [opened]
issues:
types: [opened]
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions-cool/translation-helper@v1.2.0

View File

@ -1,25 +0,0 @@
name: Issue Wontfix
on:
issues:
types: [labeled]
jobs:
lock-issue:
runs-on: ubuntu-latest
if: github.event.label.name == 'wontfix'
steps:
- name: Create comment
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hello @${{ github.event.issue.user.login }}, this issue will not be worked on and will be closed.
你好 @${{ github.event.issue.user.login }},这不会被处理,将被关闭。
- name: Close issue
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issue'
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -3,6 +3,7 @@ name: release
on: on:
release: release:
types: [ published ] types: [ published ]
permissions: write-all
jobs: jobs:
release: release:
@ -13,10 +14,27 @@ jobs:
name: Release name: Release
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: 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 - name: Prerelease
uses: irongut/EditRelease@v1.2.0 uses: irongut/EditRelease@v1.2.0
with: with:
token: ${{ secrets.MY_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }} id: ${{ github.event.release.id }}
prerelease: true prerelease: true
@ -40,6 +58,8 @@ jobs:
- name: Build - name: Build
run: | run: |
bash build.sh release bash build.sh release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets - name: Upload assets
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
@ -47,29 +67,32 @@ jobs:
files: build/compress/* files: build/compress/*
prerelease: false prerelease: false
release_desktop:
needs: release
name: 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: Add tag # TODO: We do not have desktop clients right now. We may need a better way to
run: | # trigger the build of desktop client when we actually have it.
git config --local user.email "bot@nn.ci" # release_desktop:
git config --local user.name "IlaBot" # needs: release
version=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g') # name: Release desktop
git tag -a $version -m "release $version" # runs-on: ubuntu-latest
# steps:
# - name: Checkout repo
# uses: actions/checkout@v4
# with:
# repository: openlistteam/desktop-release
# ref: main
# persist-credentials: false
# fetch-depth: 0
- name: Push tags # - name: Add tag
uses: ad-m/github-push-action@master # run: |
with: # git config --local user.email "bot@nn.ci"
github_token: ${{ secrets.MY_TOKEN }} # git config --local user.name "IlaBot"
branch: main # version=$(wget -qO- -t1 -T2 "https://api.github.com/repos/openlistteam/openlist/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
repository: alist-org/desktop-release # git tag -a $version -m "release $version"
# - name: Push tags
# uses: ad-m/github-push-action@master
# with:
# github_token: ${{ secrets.MY_TOKEN }}
# branch: main
# repository: openlistteam/desktop-release

View File

@ -3,6 +3,8 @@ name: release_android
on: on:
release: release:
types: [ published ] types: [ published ]
permissions: write-all
jobs: jobs:
release_android: release_android:
@ -27,6 +29,8 @@ jobs:
- name: Build - name: Build
run: | run: |
bash build.sh release android bash build.sh release android
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets - name: Upload assets
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2

View File

@ -1,13 +1,45 @@
name: release_docker name: release_docker
on: on:
workflow_dispatch:
inputs:
manual_tag:
description: 'Tag name (like v0.1.0). Required if as_latest is true.'
required: false
type: string
as_latest:
description: 'Tag as latest?'
required: true
default: 'false'
type: choice
options:
- 'true'
- 'false'
push: push:
tags: tags:
- 'v*' - 'v*'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
ORG_NAME: openlistteam
IMAGE_NAME: openlist-git
IMAGE_NAME_DOCKERHUB: openlist
REGISTRY: ghcr.io
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' || github.event_name == 'workflow_dispatch' }}
IMAGE_IS_PROD: ${{ github.ref_type == 'tag' || github.event.inputs.as_latest == 'true' }}
IMAGE_TAGS_BETA: |
type=raw,value=beta,enable={{is_default_branch}}
permissions: write-all
jobs: jobs:
release_docker: build_binary:
name: Release Docker name: Build Binaries for Docker Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -22,20 +54,65 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: build/musl-libs path: build/musl-libs
key: docker-musl-libs key: docker-musl-libs-v2
- name: Download Musl Library - name: Download Musl Library
if: steps.cache-musl.outputs.cache-hit != 'true' if: steps.cache-musl.outputs.cache-hit != 'true'
run: bash build.sh prepare docker-multiplatform run: bash build.sh prepare docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build go binary - name: Build go binary (beta)
if: env.IMAGE_IS_PROD != 'true'
run: bash build.sh beta docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build go binary (release)
if: env.IMAGE_IS_PROD == 'true'
run: bash build.sh release docker-multiplatform run: bash build.sh release docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta - name: Upload artifacts
id: meta uses: actions/upload-artifact@v4
uses: docker/metadata-action@v5
with: with:
images: xhofe/alist 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 - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
@ -43,66 +120,46 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to GitHub Container Registry
if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: xhofe registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub Container Registry
if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3
with:
username: ${{ env.ORG_NAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.ORG_NAME }}/${{ env.IMAGE_NAME }}
${{ env.ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}
tags: >
${{ env.IMAGE_IS_PROD == 'true' && (
github.event_name == 'workflow_dispatch'
&& format('type=raw,value={0}', github.event.inputs.manual_tag)
|| format('type=raw,value={0}', github.ref_name)
) || env.IMAGE_TAGS_BETA }}
flavor: |
latest=${{ env.IMAGE_IS_PROD }}
${{ matrix.tag_favor }}
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile.ci file: Dockerfile.ci
push: true push: ${{ env.IMAGE_PUSH == 'true' }}
build-args: ${{ matrix.build_arg }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x platforms: ${{ env.RELEASE_PLATFORMS }}
- name: Docker meta with ffmpeg
id: meta-ffmpeg
uses: docker/metadata-action@v5
with:
images: xhofe/alist
flavor: |
latest=true
suffix=-ffmpeg,onlatest=true
- name: Build and push with ffmpeg
id: docker_build_ffmpeg
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ci
push: true
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
labels: ${{ steps.meta-ffmpeg.outputs.labels }}
build-args: INSTALL_FFMPEG=true
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@v4
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

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

@ -0,0 +1,38 @@
name: release_freebsd
on:
release:
types: [ published ]
permissions: write-all
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
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*

View File

@ -3,7 +3,7 @@ name: release_linux_musl
on: on:
release: release:
types: [ published ] types: [ published ]
permissions: write-all
jobs: jobs:
release_linux_musl: release_linux_musl:
strategy: strategy:
@ -27,6 +27,8 @@ jobs:
- name: Build - name: Build
run: | run: |
bash build.sh release linux_musl bash build.sh release linux_musl
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets - name: Upload assets
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2

View File

@ -3,7 +3,8 @@ name: release_linux_musl_arm
on: on:
release: release:
types: [ published ] types: [ published ]
permissions: write-all
jobs: jobs:
release_linux_musl_arm: release_linux_musl_arm:
strategy: strategy:
@ -27,6 +28,8 @@ jobs:
- name: Build - name: Build
run: | run: |
bash build.sh release linux_musl_arm bash build.sh release linux_musl_arm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets - name: Upload assets
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2

143
.github/workflows/test_docker.yml vendored Normal file
View File

@ -0,0 +1,143 @@
name: test_docker
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
ORG_NAME: openlistteam
IMAGE_NAME: openlist-git
IMAGE_NAME_DOCKERHUB: openlist
REGISTRY: ghcr.io
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_TAGS_BETA: |
type=ref,event=pr
type=raw,value=beta,enable={{is_default_branch}}
jobs:
build_binary:
name: Build Binaries for Docker Release
runs-on: ubuntu-latest
steps:
- name: Checkout
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
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build go binary (beta)
run: bash build.sh beta docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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
permissions:
contents: read
packages: write
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 GitHub Container Registry
if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub Container Registry
if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3
with:
username: ${{ env.ORG_NAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.ORG_NAME }}/${{ env.IMAGE_NAME }}
${{ env.ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}
tags: ${{ env.IMAGE_TAGS_BETA }}
flavor: |
${{ matrix.tag_favor }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ci
push: ${{ env.IMAGE_PUSH == 'true' }}
build-args: ${{ matrix.build_arg }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ env.RELEASE_PLATFORMS }}

View File

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
i@nn.ci. [Telegram Group](https://t.me/OpenListTeam).
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

View File

@ -2,7 +2,7 @@
## Setup your machine ## Setup your machine
`alist` is written in [Go](https://golang.org/) and [React](https://reactjs.org/). `OpenList` is written in [Go](https://golang.org/) and [React](https://reactjs.org/).
Prerequisites: Prerequisites:
@ -11,11 +11,11 @@ Prerequisites:
- [gcc](https://gcc.gnu.org/) - [gcc](https://gcc.gnu.org/)
- [nodejs](https://nodejs.org/) - [nodejs](https://nodejs.org/)
Clone `alist` and `alist-web` anywhere: Clone `OpenList` and `OpenList-Frontend` anywhere:
```shell ```shell
$ git clone https://github.com/alist-org/alist.git $ git clone https://github.com/OpenListTeam/OpenList.git
$ git clone --recurse-submodules https://github.com/alist-org/alist-web.git $ git clone --recurse-submodules https://github.com/OpenListTeam/OpenList-Frontend.git
``` ```
You should switch to the `main` branch for development. You should switch to the `main` branch for development.
@ -103,5 +103,5 @@ The rest of the commit message is then used for this.
## Submit a pull request ## Submit a pull request
Push your branch to your `alist` fork and open a pull request against the Push your branch to your `openlist` fork and open a pull request against the
`main` branch. `main` branch.

View File

@ -1,7 +1,7 @@
FROM alpine:edge as builder FROM docker.io/library/alpine:edge AS builder
LABEL stage=go-builder LABEL stage=go-builder
WORKDIR /app/ WORKDIR /app/
RUN apk add --no-cache bash curl gcc git go musl-dev RUN apk add --no-cache bash curl jq gcc git go musl-dev
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY ./ ./ COPY ./ ./
@ -10,21 +10,33 @@ RUN bash build.sh release docker
FROM alpine:edge FROM alpine:edge
ARG INSTALL_FFMPEG=false ARG INSTALL_FFMPEG=false
LABEL MAINTAINER="i@nn.ci" ARG INSTALL_ARIA2=false
LABEL MAINTAINER="OpenList"
WORKDIR /opt/alist/ WORKDIR /opt/openlist/
RUN apk update && \ RUN apk update && \
apk upgrade --no-cache && \ apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates su-exec tzdata; \ apk add --no-cache bash ca-certificates su-exec tzdata; \
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \ [ "$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/* rm -rf /var/cache/apk/*
COPY --from=builder /app/bin/alist ./ COPY --chmod=755 --from=builder /app/bin/openlist ./
COPY entrypoint.sh /entrypoint.sh COPY --chmod=755 entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh && /entrypoint.sh version RUN /entrypoint.sh version
ENV PUID=0 PGID=0 UMASK=022 ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
VOLUME /opt/alist/data/ VOLUME /opt/openlist/data/
EXPOSE 5244 5245 EXPOSE 5244 5245
CMD [ "/entrypoint.sh" ] CMD [ "/entrypoint.sh" ]

View File

@ -1,22 +1,34 @@
FROM alpine:edge FROM docker.io/library/alpine:edge
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG INSTALL_FFMPEG=false ARG INSTALL_FFMPEG=false
LABEL MAINTAINER="i@nn.ci" ARG INSTALL_ARIA2=false
LABEL MAINTAINER="OpenList"
WORKDIR /opt/alist/ WORKDIR /opt/openlist/
RUN apk update && \ RUN apk update && \
apk upgrade --no-cache && \ apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates su-exec tzdata; \ apk add --no-cache bash ca-certificates su-exec tzdata; \
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \ [ "$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/* rm -rf /var/cache/apk/*
COPY /build/${TARGETPLATFORM}/alist ./ COPY --chmod=755 /build/${TARGETPLATFORM}/openlist ./
COPY entrypoint.sh /entrypoint.sh COPY --chmod=755 entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh && /entrypoint.sh version RUN /entrypoint.sh version
ENV PUID=0 PGID=0 UMASK=022 ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
VOLUME /opt/alist/data/ VOLUME /opt/openlist/data/
EXPOSE 5244 5245 EXPOSE 5244 5245
CMD [ "/entrypoint.sh" ] CMD [ "/entrypoint.sh" ]

View File

@ -1,45 +1,41 @@
<div align="center"> <div align="center">
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a> <img width="100px" alt="logo" src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg"/></a>
<p><em>🗂A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p> <p><em>🗂A file list program that supports multiple storages, powered by Gin and SolidJS, fork of AList.</em></p>
<div> <div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3"> <a href="https://goreportcard.com/report/github.com/OpenListTeam/OpenList/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" /> <img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
</a> </a>
<a href="https://github.com/alist-org/alist/blob/main/LICENSE"> <a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" /> <img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" />
</a> </a>
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild"> <a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" /> <img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" />
</a> </a>
<a href="https://github.com/alist-org/alist/releases"> <a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" /> <img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
<img src="https://badges.crowdin.net/alist/localized.svg">
</a> </a>
</div> </div>
<div> <div>
<a href="https://github.com/alist-org/alist/discussions"> <a href="https://github.com/OpenListTeam/OpenList/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" /> <img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" />
</a> </a>
<a href="https://discord.gg/F4ymsH4xv2"> <a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" /> <img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<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">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a>
<a href="https://alist.nn.ci/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a> </a>
</div> </div>
</div> </div>
--- ---
English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md) > [!IMPORTANT]
>
> Drop-in replacement for AList with long-term governance, no hidden risks, and full transparency, built to defend open source against trust-based attacks.
>
> We sincerely thank the author [Xhofe](https://github.com/Xhofe) of the original project [AlistGo/alist](https://github.com/AlistGo/alist) and all other contributors.
>
> This fork is not yet stable, specific migration progress can be viewed in [OpenList Migration Work Summary](https://github.com/OpenListTeam/OpenList/issues/6).
English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE OF CONDUCT](./CODE_OF_CONDUCT.md)
## Features ## Features
@ -58,7 +54,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] WebDav(Support OneDrive/SharePoint without API) - [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ )) - [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
- [x] [Mediatrack](https://www.mediatrack.cn/) - [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] [YandexDisk](https://disk.yandex.com/)
- [x] [BaiduNetdisk](http://pan.baidu.com/) - [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Terabox](https://www.terabox.com/main) - [x] [Terabox](https://www.terabox.com/main)
@ -77,6 +73,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] [Dropbox](https://www.dropbox.com/) - [x] [Dropbox](https://www.dropbox.com/)
- [x] [FeijiPan](https://www.feijipan.com/) - [x] [FeijiPan](https://www.feijipan.com/)
- [x] [dogecloud](https://www.dogecloud.com/product/oss) - [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] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...) - [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode - [x] Image preview in gallery mode
@ -87,8 +84,8 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] Dark mode - [x] Dark mode
- [x] I18n - [x] I18n
- [x] Protected routes (password protection and authentication) - [x] Protected routes (password protection and authentication)
- [x] WebDav (see https://alist.nn.ci/guide/webdav.html for details) - [x] WebDav (waiting for detail documents)
- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist) - [ ] Docker Deploy (rebuilding)
- [x] Cloudflare Workers proxy - [x] Cloudflare Workers proxy
- [x] File/Folder package download - [x] File/Folder package download
- [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy - [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy
@ -98,44 +95,35 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
## Document ## Document
<https://alist.nn.ci/> - https://docs.oplist.org
- https://docs.openlist.team
## Demo ## Demo
<https://al.nn.ci> N/A (to be rebuilt)
## Discussion ## Discussion
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.** Please refer to [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) for raising general questions, ***Issues* is for bug reports and feature requests only.**
## Sponsor
AList is an open-source software, if you happen to like this project and want me to keep going, please consider sponsoring me or providing a single donation! Thanks for all the love and support:
https://alist.nn.ci/guide/sponsor.html
### Special sponsors
- [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 ## Contributors
Thanks goes to these wonderful people: Thanks goes to these wonderful people:
[![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) [![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
## License ## License
The `AList` is open-source software licensed under the AGPL-3.0 license. The `OpenList` is open-source software licensed under the AGPL-3.0 license.
## Disclaimer ## Disclaimer
- This program is a free and open source project. It is designed to share files on the network disk, which is convenient for downloading and learning Golang. Please abide by relevant laws and regulations when using it, and do not abuse it; - This program is a free and open source project. It is designed to share files on the network disk, which is convenient for downloading and learning Golang. Please abide by relevant laws and regulations when using it, and do not abuse it;
- This program is implemented by calling the official sdk/interface, without destroying the official interface behavior; - This program is implemented by calling the official sdk/interface, without destroying the official interface behavior;
- This program only does 302 redirect/traffic forwarding, and does not intercept, store, or tamper with any user data; - This program only does 302 redirect/traffic forwarding, and does not intercept, store, or tamper with any user data;
- Before using this program, you should understand and bear the corresponding risks, including but not limited to account ban, download speed limit, etc., which is none of this program's business; - Before using this program, you should understand and bear the corresponding risks, including but not limited to account ban, download speed limit, etc., which is none of this program's business;
- If there is any infringement, please contact me by [email](mailto:i@nn.ci), and it will be dealt with in time. - If there is any infringement, please contact [OpenListTeam](https://github.com/OpenListTeam), and it will be dealt with in time.
--- ---
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2) > [@GitHub](https://github.com/OpenListTeam) · [Telegram Group](https://t.me/OpenListTeam) · [Telegram Channel](https://t.me/OpenListOfficial)

View File

@ -1,45 +1,41 @@
<div align="center"> <div align="center">
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a> <img width="100px" alt="logo" src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg"/></a>
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p> <p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 SolidJS基于 AList 项目 fork 开发</em></p>
<div> <div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3"> <a href="https://goreportcard.com/report/github.com/OpenListTeam/OpenList/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" /> <img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
</a> </a>
<a href="https://github.com/alist-org/alist/blob/main/LICENSE"> <a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" /> <img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" />
</a> </a>
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild"> <a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" /> <img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" />
</a> </a>
<a href="https://github.com/alist-org/alist/releases"> <a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" /> <img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
<img src="https://badges.crowdin.net/alist/localized.svg">
</a> </a>
</div> </div>
<div> <div>
<a href="https://github.com/alist-org/alist/discussions"> <a href="https://github.com/OpenListTeam/OpenList/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" /> <img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" />
</a> </a>
<a href="https://discord.gg/F4ymsH4xv2"> <a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" /> <img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<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">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a>
<a href="https://alist.nn.ci/zh/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a> </a>
</div> </div>
</div> </div>
--- ---
[English](./README.md) | 中文 | [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md) > [!IMPORTANT]
>
> 一个更可信、可持续的 AList 开源替代方案,防范未来可能的闭源、黑箱或不可信变更。
>
> 我们诚挚地感谢原项目 [AlistGo/alist](https://github.com/AlistGo/alist) 的作者 [Xhofe](https://github.com/Xhofe) 以及其他所有贡献者。
>
> 本 Fork 尚未稳定, 具体迁移进度可在 [OpenList 迁移工作总结](https://github.com/OpenListTeam/OpenList/issues/6) 中查看。
[English](./README.md) | 中文 | [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE OF CONDUCT](./CODE_OF_CONDUCT.md)
## 功能 ## 功能
@ -58,7 +54,7 @@
- [x] WebDav(支持无API的OneDrive/SharePoint) - [x] WebDav(支持无API的OneDrive/SharePoint)
- [x] Teambition[中国](https://www.teambition.com/ )[国际](https://us.teambition.com/ ) - [x] Teambition[中国](https://www.teambition.com/ )[国际](https://us.teambition.com/ )
- [x] [分秒帧](https://www.mediatrack.cn/) - [x] [分秒帧](https://www.mediatrack.cn/)
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云) - [x] [和彩云](https://yun.139.com/) (个人云, 家庭云,共享群组)
- [x] [Yandex.Disk](https://disk.yandex.com/) - [x] [Yandex.Disk](https://disk.yandex.com/)
- [x] [百度网盘](http://pan.baidu.com/) - [x] [百度网盘](http://pan.baidu.com/)
- [x] [UC网盘](https://drive.uc.cn) - [x] [UC网盘](https://drive.uc.cn)
@ -86,8 +82,8 @@
- [x] 黑暗模式 - [x] 黑暗模式
- [x] 国际化 - [x] 国际化
- [x] 受保护的路由(密码保护和身份验证) - [x] 受保护的路由(密码保护和身份验证)
- [x] WebDav (具体见 https://alist.nn.ci/zh/guide/webdav.html) - [x] WebDav (详细文档待补充)
- [x] [Docker 部署](https://hub.docker.com/r/xhofe/alist) - [ ] Docker 部署(重建中)
- [x] Cloudflare workers 中转 - [x] Cloudflare workers 中转
- [x] 文件/文件夹打包下载 - [x] 文件/文件夹打包下载
- [x] 网页上传(可以允许访客上传),删除,新建文件夹,重命名,移动,复制 - [x] 网页上传(可以允许访客上传),删除,新建文件夹,重命名,移动,复制
@ -97,43 +93,35 @@
## 文档 ## 文档
<https://alist.nn.ci/zh/> - https://docs.oplist.org
- https://docs.openlist.team
## Demo ## Demo
<https://al.nn.ci> N/A重建中
## 讨论 ## 讨论
一般问题请到[讨论论坛](https://github.com/alist-org/alist/discussions) **issue仅针对错误报告和功能请求。** 一般问题请到 [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) 讨论***Issues* 仅针对错误报告和功能请求。**
## 赞助
AList 是一个开源软件如果你碰巧喜欢这个项目并希望我继续下去请考虑赞助我或提供一个单一的捐款感谢所有的爱和支持https://alist.nn.ci/zh/guide/sponsor.html
### 特别赞助
- [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://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) [![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
## 许可 ## 许可
`AList` AGPL-3.0 许可许可的开源软件。 `OpenList` AGPL-3.0 许可许可的开源软件。
## 免责声明 ## 免责声明
- 本程序为免费开源项目旨在分享网盘文件方便下载以及学习golang使用时请遵守相关法律法规请勿滥用 - 本程序为免费开源项目旨在分享网盘文件方便下载以及学习golang使用时请遵守相关法律法规请勿滥用
- 本程序通过调用官方sdk/接口实现,无破坏官方接口行为; - 本程序通过调用官方sdk/接口实现,无破坏官方接口行为;
- 本程序仅做302重定向/流量转发,不拦截、存储、篡改任何用户数据; - 本程序仅做302重定向/流量转发,不拦截、存储、篡改任何用户数据;
- 在使用本程序之前你应了解并承担相应的风险包括但不限于账号被ban下载限速等与本程序无关 - 在使用本程序之前你应了解并承担相应的风险包括但不限于账号被ban下载限速等与本程序无关
- 如有侵权,请通过[邮件](mailto:i@nn.ci)与我联系,会及时处理。 - 如有侵权,请联系[OpenListTeam](https://github.com/OpenListTeam),团队会及时处理。
--- ---
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2) > [@GitHub](https://github.com/OpenListTeam) · [Telegram 交流群](https://t.me/OpenListTeam) · [Telegram 频道](https://t.me/OpenListOfficial)

View File

@ -1,45 +1,41 @@
<div align="center"> <div align="center">
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a> <img width="100px" alt="logo" src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg"/></a>
<p><em>🗂Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p> <p><em>🗂複数のストレージをサポートするファイルリストプログラムで、Gin と SolidJS を使用し、AList プロジェクトをフォークして開発されました</em></p>
<div> <div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3"> <a href="https://goreportcard.com/report/github.com/OpenListTeam/OpenList/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" /> <img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
</a> </a>
<a href="https://github.com/alist-org/alist/blob/main/LICENSE"> <a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" /> <img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" />
</a> </a>
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild"> <a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" /> <img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" />
</a> </a>
<a href="https://github.com/alist-org/alist/releases"> <a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" /> <img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
<img src="https://badges.crowdin.net/alist/localized.svg">
</a> </a>
</div> </div>
<div> <div>
<a href="https://github.com/alist-org/alist/discussions"> <a href="https://github.com/OpenListTeam/OpenList/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" /> <img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" />
</a> </a>
<a href="https://discord.gg/F4ymsH4xv2"> <a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" /> <img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<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">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a>
<a href="https://alist.nn.ci/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a> </a>
</div> </div>
</div> </div>
--- ---
[English](./README.md) | [中文](./README_cn.md) | 日本語 | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md) > [!IMPORTANT]
>
> より信頼性が高く、持続可能なAListのオープンソース代替案で、将来起こりうる非公開化、ブラックボックス化、または信頼できない変更から保護します。
>
> 元のプロジェクト [AlistGo/alist](https://github.com/AlistGo/alist) の作者 [Xhofe](https://github.com/Xhofe) および他のすべての貢献者に心から感謝いたします。
>
> このForkはまだ安定していません。具体的な移行の進捗状況は [OpenList 移行作業のまとめ](https://github.com/OpenListTeam/OpenList/issues/6) でご確認いただけます。
[English](./README.md) | [中文](./README_cn.md) | 日本語 | [Contributing](./CONTRIBUTING.md) | [CODE OF CONDUCT](./CODE_OF_CONDUCT.md)
## 特徴 ## 特徴
@ -58,7 +54,7 @@
- [x] WebDav(Support OneDrive/SharePoint without API) - [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ )) - [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
- [x] [Mediatrack](https://www.mediatrack.cn/) - [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] [YandexDisk](https://disk.yandex.com/)
- [x] [BaiduNetdisk](http://pan.baidu.com/) - [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Terabox](https://www.terabox.com/main) - [x] [Terabox](https://www.terabox.com/main)
@ -87,8 +83,8 @@
- [x] ダークモード - [x] ダークモード
- [x] 国際化 - [x] 国際化
- [x] 保護されたルート (パスワード保護と認証) - [x] 保護されたルート (パスワード保護と認証)
- [x] WebDav (詳細は https://alist.nn.ci/guide/webdav.html を参照) - [x] WebDav(詳細なドキュメントは今後追加予定)
- [x] [Docker デプロイ](https://hub.docker.com/r/xhofe/alist) - [ ] Docker デプロイ(再構築中)
- [x] Cloudflare ワーカープロキシ - [x] Cloudflare ワーカープロキシ
- [x] ファイル/フォルダパッケージのダウンロード - [x] ファイル/フォルダパッケージのダウンロード
- [x] ウェブアップロード(訪問者にアップロードを許可できる), 削除, mkdir, 名前変更, 移動, コピー - [x] ウェブアップロード(訪問者にアップロードを許可できる), 削除, mkdir, 名前変更, 移動, コピー
@ -98,44 +94,35 @@
## ドキュメント ## ドキュメント
<https://alist.nn.ci/> - https://docs.oplist.org
- https://docs.openlist.team
## デモ ## デモ
<https://al.nn.ci> N/A (再構築中)
## ディスカッション ## ディスカッション
一般的なご質問は[ディスカッションフォーラム](https://github.com/alist-org/alist/discussions)をご利用ください。**問題はバグレポートと機能リクエストのみです。** 一般的なご質問は [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) をご利用ください。***Issues* はバグ報告と機能リクエストに限定されています。**
## スポンサー
AList はオープンソースのソフトウェアです。もしあなたがこのプロジェクトを気に入ってくださり、続けて欲しいと思ってくださるなら、ぜひスポンサーになってくださるか、1口でも寄付をしてくださるようご検討くださいすべての愛とサポートに感謝します:
https://alist.nn.ci/guide/sponsor.html
### スペシャルスポンサー
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
## コントリビューター ## コントリビューター
これらの素晴らしい人々に感謝します: これらの素晴らしい人々に感謝します:
[![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) [![Contributors](https://contrib.rocks/image?repo=openlistteam/openlist)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
## ライセンス ## ライセンス
`AList` は AGPL-3.0 ライセンスの下でライセンスされたオープンソースソフトウェアです。 `OpenList`は AGPL-3.0 ライセンスの下で公開されているオープンソースソフトウェアです。
## 免責事項 ## 免責事項
- このプログラムはフリーでオープンソースのプロジェクトです。ネットワークディスク上でファイルを共有するように設計されており、golang のダウンロードや学習に便利です。利用にあたっては関連法規を遵守し、悪用しないようお願いします; - このプログラムはフリーでオープンソースのプロジェクトです。ネットワークディスク上でファイルを共有するように設計されており、golang のダウンロードや学習に便利です。利用にあたっては関連法規を遵守し、悪用しないようお願いします;
- このプログラムは、公式インターフェースの動作を破壊することなく、公式 sdk/インターフェースを呼び出すことで実装されています; - このプログラムは、公式インターフェースの動作を破壊することなく、公式 sdk/インターフェースを呼び出すことで実装されています;
- このプログラムは、302リダイレクト/トラフィック転送のみを行い、いかなるユーザーデータも傍受、保存、改ざんしません; - このプログラムは、302リダイレクト/トラフィック転送のみを行い、いかなるユーザーデータも傍受、保存、改ざんしません;
- このプログラムを使用する前に、アカウントの禁止、ダウンロード速度の制限など、対応するリスクを理解し、負担する必要があります; - このプログラムを使用する前に、アカウントの禁止、ダウンロード速度の制限など、対応するリスクを理解し、負担する必要があります;
- もし侵害があれば、[メール](mailto:i@nn.ci)で私に連絡してください - 権利侵害がある場合は、[OpenListTeam](https://github.com/OpenListTeam) までご連絡ください。チームが迅速に対応いたします
--- ---
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2) > [@GitHub](https://github.com/OpenListTeam) · [Telegram Group](https://t.me/OpenListTeam) · [Telegram Channel](https://t.me/OpenListOfficial)

198
build.sh
View File

@ -1,15 +1,27 @@
appName="alist" set -e
appName="openlist"
builtAt="$(date +'%F %T %z')" builtAt="$(date +'%F %T %z')"
goVersion=$(go version | sed 's/go version //') gitAuthor="The OpenList Projects Contributors <noreply@openlist.team>"
gitAuthor="Xhofe <i@nn.ci>"
gitCommit=$(git log --pretty=format:"%h" -1) gitCommit=$(git log --pretty=format:"%h" -1)
githubAuthHeader=""
githubAuthValue=""
if [ -n "$GITHUB_TOKEN" ]; then
githubAuthHeader="--header"
githubAuthValue="Authorization: Bearer $GITHUB_TOKEN"
fi
if [ "$1" = "dev" ]; then if [ "$1" = "dev" ]; then
version="dev" version="dev"
webVersion="dev" webVersion="dev"
elif [ "$1" = "beta" ]; then
version="beta"
webVersion="dev"
else else
version=$(git describe --abbrev=0 --tags) git tag -d beta || true
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') # Always true if there's no tag
version=$(git describe --abbrev=0 --tags 2>/dev/null || echo "v0.0.0")
webVersion=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
fi fi
echo "backend version: $version" echo "backend version: $version"
@ -17,27 +29,36 @@ echo "frontend version: $webVersion"
ldflags="\ ldflags="\
-w -s \ -w -s \
-X 'github.com/alist-org/alist/v3/internal/conf.BuiltAt=$builtAt' \ -X 'github.com/OpenListTeam/OpenList/internal/conf.BuiltAt=$builtAt' \
-X 'github.com/alist-org/alist/v3/internal/conf.GoVersion=$goVersion' \ -X 'github.com/OpenListTeam/OpenList/internal/conf.GitAuthor=$gitAuthor' \
-X 'github.com/alist-org/alist/v3/internal/conf.GitAuthor=$gitAuthor' \ -X 'github.com/OpenListTeam/OpenList/internal/conf.GitCommit=$gitCommit' \
-X 'github.com/alist-org/alist/v3/internal/conf.GitCommit=$gitCommit' \ -X 'github.com/OpenListTeam/OpenList/internal/conf.Version=$version' \
-X 'github.com/alist-org/alist/v3/internal/conf.Version=$version' \ -X 'github.com/OpenListTeam/OpenList/internal/conf.WebVersion=$webVersion' \
-X 'github.com/alist-org/alist/v3/internal/conf.WebVersion=$webVersion' \
" "
FetchWebDev() { FetchWebDev() {
curl -L https://codeload.github.com/alist-org/web-dist/tar.gz/refs/heads/dev -o web-dist-dev.tar.gz pre_release_tag=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases | jq -r 'map(select(.prerelease)) | first | .tag_name')
tar -zxvf web-dist-dev.tar.gz if [ -z "$pre_release_tag" ] || [ "$pre_release_tag" == "null" ]; then
rm -rf public/dist # fall back to latest release
mv -f web-dist-dev/dist public pre_release_json=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest")
rm -rf web-dist-dev web-dist-dev.tar.gz else
pre_release_json=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/tags/$pre_release_tag")
fi
pre_release_assets=$(echo "$pre_release_json" | jq -r '.assets[].browser_download_url')
pre_release_tar_url=$(echo "$pre_release_assets" | grep "openlist-frontend-dist" | grep "\.tar\.gz$")
curl -fsSL "$pre_release_tar_url" -o web-dist-dev.tar.gz
rm -rf public/dist && mkdir -p public/dist
tar -zxvf web-dist-dev.tar.gz -C public/dist
rm -rf web-dist-dev.tar.gz
} }
FetchWebRelease() { FetchWebRelease() {
curl -L https://github.com/alist-org/alist-web/releases/latest/download/dist.tar.gz -o dist.tar.gz release_json=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest")
tar -zxvf dist.tar.gz release_assets=$(echo "$release_json" | jq -r '.assets[].browser_download_url')
rm -rf public/dist release_tar_url=$(echo "$release_assets" | grep "openlist-frontend-dist" | grep "\.tar\.gz$")
mv -f dist public curl -fsSL "$release_tar_url" -o dist.tar.gz
rm -rf public/dist && mkdir -p public/dist
tar -zxvf dist.tar.gz -C public/dist
rm -rf dist.tar.gz rm -rf dist.tar.gz
} }
@ -57,11 +78,11 @@ BuildDev() {
rm -rf .git/ rm -rf .git/
mkdir -p "dist" mkdir -p "dist"
muslflags="--extldflags '-static -fpic' $ldflags" muslflags="--extldflags '-static -fpic' $ldflags"
BASE="https://musl.nn.ci/" BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross) FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross)
for i in "${FILES[@]}"; do for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz" url="${BASE}${i}.tgz"
curl -L -o "${i}.tgz" "${url}" curl -fsSL -o "${i}.tgz" "${url}"
sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
done done
OS_ARCHES=(linux-musl-amd64 linux-musl-arm64) OS_ARCHES=(linux-musl-amd64 linux-musl-arm64)
@ -77,26 +98,26 @@ BuildDev() {
go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter . go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
done done
xgo -targets=windows/amd64,darwin/amd64,darwin/arm64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter . xgo -targets=windows/amd64,darwin/amd64,darwin/arm64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
mv alist-* dist mv "$appName"-* dist
cd dist cd dist
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe cp ./"$appName"-windows-amd64.exe ./"$appName"-windows-amd64-upx.exe
upx -9 ./alist-windows-amd64-upx.exe upx -9 ./"$appName"-windows-amd64-upx.exe
find . -type f -print0 | xargs -0 md5sum >md5.txt find . -type f -print0 | xargs -0 md5sum >md5.txt
cat md5.txt cat md5.txt
} }
BuildDocker() { BuildDocker() {
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter . go build -o ./bin/"$appName" -ldflags="$ldflags" -tags=jsoniter .
} }
PrepareBuildDockerMusl() { PrepareBuildDockerMusl() {
mkdir -p build/musl-libs mkdir -p build/musl-libs
BASE="https://musl.cc/" BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross) FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross riscv64-linux-musl-cross powerpc64le-linux-musl-cross)
for i in "${FILES[@]}"; do for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz" url="${BASE}${i}.tgz"
lib_tgz="build/${i}.tgz" lib_tgz="build/${i}.tgz"
curl -L -o "${lib_tgz}" "${url}" curl -fsSL -o "${lib_tgz}" "${url}"
tar xf "${lib_tgz}" --strip-components 1 -C build/musl-libs tar xf "${lib_tgz}" --strip-components 1 -C build/musl-libs
rm -f "${lib_tgz}" rm -f "${lib_tgz}"
done done
@ -111,8 +132,8 @@ BuildDockerMultiplatform() {
docker_lflags="--extldflags '-static -fpic' $ldflags" docker_lflags="--extldflags '-static -fpic' $ldflags"
export CGO_ENABLED=1 export CGO_ENABLED=1
OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-s390x) 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) 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 for i in "${!OS_ARCHES[@]}"; do
os_arch=${OS_ARCHES[$i]} os_arch=${OS_ARCHES[$i]}
cgo_cc=${CGO_ARGS[$i]} cgo_cc=${CGO_ARGS[$i]}
@ -122,7 +143,7 @@ BuildDockerMultiplatform() {
export GOARCH=$arch export GOARCH=$arch
export CC=${cgo_cc} export CC=${cgo_cc}
echo "building for $os_arch" echo "building for $os_arch"
go build -o build/$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter . go build -o build/$os/$arch/"$appName" -ldflags="$docker_lflags" -tags=jsoniter .
done done
DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7) DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)
@ -136,36 +157,36 @@ BuildDockerMultiplatform() {
export GOARM=${GO_ARM[$i]} export GOARM=${GO_ARM[$i]}
export CC=${cgo_cc} export CC=${cgo_cc}
echo "building for $docker_arch" echo "building for $docker_arch"
go build -o build/${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter . go build -o build/${docker_arch%%-*}/${docker_arch##*-}/"$appName" -ldflags="$docker_lflags" -tags=jsoniter .
done done
} }
BuildRelease() { BuildRelease() {
rm -rf .git/ rm -rf .git/
mkdir -p "build" mkdir -p "build"
BuildWinArm64 ./build/alist-windows-arm64.exe BuildWinArm64 ./build/"$appName"-windows-arm64.exe
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter . xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
# why? Because some target platforms seem to have issues with upx compression # why? Because some target platforms seem to have issues with upx compression
upx -9 ./alist-linux-amd64 upx -9 ./"$appName"-linux-amd64
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe cp ./"$appName"-windows-amd64.exe ./"$appName"-windows-amd64-upx.exe
upx -9 ./alist-windows-amd64-upx.exe upx -9 ./"$appName"-windows-amd64-upx.exe
mv alist-* build mv "$appName"-* build
} }
BuildReleaseLinuxMusl() { BuildReleaseLinuxMusl() {
rm -rf .git/ rm -rf .git/
mkdir -p "build" mkdir -p "build"
muslflags="--extldflags '-static -fpic' $ldflags" muslflags="--extldflags '-static -fpic' $ldflags"
BASE="https://musl.nn.ci/" BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross mips-linux-musl-cross mips64-linux-musl-cross mips64el-linux-musl-cross mipsel-linux-musl-cross powerpc64le-linux-musl-cross s390x-linux-musl-cross) FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross mips-linux-musl-cross mips64-linux-musl-cross mips64el-linux-musl-cross mipsel-linux-musl-cross powerpc64le-linux-musl-cross s390x-linux-musl-cross loongarch64-linux-musl-cross)
for i in "${FILES[@]}"; do for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz" url="${BASE}${i}.tgz"
curl -L -o "${i}.tgz" "${url}" curl -fsSL -o "${i}.tgz" "${url}"
sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
rm -f "${i}.tgz" rm -f "${i}.tgz"
done done
OS_ARCHES=(linux-musl-amd64 linux-musl-arm64 linux-musl-mips linux-musl-mips64 linux-musl-mips64le linux-musl-mipsle linux-musl-ppc64le linux-musl-s390x) OS_ARCHES=(linux-musl-amd64 linux-musl-arm64 linux-musl-mips linux-musl-mips64 linux-musl-mips64le linux-musl-mipsle linux-musl-ppc64le linux-musl-s390x linux-musl-loong64)
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc mips-linux-musl-gcc mips64-linux-musl-gcc mips64el-linux-musl-gcc mipsel-linux-musl-gcc powerpc64le-linux-musl-gcc s390x-linux-musl-gcc) CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc mips-linux-musl-gcc mips64-linux-musl-gcc mips64el-linux-musl-gcc mipsel-linux-musl-gcc powerpc64le-linux-musl-gcc s390x-linux-musl-gcc loongarch64-linux-musl-gcc)
for i in "${!OS_ARCHES[@]}"; do for i in "${!OS_ARCHES[@]}"; do
os_arch=${OS_ARCHES[$i]} os_arch=${OS_ARCHES[$i]}
cgo_cc=${CGO_ARGS[$i]} cgo_cc=${CGO_ARGS[$i]}
@ -182,18 +203,14 @@ BuildReleaseLinuxMuslArm() {
rm -rf .git/ rm -rf .git/
mkdir -p "build" mkdir -p "build"
muslflags="--extldflags '-static -fpic' $ldflags" muslflags="--extldflags '-static -fpic' $ldflags"
BASE="https://musl.nn.ci/" BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
# FILES=(arm-linux-musleabi-cross arm-linux-musleabihf-cross armeb-linux-musleabi-cross armeb-linux-musleabihf-cross armel-linux-musleabi-cross armel-linux-musleabihf-cross armv5l-linux-musleabi-cross armv5l-linux-musleabihf-cross armv6-linux-musleabi-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross armv7m-linux-musleabi-cross armv7r-linux-musleabihf-cross)
FILES=(arm-linux-musleabi-cross arm-linux-musleabihf-cross armel-linux-musleabi-cross armel-linux-musleabihf-cross armv5l-linux-musleabi-cross armv5l-linux-musleabihf-cross armv6-linux-musleabi-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross armv7m-linux-musleabi-cross armv7r-linux-musleabihf-cross) FILES=(arm-linux-musleabi-cross arm-linux-musleabihf-cross armel-linux-musleabi-cross armel-linux-musleabihf-cross armv5l-linux-musleabi-cross armv5l-linux-musleabihf-cross armv6-linux-musleabi-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross armv7m-linux-musleabi-cross armv7r-linux-musleabihf-cross)
for i in "${FILES[@]}"; do for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz" url="${BASE}${i}.tgz"
curl -L -o "${i}.tgz" "${url}" curl -fsSL -o "${i}.tgz" "${url}"
sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
rm -f "${i}.tgz" rm -f "${i}.tgz"
done done
# OS_ARCHES=(linux-musleabi-arm linux-musleabihf-arm linux-musleabi-armeb linux-musleabihf-armeb linux-musleabi-armel linux-musleabihf-armel linux-musleabi-armv5l linux-musleabihf-armv5l linux-musleabi-armv6 linux-musleabihf-armv6 linux-musleabihf-armv7l linux-musleabi-armv7m linux-musleabihf-armv7r)
# CGO_ARGS=(arm-linux-musleabi-gcc arm-linux-musleabihf-gcc armeb-linux-musleabi-gcc armeb-linux-musleabihf-gcc armel-linux-musleabi-gcc armel-linux-musleabihf-gcc armv5l-linux-musleabi-gcc armv5l-linux-musleabihf-gcc armv6-linux-musleabi-gcc armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc armv7m-linux-musleabi-gcc armv7r-linux-musleabihf-gcc)
# GOARMS=('' '' '' '' '' '' '5' '5' '6' '6' '7' '7' '7')
OS_ARCHES=(linux-musleabi-arm linux-musleabihf-arm linux-musleabi-armel linux-musleabihf-armel linux-musleabi-armv5l linux-musleabihf-armv5l linux-musleabi-armv6 linux-musleabihf-armv6 linux-musleabihf-armv7l linux-musleabi-armv7m linux-musleabihf-armv7r) OS_ARCHES=(linux-musleabi-arm linux-musleabihf-arm linux-musleabi-armel linux-musleabihf-armel linux-musleabi-armv5l linux-musleabihf-armv5l linux-musleabi-armv6 linux-musleabihf-armv6 linux-musleabihf-armv7l linux-musleabi-armv7m linux-musleabihf-armv7r)
CGO_ARGS=(arm-linux-musleabi-gcc arm-linux-musleabihf-gcc armel-linux-musleabi-gcc armel-linux-musleabihf-gcc armv5l-linux-musleabi-gcc armv5l-linux-musleabihf-gcc armv6-linux-musleabi-gcc armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc armv7m-linux-musleabi-gcc armv7r-linux-musleabihf-gcc) CGO_ARGS=(arm-linux-musleabi-gcc arm-linux-musleabihf-gcc armel-linux-musleabi-gcc armel-linux-musleabihf-gcc armv5l-linux-musleabi-gcc armv5l-linux-musleabihf-gcc armv6-linux-musleabi-gcc armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc armv7m-linux-musleabi-gcc armv7r-linux-musleabihf-gcc)
GOARMS=('' '' '' '' '5' '5' '6' '6' '7' '7' '7') GOARMS=('' '' '' '' '5' '5' '6' '6' '7' '7' '7')
@ -232,28 +249,76 @@ BuildReleaseAndroid() {
done done
} }
BuildReleaseFreeBSD() {
rm -rf .git/
mkdir -p "build/freebsd"
# Get latest FreeBSD 14.x release version from GitHub
freebsd_version=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue "https://api.github.com/repos/freebsd/freebsd-src/tags" | \
jq -r '.[].name' | \
grep '^release/14\.' | \
sort -V | \
tail -1 | \
sed 's/release\///' | \
sed 's/\.0$//')
if [ -z "$freebsd_version" ]; then
echo "Failed to get FreeBSD version, falling back to 14.3"
freebsd_version="14.3"
fi
echo "Using FreeBSD version: $freebsd_version"
OS_ARCHES=(amd64 arm64 i386)
GO_ARCHES=(amd64 arm64 386)
CGO_ARGS=(x86_64-unknown-freebsd${freebsd_version} aarch64-unknown-freebsd${freebsd_version} i386-unknown-freebsd${freebsd_version})
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}/${freebsd_version}-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() { MakeRelease() {
cd build cd build
if [ -d compress ]; then
rm -rv compress
fi
mkdir compress mkdir compress
for i in $(find . -type f -name "$appName-linux-*"); do for i in $(find . -type f -name "$appName-linux-*"); do
cp "$i" alist cp "$i" "$appName"
tar -czvf compress/"$i".tar.gz alist tar -czvf compress/"$i".tar.gz "$appName"
rm -f alist rm -f "$appName"
done done
for i in $(find . -type f -name "$appName-android-*"); do for i in $(find . -type f -name "$appName-android-*"); do
cp "$i" alist cp "$i" "$appName"
tar -czvf compress/"$i".tar.gz alist tar -czvf compress/"$i".tar.gz "$appName"
rm -f alist rm -f "$appName"
done done
for i in $(find . -type f -name "$appName-darwin-*"); do for i in $(find . -type f -name "$appName-darwin-*"); do
cp "$i" alist cp "$i" "$appName"
tar -czvf compress/"$i".tar.gz alist tar -czvf compress/"$i".tar.gz "$appName"
rm -f alist rm -f "$appName"
done
for i in $(find . -type f -name "$appName-freebsd-*"); do
cp "$i" "$appName"
tar -czvf compress/"$i".tar.gz "$appName"
rm -f "$appName"
done done
for i in $(find . -type f -name "$appName-windows-*"); do for i in $(find . -type f -name "$appName-windows-*"); do
cp "$i" alist.exe cp "$i" "$appName".exe
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip alist.exe zip compress/$(echo $i | sed 's/\.[^.]*$//').zip "$appName".exe
rm -f alist.exe rm -f "$appName".exe
done done
cd compress cd compress
find . -type f -print0 | xargs -0 md5sum >"$1" find . -type f -print0 | xargs -0 md5sum >"$1"
@ -272,8 +337,12 @@ if [ "$1" = "dev" ]; then
else else
BuildDev BuildDev
fi fi
elif [ "$1" = "release" ]; then elif [ "$1" = "release" -o "$1" = "beta" ]; then
FetchWebRelease if [ "$1" = "beta" ]; then
FetchWebDev
else
FetchWebRelease
fi
if [ "$2" = "docker" ]; then if [ "$2" = "docker" ]; then
BuildDocker BuildDocker
elif [ "$2" = "docker-multiplatform" ]; then elif [ "$2" = "docker-multiplatform" ]; then
@ -287,6 +356,9 @@ elif [ "$1" = "release" ]; then
elif [ "$2" = "android" ]; then elif [ "$2" = "android" ]; then
BuildReleaseAndroid BuildReleaseAndroid
MakeRelease "md5-android.txt" MakeRelease "md5-android.txt"
elif [ "$2" = "freebsd" ]; then
BuildReleaseFreeBSD
MakeRelease "md5-freebsd.txt"
elif [ "$2" = "web" ]; then elif [ "$2" = "web" ]; then
echo "web only" echo "web only"
else else

View File

@ -4,11 +4,11 @@ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
package cmd package cmd
import ( import (
"github.com/alist-org/alist/v3/internal/conf" "github.com/OpenListTeam/OpenList/internal/conf"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
"github.com/alist-org/alist/v3/internal/setting" "github.com/OpenListTeam/OpenList/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/alist-org/alist/v3/pkg/utils/random" "github.com/OpenListTeam/OpenList/pkg/utils/random"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -26,8 +26,8 @@ var AdminCmd = &cobra.Command{
} else { } else {
utils.Log.Infof("Admin user's username: %s", admin.Username) utils.Log.Infof("Admin user's username: %s", admin.Username)
utils.Log.Infof("The password can only be output at the first startup, and then stored as a hash value, which cannot be reversed") utils.Log.Infof("The password can only be output at the first startup, and then stored as a hash value, which cannot be reversed")
utils.Log.Infof("You can reset the password with a random string by running [alist admin random]") utils.Log.Infof("You can reset the password with a random string by running [openlist admin random]")
utils.Log.Infof("You can also set a new password by running [alist admin set NEW_PASSWORD]") utils.Log.Infof("You can also set a new password by running [openlist admin set NEW_PASSWORD]")
} }
}, },
} }

View File

@ -4,8 +4,8 @@ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
package cmd package cmd
import ( import (
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View File

@ -5,10 +5,10 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"github.com/alist-org/alist/v3/internal/bootstrap" "github.com/OpenListTeam/OpenList/internal/bootstrap"
"github.com/alist-org/alist/v3/internal/bootstrap/data" "github.com/OpenListTeam/OpenList/internal/bootstrap/data"
"github.com/alist-org/alist/v3/internal/db" "github.com/OpenListTeam/OpenList/internal/db"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -17,7 +17,9 @@ func Init() {
bootstrap.Log() bootstrap.Log()
bootstrap.InitDB() bootstrap.InitDB()
data.InitData() data.InitData()
bootstrap.InitStreamLimit()
bootstrap.InitIndex() bootstrap.InitIndex()
bootstrap.InitUpgradePatch()
} }
func Release() { func Release() {

55
cmd/kill.go Normal file
View File

@ -0,0 +1,55 @@
package cmd
import (
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// KillCmd represents the kill command
var KillCmd = &cobra.Command{
Use: "kill",
Short: "Force kill openlist 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 `openlist 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

@ -11,11 +11,12 @@ import (
"reflect" "reflect"
"strings" "strings"
_ "github.com/alist-org/alist/v3/drivers" _ "github.com/OpenListTeam/OpenList/drivers"
"github.com/alist-org/alist/v3/internal/bootstrap/data" "github.com/OpenListTeam/OpenList/internal/bootstrap"
"github.com/alist-org/alist/v3/internal/conf" "github.com/OpenListTeam/OpenList/internal/bootstrap/data"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/conf"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/internal/op"
"github.com/OpenListTeam/OpenList/pkg/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -24,6 +25,8 @@ type KV[V any] map[string]V
type Drivers KV[KV[interface{}]] type Drivers KV[KV[interface{}]]
var frontendPath string
func firstUpper(s string) string { func firstUpper(s string) string {
if s == "" { if s == "" {
return "" return ""
@ -38,7 +41,7 @@ func convert(s string) string {
} }
func writeFile(name string, data interface{}) { func writeFile(name string, data interface{}) {
f, err := os.Open(fmt.Sprintf("../alist-web/src/lang/en/%s.json", name)) f, err := os.Open(fmt.Sprintf("%s/src/lang/en/%s.json", frontendPath, name))
if err != nil { if err != nil {
log.Errorf("failed to open %s.json: %+v", name, err) log.Errorf("failed to open %s.json: %+v", name, err)
return return
@ -137,6 +140,8 @@ var LangCmd = &cobra.Command{
Use: "lang", Use: "lang",
Short: "Generate language json file", Short: "Generate language json file",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
frontendPath, _ = cmd.Flags().GetString("frontend-path")
bootstrap.InitConfig()
err := os.MkdirAll("lang", 0777) err := os.MkdirAll("lang", 0777)
if err != nil { if err != nil {
utils.Log.Fatalf("failed create folder: %s", err.Error()) utils.Log.Fatalf("failed create folder: %s", err.Error())
@ -149,6 +154,9 @@ var LangCmd = &cobra.Command{
func init() { func init() {
RootCmd.AddCommand(LangCmd) RootCmd.AddCommand(LangCmd)
// Add frontend-path flag
LangCmd.Flags().String("frontend-path", "../OpenList-Frontend", "Path to the frontend project directory")
// Here you will define your flags and configuration settings. // Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command // Cobra supports Persistent Flags which will work for this command

View File

@ -10,7 +10,7 @@ import (
// RestartCmd represents the restart command // RestartCmd represents the restart command
var RestartCmd = &cobra.Command{ var RestartCmd = &cobra.Command{
Use: "restart", Use: "restart",
Short: "Restart alist server by daemon/pid file", Short: "Restart openlist server by daemon/pid file",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
stop() stop()
start() start()

View File

@ -4,18 +4,19 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/alist-org/alist/v3/cmd/flags" "github.com/OpenListTeam/OpenList/cmd/flags"
_ "github.com/alist-org/alist/v3/drivers" _ "github.com/OpenListTeam/OpenList/drivers"
_ "github.com/alist-org/alist/v3/internal/offline_download" _ "github.com/OpenListTeam/OpenList/internal/archive"
_ "github.com/OpenListTeam/OpenList/internal/offline_download"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var RootCmd = &cobra.Command{ var RootCmd = &cobra.Command{
Use: "alist", Use: "openlist",
Short: "A file list program that supports multiple storage.", Short: "A file list program that supports multiple storage.",
Long: `A file list program that supports multiple storage, Long: `A file list program that supports multiple storage,
built with love by Xhofe and friends in Go/Solid.js. built with love by Xhofe and friends in Go/Solid.js.
Complete documentation is available at https://alist.nn.ci/`, Complete documentation is available at https://docs.openlist.team/`,
} }
func Execute() { func Execute() {

View File

@ -13,14 +13,19 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/alist-org/alist/v3/cmd/flags" "github.com/OpenListTeam/OpenList/cmd/flags"
"github.com/alist-org/alist/v3/internal/bootstrap" "github.com/OpenListTeam/OpenList/internal/bootstrap"
"github.com/alist-org/alist/v3/internal/conf" "github.com/OpenListTeam/OpenList/internal/conf"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/internal/fs"
"github.com/alist-org/alist/v3/server" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/server"
"github.com/OpenListTeam/sftpd-openlist"
ftpserver "github.com/fclairamb/ftpserverlib"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
) )
// ServerCmd represents the server command // ServerCmd represents the server command
@ -44,11 +49,15 @@ the address is defined in config file`,
r := gin.New() r := gin.New()
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out)) r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.Init(r) server.Init(r)
var httpHandler http.Handler = r
if conf.Conf.Scheme.EnableH2c {
httpHandler = h2c.NewHandler(r, &http2.Server{})
}
var httpSrv, httpsSrv, unixSrv *http.Server var httpSrv, httpsSrv, unixSrv *http.Server
if conf.Conf.Scheme.HttpPort != -1 { if conf.Conf.Scheme.HttpPort != -1 {
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort) httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
utils.Log.Infof("start HTTP server @ %s", httpBase) utils.Log.Infof("start HTTP server @ %s", httpBase)
httpSrv = &http.Server{Addr: httpBase, Handler: r} httpSrv = &http.Server{Addr: httpBase, Handler: httpHandler}
go func() { go func() {
err := httpSrv.ListenAndServe() err := httpSrv.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) { if err != nil && !errors.Is(err, http.ErrServerClosed) {
@ -69,7 +78,7 @@ the address is defined in config file`,
} }
if conf.Conf.Scheme.UnixFile != "" { if conf.Conf.Scheme.UnixFile != "" {
utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile) utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile)
unixSrv = &http.Server{Handler: r} unixSrv = &http.Server{Handler: httpHandler}
go func() { go func() {
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile) listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
if err != nil { if err != nil {
@ -112,6 +121,42 @@ the address is defined in config file`,
} }
}() }()
} }
var ftpDriver *server.FtpMainDriver
var ftpServer *ftpserver.FtpServer
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable {
var err error
ftpDriver, err = server.NewMainDriver()
if err != nil {
utils.Log.Fatalf("failed to start ftp driver: %s", err.Error())
} else {
utils.Log.Infof("start ftp server on %s", conf.Conf.FTP.Listen)
go func() {
ftpServer = ftpserver.NewFtpServer(ftpDriver)
err = ftpServer.ListenAndServe()
if err != nil {
utils.Log.Fatalf("problem ftp server listening: %s", err.Error())
}
}()
}
}
var sftpDriver *server.SftpDriver
var sftpServer *sftpd.SftpServer
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable {
var err error
sftpDriver, err = server.NewSftpDriver()
if err != nil {
utils.Log.Fatalf("failed to start sftp driver: %s", err.Error())
} else {
utils.Log.Infof("start sftp server on %s", conf.Conf.SFTP.Listen)
go func() {
sftpServer = sftpd.NewSftpServer(sftpDriver)
err = sftpServer.RunServer()
if err != nil {
utils.Log.Fatalf("problem sftp server listening: %s", err.Error())
}
}()
}
}
// Wait for interrupt signal to gracefully shutdown the server with // Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 1 second. // a timeout of 1 second.
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
@ -121,6 +166,7 @@ the address is defined in config file`,
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
utils.Log.Println("Shutdown server...") utils.Log.Println("Shutdown server...")
fs.ArchiveContentUploadTaskManager.RemoveAll()
Release() Release()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
@ -152,6 +198,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() wg.Wait()
utils.Log.Println("Server exit") utils.Log.Println("Server exit")
}, },
@ -171,8 +236,8 @@ func init() {
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") // serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
} }
// OutAlistInit 暴露用于外部启动server的函数 // OutOpenListInit 暴露用于外部启动server的函数
func OutAlistInit() { func OutOpenListInit() {
var ( var (
cmd *cobra.Command cmd *cobra.Command
args []string args []string

View File

@ -16,7 +16,7 @@ import (
// StartCmd represents the start command // StartCmd represents the start command
var StartCmd = &cobra.Command{ var StartCmd = &cobra.Command{
Use: "start", Use: "start",
Short: "Silent start alist server with `--force-bin-dir`", Short: "Silent start openlist server with `--force-bin-dir`",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
start() start()
}, },
@ -27,7 +27,7 @@ func start() {
if pid != -1 { if pid != -1 {
_, err := os.FindProcess(pid) _, err := os.FindProcess(pid)
if err == nil { if err == nil {
log.Info("alist already started, pid ", pid) log.Info("openlist already started, pid ", pid)
return return
} }
} }
@ -52,7 +52,7 @@ func start() {
log.Infof("success start pid: %d", cmd.Process.Pid) log.Infof("success start pid: %d", cmd.Process.Pid)
err = os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0666) err = os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0666)
if err != nil { if err != nil {
log.Warn("failed to record pid, you may not be able to stop the program with `./alist stop`") log.Warn("failed to record pid, you may not be able to stop the program with `./openlist stop`")
} }
} }

View File

@ -1,10 +1,10 @@
/* //go:build !windows
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd package cmd
import ( import (
"os" "os"
"syscall"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -13,7 +13,7 @@ import (
// StopCmd represents the stop command // StopCmd represents the stop command
var StopCmd = &cobra.Command{ var StopCmd = &cobra.Command{
Use: "stop", Use: "stop",
Short: "Stop alist server by daemon/pid file", Short: "Stop openlist server by daemon/pid file",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
stop() stop()
}, },
@ -22,7 +22,7 @@ var StopCmd = &cobra.Command{
func stop() { func stop() {
initDaemon() initDaemon()
if pid == -1 { if pid == -1 {
log.Info("Seems not have been started. Try use `alist start` to start server.") log.Info("Seems not have been started. Try use `openlist start` to start server.")
return return
} }
process, err := os.FindProcess(pid) process, err := os.FindProcess(pid)
@ -30,11 +30,11 @@ func stop() {
log.Errorf("failed to find process by pid: %d, reason: %v", pid, process) log.Errorf("failed to find process by pid: %d, reason: %v", pid, process)
return return
} }
err = process.Kill() err = process.Signal(syscall.SIGTERM)
if err != nil { if err != nil {
log.Errorf("failed to kill process %d: %v", pid, err) log.Errorf("failed to terminate process %d: %v", pid, err)
} else { } else {
log.Info("killed process: ", pid) log.Info("terminated process: ", pid)
} }
err = os.Remove(pidFile) err = os.Remove(pidFile)
if err != nil { 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

@ -7,8 +7,8 @@ import (
"os" "os"
"strconv" "strconv"
"github.com/alist-org/alist/v3/internal/db" "github.com/OpenListTeam/OpenList/internal/db"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"

View File

@ -5,10 +5,10 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/alist-org/alist/v3/internal/conf" "github.com/OpenListTeam/OpenList/internal/conf"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
"github.com/alist-org/alist/v3/internal/setting" "github.com/OpenListTeam/OpenList/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
) )

View File

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

View File

@ -1,9 +1,8 @@
version: '3.3'
services: services:
alist: openlist:
restart: always restart: always
volumes: volumes:
- '/etc/alist:/opt/alist/data' - '/etc/openlist:/opt/openlist/data'
ports: ports:
- '5244:5244' - '5244:5244'
- '5245:5245' - '5245:5245'
@ -12,5 +11,5 @@ services:
- PGID=0 - PGID=0
- UMASK=022 - UMASK=022
- TZ=UTC - TZ=UTC
container_name: alist container_name: openlist
image: 'xhofe/alist:latest' image: 'openlistteam/openlist:latest'

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

@ -0,0 +1,43 @@
package _115
import (
"github.com/OpenListTeam/OpenList/drivers/base"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
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

@ -3,12 +3,13 @@ package _115
import ( import (
"context" "context"
"strings" "strings"
"sync"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/http_range"
"github.com/OpenListTeam/OpenList/pkg/utils"
driver115 "github.com/SheltonZhu/115driver/pkg/driver" driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
@ -16,8 +17,9 @@ import (
type Pan115 struct { type Pan115 struct {
model.Storage model.Storage
Addition Addition
client *driver115.Pan115Client client *driver115.Pan115Client
limiter *rate.Limiter limiter *rate.Limiter
appVerOnce sync.Once
} }
func (d *Pan115) Config() driver.Config { func (d *Pan115) Config() driver.Config {
@ -29,6 +31,7 @@ func (d *Pan115) GetAddition() driver.Additional {
} }
func (d *Pan115) Init(ctx context.Context) error { func (d *Pan115) Init(ctx context.Context) error {
d.appVerOnce.Do(d.initAppVer)
if d.LimitRate > 0 { if d.LimitRate > 0 {
d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1) d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
} }
@ -76,28 +79,60 @@ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
return link, nil return link, nil
} }
func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil { if err := d.WaitLimit(ctx); err != nil {
return err return nil, err
} }
if _, err := d.client.Mkdir(parentDir.GetID(), dirName); err != nil {
return err result := driver115.MkdirResp{}
form := map[string]string{
"pid": parentDir.GetID(),
"cname": dirName,
} }
return nil req := d.client.NewRequest().
SetFormData(form).
SetResult(&result).
ForceContentType("application/json;charset=UTF-8")
resp, err := req.Post(driver115.ApiDirAdd)
err = driver115.CheckErr(err, &result, resp)
if err != nil {
return nil, err
}
f, err := d.getNewFile(result.FileID)
if err != nil {
return nil, nil
}
return f, nil
} }
func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil { if err := d.WaitLimit(ctx); err != nil {
return err return nil, err
} }
return d.client.Move(dstDir.GetID(), srcObj.GetID()) if err := d.client.Move(dstDir.GetID(), srcObj.GetID()); err != nil {
return nil, err
}
f, err := d.getNewFile(srcObj.GetID())
if err != nil {
return nil, nil
}
return f, nil
} }
func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) error { func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil { if err := d.WaitLimit(ctx); err != nil {
return err return nil, err
} }
return d.client.Rename(srcObj.GetID(), newName) if err := d.client.Rename(srcObj.GetID(), newName); err != nil {
return nil, err
}
f, err := d.getNewFile((srcObj.GetID()))
if err != nil {
return nil, nil
}
return f, nil
} }
func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
@ -114,9 +149,9 @@ func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {
return d.client.Delete(obj.GetID()) 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 { if err := d.WaitLimit(ctx); err != nil {
return err return nil, err
} }
var ( var (
@ -125,10 +160,10 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
) )
if ok, err := d.client.UploadAvailable(); err != nil || !ok { if ok, err := d.client.UploadAvailable(); err != nil || !ok {
return err return nil, err
} }
if stream.GetSize() > d.client.UploadMetaInfo.SizeLimit { if stream.GetSize() > d.client.UploadMetaInfo.SizeLimit {
return driver115.ErrUploadTooLarge return nil, driver115.ErrUploadTooLarge
} }
//if digest, err = d.client.GetDigestResult(stream); err != nil { //if digest, err = d.client.GetDigestResult(stream); err != nil {
// return err // return err
@ -141,22 +176,22 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
} }
reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: hashSize}) reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: hashSize})
if err != nil { if err != nil {
return err return nil, err
} }
preHash, err := utils.HashReader(utils.SHA1, reader) preHash, err := utils.HashReader(utils.SHA1, reader)
if err != nil { if err != nil {
return err return nil, err
} }
preHash = strings.ToUpper(preHash) preHash = strings.ToUpper(preHash)
fullHash := stream.GetHash().GetHash(utils.SHA1) fullHash := stream.GetHash().GetHash(utils.SHA1)
if len(fullHash) <= 0 { if len(fullHash) <= 0 {
tmpF, err := stream.CacheFullInTempFile() tmpF, err := stream.CacheFullInTempFile()
if err != nil { if err != nil {
return err return nil, err
} }
fullHash, err = utils.HashFile(utils.SHA1, tmpF) fullHash, err = utils.HashFile(utils.SHA1, tmpF)
if err != nil { if err != nil {
return err return nil, err
} }
} }
fullHash = strings.ToUpper(fullHash) fullHash = strings.ToUpper(fullHash)
@ -165,20 +200,36 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
// note that 115 add timeout for rapid-upload, // note that 115 add timeout for rapid-upload,
// and "sig invalid" err is thrown even when the hash is correct after timeout. // 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 { 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 { if matched, err := fastInfo.Ok(); err != nil {
return err return nil, err
} else if matched { } 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改用普通模式上传 if stream.GetSize() <= 10*utils.MB { // 文件大小小于10MB改用普通模式上传
return d.client.UploadByOSS(&fastInfo.UploadOSSParams, stream, dirID) 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) { func (d *Pan115) OfflineList(ctx context.Context) ([]*driver115.OfflineTask, error) {
@ -190,7 +241,7 @@ func (d *Pan115) OfflineList(ctx context.Context) ([]*driver115.OfflineTask, err
} }
func (d *Pan115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) { func (d *Pan115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {
return d.client.AddOfflineTaskURIs(uris, dstDir.GetID()) return d.client.AddOfflineTaskURIs(uris, dstDir.GetID(), driver115.WithAppVer(appVer))
} }
func (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, deleteFiles bool) error { func (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, deleteFiles bool) error {

View File

@ -1,16 +1,16 @@
package _115 package _115
import ( import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
) )
type Addition struct { type Addition struct {
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"` Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"` QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
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"` 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:"56" help:"list api per page size of 115 driver"` PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"` LimitRate float64 `json:"limit_rate" type:"float" default:"2" help:"limit all api request rate ([limit]r/1s)"`
driver.RootID driver.RootID
} }

View File

@ -1,10 +1,11 @@
package _115 package _115
import ( import (
"github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"time" "time"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/SheltonZhu/115driver/pkg/driver"
) )
var _ model.Obj = (*FileObj)(nil) var _ model.Obj = (*FileObj)(nil)
@ -20,3 +21,18 @@ func (f *FileObj) CreateTime() time.Time {
func (f *FileObj) GetHash() utils.HashInfo { func (f *FileObj) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.SHA1, f.Sha1) 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,36 +2,39 @@ package _115
import ( import (
"bytes" "bytes"
"context"
"crypto/md5"
"crypto/tls" "crypto/tls"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/alist-org/alist/v3/internal/conf" "github.com/OpenListTeam/OpenList/internal/conf"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/pkg/http_range" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/http_range"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/aliyun/aliyun-oss-go-sdk/oss"
cipher "github.com/SheltonZhu/115driver/pkg/crypto/ec115"
crypto "github.com/SheltonZhu/115driver/pkg/crypto/m115"
driver115 "github.com/SheltonZhu/115driver/pkg/driver" driver115 "github.com/SheltonZhu/115driver/pkg/driver"
crypto "github.com/gaoyb7/115drive-webdav/115"
"github.com/orzogc/fake115uploader/cipher"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
var UserAgent = driver115.UA115Desktop // var UserAgent = driver115.UA115Browser
func (d *Pan115) login() error { func (d *Pan115) login() error {
var err error var err error
opts := []driver115.Option{ opts := []driver115.Option{
driver115.UA(UserAgent), driver115.UA(d.getUA()),
func(c *driver115.Pan115Client) { func(c *driver115.Pan115Client) {
c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify}) c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
}, },
@ -45,7 +48,7 @@ func (d *Pan115) login() error {
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil { if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
return errors.Wrap(err, "failed to login by qrcode") return errors.Wrap(err, "failed to login by qrcode")
} }
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID) d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
d.QRCodeToken = "" d.QRCodeToken = ""
} else if d.Cookie != "" { } else if d.Cookie != "" {
if err = cr.FromCookie(d.Cookie); err != nil { if err = cr.FromCookie(d.Cookie); err != nil {
@ -63,7 +66,7 @@ func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
if d.PageSize <= 0 { if d.PageSize <= 0 {
d.PageSize = driver115.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 { if err != nil {
return nil, err return nil, err
} }
@ -73,14 +76,42 @@ func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
return res, nil return res, nil
} }
const ( func (d *Pan115) getNewFile(fileId string) (*FileObj, error) {
appVer = "2.0.3.6" file, err := d.client.GetFile(fileId)
) if err != nil {
return nil, err
}
return &FileObj{*file}, nil
}
func (c *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) { func (d *Pan115) getNewFileByPickCode(pickCode string) (*FileObj, error) {
result := driver115.GetFileInfoResponse{}
req := d.client.NewRequest().
SetQueryParam("pick_code", pickCode).
ForceContentType("application/json;charset=UTF-8").
SetResult(&result)
resp, err := req.Get(driver115.ApiFileInfo)
if err := driver115.CheckErr(err, &result, resp); err != nil {
return nil, err
}
if len(result.Files) == 0 {
return nil, errors.New("not get file info")
}
fileInfo := result.Files[0]
f := &FileObj{}
f.From(fileInfo)
return f, nil
}
func (d *Pan115) getUA() string {
return fmt.Sprintf("Mozilla/5.0 115Browser/%s", appVer)
}
func (d *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
key := crypto.GenerateKey() key := crypto.GenerateKey()
result := driver115.DownloadResp{} result := driver115.DownloadResp{}
params, err := utils.Json.Marshal(map[string]string{"pickcode": pickCode}) params, err := utils.Json.Marshal(map[string]string{"pick_code": pickCode})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -88,13 +119,13 @@ func (c *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, e
data := crypto.Encode(params, key) data := crypto.Encode(params, key)
bodyReader := strings.NewReader(url.Values{"data": []string{data}}.Encode()) bodyReader := strings.NewReader(url.Values{"data": []string{data}}.Encode())
reqUrl := fmt.Sprintf("%s?t=%s", driver115.ApiDownloadGetUrl, driver115.Now().String()) reqUrl := fmt.Sprintf("%s?t=%s", driver115.AndroidApiDownloadGetUrl, driver115.Now().String())
req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader) req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cookie", c.Cookie) req.Header.Set("Cookie", d.Cookie)
req.Header.Set("User-Agent", ua) req.Header.Set("User-Agent", ua)
resp, err := c.client.Client.GetClient().Do(req) resp, err := d.client.Client.GetClient().Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -112,24 +143,30 @@ func (c *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, e
return nil, err return nil, err
} }
bytes, err := crypto.Decode(string(result.EncodedData), key) b, err := crypto.Decode(string(result.EncodedData), key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
downloadInfo := driver115.DownloadData{} downloadInfo := struct {
if err := utils.Json.Unmarshal(bytes, &downloadInfo); err != nil { Url string `json:"url"`
}{}
if err := utils.Json.Unmarshal(b, &downloadInfo); err != nil {
return nil, err return nil, err
} }
for _, info := range downloadInfo { info := &driver115.DownloadInfo{}
if info.FileSize < 0 { info.PickCode = pickCode
return nil, driver115.ErrDownloadEmpty info.Header = resp.Request.Header
} info.Url.Url = downloadInfo.Url
info.Header = resp.Request.Header return info, nil
return info, nil }
}
return nil, driver115.ErrUnexpected func (c *Pan115) GenerateToken(fileID, preID, timeStamp, fileSize, signKey, signVal string) string {
userID := strconv.FormatInt(c.client.UserID, 10)
userIDMd5 := md5.Sum([]byte(userID))
tokenMd5 := md5.Sum([]byte(md5Salt + fileID + fileSize + signKey + signVal + userID + timeStamp + hex.EncodeToString(userIDMd5[:]) + appVer))
return hex.EncodeToString(tokenMd5[:])
} }
func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) { func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) {
@ -161,7 +198,7 @@ func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID stri
signKey, signVal := "", "" signKey, signVal := "", ""
for retry := true; retry; { for retry := true; retry; {
t := driver115.Now() t := driver115.NowMilli()
if encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil { if encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil {
return nil, err return nil, err
@ -172,7 +209,7 @@ func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID stri
} }
form.Set("t", t.String()) 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 != "" { if signKey != "" && signVal != "" {
form.Set("sign_key", signKey) form.Set("sign_key", signKey)
form.Set("sign_val", signVal) form.Set("sign_val", signVal)
@ -225,6 +262,9 @@ func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result stri
length := end - start + 1 length := end - start + 1
reader, err := stream.RangeRead(http_range.Range{Start: start, Length: length}) reader, err := stream.RangeRead(http_range.Range{Start: start, Length: length})
if err != nil {
return "", err
}
hashStr, err := utils.HashReader(utils.SHA1, reader) hashStr, err := utils.HashReader(utils.SHA1, reader)
if err != nil { if err != nil {
return "", err return "", err
@ -233,8 +273,43 @@ func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result stri
return 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 // 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 ( var (
chunks []oss.FileChunk chunks []oss.FileChunk
parts []oss.UploadPart parts []oss.UploadPart
@ -242,12 +317,13 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
ossClient *oss.Client ossClient *oss.Client
bucket *oss.Bucket bucket *oss.Bucket
ossToken *driver115.UploadOSSTokenResp ossToken *driver115.UploadOSSTokenResp
bodyBytes []byte
err error err error
) )
tmpF, err := stream.CacheFullInTempFile() tmpF, err := s.CacheFullInTempFile()
if err != nil { if err != nil {
return err return nil, err
} }
options := driver115.DefalutUploadMultipartOptions() options := driver115.DefalutUploadMultipartOptions()
@ -256,17 +332,19 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
f(options) f(options)
} }
} }
// oss 启用Sequential必须按顺序上传
options.ThreadsNum = 1
if ossToken, err = d.client.GetOSSToken(); err != nil { 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 { if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
return err return nil, err
} }
if bucket, err = ossClient.Bucket(params.Bucket); err != nil { if bucket, err = ossClient.Bucket(params.Bucket); err != nil {
return err return nil, err
} }
// ossToken一小时后就会失效所以每50分钟重新获取一次 // ossToken一小时后就会失效所以每50分钟重新获取一次
@ -276,14 +354,15 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
timeout := time.NewTimer(options.Timeout) timeout := time.NewTimer(options.Timeout)
if chunks, err = SplitFile(fileSize); err != nil { if chunks, err = SplitFile(fileSize); err != nil {
return err return nil, err
} }
if imur, err = bucket.InitiateMultipartUpload(params.Object, if imur, err = bucket.InitiateMultipartUpload(params.Object,
oss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken), oss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken),
oss.UserAgentHeader(driver115.OSSUserAgent), oss.UserAgentHeader(driver115.OSSUserAgent),
oss.EnableSha1(), oss.Sequential(),
); err != nil { ); err != nil {
return err return nil, err
} }
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
@ -301,6 +380,7 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
quit <- struct{}{} quit <- struct{}{}
}() }()
completedNum := atomic.Int32{}
// consumers // consumers
for i := 0; i < options.ThreadsNum; i++ { for i := 0; i < options.ThreadsNum; i++ {
go func(threadId int) { go func(threadId int) {
@ -313,25 +393,28 @@ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize i
var part oss.UploadPart // 出现错误就继续尝试共尝试3次 var part oss.UploadPart // 出现错误就继续尝试共尝试3次
for retry := 0; retry < 3; retry++ { for retry := 0; retry < 3; retry++ {
select { select {
case <-ctx.Done():
break
case <-ticker.C: case <-ticker.C:
if ossToken, err = d.client.GetOSSToken(); err != nil { // 到时重新获取ossToken if ossToken, err = d.client.GetOSSToken(); err != nil { // 到时重新获取ossToken
errCh <- errors.Wrap(err, "刷新token时出现错误") errCh <- errors.Wrap(err, "刷新token时出现错误")
} }
default: default:
} }
buf := make([]byte, chunk.Size) buf := make([]byte, chunk.Size)
if _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) { if _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) {
continue continue
} }
if part, err = bucket.UploadPart(imur, driver.NewLimitedUploadStream(ctx, bytes.NewReader(buf)),
b := bytes.NewBuffer(buf) chunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {
if part, err = bucket.UploadPart(imur, b, chunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {
break break
} }
} }
if err != nil { 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 UploadedPartsCh <- part
} }
@ -350,25 +433,31 @@ LOOP:
case <-ticker.C: case <-ticker.C:
// 到时重新获取ossToken // 到时重新获取ossToken
if ossToken, err = d.client.GetOSSToken(); err != nil { if ossToken, err = d.client.GetOSSToken(); err != nil {
return err return nil, err
} }
case <-quit: case <-quit:
break LOOP break LOOP
case <-errCh: case <-errCh:
return err return nil, err
case <-timeout.C: case <-timeout.C:
return fmt.Errorf("time out") return nil, fmt.Errorf("time out")
} }
} }
// EOF错误是xml的Unmarshal导致的响应其实是json格式所以实际上上传是成功的 // 不知道啥原因oss那边分片上传不计算sha1导致115服务器校验错误
if _, err = bucket.CompleteMultipartUpload(imur, parts, driver115.OssOption(params, ossToken)...); err != nil && !errors.Is(err, io.EOF) { // params.Callback.Callback = strings.ReplaceAll(params.Callback.Callback, "${sha1}", params.SHA1)
// 当文件名含有 &< 这两个字符之一时响应的xml解析会出现错误实际上上传是成功的 if _, err := bucket.CompleteMultipartUpload(imur, parts, append(
if filename := filepath.Base(stream.GetName()); !strings.ContainsAny(filename, "&<") { driver115.OssOption(params, ossToken),
return err 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) { func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
@ -377,27 +466,6 @@ func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
} }
} }
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) { func SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {
for i := int64(1); i < 10; i++ { for i := int64(1); i < 10; i++ {
if fileSize < i*utils.GB { // 文件大小小于iGB时分为i*1000片 if fileSize < i*utils.GB { // 文件大小小于iGB时分为i*1000片

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

@ -0,0 +1,335 @@
package _115_open
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/OpenListTeam/OpenList/cmd/flags"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/OpenListTeam/OpenList/pkg/utils"
sdk "github.com/xhofe/115-sdk-go"
"golang.org/x/time/rate"
)
type Open115 struct {
model.Storage
Addition
client *sdk.Client
limiter *rate.Limiter
}
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
}
if d.Addition.LimitRate > 0 {
d.limiter = rate.NewLimiter(rate.Limit(d.Addition.LimitRate), 1)
}
return nil
}
func (d *Open115) WaitLimit(ctx context.Context) error {
if d.limiter != nil {
return d.limiter.Wait(ctx)
}
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 {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
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) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
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) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
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) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
_, 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) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
_, 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) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
_, 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 {
if err := d.WaitLimit(ctx); err != nil {
return err
}
_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 {
if err := d.WaitLimit(ctx); err != nil {
return err
}
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)

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

@ -0,0 +1,37 @@
package _115_open
import (
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/op"
)
type Addition struct {
// Usually one of two
driver.RootID
// define other
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"`
LimitRate float64 `json:"limit_rate,string" type:"float" default:"1" help:"limit all api request rate ([limit]r/1s)"`
AccessToken string `json:"access_token" required:"true"`
RefreshToken string `json:"refresh_token" required:"true"`
}
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/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/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/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/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

View File

@ -3,11 +3,11 @@ package _115_share
import ( import (
"context" "context"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
driver115 "github.com/SheltonZhu/115driver/pkg/driver" driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )

View File

@ -1,16 +1,16 @@
package _115_share package _115_share
import ( import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
) )
type Addition struct { type Addition struct {
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"` Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"` QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
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"` 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:"20" help:"list api per page size of 115 driver"` PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"` 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"` 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"` ReceiveCode string `json:"receive_code" type:"text" required:"true" help:"receive code of 115 share link"`
driver.RootID driver.RootID
@ -18,7 +18,7 @@ type Addition struct {
var config = driver.Config{ var config = driver.Config{
Name: "115 Share", Name: "115 Share",
DefaultRoot: "", DefaultRoot: "0",
// OnlyProxy: true, // OnlyProxy: true,
// OnlyLocal: true, // OnlyLocal: true,
CheckStatus: false, CheckStatus: false,

View File

@ -5,9 +5,9 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
driver115 "github.com/SheltonZhu/115driver/pkg/driver" 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" "github.com/pkg/errors"
) )
@ -96,7 +96,7 @@ func (d *Pan115Share) login() error {
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil { if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
return errors.Wrap(err, "failed to login by qrcode") return errors.Wrap(err, "failed to login by qrcode")
} }
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID) d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
d.QRCodeToken = "" d.QRCodeToken = ""
} else if d.Cookie != "" { } else if d.Cookie != "" {
if err = cr.FromCookie(d.Cookie); err != nil { if err = cr.FromCookie(d.Cookie); err != nil {

View File

@ -2,22 +2,21 @@ package _123
import ( import (
"context" "context"
"crypto/md5"
"encoding/base64" "encoding/base64"
"encoding/hex"
"fmt" "fmt"
"golang.org/x/time/rate"
"io"
"net/http" "net/http"
"net/url" "net/url"
"sync" "sync"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "golang.org/x/time/rate"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/internal/stream"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
@ -41,12 +40,12 @@ func (d *Pan123) GetAddition() driver.Additional {
} }
func (d *Pan123) Init(ctx context.Context) error { 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 return err
} }
func (d *Pan123) Drop(ctx context.Context) error { 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{}) req.SetBody(base.Json{})
}, nil) }, nil)
return nil return nil
@ -81,7 +80,8 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
"size": f.Size, "size": f.Size,
"type": f.Type, "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) req.SetBody(data).SetHeaders(headers)
}, nil) }, nil)
if err != nil { if err != nil {
@ -134,7 +134,7 @@ func (d *Pan123) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
"size": 0, "size": 0,
"type": 1, "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) req.SetBody(data)
}, nil) }, nil)
return err return err
@ -145,7 +145,7 @@ func (d *Pan123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
"fileIdList": []base.Json{{"FileId": srcObj.GetID()}}, "fileIdList": []base.Json{{"FileId": srcObj.GetID()}},
"parentFileId": dstDir.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) req.SetBody(data)
}, nil) }, nil)
return err return err
@ -157,7 +157,7 @@ func (d *Pan123) Rename(ctx context.Context, srcObj model.Obj, newName string) e
"fileId": srcObj.GetID(), "fileId": srcObj.GetID(),
"fileName": newName, "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) req.SetBody(data)
}, nil) }, nil)
return err return err
@ -174,7 +174,7 @@ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
"operation": true, "operation": true,
"fileTrashInfoList": []File{f}, "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) req.SetBody(data)
}, nil) }, nil)
return err return err
@ -183,36 +183,26 @@ 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 { func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
// const DEFAULT int64 = 10485760 etag := file.GetHash().GetHash(utils.MD5)
h := md5.New() var err error
// need to calculate md5 of the full content if len(etag) < utils.MD5.Width {
tempFile, err := stream.CacheFullInTempFile() _, etag, err = stream.CacheFullInTempFileAndHash(file, utils.MD5)
if err != nil { if err != nil {
return err 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))
data := base.Json{ data := base.Json{
"driveId": 0, "driveId": 0,
"duplicate": 2, // 2->覆盖 1->重命名 0->默认 "duplicate": 2, // 2->覆盖 1->重命名 0->默认
"etag": etag, "etag": etag,
"fileName": stream.GetName(), "fileName": file.GetName(),
"parentFileId": dstDir.GetID(), "parentFileId": dstDir.GetID(),
"size": stream.GetSize(), "size": file.GetSize(),
"type": 0, "type": 0,
} }
var resp UploadResp 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) req.SetBody(data).SetContext(ctx)
}, &resp) }, &resp)
if err != nil { if err != nil {
@ -223,7 +213,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return nil return nil
} }
if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" { 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 return err
} else { } else {
cfg := &aws.Config{ cfg := &aws.Config{
@ -237,17 +227,23 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err return err
} }
uploader := s3manager.NewUploader(s) uploader := s3manager.NewUploader(s)
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize { if file.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1) uploader.PartSize = file.GetSize() / (s3manager.MaxUploadParts - 1)
} }
input := &s3manager.UploadInput{ input := &s3manager.UploadInput{
Bucket: &resp.Data.Bucket, Bucket: &resp.Data.Bucket,
Key: &resp.Data.Key, Key: &resp.Data.Key,
Body: tempFile, Body: driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
Reader: file,
UpdateProgress: up,
}),
} }
_, err = uploader.UploadWithContext(ctx, input) _, err = uploader.UploadWithContext(ctx, input)
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{ req.SetBody(base.Json{
"fileId": resp.Data.FileId, "fileId": resp.Data.FileId,
}).SetContext(ctx) }).SetContext(ctx)

View File

@ -1,8 +1,8 @@
package _123 package _123
import ( import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
) )
type Addition struct { type Addition struct {

View File

@ -1,14 +1,15 @@
package _123 package _123
import ( import (
"github.com/alist-org/alist/v3/pkg/utils"
"net/url" "net/url"
"path" "path"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/model"
) )
type File struct { type File struct {

View File

@ -4,14 +4,13 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"math"
"net/http" "net/http"
"strconv" "strconv"
"github.com/alist-org/alist/v3/drivers/base" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
) )
@ -25,7 +24,7 @@ func (d *Pan123) getS3PreSignedUrls(ctx context.Context, upReq *UploadResp, star
"StorageNode": upReq.Data.StorageNode, "StorageNode": upReq.Data.StorageNode,
} }
var s3PreSignedUrls S3PreSignedURLs 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) req.SetBody(data).SetContext(ctx)
}, &s3PreSignedUrls) }, &s3PreSignedUrls)
if err != nil { if err != nil {
@ -44,7 +43,7 @@ func (d *Pan123) getS3Auth(ctx context.Context, upReq *UploadResp, start, end in
"uploadId": upReq.Data.UploadId, "uploadId": upReq.Data.UploadId,
} }
var s3PreSignedUrls S3PreSignedURLs 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) req.SetBody(data).SetContext(ctx)
}, &s3PreSignedUrls) }, &s3PreSignedUrls)
if err != nil { if err != nil {
@ -63,21 +62,31 @@ func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp, file model.F
"key": upReq.Data.Key, "key": upReq.Data.Key,
"uploadId": upReq.Data.UploadId, "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) req.SetBody(data).SetContext(ctx)
}, nil) }, nil)
return err 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) tmpF, err := file.CacheFullInTempFile()
if err != nil {
return err
}
// fetch s3 pre signed urls // fetch s3 pre signed urls
chunkCount := int(math.Ceil(float64(file.GetSize()) / float64(chunkSize))) size := file.GetSize()
chunkSize := min(size, 16*utils.MB)
chunkCount := int(size / chunkSize)
lastChunkSize := size % chunkSize
if lastChunkSize > 0 {
chunkCount++
} else {
lastChunkSize = chunkSize
}
// only 1 batch is allowed // only 1 batch is allowed
isMultipart := chunkCount > 1
batchSize := 1 batchSize := 1
getS3UploadUrl := d.getS3Auth getS3UploadUrl := d.getS3Auth
if isMultipart { if chunkCount > 1 {
batchSize = 10 batchSize = 10
getS3UploadUrl = d.getS3PreSignedUrls getS3UploadUrl = d.getS3PreSignedUrls
} }
@ -86,10 +95,7 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
return ctx.Err() return ctx.Err()
} }
start := i start := i
end := i + batchSize end := min(i+batchSize, chunkCount+1)
if end > chunkCount+1 {
end = chunkCount + 1
}
s3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, start, end) s3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, start, end)
if err != nil { if err != nil {
return err return err
@ -101,9 +107,9 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
} }
curSize := chunkSize curSize := chunkSize
if j == chunkCount { if j == chunkCount {
curSize = file.GetSize() - (int64(chunkCount)-1)*chunkSize curSize = lastChunkSize
} }
err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.LimitReader(reader, chunkSize), curSize, false, getS3UploadUrl) err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.NewSectionReader(tmpF, chunkSize*int64(j-1), curSize), curSize, false, getS3UploadUrl)
if err != nil { if err != nil {
return err return err
} }
@ -114,12 +120,12 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
return d.completeS3(ctx, upReq, file, chunkCount > 1) return d.completeS3(ctx, upReq, file, chunkCount > 1)
} }
func (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSignedUrls *S3PreSignedURLs, cur, end int, reader io.Reader, curSize int64, retry bool, getS3UploadUrl func(ctx context.Context, upReq *UploadResp, start int, end int) (*S3PreSignedURLs, error)) error { func (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSignedUrls *S3PreSignedURLs, cur, end int, reader *io.SectionReader, curSize int64, retry bool, getS3UploadUrl func(ctx context.Context, upReq *UploadResp, start int, end int) (*S3PreSignedURLs, error)) error {
uploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)] uploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]
if uploadUrl == "" { if uploadUrl == "" {
return fmt.Errorf("upload url is empty, s3PreSignedUrls: %+v", s3PreSignedUrls) return fmt.Errorf("upload url is empty, s3PreSignedUrls: %+v", s3PreSignedUrls)
} }
req, err := http.NewRequest("PUT", uploadUrl, reader) req, err := http.NewRequest("PUT", uploadUrl, driver.NewLimitedUploadStream(ctx, reader))
if err != nil { if err != nil {
return err return err
} }
@ -142,6 +148,7 @@ func (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSign
} }
s3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls s3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls
// retry // retry
reader.Seek(0, io.SeekStart)
return d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, cur, end, reader, curSize, true, getS3UploadUrl) return d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, cur, end, reader, curSize, true, getS3UploadUrl)
} }
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {

View File

@ -13,8 +13,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -26,8 +26,9 @@ const (
Api = "https://www.123pan.com/api" Api = "https://www.123pan.com/api"
AApi = "https://www.123pan.com/a/api" AApi = "https://www.123pan.com/a/api"
BApi = "https://www.123pan.com/b/api" BApi = "https://www.123pan.com/b/api"
LoginApi = "https://login.123pan.com/api"
MainApi = BApi MainApi = BApi
SignIn = MainApi + "/user/sign_in" SignIn = LoginApi + "/user/sign_in"
Logout = MainApi + "/user/logout" Logout = MainApi + "/user/logout"
UserInfo = MainApi + "/user/info" UserInfo = MainApi + "/user/info"
FileList = MainApi + "/file/list/new" FileList = MainApi + "/file/list/new"
@ -162,7 +163,7 @@ func (d *Pan123) login() error {
SetHeaders(map[string]string{ SetHeaders(map[string]string{
"origin": "https://www.123pan.com", "origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/", "referer": "https://www.123pan.com/",
"user-agent": "Dart/2.19(dart:io)-alist", "user-agent": "Dart/2.19(dart:io)-openlist",
"platform": "web", "platform": "web",
"app-version": "3", "app-version": "3",
//"user-agent": base.UserAgent, //"user-agent": base.UserAgent,
@ -193,13 +194,15 @@ func (d *Pan123) login() error {
// return &authKey, nil // 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 := base.RestyClient.R()
req.SetHeaders(map[string]string{ req.SetHeaders(map[string]string{
"origin": "https://www.123pan.com", "origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/", "referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken, "authorization": "Bearer " + d.AccessToken,
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",
"platform": "web", "platform": "web",
"app-version": "3", "app-version": "3",
//"user-agent": base.UserAgent, //"user-agent": base.UserAgent,
@ -222,12 +225,13 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
body := res.Body() body := res.Body()
code := utils.Json.Get(body, "code").ToInt() code := utils.Json.Get(body, "code").ToInt()
if code != 0 { if code != 0 {
if code == 401 { if !isRetry && code == 401 {
err := d.login() err := d.login()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return d.request(url, method, callback, resp) isRetry = true
goto do
} }
return nil, errors.New(jsoniter.Get(body, "message").ToString()) return nil, errors.New(jsoniter.Get(body, "message").ToString())
} }
@ -259,7 +263,7 @@ func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]
"operateType": "4", "operateType": "4",
"inDirectSpace": "false", "inDirectSpace": "false",
} }
_res, err := d.request(FileList, http.MethodGet, func(req *resty.Request) { _res, err := d.Request(FileList, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query) req.SetQueryParams(query)
}, &resp) }, &resp)
if err != nil { if err != nil {

View File

@ -5,10 +5,10 @@ import (
stdpath "path" stdpath "path"
"time" "time"
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/OpenListTeam/OpenList/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
) )
type Pan123Link struct { type Pan123Link struct {

View File

@ -1,8 +1,8 @@
package _123Link package _123Link
import ( import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
) )
type Addition struct { type Addition struct {

View File

@ -3,8 +3,8 @@ package _123Link
import ( import (
"time" "time"
"github.com/alist-org/alist/v3/internal/errs" "github.com/OpenListTeam/OpenList/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/model"
) )
// Node is a node in the folder tree // Node is a node in the folder tree

129
drivers/123_open/driver.go Normal file
View File

@ -0,0 +1,129 @@
package _123_open
import (
"context"
"strconv"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/OpenListTeam/OpenList/internal/stream"
"github.com/OpenListTeam/OpenList/pkg/utils"
)
type Open123 struct {
model.Storage
Addition
}
func (d *Open123) Config() driver.Config {
return config
}
func (d *Open123) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Open123) Init(ctx context.Context) error {
if d.UploadThread < 1 || d.UploadThread > 32 {
d.UploadThread = 3
}
return nil
}
func (d *Open123) Drop(ctx context.Context) error {
op.MustSaveDriverStorage(d)
return nil
}
func (d *Open123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
fileLastId := int64(0)
parentFileId, err := strconv.ParseInt(dir.GetID(), 10, 64)
if err != nil {
return nil, err
}
res := make([]File, 0)
for fileLastId != -1 {
files, err := d.getFiles(parentFileId, 100, fileLastId)
if err != nil {
return nil, err
}
// 目前123panAPI请求trashed失效只能通过遍历过滤
for i := range files.Data.FileList {
if files.Data.FileList[i].Trashed == 0 {
res = append(res, files.Data.FileList[i])
}
}
fileLastId = files.Data.LastFileId
}
return utils.SliceConvert(res, func(src File) (model.Obj, error) {
return src, nil
})
}
func (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
fileId, _ := strconv.ParseInt(file.GetID(), 10, 64)
res, err := d.getDownloadInfo(fileId)
if err != nil {
return nil, err
}
link := model.Link{URL: res.Data.DownloadUrl}
return &link, nil
}
func (d *Open123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
parentFileId, _ := strconv.ParseInt(parentDir.GetID(), 10, 64)
return d.mkdir(parentFileId, dirName)
}
func (d *Open123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
toParentFileID, _ := strconv.ParseInt(dstDir.GetID(), 10, 64)
return d.move(srcObj.(File).FileId, toParentFileID)
}
func (d *Open123) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
fileId, _ := strconv.ParseInt(srcObj.GetID(), 10, 64)
return d.rename(fileId, newName)
}
func (d *Open123) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *Open123) Remove(ctx context.Context, obj model.Obj) error {
fileId, _ := strconv.ParseInt(obj.GetID(), 10, 64)
return d.trash(fileId)
}
func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
parentFileId, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
etag := file.GetHash().GetHash(utils.MD5)
if len(etag) < utils.MD5.Width {
_, etag, err = stream.CacheFullInTempFileAndHash(file, utils.MD5)
if err != nil {
return err
}
}
createResp, err := d.create(parentFileId, file.GetName(), etag, file.GetSize(), 2, false)
if err != nil {
return err
}
if createResp.Data.Reuse {
return nil
}
up(10)
return d.Upload(ctx, file, createResp, up)
}
var _ driver.Driver = (*Open123)(nil)

39
drivers/123_open/meta.go Normal file
View File

@ -0,0 +1,39 @@
package _123_open
import (
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/op"
)
type Addition struct {
// refresh_token方式的AccessToken 【对个人开发者暂未开放】
RefreshToken string `json:"RefreshToken" required:"false"`
// 通过 https://www.123pan.com/developer 申请
ClientID string `json:"ClientID" required:"false"`
ClientSecret string `json:"ClientSecret" required:"false"`
// 直接写入AccessToken
AccessToken string `json:"AccessToken" required:"false"`
// 用户名+密码方式登录的AccessToken可以兼容
//Username string `json:"username" required:"false"`
//Password string `json:"password" required:"false"`
// 上传线程数
UploadThread int `json:"UploadThread" type:"number" default:"3" help:"the threads of upload"`
driver.RootID
}
var config = driver.Config{
Name: "123 Open",
DefaultRoot: "0",
LocalSort: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Open123{}
})
}

205
drivers/123_open/types.go Normal file
View File

@ -0,0 +1,205 @@
package _123_open
import (
"strconv"
"time"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
)
type ApiInfo struct {
url string
qps int
token chan struct{}
}
func (a *ApiInfo) Require() {
if a.qps > 0 {
a.token <- struct{}{}
}
}
func (a *ApiInfo) Release() {
if a.qps > 0 {
time.AfterFunc(time.Second, func() {
<-a.token
})
}
}
func (a *ApiInfo) SetQPS(qps int) {
a.qps = qps
a.token = make(chan struct{}, qps)
}
func (a *ApiInfo) NowLen() int {
return len(a.token)
}
func InitApiInfo(url string, qps int) *ApiInfo {
return &ApiInfo{
url: url,
qps: qps,
token: make(chan struct{}, qps),
}
}
type File struct {
FileName string `json:"filename"`
Size int64 `json:"size"`
CreateAt string `json:"createAt"`
UpdateAt string `json:"updateAt"`
FileId int64 `json:"fileId"`
Type int `json:"type"`
Etag string `json:"etag"`
S3KeyFlag string `json:"s3KeyFlag"`
ParentFileId int `json:"parentFileId"`
Category int `json:"category"`
Status int `json:"status"`
Trashed int `json:"trashed"`
}
func (f File) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.MD5, f.Etag)
}
func (f File) GetPath() string {
return ""
}
func (f File) GetSize() int64 {
return f.Size
}
func (f File) GetName() string {
return f.FileName
}
func (f File) CreateTime() time.Time {
parsedTime, err := time.Parse("2006-01-02 15:04:05", f.CreateAt)
if err != nil {
return time.Now()
}
return parsedTime
}
func (f File) ModTime() time.Time {
parsedTime, err := time.Parse("2006-01-02 15:04:05", f.UpdateAt)
if err != nil {
return time.Now()
}
return parsedTime
}
func (f File) IsDir() bool {
return f.Type == 1
}
func (f File) GetID() string {
return strconv.FormatInt(f.FileId, 10)
}
var _ model.Obj = (*File)(nil)
type BaseResp struct {
Code int `json:"code"`
Message string `json:"message"`
XTraceID string `json:"x-traceID"`
}
type AccessTokenResp struct {
BaseResp
Data struct {
AccessToken string `json:"accessToken"`
ExpiredAt string `json:"expiredAt"`
} `json:"data"`
}
type RefreshTokenResp struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}
type UserInfoResp struct {
BaseResp
Data struct {
UID int64 `json:"uid"`
Username string `json:"username"`
DisplayName string `json:"displayName"`
HeadImage string `json:"headImage"`
Passport string `json:"passport"`
Mail string `json:"mail"`
SpaceUsed int64 `json:"spaceUsed"`
SpacePermanent int64 `json:"spacePermanent"`
SpaceTemp int64 `json:"spaceTemp"`
SpaceTempExpr string `json:"spaceTempExpr"`
Vip bool `json:"vip"`
DirectTraffic int64 `json:"directTraffic"`
IsHideUID bool `json:"isHideUID"`
} `json:"data"`
}
type FileListResp struct {
BaseResp
Data struct {
LastFileId int64 `json:"lastFileId"`
FileList []File `json:"fileList"`
} `json:"data"`
}
type DownloadInfoResp struct {
BaseResp
Data struct {
DownloadUrl string `json:"downloadUrl"`
} `json:"data"`
}
type UploadCreateResp struct {
BaseResp
Data struct {
FileID int64 `json:"fileID"`
PreuploadID string `json:"preuploadID"`
Reuse bool `json:"reuse"`
SliceSize int64 `json:"sliceSize"`
} `json:"data"`
}
type UploadUrlResp struct {
BaseResp
Data struct {
PresignedURL string `json:"presignedURL"`
}
}
type UploadCompleteResp struct {
BaseResp
Data struct {
Async bool `json:"async"`
Completed bool `json:"completed"`
FileID int64 `json:"fileID"`
} `json:"data"`
}
type UploadAsyncResp struct {
BaseResp
Data struct {
Completed bool `json:"completed"`
FileID int64 `json:"fileID"`
} `json:"data"`
}
type UploadResp struct {
BaseResp
Data struct {
AccessKeyId string `json:"AccessKeyId"`
Bucket string `json:"Bucket"`
Key string `json:"Key"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken"`
FileId int64 `json:"FileId"`
Reuse bool `json:"Reuse"`
EndPoint string `json:"EndPoint"`
StorageNode string `json:"StorageNode"`
UploadId string `json:"UploadId"`
} `json:"data"`
}

150
drivers/123_open/upload.go Normal file
View File

@ -0,0 +1,150 @@
package _123_open
import (
"context"
"net/http"
"time"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/errgroup"
"github.com/OpenListTeam/OpenList/pkg/http_range"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/avast/retry-go"
"github.com/go-resty/resty/v2"
)
func (d *Open123) create(parentFileID int64, filename string, etag string, size int64, duplicate int, containDir bool) (*UploadCreateResp, error) {
var resp UploadCreateResp
_, err := d.Request(UploadCreate, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"parentFileId": parentFileID,
"filename": filename,
"etag": etag,
"size": size,
"duplicate": duplicate,
"containDir": containDir,
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *Open123) url(preuploadID string, sliceNo int64) (string, error) {
// get upload url
var resp UploadUrlResp
_, err := d.Request(UploadUrl, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"preuploadId": preuploadID,
"sliceNo": sliceNo,
})
}, &resp)
if err != nil {
return "", err
}
return resp.Data.PresignedURL, nil
}
func (d *Open123) complete(preuploadID string) (*UploadCompleteResp, error) {
var resp UploadCompleteResp
_, err := d.Request(UploadComplete, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"preuploadID": preuploadID,
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *Open123) async(preuploadID string) (*UploadAsyncResp, error) {
var resp UploadAsyncResp
_, err := d.Request(UploadAsync, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"preuploadID": preuploadID,
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createResp *UploadCreateResp, up driver.UpdateProgress) error {
size := file.GetSize()
chunkSize := createResp.Data.SliceSize
uploadNums := (size + chunkSize - 1) / chunkSize
threadG, uploadCtx := errgroup.NewGroupWithContext(ctx, d.UploadThread,
retry.Attempts(3),
retry.Delay(time.Second),
retry.DelayType(retry.BackOffDelay))
for partIndex := int64(0); partIndex < uploadNums; partIndex++ {
if utils.IsCanceled(uploadCtx) {
return ctx.Err()
}
partIndex := partIndex
partNumber := partIndex + 1 // 分片号从1开始
offset := partIndex * chunkSize
size := min(chunkSize, size-offset)
limitedReader, err := file.RangeRead(http_range.Range{
Start: offset,
Length: size})
if err != nil {
return err
}
limitedReader = driver.NewLimitedUploadStream(ctx, limitedReader)
threadG.Go(func(ctx context.Context) error {
uploadPartUrl, err := d.url(createResp.Data.PreuploadID, partNumber)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, "PUT", uploadPartUrl, limitedReader)
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = size
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
_ = res.Body.Close()
progress := 10.0 + 85.0*float64(threadG.Success())/float64(uploadNums)
up(progress)
return nil
})
}
if err := threadG.Wait(); err != nil {
return err
}
uploadCompleteResp, err := d.complete(createResp.Data.PreuploadID)
if err != nil {
return err
}
if uploadCompleteResp.Data.Async == false || uploadCompleteResp.Data.Completed {
return nil
}
for {
uploadAsyncResp, err := d.async(createResp.Data.PreuploadID)
if err != nil {
return err
}
if uploadAsyncResp.Data.Completed {
break
}
}
up(100)
return nil
}

217
drivers/123_open/util.go Normal file
View File

@ -0,0 +1,217 @@
package _123_open
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"time"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
var ( //不同情况下获取的AccessTokenQPS限制不同 如下模块化易于拓展
Api = "https://open-api.123pan.com"
AccessToken = InitApiInfo(Api+"/api/v1/access_token", 1)
RefreshToken = InitApiInfo(Api+"/api/v1/oauth2/access_token", 1)
UserInfo = InitApiInfo(Api+"/api/v1/user/info", 1)
FileList = InitApiInfo(Api+"/api/v2/file/list", 4)
DownloadInfo = InitApiInfo(Api+"/api/v1/file/download_info", 0)
Mkdir = InitApiInfo(Api+"/upload/v1/file/mkdir", 2)
Move = InitApiInfo(Api+"/api/v1/file/move", 1)
Rename = InitApiInfo(Api+"/api/v1/file/name", 1)
Trash = InitApiInfo(Api+"/api/v1/file/trash", 2)
UploadCreate = InitApiInfo(Api+"/upload/v1/file/create", 2)
UploadUrl = InitApiInfo(Api+"/upload/v1/file/get_upload_url", 0)
UploadComplete = InitApiInfo(Api+"/upload/v1/file/upload_complete", 0)
UploadAsync = InitApiInfo(Api+"/upload/v1/file/upload_async_result", 1)
)
func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
retryToken := true
for {
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"authorization": "Bearer " + d.AccessToken,
"platform": "open_platform",
"Content-Type": "application/json",
})
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
log.Debugf("API: %s, QPS: %d, NowLen: %d", apiInfo.url, apiInfo.qps, apiInfo.NowLen())
apiInfo.Require()
defer apiInfo.Release()
res, err := req.Execute(method, apiInfo.url)
if err != nil {
return nil, err
}
body := res.Body()
// 解析为通用响应
var baseResp BaseResp
if err = json.Unmarshal(body, &baseResp); err != nil {
return nil, err
}
if baseResp.Code == 0 {
return body, nil
} else if baseResp.Code == 401 && retryToken {
retryToken = false
if err := d.flushAccessToken(); err != nil {
return nil, err
}
} else if baseResp.Code == 429 {
time.Sleep(500 * time.Millisecond)
log.Warningf("API: %s, QPS: %d, 请求太频繁对应API提示过多请减小QPS", apiInfo.url, apiInfo.qps)
} else {
return nil, errors.New(baseResp.Message)
}
}
}
func (d *Open123) flushAccessToken() error {
if d.Addition.ClientID != "" {
if d.Addition.ClientSecret != "" {
var resp AccessTokenResp
_, err := d.Request(AccessToken, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"clientID": d.ClientID,
"clientSecret": d.ClientSecret,
})
}, &resp)
if err != nil {
return err
}
d.AccessToken = resp.Data.AccessToken
op.MustSaveDriverStorage(d)
} else if d.Addition.RefreshToken != "" {
var resp RefreshTokenResp
_, err := d.Request(RefreshToken, http.MethodPost, func(req *resty.Request) {
req.SetQueryParam("client_id", d.ClientID)
req.SetQueryParam("grant_type", "refresh_token")
req.SetQueryParam("refresh_token", d.Addition.RefreshToken)
}, &resp)
if err != nil {
return err
}
d.AccessToken = resp.AccessToken
d.RefreshToken = resp.RefreshToken
op.MustSaveDriverStorage(d)
}
}
return nil
}
func (d *Open123) getUserInfo() (*UserInfoResp, error) {
var resp UserInfoResp
if _, err := d.Request(UserInfo, http.MethodGet, nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*FileListResp, error) {
var resp FileListResp
_, err := d.Request(FileList, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(
map[string]string{
"parentFileId": strconv.FormatInt(parentFileId, 10),
"limit": strconv.Itoa(limit),
"lastFileId": strconv.FormatInt(lastFileId, 10),
"trashed": "false",
"searchMode": "",
"searchData": "",
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *Open123) getDownloadInfo(fileId int64) (*DownloadInfoResp, error) {
var resp DownloadInfoResp
_, err := d.Request(DownloadInfo, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"fileId": strconv.FormatInt(fileId, 10),
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *Open123) mkdir(parentID int64, name string) error {
_, err := d.Request(Mkdir, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"parentID": strconv.FormatInt(parentID, 10),
"name": name,
})
}, nil)
if err != nil {
return err
}
return nil
}
func (d *Open123) move(fileID, toParentFileID int64) error {
_, err := d.Request(Move, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"fileIDs": []int64{fileID},
"toParentFileID": toParentFileID,
})
}, nil)
if err != nil {
return err
}
return nil
}
func (d *Open123) rename(fileId int64, fileName string) error {
_, err := d.Request(Rename, http.MethodPut, func(req *resty.Request) {
req.SetBody(base.Json{
"fileId": fileId,
"fileName": fileName,
})
}, nil)
if err != nil {
return err
}
return nil
}
func (d *Open123) trash(fileId int64) error {
_, err := d.Request(Trash, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"fileIDs": []int64{fileId},
})
}, nil)
if err != nil {
return err
}
return nil
}

View File

@ -4,17 +4,19 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"golang.org/x/time/rate"
"net/http" "net/http"
"net/url" "net/url"
"sync" "sync"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "golang.org/x/time/rate"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" _123 "github.com/OpenListTeam/OpenList/drivers/123"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -23,6 +25,7 @@ type Pan123Share struct {
model.Storage model.Storage
Addition Addition
apiRateLimit sync.Map apiRateLimit sync.Map
ref *_123.Pan123
} }
func (d *Pan123Share) Config() driver.Config { func (d *Pan123Share) Config() driver.Config {
@ -39,7 +42,17 @@ func (d *Pan123Share) Init(ctx context.Context) error {
return nil 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 { func (d *Pan123Share) Drop(ctx context.Context) error {
d.ref = nil
return nil return nil
} }

View File

@ -1,8 +1,8 @@
package _123Share package _123Share
import ( import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
) )
type Addition struct { type Addition struct {

View File

@ -1,14 +1,15 @@
package _123Share package _123Share
import ( import (
"github.com/alist-org/alist/v3/pkg/utils"
"net/url" "net/url"
"path" "path"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/model"
) )
type File struct { type File struct {

View File

@ -13,8 +13,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@ -53,12 +53,15 @@ func GetApi(rawUrl string) string {
} }
func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
if d.ref != nil {
return d.ref.Request(url, method, callback, resp)
}
req := base.RestyClient.R() req := base.RestyClient.R()
req.SetHeaders(map[string]string{ req.SetHeaders(map[string]string{
"origin": "https://www.123pan.com", "origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/", "referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken, "authorization": "Bearer " + d.AccessToken,
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) opnelist-client",
"platform": "web", "platform": "web",
"app-version": "3", "app-version": "3",
//"user-agent": base.UserAgent, //"user-agent": base.UserAgent,

View File

@ -2,28 +2,32 @@ package _139
import ( import (
"context" "context"
"encoding/base64" "encoding/xml"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"path"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/OpenListTeam/OpenList/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" streamPkg "github.com/OpenListTeam/OpenList/internal/stream"
"github.com/alist-org/alist/v3/pkg/cron" "github.com/OpenListTeam/OpenList/pkg/cron"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/pkg/utils/random"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type Yun139 struct { type Yun139 struct {
model.Storage model.Storage
Addition Addition
cron *cron.Cron cron *cron.Cron
Account string Account string
ref *Yun139
PersonalCloudHost string
} }
func (d *Yun139) Config() driver.Config { func (d *Yun139) Config() driver.Config {
@ -35,56 +39,79 @@ func (d *Yun139) GetAddition() driver.Additional {
} }
func (d *Yun139) Init(ctx context.Context) error { func (d *Yun139) Init(ctx context.Context) error {
if d.Authorization == "" { if d.ref == nil {
return fmt.Errorf("authorization is empty") if len(d.Authorization) == 0 {
} return fmt.Errorf("authorization is empty")
d.cron = cron.NewCron(time.Hour * 24 * 7) }
d.cron.Do(func() {
err := d.refreshToken() err := d.refreshToken()
if err != nil { if err != nil {
log.Errorf("%+v", err) return err
} }
})
// Query Route Policy
var resp QueryRoutePolicyResp
_, err = d.requestRoute(base.Json{
"userInfo": base.Json{
"userType": 1,
"accountType": 1,
"accountName": d.Account},
"modAddrType": 1,
}, &resp)
if err != nil {
return err
}
for _, policyItem := range resp.Data.RoutePolicyList {
if policyItem.ModName == "personal" {
d.PersonalCloudHost = policyItem.HttpsUrl
break
}
}
if len(d.PersonalCloudHost) == 0 {
return fmt.Errorf("PersonalCloudHost is empty")
}
d.cron = cron.NewCron(time.Hour * 12)
d.cron.Do(func() {
err := d.refreshToken()
if err != nil {
log.Errorf("%+v", err)
}
})
}
switch d.Addition.Type { switch d.Addition.Type {
case MetaPersonalNew: case MetaPersonalNew:
if len(d.Addition.RootFolderID) == 0 { if len(d.Addition.RootFolderID) == 0 {
d.RootFolderID = "/" d.RootFolderID = "/"
} }
return nil
case MetaPersonal: case MetaPersonal:
if len(d.Addition.RootFolderID) == 0 { if len(d.Addition.RootFolderID) == 0 {
d.RootFolderID = "root" d.RootFolderID = "root"
} }
fallthrough case MetaGroup:
if len(d.Addition.RootFolderID) == 0 {
d.RootFolderID = d.CloudID
}
case MetaFamily: case MetaFamily:
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
if err != nil {
return err
}
decodeStr := string(decode)
splits := strings.Split(decodeStr, ":")
if len(splits) < 2 {
return fmt.Errorf("authorization is invalid, splits < 2")
}
d.Account = splits[1]
_, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{
"qryUserExternInfoReq": base.Json{
"commonAccountInfo": base.Json{
"account": d.Account,
"accountType": 1,
},
},
}, nil)
return err
default: default:
return errs.NotImplement return errs.NotImplement
} }
return nil
}
func (d *Yun139) InitReference(storage driver.Driver) error {
refStorage, ok := storage.(*Yun139)
if ok {
d.ref = refStorage
return nil
}
return errs.NotSupport
} }
func (d *Yun139) Drop(ctx context.Context) error { func (d *Yun139) Drop(ctx context.Context) error {
if d.cron != nil { if d.cron != nil {
d.cron.Stop() d.cron.Stop()
} }
d.ref = nil
return nil return nil
} }
@ -96,6 +123,8 @@ func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
return d.getFiles(dir.GetID()) return d.getFiles(dir.GetID())
case MetaFamily: case MetaFamily:
return d.familyGetFiles(dir.GetID()) return d.familyGetFiles(dir.GetID())
case MetaGroup:
return d.groupGetFiles(dir.GetID())
default: default:
return nil, errs.NotImplement return nil, errs.NotImplement
} }
@ -108,9 +137,11 @@ func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
case MetaPersonalNew: case MetaPersonalNew:
url, err = d.personalGetLink(file.GetID()) url, err = d.personalGetLink(file.GetID())
case MetaPersonal: case MetaPersonal:
fallthrough
case MetaFamily:
url, err = d.getLink(file.GetID()) url, err = d.getLink(file.GetID())
case MetaFamily:
url, err = d.familyGetLink(file.GetID(), file.GetPath())
case MetaGroup:
url, err = d.groupGetLink(file.GetID(), file.GetPath())
default: default:
return nil, errs.NotImplement return nil, errs.NotImplement
} }
@ -131,7 +162,7 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
"type": "folder", "type": "folder",
"fileRenameMode": "force_rename", "fileRenameMode": "force_rename",
} }
pathname := "/hcy/file/create" pathname := "/file/create"
_, err = d.personalPost(pathname, data, nil) _, err = d.personalPost(pathname, data, nil)
case MetaPersonal: case MetaPersonal:
data := base.Json{ data := base.Json{
@ -139,7 +170,7 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
"parentCatalogID": parentDir.GetID(), "parentCatalogID": parentDir.GetID(),
"newCatalogName": dirName, "newCatalogName": dirName,
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
}, },
@ -150,12 +181,26 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
data := base.Json{ data := base.Json{
"cloudID": d.CloudID, "cloudID": d.CloudID,
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
"docLibName": dirName, "docLibName": dirName,
"path": path.Join(parentDir.GetPath(), parentDir.GetID()),
} }
pathname := "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc" pathname := "/orchestration/familyCloud-rebuild/cloudCatalog/v1.0/createCloudDoc"
_, err = d.post(pathname, data, nil)
case MetaGroup:
data := base.Json{
"catalogName": dirName,
"commonAccountInfo": base.Json{
"account": d.getAccount(),
"accountType": 1,
},
"groupID": d.CloudID,
"parentFileId": parentDir.GetID(),
"path": path.Join(parentDir.GetPath(), parentDir.GetID()),
}
pathname := "/orchestration/group-rebuild/catalog/v1.0/createGroupCatalog"
_, err = d.post(pathname, data, nil) _, err = d.post(pathname, data, nil)
default: default:
err = errs.NotImplement err = errs.NotImplement
@ -170,12 +215,40 @@ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj,
"fileIds": []string{srcObj.GetID()}, "fileIds": []string{srcObj.GetID()},
"toParentFileId": dstDir.GetID(), "toParentFileId": dstDir.GetID(),
} }
pathname := "/hcy/file/batchMove" pathname := "/file/batchMove"
_, err := d.personalPost(pathname, data, nil) _, err := d.personalPost(pathname, data, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return srcObj, nil return srcObj, nil
case MetaGroup:
var contentList []string
var catalogList []string
if srcObj.IsDir() {
catalogList = append(catalogList, srcObj.GetID())
} else {
contentList = append(contentList, srcObj.GetID())
}
data := base.Json{
"taskType": 3,
"srcType": 2,
"srcGroupID": d.CloudID,
"destType": 2,
"destGroupID": d.CloudID,
"destPath": dstDir.GetPath(),
"contentList": contentList,
"catalogList": catalogList,
"commonAccountInfo": base.Json{
"account": d.getAccount(),
"accountType": 1,
},
}
pathname := "/orchestration/group-rebuild/task/v1.0/createBatchOprTask"
_, err := d.post(pathname, data, nil)
if err != nil {
return nil, err
}
return srcObj, nil
case MetaPersonal: case MetaPersonal:
var contentInfoList []string var contentInfoList []string
var catalogInfoList []string var catalogInfoList []string
@ -194,7 +267,7 @@ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj,
"newCatalogID": dstDir.GetID(), "newCatalogID": dstDir.GetID(),
}, },
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
}, },
@ -219,7 +292,7 @@ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) e
"name": newName, "name": newName,
"description": "", "description": "",
} }
pathname := "/hcy/file/update" pathname := "/file/update"
_, err = d.personalPost(pathname, data, nil) _, err = d.personalPost(pathname, data, nil)
case MetaPersonal: case MetaPersonal:
var data base.Json var data base.Json
@ -229,7 +302,7 @@ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) e
"catalogID": srcObj.GetID(), "catalogID": srcObj.GetID(),
"catalogName": newName, "catalogName": newName,
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
} }
@ -239,13 +312,72 @@ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) e
"contentID": srcObj.GetID(), "contentID": srcObj.GetID(),
"contentName": newName, "contentName": newName,
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
} }
pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo" pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo"
} }
_, err = d.post(pathname, data, nil) _, err = d.post(pathname, data, nil)
case MetaGroup:
var data base.Json
var pathname string
if srcObj.IsDir() {
data = base.Json{
"groupID": d.CloudID,
"modifyCatalogID": srcObj.GetID(),
"modifyCatalogName": newName,
"path": srcObj.GetPath(),
"commonAccountInfo": base.Json{
"account": d.getAccount(),
"accountType": 1,
},
}
pathname = "/orchestration/group-rebuild/catalog/v1.0/modifyGroupCatalog"
} else {
data = base.Json{
"groupID": d.CloudID,
"contentID": srcObj.GetID(),
"contentName": newName,
"path": srcObj.GetPath(),
"commonAccountInfo": base.Json{
"account": d.getAccount(),
"accountType": 1,
},
}
pathname = "/orchestration/group-rebuild/content/v1.0/modifyGroupContent"
}
_, err = d.post(pathname, data, nil)
case MetaFamily:
var data base.Json
var pathname string
if srcObj.IsDir() {
// 网页接口不支持重命名家庭云文件夹
// data = base.Json{
// "catalogType": 3,
// "catalogID": srcObj.GetID(),
// "catalogName": newName,
// "commonAccountInfo": base.Json{
// "account": d.getAccount(),
// "accountType": 1,
// },
// "path": srcObj.GetPath(),
// }
// pathname = "/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyCatalogInfo"
return errs.NotImplement
} else {
data = base.Json{
"contentID": srcObj.GetID(),
"contentName": newName,
"commonAccountInfo": base.Json{
"account": d.getAccount(),
"accountType": 1,
},
"path": srcObj.GetPath(),
}
pathname = "/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyContentInfo"
}
_, err = d.post(pathname, data, nil)
default: default:
err = errs.NotImplement err = errs.NotImplement
} }
@ -260,7 +392,7 @@ func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
"fileIds": []string{srcObj.GetID()}, "fileIds": []string{srcObj.GetID()},
"toParentFileId": dstDir.GetID(), "toParentFileId": dstDir.GetID(),
} }
pathname := "/hcy/file/batchCopy" pathname := "/file/batchCopy"
_, err := d.personalPost(pathname, data, nil) _, err := d.personalPost(pathname, data, nil)
return err return err
case MetaPersonal: case MetaPersonal:
@ -281,7 +413,7 @@ func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
"newCatalogID": dstDir.GetID(), "newCatalogID": dstDir.GetID(),
}, },
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
}, },
@ -300,9 +432,31 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
data := base.Json{ data := base.Json{
"fileIds": []string{obj.GetID()}, "fileIds": []string{obj.GetID()},
} }
pathname := "/hcy/recyclebin/batchTrash" pathname := "/recyclebin/batchTrash"
_, err := d.personalPost(pathname, data, nil) _, err := d.personalPost(pathname, data, nil)
return err return err
case MetaGroup:
var contentList []string
var catalogList []string
// 必须使用完整路径删除
if obj.IsDir() {
catalogList = append(catalogList, obj.GetPath())
} else {
contentList = append(contentList, path.Join(obj.GetPath(), obj.GetID()))
}
data := base.Json{
"taskType": 2,
"srcGroupID": d.CloudID,
"contentList": contentList,
"catalogList": catalogList,
"commonAccountInfo": base.Json{
"account": d.getAccount(),
"accountType": 1,
},
}
pathname := "/orchestration/group-rebuild/task/v1.0/createBatchOprTask"
_, err := d.post(pathname, data, nil)
return err
case MetaPersonal: case MetaPersonal:
fallthrough fallthrough
case MetaFamily: case MetaFamily:
@ -323,7 +477,7 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
"catalogInfoList": catalogInfoList, "catalogInfoList": catalogInfoList,
}, },
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
}, },
@ -334,13 +488,15 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
"catalogList": catalogInfoList, "catalogList": catalogInfoList,
"contentList": contentInfoList, "contentList": contentInfoList,
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
"sourceCloudID": d.CloudID,
"sourceCatalogType": 1002, "sourceCatalogType": 1002,
"taskType": 2, "taskType": 2,
"path": obj.GetPath(),
} }
pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask" pathname = "/orchestration/familyCloud-rebuild/batchOprTask/v1.0/createBatchOprTask"
} }
_, err := d.post(pathname, data, nil) _, err := d.post(pathname, data, nil)
return err return err
@ -349,20 +505,15 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
} }
} }
const ( func (d *Yun139) getPartSize(size int64) int64 {
_ = iota //ignore first value by assigning to blank identifier if d.CustomUploadPartSize != 0 {
KB = 1 << (10 * iota) return d.CustomUploadPartSize
MB
GB
TB
)
func getPartSize(size int64) int64 {
// 网盘对于分片数量存在上限
if size/GB > 30 {
return 512 * MB
} }
return 100 * MB // 网盘对于分片数量存在上限
if size/utils.GB > 30 {
return 512 * utils.MB
}
return 100 * utils.MB
} }
func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
@ -370,149 +521,288 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
case MetaPersonalNew: case MetaPersonalNew:
var err error var err error
fullHash := stream.GetHash().GetHash(utils.SHA256) fullHash := stream.GetHash().GetHash(utils.SHA256)
if len(fullHash) <= 0 { if len(fullHash) != utils.SHA256.Width {
tmpF, err := stream.CacheFullInTempFile() _, fullHash, err = streamPkg.CacheFullInTempFileAndHash(stream, utils.SHA256)
if err != nil {
return err
}
fullHash, err = utils.HashFile(utils.SHA256, tmpF)
if err != nil { if err != nil {
return err return err
} }
} }
// return errs.NotImplement
size := stream.GetSize()
var partSize = d.getPartSize(size)
part := size / partSize
if size%partSize > 0 {
part++
} else if part == 0 {
part = 1
}
partInfos := make([]PartInfo, 0, part)
for i := int64(0); i < part; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
start := i * partSize
byteSize := size - start
if byteSize > partSize {
byteSize = partSize
}
partNumber := i + 1
partInfo := PartInfo{
PartNumber: partNumber,
PartSize: byteSize,
ParallelHashCtx: ParallelHashCtx{
PartOffset: start,
},
}
partInfos = append(partInfos, partInfo)
}
// 筛选出前 100 个 partInfos
firstPartInfos := partInfos
if len(firstPartInfos) > 100 {
firstPartInfos = firstPartInfos[:100]
}
// 创建任务获取上传信息和前100个分片的上传地址
data := base.Json{ data := base.Json{
"contentHash": fullHash, "contentHash": fullHash,
"contentHashAlgorithm": "SHA256", "contentHashAlgorithm": "SHA256",
"contentType": "application/octet-stream", "contentType": "application/octet-stream",
"parallelUpload": false, "parallelUpload": false,
"partInfos": []base.Json{{ "partInfos": firstPartInfos,
"parallelHashCtx": base.Json{ "size": size,
"partOffset": 0, "parentFileId": dstDir.GetID(),
}, "name": stream.GetName(),
"partNumber": 1, "type": "file",
"partSize": stream.GetSize(), "fileRenameMode": "auto_rename",
}},
"size": stream.GetSize(),
"parentFileId": dstDir.GetID(),
"name": stream.GetName(),
"type": "file",
"fileRenameMode": "auto_rename",
} }
pathname := "/hcy/file/create" pathname := "/file/create"
var resp PersonalUploadResp var resp PersonalUploadResp
_, err = d.personalPost(pathname, data, &resp) _, err = d.personalPost(pathname, data, &resp)
if err != nil { if err != nil {
return err return err
} }
if resp.Data.Exist || resp.Data.RapidUpload { // 判断文件是否已存在
// resp.Data.Exist: true 已存在同名文件且校验相同,云端不会重复增加文件,无需手动处理冲突
if resp.Data.Exist {
return nil return nil
} }
// Progress // 判断文件是否支持快传
p := driver.NewProgress(stream.GetSize(), up) // resp.Data.RapidUpload: true 支持快传,但此处直接检测是否返回分片的上传地址
// 快传的情况下同样需要手动处理冲突
if resp.Data.PartInfos != nil {
// 读取前100个分片的上传地址
uploadPartInfos := resp.Data.PartInfos
// Update Progress // 获取后续分片的上传地址
r := io.TeeReader(stream, p) for i := 101; i < len(partInfos); i += 100 {
end := i + 100
if end > len(partInfos) {
end = len(partInfos)
}
batchPartInfos := partInfos[i:end]
req, err := http.NewRequest("PUT", resp.Data.PartInfos[0].UploadUrl, r) moredata := base.Json{
if err != nil { "fileId": resp.Data.FileId,
return err "uploadId": resp.Data.UploadId,
} "partInfos": batchPartInfos,
req = req.WithContext(ctx) "commonAccountInfo": base.Json{
req.Header.Set("Content-Type", "application/octet-stream") "account": d.getAccount(),
req.Header.Set("Content-Length", fmt.Sprint(stream.GetSize())) "accountType": 1,
req.Header.Set("Origin", "https://yun.139.com") },
req.Header.Set("Referer", "https://yun.139.com/") }
req.ContentLength = stream.GetSize() pathname := "/file/getUploadUrl"
var moreresp PersonalUploadUrlResp
_, err = d.personalPost(pathname, moredata, &moreresp)
if err != nil {
return err
}
uploadPartInfos = append(uploadPartInfos, moreresp.Data.PartInfos...)
}
res, err := base.HttpClient.Do(req) // Progress
if err != nil { p := driver.NewProgress(size, up)
return err
rateLimited := driver.NewLimitedUploadStream(ctx, stream)
// 上传所有分片
for _, uploadPartInfo := range uploadPartInfos {
index := uploadPartInfo.PartNumber - 1
partSize := partInfos[index].PartSize
log.Debugf("[139] uploading part %+v/%+v", index, len(uploadPartInfos))
limitReader := io.LimitReader(rateLimited, partSize)
// Update Progress
r := io.TeeReader(limitReader, p)
req, err := http.NewRequest("PUT", uploadPartInfo.UploadUrl, r)
if err != nil {
return err
}
req = req.WithContext(ctx)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Length", fmt.Sprint(partSize))
req.Header.Set("Origin", "https://yun.139.com")
req.Header.Set("Referer", "https://yun.139.com/")
req.ContentLength = partSize
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
_ = res.Body.Close()
log.Debugf("[139] uploaded: %+v", res)
if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
data = base.Json{
"contentHash": fullHash,
"contentHashAlgorithm": "SHA256",
"fileId": resp.Data.FileId,
"uploadId": resp.Data.UploadId,
}
_, err = d.personalPost("/file/complete", data, nil)
if err != nil {
return err
}
} }
_ = res.Body.Close() // 处理冲突
log.Debugf("%+v", res) if resp.Data.FileName != stream.GetName() {
if res.StatusCode != http.StatusOK { log.Debugf("[139] conflict detected: %s != %s", resp.Data.FileName, stream.GetName())
return fmt.Errorf("unexpected status code: %d", res.StatusCode) // 给服务器一定时间处理数据,避免无法刷新文件列表
} time.Sleep(time.Millisecond * 500)
// 刷新并获取文件列表
data = base.Json{ files, err := d.List(ctx, dstDir, model.ListArgs{Refresh: true})
"contentHash": fullHash, if err != nil {
"contentHashAlgorithm": "SHA256", return err
"fileId": resp.Data.FileId, }
"uploadId": resp.Data.UploadId, // 删除旧文件
} for _, file := range files {
_, err = d.personalPost("/hcy/file/complete", data, nil) if file.GetName() == stream.GetName() {
if err != nil { log.Debugf("[139] conflict: removing old: %s", file.GetName())
return err // 删除前重命名旧文件,避免仍旧冲突
err = d.Rename(ctx, file, stream.GetName()+random.String(4))
if err != nil {
return err
}
err = d.Remove(ctx, file)
if err != nil {
return err
}
break
}
}
// 重命名新文件
for _, file := range files {
if file.GetName() == resp.Data.FileName {
log.Debugf("[139] conflict: renaming new: %s => %s", file.GetName(), stream.GetName())
err = d.Rename(ctx, file, stream.GetName())
if err != nil {
return err
}
break
}
}
} }
return nil return nil
case MetaPersonal: case MetaPersonal:
fallthrough fallthrough
case MetaFamily: case MetaFamily:
// 处理冲突
// 获取文件列表
files, err := d.List(ctx, dstDir, model.ListArgs{})
if err != nil {
return err
}
// 删除旧文件
for _, file := range files {
if file.GetName() == stream.GetName() {
log.Debugf("[139] conflict: removing old: %s", file.GetName())
// 删除前重命名旧文件,避免仍旧冲突
err = d.Rename(ctx, file, stream.GetName()+random.String(4))
if err != nil {
return err
}
err = d.Remove(ctx, file)
if err != nil {
return err
}
break
}
}
var reportSize int64
if d.ReportRealSize {
reportSize = stream.GetSize()
} else {
reportSize = 0
}
data := base.Json{ data := base.Json{
"manualRename": 2, "manualRename": 2,
"operation": 0, "operation": 0,
"fileCount": 1, "fileCount": 1,
"totalSize": 0, // 去除上传大小限制 "totalSize": reportSize,
"uploadContentList": []base.Json{{ "uploadContentList": []base.Json{{
"contentName": stream.GetName(), "contentName": stream.GetName(),
"contentSize": 0, // 去除上传大小限制 "contentSize": reportSize,
// "digest": "5a3231986ce7a6b46e408612d385bafa" // "digest": "5a3231986ce7a6b46e408612d385bafa"
}}, }},
"parentCatalogID": dstDir.GetID(), "parentCatalogID": dstDir.GetID(),
"newCatalogName": "", "newCatalogName": "",
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
} }
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest" pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
if d.isFamily() { if d.isFamily() {
// data = d.newJson(base.Json{ data = d.newJson(base.Json{
// "fileCount": 1, "fileCount": 1,
// "manualRename": 2, "manualRename": 2,
// "operation": 0, "operation": 0,
// "path": "", "path": path.Join(dstDir.GetPath(), dstDir.GetID()),
// "seqNo": "", "seqNo": random.String(32), //序列号不能为空
// "totalSize": 0, "totalSize": reportSize,
// "uploadContentList": []base.Json{{ "uploadContentList": []base.Json{{
// "contentName": stream.GetName(), "contentName": stream.GetName(),
// "contentSize": 0, "contentSize": reportSize,
// // "digest": "5a3231986ce7a6b46e408612d385bafa" // "digest": "5a3231986ce7a6b46e408612d385bafa"
// }}, }},
// }) })
// pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL" pathname = "/orchestration/familyCloud-rebuild/content/v1.0/getFileUploadURL"
return errs.NotImplement
} }
var resp UploadResp var resp UploadResp
_, err := d.post(pathname, data, &resp) _, err = d.post(pathname, data, &resp)
if err != nil { if err != nil {
return err return err
} }
if resp.Data.Result.ResultCode != "0" {
return fmt.Errorf("get file upload url failed with result code: %s, message: %s", resp.Data.Result.ResultCode, resp.Data.Result.ResultDesc)
}
size := stream.GetSize()
// Progress // Progress
p := driver.NewProgress(stream.GetSize(), up) p := driver.NewProgress(size, up)
var partSize = d.getPartSize(size)
var partSize = getPartSize(stream.GetSize()) part := size / partSize
part := (stream.GetSize() + partSize - 1) / partSize if size%partSize > 0 {
if part == 0 { part++
} else if part == 0 {
part = 1 part = 1
} }
rateLimited := driver.NewLimitedUploadStream(ctx, stream)
for i := int64(0); i < part; i++ { for i := int64(0); i < part; i++ {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
start := i * partSize start := i * partSize
byteSize := stream.GetSize() - start byteSize := min(size-start, partSize)
if byteSize > partSize {
byteSize = partSize
}
limitReader := io.LimitReader(stream, byteSize) limitReader := io.LimitReader(rateLimited, byteSize)
// Update Progress // Update Progress
r := io.TeeReader(limitReader, p) r := io.TeeReader(limitReader, p)
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r) req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r)
@ -522,7 +812,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
req = req.WithContext(ctx) req = req.WithContext(ctx)
req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName())) req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName()))
req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10)) req.Header.Set("contentSize", strconv.FormatInt(size, 10))
req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1)) req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1))
req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID) req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID)
req.Header.Set("rangeType", "0") req.Header.Set("rangeType", "0")
@ -532,13 +822,23 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
if err != nil { if err != nil {
return err return err
} }
_ = res.Body.Close()
log.Debugf("%+v", res)
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
res.Body.Close()
return fmt.Errorf("unexpected status code: %d", res.StatusCode) return fmt.Errorf("unexpected status code: %d", res.StatusCode)
} }
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("error reading response body: %v", err)
}
var result InterLayerUploadResult
err = xml.Unmarshal(bodyBytes, &result)
if err != nil {
return fmt.Errorf("error parsing XML: %v", err)
}
if result.ResultCode != 0 {
return fmt.Errorf("upload failed with result code: %d, message: %s", result.ResultCode, result.Msg)
}
} }
return nil return nil
default: default:
return errs.NotImplement return errs.NotImplement
@ -556,7 +856,7 @@ func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{},
} }
switch args.Method { switch args.Method {
case "video_preview": case "video_preview":
uri = "/hcy/videoPreview/getPreviewInfo" uri = "/videoPreview/getPreviewInfo"
default: default:
return nil, errs.NotSupport return nil, errs.NotSupport
} }

View File

@ -1,16 +1,19 @@
package _139 package _139
import ( import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
) )
type Addition struct { type Addition struct {
//Account string `json:"account" required:"true"` //Account string `json:"account" required:"true"`
Authorization string `json:"authorization" type:"text" required:"true"` Authorization string `json:"authorization" type:"text" required:"true"`
driver.RootID driver.RootID
Type string `json:"type" type:"select" options:"personal,family,personal_new" default:"personal"` Type string `json:"type" type:"select" options:"personal_new,family,group,personal" default:"personal_new"`
CloudID string `json:"cloud_id"` 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"`
UseLargeThumbnail bool `json:"use_large_thumbnail" type:"bool" default:"false" help:"Enable to use large thumbnail for images"`
} }
var config = driver.Config{ var config = driver.Config{

View File

@ -7,6 +7,7 @@ import (
const ( const (
MetaPersonal string = "personal" MetaPersonal string = "personal"
MetaFamily string = "family" MetaFamily string = "family"
MetaGroup string = "group"
MetaPersonalNew string = "personal_new" MetaPersonalNew string = "personal_new"
) )
@ -54,6 +55,7 @@ type Content struct {
//ContentDesc string `json:"contentDesc"` //ContentDesc string `json:"contentDesc"`
//ContentType int `json:"contentType"` //ContentType int `json:"contentType"`
//ContentOrigin int `json:"contentOrigin"` //ContentOrigin int `json:"contentOrigin"`
CreateTime string `json:"createTime"`
UpdateTime string `json:"updateTime"` UpdateTime string `json:"updateTime"`
//CommentCount int `json:"commentCount"` //CommentCount int `json:"commentCount"`
ThumbnailURL string `json:"thumbnailURL"` ThumbnailURL string `json:"thumbnailURL"`
@ -141,6 +143,13 @@ type UploadResp struct {
} `json:"data"` } `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 { type CloudContent struct {
ContentID string `json:"contentID"` ContentID string `json:"contentID"`
//Modifier string `json:"modifier"` //Modifier string `json:"modifier"`
@ -196,6 +205,37 @@ type QueryContentListResp struct {
} `json:"data"` } `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 { type PersonalThumbnail struct {
Style string `json:"style"` Style string `json:"style"`
Url string `json:"url"` Url string `json:"url"`
@ -228,6 +268,7 @@ type PersonalUploadResp struct {
BaseResp BaseResp
Data struct { Data struct {
FileId string `json:"fileId"` FileId string `json:"fileId"`
FileName string `json:"fileName"`
PartInfos []PersonalPartInfo `json:"partInfos"` PartInfos []PersonalPartInfo `json:"partInfos"`
Exist bool `json:"exist"` Exist bool `json:"exist"`
RapidUpload bool `json:"rapidUpload"` RapidUpload bool `json:"rapidUpload"`
@ -235,11 +276,39 @@ type PersonalUploadResp struct {
} }
} }
type RefreshTokenResp struct { type PersonalUploadUrlResp struct {
XMLName xml.Name `xml:"root"` BaseResp
Return string `xml:"return"` Data struct {
Token string `xml:"token"` FileId string `json:"fileId"`
Expiretime int32 `xml:"expiretime"` UploadId string `json:"uploadId"`
AccessToken string `xml:"accessToken"` PartInfos []PersonalPartInfo `json:"partInfos"`
Desc string `xml:"desc"` }
}
type QueryRoutePolicyResp struct {
Success bool `json:"success"`
Code string `json:"code"`
Message string `json:"message"`
Data struct {
RoutePolicyList []struct {
SiteID string `json:"siteID"`
SiteCode string `json:"siteCode"`
ModName string `json:"modName"`
HttpUrl string `json:"httpUrl"`
HttpsUrl string `json:"httpsUrl"`
EnvID string `json:"envID"`
ExtInfo string `json:"extInfo"`
HashName string `json:"hashName"`
ModAddrType int `json:"modAddrType"`
} `json:"routePolicyList"`
} `json:"data"`
}
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,16 +6,17 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"path"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/internal/op"
"github.com/alist-org/alist/v3/pkg/utils/random" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/pkg/utils/random"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -54,14 +55,38 @@ func getTime(t string) time.Time {
} }
func (d *Yun139) refreshToken() error { func (d *Yun139) refreshToken() error {
url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do" if d.ref != nil {
var resp RefreshTokenResp return d.ref.refreshToken()
}
decode, err := base64.StdEncoding.DecodeString(d.Authorization) decode, err := base64.StdEncoding.DecodeString(d.Authorization)
if err != nil { if err != nil {
return err return fmt.Errorf("authorization decode failed: %s", err)
} }
decodeStr := string(decode) decodeStr := string(decode)
splits := strings.Split(decodeStr, ":") splits := strings.Split(decodeStr, ":")
if len(splits) < 3 {
return fmt.Errorf("authorization is invalid, splits < 3")
}
d.Account = splits[1]
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>" reqBody := "<root><token>" + splits[2] + "</token><account>" + splits[1] + "</account><clienttype>656</clienttype></root>"
_, err = base.RestyClient.R(). _, err = base.RestyClient.R().
ForceContentType("application/xml"). ForceContentType("application/xml").
@ -99,21 +124,22 @@ func (d *Yun139) request(pathname string, method string, callback base.ReqCallba
req.SetHeaders(map[string]string{ req.SetHeaders(map[string]string{
"Accept": "application/json, text/plain, */*", "Accept": "application/json, text/plain, */*",
"CMS-DEVICE": "default", "CMS-DEVICE": "default",
"Authorization": "Basic " + d.Authorization, "Authorization": "Basic " + d.getAuthorization(),
"mcloud-channel": "1000101", "mcloud-channel": "1000101",
"mcloud-client": "10701", "mcloud-client": "10701",
//"mcloud-route": "001", //"mcloud-route": "001",
"mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign), "mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
//"mcloud-skey":"", //"mcloud-skey":"",
"mcloud-version": "6.6.0", "mcloud-version": "7.14.0",
"Origin": "https://yun.139.com", "Origin": "https://yun.139.com",
"Referer": "https://yun.139.com/w/", "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-DeviceInfo": "||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||",
"x-huawei-channelSrc": "10000034", "x-huawei-channelSrc": "10000034",
"x-inner-ntwk": "2", "x-inner-ntwk": "2",
"x-m4c-caller": "PC", "x-m4c-caller": "PC",
"x-m4c-src": "10002", "x-m4c-src": "10002",
"x-SvcType": svcType, "x-SvcType": svcType,
"Inner-Hcy-Router-Https": "1",
}) })
var e BaseResp var e BaseResp
@ -131,6 +157,64 @@ func (d *Yun139) request(pathname string, method string, callback base.ReqCallba
} }
return res.Body(), nil return res.Body(), nil
} }
func (d *Yun139) requestRoute(data interface{}, resp interface{}) ([]byte, error) {
url := "https://user-njs.yun.139.com/user/route/qryRoutePolicy"
req := base.RestyClient.R()
randStr := random.String(16)
ts := time.Now().Format("2006-01-02 15:04:05")
callback := func(req *resty.Request) {
req.SetBody(data)
}
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, */*",
"CMS-DEVICE": "default",
"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": "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
req.SetResult(&e)
res, err := req.Execute(http.MethodPost, url)
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) post(pathname string, data interface{}, resp interface{}) ([]byte, error) { func (d *Yun139) post(pathname string, data interface{}, resp interface{}) ([]byte, error) {
return d.request(pathname, http.MethodPost, func(req *resty.Request) { return d.request(pathname, http.MethodPost, func(req *resty.Request) {
req.SetBody(data) req.SetBody(data)
@ -151,7 +235,7 @@ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
"catalogSortType": 0, "catalogSortType": 0,
"contentSortType": 0, "contentSortType": 0,
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
} }
@ -199,7 +283,7 @@ func (d *Yun139) newJson(data map[string]interface{}) base.Json {
"cloudID": d.CloudID, "cloudID": d.CloudID,
"cloudType": 1, "cloudType": 1,
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
} }
@ -220,10 +304,11 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
"sortDirection": 1, "sortDirection": 1,
}) })
var resp QueryContentListResp 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 { if err != nil {
return nil, err return nil, err
} }
path := resp.Data.Path
for _, catalog := range resp.Data.CloudCatalogList { for _, catalog := range resp.Data.CloudCatalogList {
f := model.Object{ f := model.Object{
ID: catalog.CatalogID, ID: catalog.CatalogID,
@ -232,6 +317,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
IsFolder: true, IsFolder: true,
Modified: getTime(catalog.LastUpdateTime), Modified: getTime(catalog.LastUpdateTime),
Ctime: getTime(catalog.CreateTime), Ctime: getTime(catalog.CreateTime),
Path: path, // 文件夹上一级的Path
} }
files = append(files, &f) files = append(files, &f)
} }
@ -243,13 +329,14 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
Size: content.ContentSize, Size: content.ContentSize,
Modified: getTime(content.LastUpdateTime), Modified: getTime(content.LastUpdateTime),
Ctime: getTime(content.CreateTime), Ctime: getTime(content.CreateTime),
Path: path, // 文件所在目录的Path
}, },
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL}, Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
//Thumbnail: content.BigthumbnailURL, //Thumbnail: content.BigthumbnailURL,
} }
files = append(files, &f) files = append(files, &f)
} }
if 100*pageNum > resp.Data.TotalCount { if resp.Data.TotalCount == 0 {
break break
} }
pageNum++ pageNum++
@ -257,12 +344,67 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
return files, nil 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) { func (d *Yun139) getLink(contentId string) (string, error) {
data := base.Json{ data := base.Json{
"appName": "", "appName": "",
"contentID": contentId, "contentID": contentId,
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.getAccount(),
"accountType": 1, "accountType": 1,
}, },
} }
@ -273,6 +415,32 @@ func (d *Yun139) getLink(contentId string) (string, error) {
} }
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil 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 { func unicode(str string) string {
textQuoted := strconv.QuoteToASCII(str) textQuoted := strconv.QuoteToASCII(str)
@ -281,7 +449,7 @@ func unicode(str string) string {
} }
func (d *Yun139) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { func (d *Yun139) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
url := "https://personal-kd-njs.yun.139.com" + pathname url := d.getPersonalCloudHost() + pathname
req := base.RestyClient.R() req := base.RestyClient.R()
randStr := random.String(16) randStr := random.String(16)
ts := time.Now().Format("2006-01-02 15:04:05") ts := time.Now().Format("2006-01-02 15:04:05")
@ -299,17 +467,15 @@ func (d *Yun139) personalRequest(pathname string, method string, callback base.R
} }
req.SetHeaders(map[string]string{ req.SetHeaders(map[string]string{
"Accept": "application/json, text/plain, */*", "Accept": "application/json, text/plain, */*",
"Authorization": "Basic " + d.Authorization, "Authorization": "Basic " + d.getAuthorization(),
"Caller": "web", "Caller": "web",
"Cms-Device": "default", "Cms-Device": "default",
"Mcloud-Channel": "1000101", "Mcloud-Channel": "1000101",
"Mcloud-Client": "10701", "Mcloud-Client": "10701",
"Mcloud-Route": "001", "Mcloud-Route": "001",
"Mcloud-Sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign), "Mcloud-Sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
"Mcloud-Version": "7.13.0", "Mcloud-Version": "7.14.0",
"Origin": "https://yun.139.com", "x-DeviceInfo": "||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||",
"Referer": "https://yun.139.com/w/",
"x-DeviceInfo": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||",
"x-huawei-channelSrc": "10000034", "x-huawei-channelSrc": "10000034",
"x-inner-ntwk": "2", "x-inner-ntwk": "2",
"x-m4c-caller": "PC", "x-m4c-caller": "PC",
@ -318,7 +484,7 @@ func (d *Yun139) personalRequest(pathname string, method string, callback base.R
"X-Yun-Api-Version": "v1", "X-Yun-Api-Version": "v1",
"X-Yun-App-Channel": "10000034", "X-Yun-App-Channel": "10000034",
"X-Yun-Channel-Source": "10000034", "X-Yun-Channel-Source": "10000034",
"X-Yun-Client-Info": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||dW5kZWZpbmVk||", "X-Yun-Client-Info": "||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||dW5kZWZpbmVk||",
"X-Yun-Module-Type": "100", "X-Yun-Module-Type": "100",
"X-Yun-Svc-Type": "1", "X-Yun-Svc-Type": "1",
}) })
@ -370,7 +536,7 @@ func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
"parentFileId": fileId, "parentFileId": fileId,
} }
var resp PersonalListResp var resp PersonalListResp
_, err := d.personalPost("/hcy/file/list", data, &resp) _, err := d.personalPost("/file/list", data, &resp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -390,7 +556,15 @@ func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
} else { } else {
var Thumbnails = item.Thumbnails var Thumbnails = item.Thumbnails
var ThumbnailUrl string var ThumbnailUrl string
if len(Thumbnails) > 0 { if d.UseLargeThumbnail {
for _, thumb := range Thumbnails {
if strings.Contains(thumb.Style, "Large") {
ThumbnailUrl = thumb.Url
break
}
}
}
if ThumbnailUrl == "" && len(Thumbnails) > 0 {
ThumbnailUrl = Thumbnails[len(Thumbnails)-1].Url ThumbnailUrl = Thumbnails[len(Thumbnails)-1].Url
} }
f = &model.ObjThumb{ f = &model.ObjThumb{
@ -418,7 +592,7 @@ func (d *Yun139) personalGetLink(fileId string) (string, error) {
data := base.Json{ data := base.Json{
"fileId": fileId, "fileId": fileId,
} }
res, err := d.personalPost("/hcy/file/getDownloadUrl", res, err := d.personalPost("/file/getDownloadUrl",
data, nil) data, nil)
if err != nil { if err != nil {
return "", err return "", err
@ -430,3 +604,22 @@ func (d *Yun139) personalGetLink(fileId string) (string, error) {
return jsoniter.Get(res, "data", "url").ToString(), nil 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
}
func (d *Yun139) getPersonalCloudHost() string {
if d.ref != nil {
return d.ref.getPersonalCloudHost()
}
return d.PersonalCloudHost
}

View File

@ -5,10 +5,10 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/alist-org/alist/v3/drivers/base" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -18,7 +18,7 @@ import (
"strconv" "strconv"
"strings" "strings"
myrand "github.com/alist-org/alist/v3/pkg/utils/random" myrand "github.com/OpenListTeam/OpenList/pkg/utils/random"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -4,7 +4,7 @@ import (
"errors" "errors"
"strconv" "strconv"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -1,8 +1,8 @@
package _189 package _189
import ( import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
) )
type Addition struct { type Addition struct {

View File

@ -15,11 +15,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
myrand "github.com/alist-org/alist/v3/pkg/utils/random" myrand "github.com/OpenListTeam/OpenList/pkg/utils/random"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -365,7 +365,7 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
log.Debugf("uploadData: %+v", uploadData) log.Debugf("uploadData: %+v", uploadData)
requestURL := uploadData.RequestURL requestURL := uploadData.RequestURL
uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&") 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 { if err != nil {
return err return err
} }
@ -375,11 +375,11 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
req.Header.Set(v[0:i], v[i+1:]) req.Header.Set(v[0:i], v[i+1:])
} }
r, err := base.HttpClient.Do(req) r, err := base.HttpClient.Do(req)
log.Debugf("%+v %+v", r, r.Request.Header)
r.Body.Close()
if err != nil { if err != nil {
return err return err
} }
log.Debugf("%+v %+v", r, r.Request.Header)
_ = r.Body.Close()
up(float64(i) * 100 / float64(count)) up(float64(i) * 100 / float64(count))
} }
fileMd5 := hex.EncodeToString(md5Sum.Sum(nil)) fileMd5 := hex.EncodeToString(md5Sum.Sum(nil))

View File

@ -1,19 +1,20 @@
package _189pc package _189pc
import ( import (
"container/ring"
"context" "context"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/OpenListTeam/OpenList/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/google/uuid"
) )
type Cloud189PC struct { type Cloud189PC struct {
@ -29,10 +30,11 @@ type Cloud189PC struct {
uploadThread int uploadThread int
familyTransferFolder *ring.Ring familyTransferFolder *Cloud189Folder
cleanFamilyTransferFile func() cleanFamilyTransferFile func()
storageConfig driver.Config storageConfig driver.Config
ref *Cloud189PC
} }
func (y *Cloud189PC) Config() driver.Config { func (y *Cloud189PC) Config() driver.Config {
@ -47,9 +49,18 @@ func (y *Cloud189PC) GetAddition() driver.Additional {
} }
func (y *Cloud189PC) Init(ctx context.Context) (err error) { func (y *Cloud189PC) Init(ctx context.Context) (err error) {
// 兼容旧上传接口 y.storageConfig = config
y.storageConfig.NoOverwriteUpload = y.isFamily() && (y.Addition.RapidUpload || y.Addition.UploadMethod == "old") 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" { if y.isFamily() && y.RootFolderID == "-11" {
y.RootFolderID = "" y.RootFolderID = ""
@ -64,20 +75,22 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
y.uploadThread, y.UploadThread = 3, "3" y.uploadThread, y.UploadThread = 3, "3"
} }
// 初始化请求客户端 if y.ref == nil {
if y.client == nil { // 初始化请求客户端
y.client = base.NewRestyClient().SetHeaders(map[string]string{ if y.client == nil {
"Accept": "application/json;charset=UTF-8", y.client = base.NewRestyClient().SetHeaders(map[string]string{
"Referer": WEB_URL, "Accept": "application/json;charset=UTF-8",
}) "Referer": WEB_URL,
} })
}
// 避免重复登陆 // 避免重复登陆
identity := utils.GetMD5EncodeStr(y.Username + y.Password) identity := utils.GetMD5EncodeStr(y.Username + y.Password)
if !y.isLogin() || y.identity != identity { if !y.isLogin() || y.identity != identity {
y.identity = identity y.identity = identity
if err = y.login(); err != nil { if err = y.login(); err != nil {
return return
}
} }
} }
@ -88,13 +101,14 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
} }
} }
// 创建中转文件夹,防止重名文件 // 创建中转文件夹
if y.FamilyTransfer { if y.FamilyTransfer {
if y.familyTransferFolder, err = y.createFamilyTransferFolder(32); err != nil { if err := y.createFamilyTransferFolder(); err != nil {
return err return err
} }
} }
// 清理转存文件节流
y.cleanFamilyTransferFile = utils.NewThrottle2(time.Minute, func() { y.cleanFamilyTransferFile = utils.NewThrottle2(time.Minute, func() {
if err := y.cleanFamilyTransfer(context.TODO()); err != nil { if err := y.cleanFamilyTransfer(context.TODO()); err != nil {
utils.Log.Errorf("cleanFamilyTransferFolderError:%s", err) utils.Log.Errorf("cleanFamilyTransferFolderError:%s", err)
@ -103,7 +117,17 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
return 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 { func (y *Cloud189PC) Drop(ctx context.Context) error {
y.ref = nil
return nil return nil
} }
@ -314,35 +338,49 @@ func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
if !isFamily && y.FamilyTransfer { if !isFamily && y.FamilyTransfer {
// 修改上传目标为家庭云文件夹 // 修改上传目标为家庭云文件夹
transferDstDir := dstDir transferDstDir := dstDir
dstDir = (y.familyTransferFolder.Value).(*Cloud189Folder) dstDir = y.familyTransferFolder
y.familyTransferFolder = y.familyTransferFolder.Next()
// 使用临时文件名
srcName := stream.GetName()
stream = &WrapFileStreamer{
FileStreamer: stream,
Name: fmt.Sprintf("0%s.transfer", uuid.NewString()),
}
// 使用家庭云上传
isFamily = true isFamily = true
overwrite = false overwrite = false
defer func() { defer func() {
if newObj != nil { if newObj != nil {
// 批量任务有概率删不掉
y.cleanFamilyTransferFile()
// 转存家庭云文件到个人云 // 转存家庭云文件到个人云
err = y.SaveFamilyFileToPersonCloud(context.TODO(), y.FamilyID, newObj, transferDstDir, true) err = y.SaveFamilyFileToPersonCloud(context.TODO(), y.FamilyID, newObj, transferDstDir, true)
// 删除家庭云源文件
task := BatchTaskInfo{ go y.Delete(context.TODO(), y.FamilyID, newObj)
FileId: newObj.GetID(), // 批量任务有概率删不掉
FileName: newObj.GetName(), go y.cleanFamilyTransferFile()
IsFolder: BoolToNumber(newObj.IsDir()), // 转存失败返回错误
if err != nil {
return
} }
// 删除源文件 // 查找转存文件
if resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, task); err == nil { var file *Cloud189File
y.WaitBatchTask("DELETE", resp.TaskID, time.Second) file, err = y.findFileByName(context.TODO(), newObj.GetName(), transferDstDir.GetID(), false)
// 永久删除 if err != nil {
if resp, err := y.CreateBatchTask("CLEAR_RECYCLE", y.FamilyID, "", nil, task); err == nil { if err == errs.ObjectNotFound {
y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second) err = fmt.Errorf("unknown error: No transfer file obtained %s", newObj.GetName())
} }
return
} }
newObj = nil
// 重命名转存文件
newObj, err = y.Rename(context.TODO(), file, srcName)
if err != nil {
// 重命名失败删除源文件
_ = y.Delete(context.TODO(), "", file)
}
return
} }
}() }()
} }

View File

@ -18,7 +18,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/pkg/utils/random" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils/random"
) )
func clientSuffix() map[string]string { func clientSuffix() map[string]string {
@ -208,3 +209,12 @@ func IF[V any](o bool, t V, f V) V {
} }
return f return f
} }
type WrapFileStreamer struct {
model.FileStreamer
Name string
}
func (w *WrapFileStreamer) GetName() string {
return w.Name
}

View File

@ -1,8 +1,8 @@
package _189pc package _189pc
import ( import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
) )
type Addition struct { type Addition struct {

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/pkg/utils"
) )
// 居然有四种返回方式 // 居然有四种返回方式

View File

@ -2,32 +2,32 @@ package _189pc
import ( import (
"bytes" "bytes"
"container/ring"
"context" "context"
"crypto/md5"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
"math"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"os"
"regexp" "regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/OpenListTeam/OpenList/drivers/base"
"github.com/alist-org/alist/v3/internal/conf" "github.com/OpenListTeam/OpenList/internal/conf"
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/errs"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/internal/setting" "github.com/OpenListTeam/OpenList/internal/op"
"github.com/alist-org/alist/v3/pkg/errgroup" "github.com/OpenListTeam/OpenList/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/internal/stream"
"github.com/OpenListTeam/OpenList/pkg/errgroup"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/avast/retry-go" "github.com/avast/retry-go"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
@ -57,11 +57,11 @@ const (
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string { func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
dateOfGmt := getHttpDateStr() dateOfGmt := getHttpDateStr()
sessionKey := y.tokenInfo.SessionKey sessionKey := y.getTokenInfo().SessionKey
sessionSecret := y.tokenInfo.SessionSecret sessionSecret := y.getTokenInfo().SessionSecret
if isFamily { if isFamily {
sessionKey = y.tokenInfo.FamilySessionKey sessionKey = y.getTokenInfo().FamilySessionKey
sessionSecret = y.tokenInfo.FamilySessionSecret sessionSecret = y.getTokenInfo().FamilySessionSecret
} }
header := map[string]string{ header := map[string]string{
@ -74,9 +74,9 @@ func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool)
} }
func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string { func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
sessionSecret := y.tokenInfo.SessionSecret sessionSecret := y.getTokenInfo().SessionSecret
if isFamily { if isFamily {
sessionSecret = y.tokenInfo.FamilySessionSecret sessionSecret = y.getTokenInfo().FamilySessionSecret
} }
if params != nil { if params != nil {
return AesECBEncrypt(params.Encode(), sessionSecret[:16]) return AesECBEncrypt(params.Encode(), sessionSecret[:16])
@ -85,7 +85,7 @@ func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
} }
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) { func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) {
req := y.client.R().SetQueryParams(clientSuffix()) req := y.getClient().R().SetQueryParams(clientSuffix())
// 设置params // 设置params
paramsData := y.EncryptParams(params, isBool(isFamily...)) paramsData := y.EncryptParams(params, isBool(isFamily...))
@ -174,8 +174,8 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
} }
var erron RespErr var erron RespErr
jsoniter.Unmarshal(body, &erron) _ = jsoniter.Unmarshal(body, &erron)
xml.Unmarshal(body, &erron) _ = xml.Unmarshal(body, &erron)
if erron.HasError() { if erron.HasError() {
return nil, &erron return nil, &erron
} }
@ -185,39 +185,9 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
return body, nil return body, nil
} }
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) { func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
fullUrl := API_URL res := make([]model.Obj, 0, 100)
if isFamily {
fullUrl += "/family/file"
}
fullUrl += "/listFiles.action"
res := make([]model.Obj, 0, 130)
for pageNum := 1; ; pageNum++ { for pageNum := 1; ; pageNum++ {
var resp Cloud189FilesResp resp, err := y.getFilesWithPage(ctx, fileId, isFamily, pageNum, 1000, y.OrderBy, y.OrderDirection)
_, 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 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, isFamily)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -236,6 +206,63 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool)
return res, nil 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) { func (y *Cloud189PC) login() (err error) {
// 初始化登陆所需参数 // 初始化登陆所需参数
if y.loginParam == nil { if y.loginParam == nil {
@ -403,6 +430,9 @@ func (y *Cloud189PC) initLoginParam() error {
// 刷新会话 // 刷新会话
func (y *Cloud189PC) refreshSession() (err error) { func (y *Cloud189PC) refreshSession() (err error) {
if y.ref != nil {
return y.ref.refreshSession()
}
var erron RespErr var erron RespErr
var userSessionResp UserSessionResp var userSessionResp UserSessionResp
_, err = y.client.R(). _, err = y.client.R().
@ -441,12 +471,8 @@ func (y *Cloud189PC) refreshSession() (err error) {
// 普通上传 // 普通上传
// 无法上传大小为0的文件 // 无法上传大小为0的文件
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (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()) size := file.GetSize()
count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize))) sliceSize := partSize(size)
lastPartSize := file.GetSize() % sliceSize
if file.GetSize() > 0 && lastPartSize == 0 {
lastPartSize = sliceSize
}
params := Params{ params := Params{
"parentFolderId": dstDir.GetID(), "parentFolderId": dstDir.GetID(),
@ -479,23 +505,29 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
retry.Delay(time.Second), retry.Delay(time.Second),
retry.DelayType(retry.BackOffDelay)) retry.DelayType(retry.BackOffDelay))
fileMd5 := md5.New() count := int(size / sliceSize)
silceMd5 := md5.New() lastPartSize := size % sliceSize
if lastPartSize > 0 {
count++
} else {
lastPartSize = sliceSize
}
fileMd5 := utils.MD5.NewFunc()
silceMd5 := utils.MD5.NewFunc()
silceMd5Hexs := make([]string, 0, count) silceMd5Hexs := make([]string, 0, count)
teeReader := io.TeeReader(file, io.MultiWriter(fileMd5, silceMd5))
byteSize := sliceSize
for i := 1; i <= count; i++ { for i := 1; i <= count; i++ {
if utils.IsCanceled(upCtx) { if utils.IsCanceled(upCtx) {
break break
} }
byteData := make([]byte, sliceSize)
if i == count { if i == count {
byteData = byteData[:lastPartSize] byteSize = lastPartSize
} }
byteData := make([]byte, byteSize)
// 读取块 // 读取块
silceMd5.Reset() silceMd5.Reset()
if _, err := io.ReadFull(io.TeeReader(file, io.MultiWriter(fileMd5, silceMd5)), byteData); err != io.EOF && err != nil { if _, err := io.ReadFull(teeReader, byteData); err != io.EOF && err != nil {
return nil, err return nil, err
} }
@ -512,7 +544,8 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
// step.4 上传切片 // step.4 上传切片
uploadUrl := uploadUrls[0] uploadUrl := uploadUrls[0]
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData), isFamily) _, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false,
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)), isFamily)
if err != nil { if err != nil {
return err return err
} }
@ -569,24 +602,43 @@ func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream m
// 快传 // 快传
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (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() var (
if err != nil { cache = file.GetFile()
return nil, err tmpF *os.File
err error
)
size := file.GetSize()
if _, ok := cache.(io.ReaderAt); !ok && size > 0 {
tmpF, err = os.CreateTemp(conf.Conf.TempDir, "file-*")
if err != nil {
return nil, err
}
defer func() {
_ = tmpF.Close()
_ = os.Remove(tmpF.Name())
}()
cache = tmpF
} }
sliceSize := partSize(size)
var sliceSize = partSize(file.GetSize()) count := int(size / sliceSize)
count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize))) lastSliceSize := size % sliceSize
lastSliceSize := file.GetSize() % sliceSize if lastSliceSize > 0 {
if file.GetSize() > 0 && lastSliceSize == 0 { count++
} else {
lastSliceSize = sliceSize lastSliceSize = sliceSize
} }
//step.1 优先计算所需信息 //step.1 优先计算所需信息
byteSize := sliceSize byteSize := sliceSize
fileMd5 := md5.New() fileMd5 := utils.MD5.NewFunc()
silceMd5 := md5.New() sliceMd5 := utils.MD5.NewFunc()
silceMd5Hexs := make([]string, 0, count) sliceMd5Hexs := make([]string, 0, count)
partInfos := make([]string, 0, count) partInfos := make([]string, 0, count)
writers := []io.Writer{fileMd5, sliceMd5}
if tmpF != nil {
writers = append(writers, tmpF)
}
written := int64(0)
for i := 1; i <= count; i++ { for i := 1; i <= count; i++ {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return nil, ctx.Err() return nil, ctx.Err()
@ -596,19 +648,31 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
byteSize = lastSliceSize byteSize = lastSliceSize
} }
silceMd5.Reset() n, err := utils.CopyWithBufferN(io.MultiWriter(writers...), file, byteSize)
if _, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF { written += n
if err != nil && err != io.EOF {
return nil, err return nil, err
} }
md5Byte := silceMd5.Sum(nil) md5Byte := sliceMd5.Sum(nil)
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte))) sliceMd5Hexs = append(sliceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte)))
partInfos = append(partInfos, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte))) partInfos = append(partInfos, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte)))
sliceMd5.Reset()
}
if tmpF != nil {
if size > 0 && written != size {
return nil, errs.NewErr(err, "CreateTempFile failed, incoming stream actual size= %d, expect = %d ", written, size)
}
_, err = tmpF.Seek(0, io.SeekStart)
if err != nil {
return nil, errs.NewErr(err, "CreateTempFile failed, can't seek to 0 ")
}
} }
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil))) fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
sliceMd5Hex := fileMd5Hex sliceMd5Hex := fileMd5Hex
if file.GetSize() > sliceSize { if size > sliceSize {
sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(silceMd5Hexs, "\n"))) sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(sliceMd5Hexs, "\n")))
} }
fullUrl := UPLOAD_URL fullUrl := UPLOAD_URL
@ -620,7 +684,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 { if !ok {
//step.2 预上传 //step.2 预上传
params := Params{ params := Params{
@ -674,7 +738,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
} }
// step.4 上传切片 // step.4 上传切片
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize), isFamily) _, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(cache, offset, byteSize), isFamily)
if err != nil { if err != nil {
return err return err
} }
@ -687,7 +751,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
if err = threadG.Wait(); err != nil { if err = threadG.Wait(); err != nil {
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
uploadProgress.UploadParts = utils.SliceFilter(uploadProgress.UploadParts, func(s string) bool { return s != "" }) 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 return nil, err
} }
@ -756,14 +820,11 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uplo
// 旧版本上传,家庭云不支持覆盖 // 旧版本上传,家庭云不支持覆盖
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (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() tempFile, fileMd5, err := stream.CacheFullInTempFileAndHash(file, utils.MD5)
if err != nil {
return nil, err
}
fileMd5, err := utils.HashFile(utils.MD5, tempFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rateLimited := driver.NewLimitedUploadStream(ctx, io.NopCloser(tempFile))
// 创建上传会话 // 创建上传会话
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily) uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily)
@ -790,7 +851,7 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId) header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
} }
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile), isFamily) _, err := y.put(ctx, status.FileUploadUrl, header, true, rateLimited, isFamily)
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" { if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
return nil, err return nil, err
} }
@ -899,8 +960,7 @@ func (y *Cloud189PC) isLogin() bool {
} }
// 创建家庭云中转文件夹 // 创建家庭云中转文件夹
func (y *Cloud189PC) createFamilyTransferFolder(count int) (*ring.Ring, error) { func (y *Cloud189PC) createFamilyTransferFolder() error {
folders := ring.New(count)
var rootFolder Cloud189Folder var rootFolder Cloud189Folder
_, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) { _, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) {
req.SetQueryParams(map[string]string{ req.SetQueryParams(map[string]string{
@ -909,81 +969,61 @@ func (y *Cloud189PC) createFamilyTransferFolder(count int) (*ring.Ring, error) {
}) })
}, &rootFolder, true) }, &rootFolder, true)
if err != nil { if err != nil {
return nil, err return err
} }
y.familyTransferFolder = &rootFolder
folderCount := 0 return nil
// 获取已有目录
files, err := y.getFiles(context.TODO(), rootFolder.GetID(), true)
if err != nil {
return nil, err
}
for _, file := range files {
if folder, ok := file.(*Cloud189Folder); ok {
folders.Value = folder
folders = folders.Next()
folderCount++
}
}
// 创建新的目录
for folderCount < count {
var newFolder Cloud189Folder
_, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"folderName": uuid.NewString(),
"familyId": y.FamilyID,
"parentId": rootFolder.GetID(),
})
}, &newFolder, true)
if err != nil {
return nil, err
}
folders.Value = &newFolder
folders = folders.Next()
folderCount++
}
return folders, nil
} }
// 清理中转文件夹 // 清理中转文件夹
func (y *Cloud189PC) cleanFamilyTransfer(ctx context.Context) error { func (y *Cloud189PC) cleanFamilyTransfer(ctx context.Context) error {
var tasks []BatchTaskInfo transferFolderId := y.familyTransferFolder.GetID()
r := y.familyTransferFolder for pageNum := 1; ; pageNum++ {
for p := r.Next(); p != r; p = p.Next() { resp, err := y.getFilesWithPage(ctx, transferFolderId, true, pageNum, 100, "lastOpTime", "asc")
folder := p.Value.(*Cloud189Folder)
files, err := y.getFiles(ctx, folder.GetID(), true)
if err != nil { if err != nil {
return err return err
} }
for _, file := range files { // 获取完毕跳出
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{ tasks = append(tasks, BatchTaskInfo{
FileId: file.GetID(), FileId: file.GetID(),
FileName: file.GetName(), FileName: file.GetName(),
IsFolder: BoolToNumber(file.IsDir()), IsFolder: BoolToNumber(file.IsDir()),
}) })
} }
}
if len(tasks) > 0 { if len(tasks) > 0 {
// 删除 // 删除
resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, tasks...) resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, tasks...)
if err != nil { 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 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 return nil
} }
@ -1008,7 +1048,7 @@ func (y *Cloud189PC) getFamilyID() (string, error) {
return "", fmt.Errorf("cannot get automatically,please input family_id") return "", fmt.Errorf("cannot get automatically,please input family_id")
} }
for _, info := range infos { 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(info.FamilyID), nil
} }
} }
@ -1060,6 +1100,34 @@ func (y *Cloud189PC) SaveFamilyFileToPersonCloud(ctx context.Context, familyId s
} }
} }
// 永久删除文件
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) { func (y *Cloud189PC) CreateBatchTask(aType string, familyID string, targetFolderId string, other map[string]string, taskInfos ...BatchTaskInfo) (*CreateBatchTaskResp, error) {
var resp CreateBatchTaskResp var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) { _, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
@ -1142,3 +1210,17 @@ func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration)
time.Sleep(t) 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,13 +3,16 @@ package alias
import ( import (
"context" "context"
"errors" "errors"
"io"
stdpath "path"
"strings" "strings"
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/OpenListTeam/OpenList/internal/errs"
"github.com/alist-org/alist/v3/internal/fs" "github.com/OpenListTeam/OpenList/internal/fs"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/internal/stream"
"github.com/OpenListTeam/OpenList/pkg/utils"
) )
type Alias struct { type Alias struct {
@ -110,16 +113,77 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
for _, dst := range dsts { for _, dst := range dsts {
link, err := d.link(ctx, dst, sub, args) link, err := d.link(ctx, dst, sub, args)
if err == nil { 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 link, nil
} }
} }
return nil, errs.ObjectNotFound return nil, errs.ObjectNotFound
} }
func (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) error { func (d *Alias) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
reqPath, err := d.getReqPath(ctx, srcObj) if !d.Writable {
return errs.PermissionDenied
}
reqPath, err := d.getReqPath(ctx, parentDir, true)
if err == nil { if err == nil {
return fs.Rename(ctx, *reqPath, newName) for _, path := range reqPath {
err = errors.Join(err, fs.MakeDir(ctx, stdpath.Join(*path, dirName)))
}
return err
}
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
}
if len(srcPath) == len(dstPath) {
for i := range srcPath {
err = errors.Join(err, fs.Move(ctx, *srcPath[i], *dstPath[i]))
}
return err
} else {
return errors.New("parallel paths mismatch")
}
}
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 {
for _, path := range reqPath {
err = errors.Join(err, fs.Rename(ctx, *path, newName))
}
return err
} }
if errs.IsNotImplement(err) { if errs.IsNotImplement(err) {
return errors.New("same-name files cannot be Rename") return errors.New("same-name files cannot be Rename")
@ -127,10 +191,51 @@ func (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) er
return err 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
}
if len(srcPath) == len(dstPath) {
for i := range srcPath {
_, e := fs.Copy(ctx, *srcPath[i], *dstPath[i])
err = errors.Join(err, e)
}
return err
} else if len(srcPath) == 1 || !d.ProtectSameName {
for _, path := range dstPath {
_, e := fs.Copy(ctx, *srcPath[0], *path)
err = errors.Join(err, e)
}
return err
} else {
return errors.New("parallel paths mismatch")
}
}
func (d *Alias) Remove(ctx context.Context, obj model.Obj) error { func (d *Alias) Remove(ctx context.Context, obj model.Obj) error {
reqPath, err := d.getReqPath(ctx, obj) if !d.Writable {
return errs.PermissionDenied
}
reqPath, err := d.getReqPath(ctx, obj, false)
if err == nil { if err == nil {
return fs.Remove(ctx, *reqPath) for _, path := range reqPath {
err = errors.Join(err, fs.Remove(ctx, *path))
}
return err
} }
if errs.IsNotImplement(err) { if errs.IsNotImplement(err) {
return errors.New("same-name files cannot be Delete") return errors.New("same-name files cannot be Delete")
@ -138,4 +243,147 @@ func (d *Alias) Remove(ctx context.Context, obj model.Obj) error {
return err 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 {
if len(reqPath) == 1 {
return fs.PutDirectly(ctx, *reqPath[0], s)
} else {
defer s.Close()
file, err := s.CacheFullInTempFile()
if err != nil {
return err
}
for _, path := range reqPath {
err = errors.Join(err, fs.PutDirectly(ctx, *path, &stream.FileStream{
Obj: s,
Mimetype: s.GetMimetype(),
WebPutAsTask: s.NeedStore(),
Reader: file,
}))
_, e := file.Seek(0, io.SeekStart)
if e != nil {
return errors.Join(err, e)
}
}
return err
}
}
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 {
for _, path := range reqPath {
err = errors.Join(err, fs.PutURL(ctx, *path, name, url))
}
return err
}
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
}
if len(srcPath) == len(dstPath) {
for i := range srcPath {
_, e := fs.ArchiveDecompress(ctx, *srcPath[i], *dstPath[i], args)
err = errors.Join(err, e)
}
return err
} else if len(srcPath) == 1 || !d.ProtectSameName {
for _, path := range dstPath {
_, e := fs.ArchiveDecompress(ctx, *srcPath[0], *path, args)
err = errors.Join(err, e)
}
return err
} else {
return errors.New("parallel paths mismatch")
}
}
var _ driver.Driver = (*Alias)(nil) var _ driver.Driver = (*Alias)(nil)

View File

@ -1,23 +1,27 @@
package alias package alias
import ( import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/op" "github.com/OpenListTeam/OpenList/internal/op"
) )
type Addition struct { type Addition struct {
// Usually one of two // Usually one of two
// driver.RootPath // driver.RootPath
// define other // define other
Paths string `json:"paths" required:"true" type:"text"` Paths string `json:"paths" required:"true" type:"text"`
ProtectSameName bool `json:"protect_same_name" default:"true" required:"false" help:"Protects same-name files from Delete or Rename"` ProtectSameName bool `json:"protect_same_name" default:"true" required:"false" help:"Protects same-name files from Delete or Rename"`
ParallelWrite bool `json:"parallel_write" type:"bool" default:"false"`
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{ var config = driver.Config{
Name: "Alias", Name: "Alias",
LocalSort: true, LocalSort: true,
NoCache: true, NoCache: true,
NoUpload: true, NoUpload: false,
DefaultRoot: "/", DefaultRoot: "/",
ProxyRangeOption: true, ProxyRangeOption: true,
} }

View File

@ -3,15 +3,18 @@ package alias
import ( import (
"context" "context"
"fmt" "fmt"
"net/url"
stdpath "path" stdpath "path"
"strings" "strings"
"github.com/alist-org/alist/v3/internal/errs" "github.com/OpenListTeam/OpenList/internal/driver"
"github.com/alist-org/alist/v3/internal/fs" "github.com/OpenListTeam/OpenList/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/OpenListTeam/OpenList/internal/fs"
"github.com/alist-org/alist/v3/internal/sign" "github.com/OpenListTeam/OpenList/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/OpenListTeam/OpenList/internal/op"
"github.com/alist-org/alist/v3/server/common" "github.com/OpenListTeam/OpenList/internal/sign"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/server/common"
) )
func (d *Alias) listRoot() []model.Obj { func (d *Alias) listRoot() []model.Obj {
@ -62,6 +65,7 @@ func (d *Alias) get(ctx context.Context, path string, dst, sub string) (model.Ob
Size: obj.GetSize(), Size: obj.GetSize(),
Modified: obj.ModTime(), Modified: obj.ModTime(),
IsFolder: obj.IsDir(), IsFolder: obj.IsDir(),
HashInfo: obj.GetHash(),
}, nil }, nil
} }
@ -94,10 +98,15 @@ func (d *Alias) list(ctx context.Context, dst, sub string, args *fs.ListArgs) ([
func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) { func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) {
reqPath := stdpath.Join(dst, sub) reqPath := stdpath.Join(dst, sub)
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{}) // 参考 crypt 驱动
storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)
if err != nil { if err != nil {
return nil, err 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}) _, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
if err != nil { if err != nil {
return nil, err return nil, err
@ -114,38 +123,109 @@ func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs)
} }
return link, nil return link, nil
} }
link, _, err := fs.Link(ctx, reqPath, args) link, _, err := op.Link(ctx, storage, reqActualPath, args)
return link, err return link, err
} }
func (d *Alias) getReqPath(ctx context.Context, obj model.Obj) (*string, error) { func (d *Alias) getReqPath(ctx context.Context, obj model.Obj, isParent bool) ([]*string, error) {
root, sub := d.getRootAndPath(obj.GetPath()) root, sub := d.getRootAndPath(obj.GetPath())
if sub == "" { if sub == "" && !isParent {
return nil, errs.NotSupport return nil, errs.NotSupport
} }
dsts, ok := d.pathMap[root] dsts, ok := d.pathMap[root]
all := true
if !ok { if !ok {
return nil, errs.ObjectNotFound return nil, errs.ObjectNotFound
} }
var reqPath *string var reqPath []*string
for _, dst := range dsts { for _, dst := range dsts {
path := stdpath.Join(dst, sub) path := stdpath.Join(dst, sub)
_, err := fs.Get(ctx, path, &fs.GetArgs{NoLog: true}) _, err := fs.Get(ctx, path, &fs.GetArgs{NoLog: true})
if err != nil { if err != nil {
all = false
if d.ProtectSameName && d.ParallelWrite && len(reqPath) >= 2 {
return nil, errs.NotImplement
}
continue continue
} }
if !d.ProtectSameName { if !d.ProtectSameName && !d.ParallelWrite {
return &path, nil return []*string{&path}, nil
} }
if ok { reqPath = append(reqPath, &path)
ok = false if d.ProtectSameName && !d.ParallelWrite && len(reqPath) >= 2 {
} else { return nil, errs.NotImplement
}
if d.ProtectSameName && d.ParallelWrite && len(reqPath) >= 2 && !all {
return nil, errs.NotImplement return nil, errs.NotImplement
} }
reqPath = &path
} }
if reqPath == nil { if len(reqPath) == 0 {
return nil, errs.ObjectNotFound return nil, errs.ObjectNotFound
} }
return reqPath, nil 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

@ -1,118 +0,0 @@
package alist_v2
import (
"context"
"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/server/common"
)
type AListV2 struct {
model.Storage
Addition
}
func (d *AListV2) Config() driver.Config {
return config
}
func (d *AListV2) GetAddition() driver.Additional {
return &d.Addition
}
func (d *AListV2) Init(ctx context.Context) error {
if len(d.Addition.Address) > 0 && string(d.Addition.Address[len(d.Addition.Address)-1]) == "/" {
d.Addition.Address = d.Addition.Address[0 : len(d.Addition.Address)-1]
}
// TODO login / refresh token
//op.MustSaveDriverStorage(d)
return nil
}
func (d *AListV2) Drop(ctx context.Context) error {
return nil
}
func (d *AListV2) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
url := d.Address + "/api/public/path"
var resp common.Resp[PathResp]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(PathReq{
PageNum: 0,
PageSize: 0,
Path: dir.GetPath(),
Password: d.Password,
}).Post(url)
if err != nil {
return nil, err
}
var files []model.Obj
for _, f := range resp.Data.Files {
file := model.ObjThumb{
Object: model.Object{
Name: f.Name,
Modified: *f.UpdatedAt,
Size: f.Size,
IsFolder: f.Type == 1,
},
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
}
files = append(files, &file)
}
return files, nil
}
func (d *AListV2) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
url := d.Address + "/api/public/path"
var resp common.Resp[PathResp]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(PathReq{
PageNum: 0,
PageSize: 0,
Path: file.GetPath(),
Password: d.Password,
}).Post(url)
if err != nil {
return nil, err
}
return &model.Link{
URL: resp.Data.Files[0].Url,
}, nil
}
func (d *AListV2) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotImplement
}
func (d *AListV2) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *AListV2) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return errs.NotImplement
}
func (d *AListV2) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *AListV2) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotImplement
}
func (d *AListV2) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
return errs.NotImplement
}
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*AListV2)(nil)

View File

@ -1,26 +0,0 @@
package alist_v2
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootPath
Address string `json:"url" required:"true"`
Password string `json:"password"`
AccessToken string `json:"access_token"`
}
var config = driver.Config{
Name: "AList V2",
LocalSort: true,
NoUpload: true,
DefaultRoot: "/",
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &AListV2{}
})
}

View File

@ -1,31 +0,0 @@
package alist_v2
import (
"time"
)
type File struct {
Id string `json:"-"`
Name string `json:"name"`
Size int64 `json:"size"`
Type int `json:"type"`
Driver string `json:"driver"`
UpdatedAt *time.Time `json:"updated_at"`
Thumbnail string `json:"thumbnail"`
Url string `json:"url"`
SizeStr string `json:"size_str"`
TimeStr string `json:"time_str"`
}
type PathResp struct {
Type string `json:"type"`
//Meta Meta `json:"meta"`
Files []File `json:"files"`
}
type PathReq struct {
PageNum int `json:"page_num"`
PageSize int `json:"page_size"`
Password string `json:"password"`
Path string `json:"path"`
}

View File

@ -1 +0,0 @@
package alist_v2

View File

@ -1,226 +0,0 @@
package alist_v3
import (
"context"
"fmt"
"io"
"net/http"
"path"
"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/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
type AListV3 struct {
model.Storage
Addition
}
func (d *AListV3) Config() driver.Config {
return config
}
func (d *AListV3) GetAddition() driver.Additional {
return &d.Addition
}
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) {
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 != 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) {
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)
if err != nil {
return err
}
allowMounted := utils.Json.Get(res.Body(), "data", conf.AllowMounted).ToString() == "true"
if !allowMounted {
return fmt.Errorf("the site does not allow mounted")
}
}
return err
}
func (d *AListV3) Drop(ctx context.Context) error {
return nil
}
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) {
req.SetResult(&resp).SetBody(ListReq{
PageReq: model.PageReq{
Page: 1,
PerPage: 0,
},
Path: dir.GetPath(),
Password: d.MetaPassword,
Refresh: false,
})
})
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) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var resp common.Resp[FsGetResp]
// if PassUAToUpsteam is true, then pass the user-agent to the upstream
userAgent := base.UserAgent
if d.PassUAToUpsteam {
userAgent = args.Header.Get("user-agent")
if userAgent == "" {
userAgent = base.UserAgent
}
}
_, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(FsGetReq{
Path: file.GetPath(),
Password: d.MetaPassword,
}).SetHeader("user-agent", userAgent)
})
if err != nil {
return nil, err
}
return &model.Link{
URL: resp.Data.RawURL,
}, nil
}
func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
_, err := d.request("/fs/mkdir", http.MethodPost, func(req *resty.Request) {
req.SetBody(MkdirOrLinkReq{
Path: path.Join(parentDir.GetPath(), dirName),
})
})
return err
}
func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := d.request("/fs/move", http.MethodPost, func(req *resty.Request) {
req.SetBody(MoveCopyReq{
SrcDir: path.Dir(srcObj.GetPath()),
DstDir: dstDir.GetPath(),
Names: []string{srcObj.GetName()},
})
})
return err
}
func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
_, err := d.request("/fs/rename", http.MethodPost, func(req *resty.Request) {
req.SetBody(RenameReq{
Path: srcObj.GetPath(),
Name: newName,
})
})
return err
}
func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := d.request("/fs/copy", http.MethodPost, func(req *resty.Request) {
req.SetBody(MoveCopyReq{
SrcDir: path.Dir(srcObj.GetPath()),
DstDir: dstDir.GetPath(),
Names: []string{srcObj.GetName()},
})
})
return err
}
func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
_, err := d.request("/fs/remove", http.MethodPost, func(req *resty.Request) {
req.SetBody(RemoveReq{
Dir: path.Dir(obj.GetPath()),
Names: []string{obj.GetName()},
})
})
return err
}
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+"/api/fs/put", stream)
if err != nil {
return err
}
req.Header.Set("Authorization", d.Token)
req.Header.Set("File-Path", path.Join(dstDir.GetPath(), stream.GetName()))
req.Header.Set("Password", d.MetaPassword)
req.ContentLength = stream.GetSize()
// client := base.NewHttpClient()
// client.Timeout = time.Hour * 6
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
bytes, err := io.ReadAll(res.Body)
if err != nil {
return err
}
log.Debugf("[alist_v3] response body: %s", string(bytes))
if res.StatusCode >= 400 {
return fmt.Errorf("request failed, status: %s", res.Status)
}
code := utils.Json.Get(bytes, "code").ToInt()
if code != 200 {
if code == 401 || code == 403 {
err = d.login()
if err != nil {
return err
}
}
return fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(bytes, "message").ToString())
}
return nil
}
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*AListV3)(nil)

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