Compare commits

...

438 Commits

Author SHA1 Message Date
5e8bfb017e fix(123): add Referer to request (close #4631) 2023-07-04 18:36:46 +08:00
7d20a01dba feat!: support listen to the unix (close #4671)
Starting from this commit, the HTTP server related config all move to the scheme
2023-07-04 17:56:02 +08:00
59dbf4496f feat(offline_download): try to init client if not ready (close #4674) 2023-07-03 22:57:42 +08:00
12f40608e6 fix(oidc): use TOTP as state verification to replace the static 'state' parameter (#4665) 2023-07-03 22:41:08 +08:00
89832c296f fix: judge can proxy with ext (close #4688) 2023-07-03 20:41:37 +08:00
f09bb88846 fix(thunder): upload issues (close #4663 in #4667) 2023-06-29 13:21:30 +08:00
c518f59528 feat: add MoPan driver (close #4325 in #4659)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-06-28 14:53:43 +08:00
e9c74f9959 fix: regexp rename error (close #4644 in #4653)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-06-26 15:15:57 +08:00
21b8e7f6e5 fix(aliyundrive_share): add limit rate and lift rate limit restrictions (#4587) 2023-06-26 14:49:21 +08:00
2ae9cd8634 fix(dropbox): failed get link in #4639
close cfee536b96 (commitcomment-119404554)
2023-06-25 17:07:31 +08:00
cfee536b96 feat: add Dropbox driver (#4639 close #4590)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-06-23 17:36:40 +08:00
1c8fe3b24c fix(aliyundrive_open): adaptive part size adjustment (#4609)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-06-23 14:25:30 +08:00
84e23c397d fix(baidu_netdisk): rollback #3652 (close #4628) 2023-06-21 18:37:25 +08:00
f7baec2e65 feat: add WoPan driver (close #4541) 2023-06-17 20:20:00 +08:00
378bab32f1 chore(aliyundrive_share): increase the limit of the list api (#4588) 2023-06-17 20:10:34 +08:00
6cd8151cad fix(aliyundrive_open): change default oauth_token_url 2023-06-16 15:03:27 +08:00
541449e10f docs: add special sponsor [skip ci] 2023-06-14 05:42:21 +08:00
ca5a53fc24 fix(aliyundrive_open): openFile/list rate limit 2023-06-11 18:18:09 +08:00
f646d2a699 feat!: listen to both http & https (#4536)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-06-11 18:17:37 +08:00
363e036bf0 chore: fix typo [skip ci] 2023-06-10 22:25:35 +08:00
e23f00f349 fix(139): avoid panic due to Authorization for emptiness 2023-06-10 00:12:04 +08:00
9600267bda ci: add linux-musl-amd64/arm64 to dev build 2023-06-09 23:43:52 +08:00
a66b0e0151 feat(139): auto extract account from Authorization 2023-06-09 23:41:41 +08:00
3bfa00d5d2 fix(189pc): add REQID header 2023-06-09 23:33:12 +08:00
6cbd2532cc fix(139): modify the authentication mode 2023-06-09 23:02:02 +08:00
47976af0d3 feat: set ProxyFromEnvironment for default http client (#4546) 2023-06-09 22:08:54 +08:00
4dca52be85 fix(s3): optional add filename to disposition (close #4538) 2023-06-06 22:47:27 +08:00
62bb09300d chore: fix typo [skip ci] 2023-06-06 19:34:10 +08:00
f9e067abec feat: support delayed start (#4532) 2023-06-05 16:00:31 +08:00
1e62666406 feat(baidu_netdisk): allow custom crack ua 2023-06-04 15:57:41 +08:00
0e0cdf15ef chore: change daysUntilClose [skip ci] 2023-06-03 21:15:52 +08:00
b124fdc092 perf(baidu): avoid refreshing the token on every startup 2023-06-02 18:31:42 +08:00
5141b3c165 fix(deps): update module github.com/gin-gonic/gin to v1.9.1 [security] [skip ci] (#4521)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-02 18:31:14 +08:00
881d6e271e feat: add OIDC single sign-on (#4496)
close #3914
close #4315
2023-06-02 18:22:07 +08:00
bd2418c438 feat(deps): update alpine to 3.18 2023-05-28 19:30:42 +08:00
8421c72c5c fix(seafile): driver panic while downloading or uploading file (#4491)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-05-28 16:45:46 +08:00
a80e21997c feat(cloudreve): auto remove trailing slash in address (#4492)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-05-28 16:18:09 +08:00
4369cbbac3 fix(alist_v3): missed Content-Length on upload (close #4457) 2023-05-27 20:23:36 +08:00
89f76d7899 feat: add UC driver (close #1127 in #4459)
Co-authored-by: lj98568 <lj98568@alibaba-inc.com>
Co-authored-by: Andy Hsu <i@nn.ci>
2023-05-27 19:36:14 +08:00
ef68f84787 fix(baidu_photo): legal album title check (close #4479 in #4487) 2023-05-27 17:07:57 +08:00
2c1f70fbe9 fix(189pc): large file upload error (close #4417 in #4438) 2023-05-27 14:28:58 +08:00
b2f5757f8d fix(copy): copy from driver that return writer (close #4291) 2023-05-26 21:57:43 +08:00
6b97b4eb20 feat(s3): set content type from stream when uploading (#4460)
Co-authored-by: guopeilun <guopl@flatincbr.com>
2023-05-24 18:02:49 +08:00
645c10c11f fix(deps): update module github.com/sirupsen/logrus to v1.9.2 (#4402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-20 22:15:32 +08:00
571bcf07b0 fix(alias): add api prefix for proxy url (close #4392) 2023-05-19 00:12:57 +08:00
63de65be45 fix: increase timeout for http_client (close #4409) 2023-05-18 23:32:05 +08:00
a3446720a2 fix: make TlsInsecureSkipVerify enable for all request (#4386) 2023-05-14 17:05:47 +08:00
3c4c2ad4e0 feat(teambition): support s3 upload method (close #4365) 2023-05-13 23:06:25 +08:00
077a525961 fix(189): adapt new login method (close #4378) 2023-05-13 17:28:40 +08:00
5be79eb26e feat: add robots.txt setting (close #4303) 2023-05-12 16:53:15 +08:00
ddc19ab699 fix(deps): update module github.com/blevesearch/bleve/v2 to v2.3.8 [skip ci] (#4322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-12 16:34:25 +08:00
ddfca5a29b fix(deps): update module github.com/aws/aws-sdk-go to v1.44.262 (#3285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-12 16:25:30 +08:00
c19166be1c feat(google_drive): support sa (close #3132 in #4360)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-05-12 14:47:50 +08:00
daad61443c feat(local): support thumbnail cache (close #4216) 2023-05-11 19:57:24 +08:00
4b0c01158d fix: panic on nil pointer 2023-05-11 19:44:44 +08:00
f97f1d532e fix(webdav): don't retry for put if body isn't seeker (close #4149 close #4238) 2023-05-11 18:57:35 +08:00
e15755fef0 fix(189): enable TlsInsecureSkipVerify (close #4355) 2023-05-11 18:48:31 +08:00
ea88998325 docs: add help message for mount path (#4364)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-05-11 18:40:56 +08:00
74d971aa8a docs: fix git address [skip ci] (#4366) 2023-05-11 15:05:33 +08:00
d41d868a8d fix(baidu_photo): change folder name length limit (close #4351 in #4353)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-05-09 20:44:57 +08:00
555cc26cbf fix(deps): update module golang.org/x/crypto to v0.9.0 (#4350)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-09 20:28:52 +08:00
ab4215080b fix(deps): update module golang.org/x/net to v0.10.0 (#4347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-09 16:31:17 +08:00
9502f5acd7 fix(cloudreve): skip init login when using cookie (#4341) 2023-05-08 19:25:36 +08:00
b03879403f feat(cloudreve): support use cookie to login (close #4324 in #4339)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-05-08 15:19:51 +08:00
ee4ac81677 fix(webdav): can't rename on infini-cloud (close #4333) 2023-05-08 14:21:12 +08:00
b69fc8c306 ci: increase daysUntilClose to avoid use stale-bot [skip ci] 2023-05-07 21:07:31 +08:00
ee6c31332d feat(drivers): ipfs api (#4265)
Co-authored-by: Andy Hsu <i@nn.ci>
2023-05-05 17:42:22 +08:00
9fa16bd5fc ci: use github helper to close stale issue 2023-05-05 16:29:59 +08:00
c77ed5fcb0 feat(aliyundrive_open): limit rate for List and Link (close #4290) 2023-05-02 22:06:03 +08:00
822be17fb9 feat(aliyundrive_open): add expiration for link (close #4061) 2023-05-02 16:12:40 +08:00
7e3b13ea2d fix: fs/list interface conversion from copy alias (close #4279) 2023-05-01 15:45:45 +08:00
f8fb48fb32 fix: cannot connect to Casdoor SSO (close #4266 in #4274) 2023-05-01 15:32:34 +08:00
4bf46268da feat(alias): support thumbnail (close #4256) 2023-04-28 00:17:15 +08:00
b7ea73b3c2 fix(aliyundrive_open): can't refresh token if access_token is empty (#4255) 2023-04-28 00:01:47 +08:00
9fbc54314d chore(aliyundrive_open): change base url 2023-04-27 16:38:40 +08:00
cf8ab29a17 feat: optional allow be mounted (close #4218) 2023-04-27 16:33:01 +08:00
51cadd2d49 fix: ignore handle in json (close #4251 close #4252) 2023-04-27 15:39:32 +08:00
2bae8e129e feat: add Casdoor single sign-on (#4222) 2023-04-26 16:01:40 +08:00
9d55ad3af6 fix(123): get download url (close #4244) 2023-04-26 15:06:24 +08:00
36cd504783 fix(alist_v3): missed meta_password update
fix: adb0739dfe (commitcomment-110328033)
2023-04-24 20:56:46 +08:00
49f13b9b90 fix(baidu_photo): upload file has web prefix (close #4233 in #4235) 2023-04-24 19:13:33 +08:00
adb0739dfe feat!(alist_v3): support username & password login (close #4226)
Breaking changes:
- rename access_token to token
- rename old password to meta_password
2023-04-23 17:48:26 +08:00
340cb940e3 fix(qbittorrent): set autoTMM (#4217) 2023-04-22 13:33:54 +08:00
8711f2a1c5 feat(quark): shard request file (close #4175) 2023-04-17 15:33:38 +08:00
7f35aab071 revert(quark): remove preset range header 2023-04-17 14:39:21 +08:00
ecd167d2f9 feat(quark): add preset range header (close #4166) 2023-04-16 19:26:03 +08:00
220fd30830 fix: the recursive subdirectory moving bug (#4171) 2023-04-16 16:08:12 +08:00
5cba10446e fix(123): adapt new upload method (close #4141) 2023-04-14 15:48:39 +08:00
a9bdb15205 ci: fix golang version in auto_lang [skip ci] 2023-04-14 13:49:13 +08:00
c5f6a90f54 fix(quark): download file size limit (close #4140) 2023-04-14 13:47:05 +08:00
46f9aefb04 feat: empty folder clear API [ckip ci] (#4132)
* 增加清理空文件夹API

* 修复嵌套文件夹删除Bug

 Author:    varg247 <varg247@gmail.com>

---------

Co-authored-by: varg247 <varg247@qq.com>
2023-04-13 15:39:21 +08:00
fdcad9c154 fix(123): incorrect endpoint (close #4046) 2023-04-12 23:04:12 +08:00
027025361a ci: fixed version of alpine 2023-04-12 16:01:49 +08:00
f1245153b9 chore(deps): upgrade to go@1.20 2023-04-12 15:42:27 +08:00
570b8be022 fix(onedrive): error check in upBig 2023-04-11 22:52:42 +08:00
86a773674a feat(task): print stack trace if panic 2023-04-11 15:16:57 +08:00
75fd0ee185 feat(s3): optional remove bucket name from path (close #4069) 2023-04-09 19:25:52 +08:00
cc43238bd1 fix(alias): disable log completely (#4054) 2023-04-09 15:46:26 +08:00
c0a6beecea fix(alias): panic on nil pointer (close #4093) 2023-04-09 14:06:04 +08:00
c77eebb035 fix(deps): update module golang.org/x/image to v0.7.0 (#4065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-08 21:51:51 +08:00
b1efb86b28 fix(deps): update module golang.org/x/net to v0.9.0 [skip ci] (#4066)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-08 21:20:19 +08:00
0707449c8f fix(deps): update module golang.org/x/crypto to v0.8.0 [skip ci] (#4076)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-08 21:18:39 +08:00
0f8a84f67e perf(alias): disabled log on fs call (close #4054) 2023-04-07 00:02:07 +08:00
a475783b00 fix(deps): update module github.com/spf13/cobra to v1.7.0 [skip ci] (#4041)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-06 21:41:41 +08:00
67413015e8 ci: use non-upx prebuilt for windows by default 2023-04-06 21:38:57 +08:00
3a311a47af fix(deps): update module github.com/upyun/go-sdk/v3 to v3.0.4 (#4039)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-04 17:10:31 +08:00
9ccd802126 fix(123): api prefix changed (close #4038) 2023-04-04 16:39:56 +08:00
0acba7cd22 perf(123): reduce login count 2023-04-03 11:24:29 +08:00
3cdb8e7a81 fix(trainbit): incorrect filename display (#4027) 2023-04-02 21:13:20 +08:00
d3efee2ea1 fix(s3): increase PartSize if filesize > 50000MB (close #4017) 2023-04-02 16:09:27 +08:00
4ec274e748 fix(aliyundrive_open): refresh upload url if expired (#3999 close #3823)
* fix(aliyundrive_open): refresh upload url for large files

* fix(aliyundrive_open): retry upload on url expiry

* fix(aliyundrive_open): ignore 409 error

* feat(aliyundrive): cleanup upload retry logic

* feat(util): add multireadable io utility

* feat(aliyundrive_open): make upload fully stream

* feat(aliyundrive_open): refresh upload url every 20 puts

* fix(aliyundrive_open): part info panic

* chore: change refresh upload url strategy

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-04-01 14:54:29 +08:00
3b07c72f88 fix(proxy): ignore Referer if got redirect (close #3996) 2023-03-31 20:29:55 +08:00
0c5820a98f docs(aliyundrive_open): revised the sentence that may cause ambiguity (#3989) 2023-03-29 20:26:21 +08:00
86beadc0ed fix: missed sign with enable sign_all (close #3957) 2023-03-26 16:19:01 +08:00
be62d64dba chore: cancel 2fa succeed tips 2023-03-25 18:36:13 +08:00
112363031a feat: add fine-grained control for link signing (#3924)
* Determine whether the URL requires Sign

* Add File and Mem based KV

NOT TESTED: TokenKV Function

* Change Token KV func to common func.

Add File based KV func

* Remove KV, Remove Token

I found that the original Sign function is enough to complete the link signature, and only need to add simple configuration items to meet the requirements.

* Add IsStorageSigned func to judge if Signing is enabled in the storage settings.

It should be working now.

* Add a SIGN button to the management panel.

* Add enable_sign to the basic storage struct.

Can enable sign for every driver now.

Bug: When sign enabled, in download page, Copy link doesn't contain a sign.

(Not done yet)

* Fix a bug from commit 8f6c25f.

Response of fsread function does not contain sign.

* Optimize code and follow advices.

- Add back public/dist/README.md

- Enable sign when DownProxyUrl is enabled

- Merge needSign() to isEncrypt() in fsread.go

* simplify code

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-03-24 22:44:33 +08:00
48dc3552a6 fix(url_tree): incorrect tree structure 2023-03-24 20:34:03 +08:00
663814c9ef fix(url_tree): fix test url [skip ci] (#3940) 2023-03-24 20:26:00 +08:00
bd892e6a63 feat(drivers): new driver UrlTree (close #3268 in #3933)
* feat(drivers): new driver `urls` (close #3268)

* chore: rename

* support customize basic info or get from url

* dfs tree to calculate folder size

* go mod tidy

* add help message
2023-03-24 15:13:54 +08:00
4fd2c09845 fix(115): download issue due to ua (close #3931 in #3932) 2023-03-23 22:57:44 +08:00
0eab31bdf5 fix(local): filename with whitespace issue (#3928)
* fix(local): filename whitespace problem

* fix(deps): remove deprecated package io/ioutil

---------

Co-authored-by: XZB <i@1248.ink>
2023-03-23 15:18:37 +08:00
c6af22b97e feat: add thumbnail to fs/get api (#3927) 2023-03-23 13:59:39 +08:00
b2a5110672 feat(onedrive): support application authorization method (#3906) 2023-03-23 13:26:03 +08:00
c628992ea6 ci: add log required on question label [skip ci] 2023-03-22 14:03:04 +08:00
c65d868e09 fix(baidu_share): large file download (#3887 close #3876)
* fix(baidushare): large file download

* refactor: optimize client
2023-03-20 17:46:15 +08:00
aeb48b2ecc perf(aliyundrive_open): don't refresh token on init if token valid 2023-03-20 15:00:02 +08:00
cefec1a663 style: sort imports 2023-03-20 14:59:01 +08:00
e7ad830aa8 fix(cloudreve): captcha code ocr (#3889 close #3662) 2023-03-19 20:30:39 +08:00
b27eed265a fix(deps): update module github.com/blevesearch/bleve/v2 to v2.3.7 [skip ci] (#3874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-19 20:26:48 +08:00
3abe26473c fix(trainbit): decode html code (#3883) 2023-03-19 15:25:06 +08:00
023107226c fix(trainbit): remove unnecessary operation (#3881) 2023-03-18 13:52:36 +08:00
8b109cfe40 fix(smb): byte alignment (close #3868) 2023-03-17 16:32:34 +08:00
b48e97d406 chore: fix release name [skip ci] 2023-03-16 22:47:01 +08:00
6c91cfeb90 chore(deps): update actions/setup-go action to v4 (#3858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-16 18:28:51 +08:00
bfd1f25972 fix(deps): update module github.com/deckarep/golang-set/v2 to v2.3.0 [skip ci] (#3852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-16 15:58:02 +08:00
8c0defce09 feat(task): add clear succeeded and retry (#3856 close #3776) 2023-03-16 15:56:27 +08:00
a1e88cfa05 fix(teambition): empty token for upload (close #3854) 2023-03-15 14:56:41 +08:00
443f5ffbcc feat(alias): auto flatten if only one root 2023-03-14 20:25:52 +08:00
b8bc94306d fix(alias): check obj exist for every storage (fix d9795ff) 2023-03-14 20:11:25 +08:00
d9795ff22f feat(alias): support proxy and direct together 2023-03-14 13:46:27 +08:00
c4108007cd fix: spaces in filename will be replaced with plus sign (#3841)
Co-authored-by: XZB <i@1248.ink>
2023-03-14 12:27:42 +08:00
f3db23a41e feat(qbittorrent): add offline download seed time (#3842 close #3588) 2023-03-14 12:13:23 +08:00
4741a75c92 feat(115): update upload api to v4.0 add pagesize option (#3840 close #3753) 2023-03-13 20:02:52 +08:00
301756ba03 feat(drivers): alias a new storage with multi path (close #3248) 2023-03-13 15:35:37 +08:00
3b2703a5e5 feat(drivers): add the support for Trainbit (#3813)
* feat: add the support for Trainbit
read only

* feat: add the support for Trainbit
modify the structure of code
allow to create folder, move, rename and remove

* feat: add the support for Trainbit
allow to upload file

* feat: add the support for Trainbit
get token from page

* feat: add the support for Trainbit
display progress of updating

* feat: add the support for Trainbit
fix bug of time zone

* feat: add the support for Trainbit
fix the bug of filename
2023-03-12 22:18:55 +08:00
2a601f06cb feat(drivers): add BaiduYun share link support (#3801)
新增百度网盘分享链接挂载
2023-03-12 14:00:11 +08:00
adc3a56552 feat(aliyundrive): make checksum cancellable (#3814) 2023-03-12 13:59:40 +08:00
4d9a29bddd feat(ftp): support seek/range request (#3811) 2023-03-11 21:02:47 +08:00
666e02f0c3 fix(storage): explicitly set storages' status to disabled (#3810) 2023-03-11 20:45:35 +08:00
6aaec19c1c feat: allow override startup command for Docker image (#3800)
This is to enable the use case where the stock Docker image is used with
different flags. E.g. `docker run xhofe/alist:latest ./alist server --data=mydata`

This was the behavior until PR#2818 changed it. This would make the image more usable.
2023-03-11 15:33:59 +08:00
1091e1b740 feat: file aggregation and regular rename api (#3788)
* 增加文件聚合接口,将给定文件夹下所有文件移动到目标文件夹。

* 增加文件正则重命名接口。

---------

Co-authored-by: varg247 <varg247@qq.com>
2023-03-10 19:01:49 +08:00
d06c605421 fix: smb drive lastConnTime data race (#3787 close #3782) 2023-03-10 15:59:53 +08:00
43de823058 fix: path IsApply check (close #3784) 2023-03-09 21:03:56 +08:00
02d0aef611 feat(aliyundrive_open): add internal upload (aliyun ECS for Beijing area only) (#3775) 2023-03-09 20:48:30 +08:00
5596661ce8 feat(aliyundrive_open): optional delete file directly (close #3769) 2023-03-08 19:19:13 +08:00
2379cb8d67 style: go mod tidy 2023-03-08 19:08:11 +08:00
8c0ebe0841 revert: "fix(deps): update module gorm.io/gorm to v1.24.6 (#3684)" (close #3746)
This reverts commit c595fd7f94.
2023-03-08 19:07:04 +08:00
fd868bac84 fix(deps): update module github.com/caarlos0/env/v7 to v7.1.0 (#3763)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-08 16:21:43 +08:00
ebcbb29a0f feat: ping api (close #3752) 2023-03-07 19:05:52 +08:00
00ff0a43a7 feat(cmd): disable a storage with specific mountPath (close #3564) 2023-03-07 19:01:40 +08:00
3d3f23ec9e fix: upload check if disable sub folder (close #3741) 2023-03-07 14:13:39 +08:00
d484219c48 fix(security): compare auth token in constant time (#3740 close #3739) 2023-03-06 23:41:06 +08:00
dd4c97393e feat: show sso settings at a more reasonable sort (#3735) 2023-03-06 20:59:45 +08:00
07b8ff25a7 ci: auto release desktop 2023-03-06 18:05:57 +08:00
0d5c3c5080 fix(deps): update module github.com/deckarep/golang-set/v2 to v2.2.0 [skip ci] (#3727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-06 17:54:17 +08:00
75b4429f73 feat(quark): enable NoOverwriteUpload (#3720) 2023-03-05 18:00:00 +08:00
34ef6bd18d feat(115): enable NoOverwriteUpload [skip ci] (close #3669) 2023-03-05 17:59:19 +08:00
c915313ec9 feat: rename then delete if storage doesn't support overwrite upload (close #3643) 2023-03-05 15:36:12 +08:00
12a095a1d6 fix: slice bounds out of range on CanAccess check 2023-03-05 15:29:53 +08:00
dc000f640a feat: optional log to std 2023-03-05 15:07:06 +08:00
aa1c5b2be3 fix(deps): update module golang.org/x/crypto to v0.7.0 [skip ci] (#3717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-05 14:32:41 +08:00
1d4ec3c50d fix(deps): update module golang.org/x/net to v0.8.0 [skip ci] (#3715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-05 14:32:26 +08:00
ebfeef52f4 fix(deps): update module golang.org/x/image to v0.6.0 [skip ci] (#3714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-05 13:52:53 +08:00
c595fd7f94 fix(deps): update module gorm.io/gorm to v1.24.6 (#3684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 19:15:50 +08:00
421052f88a fix(deps): update github.com/t3rm1n4l/go-mega digest to a01a2cd (#3665)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 19:03:38 +08:00
603681fbe6 feat: rebuild Single sign-on system (#3649 close #3571)
* rebuild single sign on system

* perf: use cache

* fix: codefactor check

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-03-02 17:55:33 +08:00
f442185aa5 perf(123): optimize login error 2023-02-28 21:17:15 +08:00
ca9e739465 fix: hide apply to sub path without enable (close #3661) 2023-02-28 18:43:52 +08:00
53a1c4283b fix(baidu_netdisk): maybe optimize crack api (#3652)
User-Agent to netdisk and remove origin=dlna(is baned)
2023-02-28 18:27:07 +08:00
93dd768234 fix(webdav): disabled is not working in webdav (#3659)
A disabled user with webdav permission can use webdav normally, which is not allowed.
2023-02-28 18:26:13 +08:00
c9c4d6bc7e fix!(local): perm on mkdir (close #3626) 2023-02-26 21:25:32 +08:00
81e10f8939 ci: set prerelease before the build completes 2023-02-25 18:06:35 +08:00
4dd753de52 fix(aliyundrive_open): missed expire_sec while get link (close #3610) 2023-02-25 17:54:36 +08:00
79df63d319 chore(aliyundrive): change alert info 2023-02-25 14:28:27 +08:00
ec54831162 fix: only refresh token while do request (close #3591) 2023-02-24 20:31:12 +08:00
c8f3e8ab4d feat!: skip tls insecure verify by default 2023-02-23 22:33:54 +08:00
4be8524d80 feat: add alert for driver 2023-02-23 22:03:11 +08:00
0d3146b51d fix(webdav): disable put with empty path (close #3569) 2023-02-23 21:19:50 +08:00
f95d843969 feat(aliyundrive): add url_expire_sec for video preview (close #3522) 2023-02-23 20:50:31 +08:00
28aee8c493 feat: add aliyundrive open driver (#3437)
close #3533 
close #3521 
close #3459 
close #3375 

* feat: add aliyundrive open driver

* feat: adapt alist api

* fix: trailing spaces

* feat(aliyundrive_open): video preview api
2023-02-23 20:45:57 +08:00
de3ea82eb9 ci: add closeComment for stale 2023-02-22 22:17:33 +08:00
268ba3d069 fix(deps): update module github.com/gin-gonic/gin to v1.9.0 [skip ci] (#3551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 21:24:35 +08:00
309d6558fb feat(local): add thumbnail for video with ffmpeg (#3556)
* feat(local): add ffmpeg

* fix: missed `+`

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-02-22 21:19:42 +08:00
c08fdfc868 fix: missed assignment [skip ci] 2023-02-22 20:20:28 +08:00
1b28e6af3e ci: replace issues-helper with stale for inactive check 2023-02-22 20:07:18 +08:00
8655e33e60 fix: incorrect api if not set site_url (6c2f348) 2023-02-21 19:57:50 +08:00
50579fef84 fix: cancel api replace to avoid missing host 2023-02-21 19:45:09 +08:00
e39299bfe2 fix(local): missed type of MkdirPerm (923937b) 2023-02-21 17:45:15 +08:00
d1ab2443f1 feat(qbittorrent): delete tags when deleting qbittorrent tasks (#3546)
* feat & refactor(qbittorrent/client): support `deleteFiles` arg for `Client.Delete()` method

* feat(qbittorrent/client): also delete tags in `Client.Delete()`
2023-02-21 16:45:41 +08:00
658cf368bb fix(deps): update github.com/t3rm1n4l/go-mega digest to b87ebf5 (#3539)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-21 16:43:37 +08:00
fd36ce59f6 fix(onedrive): either id or path in parentReference must be specified (close #3028) 2023-02-21 16:19:46 +08:00
95b3b87672 feat(sftp): support range header 2023-02-20 16:57:52 +08:00
0d07d81802 feat(smb): support range header (close #3192) 2023-02-20 16:46:38 +08:00
923937b530 feat(local): custom mkdir perm (close #3196) 2023-02-20 16:20:36 +08:00
09492193c4 fix(alist_v3): api error pass (close #3326) 2023-02-20 16:15:52 +08:00
40b26a81a0 fix!: change default epub viewer (close #3519) 2023-02-20 16:08:10 +08:00
4293a0ba8c fix(deps): update module github.com/golang-jwt/jwt/v4 to v4.5.0 [skip ci] (#3525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-20 16:06:35 +08:00
6c2f3486fc fix!: reverse proxy to sub-directory (#3483)
from this commit, if you want reverse proxy to sub-directory like `alist` with `nginx`, you need config:

```nginx
location /alist/ {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Range $http_range;
    proxy_set_header If-Range $http_if_range;
    proxy_redirect off;
    proxy_pass http://127.0.0.1:5244/alist/;
    # the max size of file to upload
    client_max_body_size 20000m;
}
```
2023-02-18 19:03:07 +08:00
3c7512f64a fix(qbittorrent): fix two file transferring related bugs [skip ci] (#3501)
* fix(qbittorrent): delete qbittorrent task before transferring

* fix(qbittorrent): parse the path correctly when the torrent contains folders
2023-02-18 18:54:51 +08:00
84219d3d70 fix(deps): update module gorm.io/driver/mysql to v1.4.7 [skip ci] (#3495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-18 18:13:41 +08:00
05d3727335 fix(deps): update module golang.org/x/image to v0.5.0 [security skip ci] (#3489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-18 18:13:22 +08:00
ee77c3b113 fix: friendly tip for initial logging in [skip ci] (#3406)
* refactor: friendly tip for initial logging in

* fix CodeFactor issue

more info pls refer to: https://segmentfault.com/a/1190000043031147
2023-02-18 17:53:11 +08:00
fcaf485e0b fix(deps): update module gorm.io/driver/postgres to v1.4.8 [skip ci] (#3496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-18 17:52:03 +08:00
bd83469bb1 fix(deps): update module golang.org/x/net to v0.7.0 [security skip ci] (#3502)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-18 17:51:33 +08:00
90f111b24f docs: translate title [skip ci] (#3498)
* Update README_cn.md

* Update README_cn.md

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-02-18 17:50:42 +08:00
7d1034c569 fix(aliyundrive): error occurred when running multiple instances at the same time (#3448)
* fix(aliyundrive):an error occurred when running multiple instances at the same time

* Update util.go

fix(aliyunpan):clear retry count
2023-02-16 22:12:19 +08:00
236c17176c fix(123): adapt new file list api (close #3464) 2023-02-16 22:09:45 +08:00
6ee4c10e8f chore(onedrive)!: change default redirect_uri [skip ci] 2023-02-16 21:37:20 +08:00
3798634028 fix(pikpak_share): change media url to content url (close #3273) (#3441) 2023-02-16 15:42:11 +08:00
567ba5ccd4 feat(aliyundrive_share): aliyun office preview (close #3408) 2023-02-15 16:52:24 +08:00
ae2ee1821a chore: change qBittorrent setting [skip ci] 2023-02-15 16:51:29 +08:00
805b1e4fa3 fix: different url encoding (close #3423) 2023-02-15 16:20:30 +08:00
d92c10da56 fix(qbittorrent): fix multiple bugs for qbittorrent download (close #3413 in #3427)
* fix(qbittorrent): wait for qbittorrent to parse torrent and create task

#3413

* fix(qbittorrent): check task state correctly

* fix(qbittorrent): fix path sent to `op.Put()`
2023-02-15 15:58:31 +08:00
6659f6d367 fix: windows arm64 build [skip ci] 2023-02-14 20:28:05 +08:00
fe416ba15c feat!: close sign_all by default 2023-02-14 19:20:15 +08:00
de66708b24 fix(aliyundrive): device session signature error (#3398)
* fix signature

* fix: indent-error-flow [skip ci]
2023-02-14 19:17:21 +08:00
2ca3e0b8bc fix(123): incorrect download url (close #3385) 2023-02-14 15:47:41 +08:00
ae04a0a760 chore: go mod tidy 2023-02-14 15:30:33 +08:00
c28168c970 feat: support qbittorrent (close #3087 in #3333)
* feat(qbittorrent): authorization and logging in support

* feat(qbittorrent/client): support `AddFromLink`

* refactor(qbittorrent/client): check authorization when getting a new client

* feat(qbittorrent/client): support `GetInfo`

* test(qbittorrent/client): update test cases

* feat(qbittorrent): init qbittorrent client on bootstrap

* feat(qbittorrent): support setting webui url via gin

* feat(qbittorrent/client): support deleting

* feat(qbittorrent/client): parse `TorrentStatus` enum when unmarshalling json in `GetInfo()`

* feat(qbittorrent/client): support getting files by id

* feat(qbittorrent): support adding qbittorrent tasks via gin

* refactor(qbittorrent/client): return a `Client` interface in `New()` instead of `*client`

* refactor: task handle

* chore: fix typo

* chore: change path

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-02-14 15:20:45 +08:00
46b2ed2507 fix(aliyundriver):x-device-id error code (#3390)
* fix(aliyundriver):x-drvice-id error code

* fix(aliyunpan):session signature error

* fix typo

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2023-02-14 14:11:07 +08:00
22843ffc70 fix(fs): copy file if symlink failed (#3368) 2023-02-13 14:41:35 +08:00
e1b6368343 feat(aliyundrive): zero copy for local file uploads (#3359) 2023-02-12 16:13:57 +08:00
62dae50d70 feat(fs): create symbolic link instead of copy local files (close #2186 in #3354) 2023-02-12 16:03:11 +08:00
43a8ed472b fix: can't login by github after disable guest (close #3314) 2023-02-09 20:12:04 +08:00
d87878c232 ci: cancel win/arm64 on dev build [skip ci] 2023-02-09 20:05:00 +08:00
ab7dee49b0 feat: add windows/arm64 target (close #3308) 2023-02-09 19:52:40 +08:00
dca115506d fix(deps): update module golang.org/x/crypto to v0.6.0 [skip ci] (#3315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 16:17:10 +08:00
be17fba0c6 fix(deps): update module golang.org/x/net to v0.6.0 [skip ci] (#3316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 16:16:43 +08:00
cd58aa5efe fix(deps): update module gorm.io/driver/mysql to v1.4.6 (#3311) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 16:00:08 +08:00
946833d2cc fix(deps): update module golang.org/x/image to v0.4.0 (#3323) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 15:59:31 +08:00
eb42d09849 chore(deps): update docker/build-push-action action to v4 (#3200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-08 22:22:33 +08:00
9d00492750 fix(deps): update module gorm.io/driver/postgres to v1.4.7 (#3312) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-08 22:20:04 +08:00
b6711d6ab9 chore(deps): update actions-cool/issues-helper action to v3.4.0 (#3279) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-08 22:12:02 +08:00
7bc46de8aa feat: settings for tls insecure skip verify (close #3306 in #3307) 2023-02-08 22:01:26 +08:00
a4f4fb2d73 chore(deps): upgrade github.com/caarlos0/env 2023-02-07 19:55:55 +08:00
a181b56ea7 feat: optional forward direct link params (close #3123) 2023-02-07 16:39:14 +08:00
d0b743d955 fix(onedrive): downloadUrl missed on personal account (close #3276) 2023-02-07 16:16:29 +08:00
a985b748e9 fix: allow_indexed check (close #3291) 2023-02-07 15:14:39 +08:00
44cb8aaafe feat: only log to std on debug/dev mode 2023-02-05 09:17:37 +08:00
51f5d1b3c4 fix(local): set perm 0777 for folder (close #2996) 2023-02-04 12:11:13 +08:00
36e0d6f787 perf(onedrive): optimize request parameter (close #3178) 2023-02-04 11:53:13 +08:00
3d0065bdcf feat!: allow disable user (close #3241)
From this commit, the guest user will be disabled by default
2023-02-04 11:44:17 +08:00
7bf8071095 fix(deps): update module github.com/aws/aws-sdk-go to v1.44.194 (#2940)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-04 11:24:47 +08:00
30d39f8e10 fix(deps): update module gorm.io/gorm to v1.24.5 (#3231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-04 11:22:39 +08:00
20d3ef7de6 fix(139): check http code & increase chunk size (#3224)
* fixed: 大文件上传导致连接重置

Signed-off-by: aimuz <mr.imuz@gmail.com>

* revert Dockerfile

---------

Signed-off-by: aimuz <mr.imuz@gmail.com>
Co-authored-by: Andy Hsu <i@nn.ci>
2023-02-04 11:20:13 +08:00
86e5dae4d1 fix(aliyundrive_share): no permission after share_id change (#3246) 2023-02-04 11:10:28 +08:00
d89b1d4871 fix(baidu_baidu_netdisk): override for create (close #3242) 2023-02-03 18:10:39 +08:00
080e6fb22a fix(google_drive): allow download abuse file (#3217)
通过添加参数acknowledgeAbuse=true,对疑似风险文件直接下载
2023-02-01 19:43:36 +08:00
e1cd71616d feat(aliyundrive): internal upload (aliyun ECS for Beijing area only) (#3188)
Co-authored-by: wangwuxuan2011 <git@wangwuxuan.cn>
2023-01-30 11:18:08 +08:00
c92e11dad5 ci: auto build docker with aria2 2023-01-27 15:16:00 +08:00
b52e8747fa fix(alist_v3): incorrect dir on remove (close #3154) 2023-01-27 14:51:56 +08:00
14305748f0 fix(lanzou): files cannot be uploaded to the specified directory (#3157)
* Update driver.go

* fix(Lanzou):files cannot be uploaded to the specified directory

Solve the problem that files cannot be uploaded to the specified directory
2023-01-27 14:46:54 +08:00
44f8112e53 fix(s3): ignore current folder in contents (close #3137) 2023-01-25 19:58:00 +08:00
6a90b1d40a fix(deps): update module github.com/caarlos0/env/v6 to v7 (#3117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-23 20:02:09 +08:00
b42ec3e810 fix: relative path judgment (close #3130) 2023-01-23 15:52:46 +08:00
28875ce304 fix(alist_v3): incorrect src_dir on move and copy (close #3121 pr #3124)
* fix(alist_v3):add dir check(close #3121)

* Update driver.go

Co-authored-by: Noah Hsu <i@nn.ci>
2023-01-22 18:52:54 +08:00
9b99e8ab70 fix(search): allow indexed check (close #3103) 2023-01-19 17:00:49 +08:00
98872a8fdb fix: cancel EXCLUSIVE mode on sqlite3
because it will result in failure to get admin's info
2023-01-19 16:49:43 +08:00
ce4a295008 fix!: check https with X-Forwarded-Proto
not read old setting `api_url` and `base_path` from this commit
2023-01-19 12:16:42 +08:00
bc1babb5b5 fix(lanzou): shortened filename when uploading files (#3099) 2023-01-19 12:05:14 +08:00
d61242d85d feat: add wma to default audio types (close #3088) 2023-01-18 10:50:28 +08:00
99d7105357 fix: move virtual files to end (close #3052) 2023-01-18 10:23:54 +08:00
be8a9c5f07 fix: mark progress as done after clear (#3086) 2023-01-18 09:39:32 +08:00
530e74c70b fix: avoid regular expression match current directory (#3078)
* fix: avoid regular expression match current directory

* fix: optimize and regexp exclude slash

Co-authored-by: wuxuan <refused@wuxuan.eu.org>
2023-01-17 21:54:25 +08:00
0a337756ba fix(quark): upload file integer divide by zero panic. (close #3076 pr #3077) 2023-01-17 18:02:06 +08:00
26fe0a7684 feat: customize index max depth
Because some driver's issue may cause infinite loop
2023-01-17 17:33:18 +08:00
9c7e451c03 perf: optimize sqlite3 (#3074)
- use journal mode to WAL
- set locking mode to EXCLUSIVE
- set auto vacuum

ref:
 - https://www.sqlite.org/pragma.html#pragma_journal_mode
 - https://www.sqlite.org/pragma.html#pragma_locking_mode
 - https://www.sqlite.org/pragma.html#pragma_auto_vacuum
2023-01-17 17:06:11 +08:00
8df1455f25 workflow: add tips for Reproduction 2023-01-17 16:34:56 +08:00
9d9377f65d fix(local): incorrect path of thumbnail (for 6453ae0) 2023-01-16 20:02:30 +08:00
8b523fab8b revert: add Getter interface back 2023-01-16 19:55:43 +08:00
6453ae0968 fix(search): empty parent where update (close #2810) 2023-01-16 17:33:24 +08:00
1cfd47a258 feat: install tzdata in the docker image (#3056)
* disable caching of repository metadata and installation of tzdata

* add TZ variable example
2023-01-16 13:43:15 +08:00
8e2069c554 fix: db non full-text import error (#3055) 2023-01-15 23:49:23 +08:00
6b8778a63c fix: don't save if refresh token is empty (close #2957) 2023-01-14 20:33:07 +08:00
aaa8c440fe fix(seafile): token refresh (#3010)
* docs: add Seafile support

* fix: Seafile token refresh
2023-01-13 21:20:21 +08:00
2dc5dec83c feat: add Cloudreve driver (close #2658 in #2997)
* feat: add cloudreve support

add cloudreve support

(#2658)

* docs(README): add suppuort cloudreve

* fix(cloudreve): add cookie refresh

Co-authored-by: panici <zhangjun@zjdeMacBook-Pro.local>
2023-01-12 19:57:43 +08:00
1eca2b83ed perf(terabox): optimize prompt message (#3002)
* perf(terabox):prompt login status when init the driver

* docs:add Terabox

* perf(terabox):prompt area is not available

* style(terabox): del else
2023-01-12 19:40:38 +08:00
48e6f3bb23 feat: add Seafile driver (#2964)
* feat: add Seafile driver

* docs: add Seafile support

* refactor: optimization

* fix: close redirect on `move` and `rename`

Co-authored-by: Noah Hsu <i@nn.ci>
2023-01-10 20:51:42 +08:00
0ad9e17196 feat: lazy index creation on searcher init (#2962) 2023-01-09 14:09:21 +08:00
9398cdaac1 fix(s3): allow http/https headers to be attached from CustomHost (#2959)
* add(s3):Allow http/https headers to be attached to CustomHost

* optimize

Co-authored-by: wangwuxuan <wangwuxuan@163.com>
Co-authored-by: Noah Hsu <i@nn.ci>
2023-01-08 21:47:45 +08:00
2f19d4a834 perf(lanzou): optimize the use of list cache (#2956)
* fix:local sort not cache

* perf(lanzou): Optimize the use of list cache
2023-01-08 21:31:35 +08:00
99a186d01b fix(139): upload failed (#2950)
fix: The file size is exceeded and cannot be uploaded
fix: File name has special characters, signature fails
improve: optimize memory usage
Signed-off-by: aimuz <mr.imuz@gmail.com>

Signed-off-by: aimuz <mr.imuz@gmail.com>
2023-01-08 16:31:00 +08:00
40ef233d24 fix(USS): resolve driver problem (#2942)
* remove:"Endpoint" and "CustomHost" are the same thing, remove "CustomHost"

* fix: file download url error

* fix: too many file get list error

Co-authored-by: wangwuxuan <wangwuxuan@163.com>
2023-01-08 16:30:05 +08:00
7c3ea193ff fix(lanzou):webdav unable to download and upload (close #2700)
* fix(lanzou):Unable to get folder

* fix(lanzou):webdav unable to download and upload. (close 2700)
2023-01-08 15:37:39 +08:00
7902b646ff feat: add database non full text index (close #2916) 2023-01-07 01:40:49 +08:00
1c453ae147 feat: add a switch to enable auto update index (close #2930) 2023-01-07 00:59:30 +08:00
cf5714ba73 fix(smb): use correct path (#2933)
There is no need to add a `.` prefix as there is no leading `/` in paths
2023-01-07 00:47:08 +08:00
d655340634 fix(lanzou): cookie type failed to get file (#2926) 2023-01-06 18:08:40 +08:00
8d4ac031c3 chore(deps): update module github.com/aws/aws-sdk-go to v1.44.174 [skip ci] (#2920)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-06 15:36:33 +08:00
a1ded3a339 refactor(baidu_photo): optimize code (close #2911 pr #2924) 2023-01-06 15:36:05 +08:00
4a0e47dbac fmt: go mod tidy 2023-01-05 19:34:18 +08:00
510d266da8 chore(deps): update module github.com/aws/aws-sdk-go to v1.44.173 [skip ci] (#2832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-05 19:32:58 +08:00
35dfb36884 chore(deps): update module gorm.io/driver/mysql to v1.4.5 [skip ci] (#2881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-05 19:31:47 +08:00
b88f4d2ba6 chore(deps): update module gorm.io/driver/sqlite to v1.4.4 [skip ci] (#2869)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-05 19:31:28 +08:00
50318da879 chore(deps): update module gorm.io/driver/postgres to v1.4.6 [skip ci] (#2867)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-05 19:18:42 +08:00
575487a0e2 chore(deps): update module gorm.io/gorm to v1.24.3 [skip ci] (#2870)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-05 19:18:15 +08:00
69d3ccaed2 chore(deps): update module golang.org/x/net to v0.5.0 [skip ci] (#2908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-05 19:17:41 +08:00
170859a112 chore(deps): update module golang.org/x/crypto to v0.5.0 [skip ci] (#2905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-05 19:16:56 +08:00
7fdcb106a5 chore(deps): update module golang.org/x/image to v0.3.0 [skip ci] (#2906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-05 17:49:45 +08:00
14d4ddb752 fix(mysql): change mysql against mode (close #2903 close #2844 pr #2904) 2023-01-05 17:11:58 +08:00
428e59a844 fix(uss): close of closed channel (close #2847 #2896)
* fix(uss): close of closed channel

* fix(uss): close of closed channel

Co-authored-by: zxdstyle <xiangdong.zhu@maitang001.com>
2023-01-04 21:43:47 +08:00
1c8d895fc0 feat(terabox): add terabox driver (close #2825 close #2678 #2849) 2022-12-31 16:44:20 +08:00
fbf3fb825b fix(baidu_netdisk): file copy and file upload [skip ci] (#2848) 2022-12-31 16:43:22 +08:00
16e07ae016 fix(s3): set default root path (close #2834) 2022-12-30 14:53:01 +08:00
d1b9db38c7 feat(docker): add docker-compose file (close #2067) 2022-12-30 14:25:22 +08:00
395f0fc5f3 fix(docker): use root user as default 2022-12-30 14:21:39 +08:00
143e4cd077 fix: mysql FULLTEXT search (#2840) 2022-12-30 14:20:04 +08:00
f777a2fab4 fix: version doesn't update 2022-12-30 01:24:37 +08:00
dad3012ec3 fix(deps): update module github.com/aws/aws-sdk-go to v1.44.169 (#2816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-29 21:22:50 +08:00
d45209edb2 fix: /entrypoint.sh permission denied 2022-12-29 17:16:30 +08:00
e89489453d fix: cache nil value for meta 2022-12-28 17:44:34 +08:00
ed6c8194a7 feat: add PUID, PGID, Umask settings to docker image (close #2525 pr #2818)
Co-authored-by: DDSRem <1448139087@qq.com>
2022-12-28 17:18:27 +08:00
83fe17c6ec feat: support github login (#2639)
* Support Github Login

* improve according to codefactor

* fix due to last updates

* optimization

Co-authored-by: Noah Hsu <i@nn.ci>
2022-12-27 22:11:22 +08:00
c00dcc8f39 fix(deps): update module github.com/gin-gonic/gin to v1.8.2 (#2785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-25 18:20:24 +08:00
e118f4a3b9 feat: update index by req.Paths 2022-12-24 20:23:04 +08:00
5e28d0f96a fix(deps): update module github.com/aws/aws-sdk-go to v1.44.167 (#2781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-24 16:14:20 +08:00
3af23f6792 feat: batch reload all storages (close #2762 pr #2775) 2022-12-21 19:21:18 +08:00
3a41b929c9 fix: pgsql search [skip ci] (close #2761 pr #2774) 2022-12-21 19:19:37 +08:00
105f22969c feat: support cancel for some drivers (close #2717) 2022-12-21 15:03:09 +08:00
e4a88a7c13 fix(deps): update module github.com/aws/aws-sdk-go to v1.44.164 (#2773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-21 12:04:32 +08:00
b0255040c6 chore: fix typo 2022-12-20 20:07:19 +08:00
f1e842e12a feat: customize settings layout (close #2765) 2022-12-20 20:04:37 +08:00
d756cf3e9f fix(local): disable copying or moving to subfolders (close #2760) 2022-12-20 16:27:04 +08:00
146619134d feat: customize proxy ignore headers (close #2763 pr #2766)
* clean referer when use proxy

* feat: customize proxy ignore headers

Co-authored-by: Noah Hsu <i@nn.ci>
2022-12-20 16:08:32 +08:00
372030071e fix(deps): update module github.com/aws/aws-sdk-go to v1.44.163 [skip ci] (#2738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-20 15:13:14 +08:00
62a06fa0f9 feat: optimize file operation interface (#2757)
* feat: optimize file operation interface

* chore: fix typo

Co-authored-by: Noah Hsu <i@nn.ci>
2022-12-20 15:02:40 +08:00
e2bcca2fbd feat: static files for embed viewers (#2739) 2022-12-19 13:34:06 +08:00
4568af9542 feat: better static file Cache-Control (#2751) 2022-12-19 13:32:00 +08:00
b50d486a63 fix: sub path check if subPath = / 2022-12-18 21:28:38 +08:00
0ae3fc608b feat: export all cmd (#2746) 2022-12-18 19:53:39 +08:00
6024e8d832 refactor: split the db package hook and cache to the op package (#2747)
* refactor:separate the setting method from the db package to the op package and add the cache

* refactor:separate the meta method from the db package to the op package

* fix:setting not load database data

* refactor:separate the user method from the db package to the op package

* refactor:remove user JoinPath error

* fix:op package user cache

* refactor:fs package list method

* fix:tile virtual paths (close #2743)

* Revert "refactor:remove user JoinPath error"

This reverts commit 4e20daaf9e700da047000d4fd4900abbe05c3848.

* clean path directly may lead to unknown behavior

* fix: The path of the meta passed in must be prefix of reqPath

* chore: rename all virtualPath to mountPath

* fix: `getStoragesByPath` and `GetStorageVirtualFilesByPath`

is_sub_path:

/a/b isn't subpath of /a/bc

* fix: don't save setting if hook error

Co-authored-by: Noah Hsu <i@nn.ci>
2022-12-18 19:51:20 +08:00
f38f4f401b fix(139): modify chunk size to avoid large file upload failure (close #2744 close #2682 pr #2745) 2022-12-18 17:48:09 +08:00
3b2ae85009 chore: only ignore root dirs (#2741) 2022-12-18 16:48:32 +08:00
faf4150d1e docs: fix badges on README.md and README_cn.md [skip ci] (#2749) 2022-12-18 16:48:03 +08:00
fb64f00640 refactor: obj name mapping and internal path processing (#2733)
* refactor:Prepare to remove the get interface

* feat:add obj Unwarp interface

* refactor:obj name mapping and program internal path processing

* chore: fix typo

* feat: unwrap get

* fix: no use op.Get to get parent id

* fix: set the path uniformly

Co-authored-by: Noah Hsu <i@nn.ci>
2022-12-17 19:49:05 +08:00
3d336b328a feat: add pikpak share driver (close #2728 pr #2731) 2022-12-16 19:10:19 +08:00
f9cf29e0b6 fix(deps): update module golang.org/x/crypto to v0.4.0 (#2638) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-16 19:08:52 +08:00
cbd038f30f fix(deps): update module golang.org/x/net to v0.4.0 (#2608) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-16 19:05:20 +08:00
2aeb75a779 fix(deps): update module github.com/blevesearch/bleve/v2 to v2.3.6 (#2727) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-16 19:05:06 +08:00
2f8eaf6bea fix(deps): update module github.com/pquerna/otp to v1.4.0 (#2708) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-16 18:15:59 +08:00
fb7a5dec1b fix(deps): update module golang.org/x/image to v0.2.0 (#2601) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-16 18:15:19 +08:00
e61bac039a fix(deps): update module github.com/aws/aws-sdk-go to v1.44.161 (#2595) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-16 18:14:56 +08:00
b3be9ef428 feat(search): use FULLTEXT index (close #2716 pr #2726) 2022-12-16 16:51:36 +08:00
5a6b600ace feat: show gorm log on debug/dev mode (#2720) 2022-12-15 17:48:52 +08:00
e58ca686e3 feat: cache static files (#2715) 2022-12-15 17:48:29 +08:00
6f4b1ba4b3 feat: log to stdout & file (#2709) 2022-12-14 13:19:08 +08:00
cdc45630ae fix: whereInParent when parent = "/" (#2706) 2022-12-14 10:37:09 +08:00
7947ff1ae4 feat: limit max connection count (#2701) 2022-12-14 10:33:58 +08:00
33bae52fa1 refactor: optimize driver initialization need to manually deserialize and assign values, and remove redundant driver registration parameters (#2691)
* refactor: optimize driver initialization need to manually deserialize and assign values, and remove redundant driver registration parameters

* fix typo

Co-authored-by: Noah Hsu <i@nn.ci>
2022-12-13 18:03:30 +08:00
3ee45c69a7 fix(baidu_netdisk): encode path for create (close #2690) 2022-12-13 17:57:41 +08:00
179d285564 feat: optimize database search (#2687)
* feat: remove index on `SearchNode.Name`

As we do not use s% on name column, index there does not work

* fix: init index after init data

Or on the first run, it will log 'init index error: readObjectStart: expect { or n, but found , error found in #0 byte of ...||..., bigger context ...||...'

* fix: match parent more precisely

It will match `/a/bc` if we search in `/a/b` originally.
But it is not backward compatible by adding a suffix `/`
to all the data in parent field
2022-12-12 20:20:01 +08:00
a2e8e96c71 feat: respond static file on loading storages (#2686) 2022-12-12 20:17:58 +08:00
5043815d48 fix(search): don't delete virtual folder while update indexes (close #2677) 2022-12-11 14:59:58 +08:00
1640f06e13 feat(search): multiple keywords split by space (#2669) 2022-12-10 19:28:34 +08:00
62ea93837c feat: alist v3 index permission (#2653)
* feat: alist v3 index permission

* fix allowIndexed check

Co-authored-by: Noah Hsu <i@nn.ci>
2022-12-10 19:03:09 +08:00
446f82888c fix(local): add sign to thumbnail (close #2536 close #2650) 2022-12-09 10:08:31 +08:00
6f1aeb47fd feat: index enhancement (close #2632 pr #2636)
* feat: index paths as setting

* feat: clear index (#2632)

* feat: check indexMQ more frequently
2022-12-09 10:02:13 +08:00
1f7c1b4f43 fix(cors): allow all methods (close #2640) 2022-12-08 11:35:21 +08:00
3fa0217c4b feat(alist-v3): support write (close #2626 pr #2635) 2022-12-07 19:02:28 +08:00
2dd30f2b77 feat(search): support with password 2022-12-07 10:45:02 +08:00
6e23c8b4c0 feat: partial update index (close #2593 close #2621 pr #2624) 2022-12-07 10:41:52 +08:00
72aa63adce fix: skip virtual driver on building index (close #2604 pr #2617) 2022-12-06 20:43:32 +08:00
e65e8be59e fix(search): missed base_path of user for parent (close #2611) 2022-12-06 17:28:39 +08:00
7aa4dfb240 feat: use natural sort in SortFiles (#2612) 2022-12-06 17:28:18 +08:00
bd324233a0 fix: can't paste image while report bug (#2597) [skip ci] 2022-12-06 09:19:49 +08:00
f1a9b68022 fix(index): update indexes in database 2022-12-05 20:23:37 +08:00
dda1da4576 fix(index): nil pointer call 2022-12-05 20:22:35 +08:00
5b7aa9c1cf feat: allow all cors headers (close #2571) 2022-12-05 20:05:20 +08:00
a28aaceaad chore(ci): only build on main branch 2022-12-05 19:52:02 +08:00
2bb200af87 fix(deps): update modules by renovate[bot]
fix(deps): update module github.com/sheltonzhu/115driver to v1.0.13 (#2413) [skip ci]

fix(deps): update module github.com/golang-jwt/jwt/v4 to v4.4.3 (#2526) [skip ci]

fix(deps): update module golang.org/x/image to v0.1.0 (#2587) [skip ci]

chore: go mod tidy
Co-Authored-By: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-05 19:50:49 +08:00
97f1efbb72 feat!: disable --force-bin-dir if --data is abs
related issues: #2580 #2542

after this commit, the `--force-bin-dir` would take no effect if `--data` is absolute path
2022-12-05 18:32:48 +08:00
bf8b6f4c2c feat: customize ignore paths of indexes 2022-12-05 16:45:11 +08:00
bd33c200dc feat: optimize index build 2022-12-05 16:07:36 +08:00
bc6baf1be0 fix(ci): sort lang json file 2022-12-05 14:40:46 +08:00
dc8d5106f9 feat: auto fix address in alist & smb storages (#2582) 2022-12-05 13:31:34 +08:00
8c0dfe2f3d feat: Search enhancement (#2562)
* feat: ignore AList storage on indexing

* fix: remove unused err in `walkFn`

* chore(ci): fix auto_lang trigger and run it

* feat: batch index

* feat: quit index & init index

* feat: set DocType for bleve data

* fix: build index cleanup check origin err
2022-12-05 13:28:39 +08:00
4e1be9bee6 fix: async init aria2 to optimize start duration 2022-12-04 00:00:40 +08:00
4c5285e094 chore(ci): format lang file (#2558) 2022-12-03 12:19:10 +08:00
0838feeb82 fix:introduce buffered response writer for webdav, fix status/error return failed. (#2544)
* fix: introduce buffered response writer for webdav, fix webdav status/error return failed.

* fix: bypass buffered writer for GET/HEAD/POST requests
2022-12-02 17:59:59 +08:00
ae791c8634 fix: hide check in canAccess (#2556)
修复 meta.Password 和 meta.Hide 都为空的情况下,会导致无权限访问
2022-12-02 17:44:29 +08:00
09f480318c fix: unify settings string (#2555) 2022-12-02 17:42:42 +08:00
4c5be5f07f feat: only show CanAccess search results (#2548)
* feat: only show `CanAccess` search results

* have done in frontend

Co-authored-by: Noah Hsu <i@nn.ci>
2022-12-02 10:09:39 +08:00
9c1ffdbb82 fix(aliyundrive): return error if got wrong http code (#2543) 2022-12-01 21:48:19 +08:00
18a63e34dd fix(task): memory alignment for curID (close #2541) 2022-12-01 13:16:31 +08:00
ff0bcfef8a feat: optional sign all files 2022-11-30 22:10:07 +08:00
4980b71ba3 fix: add hide check to canAccess (close #2532) 2022-11-30 22:01:33 +08:00
b5bf5f4325 fix: check if the req path is relative path (close #2531) 2022-11-30 21:38:00 +08:00
f9788ea7cf feat(webdav): delete privacy header and optimize 302 (#2534)
* fix: delete set-cookie from sharepoint webdav response header

* fix: avoid two redirects when using webdav

* fix: return the correct Content-Type instead of just `application/octet-stream`

* feat: webdav backend localOnly -> proxyOnly
2022-11-30 20:52:33 +08:00
83644dab85 fix: mapping filename in GetName
some missed filename mapping
2022-11-30 20:46:54 +08:00
d94cf72da2 fix(local): webp image decode while generate thumbnail (close #2484 pr #2520)
* Fix that webp thumb  in local storage won't load

* Simplify code

Co-authored-by: Noah Hsu <i@nn.ci>
2022-11-29 09:47:40 +08:00
e98561ceb1 fix: filename char mapping while build index 2022-11-28 21:08:11 +08:00
76f37373e0 fix: settings map read and write concurrently 2022-11-28 16:54:03 +08:00
61a06992c3 fix(aria2): directory missing (close #1856 pr #2504) 2022-11-28 14:05:28 +08:00
ddcba93eea feat: multiple search indexes (#2514)
* refactor: abstract search interface

* wip: ~

* fix cycle import

* objs update hook

* wip: ~

* Delete search/none

* auto update index while cache changed

* db searcher

TODO: bleve init issue

cannot open index, metadata missing

* fix size type

why float64??

* fix typo

* fix nil pointer using

* api adapt ui

* bleve: fix clear & change struct
2022-11-28 13:45:25 +08:00
bb969d8dc6 fix(aliyundrive_share): get share link download url directly (close #2472) 2022-11-24 18:50:04 +08:00
2383e851e2 fix: reset index before build new one (#2471) 2022-11-24 14:47:49 +08:00
330a767fd7 feat: build index & search with bleve (close #1740 pr #2386)
* feat: build index & search with bleve (#1740)

* delete unused struct

Co-authored-by: Noah Hsu <i@nn.ci>
2022-11-24 11:46:47 +08:00
2b902de6fd fix(build): switch to crazymax/xgo 2022-11-22 21:08:27 +08:00
85e1350af8 fix: check password while upload (close #2444) 2022-11-22 16:14:01 +08:00
c09800790b feat: custom filename char mapping
fixes #2447 #2446 #2440 #2409 #2006 #1979 #1507 #324 #691 #518 #430
2022-11-22 15:54:18 +08:00
25fd343069 chore(deps): update module gorm and aws-sdk
fix(deps): update module gorm.io/gorm to v1.24.2 (#2436)

fix(deps): update module github.com/aws/aws-sdk-go to v1.44.142 (#2407)

Co-Authored-By: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-21 17:37:03 +08:00
518487e3df fix(123): optimize error messages (#2415) 2022-11-19 21:48:03 +08:00
a02d9c8463 fix: check error type on file not found (#2383) 2022-11-18 01:30:37 +08:00
8beeba7c0c fix(google_drive): check token before return link (close #2392) 2022-11-17 09:08:31 +08:00
50fb49f0c3 fix(deps): update dependencies by renovate[bot] (#2344)
chore(deps): add renovate.json (#2344)

fix(deps): update module github.com/aws/aws-sdk-go to v1.44.137 (#2345)

chore(deps): update actions-cool/issues-helper action to v2.5.0 (#2346)

fix(deps): update module github.com/caarlos0/env/v6 to v6.10.1 (#2348)

fix(deps): update module github.com/gin-contrib/cors to v1.4.0 (#2349)

fix(deps): update module github.com/sirupsen/logrus to v1.9.0 (#2354) [skip ci]

fix(deps): update module gorm.io/driver/postgres to v1.4.5 (#2361)  [skip ci]

fix(deps): update module golang.org/x/crypto to v0.2.0 (#2357) [skip ci]

fix(deps): update module github.com/aws/aws-sdk-go to v1.44.138 (#2358) [skip ci]

fix(deps): update module gorm.io/gorm to v1.24.1 (#2366) [skip ci]

fix(deps): update module gorm.io/driver/mysql to v1.4.4 (#2360) [skip ci]

fix(deps): update module github.com/spf13/cobra to v1.6.1 (#2356) [skip ci]

chore(deps): update actions-cool/issues-helper action to v3 (#2367) [skip ci]

fix(deps): update module gorm.io/driver/sqlite to v1.4.3 (#2365) [skip ci]

chore(deps): update actions/checkout action to v3 (#2368) [skip ci]

chore(deps): update actions/setup-go action to v3 (#2374) [skip ci]

chore(deps): update actions/upload-artifact action to v3 (#2375) [skip ci]

chore(deps): update docker/build-push-action action to v3 (#2377) [skip ci]

chore(deps): update docker/login-action action to v2 (#2378) [skip ci]

chore(deps): update docker/metadata-action action to v4 (#2381) [skip ci]

chore(deps): update docker/setup-buildx-action action to v2 (#2382) [skip ci]

chore(deps): update docker/setup-qemu-action action to v2 (#2387) [skip ci]

fix(deps): update module github.com/aws/aws-sdk-go to v1.44.139 (#2394) [skip ci]

fix(deps): update module golang.org/x/crypto to v0.3.0 (#2395) [skip ci]

Co-Authored-By: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-17 08:49:15 +08:00
4dcaa24758 fix: cache is modified while sorting (close #2340) 2022-11-15 14:38:23 +08:00
3fbdf6f022 fix: resolve import cycle in alist v3 driver (close #2337 pr #2338) 2022-11-15 10:51:32 +08:00
aa9ba289bb fix(123): overwrite upload if file has no change (close #2324) 2022-11-14 17:58:49 +08:00
3b6d8987db chore: add id to resp of create storage 2022-11-13 20:17:10 +08:00
6e3df9f847 fix(google_drive): type of chunk_size (close #2303) 2022-11-12 18:46:38 +08:00
efe0e6af22 feat: silent start, stop and restart 2022-11-11 18:42:06 +08:00
00de9bf16d fix!: sign with the raw path instead of filename (#2258) 2022-11-11 16:24:25 +08:00
1743110a70 fix(123): incorrect order_by (close #2285) 2022-11-10 21:47:13 +08:00
0352a8e028 feat: add alist v2 driver (#2281) 2022-11-10 17:42:12 +08:00
c601bb794b feat(123): support mail login (close #2218 pr #2276) 2022-11-10 09:34:48 +08:00
42865486f1 fix(local): deal with relative symlink dir (#2274) 2022-11-09 18:15:42 +08:00
44f5cf40ef fix(115): update 115 driver lib to fix some bugs (#2275)
* fix duplicate cookies in client.List func
* rm useless cookie when init
2022-11-09 18:15:06 +08:00
c3ab378ac5 feat(google_drive): support shortcut (close #2268) 2022-11-09 16:19:33 +08:00
cdcbfb24c4 fix(local): directory handle (#2262)
* fix(local): check symlink dir

* fix(local): set size of dir to 0 (close #2264)
2022-11-09 11:20:09 +08:00
e05e2fd663 chore: change default timeout (close #2252) 2022-11-08 20:37:42 +08:00
6639cab1ae feat(google_drive): chunk upload (close #2241) 2022-11-07 20:58:52 +08:00
8241f0999a feat(google_drive): override upload (close #1880) 2022-11-07 20:35:35 +08:00
f3a5e3702d fix(189): force use CN time zone (close #2240) 2022-11-07 16:37:47 +08:00
46701a176d feat(aria2): mark aria2 seeding as complete (#2223)
Currently if using aria2 to download a torrent file, it does not
consider seeding + active as completed, so the torrent download task
only completes as aria2 stops seeding.

This commit uses seeder property of TaskInfo, and mark tasks with active
status and true seeder as complete.
2022-11-06 16:20:09 +08:00
26a29f20c3 fix: missed encode path while use down proxy (close #2208) 2022-11-06 14:46:47 +08:00
18cd45d257 fix: disable cache for 302 redirect (close #2216) 2022-11-05 15:54:51 +08:00
f0a533a77a feat(115): put UA as a variable (#2217)
In special cases, developers can pass in custom UA to solve the speed limit problem
Mainly for developers calling from outside
2022-11-05 15:50:57 +08:00
619a9aeb6c feat(115): add qrcode login (#2206) 2022-11-04 21:16:52 +08:00
334 changed files with 15967 additions and 3662 deletions

View File

@ -43,8 +43,8 @@ body:
attributes:
label: Reproduction / 复现链接
description: |
Please provide a link to a repo that can reproduce the problem you ran into.
请提供能复现此问题的链接
Please provide a link to a repo that can reproduce the problem you ran into. Please be aware that your issue may be closed directly if you don't provide it.
请提供能复现此问题的链接请知悉如果不提供它你的issue可能会被直接关闭。
validations:
required: true
- type: textarea
@ -54,4 +54,3 @@ body:
description: |
Please copy and paste any relevant log output.
请复制粘贴错误日志,或者截图
render: shell

19
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,19 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 44
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 20
# Issues with these labels will never be considered stale
exemptLabels:
- accepted
- security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
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

@ -7,6 +7,8 @@ on:
paths:
- 'drivers/**'
- 'internal/bootstrap/data/setting.go'
- 'internal/conf/const.go'
- 'cmd/lang.go'
workflow_dispatch:
jobs:
@ -14,17 +16,17 @@ jobs:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ 1.19 ]
go-version: [ '1.20' ]
name: auto generate lang.json
runs-on: ${{ matrix.platform }}
steps:
- name: Setup go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Checkout alist
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: alist
@ -42,6 +44,7 @@ jobs:
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 || :
@ -51,7 +54,7 @@ jobs:
cd alist-web
git add .
git config --local user.email "i@nn.ci"
git config --local user.name "Noah Hsu"
git config --local user.name "Andy Hsu"
git commit -m "chore: auto update i18n file" -a 2>/dev/null || :
cd ..

View File

@ -2,21 +2,21 @@ name: build
on:
push:
branches: [ '**' ]
branches: [ 'main' ]
pull_request:
branches: [ '**' ]
branches: [ 'main' ]
jobs:
build:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [1.19]
go-version: [ '1.20' ]
name: Build
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
@ -25,8 +25,9 @@ jobs:
- name: Install dependencies
run: |
docker pull techknowlogick/xgo:latest
go install src.techknowlogick.com/xgo@latest
sudo snap install zig --classic --beta
docker pull crazymax/xgo:latest
go install github.com/crazy-max/xgo@latest
sudo apt install upx
- name: Build
@ -34,7 +35,7 @@ jobs:
bash build.sh dev
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: alist
path: dist

View File

@ -6,34 +6,60 @@ on:
jobs:
build_docker:
name: Docker
name: Build docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
images: xhofe/alist
- name: Replace release with dev
run: |
sed -i 's/release/dev/g' Dockerfile
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
build_docker_with_aria2:
needs: build_docker
name: Build docker with aria2
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
repository: alist-org/with_aria2
ref: main
persist-credentials: false
fetch-depth: 0
- name: Commit
run: |
git config --local user.email "i@nn.ci"
git config --local user.name "Noah Hsu"
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

19
.github/workflows/changelog.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: auto changelog
on:
push:
tags:
- '*'
jobs:
changelog:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
env:
GITHUB_TOKEN: ${{secrets.MY_TOKEN}}

View File

@ -1,17 +0,0 @@
name: Check inactive
on:
schedule:
- cron: "0 0 1 * *"
jobs:
check-inactive:
runs-on: ubuntu-latest
steps:
- name: check-inactive
uses: actions-cool/issues-helper@v3
with:
actions: 'check-inactive'
token: ${{ secrets.GITHUB_TOKEN }}
inactive-day: 30
body: Hello, this issue has been inactive for more than 30 days and will be closed if inactive for another 30 days.

View File

@ -2,7 +2,7 @@ name: Close need info
on:
schedule:
- cron: "0 0 */7 * *"
- cron: "0 0 */1 * *"
workflow_dispatch:
jobs:
@ -15,8 +15,8 @@ jobs:
actions: 'close-issues'
token: ${{ secrets.GITHUB_TOKEN }}
labels: 'question'
inactive-day: 7
inactive-day: 3
close-reason: 'not_planned'
body: |
Hello @${{ github.event.issue.user.login }}, this issue was closed due to no activities in 7 days.
你好 @${{ github.event.issue.user.login }}此issue因超过7天未回复被关闭。
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

@ -14,8 +14,8 @@ jobs:
with:
actions: 'close-issues'
token: ${{ secrets.GITHUB_TOKEN }}
labels: 'inactive'
inactive-day: 30
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 60 days. You can reopen or recreate it if you think it should continue.
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

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

View File

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

View File

@ -10,11 +10,11 @@ jobs:
if: github.event.label.name == 'question'
steps:
- name: Create comment
uses: actions-cool/issues-helper@v2.0.0
uses: actions-cool/issues-helper@v3.4.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 7 days.
你好 @${{ github.event.issue.user.login }}请按照issue模板填写, 并详细说明问题/复现步骤/复现链接/实现思路或提供更多信息等, 7天内未回复issue自动关闭。
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

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

View File

@ -1,52 +1,81 @@
name: release
on:
push:
tags:
- '*'
release:
types: [ published ]
jobs:
changelog:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
env:
GITHUB_TOKEN: ${{secrets.MY_TOKEN}}
release:
needs: changelog
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [1.19]
platform: [ ubuntu-latest ]
go-version: [ '1.20' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Prerelease
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.MY_TOKEN }}
id: ${{ github.event.release.id }}
prerelease: true
- name: Setup Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install dependencies
run: |
docker pull techknowlogick/xgo:latest
go install src.techknowlogick.com/xgo@latest
sudo snap install zig --classic --beta
docker pull crazymax/xgo:latest
go install github.com/crazy-max/xgo@latest
sudo apt install upx
- name: Build
run: |
bash build.sh release
- name: Release
- name: Release latest
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.MY_TOKEN }}
id: ${{ github.event.release.id }}
prerelease: false
- name: Upload assets
uses: softprops/action-gh-release@v1
with:
files: build/compress/*
release_desktop:
needs: release
name: Release desktop
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
repository: alist-org/desktop-release
ref: main
persist-credentials: false
fetch-depth: 0
- name: Add tag
run: |
git config --local user.email "i@nn.ci"
git config --local user.name "Andy Hsu"
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')
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: alist-org/desktop-release

View File

@ -7,36 +7,62 @@ on:
jobs:
release_docker:
name: Docker
name: Release Docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
images: xhofe/alist
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
release_docker_with_aria2:
needs: release_docker
name: Release docker with aria2
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
repository: alist-org/with_aria2
ref: main
persist-credentials: false
fetch-depth: 0
- name: Add tag
run: |
git config --local user.email "i@nn.ci"
git config --local user.name "Andy Hsu"
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

14
.gitignore vendored
View File

@ -20,10 +20,12 @@ output/
# Dependency directories (remove the comment below to include it)
# vendor/
bin/*
/bin/*
*.json
data/
log/
lang/
public/dist/*
!public/dist/README.md
/build
/data/
/log/
/lang/
/daemon/
/public/dist/*
/!public/dist/README.md

View File

@ -6,7 +6,7 @@
Prerequisites:
- [git](https://nodejs.org/zh-cn/)
- [git](https://git-scm.com)
- [Go 1.19+](https://golang.org/doc/install)
- [gcc](https://gcc.gnu.org/)
- [nodejs](https://nodejs.org/)

View File

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

View File

@ -1,6 +1,6 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂A file list program that supports multiple storage, powered by Gin and Solidjs.</em></p>
<p><em>🗂A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
@ -9,7 +9,7 @@
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build" alt="Build status" />
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
@ -53,6 +53,7 @@ English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_O
- [x] FTP / SFTP
- [x] [PikPak](https://www.mypikpak.com/)
- [x] [S3](https://aws.amazon.com/s3/)
- [x] [Seafile](https://seafile.com/)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
@ -60,6 +61,8 @@ English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_O
- [x] [139yun](https://yun.139.com/) (Personal, Family)
- [x] [YandexDisk](https://disk.yandex.com/)
- [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/)
@ -69,6 +72,8 @@ English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_O
- [x] [Baidu photo](https://photo.baidu.com/)
- [x] SMB
- [x] [115](https://115.com/)
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
@ -106,6 +111,7 @@ https://alist.nn.ci/guide/sponsor.html
### Special sponsors
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (sponsored Chinese API server)
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.la/)
- [KinhDown 百度云盘不限速下载永久免费已稳定运行3年非常可靠Q群 -> 786799372](https://kinhdown.com)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)

View File

@ -9,7 +9,7 @@
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build" alt="Build status" />
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
@ -41,7 +41,7 @@
[English](./README.md) | 中文 | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
## Features
## 功能
- [x] 多种存储
- [x] 本地存储
@ -53,6 +53,7 @@
- [x] FTP / SFTP
- [x] [PikPak](https://www.mypikpak.com/)
- [x] [S3](https://aws.amazon.com/cn/s3/)
- [x] [Seafile](https://seafile.com/)
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
- [x] WebDav(支持无API的OneDrive/SharePoint)
- [x] Teambition[中国](https://www.teambition.com/ )[国际](https://us.teambition.com/ )
@ -60,6 +61,7 @@
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
- [x] [Yandex.Disk](https://disk.yandex.com/)
- [x] [百度网盘](http://pan.baidu.com/)
- [x] [UC网盘](https://drive.uc.cn)
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷网盘](https://pan.xunlei.com)
- [x] [蓝奏云](https://www.lanzou.com/)
@ -69,6 +71,8 @@
- [x] [一刻相册](https://photo.baidu.com/)
- [x] SMB
- [x] [115](https://115.com/)
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] 部署方便,开箱即用
- [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览
@ -87,7 +91,7 @@
- [x] 离线下载
- [x] 跨存储复制文件
## Document
## 文档
<https://alist.nn.ci/zh/>
@ -95,21 +99,22 @@
<https://al.nn.ci>
## Discussion
## 讨论
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) **issue仅针对错误报告和功能请求。**
## Sponsor
## 赞助
AList 是一个开源软件如果你碰巧喜欢这个项目并希望我继续下去请考虑赞助我或提供一个单一的捐款感谢所有的爱和支持https://alist.nn.ci/zh/guide/sponsor.html
### Special sponsors
### 特别赞助
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (国内API服务器赞助)
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.la/)
- [KinhDown 百度云盘不限速下载永久免费已稳定运行3年非常可靠Q群 -> 786799372](https://kinhdown.com)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
## Contributors
## 贡献者
Thanks goes to these wonderful people:

View File

@ -1,7 +1,7 @@
appName="alist"
builtAt="$(date +'%F %T %z')"
goVersion=$(go version | sed 's/go version //')
gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
gitAuthor="Xhofe <i@nn.ci>"
gitCommit=$(git log --pretty=format:"%h" -1)
if [ "$1" = "dev" ]; then
@ -41,14 +41,45 @@ FetchWebRelease() {
rm -rf dist.tar.gz
}
BuildWinArm64() {
echo building for windows-arm64
chmod +x ./wrapper/zcc-arm64
chmod +x ./wrapper/zcxx-arm64
export GOOS=windows
export GOARCH=arm64
export CC=$(pwd)/wrapper/zcc-arm64
export CXX=$(pwd)/wrapper/zcxx-arm64
go build -o "$1" -ldflags="$ldflags" -tags=jsoniter .
}
BuildDev() {
rm -rf .git/
xgo -targets=linux/amd64,windows/amd64,darwin/amd64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
mkdir -p "dist"
muslflags="--extldflags '-static -fpic' $ldflags"
BASE="https://musl.nn.ci/"
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross)
for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz"
curl -L -o "${i}.tgz" "${url}"
sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
done
OS_ARCHES=(linux-musl-amd64 linux-musl-arm64)
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc)
for i in "${!OS_ARCHES[@]}"; do
os_arch=${OS_ARCHES[$i]}
cgo_cc=${CGO_ARGS[$i]}
echo building for ${os_arch}
export GOOS=${os_arch%%-*}
export GOARCH=${os_arch##*-}
export CC=${cgo_cc}
export CGO_ENABLED=1
go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
done
xgo -targets=windows/amd64,darwin/amd64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
mv alist-* dist
cd dist
upx -9 ./alist-linux*
upx -9 ./alist-windows*
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
upx -9 ./alist-windows-amd64-upx.exe
find . -type f -print0 | xargs -0 md5sum >md5.txt
cat md5.txt
}
@ -80,10 +111,12 @@ BuildRelease() {
export CGO_ENABLED=1
go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
done
BuildWinArm64 ./build/alist-windows-arm64.exe
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
# why? Because some target platforms seem to have issues with upx compression
upx -9 ./alist-linux-amd64
upx -9 ./alist-windows*
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
upx -9 ./alist-windows-amd64-upx.exe
mv alist-* build
}

View File

@ -4,19 +4,19 @@ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
package cmd
import (
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/spf13/cobra"
)
// passwordCmd represents the password command
var passwordCmd = &cobra.Command{
// PasswordCmd represents the password command
var PasswordCmd = &cobra.Command{
Use: "admin",
Aliases: []string{"password"},
Short: "Show admin user's info",
Run: func(cmd *cobra.Command, args []string) {
Init()
admin, err := db.GetAdmin()
admin, err := op.GetAdmin()
if err != nil {
utils.Log.Errorf("failed get admin user: %+v", err)
} else {
@ -26,7 +26,7 @@ var passwordCmd = &cobra.Command{
}
func init() {
rootCmd.AddCommand(passwordCmd)
RootCmd.AddCommand(PasswordCmd)
// Here you will define your flags and configuration settings.

View File

@ -4,31 +4,33 @@ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
package cmd
import (
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/spf13/cobra"
)
// cancel2FACmd represents the delete2fa command
var cancel2FACmd = &cobra.Command{
// Cancel2FACmd represents the delete2fa command
var Cancel2FACmd = &cobra.Command{
Use: "cancel2fa",
Short: "Delete 2FA of admin user",
Run: func(cmd *cobra.Command, args []string) {
Init()
admin, err := db.GetAdmin()
admin, err := op.GetAdmin()
if err != nil {
utils.Log.Errorf("failed to get admin user: %+v", err)
} else {
err := db.Cancel2FAByUser(admin)
err := op.Cancel2FAByUser(admin)
if err != nil {
utils.Log.Errorf("failed to cancel 2FA: %+v", err)
} else {
utils.Log.Info("2FA canceled")
}
}
},
}
func init() {
rootCmd.AddCommand(cancel2FACmd)
RootCmd.AddCommand(Cancel2FACmd)
// Here you will define your flags and configuration settings.

View File

@ -1,8 +1,14 @@
package cmd
import (
"os"
"path/filepath"
"strconv"
"github.com/alist-org/alist/v3/internal/bootstrap"
"github.com/alist-org/alist/v3/internal/bootstrap/data"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
)
func Init() {
@ -10,4 +16,29 @@ func Init() {
bootstrap.Log()
bootstrap.InitDB()
data.InitData()
bootstrap.InitIndex()
}
var pid = -1
var pidFile string
func initDaemon() {
ex, err := os.Executable()
if err != nil {
log.Fatal(err)
}
exPath := filepath.Dir(ex)
_ = os.MkdirAll(filepath.Join(exPath, "daemon"), 0700)
pidFile = filepath.Join(exPath, "daemon/pid")
if utils.Exists(pidFile) {
bytes, err := os.ReadFile(pidFile)
if err != nil {
log.Fatal("failed to read pid file", err)
}
id, err := strconv.Atoi(string(bytes))
if err != nil {
log.Fatal("failed to parse pid data", err)
}
pid = id
}
}

View File

@ -6,4 +6,5 @@ var (
NoPrefix bool
Dev bool
ForceBinDir bool
LogStd bool
)

View File

@ -71,17 +71,26 @@ func writeFile(name string, data interface{}) {
} else {
log.Infof("%s.json changed, update file", name)
//log.Infof("old: %+v\nnew:%+v", oldData, data)
utils.WriteJsonToFile(fmt.Sprintf("lang/%s.json", name), data)
utils.WriteJsonToFile(fmt.Sprintf("lang/%s.json", name), newData, true)
}
}
func generateDriversJson() {
drivers := make(Drivers)
drivers["drivers"] = make(KV[interface{}])
drivers["config"] = make(KV[interface{}])
driverInfoMap := op.GetDriverInfoMap()
for k, v := range driverInfoMap {
drivers["drivers"][k] = convert(k)
items := make(KV[interface{}])
config := map[string]string{}
if v.Config.Alert != "" {
alert := strings.SplitN(v.Config.Alert, "|", 2)
if len(alert) > 1 {
config["alert"] = alert[1]
}
}
drivers["config"][k] = config
for i := range v.Additional {
item := v.Additional[i]
items[item.Name] = convert(item.Name)
@ -123,8 +132,8 @@ func generateSettingsJson() {
//utils.WriteJsonToFile("lang/settings.json", settingsLang)
}
// langCmd represents the lang command
var langCmd = &cobra.Command{
// LangCmd represents the lang command
var LangCmd = &cobra.Command{
Use: "lang",
Short: "Generate language json file",
Run: func(cmd *cobra.Command, args []string) {
@ -138,7 +147,7 @@ var langCmd = &cobra.Command{
}
func init() {
rootCmd.AddCommand(langCmd)
RootCmd.AddCommand(LangCmd)
// Here you will define your flags and configuration settings.

32
cmd/restart.go Normal file
View File

@ -0,0 +1,32 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"github.com/spf13/cobra"
)
// RestartCmd represents the restart command
var RestartCmd = &cobra.Command{
Use: "restart",
Short: "Restart alist server by daemon/pid file",
Run: func(cmd *cobra.Command, args []string) {
stop()
start()
},
}
func init() {
RootCmd.AddCommand(RestartCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// restartCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// restartCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
var RootCmd = &cobra.Command{
Use: "alist",
Short: "A file list program that supports multiple storage.",
Long: `A file list program that supports multiple storage,
@ -17,16 +17,17 @@ Complete documentation is available at https://alist.nn.ci/`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
if err := RootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "config file")
rootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
rootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
rootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
rootCmd.PersistentFlags().BoolVar(&flags.ForceBinDir, "force-bin-dir", false, "Force to use the directory where the binary file is located as data directory")
RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "data folder")
RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
RootCmd.PersistentFlags().BoolVar(&flags.ForceBinDir, "force-bin-dir", false, "Force to use the directory where the binary file is located as data directory")
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "Force to log to std")
}

View File

@ -3,9 +3,11 @@ package cmd
import (
"context"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
@ -20,15 +22,20 @@ import (
"github.com/spf13/cobra"
)
// serverCmd represents the server command
var serverCmd = &cobra.Command{
// ServerCmd represents the server command
var ServerCmd = &cobra.Command{
Use: "server",
Short: "Start the server at the specified address",
Long: `Start the server at the specified address
the address is defined in config file`,
Run: func(cmd *cobra.Command, args []string) {
Init()
if conf.Conf.DelayedStart != 0 {
utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
}
bootstrap.InitAria2()
bootstrap.InitQbittorrent()
bootstrap.LoadStorages()
if !flags.Debug && !flags.Dev {
gin.SetMode(gin.ReleaseMode)
@ -36,47 +43,90 @@ the address is defined in config file`,
r := gin.New()
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.Init(r)
base := fmt.Sprintf("%s:%d", conf.Conf.Address, conf.Conf.Port)
utils.Log.Infof("start server @ %s", base)
srv := &http.Server{Addr: base, Handler: r}
var httpSrv, httpsSrv, unixSrv *http.Server
if conf.Conf.Scheme.HttpPort != -1 {
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
utils.Log.Infof("start HTTP server @ %s", httpBase)
httpSrv = &http.Server{Addr: httpBase, Handler: r}
go func() {
var err error
if conf.Conf.Scheme.Https {
//err = r.RunTLS(base, conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
err = srv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
} else {
err = srv.ListenAndServe()
}
err := httpSrv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
utils.Log.Fatalf("failed to start: %s", err.Error())
utils.Log.Fatalf("failed to start http: %s", err.Error())
}
}()
}
if conf.Conf.Scheme.HttpsPort != -1 {
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
utils.Log.Infof("start HTTPS server @ %s", httpsBase)
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
go func() {
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
if err != nil && err != http.ErrServerClosed {
utils.Log.Fatalf("failed to start https: %s", err.Error())
}
}()
}
if conf.Conf.Scheme.UnixFile != "" {
utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile)
unixSrv = &http.Server{Handler: r}
go func() {
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
if err != nil {
utils.Log.Fatalf("failed to listen unix: %+v", err)
}
err = unixSrv.Serve(listener)
if err != nil && err != http.ErrServerClosed {
utils.Log.Fatalf("failed to start unix: %s", err.Error())
}
}()
}
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
// a timeout of 1 second.
quit := make(chan os.Signal, 1)
// kill (no param) default send syscanll.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
utils.Log.Println("Shutdown Server ...")
utils.Log.Println("Shutdown server...")
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
utils.Log.Fatal("Server Shutdown:", err)
var wg sync.WaitGroup
if conf.Conf.Scheme.HttpPort != -1 {
wg.Add(1)
go func() {
defer wg.Done()
if err := httpSrv.Shutdown(ctx); err != nil {
utils.Log.Fatal("HTTP server shutdown err: ", err)
}
// catching ctx.Done(). timeout of 3 seconds.
select {
case <-ctx.Done():
utils.Log.Println("timeout of 1 seconds.")
}()
}
utils.Log.Println("Server exiting")
if conf.Conf.Scheme.HttpsPort != -1 {
wg.Add(1)
go func() {
defer wg.Done()
if err := httpsSrv.Shutdown(ctx); err != nil {
utils.Log.Fatal("HTTPS server shutdown err: ", err)
}
}()
}
if conf.Conf.Scheme.UnixFile != "" {
wg.Add(1)
go func() {
defer wg.Done()
if err := unixSrv.Shutdown(ctx); err != nil {
utils.Log.Fatal("Unix server shutdown err: ", err)
}
}()
}
wg.Wait()
utils.Log.Println("Server exit")
},
}
func init() {
rootCmd.AddCommand(serverCmd)
RootCmd.AddCommand(ServerCmd)
// Here you will define your flags and configuration settings.
@ -95,5 +145,5 @@ func OutAlistInit() {
cmd *cobra.Command
args []string
)
serverCmd.Run(cmd, args)
ServerCmd.Run(cmd, args)
}

71
cmd/start.go Normal file
View File

@ -0,0 +1,71 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"os"
"os/exec"
"path/filepath"
"strconv"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// StartCmd represents the start command
var StartCmd = &cobra.Command{
Use: "start",
Short: "Silent start alist server with `--force-bin-dir`",
Run: func(cmd *cobra.Command, args []string) {
start()
},
}
func start() {
initDaemon()
if pid != -1 {
_, err := os.FindProcess(pid)
if err == nil {
log.Info("alist already started, pid ", pid)
return
}
}
args := os.Args
args[1] = "server"
args = append(args, "--force-bin-dir")
cmd := &exec.Cmd{
Path: args[0],
Args: args,
Env: os.Environ(),
}
stdout, err := os.OpenFile(filepath.Join(filepath.Dir(pidFile), "start.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
log.Fatal(os.Getpid(), ": failed to open start log file:", err)
}
cmd.Stderr = stdout
cmd.Stdout = stdout
err = cmd.Start()
if err != nil {
log.Fatal("failed to start children process: ", err)
}
log.Infof("success start pid: %d", cmd.Process.Pid)
err = os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0666)
if err != nil {
log.Warn("failed to record pid, you may not be able to stop the program with `./alist stop`")
}
}
func init() {
RootCmd.AddCommand(StartCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// startCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// startCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

58
cmd/stop.go Normal file
View File

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

52
cmd/storage.go Normal file
View File

@ -0,0 +1,52 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/spf13/cobra"
)
// storageCmd represents the storage command
var storageCmd = &cobra.Command{
Use: "storage",
Short: "Manage storage",
}
func init() {
var mountPath string
var disable = &cobra.Command{
Use: "disable",
Short: "Disable a storage",
Run: func(cmd *cobra.Command, args []string) {
Init()
storage, err := db.GetStorageByMountPath(mountPath)
if err != nil {
utils.Log.Errorf("failed to query storage: %+v", err)
} else {
storage.Disabled = true
err = db.UpdateStorage(storage)
if err != nil {
utils.Log.Errorf("failed to update storage: %+v", err)
} else {
utils.Log.Infof("Storage with mount path [%s] have been disabled", mountPath)
}
}
},
}
disable.Flags().StringVarP(&mountPath, "mount-path", "m", "", "The mountPath of storage")
RootCmd.AddCommand(storageCmd)
storageCmd.AddCommand(disable)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// storageCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// storageCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -1,6 +1,5 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
@ -12,8 +11,8 @@ import (
"github.com/spf13/cobra"
)
// versionCmd represents the version command
var versionCmd = &cobra.Command{
// VersionCmd represents the version command
var VersionCmd = &cobra.Command{
Use: "version",
Short: "Show current version of AList",
Run: func(cmd *cobra.Command, args []string) {
@ -30,7 +29,7 @@ WebVersion: %s
}
func init() {
rootCmd.AddCommand(versionCmd)
RootCmd.AddCommand(VersionCmd)
// Here you will define your flags and configuration settings.

16
docker-compose.yml Normal file
View File

@ -0,0 +1,16 @@
version: '3.3'
services:
alist:
restart: always
volumes:
- '/etc/alist:/opt/alist/data'
ports:
- '5244:5244'
- '5245:5245'
environment:
- PUID=0
- PGID=0
- UMASK=022
- TZ=UTC
container_name: alist
image: 'xhofe/alist:latest'

View File

@ -22,15 +22,10 @@ func (d *Pan115) Config() driver.Config {
}
func (d *Pan115) GetAddition() driver.Additional {
return d.Addition
return &d.Addition
}
func (d *Pan115) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
func (d *Pan115) Init(ctx context.Context) error {
return d.login()
}
@ -49,7 +44,11 @@ func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
}
func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
downloadInfo, err := d.client.Download(file.(driver115.File).PickCode)
downloadInfo, err := d.client.
SetUserAgent(driver115.UA115Browser).
Download(file.(driver115.File).PickCode)
// recover for upload
d.client.SetUserAgent(driver115.UA115Desktop)
if err != nil {
return nil, err
}

View File

@ -6,7 +6,9 @@ import (
)
type Addition struct {
Cookie string `json:"cookie" required:"true"`
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
PageSize int64 `json:"page_size" type:"number" default:"56" help:"list api per page size of 115 driver"`
driver.RootID
}
@ -15,12 +17,11 @@ var config = driver.Config{
DefaultRoot: "0",
OnlyProxy: true,
OnlyLocal: true,
}
func New() driver.Driver {
return &Pan115{}
NoOverwriteUpload: true,
}
func init() {
op.RegisterDriver(config, New)
op.RegisterDriver(func() driver.Driver {
return &Pan115{}
})
}

View File

@ -1,29 +1,49 @@
package _115
import (
"fmt"
"github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/pkg/errors"
)
var UserAgent = driver.UA115Desktop
func (d *Pan115) login() error {
var err error
opts := []driver.Option{
driver.WithRestyClient(base.RestyClient),
driver.UA("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 115Browser/23.9.3.2 115disk/30.1.0"),
driver.UA(UserAgent),
}
d.client = driver.New(opts...)
d.client.SetHttpClient(base.HttpClient)
cr := &driver.Credential{}
if err := cr.FromCookie(d.Addition.Cookie); err != nil {
return err
if d.Addition.QRCodeToken != "" {
s := &driver.QRCodeSession{
UID: d.Addition.QRCodeToken,
}
if cr, err = d.client.QRCodeLogin(s); err != nil {
return errors.Wrap(err, "failed to login by qrcode")
}
d.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
d.Addition.QRCodeToken = ""
} else if d.Addition.Cookie != "" {
if err = cr.FromCookie(d.Addition.Cookie); err != nil {
return errors.Wrap(err, "failed to login by cookies")
}
d.client.ImportCredential(cr)
} else {
return errors.New("missing cookie or qrcode account")
}
return d.client.LoginCheck()
}
func (d *Pan115) getFiles(fileId string) ([]driver.File, error) {
res := make([]driver.File, 0)
files, err := d.client.List(fileId)
if d.PageSize <= 0 {
d.PageSize = driver.FileListLimit
}
files, err := d.client.ListWithLimit(fileId, d.PageSize)
if err != nil {
return nil, err
}

View File

@ -4,9 +4,9 @@ import (
"bytes"
"context"
"crypto/md5"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/base64"
"fmt"
"io"
"net/http"
@ -29,7 +29,6 @@ import (
type Pan123 struct {
model.Storage
Addition
AccessToken string
}
func (d *Pan123) Config() driver.Config {
@ -37,16 +36,12 @@ func (d *Pan123) Config() driver.Config {
}
func (d *Pan123) GetAddition() driver.Additional {
return d.Addition
return &d.Addition
}
func (d *Pan123) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
func (d *Pan123) Init(ctx context.Context) error {
_, err := d.request(UserInfo, http.MethodGet, nil, nil)
return err
}
return d.login()
}
func (d *Pan123) Drop(ctx context.Context) error {
@ -63,14 +58,9 @@ func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
})
}
//func (d *Pan123) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if f, ok := file.(File); ok {
var resp DownResp
//var resp DownResp
var headers map[string]string
if !utils.IsLocalIPAddr(args.IP) {
headers = map[string]string{
@ -87,13 +77,14 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
"size": f.Size,
"type": f.Type,
}
_, err := d.request("https://www.123pan.com/api/file/download_info", http.MethodPost, func(req *resty.Request) {
resp, err := d.request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetHeaders(headers)
}, &resp)
}, nil)
if err != nil {
return nil, err
}
u, err := url.Parse(resp.Data.DownloadUrl)
downloadUrl := utils.Json.Get(resp, "data", "DownloadUrl").ToString()
u, err := url.Parse(downloadUrl)
if err != nil {
return nil, err
}
@ -105,18 +96,24 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
return nil, err
}
}
u_ := fmt.Sprintf("https://%s%s", u.Host, u.Path)
res, err := base.NoRedirectClient.R().SetQueryParamsFromValues(u.Query()).Head(u_)
u_ := u.String()
log.Debug("download url: ", u_)
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
if err != nil {
return nil, err
}
log.Debug(res.String())
link := model.Link{
URL: resp.Data.DownloadUrl,
URL: u_,
}
log.Debugln("res code: ", res.StatusCode())
if res.StatusCode() == 302 {
link.URL = res.Header().Get("location")
} else if res.StatusCode() == 200 {
link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
}
link.Header = http.Header{
"Referer": []string{"https://www.123pan.com/"},
}
return &link, nil
} else {
@ -133,7 +130,7 @@ func (d *Pan123) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
"size": 0,
"type": 1,
}
_, err := d.request("https://www.123pan.com/api/file/upload_request", http.MethodPost, func(req *resty.Request) {
_, err := d.request(Mkdir, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
@ -144,7 +141,7 @@ func (d *Pan123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
"fileIdList": []base.Json{{"FileId": srcObj.GetID()}},
"parentFileId": dstDir.GetID(),
}
_, err := d.request("https://www.123pan.com/api/file/mod_pid", http.MethodPost, func(req *resty.Request) {
_, err := d.request(Move, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
@ -156,7 +153,7 @@ func (d *Pan123) Rename(ctx context.Context, srcObj model.Obj, newName string) e
"fileId": srcObj.GetID(),
"fileName": newName,
}
_, err := d.request("https://www.123pan.com/api/file/rename", http.MethodPost, func(req *resty.Request) {
_, err := d.request(Rename, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
@ -173,7 +170,7 @@ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
"operation": true,
"fileTrashInfoList": []File{f},
}
_, err := d.request("https://www.123pan.com/b/api/file/trash", http.MethodPost, func(req *resty.Request) {
_, err := d.request(Trash, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
@ -229,19 +226,24 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
"type": 0,
}
var resp UploadResp
_, err := d.request("https://www.123pan.com/api/file/upload_request", http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
res, err := d.request(UploadRequest, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, &resp)
if err != nil {
return err
}
if resp.Data.Key == "" {
log.Debugln("upload request res: ", string(res))
if resp.Data.Reuse || resp.Data.Key == "" {
return nil
}
if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" {
err = d.newUpload(ctx, &resp, stream, uploadFile, up)
return err
} else {
cfg := &aws.Config{
Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),
Region: aws.String("123pan"),
Endpoint: aws.String("file.123pan.com"),
Endpoint: aws.String(resp.Data.EndPoint),
S3ForcePathStyle: aws.Bool(true),
}
s, err := session.NewSession(cfg)
@ -254,14 +256,15 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
Key: &resp.Data.Key,
Body: uploadFile,
}
_, err = uploader.Upload(input)
_, err = uploader.UploadWithContext(ctx, input)
}
if err != nil {
return err
}
_, err = d.request("https://www.123pan.com/api/file/upload_complete", http.MethodPost, func(req *resty.Request) {
_, err = d.request(UploadComplete, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"fileId": resp.Data.FileId,
})
}).SetContext(ctx)
}, nil)
return err
}

View File

@ -8,12 +8,11 @@ import (
type Addition struct {
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,fileId,updateAt,createAt" default:"name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
driver.RootID
// define other
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
StreamUpload bool `json:"stream_upload"`
//Field string `json:"field" type:"select" required:"true" options:"a,b,c" default:"a"`
AccessToken string
}
var config = driver.Config{
@ -21,10 +20,8 @@ var config = driver.Config{
DefaultRoot: "0",
}
func New() driver.Driver {
return &Pan123{}
}
func init() {
op.RegisterDriver(config, New)
op.RegisterDriver(func() driver.Driver {
return &Pan123{}
})
}

View File

@ -7,18 +7,6 @@ import (
"github.com/alist-org/alist/v3/internal/model"
)
type BaseResp struct {
Code int `json:"code"`
Message string `json:"message"`
}
type TokenResp struct {
BaseResp
Data struct {
Token string `json:"token"`
} `json:"data"`
}
type File struct {
FileName string `json:"FileName"`
Size int64 `json:"Size"`
@ -62,22 +50,22 @@ var _ model.Obj = (*File)(nil)
//var _ model.Thumb = (*File)(nil)
type Files struct {
BaseResp
//BaseResp
Data struct {
InfoList []File `json:"InfoList"`
Next string `json:"Next"`
} `json:"data"`
}
type DownResp struct {
BaseResp
Data struct {
DownloadUrl string `json:"DownloadUrl"`
} `json:"data"`
}
//type DownResp struct {
// //BaseResp
// Data struct {
// DownloadUrl string `json:"DownloadUrl"`
// } `json:"data"`
//}
type UploadResp struct {
BaseResp
//BaseResp
Data struct {
AccessKeyId string `json:"AccessKeyId"`
Bucket string `json:"Bucket"`
@ -85,5 +73,15 @@ type UploadResp struct {
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"`
}
type S3PreSignedURLs struct {
Data struct {
PreSignedUrls map[string]string `json:"presignedUrls"`
} `json:"data"`
}

130
drivers/123/upload.go Normal file
View File

@ -0,0 +1,130 @@
package _123
import (
"context"
"fmt"
"io"
"math"
"net/http"
"strconv"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
func (d *Pan123) getS3PreSignedUrls(ctx context.Context, upReq *UploadResp, start, end int) (*S3PreSignedURLs, error) {
data := base.Json{
"bucket": upReq.Data.Bucket,
"key": upReq.Data.Key,
"partNumberEnd": end,
"partNumberStart": start,
"uploadId": upReq.Data.UploadId,
"StorageNode": upReq.Data.StorageNode,
}
var s3PreSignedUrls S3PreSignedURLs
_, err := d.request(S3PreSignedUrls, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, &s3PreSignedUrls)
if err != nil {
return nil, err
}
return &s3PreSignedUrls, nil
}
func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp, file model.FileStreamer, isMultipart bool) error {
data := base.Json{
"StorageNode": upReq.Data.StorageNode,
"bucket": upReq.Data.Bucket,
"fileId": upReq.Data.FileId,
"fileSize": file.GetSize(),
"isMultipart": isMultipart,
"key": upReq.Data.Key,
"uploadId": upReq.Data.UploadId,
}
_, err := d.request(UploadCompleteV2, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, nil)
return err
}
func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, reader io.Reader, up driver.UpdateProgress) error {
chunkSize := int64(1024 * 1024 * 5)
// fetch s3 pre signed urls
chunkCount := int(math.Ceil(float64(file.GetSize()) / float64(chunkSize)))
// upload 10 chunks each batch
batchSize := 10
for i := 1; i <= chunkCount; i += batchSize {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
start := i
end := i + batchSize
if end > chunkCount+1 {
end = chunkCount + 1
}
s3PreSignedUrls, err := d.getS3PreSignedUrls(ctx, upReq, start, end)
if err != nil {
return err
}
// upload each chunk
for j := start; j < end; j++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
curSize := chunkSize
if j == chunkCount {
curSize = file.GetSize() - (int64(chunkCount)-1)*chunkSize
}
err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.LimitReader(reader, chunkSize), curSize, false)
if err != nil {
return err
}
up(j * 100 / chunkCount)
}
}
// complete s3 upload
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) error {
uploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]
if uploadUrl == "" {
return fmt.Errorf("upload url is empty, s3PreSignedUrls: %+v", s3PreSignedUrls)
}
req, err := http.NewRequest("PUT", uploadUrl, reader)
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = curSize
//req.Header.Set("Content-Length", strconv.FormatInt(curSize, 10))
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode == http.StatusForbidden {
if retry {
return fmt.Errorf("upload s3 chunk %d failed, status code: %d", cur, res.StatusCode)
}
// refresh s3 pre signed urls
newS3PreSignedUrls, err := d.getS3PreSignedUrls(ctx, upReq, cur, end)
if err != nil {
return err
}
s3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls
// retry
return d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, cur, end, reader, curSize, true)
}
if res.StatusCode != http.StatusOK {
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
return fmt.Errorf("upload s3 chunk %d failed, status code: %d, body: %s", cur, res.StatusCode, body)
}
return nil
}

View File

@ -4,37 +4,73 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
)
// do others that not defined in Driver interface
const (
AApi = "https://www.123pan.com/a/api"
BApi = "https://www.123pan.com/b/api"
MainApi = AApi
SignIn = MainApi + "/user/sign_in"
UserInfo = MainApi + "/user/info"
FileList = MainApi + "/file/list/new"
DownloadInfo = MainApi + "/file/download_info"
Mkdir = MainApi + "/file/upload_request"
Move = MainApi + "/file/mod_pid"
Rename = MainApi + "/file/rename"
Trash = MainApi + "/file/trash"
UploadRequest = MainApi + "/file/upload_request"
UploadComplete = MainApi + "/file/upload_complete"
S3PreSignedUrls = MainApi + "/file/s3_repare_upload_parts_batch"
S3Auth = MainApi + "/file/s3_upload_object/auth"
UploadCompleteV2 = MainApi + "/file/upload_complete/v2"
S3Complete = MainApi + "/file/s3_complete_multipart_upload"
)
func (d *Pan123) login() error {
url := "https://www.123pan.com/api/user/sign_in"
var resp TokenResp
_, err := base.RestyClient.R().
SetResult(&resp).
SetBody(base.Json{
var body base.Json
if utils.IsEmailFormat(d.Username) {
body = base.Json{
"mail": d.Username,
"password": d.Password,
"type": 2,
}
} else {
body = base.Json{
"passport": d.Username,
"password": d.Password,
}).Post(url)
"remember": true,
}
}
res, err := base.RestyClient.R().
SetBody(body).Post(SignIn)
if err != nil {
return err
}
if resp.Code != 200 {
err = fmt.Errorf(resp.Message)
if utils.Json.Get(res.Body(), "code").ToInt() != 200 {
err = fmt.Errorf(utils.Json.Get(res.Body(), "message").ToString())
} else {
d.AccessToken = resp.Data.Token
d.AccessToken = utils.Json.Get(res.Body(), "data", "token").ToString()
}
return err
}
func (d *Pan123) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
req.SetHeaders(map[string]string{
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken,
"platform": "web",
"app-version": "1.2",
})
if callback != nil {
callback(req)
}
@ -46,7 +82,7 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
return nil, err
}
body := res.Body()
code := jsoniter.Get(body, "code").ToInt()
code := utils.Json.Get(body, "code").ToInt()
if code != 0 {
if code == 401 {
err := d.login()
@ -61,27 +97,31 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
}
func (d *Pan123) getFiles(parentId string) ([]File, error) {
next := "0"
page := 1
res := make([]File, 0)
for next != "-1" {
for {
var resp Files
query := map[string]string{
"driveId": "0",
"limit": "100",
"next": next,
"next": "0",
"orderBy": d.OrderBy,
"orderDirection": d.OrderDirection,
"parentFileId": parentId,
"trashed": "false",
"Page": strconv.Itoa(page),
}
_, err := d.request("https://www.123pan.com/api/file/list/new", http.MethodGet, func(req *resty.Request) {
_, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return nil, err
}
next = resp.Data.Next
page++
res = append(res, resp.Data.InfoList...)
if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
break
}
}
return res, nil
}

View File

@ -1,13 +1,13 @@
package _139
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"math"
"net/http"
"strconv"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
@ -20,6 +20,7 @@ import (
type Yun139 struct {
model.Storage
Addition
Account string
}
func (d *Yun139) Config() driver.Config {
@ -27,15 +28,23 @@ func (d *Yun139) Config() driver.Config {
}
func (d *Yun139) GetAddition() driver.Additional {
return d.Addition
return &d.Addition
}
func (d *Yun139) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
func (d *Yun139) Init(ctx context.Context) error {
if d.Authorization == "" {
return fmt.Errorf("authorization is empty")
}
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{
@ -59,11 +68,6 @@ func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
}
}
//func (d *Yun139) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
u, err := d.getLink(file.GetID())
if err != nil {
@ -95,8 +99,7 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
}
pathname = "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc"
}
_, err := d.post(pathname,
data, nil)
_, err := d.post(pathname, data, nil)
return err
}
@ -234,15 +237,31 @@ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
return err
}
const (
_ = iota //ignore first value by assigning to blank identifier
KB = 1 << (10 * iota)
MB
GB
TB
)
func getPartSize(size int64) int64 {
// 网盘对于分片数量存在上限
if size/GB > 30 {
return 512 * MB
}
return 100 * MB
}
func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
data := base.Json{
"manualRename": 2,
"operation": 0,
"fileCount": 1,
"totalSize": stream.GetSize(),
"totalSize": 0, // 去除上传大小限制
"uploadContentList": []base.Json{{
"contentName": stream.GetName(),
"contentSize": stream.GetSize(),
"contentSize": 0, // 去除上传大小限制
// "digest": "5a3231986ce7a6b46e408612d385bafa"
}},
"parentCatalogID": dstDir.GetID(),
@ -260,10 +279,10 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
"operation": 0,
"path": "",
"seqNo": "",
"totalSize": stream.GetSize(),
"totalSize": 0,
"uploadContentList": []base.Json{{
"contentName": stream.GetName(),
"contentSize": stream.GetSize(),
"contentSize": 0,
// "digest": "5a3231986ce7a6b46e408612d385bafa"
}},
})
@ -275,47 +294,52 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
if err != nil {
return err
}
var Default int64 = 10485760
part := int(math.Ceil(float64(stream.GetSize()) / float64(Default)))
var start int64 = 0
for i := 0; i < part; i++ {
// Progress
p := driver.NewProgress(stream.GetSize(), up)
var partSize = getPartSize(stream.GetSize())
part := (stream.GetSize() + partSize - 1) / partSize
for i := int64(0); i < part; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
start := i * partSize
byteSize := stream.GetSize() - start
if byteSize > Default {
byteSize = Default
if byteSize > partSize {
byteSize = partSize
}
byteData := make([]byte, byteSize)
_, err = io.ReadFull(stream, byteData)
limitReader := io.LimitReader(stream, byteSize)
// Update Progress
r := io.TeeReader(limitReader, p)
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r)
if err != nil {
return err
}
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, bytes.NewBuffer(byteData))
if err != nil {
return err
}
headers := map[string]string{
"Accept": "*/*",
"Content-Type": "text/plain;name=" + unicode(stream.GetName()),
"contentSize": strconv.FormatInt(stream.GetSize(), 10),
"range": fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1),
"content-length": strconv.FormatInt(byteSize, 10),
"uploadtaskID": resp.Data.UploadResult.UploadTaskID,
"rangeType": "0",
"Referer": "https://yun.139.com/",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.44",
"x-SvcType": "1",
}
for k, v := range headers {
req.Header.Set(k, v)
}
req = req.WithContext(ctx)
req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName()))
req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10))
req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1))
req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID)
req.Header.Set("rangeType", "0")
req.ContentLength = byteSize
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
log.Debugf("%+v", res)
res.Body.Close()
start += byteSize
up(i * 100 / part)
if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
res.Body.Close()
}
return nil
}

View File

@ -6,8 +6,8 @@ import (
)
type Addition struct {
Account string `json:"account" required:"true"`
Cookie string `json:"cookie" type:"text" required:"true"`
//Account string `json:"account" required:"true"`
Authorization string `json:"authorization" type:"text" required:"true"`
driver.RootID
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
CloudID string `json:"cloud_id"`
@ -19,7 +19,7 @@ var config = driver.Config{
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
op.RegisterDriver(func() driver.Driver {
return &Yun139{}
})
}

View File

@ -28,12 +28,15 @@ func (d *Yun139) isFamily() bool {
func encodeURIComponent(str string) string {
r := url.QueryEscape(str)
r = strings.Replace(r, "+", "%20", -1)
r = strings.Replace(r, "%21", "!", -1)
r = strings.Replace(r, "%27", "'", -1)
r = strings.Replace(r, "%28", "(", -1)
r = strings.Replace(r, "%29", ")", -1)
r = strings.Replace(r, "%2A", "*", -1)
return r
}
func calSign(body, ts, randStr string) string {
body = strings.ReplaceAll(body, "\n", "")
body = strings.ReplaceAll(body, " ", "")
body = encodeURIComponent(body)
strs := strings.Split(body, "")
sort.Strings(strs)
@ -69,7 +72,7 @@ func (d *Yun139) request(pathname string, method string, callback base.ReqCallba
req.SetHeaders(map[string]string{
"Accept": "application/json, text/plain, */*",
"CMS-DEVICE": "default",
"Cookie": d.Cookie,
"Authorization": "Basic " + d.Authorization,
"mcloud-channel": "1000101",
"mcloud-client": "10701",
//"mcloud-route": "001",

View File

@ -26,21 +26,13 @@ func (d *Cloud189) Config() driver.Config {
}
func (d *Cloud189) GetAddition() driver.Additional {
return d.Addition
return &d.Addition
}
func (d *Cloud189) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
d.client = resty.New().
SetTimeout(base.DefaultTimeout).
SetRetryCount(3).
SetHeader("Referer", "https://cloud.189.cn/").
SetHeader("User-Agent", base.UserAgent)
return d.login()
func (d *Cloud189) Init(ctx context.Context) error {
d.client = base.NewRestyClient().
SetHeader("Referer", "https://cloud.189.cn/")
return d.newLogin()
}
func (d *Cloud189) Drop(ctx context.Context) error {
@ -51,11 +43,6 @@ func (d *Cloud189) List(ctx context.Context, dir model.Obj, args model.ListArgs)
return d.getFiles(dir.GetID())
}
//func (d *Cloud189) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *Cloud189) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var resp DownResp
u := "https://cloud.189.cn/api/portal/getFileInfo.action"
@ -204,7 +191,7 @@ func (d *Cloud189) Remove(ctx context.Context, obj model.Obj) error {
}
func (d *Cloud189) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
return d.newUpload(dstDir, stream, up)
return d.newUpload(ctx, dstDir, stream, up)
}
var _ driver.Driver = (*Cloud189)(nil)

126
drivers/189/login.go Normal file
View File

@ -0,0 +1,126 @@
package _189
import (
"errors"
"strconv"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
)
type AppConf struct {
Data struct {
AccountType string `json:"accountType"`
AgreementCheck string `json:"agreementCheck"`
AppKey string `json:"appKey"`
ClientType int `json:"clientType"`
IsOauth2 bool `json:"isOauth2"`
LoginSort string `json:"loginSort"`
MailSuffix string `json:"mailSuffix"`
PageKey string `json:"pageKey"`
ParamId string `json:"paramId"`
RegReturnUrl string `json:"regReturnUrl"`
ReqId string `json:"reqId"`
ReturnUrl string `json:"returnUrl"`
ShowFeedback string `json:"showFeedback"`
ShowPwSaveName string `json:"showPwSaveName"`
ShowQrSaveName string `json:"showQrSaveName"`
ShowSmsSaveName string `json:"showSmsSaveName"`
Sso string `json:"sso"`
} `json:"data"`
Msg string `json:"msg"`
Result string `json:"result"`
}
type EncryptConf struct {
Result int `json:"result"`
Data struct {
UpSmsOn string `json:"upSmsOn"`
Pre string `json:"pre"`
PreDomain string `json:"preDomain"`
PubKey string `json:"pubKey"`
} `json:"data"`
}
func (d *Cloud189) newLogin() error {
url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
res, err := d.client.R().Get(url)
if err != nil {
return err
}
// Is logged in
redirectURL := res.RawResponse.Request.URL
if redirectURL.String() == "https://cloud.189.cn/web/main" {
return nil
}
lt := redirectURL.Query().Get("lt")
reqId := redirectURL.Query().Get("reqId")
appId := redirectURL.Query().Get("appId")
headers := map[string]string{
"lt": lt,
"reqid": reqId,
"referer": redirectURL.String(),
"origin": "https://open.e.189.cn",
}
// get app Conf
var appConf AppConf
res, err = d.client.R().SetHeaders(headers).SetFormData(map[string]string{
"version": "2.0",
"appKey": appId,
}).SetResult(&appConf).Post("https://open.e.189.cn/api/logbox/oauth2/appConf.do")
if err != nil {
return err
}
log.Debugf("189 AppConf resp body: %s", res.String())
if appConf.Result != "0" {
return errors.New(appConf.Msg)
}
// get encrypt conf
var encryptConf EncryptConf
res, err = d.client.R().SetHeaders(headers).SetFormData(map[string]string{
"appId": appId,
}).Post("https://open.e.189.cn/api/logbox/config/encryptConf.do")
if err != nil {
return err
}
err = utils.Json.Unmarshal(res.Body(), &encryptConf)
if err != nil {
return err
}
log.Debugf("189 EncryptConf resp body: %s\n%+v", res.String(), encryptConf)
if encryptConf.Result != 0 {
return errors.New("get EncryptConf error:" + res.String())
}
// TODO: getUUID? needcaptcha
// login
loginData := map[string]string{
"version": "v2.0",
"apToken": "",
"appKey": appId,
"accountType": appConf.Data.AccountType,
"userName": encryptConf.Data.Pre + RsaEncode([]byte(d.Username), encryptConf.Data.PubKey, true),
"epd": encryptConf.Data.Pre + RsaEncode([]byte(d.Password), encryptConf.Data.PubKey, true),
"captchaType": "",
"validateCode": "",
"smsValidateCode": "",
"captchaToken": "",
"returnUrl": appConf.Data.ReturnUrl,
"mailSuffix": appConf.Data.MailSuffix,
"dynamicCheck": "FALSE",
"clientType": strconv.Itoa(appConf.Data.ClientType),
"cb_SaveName": "3",
"isOauth2": strconv.FormatBool(appConf.Data.IsOauth2),
"state": "",
"paramId": appConf.Data.ParamId,
}
res, err = d.client.R().SetHeaders(headers).SetFormData(loginData).Post("https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do")
if err != nil {
return err
}
log.Debugf("189 login resp body: %s", res.String())
loginResult := utils.Json.Get(res.Body(), "result").ToInt()
if loginResult != 0 {
return errors.New(utils.Json.Get(res.Body(), "msg").ToString())
}
return nil
}

View File

@ -8,6 +8,7 @@ import (
type Addition struct {
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
Cookie string `json:"cookie" help:"Fill in the cookie if need captcha"`
driver.RootID
}
@ -15,10 +16,11 @@ var config = driver.Config{
Name: "189Cloud",
LocalSort: true,
DefaultRoot: "-11",
Alert: `info|You can try to use 189PC driver if this driver does not work.`,
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
op.RegisterDriver(func() driver.Driver {
return &Cloud189{}
})
}

View File

@ -2,6 +2,7 @@ package _189
import (
"bytes"
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
@ -10,16 +11,13 @@ import (
"io"
"math"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
myrand "github.com/alist-org/alist/v3/pkg/utils/random"
"github.com/go-resty/resty/v2"
@ -29,118 +27,118 @@ import (
// do others that not defined in Driver interface
func (d *Cloud189) login() error {
url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
b := ""
lt := ""
ltText := regexp.MustCompile(`lt = "(.+?)"`)
var res *resty.Response
var err error
for i := 0; i < 3; i++ {
res, err = d.client.R().Get(url)
if err != nil {
return err
}
// 已经登陆
if res.RawResponse.Request.URL.String() == "https://cloud.189.cn/web/main" {
return nil
}
b = res.String()
ltTextArr := ltText.FindStringSubmatch(b)
if len(ltTextArr) > 0 {
lt = ltTextArr[1]
break
} else {
<-time.After(time.Second)
}
}
if lt == "" {
return fmt.Errorf("get page: %s \nstatus: %d \nrequest url: %s\nredirect url: %s",
b, res.StatusCode(), res.RawResponse.Request.URL.String(), res.Header().Get("location"))
}
captchaToken := regexp.MustCompile(`captchaToken' value='(.+?)'`).FindStringSubmatch(b)[1]
returnUrl := regexp.MustCompile(`returnUrl = '(.+?)'`).FindStringSubmatch(b)[1]
paramId := regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(b)[1]
//reqId := regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(b)[1]
jRsakey := regexp.MustCompile(`j_rsaKey" value="(\S+)"`).FindStringSubmatch(b)[1]
vCodeID := regexp.MustCompile(`picCaptcha\.do\?token\=([A-Za-z0-9\&\=]+)`).FindStringSubmatch(b)[1]
vCodeRS := ""
if vCodeID != "" {
// need ValidateCode
log.Debugf("try to identify verification codes")
timeStamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
u := "https://open.e.189.cn/api/logbox/oauth2/picCaptcha.do?token=" + vCodeID + timeStamp
imgRes, err := d.client.R().SetHeaders(map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/76.0",
"Referer": "https://open.e.189.cn/api/logbox/oauth2/unifyAccountLogin.do",
"Sec-Fetch-Dest": "image",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "same-origin",
}).Get(u)
if err != nil {
return err
}
// Enter the verification code manually
//err = message.GetMessenger().WaitSend(message.Message{
// Type: "image",
// Content: "data:image/png;base64," + base64.StdEncoding.EncodeToString(imgRes.Body()),
//}, 10)
//if err != nil {
// return err
//}
//vCodeRS, err = message.GetMessenger().WaitReceive(30)
// use ocr api
vRes, err := base.RestyClient.R().SetMultipartField(
"image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
Post(setting.GetStr(conf.OcrApi))
if err != nil {
return err
}
if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 {
return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString())
}
vCodeRS = jsoniter.Get(vRes.Body(), "result").ToString()
log.Debugln("code: ", vCodeRS)
}
userRsa := RsaEncode([]byte(d.Username), jRsakey, true)
passwordRsa := RsaEncode([]byte(d.Password), jRsakey, true)
url = "https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do"
var loginResp LoginResp
res, err = d.client.R().
SetHeaders(map[string]string{
"lt": lt,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
"Referer": "https://open.e.189.cn/",
"accept": "application/json;charset=UTF-8",
}).SetFormData(map[string]string{
"appKey": "cloud",
"accountType": "01",
"userName": "{RSA}" + userRsa,
"password": "{RSA}" + passwordRsa,
"validateCode": vCodeRS,
"captchaToken": captchaToken,
"returnUrl": returnUrl,
"mailSuffix": "@pan.cn",
"paramId": paramId,
"clientType": "10010",
"dynamicCheck": "FALSE",
"cb_SaveName": "1",
"isOauth2": "false",
}).Post(url)
if err != nil {
return err
}
err = utils.Json.Unmarshal(res.Body(), &loginResp)
if err != nil {
log.Error(err.Error())
return err
}
if loginResp.Result != 0 {
return fmt.Errorf(loginResp.Msg)
}
_, err = d.client.R().Get(loginResp.ToUrl)
return err
}
//func (d *Cloud189) login() error {
// url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
// b := ""
// lt := ""
// ltText := regexp.MustCompile(`lt = "(.+?)"`)
// var res *resty.Response
// var err error
// for i := 0; i < 3; i++ {
// res, err = d.client.R().Get(url)
// if err != nil {
// return err
// }
// // 已经登陆
// if res.RawResponse.Request.URL.String() == "https://cloud.189.cn/web/main" {
// return nil
// }
// b = res.String()
// ltTextArr := ltText.FindStringSubmatch(b)
// if len(ltTextArr) > 0 {
// lt = ltTextArr[1]
// break
// } else {
// <-time.After(time.Second)
// }
// }
// if lt == "" {
// return fmt.Errorf("get page: %s \nstatus: %d \nrequest url: %s\nredirect url: %s",
// b, res.StatusCode(), res.RawResponse.Request.URL.String(), res.Header().Get("location"))
// }
// captchaToken := regexp.MustCompile(`captchaToken' value='(.+?)'`).FindStringSubmatch(b)[1]
// returnUrl := regexp.MustCompile(`returnUrl = '(.+?)'`).FindStringSubmatch(b)[1]
// paramId := regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(b)[1]
// //reqId := regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(b)[1]
// jRsakey := regexp.MustCompile(`j_rsaKey" value="(\S+)"`).FindStringSubmatch(b)[1]
// vCodeID := regexp.MustCompile(`picCaptcha\.do\?token\=([A-Za-z0-9\&\=]+)`).FindStringSubmatch(b)[1]
// vCodeRS := ""
// if vCodeID != "" {
// // need ValidateCode
// log.Debugf("try to identify verification codes")
// timeStamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
// u := "https://open.e.189.cn/api/logbox/oauth2/picCaptcha.do?token=" + vCodeID + timeStamp
// imgRes, err := d.client.R().SetHeaders(map[string]string{
// "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/76.0",
// "Referer": "https://open.e.189.cn/api/logbox/oauth2/unifyAccountLogin.do",
// "Sec-Fetch-Dest": "image",
// "Sec-Fetch-Mode": "no-cors",
// "Sec-Fetch-Site": "same-origin",
// }).Get(u)
// if err != nil {
// return err
// }
// // Enter the verification code manually
// //err = message.GetMessenger().WaitSend(message.Message{
// // Type: "image",
// // Content: "data:image/png;base64," + base64.StdEncoding.EncodeToString(imgRes.Body()),
// //}, 10)
// //if err != nil {
// // return err
// //}
// //vCodeRS, err = message.GetMessenger().WaitReceive(30)
// // use ocr api
// vRes, err := base.RestyClient.R().SetMultipartField(
// "image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
// Post(setting.GetStr(conf.OcrApi))
// if err != nil {
// return err
// }
// if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 {
// return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString())
// }
// vCodeRS = jsoniter.Get(vRes.Body(), "result").ToString()
// log.Debugln("code: ", vCodeRS)
// }
// userRsa := RsaEncode([]byte(d.Username), jRsakey, true)
// passwordRsa := RsaEncode([]byte(d.Password), jRsakey, true)
// url = "https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do"
// var loginResp LoginResp
// res, err = d.client.R().
// SetHeaders(map[string]string{
// "lt": lt,
// "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
// "Referer": "https://open.e.189.cn/",
// "accept": "application/json;charset=UTF-8",
// }).SetFormData(map[string]string{
// "appKey": "cloud",
// "accountType": "01",
// "userName": "{RSA}" + userRsa,
// "password": "{RSA}" + passwordRsa,
// "validateCode": vCodeRS,
// "captchaToken": captchaToken,
// "returnUrl": returnUrl,
// "mailSuffix": "@pan.cn",
// "paramId": paramId,
// "clientType": "10010",
// "dynamicCheck": "FALSE",
// "cb_SaveName": "1",
// "isOauth2": "false",
// }).Post(url)
// if err != nil {
// return err
// }
// err = utils.Json.Unmarshal(res.Body(), &loginResp)
// if err != nil {
// log.Error(err.Error())
// return err
// }
// if loginResp.Result != 0 {
// return fmt.Errorf(loginResp.Msg)
// }
// _, err = d.client.R().Get(loginResp.ToUrl)
// return err
//}
func (d *Cloud189) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
var e Error
@ -162,7 +160,7 @@ func (d *Cloud189) request(url string, method string, callback base.ReqCallback,
//log.Debug(res.String())
if e.ErrorCode != "" {
if e.ErrorCode == "InvalidSessionKey" {
err = d.login()
err = d.newLogin()
if err != nil {
return nil, err
}
@ -178,7 +176,6 @@ func (d *Cloud189) request(url string, method string, callback base.ReqCallback,
func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
res := make([]model.Obj, 0)
pageNum := 1
loc, _ := time.LoadLocation("Local")
for {
var resp Files
_, err := d.request("https://cloud.189.cn/api/open/file/listFiles.action", http.MethodGet, func(req *resty.Request) {
@ -200,7 +197,7 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
break
}
for _, folder := range resp.FileListAO.FolderList {
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", folder.LastOpTime, loc)
lastOpTime := utils.MustParseCNTime(folder.LastOpTime)
res = append(res, &model.Object{
ID: strconv.FormatInt(folder.Id, 10),
Name: folder.Name,
@ -209,7 +206,7 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
})
}
for _, file := range resp.FileListAO.FileList {
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", file.LastOpTime, loc)
lastOpTime := utils.MustParseCNTime(file.LastOpTime)
res = append(res, &model.ObjThumb{
Object: model.Object{
ID: strconv.FormatInt(file.Id, 10),
@ -307,7 +304,7 @@ func (d *Cloud189) uploadRequest(uri string, form map[string]string, resp interf
return data, nil
}
func (d *Cloud189) newUpload(dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
sessionKey, err := d.getSessionKey()
if err != nil {
return err
@ -336,6 +333,9 @@ func (d *Cloud189) newUpload(dstDir model.Obj, file model.FileStreamer, up drive
md5s := make([]string, 0)
md5Sum := md5.New()
for i = 1; i <= count; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
byteSize = file.GetSize() - finish
if DEFAULT < byteSize {
byteSize = DEFAULT
@ -365,12 +365,15 @@ func (d *Cloud189) newUpload(dstDir model.Obj, file model.FileStreamer, up drive
log.Debugf("uploadData: %+v", uploadData)
requestURL := uploadData.RequestURL
uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&")
req, _ := http.NewRequest(http.MethodPut, requestURL, bytes.NewReader(byteData))
req, err := http.NewRequest(http.MethodPut, requestURL, bytes.NewReader(byteData))
if err != nil {
return err
}
req = req.WithContext(ctx)
for _, v := range uploadHeaders {
i := strings.Index(v, "=")
req.Header.Set(v[0:i], v[i+1:])
}
r, err := base.HttpClient.Do(req)
log.Debugf("%+v %+v", r, r.Request.Header)
r.Body.Close()

View File

@ -4,7 +4,6 @@ import (
"context"
"net/http"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
@ -13,33 +12,27 @@ import (
"github.com/go-resty/resty/v2"
)
type Yun189PC struct {
type Cloud189PC struct {
model.Storage
Addition
identity string
client *resty.Client
putClient *resty.Client
loginParam *LoginParam
tokenInfo *AppSessionResp
}
func (y *Yun189PC) Config() driver.Config {
func (y *Cloud189PC) Config() driver.Config {
return config
}
func (y *Yun189PC) GetAddition() driver.Additional {
return y.Addition
func (y *Cloud189PC) GetAddition() driver.Additional {
return &y.Addition
}
func (y *Yun189PC) Init(ctx context.Context, storage model.Storage) (err error) {
y.Storage = storage
if err = utils.Json.UnmarshalFromString(y.Storage.Addition, &y.Addition); err != nil {
return err
}
func (y *Cloud189PC) Init(ctx context.Context) (err error) {
// 处理个人云和家庭云参数
if y.isFamily() && y.RootFolderID == "-11" {
y.RootFolderID = ""
@ -56,9 +49,6 @@ func (y *Yun189PC) Init(ctx context.Context, storage model.Storage) (err error)
"Referer": WEB_URL,
})
}
if y.putClient == nil {
y.putClient = base.NewRestyClient().SetTimeout(120 * time.Second)
}
// 避免重复登陆
identity := utils.GetMD5Encode(y.Username + y.Password)
@ -78,15 +68,15 @@ func (y *Yun189PC) Init(ctx context.Context, storage model.Storage) (err error)
return
}
func (y *Yun189PC) Drop(ctx context.Context) error {
func (y *Cloud189PC) Drop(ctx context.Context) error {
return nil
}
func (y *Yun189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
func (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return y.getFiles(ctx, dir.GetID())
}
func (y *Yun189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var downloadUrl struct {
URL string `json:"fileDownloadUrl"`
}
@ -145,7 +135,7 @@ func (y *Yun189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs
return like, nil
}
func (y *Yun189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
fullUrl := API_URL
if y.isFamily() {
fullUrl += "/family/file"
@ -172,7 +162,7 @@ func (y *Yun189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName str
return err
}
func (y *Yun189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
@ -196,7 +186,7 @@ func (y *Yun189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return err
}
func (y *Yun189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
queryParam := make(map[string]string)
fullUrl := API_URL
method := http.MethodPost
@ -221,7 +211,7 @@ func (y *Yun189PC) Rename(ctx context.Context, srcObj model.Obj, newName string)
return err
}
func (y *Yun189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
@ -246,7 +236,7 @@ func (y *Yun189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return err
}
func (y *Yun189PC) Remove(ctx context.Context, obj model.Obj) error {
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
@ -270,9 +260,15 @@ func (y *Yun189PC) Remove(ctx context.Context, obj model.Obj) error {
return err
}
func (y *Yun189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
if y.RapidUpload {
return y.FastUpload(ctx, dstDir, stream, up)
}
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
switch y.UploadMethod {
case "stream":
return y.CommonUpload(ctx, dstDir, stream, up)
case "old":
return y.OldUpload(ctx, dstDir, stream, up)
case "rapid":
return y.FastUpload(ctx, dstDir, stream, up)
default:
return y.CommonUpload(ctx, dstDir, stream, up)
}
}

View File

@ -11,6 +11,7 @@ import (
"encoding/hex"
"encoding/pem"
"fmt"
"math"
"net/http"
"regexp"
"strings"
@ -131,3 +132,18 @@ func BoolToNumber(b bool) int {
}
return 0
}
// 计算分片大小
// 对分片数量有限制
// 10MIB 20 MIB 999片
// 50MIB 60MIB 70MIB 80MIB ∞MIB 1999片
func partSize(size int64) int64 {
const DEFAULT = 1024 * 1024 * 10 // 10MIB
if size > DEFAULT*2*999 {
return int64(math.Max(math.Ceil((float64(size)/1999) /*=单个切片大小*/ /float64(DEFAULT)) /*=倍率*/, 5) * DEFAULT)
}
if size > DEFAULT*999 {
return DEFAULT * 2 // 20MIB
}
return DEFAULT
}

View File

@ -14,17 +14,18 @@ type Addition struct {
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
FamilyID string `json:"family_id"`
RapidUpload bool `json:"rapid_upload"`
UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"`
NoUseOcr bool `json:"no_use_ocr"`
}
var config = driver.Config{
Name: "189CloudPC",
DefaultRoot: "-11",
CheckStatus: true,
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &Yun189PC{}
op.RegisterDriver(func() driver.Driver {
return &Cloud189PC{}
})
}

View File

@ -10,20 +10,62 @@ import (
// 居然有四种返回方式
type RespErr struct {
ResCode string `json:"res_code"`
ResCode any `json:"res_code"` // int or string
ResMessage string `json:"res_message"`
Error_ string `json:"error"`
XMLName xml.Name `xml:"error"`
Code string `json:"code" xml:"code"`
Message string `json:"message" xml:"message"`
// Code string `json:"code"`
Msg string `json:"msg"`
ErrorCode string `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
}
func (e *RespErr) HasError() bool {
switch v := e.ResCode.(type) {
case int, int64, int32:
return v != 0
case string:
return e.ResCode != ""
}
return (e.Code != "" && e.Code != "SUCCESS") || e.ErrorCode != "" || e.Error_ != ""
}
func (e *RespErr) Error() string {
switch v := e.ResCode.(type) {
case int, int64, int32:
if v != 0 {
return fmt.Sprintf("res_code: %d ,res_msg: %s", v, e.ResMessage)
}
case string:
if e.ResCode != "" {
return fmt.Sprintf("res_code: %s ,res_msg: %s", e.ResCode, e.ResMessage)
}
}
if e.Code != "" && e.Code != "SUCCESS" {
if e.Msg != "" {
return fmt.Sprintf("code: %s ,msg: %s", e.Code, e.Msg)
}
if e.Message != "" {
return fmt.Sprintf("code: %s ,msg: %s", e.Code, e.Message)
}
return "code: " + e.Code
}
if e.ErrorCode != "" {
return fmt.Sprintf("err_code: %s ,err_msg: %s", e.ErrorCode, e.ErrorMsg)
}
if e.Error_ != "" {
return fmt.Sprintf("error: %s ,message: %s", e.ErrorCode, e.Message)
}
return ""
}
// 登陆需要的参数
type LoginParam struct {
// 加密后的用户名和密码
@ -218,6 +260,42 @@ type Part struct {
RequestHeader string `json:"requestHeader"`
}
/* 第二种上传方式 */
type CreateUploadFileResp struct {
// 上传文件请求ID
UploadFileId int64 `json:"uploadFileId"`
// 上传文件数据的URL路径
FileUploadUrl string `json:"fileUploadUrl"`
// 上传文件完成后确认路径
FileCommitUrl string `json:"fileCommitUrl"`
// 文件是否已存在云盘中0-未存在1-已存在
FileDataExists int `json:"fileDataExists"`
}
type GetUploadFileStatusResp struct {
CreateUploadFileResp
// 已上传的大小
DataSize int64 `json:"dataSize"`
Size int64 `json:"size"`
}
func (r *GetUploadFileStatusResp) GetSize() int64 {
return r.DataSize + r.Size
}
type CommitUploadFileResp struct {
XMLName xml.Name `xml:"file"`
Id string `xml:"id"`
Name string `xml:"name"`
Size string `xml:"size"`
Md5 string `xml:"md5"`
CreateDate string `xml:"createDate"`
Rev string `xml:"rev"`
UserId string `xml:"userId"`
}
/* query 加密参数*/
type Params map[string]string
func (p Params) Set(k, v string) {

View File

@ -6,6 +6,7 @@ import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/xml"
"fmt"
"io"
"math"
@ -15,6 +16,7 @@ import (
"os"
"regexp"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf"
@ -23,9 +25,12 @@ import (
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/avast/retry-go"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
)
const (
@ -47,7 +52,7 @@ const (
CHANNEL_ID = "web_cloud.189.cn"
)
func (y *Yun189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}) ([]byte, error) {
func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]string {
dateOfGmt := getHttpDateStr()
sessionKey := y.tokenInfo.SessionKey
sessionSecret := y.tokenInfo.SessionSecret
@ -56,19 +61,40 @@ func (y *Yun189PC) request(url, method string, callback base.ReqCallback, params
sessionSecret = y.tokenInfo.FamilySessionSecret
}
req := y.client.R().SetQueryParams(clientSuffix()).SetHeaders(map[string]string{
header := map[string]string{
"Date": dateOfGmt,
"SessionKey": sessionKey,
"X-Request-ID": uuid.NewString(),
})
"Signature": signatureOfHmac(sessionSecret, sessionKey, method, url, dateOfGmt, params),
}
return header
}
func (y *Cloud189PC) EncryptParams(params Params) string {
sessionSecret := y.tokenInfo.SessionSecret
if y.isFamily() {
sessionSecret = y.tokenInfo.FamilySessionSecret
}
if params != nil {
return AesECBEncrypt(params.Encode(), sessionSecret[:16])
}
return ""
}
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}) ([]byte, error) {
req := y.client.R().SetQueryParams(clientSuffix())
// 设置params
var paramsData string
if params != nil {
paramsData = AesECBEncrypt(params.Encode(), sessionSecret[:16])
paramsData := y.EncryptParams(params)
if paramsData != "" {
req.SetQueryParam("params", paramsData)
}
req.SetHeader("Signature", signatureOfHmac(sessionSecret, sessionKey, method, url, dateOfGmt, paramsData))
// Signature
req.SetHeaders(y.SignatureHeader(url, method, paramsData))
var erron RespErr
req.SetError(&erron)
if callback != nil {
callback(req)
@ -80,32 +106,6 @@ func (y *Yun189PC) request(url, method string, callback base.ReqCallback, params
if err != nil {
return nil, err
}
var erron RespErr
utils.Json.Unmarshal(res.Body(), &erron)
if erron.ResCode != "" {
return nil, fmt.Errorf("res_code: %s ,res_msg: %s", erron.ResCode, erron.ResMessage)
}
if erron.Code != "" && erron.Code != "SUCCESS" {
if erron.Msg != "" {
return nil, fmt.Errorf("code: %s ,msg: %s", erron.Code, erron.Msg)
}
if erron.Message != "" {
return nil, fmt.Errorf("code: %s ,msg: %s", erron.Code, erron.Message)
}
return nil, fmt.Errorf(res.String())
}
switch erron.ErrorCode {
case "":
break
case "InvalidSessionKey":
if err = y.refreshSession(); err != nil {
return nil, err
}
return y.request(url, method, callback, params, resp)
default:
return nil, fmt.Errorf("err_code: %s ,err_msg: %s", erron.ErrorCode, erron.ErrorMsg)
}
if strings.Contains(res.String(), "userSessionBO is null") {
if err = y.refreshSession(); err != nil {
@ -114,25 +114,72 @@ func (y *Yun189PC) request(url, method string, callback base.ReqCallback, params
return y.request(url, method, callback, params, resp)
}
resCode := utils.Json.Get(res.Body(), "res_code").ToInt64()
message := utils.Json.Get(res.Body(), "res_message").ToString()
switch resCode {
case 0:
return res.Body(), nil
default:
return nil, fmt.Errorf("res_code: %d ,res_msg: %s", resCode, message)
// 处理错误
if erron.HasError() {
if erron.ErrorCode == "InvalidSessionKey" {
if err = y.refreshSession(); err != nil {
return nil, err
}
return y.request(url, method, callback, params, resp)
}
return nil, &erron
}
return res.Body(), nil
}
func (y *Yun189PC) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return y.request(url, http.MethodGet, callback, nil, resp)
}
func (y *Yun189PC) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return y.request(url, http.MethodPost, callback, nil, resp)
}
func (y *Yun189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, error) {
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
if err != nil {
return nil, err
}
query := req.URL.Query()
for key, value := range clientSuffix() {
query.Add(key, value)
}
req.URL.RawQuery = query.Encode()
for key, value := range headers {
req.Header.Add(key, value)
}
if sign {
for key, value := range y.SignatureHeader(url, http.MethodPut, "") {
req.Header.Add(key, value)
}
}
resp, err := base.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var erron RespErr
jsoniter.Unmarshal(body, &erron)
xml.Unmarshal(body, &erron)
if erron.HasError() {
return nil, &erron
}
if resp.StatusCode != http.StatusOK {
return nil, errors.Errorf("put fail,err:%s", string(body))
}
return body, nil
}
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, error) {
fullUrl := API_URL
if y.isFamily() {
fullUrl += "/family/file"
@ -184,9 +231,9 @@ func (y *Yun189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, er
return res, nil
}
func (y *Yun189PC) login() (err error) {
func (y *Cloud189PC) login() (err error) {
// 初始化登陆所需参数
if y.loginParam == nil || !y.NoUseOcr {
if y.loginParam == nil {
if err = y.initLoginParam(); err != nil {
// 验证码也通过错误返回
return err
@ -197,7 +244,7 @@ func (y *Yun189PC) login() (err error) {
y.VCode = ""
// 销毁登陆参数
y.loginParam = nil
// 遇到错误,重新加载登陆参数
// 遇到错误,重新加载登陆参数(刷新验证码)
if err != nil && y.NoUseOcr {
if err1 := y.initLoginParam(); err1 != nil {
err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
@ -249,9 +296,8 @@ func (y *Yun189PC) login() (err error) {
return
}
if erron.ResCode != "" {
err = fmt.Errorf(erron.ResMessage)
return
if erron.HasError() {
return &erron
}
if tokenInfo.ResCode != 0 {
err = fmt.Errorf(tokenInfo.ResMessage)
@ -264,7 +310,7 @@ func (y *Yun189PC) login() (err error) {
/* 初始化登陆需要的参数
* 如果遇到验证码返回错误
*/
func (y *Yun189PC) initLoginParam() error {
func (y *Cloud189PC) initLoginParam() error {
// 清除cookie
jar, _ := cookiejar.New(nil)
y.client.SetCookieJar(jar)
@ -304,6 +350,22 @@ func (y *Yun189PC) initLoginParam() error {
param.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Password)
y.loginParam = &param
// 判断是否需要验证码
resp, err := y.client.R().
SetHeader("REQID", param.ReqId).
SetFormData(map[string]string{
"appKey": APP_ID,
"accountType": ACCOUNT_TYPE,
"userName": param.RsaUsername,
}).Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do")
if err != nil {
return err
}
if resp.String() == "0" {
return nil
}
// 拉取验证码
imgRes, err := y.client.R().
SetQueryParams(map[string]string{
"token": param.CaptchaToken,
@ -335,7 +397,7 @@ func (y *Yun189PC) initLoginParam() error {
}
// 刷新会话
func (y *Yun189PC) refreshSession() (err error) {
func (y *Cloud189PC) refreshSession() (err error) {
var erron RespErr
var userSessionResp UserSessionResp
_, err = y.client.R().
@ -359,33 +421,23 @@ func (y *Yun189PC) refreshSession() (err error) {
}
}()
switch erron.ResCode {
case "":
break
case "UserInvalidOpenToken":
if erron.HasError() {
if erron.ResCode == "UserInvalidOpenToken" {
if err = y.login(); err != nil {
return err
}
default:
err = fmt.Errorf("res_code: %s ,res_msg: %s", erron.ResCode, erron.ResMessage)
return
}
switch userSessionResp.ResCode {
case 0:
return &erron
}
y.tokenInfo.UserSessionResp = userSessionResp
default:
err = fmt.Errorf("code: %d , msg: %s", userSessionResp.ResCode, userSessionResp.ResMessage)
}
return
}
// 普通上传
func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (err error) {
const DEFAULT int64 = 10485760
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
func (y *Cloud189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (err error) {
var DEFAULT = partSize(file.GetSize())
var count = int(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
requestID := uuid.NewString()
params := Params{
"parentFolderId": dstDir.GetID(),
"fileName": url.QueryEscape(file.GetName()),
@ -407,7 +459,6 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
var initMultiUpload InitMultiUploadResp
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, params, &initMultiUpload)
if err != nil {
return err
@ -417,11 +468,9 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
silceMd5 := md5.New()
silceMd5Hexs := make([]string, 0, count)
byteData := bytes.NewBuffer(make([]byte, DEFAULT))
for i := int64(1); i <= count; i++ {
select {
case <-ctx.Done():
for i := 1; i <= count; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
default:
}
// 读取块
@ -442,7 +491,6 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
_, err = y.request(fullUrl+"/getMultiUploadUrls", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"partInfo": fmt.Sprintf("%d-%s", i, silceMd5Base64),
"uploadFileId": initMultiUpload.Data.UploadFileID,
@ -453,18 +501,18 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
// 开始上传
uploadData := uploadUrl.UploadUrls[fmt.Sprint("partNumber_", i)]
res, err := y.putClient.R().
SetContext(ctx).
SetQueryParams(clientSuffix()).
SetHeaders(ParseHttpHeader(uploadData.RequestHeader)).
SetBody(byteData).
Put(uploadData.RequestURL)
err = retry.Do(func() error {
_, err := y.put(ctx, uploadData.RequestURL, ParseHttpHeader(uploadData.RequestHeader), false, bytes.NewReader(byteData.Bytes()))
return err
},
retry.Context(ctx),
retry.Attempts(3),
retry.Delay(time.Second),
retry.MaxDelay(5*time.Second))
if err != nil {
return err
}
if res.StatusCode() != http.StatusOK {
return fmt.Errorf("updload fail,msg: %s", res.String())
}
up(int(i * 100 / count))
}
@ -478,7 +526,6 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
_, err = y.request(fullUrl+"/commitMultiUploadFile", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"uploadFileId": initMultiUpload.Data.UploadFileID,
"fileMd5": fileMd5Hex,
@ -491,7 +538,7 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
}
// 快传
func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (err error) {
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (err error) {
// 需要获取完整文件md5,必须支持 io.Seek
tempFile, err := utils.CreateTempFile(file.GetReadCloser())
if err != nil {
@ -502,7 +549,7 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
_ = os.Remove(tempFile.Name())
}()
const DEFAULT int64 = 10485760
var DEFAULT = partSize(file.GetSize())
count := int(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
// 优先计算所需信息
@ -511,10 +558,8 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
silceMd5Hexs := make([]string, 0, count)
silceMd5Base64s := make([]string, 0, count)
for i := 1; i <= count; i++ {
select {
case <-ctx.Done():
if utils.IsCanceled(ctx) {
return ctx.Err()
default:
}
silceMd5.Reset()
@ -535,7 +580,6 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n")))
}
requestID := uuid.NewString()
// 检测是否支持快传
params := Params{
"parentFolderId": dstDir.GetID(),
@ -558,7 +602,6 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
var uploadInfo InitMultiUploadResp
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, params, &uploadInfo)
if err != nil {
return err
@ -570,7 +613,6 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
_, err = y.request(fullUrl+"/getMultiUploadUrls", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"uploadFileId": uploadInfo.Data.UploadFileID,
"partInfo": strings.Join(silceMd5Base64s, ","),
@ -579,26 +621,29 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
return err
}
buf := make([]byte, DEFAULT)
for i := 1; i <= count; i++ {
select {
case <-ctx.Done():
if utils.IsCanceled(ctx) {
return ctx.Err()
default:
}
n, err := io.ReadFull(tempFile, buf)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return err
}
uploadData := uploadUrls.UploadUrls[fmt.Sprint("partNumber_", i)]
res, err := y.putClient.R().
SetContext(ctx).
SetQueryParams(clientSuffix()).
SetHeaders(ParseHttpHeader(uploadData.RequestHeader)).
SetBody(io.LimitReader(tempFile, DEFAULT)).
Put(uploadData.RequestURL)
err = retry.Do(func() error {
_, err := y.put(ctx, uploadData.RequestURL, ParseHttpHeader(uploadData.RequestHeader), false, bytes.NewReader(buf[:n]))
return err
},
retry.Context(ctx),
retry.Attempts(3),
retry.Delay(time.Second),
retry.MaxDelay(5*time.Second))
if err != nil {
return err
}
if res.StatusCode() != http.StatusOK {
return fmt.Errorf("updload fail,msg: %s", res.String())
}
up(int(i * 100 / count))
}
}
@ -607,7 +652,6 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
_, err = y.request(fullUrl+"/commitMultiUploadFile", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"uploadFileId": uploadInfo.Data.UploadFileID,
"isLog": "0",
@ -616,11 +660,142 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
return err
}
func (y *Yun189PC) isFamily() bool {
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (err error) {
// 需要获取完整文件md5,必须支持 io.Seek
tempFile, err := utils.CreateTempFile(file.GetReadCloser())
if err != nil {
return err
}
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
// 计算md5
fileMd5 := md5.New()
if _, err := io.Copy(fileMd5, tempFile); err != nil {
return err
}
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return err
}
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
// 创建上传会话
var uploadInfo CreateUploadFileResp
fullUrl := API_URL + "/createUploadFile.action"
if y.isFamily() {
fullUrl = API_URL + "/family/file/createFamilyFile.action"
}
_, err = y.post(fullUrl, func(req *resty.Request) {
req.SetContext(ctx)
if y.isFamily() {
req.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"fileMd5": fileMd5Hex,
"fileName": file.GetName(),
"fileSize": fmt.Sprint(file.GetSize()),
"parentId": dstDir.GetID(),
"resumePolicy": "1",
})
} else {
req.SetFormData(map[string]string{
"parentFolderId": dstDir.GetID(),
"fileName": file.GetName(),
"size": fmt.Sprint(file.GetSize()),
"md5": fileMd5Hex,
"opertype": "3",
"flag": "1",
"resumePolicy": "1",
"isLog": "0",
// "baseFileId": "",
// "lastWrite":"",
// "localPath": strings.ReplaceAll(param.LocalPath, "\\", "/"),
// "fileExt": "",
})
}
}, &uploadInfo)
if err != nil {
return err
}
// 网盘中不存在该文件,开始上传
status := GetUploadFileStatusResp{CreateUploadFileResp: uploadInfo}
for status.Size < file.GetSize() && status.FileDataExists != 1 {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
header := map[string]string{
"ResumePolicy": "1",
"Expect": "100-continue",
}
if y.isFamily() {
header["FamilyId"] = fmt.Sprint(y.FamilyID)
header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
} else {
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
}
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile))
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
return err
}
// 获取断点状态
fullUrl := API_URL + "/getUploadFileStatus.action"
if y.isFamily() {
fullUrl = API_URL + "/family/file/getFamilyFileStatus.action"
}
_, err = y.get(fullUrl, func(req *resty.Request) {
req.SetContext(ctx).SetQueryParams(map[string]string{
"uploadFileId": fmt.Sprint(status.UploadFileId),
"resumePolicy": "1",
})
if y.isFamily() {
req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
}
}, &status)
if err != nil {
return err
}
if _, err := tempFile.Seek(status.GetSize(), io.SeekStart); err != nil {
return err
}
up(int(status.Size / file.GetSize()))
}
// 提交
var resp CommitUploadFileResp
_, err = y.post(status.FileCommitUrl, func(req *resty.Request) {
req.SetContext(ctx)
if y.isFamily() {
req.SetHeaders(map[string]string{
"ResumePolicy": "1",
"UploadFileId": fmt.Sprint(status.UploadFileId),
"FamilyId": fmt.Sprint(y.FamilyID),
})
} else {
req.SetFormData(map[string]string{
"opertype": "3",
"resumePolicy": "1",
"uploadFileId": fmt.Sprint(status.UploadFileId),
"isLog": "0",
})
}
}, &resp)
return err
}
func (y *Cloud189PC) isFamily() bool {
return y.Type == "family"
}
func (y *Yun189PC) isLogin() bool {
func (y *Cloud189PC) isLogin() bool {
if y.tokenInfo == nil {
return false
}
@ -629,7 +804,7 @@ func (y *Yun189PC) isLogin() bool {
}
// 获取家庭云所有用户信息
func (y *Yun189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
func (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
var resp FamilyInfoListResp
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp)
if err != nil {
@ -639,7 +814,7 @@ func (y *Yun189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
}
// 抽取家庭云ID
func (y *Yun189PC) getFamilyID() (string, error) {
func (y *Cloud189PC) getFamilyID() (string, error) {
infos, err := y.getFamilyInfoList()
if err != nil {
return "", err

114
drivers/alias/driver.go Normal file
View File

@ -0,0 +1,114 @@
package alias
import (
"context"
"errors"
"strings"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
)
type Alias struct {
model.Storage
Addition
pathMap map[string][]string
autoFlatten bool
oneKey string
}
func (d *Alias) Config() driver.Config {
return config
}
func (d *Alias) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Alias) Init(ctx context.Context) error {
if d.Paths == "" {
return errors.New("paths is required")
}
d.pathMap = make(map[string][]string)
for _, path := range strings.Split(d.Paths, "\n") {
path = strings.TrimSpace(path)
if path == "" {
continue
}
k, v := getPair(path)
d.pathMap[k] = append(d.pathMap[k], v)
}
if len(d.pathMap) == 1 {
for k := range d.pathMap {
d.oneKey = k
}
d.autoFlatten = true
}
return nil
}
func (d *Alias) Drop(ctx context.Context) error {
d.pathMap = nil
return nil
}
func (d *Alias) Get(ctx context.Context, path string) (model.Obj, error) {
if utils.PathEqual(path, "/") {
return &model.Object{
Name: "Root",
IsFolder: true,
Path: "/",
}, nil
}
root, sub := d.getRootAndPath(path)
dsts, ok := d.pathMap[root]
if !ok {
return nil, errs.ObjectNotFound
}
for _, dst := range dsts {
obj, err := d.get(ctx, path, dst, sub)
if err == nil {
return obj, nil
}
}
return nil, errs.ObjectNotFound
}
func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
path := dir.GetPath()
if utils.PathEqual(path, "/") && !d.autoFlatten {
return d.listRoot(), nil
}
root, sub := d.getRootAndPath(path)
dsts, ok := d.pathMap[root]
if !ok {
return nil, errs.ObjectNotFound
}
var objs []model.Obj
for _, dst := range dsts {
tmp, err := d.list(ctx, dst, sub)
if err == nil {
objs = append(objs, tmp...)
}
}
return objs, nil
}
func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
root, sub := d.getRootAndPath(file.GetPath())
dsts, ok := d.pathMap[root]
if !ok {
return nil, errs.ObjectNotFound
}
for _, dst := range dsts {
link, err := d.link(ctx, dst, sub, args)
if err == nil {
return link, nil
}
}
return nil, errs.ObjectNotFound
}
var _ driver.Driver = (*Alias)(nil)

27
drivers/alias/meta.go Normal file
View File

@ -0,0 +1,27 @@
package alias
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
// Usually one of two
// driver.RootPath
// define other
Paths string `json:"paths" required:"true" type:"text"`
}
var config = driver.Config{
Name: "Alias",
LocalSort: true,
NoCache: true,
NoUpload: true,
DefaultRoot: "/",
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Alias{}
})
}

1
drivers/alias/types.go Normal file
View File

@ -0,0 +1 @@
package alias

114
drivers/alias/util.go Normal file
View File

@ -0,0 +1,114 @@
package alias
import (
"context"
"fmt"
stdpath "path"
"strings"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/sign"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
)
func (d *Alias) listRoot() []model.Obj {
var objs []model.Obj
for k, _ := range d.pathMap {
obj := model.Object{
Name: k,
IsFolder: true,
Modified: d.Modified,
}
objs = append(objs, &obj)
}
return objs
}
// do others that not defined in Driver interface
func getPair(path string) (string, string) {
//path = strings.TrimSpace(path)
if strings.Contains(path, ":") {
pair := strings.SplitN(path, ":", 2)
if !strings.Contains(pair[0], "/") {
return pair[0], pair[1]
}
}
return stdpath.Base(path), path
}
func (d *Alias) getRootAndPath(path string) (string, string) {
if d.autoFlatten {
return d.oneKey, path
}
path = strings.TrimPrefix(path, "/")
parts := strings.SplitN(path, "/", 2)
if len(parts) == 1 {
return parts[0], ""
}
return parts[0], parts[1]
}
func (d *Alias) get(ctx context.Context, path string, dst, sub string) (model.Obj, error) {
obj, err := fs.Get(ctx, stdpath.Join(dst, sub), &fs.GetArgs{NoLog: true})
if err != nil {
return nil, err
}
return &model.Object{
Path: path,
Name: obj.GetName(),
Size: obj.GetSize(),
Modified: obj.ModTime(),
IsFolder: obj.IsDir(),
}, nil
}
func (d *Alias) list(ctx context.Context, dst, sub string) ([]model.Obj, error) {
objs, err := fs.List(ctx, stdpath.Join(dst, sub), &fs.ListArgs{NoLog: true})
// the obj must implement the model.SetPath interface
// return objs, err
if err != nil {
return nil, err
}
return utils.SliceConvert(objs, func(obj model.Obj) (model.Obj, error) {
thumb, ok := model.GetThumb(obj)
objRes := model.Object{
Name: obj.GetName(),
Size: obj.GetSize(),
Modified: obj.ModTime(),
IsFolder: obj.IsDir(),
}
if !ok {
return &objRes, nil
}
return &model.ObjThumb{
Object: objRes,
Thumbnail: model.Thumbnail{
Thumbnail: thumb,
},
}, nil
})
}
func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) {
reqPath := stdpath.Join(dst, sub)
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
if err != nil {
return nil, err
}
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
if err != nil {
return nil, err
}
if common.ShouldProxy(storage, stdpath.Base(sub)) {
return &model.Link{
URL: fmt.Sprintf("%s/p%s?sign=%s",
common.GetApiUrl(args.HttpReq),
utils.EncodePath(reqPath, true),
sign.Sign(reqPath)),
}, nil
}
link, _, err := fs.Link(ctx, reqPath, args)
return link, err
}

118
drivers/alist_v2/driver.go Normal file
View File

@ -0,0 +1,118 @@
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)

26
drivers/alist_v2/meta.go Normal file
View File

@ -0,0 +1,26 @@
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{}
})
}

31
drivers/alist_v2/types.go Normal file
View File

@ -0,0 +1,31 @@
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"`
}

1
drivers/alist_v2/util.go Normal file
View File

@ -0,0 +1 @@
package alist_v2

View File

@ -2,14 +2,19 @@ package alist_v3
import (
"context"
"fmt"
"net/http"
"path"
"strconv"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/alist-org/alist/v3/server/handles"
"github.com/go-resty/resty/v2"
)
type AListV3 struct {
@ -22,17 +27,43 @@ func (d *AListV3) Config() driver.Config {
}
func (d *AListV3) GetAddition() driver.Additional {
return d.Addition
return &d.Addition
}
func (d *AListV3) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &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
}
// TODO login / refresh token
//op.MustSaveDriverStorage(d)
// if the username is not empty and the username is not the same as the current username, then login again
if d.Username != "" && d.Username != resp.Data.Username {
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
}
@ -41,20 +72,18 @@ func (d *AListV3) Drop(ctx context.Context) error {
}
func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
url := d.Address + "/api/fs/list"
var resp common.Resp[handles.FsListResp]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(handles.ListReq{
PageReq: common.PageReq{
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.Password,
Password: d.MetaPassword,
Refresh: false,
}).Post(url)
})
})
if err != nil {
return nil, err
}
@ -74,21 +103,14 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
return files, nil
}
//func (d *AList) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
url := d.Address + "/api/fs/get"
var resp common.Resp[handles.FsGetResp]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(handles.FsGetReq{
var resp common.Resp[FsGetResp]
_, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(FsGetReq{
Path: file.GetPath(),
Password: d.Password,
}).Post(url)
Password: d.MetaPassword,
})
})
if err != nil {
return nil, err
}
@ -98,27 +120,65 @@ func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
}
func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotImplement
_, 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 {
return errs.NotImplement
_, 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 {
return errs.NotImplement
_, 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 {
return errs.NotImplement
_, 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 {
return errs.NotImplement
_, 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 {
return errs.NotImplement
_, err := d.request("/fs/put", http.MethodPut, func(req *resty.Request) {
req.SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
SetHeader("Password", d.MetaPassword).
SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
SetContentLength(true).
SetBody(stream.GetReadCloser())
})
return err
}
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {

View File

@ -8,19 +8,21 @@ import (
type Addition struct {
driver.RootPath
Address string `json:"url" required:"true"`
MetaPassword string `json:"meta_password"`
Username string `json:"username"`
Password string `json:"password"`
AccessToken string `json:"access_token"`
Token string `json:"token"`
}
var config = driver.Config{
Name: "AList V3",
LocalSort: true,
NoUpload: true,
DefaultRoot: "/",
CheckStatus: true,
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
op.RegisterDriver(func() driver.Driver {
return &AListV3{}
})
}

View File

@ -1 +1,81 @@
package alist_v3
import (
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type ListReq struct {
model.PageReq
Path string `json:"path" form:"path"`
Password string `json:"password" form:"password"`
Refresh bool `json:"refresh"`
}
type ObjResp struct {
Name string `json:"name"`
Size int64 `json:"size"`
IsDir bool `json:"is_dir"`
Modified time.Time `json:"modified"`
Sign string `json:"sign"`
Thumb string `json:"thumb"`
Type int `json:"type"`
}
type FsListResp struct {
Content []ObjResp `json:"content"`
Total int64 `json:"total"`
Readme string `json:"readme"`
Write bool `json:"write"`
Provider string `json:"provider"`
}
type FsGetReq struct {
Path string `json:"path" form:"path"`
Password string `json:"password" form:"password"`
}
type FsGetResp struct {
ObjResp
RawURL string `json:"raw_url"`
Readme string `json:"readme"`
Provider string `json:"provider"`
Related []ObjResp `json:"related"`
}
type MkdirOrLinkReq struct {
Path string `json:"path" form:"path"`
}
type MoveCopyReq struct {
SrcDir string `json:"src_dir"`
DstDir string `json:"dst_dir"`
Names []string `json:"names"`
}
type RenameReq struct {
Path string `json:"path"`
Name string `json:"name"`
}
type RemoveReq struct {
Dir string `json:"dir"`
Names []string `json:"names"`
}
type LoginResp struct {
Token string `json:"token"`
}
type MeResp struct {
Id int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
BasePath string `json:"base_path"`
Role int `json:"role"`
Disabled bool `json:"disabled"`
Permission int `json:"permission"`
SsoId string `json:"sso_id"`
Otp bool `json:"otp"`
}

View File

@ -1 +1,58 @@
package alist_v3
import (
"fmt"
"net/http"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
func (d *AListV3) login() error {
var resp common.Resp[LoginResp]
_, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) {
req.SetResult(&resp).SetBody(base.Json{
"username": d.Username,
"password": d.Password,
})
})
if err != nil {
return err
}
d.Token = resp.Data.Token
op.MustSaveDriverStorage(d)
return nil
}
func (d *AListV3) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
url := d.Address + "/api" + api
req := base.RestyClient.R()
req.SetHeader("Authorization", d.Token)
if callback != nil {
callback(req)
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
log.Debugf("[alist_v3] response body: %s", res.String())
if res.StatusCode() >= 400 {
return nil, fmt.Errorf("request failed, status: %s", res.Status())
}
code := utils.Json.Get(res.Body(), "code").ToInt()
if code != 200 {
if (code == 401 || code == 403) && !utils.IsBool(retry...) {
err = d.login()
if err != nil {
return nil, err
}
return d.request(api, method, callback, true)
}
return nil, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString())
}
return res.Body(), nil
}

View File

@ -31,6 +31,7 @@ type AliDrive struct {
AccessToken string
cron *cron.Cron
DriveId string
UserID string
}
func (d *AliDrive) Config() driver.Config {
@ -38,18 +39,13 @@ func (d *AliDrive) Config() driver.Config {
}
func (d *AliDrive) GetAddition() driver.Additional {
return d.Addition
return &d.Addition
}
func (d *AliDrive) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
func (d *AliDrive) Init(ctx context.Context) error {
// TODO login / refresh token
//op.MustSaveDriverStorage(d)
err = d.refreshToken()
err := d.refreshToken()
if err != nil {
return err
}
@ -59,6 +55,7 @@ func (d *AliDrive) Init(ctx context.Context, storage model.Storage) error {
return err
}
d.DriveId = utils.Json.Get(res, "default_drive_id").ToString()
d.UserID = utils.Json.Get(res, "user_id").ToString()
d.cron = cron.NewCron(time.Hour * 2)
d.cron.Do(func() {
err := d.refreshToken()
@ -66,7 +63,22 @@ func (d *AliDrive) Init(ctx context.Context, storage model.Storage) error {
log.Errorf("%+v", err)
}
})
return err
if global.Has(d.UserID) {
return nil
}
// init deviceID
deviceID := utils.GetSHA256Encode(d.UserID)
// init privateKey
privateKey, _ := NewPrivateKeyFromHex(deviceID)
state := State{
privateKey: privateKey,
deviceID: deviceID,
}
// store state
global.Store(d.UserID, &state)
// init signature
d.sign()
return nil
}
func (d *AliDrive) Drop(ctx context.Context) error {
@ -86,11 +98,6 @@ func (d *AliDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs)
})
}
//func (d *AliDrive) Get(ctx context.Context, path string) (model.Obj, error) {
// // TODO this is optional
// return nil, errs.NotImplement
//}
func (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
data := base.Json{
"drive_id": d.DriveId,
@ -179,10 +186,19 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
"type": "file",
}
var localFile *os.File
if fileStream, ok := file.ReadCloser.(*model.FileStream); ok {
localFile, _ = fileStream.ReadCloser.(*os.File)
}
if d.RapidUpload {
buf := bytes.NewBuffer(make([]byte, 0, 1024))
io.CopyN(buf, file, 1024)
reqBody["pre_hash"] = utils.GetSHA1Encode(buf.String())
if localFile != nil {
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
return err
}
} else {
// 把头部拼接回去
file.ReadCloser = struct {
io.Reader
@ -191,6 +207,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
Reader: io.MultiReader(buf, file),
Closer: file,
}
}
} else {
reqBody["content_hash_name"] = "none"
reqBody["proof_version"] = "v1"
@ -206,6 +223,16 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
}
if d.RapidUpload && e.Code == "PreHashMatched" {
delete(reqBody, "pre_hash")
h := sha1.New()
if localFile != nil {
if err = utils.CopyWithCtx(ctx, h, localFile, 0, nil); err != nil {
return err
}
if _, err = localFile.Seek(0, io.SeekStart); err != nil {
return err
}
} else {
tempFile, err := os.CreateTemp(conf.Conf.TempDir, "file-*")
if err != nil {
return err
@ -214,11 +241,11 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
delete(reqBody, "pre_hash")
h := sha1.New()
if _, err = io.Copy(io.MultiWriter(tempFile, h), file); err != nil {
if err = utils.CopyWithCtx(ctx, io.MultiWriter(tempFile, h), file, 0, nil); err != nil {
return err
}
localFile = tempFile
}
reqBody["content_hash"] = hex.EncodeToString(h.Sum(nil))
reqBody["content_hash_name"] = "sha1"
reqBody["proof_version"] = "v1"
@ -238,7 +265,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
if file.GetSize() > 0 {
o = r.Mod(r, i)
}
n, _ := io.NewSectionReader(tempFile, o.Int64(), 8).Read(buf[:8])
n, _ := io.NewSectionReader(localFile, o.Int64(), 8).Read(buf[:8])
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
_, err, e := d.request("https://api.aliyundrive.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
@ -251,17 +278,25 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return nil
}
// 秒传失败
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
if _, err = localFile.Seek(0, io.SeekStart); err != nil {
return err
}
file.ReadCloser = tempFile
file.ReadCloser = localFile
}
for i, partInfo := range resp.PartInfoList {
req, err := http.NewRequest("PUT", partInfo.UploadUrl, io.LimitReader(file, DEFAULT))
if utils.IsCanceled(ctx) {
return ctx.Err()
}
url := partInfo.UploadUrl
if d.InternalUpload {
url = partInfo.InternalUploadUrl
}
req, err := http.NewRequest("PUT", url, io.LimitReader(file, DEFAULT))
if err != nil {
return err
}
req = req.WithContext(ctx)
res, err := base.HttpClient.Do(req)
if err != nil {
return err
@ -302,6 +337,7 @@ func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}
case "video_preview":
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
data["category"] = "live_transcoding"
data["url_expire_sec"] = 14400
default:
return nil, errs.NotSupport
}

View File

@ -0,0 +1,16 @@
package aliyundrive
import (
"crypto/ecdsa"
"github.com/alist-org/alist/v3/pkg/generic_sync"
)
type State struct {
deviceID string
signature string
retry int
privateKey *ecdsa.PrivateKey
}
var global = generic_sync.MapOf[string, *State]{}

View File

@ -0,0 +1,66 @@
package aliyundrive
import (
"crypto/ecdsa"
"crypto/rand"
"encoding/hex"
"math/big"
"github.com/dustinxie/ecc"
)
func NewPrivateKey() (*ecdsa.PrivateKey, error) {
p256k1 := ecc.P256k1()
return ecdsa.GenerateKey(p256k1, rand.Reader)
}
func NewPrivateKeyFromHex(hex_ string) (*ecdsa.PrivateKey, error) {
data, err := hex.DecodeString(hex_)
if err != nil {
return nil, err
}
return NewPrivateKeyFromBytes(data), nil
}
func NewPrivateKeyFromBytes(priv []byte) *ecdsa.PrivateKey {
p256k1 := ecc.P256k1()
x, y := p256k1.ScalarBaseMult(priv)
return &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: p256k1,
X: x,
Y: y,
},
D: new(big.Int).SetBytes(priv),
}
}
func PrivateKeyToHex(private *ecdsa.PrivateKey) string {
return hex.EncodeToString(PrivateKeyToBytes(private))
}
func PrivateKeyToBytes(private *ecdsa.PrivateKey) []byte {
return private.D.Bytes()
}
func PublicKeyToHex(public *ecdsa.PublicKey) string {
return hex.EncodeToString(PublicKeyToBytes(public))
}
func PublicKeyToBytes(public *ecdsa.PublicKey) []byte {
x := public.X.Bytes()
if len(x) < 32 {
for i := 0; i < 32-len(x); i++ {
x = append([]byte{0}, x...)
}
}
y := public.Y.Bytes()
if len(y) < 32 {
for i := 0; i < 32-len(y); i++ {
y = append([]byte{0}, y...)
}
}
return append(x, y...)
}

View File

@ -8,20 +8,23 @@ import (
type Addition struct {
driver.RootID
RefreshToken string `json:"refresh_token" required:"true"`
//DeviceID string `json:"device_id" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
RapidUpload bool `json:"rapid_upload"`
InternalUpload bool `json:"internal_upload"`
}
var config = driver.Config{
Name: "Aliyundrive",
DefaultRoot: "root",
}
func New() driver.Driver {
return &AliDrive{}
Alert: `warning|There may be an infinite loop bug in this driver.
Deprecated, no longer maintained and will be removed in a future version.
We recommend using the official driver AliyundriveOpen.`,
}
func init() {
op.RegisterDriver(config, New)
op.RegisterDriver(func() driver.Driver {
return &AliDrive{}
})
}

View File

@ -49,6 +49,7 @@ type UploadResp struct {
UploadId string `json:"upload_id"`
PartInfoList []struct {
UploadUrl string `json:"upload_url"`
InternalUploadUrl string `json:"internal_upload_url"`
} `json:"part_info_list"`
RapidUpload bool `json:"rapid_upload"`

View File

@ -1,6 +1,8 @@
package aliyundrive
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"
@ -8,9 +10,51 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/dustinxie/ecc"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
)
func (d *AliDrive) createSession() error {
state, ok := global.Load(d.UserID)
if !ok {
return fmt.Errorf("can't load user state, user_id: %s", d.UserID)
}
d.sign()
state.retry++
if state.retry > 3 {
state.retry = 0
return fmt.Errorf("createSession failed after three retries")
}
_, err, _ := d.request("https://api.aliyundrive.com/users/v1/users/device/create_session", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"deviceName": "samsung",
"modelName": "SM-G9810",
"nonce": 0,
"pubKey": PublicKeyToHex(&state.privateKey.PublicKey),
"refreshToken": d.RefreshToken,
})
}, nil)
if err == nil{
state.retry = 0
}
return err
}
// func (d *AliDrive) renewSession() error {
// _, err, _ := d.request("https://api.aliyundrive.com/users/v1/users/device/renew_session", http.MethodPost, nil, nil)
// return err
// }
func (d *AliDrive) sign() {
state, _ := global.Load(d.UserID)
secpAppID := "5dde4e1bdf9e4966b387ba58f4b3fdc3"
singdata := fmt.Sprintf("%s:%s:%s:%d", secpAppID, state.deviceID, d.UserID, 0)
hash := sha256.Sum256([]byte(singdata))
data, _ := ecc.SignBytes(state.privateKey, hash[:], ecc.RecID|ecc.LowerS)
state.signature = hex.EncodeToString(data) //strconv.Itoa(state.nonce)
}
// do others that not defined in Driver interface
func (d *AliDrive) refreshToken() error {
@ -29,6 +73,9 @@ func (d *AliDrive) refreshToken() error {
if e.Code != "" {
return fmt.Errorf("failed to refresh token: %s", e.Message)
}
if resp.RefreshToken == "" {
return errors.New("failed to refresh token: refresh token is empty")
}
d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
op.MustSaveDriverStorage(d)
return nil
@ -36,9 +83,24 @@ func (d *AliDrive) refreshToken() error {
func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error, RespErr) {
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer\t"+d.AccessToken)
req.SetHeader("content-type", "application/json")
req.SetHeader("origin", "https://www.aliyundrive.com")
state, ok := global.Load(d.UserID)
if !ok {
if url == "https://api.aliyundrive.com/v2/user/get" {
state = &State{}
} else {
return nil, fmt.Errorf("can't load user state, user_id: %s", d.UserID), RespErr{}
}
}
req.SetHeaders(map[string]string{
"Authorization": "Bearer\t" + d.AccessToken,
"content-type": "application/json",
"origin": "https://www.aliyundrive.com",
"Referer": "https://aliyundrive.com/",
"X-Signature": state.signature,
"x-request-id": uuid.NewString(),
"X-Canary": "client=Android,app=adrive,version=v4.1.0",
"X-Device-Id": state.deviceID,
})
if callback != nil {
callback(req)
} else {
@ -54,15 +116,24 @@ func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp i
return nil, err, e
}
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
switch e.Code {
case "AccessTokenInvalid":
err = d.refreshToken()
if err != nil {
return nil, err, e
}
return d.request(url, method, callback, resp)
case "DeviceSessionSignatureInvalid":
err = d.createSession()
if err != nil {
return nil, err, e
}
default:
return nil, errors.New(e.Message), e
}
return d.request(url, method, callback, resp)
} else if res.IsError() {
return nil, errors.New("bad status code " + res.Status()), e
}
return res.Body(), nil, e
}

View File

@ -0,0 +1,243 @@
package aliyundrive_open
import (
"context"
"io"
"math"
"net/http"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
type AliyundriveOpen struct {
model.Storage
Addition
base string
DriveId string
limitList func(ctx context.Context, data base.Json) (*Files, error)
limitLink func(ctx context.Context, file model.Obj) (*model.Link, error)
}
func (d *AliyundriveOpen) Config() driver.Config {
return config
}
func (d *AliyundriveOpen) GetAddition() driver.Additional {
return &d.Addition
}
func (d *AliyundriveOpen) Init(ctx context.Context) error {
res, err := d.request("/adrive/v1.0/user/getDriveInfo", http.MethodPost, nil)
if err != nil {
return err
}
d.DriveId = utils.Json.Get(res, "default_drive_id").ToString()
d.limitList = utils.LimitRateCtx(d.list, time.Second/4)
d.limitLink = utils.LimitRateCtx(d.link, time.Second)
return nil
}
func (d *AliyundriveOpen) Drop(ctx context.Context) error {
return nil
}
func (d *AliyundriveOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles(ctx, dir.GetID())
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
return fileToObj(src), nil
})
}
func (d *AliyundriveOpen) link(ctx context.Context, file model.Obj) (*model.Link, error) {
res, err := d.request("/adrive/v1.0/openFile/getDownloadUrl", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"drive_id": d.DriveId,
"file_id": file.GetID(),
"expire_sec": 14400,
})
})
if err != nil {
return nil, err
}
url := utils.Json.Get(res, "url").ToString()
exp := time.Hour
return &model.Link{
URL: url,
Expiration: &exp,
}, nil
}
func (d *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
return d.limitLink(ctx, file)
}
func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"drive_id": d.DriveId,
"parent_file_id": parentDir.GetID(),
"name": dirName,
"type": "folder",
"check_name_mode": "refuse",
})
})
return err
}
func (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := d.request("/adrive/v1.0/openFile/move", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"drive_id": d.DriveId,
"file_id": srcObj.GetID(),
"to_parent_file_id": dstDir.GetID(),
"check_name_mode": "refuse", // optional:ignore,auto_rename,refuse
//"new_name": "newName", // The new name to use when a file of the same name exists
})
})
return err
}
func (d *AliyundriveOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
_, err := d.request("/adrive/v1.0/openFile/update", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"drive_id": d.DriveId,
"file_id": srcObj.GetID(),
"name": newName,
})
})
return err
}
func (d *AliyundriveOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := d.request("/adrive/v1.0/openFile/copy", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"drive_id": d.DriveId,
"file_id": srcObj.GetID(),
"to_parent_file_id": dstDir.GetID(),
"auto_rename": true,
})
})
return err
}
func (d *AliyundriveOpen) Remove(ctx context.Context, obj model.Obj) error {
uri := "/adrive/v1.0/openFile/recyclebin/trash"
if d.RemoveWay == "delete" {
uri = "/adrive/v1.0/openFile/delete"
}
_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"drive_id": d.DriveId,
"file_id": obj.GetID(),
})
})
return err
}
func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
// rapid_upload is not currently supported
// 1. create
// Part Size Unit: Bytes, Default: 20MB,
// Maximum number of slices 10,000, ≈195.3125GB
var partSize int64 = 20*1024*1024
createData := base.Json{
"drive_id": d.DriveId,
"parent_file_id": dstDir.GetID(),
"name": stream.GetName(),
"type": "file",
"check_name_mode": "ignore",
}
count := 1
if stream.GetSize() > partSize {
if stream.GetSize() > 1*1024*1024*1024*1024 { // file Size over 1TB
partSize = 5*1024*1024*1024 // file part size 5GB
} else if stream.GetSize() > 768*1024*1024*1024 { // over 768GB
partSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part
} else if stream.GetSize() > 512*1024*1024*1024 { // over 512GB
partSize = 82463373 // ≈ 78.6432MB
} else if stream.GetSize() > 384*1024*1024*1024 { // over 384GB
partSize = 54975582 // ≈ 52.4288MB
} else if stream.GetSize() > 256*1024*1024*1024 { // over 256GB
partSize = 41231687 // ≈ 39.3216MB
} else if stream.GetSize() > 128*1024*1024*1024 { // over 128GB
partSize = 27487791 // ≈ 26.2144MB
}
count = int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
createData["part_info_list"] = makePartInfos(count)
}
var createResp CreateResp
_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
req.SetBody(createData).SetResult(&createResp)
})
if err != nil {
return err
}
// 2. upload
preTime := time.Now()
for i := 1; i <= len(createResp.PartInfoList); i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
err = d.uploadPart(ctx, i, count, utils.NewMultiReadable(io.LimitReader(stream, partSize)), &createResp, true)
if err != nil {
return err
}
if count > 0 {
up(i * 100 / count)
}
// refresh upload url if 50 minutes passed
if time.Since(preTime) > 50*time.Minute {
createResp.PartInfoList, err = d.getUploadUrl(count, createResp.FileId, createResp.UploadId)
if err != nil {
return err
}
preTime = time.Now()
}
}
// 3. complete
_, err = d.request("/adrive/v1.0/openFile/complete", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"drive_id": d.DriveId,
"file_id": createResp.FileId,
"upload_id": createResp.UploadId,
})
})
return err
}
func (d *AliyundriveOpen) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
var resp base.Json
var uri string
data := base.Json{
"drive_id": d.DriveId,
"file_id": args.Obj.GetID(),
}
switch args.Method {
case "video_preview":
uri = "/adrive/v1.0/openFile/getVideoPreviewPlayInfo"
data["category"] = "live_transcoding"
data["url_expire_sec"] = 14400
default:
return nil, errs.NotSupport
}
_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetResult(&resp)
})
if err != nil {
return nil, err
}
return resp, nil
}
var _ driver.Driver = (*AliyundriveOpen)(nil)

View File

@ -0,0 +1,39 @@
package aliyundrive_open
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootID
RefreshToken string `json:"refresh_token" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
OauthTokenURL string `json:"oauth_token_url" default:"https://api.xhofe.top/alist/ali_open/token"`
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
InternalUpload bool `json:"internal_upload" help:"If you are using Aliyun ECS is located in Beijing, you can turn it on to boost the upload speed"`
AccessToken string
}
var config = driver.Config{
Name: "AliyundriveOpen",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "root",
NoOverwriteUpload: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &AliyundriveOpen{
base: "https://openapi.aliyundrive.com",
}
})
}

View File

@ -0,0 +1,69 @@
package aliyundrive_open
import (
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type ErrResp struct {
Code string `json:"code"`
Message string `json:"message"`
}
type Files struct {
Items []File `json:"items"`
NextMarker string `json:"next_marker"`
}
type File struct {
DriveId string `json:"drive_id"`
FileId string `json:"file_id"`
ParentFileId string `json:"parent_file_id"`
Name string `json:"name"`
Size int64 `json:"size"`
FileExtension string `json:"file_extension"`
ContentHash string `json:"content_hash"`
Category string `json:"category"`
Type string `json:"type"`
Thumbnail string `json:"thumbnail"`
Url string `json:"url"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func fileToObj(f File) *model.ObjThumb {
return &model.ObjThumb{
Object: model.Object{
ID: f.FileId,
Name: f.Name,
Size: f.Size,
Modified: f.UpdatedAt,
IsFolder: f.Type == "folder",
},
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
}
}
type PartInfo struct {
Etag interface{} `json:"etag"`
PartNumber int `json:"part_number"`
PartSize interface{} `json:"part_size"`
UploadUrl string `json:"upload_url"`
ContentType string `json:"content_type"`
}
type CreateResp struct {
//Type string `json:"type"`
//ParentFileId string `json:"parent_file_id"`
//DriveId string `json:"drive_id"`
FileId string `json:"file_id"`
//RevisionId string `json:"revision_id"`
//EncryptMode string `json:"encrypt_mode"`
//DomainId string `json:"domain_id"`
//FileName string `json:"file_name"`
UploadId string `json:"upload_id"`
//Location string `json:"location"`
RapidUpload bool `json:"rapid_upload"`
PartInfoList []PartInfo `json:"part_info_list"`
}

View File

@ -0,0 +1,175 @@
package aliyundrive_open
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
// do others that not defined in Driver interface
func (d *AliyundriveOpen) refreshToken() error {
url := d.base + "/oauth/access_token"
if d.OauthTokenURL != "" && d.ClientID == "" {
url = d.OauthTokenURL
}
var resp base.TokenResp
var e ErrResp
_, err := base.RestyClient.R().
ForceContentType("application/json").
SetBody(base.Json{
"client_id": d.ClientID,
"client_secret": d.ClientSecret,
"grant_type": "refresh_token",
"refresh_token": d.RefreshToken,
}).
SetResult(&resp).
SetError(&e).
Post(url)
if err != nil {
return err
}
if e.Code != "" {
return fmt.Errorf("failed to refresh token: %s", e.Message)
}
if resp.RefreshToken == "" {
return errors.New("failed to refresh token: refresh token is empty")
}
d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
op.MustSaveDriverStorage(d)
return nil
}
func (d *AliyundriveOpen) request(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
req := base.RestyClient.R()
// TODO check whether access_token is expired
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
if method == http.MethodPost {
req.SetHeader("Content-Type", "application/json")
}
if callback != nil {
callback(req)
}
var e ErrResp
req.SetError(&e)
res, err := req.Execute(method, d.base+uri)
if err != nil {
return nil, err
}
isRetry := len(retry) > 0 && retry[0]
if e.Code != "" {
if !isRetry && (utils.SliceContains([]string{"AccessTokenInvalid", "AccessTokenExpired", "I400JD"}, e.Code) || d.AccessToken == "") {
err = d.refreshToken()
if err != nil {
return nil, err
}
return d.request(uri, method, callback, true)
}
return nil, fmt.Errorf("%s:%s", e.Code, e.Message)
}
return res.Body(), nil
}
func (d *AliyundriveOpen) list(ctx context.Context, data base.Json) (*Files, error) {
var resp Files
_, err := d.request("/adrive/v1.0/openFile/list", http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetResult(&resp)
})
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *AliyundriveOpen) getFiles(ctx context.Context, fileId string) ([]File, error) {
marker := "first"
res := make([]File, 0)
for marker != "" {
if marker == "first" {
marker = ""
}
data := base.Json{
"drive_id": d.DriveId,
"limit": 200,
"marker": marker,
"order_by": d.OrderBy,
"order_direction": d.OrderDirection,
"parent_file_id": fileId,
//"category": "",
//"type": "",
//"video_thumbnail_time": 120000,
//"video_thumbnail_width": 480,
//"image_thumbnail_width": 480,
}
resp, err := d.limitList(ctx, data)
if err != nil {
return nil, err
}
marker = resp.NextMarker
res = append(res, resp.Items...)
}
return res, nil
}
func makePartInfos(size int) []base.Json {
partInfoList := make([]base.Json, size)
for i := 0; i < size; i++ {
partInfoList[i] = base.Json{"part_number": 1 + i}
}
return partInfoList
}
func (d *AliyundriveOpen) getUploadUrl(count int, fileId, uploadId string) ([]PartInfo, error) {
partInfoList := makePartInfos(count)
var resp CreateResp
_, err := d.request("/adrive/v1.0/openFile/getUploadUrl", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"drive_id": d.DriveId,
"file_id": fileId,
"part_info_list": partInfoList,
"upload_id": uploadId,
}).SetResult(&resp)
})
return resp.PartInfoList, err
}
func (d *AliyundriveOpen) uploadPart(ctx context.Context, i, count int, reader *utils.MultiReadable, resp *CreateResp, retry bool) error {
partInfo := resp.PartInfoList[i-1]
uploadUrl := partInfo.UploadUrl
if d.InternalUpload {
uploadUrl = strings.ReplaceAll(uploadUrl, "https://cn-beijing-data.aliyundrive.net/", "http://ccp-bj29-bj-1592982087.oss-cn-beijing-internal.aliyuncs.com/")
}
req, err := http.NewRequest("PUT", uploadUrl, reader)
if err != nil {
return err
}
req = req.WithContext(ctx)
res, err := base.HttpClient.Do(req)
if err != nil {
if retry {
reader.Reset()
return d.uploadPart(ctx, i, count, reader, resp, false)
}
return err
}
res.Body.Close()
if retry && res.StatusCode == http.StatusForbidden {
resp.PartInfoList, err = d.getUploadUrl(count, resp.FileId, resp.UploadId)
if err != nil {
return err
}
reader.Reset()
return d.uploadPart(ctx, i, count, reader, resp, false)
}
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusConflict {
return fmt.Errorf("upload status: %d", res.StatusCode)
}
return nil
}

View File

@ -2,7 +2,6 @@ package aliyundrive_share
import (
"context"
"errors"
"net/http"
"time"
@ -12,6 +11,7 @@ import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/cron"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
@ -22,6 +22,9 @@ type AliyundriveShare struct {
ShareToken string
DriveId string
cron *cron.Cron
limitList func(ctx context.Context, dir model.Obj) ([]model.Obj, error)
limitLink func(ctx context.Context, file model.Obj) (*model.Link, error)
}
func (d *AliyundriveShare) Config() driver.Config {
@ -29,16 +32,11 @@ func (d *AliyundriveShare) Config() driver.Config {
}
func (d *AliyundriveShare) GetAddition() driver.Additional {
return d.Addition
return &d.Addition
}
func (d *AliyundriveShare) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
err = d.refreshToken()
func (d *AliyundriveShare) Init(ctx context.Context) error {
err := d.refreshToken()
if err != nil {
return err
}
@ -53,6 +51,8 @@ func (d *AliyundriveShare) Init(ctx context.Context, storage model.Storage) erro
log.Errorf("%+v", err)
}
})
d.limitList = utils.LimitRateCtx(d.list, time.Second/4)
d.limitLink = utils.LimitRateCtx(d.link, time.Second)
return nil
}
@ -60,10 +60,15 @@ func (d *AliyundriveShare) Drop(ctx context.Context) error {
if d.cron != nil {
d.cron.Stop()
}
d.DriveId = ""
return nil
}
func (d *AliyundriveShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return d.limitList(ctx, dir)
}
func (d *AliyundriveShare) list(ctx context.Context, dir model.Obj) ([]model.Obj, error) {
files, err := d.getFiles(dir.GetID())
if err != nil {
return nil, err
@ -73,106 +78,56 @@ func (d *AliyundriveShare) List(ctx context.Context, dir model.Obj, args model.L
})
}
//func (d *AliyundriveShare) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *AliyundriveShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
return d.limitLink(ctx, file)
}
func (d *AliyundriveShare) link(ctx context.Context, file model.Obj) (*model.Link, error) {
data := base.Json{
"drive_id": d.DriveId,
"file_id": file.GetID(),
"expire_sec": 14400,
}
var e ErrorResp
res, err := base.RestyClient.R().
SetError(&e).SetBody(data).
SetHeader("content-type", "application/json").
SetHeader("Authorization", "Bearer\t"+d.AccessToken).
Post("https://api.aliyundrive.com/v2/file/get_download_url")
if err != nil {
return nil, err
}
var u string
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = d.refreshToken()
if err != nil {
return nil, err
}
return d.Link(ctx, file, args)
} else if e.Code == "ForbiddenNoPermission.File" {
data = utils.MergeMap(data, base.Json{
// Only ten minutes valid
// // Only ten minutes lifetime
"expire_sec": 600,
"share_id": d.ShareId,
})
}
var resp ShareLinkResp
var e2 ErrorResp
_, err = base.RestyClient.R().
SetError(&e2).SetBody(data).SetResult(&resp).
SetHeader("content-type", "application/json").
SetHeader("Authorization", "Bearer\t"+d.AccessToken).
SetHeader("x-share-token", d.ShareToken).
Post("https://api.aliyundrive.com/v2/file/get_share_link_download_url")
_, err := d.request("https://api.aliyundrive.com/v2/file/get_share_link_download_url", http.MethodPost, func(req *resty.Request) {
req.SetHeader(CanaryHeaderKey, CanaryHeaderValue).SetBody(data).SetResult(&resp)
})
if err != nil {
return nil, err
}
if e2.Code != "" {
if e2.Code == "AccessTokenInvalid" || e2.Code == "ShareLinkTokenInvalid" {
err = d.getShareToken()
if err != nil {
return nil, err
}
return d.Link(ctx, file, args)
} else {
return nil, errors.New(e2.Code + ":" + e2.Message)
}
} else {
u = resp.DownloadUrl
}
} else {
return nil, errors.New(e.Code + ":" + e.Message)
}
} else {
u = utils.Json.Get(res.Body(), "url").ToString()
}
return &model.Link{
Header: http.Header{
"Referer": []string{"https://www.aliyundrive.com/"},
},
URL: u,
URL: resp.DownloadUrl,
}, nil
}
func (d *AliyundriveShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
// TODO create folder
return errs.NotSupport
}
func (d *AliyundriveShare) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
// TODO move obj
return errs.NotSupport
}
func (d *AliyundriveShare) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
// TODO rename obj
return errs.NotSupport
}
func (d *AliyundriveShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
// TODO copy obj
return errs.NotSupport
}
func (d *AliyundriveShare) Remove(ctx context.Context, obj model.Obj) error {
// TODO remove obj
return errs.NotSupport
}
func (d *AliyundriveShare) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
// TODO upload file
return errs.NotSupport
func (d *AliyundriveShare) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
var resp base.Json
var url string
data := base.Json{
"share_id": d.ShareId,
"file_id": args.Obj.GetID(),
}
switch args.Method {
case "doc_preview":
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
case "video_preview":
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
data["category"] = "live_transcoding"
default:
return nil, errs.NotSupport
}
_, err := d.request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetResult(&resp)
})
if err != nil {
return nil, err
}
return resp, nil
}
var _ driver.Driver = (*AliyundriveShare)(nil)

View File

@ -23,7 +23,7 @@ var config = driver.Config{
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
op.RegisterDriver(func() driver.Driver {
return &AliyundriveShare{}
})
}

View File

@ -9,6 +9,12 @@ import (
log "github.com/sirupsen/logrus"
)
const (
// CanaryHeaderKey CanaryHeaderValue for lifting rate limit restrictions
CanaryHeaderKey = "X-Canary"
CanaryHeaderValue = "client=web,app=share,version=v2.3.1"
)
func (d *AliyundriveShare) refreshToken() error {
url := "https://auth.aliyundrive.com/v2/account/token"
var resp base.TokenResp
@ -52,12 +58,47 @@ func (d *AliyundriveShare) getShareToken() error {
return nil
}
func (d *AliyundriveShare) request(url, method string, callback base.ReqCallback) ([]byte, error) {
var e ErrorResp
req := base.RestyClient.R().
SetError(&e).
SetHeader("content-type", "application/json").
SetHeader("Authorization", "Bearer\t"+d.AccessToken).
SetHeader(CanaryHeaderKey, CanaryHeaderValue).
SetHeader("x-share-token", d.ShareToken)
if callback != nil {
callback(req)
} else {
req.SetBody("{}")
}
resp, err := req.Execute(method, url)
if err != nil {
return nil, err
}
if e.Code != "" {
if e.Code == "AccessTokenInvalid" || e.Code == "ShareLinkTokenInvalid" {
if e.Code == "AccessTokenInvalid" {
err = d.refreshToken()
} else {
err = d.getShareToken()
}
if err != nil {
return nil, err
}
return d.request(url, method, callback)
} else {
return nil, errors.New(e.Code + ": " + e.Message)
}
}
return resp.Body(), nil
}
func (d *AliyundriveShare) getFiles(fileId string) ([]File, error) {
files := make([]File, 0)
data := base.Json{
"image_thumbnail_process": "image/resize,w_160/format,jpeg",
"image_url_process": "image/resize,w_1920/format,jpeg",
"limit": 100,
"limit": 200,
"order_by": d.OrderBy,
"order_direction": d.OrderDirection,
"parent_file_id": fileId,
@ -73,6 +114,7 @@ func (d *AliyundriveShare) getFiles(fileId string) ([]File, error) {
var resp ListResp
res, err := base.RestyClient.R().
SetHeader("x-share-token", d.ShareToken).
SetHeader(CanaryHeaderKey, CanaryHeaderValue).
SetResult(&resp).SetError(&e).SetBody(data).
Post("https://api.aliyundrive.com/adrive/v3/file/list")
if err != nil {

View File

@ -6,29 +6,44 @@ import (
_ "github.com/alist-org/alist/v3/drivers/139"
_ "github.com/alist-org/alist/v3/drivers/189"
_ "github.com/alist-org/alist/v3/drivers/189pc"
_ "github.com/alist-org/alist/v3/drivers/alias"
_ "github.com/alist-org/alist/v3/drivers/alist_v2"
_ "github.com/alist-org/alist/v3/drivers/alist_v3"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_open"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_share"
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
_ "github.com/alist-org/alist/v3/drivers/baidu_share"
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
_ "github.com/alist-org/alist/v3/drivers/dropbox"
_ "github.com/alist-org/alist/v3/drivers/ftp"
_ "github.com/alist-org/alist/v3/drivers/google_drive"
_ "github.com/alist-org/alist/v3/drivers/google_photo"
_ "github.com/alist-org/alist/v3/drivers/ipfs_api"
_ "github.com/alist-org/alist/v3/drivers/lanzou"
_ "github.com/alist-org/alist/v3/drivers/local"
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
_ "github.com/alist-org/alist/v3/drivers/mega"
_ "github.com/alist-org/alist/v3/drivers/mopan"
_ "github.com/alist-org/alist/v3/drivers/onedrive"
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
_ "github.com/alist-org/alist/v3/drivers/pikpak"
_ "github.com/alist-org/alist/v3/drivers/quark"
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
_ "github.com/alist-org/alist/v3/drivers/s3"
_ "github.com/alist-org/alist/v3/drivers/seafile"
_ "github.com/alist-org/alist/v3/drivers/sftp"
_ "github.com/alist-org/alist/v3/drivers/smb"
_ "github.com/alist-org/alist/v3/drivers/teambition"
_ "github.com/alist-org/alist/v3/drivers/terabox"
_ "github.com/alist-org/alist/v3/drivers/thunder"
_ "github.com/alist-org/alist/v3/drivers/trainbit"
_ "github.com/alist-org/alist/v3/drivers/url_tree"
_ "github.com/alist-org/alist/v3/drivers/uss"
_ "github.com/alist-org/alist/v3/drivers/virtual"
_ "github.com/alist-org/alist/v3/drivers/webdav"
_ "github.com/alist-org/alist/v3/drivers/wopan"
_ "github.com/alist-org/alist/v3/drivers/yandex_disk"
)

View File

@ -23,7 +23,6 @@ import (
type BaiduNetdisk struct {
model.Storage
Addition
AccessToken string
}
func (d *BaiduNetdisk) Config() driver.Config {
@ -31,16 +30,15 @@ func (d *BaiduNetdisk) Config() driver.Config {
}
func (d *BaiduNetdisk) GetAddition() driver.Additional {
return d.Addition
return &d.Addition
}
func (d *BaiduNetdisk) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
func (d *BaiduNetdisk) Init(ctx context.Context) error {
res, err := d.get("/xpan/nas", map[string]string{
"method": "uinfo",
}, nil)
log.Debugf("[baidu] get uinfo: %s", string(res))
return err
}
return d.refreshToken()
}
func (d *BaiduNetdisk) Drop(ctx context.Context) error {
@ -57,11 +55,6 @@ func (d *BaiduNetdisk) List(ctx context.Context, dir model.Obj, args model.ListA
})
}
//func (d *BaiduNetdisk) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *BaiduNetdisk) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if d.DownloadAPI == "crack" {
return d.linkCrack(file, args)
@ -98,12 +91,11 @@ func (d *BaiduNetdisk) Rename(ctx context.Context, srcObj model.Obj, newName str
}
func (d *BaiduNetdisk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
dest, newname := stdpath.Split(dstDir.GetPath())
data := []base.Json{
{
"path": srcObj.GetPath(),
"dest": dest,
"newname": newname,
"dest": dstDir.GetPath(),
"newname": srcObj.GetName(),
},
}
_, err := d.manage("copy", data)
@ -175,7 +167,8 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
return err
}
}
path := encodeURIComponent(stdpath.Join(dstDir.GetPath(), stream.GetName()))
rawPath := stdpath.Join(dstDir.GetPath(), stream.GetName())
path := encodeURIComponent(rawPath)
block_list_str := fmt.Sprintf("[%s]", strings.Join(block_list, ","))
data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&block_list=%s&content-md5=%s&slice-md5=%s",
path, stream.GetSize(),
@ -202,6 +195,9 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
}
left = stream.GetSize()
for i, partseq := range precreateResp.BlockList {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
byteSize := Default
var byteData []byte
if left < Default {
@ -217,7 +213,11 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
}
u := "https://d.pcs.baidu.com/rest/2.0/pcs/superfile2"
params["partseq"] = strconv.Itoa(partseq)
res, err := base.RestyClient.R().SetQueryParams(params).SetFileReader("file", stream.GetName(), bytes.NewReader(byteData)).Post(u)
res, err := base.RestyClient.R().
SetContext(ctx).
SetQueryParams(params).
SetFileReader("file", stream.GetName(), bytes.NewReader(byteData)).
Post(u)
if err != nil {
return err
}
@ -226,7 +226,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
up(i * 100 / len(precreateResp.BlockList))
}
}
_, err = d.create(path, stream.GetSize(), 0, precreateResp.Uploadid, block_list_str)
_, err = d.create(rawPath, stream.GetSize(), 0, precreateResp.Uploadid, block_list_str)
return err
}

View File

@ -13,6 +13,8 @@ type Addition struct {
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
AccessToken string
}
var config = driver.Config{
@ -20,10 +22,8 @@ var config = driver.Config{
DefaultRoot: "/",
}
func New() driver.Driver {
return &BaiduNetdisk{}
}
func init() {
op.RegisterDriver(config, New)
op.RegisterDriver(func() driver.Driver {
return &BaiduNetdisk{}
})
}

View File

@ -165,7 +165,7 @@ func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Li
return &model.Link{
URL: resp.Info[0].Dlink,
Header: http.Header{
"User-Agent": []string{"pan.baidu.com"},
"User-Agent": []string{d.CustomCrackUA},
},
}, nil
}
@ -187,7 +187,7 @@ func (d *BaiduNetdisk) create(path string, size int64, isdir int, uploadid, bloc
params := map[string]string{
"method": "create",
}
data := fmt.Sprintf("path=%s&size=%d&isdir=%d", path, size, isdir)
data := fmt.Sprintf("path=%s&size=%d&isdir=%d&rtype=3", encodeURIComponent(path), size, isdir)
if uploadid != "" {
data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list)
}

View File

@ -9,6 +9,8 @@ import (
"math"
"os"
"regexp"
"strconv"
"strings"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
@ -22,6 +24,8 @@ type BaiduPhoto struct {
Addition
AccessToken string
Uk int64
root model.Obj
}
func (d *BaiduPhoto) Config() driver.Config {
@ -29,26 +33,67 @@ func (d *BaiduPhoto) Config() driver.Config {
}
func (d *BaiduPhoto) GetAddition() driver.Additional {
return d.Addition
return &d.Addition
}
func (d *BaiduPhoto) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
func (d *BaiduPhoto) Init(ctx context.Context) error {
if err := d.refreshToken(); err != nil {
return err
}
// root
if d.AlbumID != "" {
albumID := strings.Split(d.AlbumID, "|")[0]
album, err := d.GetAlbumDetail(ctx, albumID)
if err != nil {
return err
}
return d.refreshToken()
d.root = album
} else {
d.root = &Root{
Name: "root",
Modified: d.Modified,
IsFolder: true,
}
}
// uk
info, err := d.uInfo()
if err != nil {
return err
}
d.Uk, err = strconv.ParseInt(info.YouaID, 10, 64)
return err
}
func (d *BaiduPhoto) GetRoot(ctx context.Context) (model.Obj, error) {
return d.root, nil
}
func (d *BaiduPhoto) Drop(ctx context.Context) error {
d.AccessToken = ""
d.Uk = 0
d.root = nil
return nil
}
func (d *BaiduPhoto) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
var objs []model.Obj
var err error
if IsRoot(dir) {
/* album */
if album, ok := dir.(*Album); ok {
var files []AlbumFile
files, err = d.GetAllAlbumFile(ctx, album, "")
if err != nil {
return nil, err
}
return utils.MustSliceConvert(files, func(file AlbumFile) model.Obj {
return &file
}), nil
}
/* root */
var albums []Album
if d.ShowType != "root_only_file" {
albums, err = d.GetAllAlbum(ctx)
@ -65,119 +110,105 @@ func (d *BaiduPhoto) List(ctx context.Context, dir model.Obj, args model.ListArg
}
}
alubmName := make(map[string]int)
objs, _ = utils.SliceConvert(albums, func(album Album) (model.Obj, error) {
i := alubmName[album.GetName()]
if i != 0 {
alubmName[album.GetName()]++
album.Title = fmt.Sprintf("%s(%d)", album.Title, i)
}
alubmName[album.GetName()]++
return &album, nil
})
for i := 0; i < len(files); i++ {
objs = append(objs, &files[i])
}
} else if IsAlbum(dir) || IsAlbumRoot(dir) {
var files []AlbumFile
files, err = d.GetAllAlbumFile(ctx, splitID(dir.GetID())[0], "")
if err != nil {
return nil, err
}
objs = make([]model.Obj, 0, len(files))
for i := 0; i < len(files); i++ {
objs = append(objs, &files[i])
}
}
return objs, nil
return append(
utils.MustSliceConvert(albums, func(album Album) model.Obj {
return &album
}),
utils.MustSliceConvert(files, func(album File) model.Obj {
return &album
})...,
), nil
}
func (d *BaiduPhoto) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if IsAlbumFile(file) {
return d.linkAlbum(ctx, file, args)
} else if IsFile(file) {
switch file := file.(type) {
case *File:
return d.linkFile(ctx, file, args)
case *AlbumFile:
return d.linkAlbum(ctx, file, args)
}
return nil, errs.NotFile
}
func (d *BaiduPhoto) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if IsRoot(parentDir) {
code := regexp.MustCompile(`(?i)join:([\S]*)`).FindStringSubmatch(dirName)
var joinReg = regexp.MustCompile(`(?i)join:([\S]*)`)
func (d *BaiduPhoto) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
if _, ok := parentDir.(*Root); ok {
code := joinReg.FindStringSubmatch(dirName)
if len(code) > 1 {
return d.JoinAlbum(ctx, code[1])
}
return d.CreateAlbum(ctx, dirName)
}
return errs.NotSupport
return nil, errs.NotSupport
}
func (d *BaiduPhoto) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
if IsFile(srcObj) {
if IsAlbum(dstDir) {
func (d *BaiduPhoto) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
switch file := srcObj.(type) {
case *File:
if album, ok := dstDir.(*Album); ok {
//rootfile -> album
e := splitID(dstDir.GetID())
return d.AddAlbumFile(ctx, e[0], e[1], srcObj.GetID())
return d.AddAlbumFile(ctx, album, file)
}
} else if IsAlbumFile(srcObj) {
if IsRoot(dstDir) {
case *AlbumFile:
switch album := dstDir.(type) {
case *Root:
//albumfile -> root
e := splitID(srcObj.GetID())
_, err := d.CopyAlbumFile(ctx, e[1], e[2], e[3], srcObj.GetID())
return err
} else if IsAlbum(dstDir) {
return d.CopyAlbumFile(ctx, file)
case *Album:
// albumfile -> root -> album
e := splitID(srcObj.GetID())
file, err := d.CopyAlbumFile(ctx, e[1], e[2], e[3], srcObj.GetID())
rootfile, err := d.CopyAlbumFile(ctx, file)
if err != nil {
return err
return nil, err
}
e = splitID(dstDir.GetID())
return d.AddAlbumFile(ctx, e[0], e[1], fmt.Sprint(file.Fsid))
return d.AddAlbumFile(ctx, album, rootfile)
}
}
return errs.NotSupport
return nil, errs.NotSupport
}
func (d *BaiduPhoto) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
func (d *BaiduPhoto) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
// 仅支持相册之间移动
if IsAlbumFile(srcObj) && IsAlbum(dstDir) {
err := d.Copy(ctx, srcObj, dstDir)
if file, ok := srcObj.(*AlbumFile); ok {
if _, ok := dstDir.(*Album); ok {
newObj, err := d.Copy(ctx, srcObj, dstDir)
if err != nil {
return err
return nil, err
}
e := splitID(srcObj.GetID())
return d.DeleteAlbumFile(ctx, e[1], e[2], srcObj.GetID())
// 删除原相册文件
_ = d.DeleteAlbumFile(ctx, file)
return newObj, nil
}
return errs.NotSupport
}
return nil, errs.NotSupport
}
func (d *BaiduPhoto) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
func (d *BaiduPhoto) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
// 仅支持相册改名
if IsAlbum(srcObj) {
e := splitID(srcObj.GetID())
return d.SetAlbumName(ctx, e[0], e[1], newName)
if album, ok := srcObj.(*Album); ok {
return d.SetAlbumName(ctx, album, newName)
}
return errs.NotSupport
return nil, errs.NotSupport
}
func (d *BaiduPhoto) Remove(ctx context.Context, obj model.Obj) error {
e := splitID(obj.GetID())
if IsFile(obj) {
return d.DeleteFile(ctx, e[0])
} else if IsAlbum(obj) {
return d.DeleteAlbum(ctx, e[0], e[1])
} else if IsAlbumFile(obj) {
return d.DeleteAlbumFile(ctx, e[1], e[2], obj.GetID())
switch obj := obj.(type) {
case *File:
return d.DeleteFile(ctx, obj)
case *AlbumFile:
return d.DeleteAlbumFile(ctx, obj)
case *Album:
return d.DeleteAlbum(ctx, obj)
}
return errs.NotSupport
}
func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
// 需要获取完整文件md5,必须支持 io.Seek
tempFile, err := utils.CreateTempFile(stream.GetReadCloser())
if err != nil {
return err
return nil, err
}
defer func() {
_ = tempFile.Close()
@ -195,20 +226,19 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
sliceMd52 := md5.New()
slicemd52Write := utils.LimitWriter(sliceMd52, SliceSize)
for i := 1; i <= count; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
if utils.IsCanceled(ctx) {
return nil, ctx.Err()
}
_, err := io.CopyN(io.MultiWriter(fileMd5, sliceMd5, slicemd52Write), tempFile, DEFAULT)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return err
return nil, err
}
sliceMD5List = append(sliceMD5List, hex.EncodeToString(sliceMd5.Sum(nil)))
sliceMd5.Reset()
}
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return err
return nil, err
}
content_md5 := hex.EncodeToString(fileMd5.Sum(nil))
slice_md5 := hex.EncodeToString(sliceMd52.Sum(nil))
@ -219,7 +249,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
"isdir": "0",
"rtype": "1",
"ctype": "11",
"path": stream.GetName(),
"path": fmt.Sprintf("/%s", stream.GetName()),
"size": fmt.Sprint(stream.GetSize()),
"slice-md5": slice_md5,
"content-md5": content_md5,
@ -233,7 +263,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
r.SetFormData(params)
}, &precreateResp)
if err != nil {
return err
return nil, err
}
switch precreateResp.ReturnType {
@ -245,6 +275,9 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
}
for i := 0; i < count; i++ {
if utils.IsCanceled(ctx) {
return nil, ctx.Err()
}
uploadParams["partseq"] = fmt.Sprint(i)
_, err = d.Post("https://c3.pcs.baidu.com/rest/2.0/pcs/superfile2", func(r *resty.Request) {
r.SetContext(ctx)
@ -252,7 +285,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
r.SetFileReader("file", stream.GetName(), io.LimitReader(tempFile, DEFAULT))
}, nil)
if err != nil {
return err
return nil, err
}
up(i * 100 / count)
}
@ -264,19 +297,24 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
r.SetFormData(params)
}, &precreateResp)
if err != nil {
return err
return nil, err
}
fallthrough
case 3: // 增加到相册
if IsAlbum(dstDir) || IsAlbumRoot(dstDir) {
e := splitID(dstDir.GetID())
err = d.AddAlbumFile(ctx, e[0], e[1], fmt.Sprint(precreateResp.Data.FsID))
if err != nil {
return err
rootfile := precreateResp.Data.toFile()
if album, ok := dstDir.(*Album); ok {
return d.AddAlbumFile(ctx, album, rootfile)
}
return rootfile, nil
}
}
return nil
return nil, errs.NotSupport
}
var _ driver.Driver = (*BaiduPhoto)(nil)
var _ driver.GetRooter = (*BaiduPhoto)(nil)
var _ driver.MkdirResult = (*BaiduPhoto)(nil)
var _ driver.CopyResult = (*BaiduPhoto)(nil)
var _ driver.MoveResult = (*BaiduPhoto)(nil)
var _ driver.Remove = (*BaiduPhoto)(nil)
var _ driver.PutResult = (*BaiduPhoto)(nil)
var _ driver.RenameResult = (*BaiduPhoto)(nil)

View File

@ -4,104 +4,68 @@ import (
"fmt"
"math"
"math/rand"
"regexp"
"strings"
"time"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
)
//Tid生成
// Tid生成
func getTid() string {
return fmt.Sprintf("3%d%.0f", time.Now().Unix(), math.Floor(9000000*rand.Float64()+1000000))
}
// 检查名称
func checkName(name string) bool {
return len(name) <= 20 && regexp.MustCompile("[\u4e00-\u9fa5A-Za-z0-9_-]").MatchString(name)
}
func toTime(t int64) *time.Time {
tm := time.Unix(t, 0)
return &tm
}
func fsidsFormat(ids ...string) string {
var buf []string
for _, id := range ids {
e := splitID(id)
buf = append(buf, fmt.Sprintf(`{"fsid":%s,"uk":%s}`, e[0], e[3]))
}
func fsidsFormatNotUk(ids ...int64) string {
buf := utils.MustSliceConvert(ids, func(id int64) string {
return fmt.Sprintf(`{"fsid":%d}`, id)
})
return fmt.Sprintf("[%s]", strings.Join(buf, ","))
}
func fsidsFormatNotUk(ids ...string) string {
var buf []string
for _, id := range ids {
buf = append(buf, fmt.Sprintf(`{"fsid":%s}`, splitID(id)[0]))
}
return fmt.Sprintf("[%s]", strings.Join(buf, ","))
}
/*
结构
{fsid} 文件
{album_id}|{tid} 相册
{fsid}|{album_id}|{tid}|{uk} 相册文件
*/
func splitID(id string) []string {
return strings.SplitN(id, "|", 4)[:4]
}
/*
结构
{fsid} 文件
{album_id}|{tid} 相册
{fsid}|{album_id}|{tid}|{uk} 相册文件
*/
func joinID(ids ...interface{}) string {
idsStr := make([]string, 0, len(ids))
for _, id := range ids {
idsStr = append(idsStr, fmt.Sprint(id))
}
return strings.Join(idsStr, "|")
}
func getFileName(path string) string {
return path[strings.LastIndex(path, "/")+1:]
}
// 相册
func IsAlbum(obj model.Obj) bool {
return obj.IsDir() && obj.GetPath() == "album"
}
// 根目录
func IsRoot(obj model.Obj) bool {
return obj.IsDir() && obj.GetPath() == "" && obj.GetID() == ""
}
// 以相册为根目录
func IsAlbumRoot(obj model.Obj) bool {
return obj.IsDir() && obj.GetPath() == "" && obj.GetID() != ""
}
// 根文件
func IsFile(obj model.Obj) bool {
return !obj.IsDir() && obj.GetPath() == "file"
}
// 相册文件
func IsAlbumFile(obj model.Obj) bool {
return !obj.IsDir() && obj.GetPath() == "albumfile"
}
func MustString(str string, err error) string {
return str
}
/*
* 处理文件变化
* 最大程度利用重复数据
**/
func copyFile(file *AlbumFile, cf *CopyFile) *File {
return &File{
Fsid: cf.Fsid,
Path: cf.Path,
Ctime: cf.Ctime,
Mtime: cf.Ctime,
Size: file.Size,
Thumburl: file.Thumburl,
}
}
func moveFileToAlbumFile(file *File, album *Album, uk int64) *AlbumFile {
return &AlbumFile{
File: *file,
AlbumID: album.AlbumID,
Tid: album.Tid,
Uk: uk,
}
}
func renameAlbum(album *Album, newName string) *Album {
return &Album{
AlbumID: album.AlbumID,
Tid: album.Tid,
JoinTime: album.JoinTime,
CreateTime: album.CreateTime,
Title: newName,
Mtime: time.Now().Unix(),
}
}

View File

@ -14,17 +14,13 @@ type Addition struct {
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
}
func (a Addition) GetRootId() string {
return a.AlbumID
}
var config = driver.Config{
Name: "BaiduPhoto",
LocalSort: true,
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
op.RegisterDriver(func() driver.Driver {
return &BaiduPhoto{}
})
}

View File

@ -3,6 +3,8 @@ package baiduphoto
import (
"fmt"
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type TokenErrResp struct {
@ -19,6 +21,12 @@ type Erron struct {
RequestID int `json:"request_id"`
}
// 用户信息
type UInfo struct {
// uk
YouaID string `json:"youa_id"`
}
type Page struct {
HasMore int `json:"has_more"`
Cursor string `json:"cursor"`
@ -28,6 +36,8 @@ func (p Page) HasNextPage() bool {
return p.HasMore == 1
}
type Root = model.Object
type (
FileListResp struct {
Page
@ -55,8 +65,8 @@ func (c *File) ModTime() time.Time {
return *c.parseTime
}
func (c *File) IsDir() bool { return false }
func (c *File) GetID() string { return joinID(c.Fsid) }
func (c *File) GetPath() string { return "file" }
func (c *File) GetID() string { return "" }
func (c *File) GetPath() string { return "" }
func (c *File) Thumb() string {
if len(c.Thumburl) > 0 {
return c.Thumburl[0]
@ -100,7 +110,7 @@ type (
)
func (a *Album) GetSize() int64 { return 0 }
func (a *Album) GetName() string { return fmt.Sprint(a.Title) }
func (a *Album) GetName() string { return a.Title }
func (a *Album) ModTime() time.Time {
if a.parseTime == nil {
a.parseTime = toTime(a.Mtime)
@ -108,11 +118,8 @@ func (a *Album) ModTime() time.Time {
return *a.parseTime
}
func (a *Album) IsDir() bool { return true }
func (a *Album) GetID() string { return joinID(a.AlbumID, a.Tid) }
func (a *Album) GetPath() string { return "album" }
func (af *AlbumFile) GetID() string { return joinID(af.Fsid, af.AlbumID, af.Tid, af.Uk) }
func (c *AlbumFile) GetPath() string { return "albumfile" }
func (a *Album) GetID() string { return "" }
func (a *Album) GetPath() string { return "" }
type (
CopyFileResp struct {
@ -120,6 +127,7 @@ type (
}
CopyFile struct {
FromFsid int64 `json:"from_fsid"` // 源ID
Ctime int64 `json:"ctime"`
Fsid int64 `json:"fsid"` // 目标ID
Path string `json:"path"`
ShootTime int `json:"shoot_time"`
@ -134,8 +142,8 @@ type (
Md5 string `json:"md5"`
ServerFilename string `json:"server_filename"`
Path string `json:"path"`
Ctime int `json:"ctime"`
Mtime int `json:"mtime"`
Ctime int64 `json:"ctime"`
Mtime int64 `json:"mtime"`
Isdir int `json:"isdir"`
Category int `json:"category"`
ServerMd5 string `json:"server_md5"`
@ -158,6 +166,18 @@ type (
}
)
func (f *UploadFile) toFile() *File {
return &File{
Fsid: f.FsID,
Path: f.Path,
Size: f.Size,
Ctime: f.Ctime,
Mtime: f.Mtime,
Thumburl: nil,
}
}
/* 共享相册部分 */
type InviteResp struct {
Pdata struct {
// 邀请码
@ -167,3 +187,9 @@ type InviteResp struct {
ShareID string `json:"share_id"`
} `json:"pdata"`
}
/* 加入相册部分 */
type JoinOrCreateAlbumResp struct {
AlbumID string `json:"album_id"`
AlreadyExists int `json:"already_exists"`
}

View File

@ -2,10 +2,8 @@ package baiduphoto
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/errs"
@ -17,18 +15,15 @@ import (
const (
API_URL = "https://photo.baidu.com/youai"
USER_API_URL = API_URL + "/user/v1"
ALBUM_API_URL = API_URL + "/album/v1"
FILE_API_URL_V1 = API_URL + "/file/v1"
FILE_API_URL_V2 = API_URL + "/file/v2"
)
var (
ErrNotSupportName = errors.New("only chinese and english, numbers and underscores are supported, and the length is no more than 20")
)
func (p *BaiduPhoto) Request(furl string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R().
SetQueryParam("access_token", p.AccessToken)
SetQueryParam("access_token", d.AccessToken)
if callback != nil {
callback(req)
}
@ -48,8 +43,10 @@ func (p *BaiduPhoto) Request(furl string, method string, callback base.ReqCallba
return nil, fmt.Errorf("you have joined album")
case 50820:
return nil, fmt.Errorf("no shared albums found")
case 50100:
return nil, fmt.Errorf("illegal title, only supports 50 characters")
case -6:
if err = p.refreshToken(); err != nil {
if err = d.refreshToken(); err != nil {
return nil, err
}
default:
@ -58,15 +55,15 @@ func (p *BaiduPhoto) Request(furl string, method string, callback base.ReqCallba
return res.Body(), nil
}
func (p *BaiduPhoto) refreshToken() error {
func (d *BaiduPhoto) refreshToken() error {
u := "https://openapi.baidu.com/oauth/2.0/token"
var resp base.TokenResp
var e TokenErrResp
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
"grant_type": "refresh_token",
"refresh_token": p.RefreshToken,
"client_id": p.ClientID,
"client_secret": p.ClientSecret,
"refresh_token": d.RefreshToken,
"client_id": d.ClientID,
"client_secret": d.ClientSecret,
}).Get(u)
if err != nil {
return err
@ -77,25 +74,25 @@ func (p *BaiduPhoto) refreshToken() error {
if resp.RefreshToken == "" {
return errs.EmptyToken
}
p.AccessToken, p.RefreshToken = resp.AccessToken, resp.RefreshToken
op.MustSaveDriverStorage(p)
d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
op.MustSaveDriverStorage(d)
return nil
}
func (p *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return p.Request(furl, http.MethodGet, callback, resp)
func (d *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return d.Request(furl, http.MethodGet, callback, resp)
}
func (p *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return p.Request(furl, http.MethodPost, callback, resp)
func (d *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return d.Request(furl, http.MethodPost, callback, resp)
}
// 获取所有文件
func (p *BaiduPhoto) GetAllFile(ctx context.Context) (files []File, err error) {
func (d *BaiduPhoto) GetAllFile(ctx context.Context) (files []File, err error) {
var cursor string
for {
var resp FileListResp
_, err = p.Get(FILE_API_URL_V1+"/list", func(r *resty.Request) {
_, err = d.Get(FILE_API_URL_V1+"/list", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"need_thumbnail": "1",
@ -116,22 +113,22 @@ func (p *BaiduPhoto) GetAllFile(ctx context.Context) (files []File, err error) {
}
// 删除根文件
func (p *BaiduPhoto) DeleteFile(ctx context.Context, fileIDs ...string) error {
_, err := p.Get(FILE_API_URL_V1+"/delete", func(req *resty.Request) {
func (d *BaiduPhoto) DeleteFile(ctx context.Context, file *File) error {
_, err := d.Get(FILE_API_URL_V1+"/delete", func(req *resty.Request) {
req.SetContext(ctx)
req.SetQueryParams(map[string]string{
"fsid_list": fmt.Sprintf("[%s]", strings.Join(fileIDs, ",")),
"fsid_list": fmt.Sprintf("[%d]", file.Fsid),
})
}, nil)
return err
}
// 获取所有相册
func (p *BaiduPhoto) GetAllAlbum(ctx context.Context) (albums []Album, err error) {
func (d *BaiduPhoto) GetAllAlbum(ctx context.Context) (albums []Album, err error) {
var cursor string
for {
var resp AlbumListResp
_, err = p.Get(ALBUM_API_URL+"/list", func(r *resty.Request) {
_, err = d.Get(ALBUM_API_URL+"/list", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"need_amount": "1",
@ -156,14 +153,14 @@ func (p *BaiduPhoto) GetAllAlbum(ctx context.Context) (albums []Album, err error
}
// 获取相册中所有文件
func (p *BaiduPhoto) GetAllAlbumFile(ctx context.Context, albumID, passwd string) (files []AlbumFile, err error) {
func (d *BaiduPhoto) GetAllAlbumFile(ctx context.Context, album *Album, passwd string) (files []AlbumFile, err error) {
var cursor string
for {
var resp AlbumFileListResp
_, err = p.Get(ALBUM_API_URL+"/listfile", func(r *resty.Request) {
_, err = d.Get(ALBUM_API_URL+"/listfile", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"album_id": albumID,
"album_id": album.AlbumID,
"need_amount": "1",
"limit": "1000",
"passwd": passwd,
@ -187,45 +184,45 @@ func (p *BaiduPhoto) GetAllAlbumFile(ctx context.Context, albumID, passwd string
}
// 创建相册
func (p *BaiduPhoto) CreateAlbum(ctx context.Context, name string) error {
if !checkName(name) {
return ErrNotSupportName
}
_, err := p.Post(ALBUM_API_URL+"/create", func(r *resty.Request) {
r.SetContext(ctx)
func (d *BaiduPhoto) CreateAlbum(ctx context.Context, name string) (*Album, error) {
var resp JoinOrCreateAlbumResp
_, err := d.Post(ALBUM_API_URL+"/create", func(r *resty.Request) {
r.SetContext(ctx).SetResult(&resp)
r.SetQueryParams(map[string]string{
"title": name,
"tid": getTid(),
"source": "0",
})
}, nil)
return err
if err != nil {
return nil, err
}
return d.GetAlbumDetail(ctx, resp.AlbumID)
}
// 相册改名
func (p *BaiduPhoto) SetAlbumName(ctx context.Context, albumID, tID, name string) error {
if !checkName(name) {
return ErrNotSupportName
}
_, err := p.Post(ALBUM_API_URL+"/settitle", func(r *resty.Request) {
func (d *BaiduPhoto) SetAlbumName(ctx context.Context, album *Album, name string) (*Album, error) {
_, err := d.Post(ALBUM_API_URL+"/settitle", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(map[string]string{
"title": name,
"album_id": albumID,
"tid": tID,
"album_id": album.AlbumID,
"tid": fmt.Sprint(album.Tid),
})
}, nil)
return err
if err != nil {
return nil, err
}
return renameAlbum(album, name), nil
}
// 删除相册
func (p *BaiduPhoto) DeleteAlbum(ctx context.Context, albumID, tID string) error {
_, err := p.Post(ALBUM_API_URL+"/delete", func(r *resty.Request) {
func (d *BaiduPhoto) DeleteAlbum(ctx context.Context, album *Album) error {
_, err := d.Post(ALBUM_API_URL+"/delete", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(map[string]string{
"album_id": albumID,
"tid": tID,
"album_id": album.AlbumID,
"tid": fmt.Sprint(album.Tid),
"delete_origin_image": "0", // 是否删除原图 0 不删除 1 删除
})
}, nil)
@ -233,13 +230,13 @@ func (p *BaiduPhoto) DeleteAlbum(ctx context.Context, albumID, tID string) error
}
// 删除相册文件
func (p *BaiduPhoto) DeleteAlbumFile(ctx context.Context, albumID, tID string, fileIDs ...string) error {
_, err := p.Post(ALBUM_API_URL+"/delfile", func(r *resty.Request) {
func (d *BaiduPhoto) DeleteAlbumFile(ctx context.Context, file *AlbumFile) error {
_, err := d.Post(ALBUM_API_URL+"/delfile", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(map[string]string{
"album_id": albumID,
"tid": tID,
"list": fsidsFormat(fileIDs...),
"album_id": fmt.Sprint(file.AlbumID),
"tid": fmt.Sprint(file.Tid),
"list": fmt.Sprintf(`[{"fsid":%d,"uk":%d}]`, file.Fsid, file.Uk),
"del_origin": "0", // 是否删除原图 0 不删除 1 删除
})
}, nil)
@ -247,41 +244,44 @@ func (p *BaiduPhoto) DeleteAlbumFile(ctx context.Context, albumID, tID string, f
}
// 增加相册文件
func (p *BaiduPhoto) AddAlbumFile(ctx context.Context, albumID, tID string, fileIDs ...string) error {
_, err := p.Get(ALBUM_API_URL+"/addfile", func(r *resty.Request) {
func (d *BaiduPhoto) AddAlbumFile(ctx context.Context, album *Album, file *File) (*AlbumFile, error) {
_, err := d.Get(ALBUM_API_URL+"/addfile", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"album_id": albumID,
"tid": tID,
"list": fsidsFormatNotUk(fileIDs...),
"album_id": fmt.Sprint(album.AlbumID),
"tid": fmt.Sprint(album.Tid),
"list": fsidsFormatNotUk(file.Fsid),
})
}, nil)
return err
if err != nil {
return nil, err
}
return moveFileToAlbumFile(file, album, d.Uk), nil
}
// 保存相册文件为根文件
func (p *BaiduPhoto) CopyAlbumFile(ctx context.Context, albumID, tID, uk string, fileID ...string) (*CopyFile, error) {
func (d *BaiduPhoto) CopyAlbumFile(ctx context.Context, file *AlbumFile) (*File, error) {
var resp CopyFileResp
_, err := p.Post(ALBUM_API_URL+"/copyfile", func(r *resty.Request) {
_, err := d.Post(ALBUM_API_URL+"/copyfile", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(map[string]string{
"album_id": albumID,
"tid": tID,
"uk": uk,
"list": fsidsFormatNotUk(fileID...),
"album_id": file.AlbumID,
"tid": fmt.Sprint(file.Tid),
"uk": fmt.Sprint(file.Uk),
"list": fsidsFormatNotUk(file.Fsid),
})
r.SetResult(&resp)
}, nil)
if err != nil {
return nil, err
}
return &resp.List[0], nil
return copyFile(file, &resp.List[0]), nil
}
// 加入相册
func (p *BaiduPhoto) JoinAlbum(ctx context.Context, code string) error {
func (d *BaiduPhoto) JoinAlbum(ctx context.Context, code string) (*Album, error) {
var resp InviteResp
_, err := p.Get(ALBUM_API_URL+"/querypcode", func(req *resty.Request) {
_, err := d.Get(ALBUM_API_URL+"/querypcode", func(req *resty.Request) {
req.SetContext(ctx)
req.SetQueryParams(map[string]string{
"pcode": code,
@ -289,18 +289,37 @@ func (p *BaiduPhoto) JoinAlbum(ctx context.Context, code string) error {
})
}, &resp)
if err != nil {
return err
return nil, err
}
_, err = p.Get(ALBUM_API_URL+"/join", func(req *resty.Request) {
var resp2 JoinOrCreateAlbumResp
_, err = d.Get(ALBUM_API_URL+"/join", func(req *resty.Request) {
req.SetContext(ctx)
req.SetQueryParams(map[string]string{
"invite_code": resp.Pdata.InviteCode,
})
}, nil)
return err
}, &resp2)
if err != nil {
return nil, err
}
return d.GetAlbumDetail(ctx, resp2.AlbumID)
}
func (d *BaiduPhoto) linkAlbum(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
// 获取相册详细信息
func (d *BaiduPhoto) GetAlbumDetail(ctx context.Context, albumID string) (*Album, error) {
var album Album
_, err := d.Get(ALBUM_API_URL+"/detail", func(req *resty.Request) {
req.SetContext(ctx).SetResult(&album)
req.SetQueryParams(map[string]string{
"album_id": albumID,
})
}, &album)
if err != nil {
return nil, err
}
return &album, nil
}
func (d *BaiduPhoto) linkAlbum(ctx context.Context, file *AlbumFile, args model.LinkArgs) (*model.Link, error) {
headers := map[string]string{
"User-Agent": base.UserAgent,
}
@ -311,16 +330,15 @@ func (d *BaiduPhoto) linkAlbum(ctx context.Context, file model.Obj, args model.L
headers["X-Forwarded-For"] = args.IP
}
e := splitID(file.GetID())
res, err := base.NoRedirectClient.R().
SetContext(ctx).
SetHeaders(headers).
SetQueryParams(map[string]string{
"access_token": d.AccessToken,
"fsid": e[0],
"album_id": e[1],
"tid": e[2],
"uk": e[3],
"fsid": fmt.Sprint(file.Fsid),
"album_id": file.AlbumID,
"tid": fmt.Sprint(file.Tid),
"uk": fmt.Sprint(file.Uk),
}).
Head(ALBUM_API_URL + "/download")
@ -328,19 +346,17 @@ func (d *BaiduPhoto) linkAlbum(ctx context.Context, file model.Obj, args model.L
return nil, err
}
//exp := 8 * time.Hour
link := &model.Link{
URL: res.Header().Get("location"),
Header: http.Header{
"User-Agent": []string{headers["User-Agent"]},
"Referer": []string{"https://photo.baidu.com/"},
},
//Expiration: &exp,
}
return link, nil
}
func (d *BaiduPhoto) linkFile(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
func (d *BaiduPhoto) linkFile(ctx context.Context, file *File, args model.LinkArgs) (*model.Link, error) {
headers := map[string]string{
"User-Agent": base.UserAgent,
}
@ -358,21 +374,31 @@ func (d *BaiduPhoto) linkFile(ctx context.Context, file model.Obj, args model.Li
r.SetContext(ctx)
r.SetHeaders(headers)
r.SetQueryParams(map[string]string{
"fsid": splitID(file.GetID())[0],
"fsid": fmt.Sprint(file.Fsid),
})
}, &downloadUrl)
if err != nil {
return nil, err
}
//exp := 8 * time.Hour
link := &model.Link{
URL: downloadUrl.Dlink,
Header: http.Header{
"User-Agent": []string{headers["User-Agent"]},
"Referer": []string{"https://photo.baidu.com/"},
},
//Expiration: &exp,
}
return link, nil
}
// 获取uk
func (d *BaiduPhoto) uInfo() (*UInfo, error) {
var info UInfo
_, err := d.Get(USER_API_URL+"/getuinfo", func(req *resty.Request) {
}, &info)
if err != nil {
return nil, err
}
return &info, nil
}

View File

@ -0,0 +1,251 @@
package baidu_share
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"time"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/go-resty/resty/v2"
)
type BaiduShare struct {
model.Storage
Addition
client *resty.Client
info struct {
Root string
Seckey string
Shareid string
Uk string
}
}
func (d *BaiduShare) Config() driver.Config {
return config
}
func (d *BaiduShare) GetAddition() driver.Additional {
return &d.Addition
}
func (d *BaiduShare) Init(ctx context.Context) error {
// TODO login / refresh token
//op.MustSaveDriverStorage(d)
d.client = resty.New().
SetBaseURL("https://pan.baidu.com").
SetHeader("User-Agent", "netdisk").
SetCookie(&http.Cookie{Name: "BDUSS", Value: d.BDUSS}).
SetCookie(&http.Cookie{Name: "ndut_fmt"})
respJson := struct {
Errno int64 `json:"errno"`
Data struct {
List [1]struct {
Path string `json:"path"`
} `json:"list"`
Uk json.Number `json:"uk"`
Shareid json.Number `json:"shareid"`
Seckey string `json:"seckey"`
} `json:"data"`
}{}
resp, err := d.client.R().
SetBody(url.Values{
"pwd": {d.Pwd},
"root": {"1"},
"shorturl": {d.Surl},
}.Encode()).
SetResult(&respJson).
Post("share/wxlist?channel=weixin&version=2.2.2&clienttype=25&web=1")
if err == nil {
if resp.IsSuccess() && respJson.Errno == 0 {
d.info.Root = path.Dir(respJson.Data.List[0].Path)
d.info.Seckey = respJson.Data.Seckey
d.info.Shareid = respJson.Data.Shareid.String()
d.info.Uk = respJson.Data.Uk.String()
} else {
err = fmt.Errorf(" %s; %s; ", resp.Status(), resp.Body())
}
}
return err
}
func (d *BaiduShare) Drop(ctx context.Context) error {
return nil
}
func (d *BaiduShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
// TODO return the files list, required
reqDir := dir.GetPath()
isRoot := "0"
if reqDir == d.RootFolderPath {
reqDir = path.Join(d.info.Root, reqDir)
}
if reqDir == d.info.Root {
isRoot = "1"
}
objs := []model.Obj{}
var err error
var page uint64 = 1
more := true
for more && err == nil {
respJson := struct {
Errno int64 `json:"errno"`
Data struct {
More bool `json:"has_more"`
List []struct {
Fsid json.Number `json:"fs_id"`
Isdir json.Number `json:"isdir"`
Path string `json:"path"`
Name string `json:"server_filename"`
Mtime json.Number `json:"server_mtime"`
Size json.Number `json:"size"`
} `json:"list"`
} `json:"data"`
}{}
resp, e := d.client.R().
SetBody(url.Values{
"dir": {reqDir},
"num": {"1000"},
"order": {"time"},
"page": {fmt.Sprint(page)},
"pwd": {d.Pwd},
"root": {isRoot},
"shorturl": {d.Surl},
}.Encode()).
SetResult(&respJson).
Post("share/wxlist?channel=weixin&version=2.2.2&clienttype=25&web=1")
err = e
if err == nil {
if resp.IsSuccess() && respJson.Errno == 0 {
page++
more = respJson.Data.More
for _, v := range respJson.Data.List {
size, _ := v.Size.Int64()
mtime, _ := v.Mtime.Int64()
objs = append(objs, &model.Object{
ID: v.Fsid.String(),
Path: v.Path,
Name: v.Name,
Size: size,
Modified: time.Unix(mtime, 0),
IsFolder: v.Isdir.String() == "1",
})
}
} else {
err = fmt.Errorf(" %s; %s; ", resp.Status(), resp.Body())
}
}
}
return objs, err
}
func (d *BaiduShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
// TODO return link of file, required
link := model.Link{Header: d.client.Header}
sign := ""
stamp := ""
signJson := struct {
Errno int64 `json:"errno"`
Data struct {
Stamp json.Number `json:"timestamp"`
Sign string `json:"sign"`
} `json:"data"`
}{}
resp, err := d.client.R().
SetQueryParam("surl", d.Surl).
SetResult(&signJson).
Get("share/tplconfig?fields=sign,timestamp&channel=chunlei&web=1&app_id=250528&clienttype=0")
if err == nil {
if resp.IsSuccess() && signJson.Errno == 0 {
stamp = signJson.Data.Stamp.String()
sign = signJson.Data.Sign
} else {
err = fmt.Errorf(" %s; %s; ", resp.Status(), resp.Body())
}
}
if err == nil {
respJson := struct {
Errno int64 `json:"errno"`
List [1]struct {
Dlink string `json:"dlink"`
} `json:"list"`
}{}
resp, err = d.client.R().
SetQueryParam("sign", sign).
SetQueryParam("timestamp", stamp).
SetBody(url.Values{
"encrypt": {"0"},
"extra": {fmt.Sprintf(`{"sekey":"%s"}`, d.info.Seckey)},
"fid_list": {fmt.Sprintf("[%s]", file.GetID())},
"primaryid": {d.info.Shareid},
"product": {"share"},
"type": {"nolimit"},
"uk": {d.info.Uk},
}.Encode()).
SetResult(&respJson).
Post("api/sharedownload?app_id=250528&channel=chunlei&clienttype=12&web=1")
if err == nil {
if resp.IsSuccess() && respJson.Errno == 0 && respJson.List[0].Dlink != "" {
link.URL = respJson.List[0].Dlink
} else {
err = fmt.Errorf(" %s; %s; ", resp.Status(), resp.Body())
}
}
if err == nil {
resp, err = d.client.R().
SetDoNotParseResponse(true).
Get(link.URL)
if err == nil {
defer resp.RawBody().Close()
if resp.IsError() {
byt, _ := io.ReadAll(resp.RawBody())
err = fmt.Errorf(" %s; %s; ", resp.Status(), byt)
}
}
}
}
return &link, err
}
func (d *BaiduShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
// TODO create folder, optional
return errs.NotSupport
}
func (d *BaiduShare) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
// TODO move obj, optional
return errs.NotSupport
}
func (d *BaiduShare) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
// TODO rename obj, optional
return errs.NotSupport
}
func (d *BaiduShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
// TODO copy obj, optional
return errs.NotSupport
}
func (d *BaiduShare) Remove(ctx context.Context, obj model.Obj) error {
// TODO remove obj, optional
return errs.NotSupport
}
func (d *BaiduShare) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
// TODO upload file, optional
return errs.NotSupport
}
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*BaiduShare)(nil)

View File

@ -0,0 +1,37 @@
package baidu_share
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
// Usually one of two
driver.RootPath
// driver.RootID
// define other
// Field string `json:"field" type:"select" required:"true" options:"a,b,c" default:"a"`
Surl string `json:"surl"`
Pwd string `json:"pwd"`
BDUSS string `json:"BDUSS"`
}
var config = driver.Config{
Name: "BaiduShare",
LocalSort: true,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: true,
NeedMs: false,
DefaultRoot: "/",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &BaiduShare{}
})
}

View File

@ -0,0 +1 @@
package baidu_share

View File

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

View File

@ -1,30 +1,49 @@
package base
import (
"crypto/tls"
"net/http"
"time"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/go-resty/resty/v2"
)
var NoRedirectClient *resty.Client
var RestyClient = NewRestyClient()
var HttpClient = &http.Client{}
var (
NoRedirectClient *resty.Client
RestyClient *resty.Client
HttpClient *http.Client
)
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
var DefaultTimeout = time.Second * 10
var DefaultTimeout = time.Second * 30
func init() {
func InitClient() {
NoRedirectClient = resty.New().SetRedirectPolicy(
resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}),
)
).SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
NoRedirectClient.SetHeader("user-agent", UserAgent)
RestyClient = NewRestyClient()
HttpClient = NewHttpClient()
}
func NewRestyClient() *resty.Client {
return resty.New().
client := resty.New().
SetHeader("user-agent", UserAgent).
SetRetryCount(3).
SetTimeout(DefaultTimeout)
SetTimeout(DefaultTimeout).
SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
return client
}
func NewHttpClient() *http.Client {
return &http.Client{
Timeout: time.Hour * 48,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify},
},
}
}

30
drivers/base/util.go Normal file
View File

@ -0,0 +1,30 @@
package base
import (
"io"
"net/http"
"strconv"
"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"
)
func HandleRange(link *model.Link, file io.ReadSeekCloser, header http.Header, size int64) {
if header.Get("Range") != "" {
r, err := http_range.ParseRange(header.Get("Range"), size)
if err == nil && len(r) > 0 {
_, err := file.Seek(r[0].Start, io.SeekStart)
if err == nil {
link.Data = utils.NewLimitReadCloser(file, func() error {
return file.Close()
}, r[0].Length)
link.Status = http.StatusPartialContent
link.Header = http.Header{
"Content-Range": []string{r[0].ContentRange(size)},
"Content-Length": []string{strconv.FormatInt(r[0].Length, 10)},
}
}
}
}
}

189
drivers/cloudreve/driver.go Normal file
View File

@ -0,0 +1,189 @@
package cloudreve
import (
"context"
"io"
"net/http"
"strconv"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
type Cloudreve struct {
model.Storage
Addition
}
func (d *Cloudreve) Config() driver.Config {
return config
}
func (d *Cloudreve) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Cloudreve) Init(ctx context.Context) error {
if d.Cookie != "" {
return nil
}
// removing trailing slash
d.Address = strings.TrimSuffix(d.Address, "/")
return d.login()
}
func (d *Cloudreve) Drop(ctx context.Context) error {
d.Cookie = ""
return nil
}
func (d *Cloudreve) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
var r DirectoryResp
err := d.request(http.MethodGet, "/directory"+dir.GetPath(), nil, &r)
if err != nil {
return nil, err
}
return utils.SliceConvert(r.Objects, func(src Object) (model.Obj, error) {
return objectToObj(src), nil
})
}
func (d *Cloudreve) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var dUrl string
err := d.request(http.MethodPut, "/file/download/"+file.GetID(), nil, &dUrl)
if err != nil {
return nil, err
}
return &model.Link{
URL: dUrl,
}, nil
}
func (d *Cloudreve) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return d.request(http.MethodPut, "/directory", func(req *resty.Request) {
req.SetBody(base.Json{
"path": parentDir.GetPath() + "/" + dirName,
})
}, nil)
}
func (d *Cloudreve) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
body := base.Json{
"action": "move",
"src_dir": srcObj.GetPath(),
"dst": dstDir.GetPath(),
"src": convertSrc(srcObj),
}
return d.request(http.MethodPatch, "/object", func(req *resty.Request) {
req.SetBody(body)
}, nil)
}
func (d *Cloudreve) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
body := base.Json{
"action": "rename",
"new_name": newName,
"src": convertSrc(srcObj),
}
return d.request(http.MethodPatch, "/object/rename", func(req *resty.Request) {
req.SetBody(body)
}, nil)
}
func (d *Cloudreve) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
body := base.Json{
"src_dir": srcObj.GetPath(),
"dst": dstDir.GetPath(),
"src": convertSrc(srcObj),
}
return d.request(http.MethodPost, "/object/copy", func(req *resty.Request) {
req.SetBody(body)
}, nil)
}
func (d *Cloudreve) Remove(ctx context.Context, obj model.Obj) error {
body := convertSrc(obj)
err := d.request(http.MethodDelete, "/object", func(req *resty.Request) {
req.SetBody(body)
}, nil)
return err
}
func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
if stream.GetReadCloser() == http.NoBody {
return d.create(ctx, dstDir, stream)
}
var r DirectoryResp
err := d.request(http.MethodGet, "/directory"+dstDir.GetPath(), nil, &r)
if err != nil {
return err
}
uploadBody := base.Json{
"path": dstDir.GetPath(),
"size": stream.GetSize(),
"name": stream.GetName(),
"policy_id": r.Policy.Id,
"last_modified": stream.ModTime().Unix(),
}
var u UploadInfo
err = d.request(http.MethodPut, "/file/upload", func(req *resty.Request) {
req.SetBody(uploadBody)
}, &u)
if err != nil {
return err
}
var chunkSize = u.ChunkSize
var buf []byte
var chunk int
for {
var n int
buf = make([]byte, chunkSize)
n, err = io.ReadAtLeast(stream, buf, chunkSize)
if err != nil && err != io.ErrUnexpectedEOF {
if err == io.EOF {
return nil
}
return err
}
if n == 0 {
break
}
buf = buf[:n]
err = d.request(http.MethodPost, "/file/upload/"+u.SessionID+"/"+strconv.Itoa(chunk), func(req *resty.Request) {
req.SetHeader("Content-Type", "application/octet-stream")
req.SetHeader("Content-Length", strconv.Itoa(n))
req.SetBody(buf)
}, nil)
if err != nil {
break
}
chunk++
}
return err
}
func (d *Cloudreve) create(ctx context.Context, dir model.Obj, file model.Obj) error {
body := base.Json{"path": dir.GetPath() + "/" + file.GetName()}
if file.IsDir() {
err := d.request(http.MethodPut, "directory", func(req *resty.Request) {
req.SetBody(body)
}, nil)
return err
}
return d.request(http.MethodPost, "/file/create", func(req *resty.Request) {
req.SetBody(body)
}, nil)
}
//func (d *Cloudreve) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*Cloudreve)(nil)

27
drivers/cloudreve/meta.go Normal file
View File

@ -0,0 +1,27 @@
package cloudreve
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
// Usually one of two
driver.RootPath
// define other
Address string `json:"address" required:"true"`
Username string `json:"username"`
Password string `json:"password"`
Cookie string `json:"cookie"`
}
var config = driver.Config{
Name: "Cloudreve",
DefaultRoot: "/",
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Cloudreve{}
})
}

View File

@ -0,0 +1,60 @@
package cloudreve
import (
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type Resp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
type Policy struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
MaxSize int `json:"max_size"`
FileType []string `json:"file_type"`
}
type UploadInfo struct {
SessionID string `json:"sessionID"`
ChunkSize int `json:"chunkSize"`
Expires int `json:"expires"`
}
type DirectoryResp struct {
Parent string `json:"parent"`
Objects []Object `json:"objects"`
Policy Policy `json:"policy"`
}
type Object struct {
Id string `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
Pic string `json:"pic"`
Size int `json:"size"`
Type string `json:"type"`
Date time.Time `json:"date"`
CreateDate time.Time `json:"create_date"`
SourceEnabled bool `json:"source_enabled"`
}
func objectToObj(f Object) *model.Object {
return &model.Object{
ID: f.Id,
Name: f.Name,
Size: int64(f.Size),
Modified: f.Date,
IsFolder: f.Type == "dir",
}
}
type Config struct {
LoginCaptcha bool `json:"loginCaptcha"`
CaptchaType string `json:"captcha_type"`
}

148
drivers/cloudreve/util.go Normal file
View File

@ -0,0 +1,148 @@
package cloudreve
import (
"encoding/base64"
"errors"
"net/http"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/cookie"
"github.com/go-resty/resty/v2"
json "github.com/json-iterator/go"
jsoniter "github.com/json-iterator/go"
)
// do others that not defined in Driver interface
const loginPath = "/user/session"
func (d *Cloudreve) request(method string, path string, callback base.ReqCallback, out interface{}) error {
u := d.Address + "/api/v3" + path
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"Cookie": "cloudreve-session=" + d.Cookie,
"Accept": "application/json, text/plain, */*",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
})
var r Resp
req.SetResult(&r)
if callback != nil {
callback(req)
}
resp, err := req.Execute(method, u)
if err != nil {
return err
}
if !resp.IsSuccess() {
return errors.New(resp.String())
}
if r.Code != 0 {
// 刷新 cookie
if r.Code == http.StatusUnauthorized && path != loginPath {
if d.Username != "" && d.Password != "" {
err = d.login()
if err != nil {
return err
}
return d.request(method, path, callback, out)
}
}
return errors.New(r.Msg)
}
sess := cookie.GetCookie(resp.Cookies(), "cloudreve-session")
if sess != nil {
d.Cookie = sess.Value
}
if out != nil && r.Data != nil {
var marshal []byte
marshal, err = json.Marshal(r.Data)
if err != nil {
return err
}
err = json.Unmarshal(marshal, out)
if err != nil {
return err
}
}
return nil
}
func (d *Cloudreve) login() error {
var siteConfig Config
err := d.request(http.MethodGet, "/site/config", nil, &siteConfig)
if err != nil {
return err
}
for i := 0; i < 5; i++ {
err = d.doLogin(siteConfig.LoginCaptcha)
if err == nil {
break
}
if err != nil && err.Error() != "CAPTCHA not match." {
break
}
}
return err
}
func (d *Cloudreve) doLogin(needCaptcha bool) error {
var captchaCode string
var err error
if needCaptcha {
var captcha string
err = d.request(http.MethodGet, "/site/captcha", nil, &captcha)
if err != nil {
return err
}
if len(captcha) == 0 {
return errors.New("can not get captcha")
}
i := strings.Index(captcha, ",")
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(captcha[i+1:]))
vRes, err := base.RestyClient.R().SetMultipartField(
"image", "validateCode.png", "image/png", dec).
Post(setting.GetStr(conf.OcrApi))
if err != nil {
return err
}
if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 {
return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString())
}
captchaCode = jsoniter.Get(vRes.Body(), "result").ToString()
}
var resp Resp
err = d.request(http.MethodPost, loginPath, func(req *resty.Request) {
req.SetBody(base.Json{
"username": d.Addition.Username,
"Password": d.Addition.Password,
"captchaCode": captchaCode,
})
}, &resp)
return err
}
func convertSrc(obj model.Obj) map[string]interface{} {
m := make(map[string]interface{})
var dirs []string
var items []string
if obj.IsDir() {
dirs = append(dirs, obj.GetID())
} else {
items = append(items, obj.GetID())
}
m["dirs"] = dirs
m["items"] = items
return m
}

222
drivers/dropbox/driver.go Normal file
View File

@ -0,0 +1,222 @@
package dropbox
import (
"context"
"fmt"
"io"
"math"
"net/http"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
type Dropbox struct {
model.Storage
Addition
base string
contentBase string
}
func (d *Dropbox) Config() driver.Config {
return config
}
func (d *Dropbox) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Dropbox) Init(ctx context.Context) error {
query := "foo"
res, err := d.request("/2/check/user", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"query": query,
})
})
if err != nil {
return err
}
result := utils.Json.Get(res, "result").ToString()
if result != query {
return fmt.Errorf("failed to check user: %s", string(res))
}
return nil
}
func (d *Dropbox) Drop(ctx context.Context) error {
return nil
}
func (d *Dropbox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles(ctx, dir.GetPath())
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
return fileToObj(src), nil
})
}
func (d *Dropbox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
res, err := d.request("/2/files/get_temporary_link", http.MethodPost, func(req *resty.Request) {
req.SetContext(ctx).SetBody(base.Json{
"path": file.GetPath(),
})
})
if err != nil {
return nil, err
}
url := utils.Json.Get(res, "link").ToString()
exp := time.Hour
return &model.Link{
URL: url,
Expiration: &exp,
}, nil
}
func (d *Dropbox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
_, err := d.request("/2/files/create_folder_v2", http.MethodPost, func(req *resty.Request) {
req.SetContext(ctx).SetBody(base.Json{
"autorename": false,
"path": parentDir.GetPath() + "/" + dirName,
})
})
return err
}
func (d *Dropbox) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
toPath := dstDir.GetPath() + "/" + srcObj.GetName()
_, err := d.request("/2/files/move_v2", http.MethodPost, func(req *resty.Request) {
req.SetContext(ctx).SetBody(base.Json{
"allow_ownership_transfer": false,
"allow_shared_folder": false,
"autorename": false,
"from_path": srcObj.GetID(),
"to_path": toPath,
})
})
return err
}
func (d *Dropbox) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
path := srcObj.GetPath()
fileName := srcObj.GetName()
toPath := path[:len(path)-len(fileName)] + newName
_, err := d.request("/2/files/move_v2", http.MethodPost, func(req *resty.Request) {
req.SetContext(ctx).SetBody(base.Json{
"allow_ownership_transfer": false,
"allow_shared_folder": false,
"autorename": false,
"from_path": srcObj.GetID(),
"to_path": toPath,
})
})
return err
}
func (d *Dropbox) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
toPath := dstDir.GetPath() + "/" + srcObj.GetName()
_, err := d.request("/2/files/copy_v2", http.MethodPost, func(req *resty.Request) {
req.SetContext(ctx).SetBody(base.Json{
"allow_ownership_transfer": false,
"allow_shared_folder": false,
"autorename": false,
"from_path": srcObj.GetID(),
"to_path": toPath,
})
})
return err
}
func (d *Dropbox) Remove(ctx context.Context, obj model.Obj) error {
uri := "/2/files/delete_v2"
_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {
req.SetContext(ctx).SetBody(base.Json{
"path": obj.GetID(),
})
})
return err
}
func (d *Dropbox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
// 1. start
sessionId, err := d.startUploadSession(ctx)
if err != nil {
return err
}
// 2.append
// A single request should not upload more than 150 MB, and each call must be multiple of 4MB (except for last call)
const PartSize = 20971520
count := 1
if stream.GetSize() > PartSize {
count = int(math.Ceil(float64(stream.GetSize()) / float64(PartSize)))
}
offset := int64(0)
for i := 0; i < count; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
start := i * PartSize
byteSize := stream.GetSize() - int64(start)
if byteSize > PartSize {
byteSize = PartSize
}
url := d.contentBase + "/2/files/upload_session/append_v2"
reader := io.LimitReader(stream, PartSize)
req, err := http.NewRequest(http.MethodPost, url, reader)
if err != nil {
log.Errorf("failed to update file when append to upload session, err: %+v", err)
return err
}
req = req.WithContext(ctx)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Authorization", "Bearer "+d.AccessToken)
args := UploadAppendArgs{
Close: false,
Cursor: UploadCursor{
Offset: offset,
SessionID: sessionId,
},
}
argsJson, err := utils.Json.MarshalToString(args)
if err != nil {
return err
}
req.Header.Set("Dropbox-API-Arg", argsJson)
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
_ = res.Body.Close()
if count > 0 {
up((i + 1) * 100 / count)
}
offset += byteSize
}
// 3.finish
toPath := dstDir.GetPath() + "/" + stream.GetName()
err2 := d.finishUploadSession(ctx, toPath, offset, sessionId)
if err2 != nil {
return err2
}
return err
}
var _ driver.Driver = (*Dropbox)(nil)

42
drivers/dropbox/meta.go Normal file
View File

@ -0,0 +1,42 @@
package dropbox
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
const (
DefaultClientID = "76lrwrklhdn1icb"
)
type Addition struct {
RefreshToken string `json:"refresh_token" required:"true"`
driver.RootPath
OauthTokenURL string `json:"oauth_token_url" default:"https://api.xhofe.top/alist/dropbox/token"`
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
AccessToken string
}
var config = driver.Config{
Name: "Dropbox",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "",
NoOverwriteUpload: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Dropbox{
base: "https://api.dropboxapi.com",
contentBase: "https://content.dropboxapi.com",
}
})
}

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