Compare commits

...

172 Commits

Author SHA1 Message Date
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
244 changed files with 7269 additions and 2667 deletions

View File

@ -43,8 +43,8 @@ body:
attributes: attributes:
label: Reproduction / 复现链接 label: Reproduction / 复现链接
description: | 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: validations:
required: true required: true
- type: textarea - type: textarea
@ -54,4 +54,3 @@ body:
description: | description: |
Please copy and paste any relevant log output. Please copy and paste any relevant log output.
请复制粘贴错误日志,或者截图 请复制粘贴错误日志,或者截图
render: shell

View File

@ -7,6 +7,8 @@ on:
paths: paths:
- 'drivers/**' - 'drivers/**'
- 'internal/bootstrap/data/setting.go' - 'internal/bootstrap/data/setting.go'
- 'internal/conf/const.go'
- 'cmd/lang.go'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -19,12 +21,12 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Setup go - name: Setup go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Checkout alist - name: Checkout alist
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
path: alist path: alist
@ -42,6 +44,7 @@ jobs:
cd alist cd alist
go run ./main.go lang go run ./main.go lang
cd .. cd ..
- name: Copy lang file - name: Copy lang file
run: | run: |
cp -f ./alist/lang/*.json ./alist-web/src/lang/en/ 2>/dev/null || : cp -f ./alist/lang/*.json ./alist-web/src/lang/en/ 2>/dev/null || :
@ -61,4 +64,4 @@ jobs:
github_token: ${{ secrets.MY_TOKEN }} github_token: ${{ secrets.MY_TOKEN }}
branch: main branch: main
directory: alist-web directory: alist-web
repository: alist-org/alist-web repository: alist-org/alist-web

View File

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

View File

@ -10,27 +10,27 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: xhofe/alist images: xhofe/alist
- name: Replace release with dev - name: Replace release with dev
run: | run: |
sed -i 's/release/dev/g' Dockerfile sed -i 's/release/dev/g' Dockerfile
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: xhofe username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: true push: true

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result - run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
@ -27,19 +27,19 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install dependencies - name: Install dependencies
run: | run: |
docker pull techknowlogick/xgo:latest docker pull crazymax/xgo:latest
go install src.techknowlogick.com/xgo@latest go install github.com/crazy-max/xgo@latest
sudo apt install upx sudo apt install upx
- name: Build - name: Build

View File

@ -11,29 +11,29 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: xhofe/alist images: xhofe/alist
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: xhofe username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: true push: true

14
.gitignore vendored
View File

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

View File

@ -2,7 +2,7 @@ FROM alpine:edge as builder
LABEL stage=go-builder LABEL stage=go-builder
WORKDIR /app/ WORKDIR /app/
COPY ./ ./ 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 bash build.sh release docker
FROM alpine:edge FROM alpine:edge
@ -10,6 +10,9 @@ LABEL MAINTAINER="i@nn.ci"
VOLUME /opt/alist/data/ VOLUME /opt/alist/data/
WORKDIR /opt/alist/ WORKDIR /opt/alist/
COPY --from=builder /app/bin/alist ./ COPY --from=builder /app/bin/alist ./
RUN apk add ca-certificates 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 EXPOSE 5244
CMD [ "./alist", "server", "--no-prefix" ] ENTRYPOINT [ "/entrypoint.sh" ]

View File

@ -9,7 +9,7 @@
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" /> <img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a> </a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild"> <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>
<a href="https://github.com/Xhofe/alist/releases"> <a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" /> <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] FTP / SFTP
- [x] [PikPak](https://www.mypikpak.com/) - [x] [PikPak](https://www.mypikpak.com/)
- [x] [S3](https://aws.amazon.com/s3/) - [x] [S3](https://aws.amazon.com/s3/)
- [x] [Seafile](https://seafile.com/)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage) - [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] WebDav(Support OneDrive/SharePoint without API) - [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ )) - [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
@ -60,6 +61,7 @@ English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_O
- [x] [139yun](https://yun.139.com/) (Personal, Family) - [x] [139yun](https://yun.139.com/) (Personal, Family)
- [x] [YandexDisk](https://disk.yandex.com/) - [x] [YandexDisk](https://disk.yandex.com/)
- [x] [BaiduNetdisk](http://pan.baidu.com/) - [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [Quark](https://pan.quark.cn) - [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com) - [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/) - [x] [Lanzou](https://www.lanzou.com/)
@ -69,6 +71,7 @@ English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_O
- [x] [Baidu photo](https://photo.baidu.com/) - [x] [Baidu photo](https://photo.baidu.com/)
- [x] SMB - [x] SMB
- [x] [115](https://115.com/) - [x] [115](https://115.com/)
- [X] Cloudreve
- [x] Easy to deploy and out-of-the-box - [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...) - [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode - [x] Image preview in gallery mode

View File

@ -9,7 +9,7 @@
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" /> <img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a> </a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild"> <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>
<a href="https://github.com/Xhofe/alist/releases"> <a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" /> <img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
@ -53,6 +53,7 @@
- [x] FTP / SFTP - [x] FTP / SFTP
- [x] [PikPak](https://www.mypikpak.com/) - [x] [PikPak](https://www.mypikpak.com/)
- [x] [S3](https://aws.amazon.com/cn/s3/) - [x] [S3](https://aws.amazon.com/cn/s3/)
- [x] [Seafile](https://seafile.com/)
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage) - [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
- [x] WebDav(支持无API的OneDrive/SharePoint) - [x] WebDav(支持无API的OneDrive/SharePoint)
- [x] Teambition[中国](https://www.teambition.com/ )[国际](https://us.teambition.com/ ) - [x] Teambition[中国](https://www.teambition.com/ )[国际](https://us.teambition.com/ )
@ -69,6 +70,7 @@
- [x] [一刻相册](https://photo.baidu.com/) - [x] [一刻相册](https://photo.baidu.com/)
- [x] SMB - [x] SMB
- [x] [115](https://115.com/) - [x] [115](https://115.com/)
- [X] Cloudreve
- [x] 部署方便,开箱即用 - [x] 部署方便,开箱即用
- [x] 文件预览PDF、markdown、代码、纯文本…… - [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览 - [x] 画廊模式下的图像预览

View File

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

View File

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

View File

@ -1,8 +1,14 @@
package cmd package cmd
import ( import (
"os"
"path/filepath"
"strconv"
"github.com/alist-org/alist/v3/internal/bootstrap" "github.com/alist-org/alist/v3/internal/bootstrap"
"github.com/alist-org/alist/v3/internal/bootstrap/data" "github.com/alist-org/alist/v3/internal/bootstrap/data"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
) )
func Init() { func Init() {
@ -10,4 +16,29 @@ func Init() {
bootstrap.Log() bootstrap.Log()
bootstrap.InitDB() bootstrap.InitDB()
data.InitData() 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

@ -71,7 +71,7 @@ func writeFile(name string, data interface{}) {
} else { } else {
log.Infof("%s.json changed, update file", name) log.Infof("%s.json changed, update file", name)
//log.Infof("old: %+v\nnew:%+v", oldData, data) //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)
} }
} }
@ -123,8 +123,8 @@ func generateSettingsJson() {
//utils.WriteJsonToFile("lang/settings.json", settingsLang) //utils.WriteJsonToFile("lang/settings.json", settingsLang)
} }
// langCmd represents the lang command // LangCmd represents the lang command
var langCmd = &cobra.Command{ var LangCmd = &cobra.Command{
Use: "lang", Use: "lang",
Short: "Generate language json file", Short: "Generate language json file",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
@ -138,7 +138,7 @@ var langCmd = &cobra.Command{
} }
func init() { func init() {
rootCmd.AddCommand(langCmd) RootCmd.AddCommand(LangCmd)
// Here you will define your flags and configuration settings. // 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" "github.com/spf13/cobra"
) )
var rootCmd = &cobra.Command{ var RootCmd = &cobra.Command{
Use: "alist", Use: "alist",
Short: "A file list program that supports multiple storage.", Short: "A file list program that supports multiple storage.",
Long: `A file list program that supports multiple storage, Long: `A file list program that supports multiple storage,
@ -17,16 +17,16 @@ Complete documentation is available at https://alist.nn.ci/`,
} }
func Execute() { func Execute() {
if err := rootCmd.Execute(); err != nil { if err := RootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) os.Exit(1)
} }
} }
func init() { func init() {
rootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "config file") RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "config file")
rootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode") 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.NoPrefix, "no-prefix", false, "disable env prefix")
rootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode") 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.ForceBinDir, "force-bin-dir", false, "Force to use the directory where the binary file is located as data directory")
} }

View File

@ -20,8 +20,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// serverCmd represents the server command // ServerCmd represents the server command
var serverCmd = &cobra.Command{ var ServerCmd = &cobra.Command{
Use: "server", Use: "server",
Short: "Start the server at the specified address", Short: "Start the server at the specified address",
Long: `Start the server at the specified address Long: `Start the server at the specified address
@ -76,7 +76,7 @@ the address is defined in config file`,
} }
func init() { func init() {
rootCmd.AddCommand(serverCmd) RootCmd.AddCommand(ServerCmd)
// Here you will define your flags and configuration settings. // Here you will define your flags and configuration settings.
@ -95,5 +95,5 @@ func OutAlistInit() {
cmd *cobra.Command cmd *cobra.Command
args []string 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")
}

View File

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

15
docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
version: '3.3'
services:
alist:
restart: always
volumes:
- '/etc/alist:/opt/alist/data'
ports:
- '5244:5244'
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 { func (d *Pan115) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *Pan115) Init(ctx context.Context, storage model.Storage) error { func (d *Pan115) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
return d.login() return d.login()
} }

View File

@ -6,7 +6,8 @@ import (
) )
type Addition struct { type Addition struct {
Cookie string `json:"cookie" required:"true"` Cookie string `json:"cookie"`
QRCodeToken string `json:"qrcode_token"`
driver.RootID driver.RootID
} }
@ -17,10 +18,8 @@ var config = driver.Config{
OnlyLocal: true, OnlyLocal: true,
} }
func New() driver.Driver {
return &Pan115{}
}
func init() { func init() {
op.RegisterDriver(config, New) op.RegisterDriver(func() driver.Driver {
return &Pan115{}
})
} }

View File

@ -1,23 +1,38 @@
package _115 package _115
import ( import (
"fmt"
"github.com/SheltonZhu/115driver/pkg/driver" "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/drivers/base" "github.com/pkg/errors"
) )
var UserAgent = "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"
func (d *Pan115) login() error { func (d *Pan115) login() error {
var err error
opts := []driver.Option{ opts := []driver.Option{
driver.WithRestyClient(base.RestyClient), driver.UA(UserAgent),
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"),
} }
d.client = driver.New(opts...) d.client = driver.New(opts...)
cr := &driver.Credential{} cr := &driver.Credential{}
if err := cr.FromCookie(d.Addition.Cookie); err != nil { if d.Addition.QRCodeToken != "" {
return err 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")
} }
d.client.ImportCredential(cr)
return d.client.LoginCheck() return d.client.LoginCheck()
} }

View File

@ -4,9 +4,9 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/md5" "crypto/md5"
"encoding/base64"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -37,15 +37,10 @@ func (d *Pan123) Config() driver.Config {
} }
func (d *Pan123) GetAddition() driver.Additional { func (d *Pan123) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *Pan123) Init(ctx context.Context, storage model.Storage) error { func (d *Pan123) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
return d.login() return d.login()
} }
@ -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) { func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if f, ok := file.(File); ok { if f, ok := file.(File); ok {
var resp DownResp //var resp DownResp
var headers map[string]string var headers map[string]string
if !utils.IsLocalIPAddr(args.IP) { if !utils.IsLocalIPAddr(args.IP) {
headers = map[string]string{ headers = map[string]string{
@ -87,13 +77,14 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
"size": f.Size, "size": f.Size,
"type": f.Type, "type": f.Type,
} }
_, err := d.request("https://www.123pan.com/api/file/download_info", http.MethodPost, func(req *resty.Request) { resp, err := d.request("https://www.123pan.com/api/file/download_info", http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetHeaders(headers) req.SetBody(data).SetHeaders(headers)
}, &resp) }, nil)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -112,7 +103,7 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
} }
log.Debug(res.String()) log.Debug(res.String())
link := model.Link{ link := model.Link{
URL: resp.Data.DownloadUrl, URL: downloadUrl,
} }
log.Debugln("res code: ", res.StatusCode()) log.Debugln("res code: ", res.StatusCode())
if res.StatusCode() == 302 { if res.StatusCode() == 302 {
@ -229,13 +220,13 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
"type": 0, "type": 0,
} }
var resp UploadResp var resp UploadResp
_, err := d.request("https://www.123pan.com/api/file/upload_request", http.MethodPost, func(req *resty.Request) { _, err := d.request("https://www.123pan.com/a/api/file/upload_request", http.MethodPost, func(req *resty.Request) {
req.SetBody(data) req.SetBody(data).SetContext(ctx)
}, &resp) }, &resp)
if err != nil { if err != nil {
return err return err
} }
if resp.Data.Key == "" { if resp.Data.Reuse || resp.Data.Key == "" {
return nil return nil
} }
cfg := &aws.Config{ cfg := &aws.Config{
@ -254,14 +245,14 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
Key: &resp.Data.Key, Key: &resp.Data.Key,
Body: uploadFile, Body: uploadFile,
} }
_, err = uploader.Upload(input) _, err = uploader.UploadWithContext(ctx, input)
if err != nil { if err != nil {
return err return err
} }
_, err = d.request("https://www.123pan.com/api/file/upload_complete", http.MethodPost, func(req *resty.Request) { _, err = d.request("https://www.123pan.com/api/file/upload_complete", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{ req.SetBody(base.Json{
"fileId": resp.Data.FileId, "fileId": resp.Data.FileId,
}) }).SetContext(ctx)
}, nil) }, nil)
return err return err
} }

View File

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

View File

@ -7,13 +7,13 @@ import (
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
) )
type BaseResp struct { //type BaseResp struct {
Code int `json:"code"` // Code interface{} `json:"code"`
Message string `json:"message"` // Message string `json:"message"`
} //}
type TokenResp struct { type TokenResp struct {
BaseResp //BaseResp
Data struct { Data struct {
Token string `json:"token"` Token string `json:"token"`
} `json:"data"` } `json:"data"`
@ -62,22 +62,22 @@ var _ model.Obj = (*File)(nil)
//var _ model.Thumb = (*File)(nil) //var _ model.Thumb = (*File)(nil)
type Files struct { type Files struct {
BaseResp //BaseResp
Data struct { Data struct {
InfoList []File `json:"InfoList"` InfoList []File `json:"InfoList"`
Next string `json:"Next"` Next string `json:"Next"`
} `json:"data"` } `json:"data"`
} }
type DownResp struct { //type DownResp struct {
BaseResp // //BaseResp
Data struct { // Data struct {
DownloadUrl string `json:"DownloadUrl"` // DownloadUrl string `json:"DownloadUrl"`
} `json:"data"` // } `json:"data"`
} //}
type UploadResp struct { type UploadResp struct {
BaseResp //BaseResp
Data struct { Data struct {
AccessKeyId string `json:"AccessKeyId"` AccessKeyId string `json:"AccessKeyId"`
Bucket string `json:"Bucket"` Bucket string `json:"Bucket"`
@ -85,5 +85,6 @@ type UploadResp struct {
SecretAccessKey string `json:"SecretAccessKey"` SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken"` SessionToken string `json:"SessionToken"`
FileId int64 `json:"FileId"` FileId int64 `json:"FileId"`
Reuse bool `json:"Reuse"`
} `json:"data"` } `json:"data"`
} }

View File

@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@ -13,19 +14,29 @@ import (
// do others that not defined in Driver interface // do others that not defined in Driver interface
func (d *Pan123) login() error { func (d *Pan123) login() error {
url := "https://www.123pan.com/api/user/sign_in" var body base.Json
var resp TokenResp url := "https://www.123pan.com/a/api/user/sign_in"
_, err := base.RestyClient.R(). if utils.IsEmailFormat(d.Username) {
SetResult(&resp). body = base.Json{
SetBody(base.Json{ "mail": d.Username,
"password": d.Password,
"type": 2,
}
} else {
body = base.Json{
"passport": d.Username, "passport": d.Username,
"password": d.Password, "password": d.Password,
}).Post(url) }
}
var resp TokenResp
res, err := base.RestyClient.R().
SetResult(&resp).
SetBody(body).Post(url)
if err != nil { if err != nil {
return err return err
} }
if resp.Code != 200 { if utils.Json.Get(res.Body(), "code").ToInt() != 200 {
err = fmt.Errorf(resp.Message) err = fmt.Errorf(utils.Json.Get(res.Body(), "message").ToString())
} else { } else {
d.AccessToken = resp.Data.Token d.AccessToken = resp.Data.Token
} }
@ -34,7 +45,12 @@ func (d *Pan123) login() error {
func (d *Pan123) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { func (d *Pan123) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R() req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken) req.SetHeaders(map[string]string{
"origin": "https://www.123pan.com",
"authorization": "Bearer " + d.AccessToken,
"platform": "web",
"app-version": "1.2",
})
if callback != nil { if callback != nil {
callback(req) callback(req)
} }
@ -46,7 +62,7 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
return nil, err return nil, err
} }
body := res.Body() body := res.Body()
code := jsoniter.Get(body, "code").ToInt() code := utils.Json.Get(body, "code").ToInt()
if code != 0 { if code != 0 {
if code == 401 { if code == 401 {
err := d.login() err := d.login()

View File

@ -1,11 +1,9 @@
package _139 package _139
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"math"
"net/http" "net/http"
"strconv" "strconv"
@ -27,16 +25,11 @@ func (d *Yun139) Config() driver.Config {
} }
func (d *Yun139) GetAddition() driver.Additional { func (d *Yun139) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *Yun139) Init(ctx context.Context, storage model.Storage) error { func (d *Yun139) Init(ctx context.Context) error {
d.Storage = storage _, err := d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
_, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{
"qryUserExternInfoReq": base.Json{ "qryUserExternInfoReq": base.Json{
"commonAccountInfo": base.Json{ "commonAccountInfo": base.Json{
"account": d.Account, "account": d.Account,
@ -59,11 +52,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) { func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
u, err := d.getLink(file.GetID()) u, err := d.getLink(file.GetID())
if err != nil { if err != nil {
@ -239,10 +227,10 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
"manualRename": 2, "manualRename": 2,
"operation": 0, "operation": 0,
"fileCount": 1, "fileCount": 1,
"totalSize": stream.GetSize(), "totalSize": 0, // 去除上传大小限制
"uploadContentList": []base.Json{{ "uploadContentList": []base.Json{{
"contentName": stream.GetName(), "contentName": stream.GetName(),
"contentSize": stream.GetSize(), "contentSize": 0, // 去除上传大小限制
// "digest": "5a3231986ce7a6b46e408612d385bafa" // "digest": "5a3231986ce7a6b46e408612d385bafa"
}}, }},
"parentCatalogID": dstDir.GetID(), "parentCatalogID": dstDir.GetID(),
@ -260,10 +248,10 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
"operation": 0, "operation": 0,
"path": "", "path": "",
"seqNo": "", "seqNo": "",
"totalSize": stream.GetSize(), "totalSize": 0,
"uploadContentList": []base.Json{{ "uploadContentList": []base.Json{{
"contentName": stream.GetName(), "contentName": stream.GetName(),
"contentSize": stream.GetSize(), "contentSize": 0,
// "digest": "5a3231986ce7a6b46e408612d385bafa" // "digest": "5a3231986ce7a6b46e408612d385bafa"
}}, }},
}) })
@ -275,47 +263,47 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
if err != nil { if err != nil {
return err return err
} }
var Default int64 = 10485760
part := int(math.Ceil(float64(stream.GetSize()) / float64(Default))) // Progress
var start int64 = 0 p := driver.NewProgress(stream.GetSize(), up)
for i := 0; i < part; i++ {
var Default int64 = 104857600
part := (stream.GetSize() + Default - 1) / Default
for i := int64(0); i < part; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
start := i * Default
byteSize := stream.GetSize() - start byteSize := stream.GetSize() - start
if byteSize > Default { if byteSize > Default {
byteSize = Default byteSize = Default
} }
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 { if err != nil {
return err return err
} }
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, bytes.NewBuffer(byteData))
if err != nil { req = req.WithContext(ctx)
return err req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName()))
} req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10))
headers := map[string]string{ req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1))
"Accept": "*/*", req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID)
"Content-Type": "text/plain;name=" + unicode(stream.GetName()), req.Header.Set("rangeType", "0")
"contentSize": strconv.FormatInt(stream.GetSize(), 10), req.ContentLength = byteSize
"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)
}
res, err := base.HttpClient.Do(req) res, err := base.HttpClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
log.Debugf("%+v", res) log.Debugf("%+v", res)
res.Body.Close() res.Body.Close()
start += byteSize
up(i * 100 / part)
} }
return nil return nil
} }

View File

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

View File

@ -28,12 +28,15 @@ func (d *Yun139) isFamily() bool {
func encodeURIComponent(str string) string { func encodeURIComponent(str string) string {
r := url.QueryEscape(str) r := url.QueryEscape(str)
r = strings.Replace(r, "+", "%20", -1) 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 return r
} }
func calSign(body, ts, randStr string) string { func calSign(body, ts, randStr string) string {
body = strings.ReplaceAll(body, "\n", "")
body = strings.ReplaceAll(body, " ", "")
body = encodeURIComponent(body) body = encodeURIComponent(body)
strs := strings.Split(body, "") strs := strings.Split(body, "")
sort.Strings(strs) sort.Strings(strs)

View File

@ -26,15 +26,10 @@ func (d *Cloud189) Config() driver.Config {
} }
func (d *Cloud189) GetAddition() driver.Additional { func (d *Cloud189) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *Cloud189) Init(ctx context.Context, storage model.Storage) error { func (d *Cloud189) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
d.client = resty.New(). d.client = resty.New().
SetTimeout(base.DefaultTimeout). SetTimeout(base.DefaultTimeout).
SetRetryCount(3). SetRetryCount(3).
@ -51,11 +46,6 @@ func (d *Cloud189) List(ctx context.Context, dir model.Obj, args model.ListArgs)
return d.getFiles(dir.GetID()) 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) { func (d *Cloud189) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var resp DownResp var resp DownResp
u := "https://cloud.189.cn/api/portal/getFileInfo.action" u := "https://cloud.189.cn/api/portal/getFileInfo.action"
@ -204,7 +194,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 { 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) var _ driver.Driver = (*Cloud189)(nil)

View File

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

View File

@ -2,6 +2,7 @@ package _189
import ( import (
"bytes" "bytes"
"context"
"crypto/md5" "crypto/md5"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
@ -178,7 +179,6 @@ func (d *Cloud189) request(url string, method string, callback base.ReqCallback,
func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) { func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
res := make([]model.Obj, 0) res := make([]model.Obj, 0)
pageNum := 1 pageNum := 1
loc, _ := time.LoadLocation("Local")
for { for {
var resp Files var resp Files
_, err := d.request("https://cloud.189.cn/api/open/file/listFiles.action", http.MethodGet, func(req *resty.Request) { _, err := d.request("https://cloud.189.cn/api/open/file/listFiles.action", http.MethodGet, func(req *resty.Request) {
@ -200,7 +200,7 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
break break
} }
for _, folder := range resp.FileListAO.FolderList { 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{ res = append(res, &model.Object{
ID: strconv.FormatInt(folder.Id, 10), ID: strconv.FormatInt(folder.Id, 10),
Name: folder.Name, Name: folder.Name,
@ -209,7 +209,7 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
}) })
} }
for _, file := range resp.FileListAO.FileList { 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{ res = append(res, &model.ObjThumb{
Object: model.Object{ Object: model.Object{
ID: strconv.FormatInt(file.Id, 10), ID: strconv.FormatInt(file.Id, 10),
@ -307,7 +307,7 @@ func (d *Cloud189) uploadRequest(uri string, form map[string]string, resp interf
return data, nil 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() sessionKey, err := d.getSessionKey()
if err != nil { if err != nil {
return err return err
@ -336,6 +336,9 @@ func (d *Cloud189) newUpload(dstDir model.Obj, file model.FileStreamer, up drive
md5s := make([]string, 0) md5s := make([]string, 0)
md5Sum := md5.New() md5Sum := md5.New()
for i = 1; i <= count; i++ { for i = 1; i <= count; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
byteSize = file.GetSize() - finish byteSize = file.GetSize() - finish
if DEFAULT < byteSize { if DEFAULT < byteSize {
byteSize = DEFAULT byteSize = DEFAULT
@ -365,12 +368,15 @@ func (d *Cloud189) newUpload(dstDir model.Obj, file model.FileStreamer, up drive
log.Debugf("uploadData: %+v", uploadData) log.Debugf("uploadData: %+v", uploadData)
requestURL := uploadData.RequestURL requestURL := uploadData.RequestURL
uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&") uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&")
req, _ := 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 { for _, v := range uploadHeaders {
i := strings.Index(v, "=") i := strings.Index(v, "=")
req.Header.Set(v[0:i], v[i+1:]) req.Header.Set(v[0:i], v[i+1:])
} }
r, err := base.HttpClient.Do(req) r, err := base.HttpClient.Do(req)
log.Debugf("%+v %+v", r, r.Request.Header) log.Debugf("%+v %+v", r, r.Request.Header)
r.Body.Close() r.Body.Close()

View File

@ -13,7 +13,7 @@ import (
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
) )
type Yun189PC struct { type Cloud189PC struct {
model.Storage model.Storage
Addition Addition
@ -26,20 +26,15 @@ type Yun189PC struct {
tokenInfo *AppSessionResp tokenInfo *AppSessionResp
} }
func (y *Yun189PC) Config() driver.Config { func (y *Cloud189PC) Config() driver.Config {
return config return config
} }
func (y *Yun189PC) GetAddition() driver.Additional { func (y *Cloud189PC) GetAddition() driver.Additional {
return y.Addition return &y.Addition
} }
func (y *Yun189PC) Init(ctx context.Context, storage model.Storage) (err error) { func (y *Cloud189PC) Init(ctx context.Context) (err error) {
y.Storage = storage
if err = utils.Json.UnmarshalFromString(y.Storage.Addition, &y.Addition); err != nil {
return err
}
// 处理个人云和家庭云参数 // 处理个人云和家庭云参数
if y.isFamily() && y.RootFolderID == "-11" { if y.isFamily() && y.RootFolderID == "-11" {
y.RootFolderID = "" y.RootFolderID = ""
@ -78,15 +73,15 @@ func (y *Yun189PC) Init(ctx context.Context, storage model.Storage) (err error)
return return
} }
func (y *Yun189PC) Drop(ctx context.Context) error { func (y *Cloud189PC) Drop(ctx context.Context) error {
return nil 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()) 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 { var downloadUrl struct {
URL string `json:"fileDownloadUrl"` URL string `json:"fileDownloadUrl"`
} }
@ -145,7 +140,7 @@ func (y *Yun189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs
return like, nil 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 fullUrl := API_URL
if y.isFamily() { if y.isFamily() {
fullUrl += "/family/file" fullUrl += "/family/file"
@ -172,7 +167,7 @@ func (y *Yun189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName str
return err 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) { _, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
@ -196,7 +191,7 @@ func (y *Yun189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return err 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) queryParam := make(map[string]string)
fullUrl := API_URL fullUrl := API_URL
method := http.MethodPost method := http.MethodPost
@ -221,7 +216,7 @@ func (y *Yun189PC) Rename(ctx context.Context, srcObj model.Obj, newName string)
return err 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) { _, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
@ -246,7 +241,7 @@ func (y *Yun189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return err 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) { _, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
@ -270,7 +265,7 @@ func (y *Yun189PC) Remove(ctx context.Context, obj model.Obj) error {
return err return err
} }
func (y *Yun189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
if y.RapidUpload { if y.RapidUpload {
return y.FastUpload(ctx, dstDir, stream, up) return y.FastUpload(ctx, dstDir, stream, up)
} }

View File

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

View File

@ -47,7 +47,7 @@ const (
CHANNEL_ID = "web_cloud.189.cn" 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) request(url, method string, callback base.ReqCallback, params Params, resp interface{}) ([]byte, error) {
dateOfGmt := getHttpDateStr() dateOfGmt := getHttpDateStr()
sessionKey := y.tokenInfo.SessionKey sessionKey := y.tokenInfo.SessionKey
sessionSecret := y.tokenInfo.SessionSecret sessionSecret := y.tokenInfo.SessionSecret
@ -124,15 +124,15 @@ func (y *Yun189PC) request(url, method string, callback base.ReqCallback, params
} }
} }
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) 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) return y.request(url, http.MethodPost, callback, nil, resp)
} }
func (y *Yun189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, error) { func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, error) {
fullUrl := API_URL fullUrl := API_URL
if y.isFamily() { if y.isFamily() {
fullUrl += "/family/file" fullUrl += "/family/file"
@ -184,7 +184,7 @@ func (y *Yun189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, er
return res, nil 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 || !y.NoUseOcr {
if err = y.initLoginParam(); err != nil { if err = y.initLoginParam(); err != nil {
@ -264,7 +264,7 @@ func (y *Yun189PC) login() (err error) {
/* 初始化登陆需要的参数 /* 初始化登陆需要的参数
* 如果遇到验证码返回错误 * 如果遇到验证码返回错误
*/ */
func (y *Yun189PC) initLoginParam() error { func (y *Cloud189PC) initLoginParam() error {
// 清除cookie // 清除cookie
jar, _ := cookiejar.New(nil) jar, _ := cookiejar.New(nil)
y.client.SetCookieJar(jar) y.client.SetCookieJar(jar)
@ -335,7 +335,7 @@ func (y *Yun189PC) initLoginParam() error {
} }
// 刷新会话 // 刷新会话
func (y *Yun189PC) refreshSession() (err error) { func (y *Cloud189PC) refreshSession() (err error) {
var erron RespErr var erron RespErr
var userSessionResp UserSessionResp var userSessionResp UserSessionResp
_, err = y.client.R(). _, err = y.client.R().
@ -381,7 +381,7 @@ func (y *Yun189PC) refreshSession() (err error) {
} }
// 普通上传 // 普通上传
func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (err error) { func (y *Cloud189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (err error) {
const DEFAULT int64 = 10485760 const DEFAULT int64 = 10485760
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT))) var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
@ -418,10 +418,8 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
silceMd5Hexs := make([]string, 0, count) silceMd5Hexs := make([]string, 0, count)
byteData := bytes.NewBuffer(make([]byte, DEFAULT)) byteData := bytes.NewBuffer(make([]byte, DEFAULT))
for i := int64(1); i <= count; i++ { for i := int64(1); i <= count; i++ {
select { if utils.IsCanceled(ctx) {
case <-ctx.Done():
return ctx.Err() return ctx.Err()
default:
} }
// 读取块 // 读取块
@ -491,7 +489,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 // 需要获取完整文件md5,必须支持 io.Seek
tempFile, err := utils.CreateTempFile(file.GetReadCloser()) tempFile, err := utils.CreateTempFile(file.GetReadCloser())
if err != nil { if err != nil {
@ -511,10 +509,8 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
silceMd5Hexs := make([]string, 0, count) silceMd5Hexs := make([]string, 0, count)
silceMd5Base64s := make([]string, 0, count) silceMd5Base64s := make([]string, 0, count)
for i := 1; i <= count; i++ { for i := 1; i <= count; i++ {
select { if utils.IsCanceled(ctx) {
case <-ctx.Done():
return ctx.Err() return ctx.Err()
default:
} }
silceMd5.Reset() silceMd5.Reset()
@ -616,11 +612,11 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
return err return err
} }
func (y *Yun189PC) isFamily() bool { func (y *Cloud189PC) isFamily() bool {
return y.Type == "family" return y.Type == "family"
} }
func (y *Yun189PC) isLogin() bool { func (y *Cloud189PC) isLogin() bool {
if y.tokenInfo == nil { if y.tokenInfo == nil {
return false return false
} }
@ -629,7 +625,7 @@ func (y *Yun189PC) isLogin() bool {
} }
// 获取家庭云所有用户信息 // 获取家庭云所有用户信息
func (y *Yun189PC) getFamilyInfoList() ([]FamilyInfoResp, error) { func (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
var resp FamilyInfoListResp var resp FamilyInfoListResp
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp) _, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp)
if err != nil { if err != nil {
@ -639,7 +635,7 @@ func (y *Yun189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
} }
// 抽取家庭云ID // 抽取家庭云ID
func (y *Yun189PC) getFamilyID() (string, error) { func (y *Cloud189PC) getFamilyID() (string, error) {
infos, err := y.getFamilyInfoList() infos, err := y.getFamilyInfoList()
if err != nil { if err != nil {
return "", err return "", 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,15 @@ package alist_v3
import ( import (
"context" "context"
"io"
"path"
"strconv"
"strings"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "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/common"
"github.com/alist-org/alist/v3/server/handles"
) )
type AListV3 struct { type AListV3 struct {
@ -22,18 +23,14 @@ func (d *AListV3) Config() driver.Config {
} }
func (d *AListV3) GetAddition() driver.Additional { func (d *AListV3) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *AListV3) Init(ctx context.Context, storage model.Storage) error { func (d *AListV3) Init(ctx context.Context) error {
d.Storage = storage d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/")
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
// TODO login / refresh token // TODO login / refresh token
//op.MustSaveDriverStorage(d) //op.MustSaveDriverStorage(d)
return err return nil
} }
func (d *AListV3) Drop(ctx context.Context) error { func (d *AListV3) Drop(ctx context.Context) error {
@ -42,12 +39,12 @@ func (d *AListV3) Drop(ctx context.Context) error {
func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
url := d.Address + "/api/fs/list" url := d.Address + "/api/fs/list"
var resp common.Resp[handles.FsListResp] var resp common.Resp[FsListResp]
_, err := base.RestyClient.R(). _, err := base.RestyClient.R().
SetResult(&resp). SetResult(&resp).
SetHeader("Authorization", d.AccessToken). SetHeader("Authorization", d.AccessToken).
SetBody(handles.ListReq{ SetBody(ListReq{
PageReq: common.PageReq{ PageReq: model.PageReq{
Page: 1, Page: 1,
PerPage: 0, PerPage: 0,
}, },
@ -74,18 +71,13 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
return files, nil 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) { func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
url := d.Address + "/api/fs/get" url := d.Address + "/api/fs/get"
var resp common.Resp[handles.FsGetResp] var resp common.Resp[FsGetResp]
_, err := base.RestyClient.R(). _, err := base.RestyClient.R().
SetResult(&resp). SetResult(&resp).
SetHeader("Authorization", d.AccessToken). SetHeader("Authorization", d.AccessToken).
SetBody(handles.FsGetReq{ SetBody(FsGetReq{
Path: file.GetPath(), Path: file.GetPath(),
Password: d.Password, Password: d.Password,
}).Post(url) }).Post(url)
@ -98,27 +90,86 @@ 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 { func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotImplement url := d.Address + "/api/fs/mkdir"
var resp common.Resp[interface{}]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(MkdirOrLinkReq{
Path: path.Join(parentDir.GetPath(), dirName),
}).Post(url)
return checkResp(resp, err)
} }
func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement url := d.Address + "/api/fs/move"
var resp common.Resp[interface{}]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(MoveCopyReq{
SrcDir: srcObj.GetPath(),
DstDir: dstDir.GetPath(),
Names: []string{srcObj.GetName()},
}).Post(url)
return checkResp(resp, err)
} }
func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error { func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return errs.NotImplement url := d.Address + "/api/fs/rename"
var resp common.Resp[interface{}]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(RenameReq{
Path: srcObj.GetPath(),
Name: newName,
}).Post(url)
return checkResp(resp, err)
} }
func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement url := d.Address + "/api/fs/copy"
var resp common.Resp[interface{}]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(MoveCopyReq{
SrcDir: srcObj.GetPath(),
DstDir: dstDir.GetPath(),
Names: []string{srcObj.GetName()},
}).Post(url)
return checkResp(resp, err)
} }
func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error { func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotImplement url := d.Address + "/api/fs/remove"
var resp common.Resp[interface{}]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(RemoveReq{
Dir: obj.GetPath(),
Names: []string{obj.GetName()},
}).Post(url)
return checkResp(resp, err)
} }
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
return errs.NotImplement url := d.Address + "/api/fs/put"
var resp common.Resp[interface{}]
fileBytes, err := io.ReadAll(stream.GetReadCloser())
if err != nil {
return nil
}
_, err = base.RestyClient.R().SetContext(ctx).
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
SetHeader("Password", d.Password).
SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
SetBody(fileBytes).Put(url)
return checkResp(resp, err)
} }
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { //func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {

View File

@ -15,12 +15,11 @@ type Addition struct {
var config = driver.Config{ var config = driver.Config{
Name: "AList V3", Name: "AList V3",
LocalSort: true, LocalSort: true,
NoUpload: true,
DefaultRoot: "/", DefaultRoot: "/",
} }
func init() { func init() {
op.RegisterDriver(config, func() driver.Driver { op.RegisterDriver(func() driver.Driver {
return &AListV3{} return &AListV3{}
}) })
} }

View File

@ -1 +1,65 @@
package alist_v3 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"`
}

View File

@ -1 +1,17 @@
package alist_v3 package alist_v3
import (
"errors"
"github.com/alist-org/alist/v3/server/common"
)
func checkResp(resp common.Resp[interface{}], err error) error {
if err != nil {
return err
}
if resp.Message == "success" {
return nil
}
return errors.New(resp.Message)
}

View File

@ -38,18 +38,13 @@ func (d *AliDrive) Config() driver.Config {
} }
func (d *AliDrive) GetAddition() driver.Additional { func (d *AliDrive) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *AliDrive) Init(ctx context.Context, storage model.Storage) error { func (d *AliDrive) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
// TODO login / refresh token // TODO login / refresh token
//op.MustSaveDriverStorage(d) //op.MustSaveDriverStorage(d)
err = d.refreshToken() err := d.refreshToken()
if err != nil { if err != nil {
return err return err
} }
@ -86,11 +81,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) { func (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
data := base.Json{ data := base.Json{
"drive_id": d.DriveId, "drive_id": d.DriveId,
@ -258,10 +248,14 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
} }
for i, partInfo := range resp.PartInfoList { for i, partInfo := range resp.PartInfoList {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
req, err := http.NewRequest("PUT", partInfo.UploadUrl, io.LimitReader(file, DEFAULT)) req, err := http.NewRequest("PUT", partInfo.UploadUrl, io.LimitReader(file, DEFAULT))
if err != nil { if err != nil {
return err return err
} }
req = req.WithContext(ctx)
res, err := base.HttpClient.Do(req) res, err := base.HttpClient.Do(req)
if err != nil { if err != nil {
return err return err

View File

@ -18,10 +18,8 @@ var config = driver.Config{
DefaultRoot: "root", DefaultRoot: "root",
} }
func New() driver.Driver {
return &AliDrive{}
}
func init() { func init() {
op.RegisterDriver(config, New) op.RegisterDriver(func() driver.Driver {
return &AliDrive{}
})
} }

View File

@ -29,6 +29,9 @@ func (d *AliDrive) refreshToken() error {
if e.Code != "" { if e.Code != "" {
return fmt.Errorf("failed to refresh token: %s", e.Message) 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 d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
op.MustSaveDriverStorage(d) op.MustSaveDriverStorage(d)
return nil return nil
@ -62,6 +65,8 @@ func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp i
return d.request(url, method, callback, resp) return d.request(url, method, callback, resp)
} }
return nil, errors.New(e.Message), e return nil, errors.New(e.Message), e
} else if res.IsError() {
return nil, errors.New("bad status code " + res.Status()), e
} }
return res.Body(), nil, e return res.Body(), nil, e
} }

View File

@ -8,7 +8,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/cron" "github.com/alist-org/alist/v3/pkg/cron"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
@ -29,16 +28,11 @@ func (d *AliyundriveShare) Config() driver.Config {
} }
func (d *AliyundriveShare) GetAddition() driver.Additional { func (d *AliyundriveShare) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *AliyundriveShare) Init(ctx context.Context, storage model.Storage) error { func (d *AliyundriveShare) Init(ctx context.Context) error {
d.Storage = storage err := d.refreshToken()
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
err = d.refreshToken()
if err != nil { if err != nil {
return err return err
} }
@ -73,69 +67,42 @@ 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) { func (d *AliyundriveShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
data := base.Json{ data := base.Json{
"drive_id": d.DriveId, "drive_id": d.DriveId,
"file_id": file.GetID(), "file_id": file.GetID(),
"expire_sec": 14400, // // Only ten minutes lifetime
"expire_sec": 600,
"share_id": d.ShareId,
} }
var resp ShareLinkResp
var e ErrorResp var e ErrorResp
res, err := base.RestyClient.R(). _, err := base.RestyClient.R().
SetError(&e).SetBody(data). SetError(&e).SetBody(data).SetResult(&resp).
SetHeader("content-type", "application/json"). SetHeader("content-type", "application/json").
SetHeader("Authorization", "Bearer\t"+d.AccessToken). SetHeader("Authorization", "Bearer\t"+d.AccessToken).
Post("https://api.aliyundrive.com/v2/file/get_download_url") SetHeader("x-share-token", d.ShareToken).
Post("https://api.aliyundrive.com/v2/file/get_share_link_download_url")
if err != nil { if err != nil {
return nil, err return nil, err
} }
var u string var u string
if e.Code != "" { if e.Code != "" {
if e.Code == "AccessTokenInvalid" { if e.Code == "AccessTokenInvalid" || e.Code == "ShareLinkTokenInvalid" {
err = d.refreshToken() if e.Code == "AccessTokenInvalid" {
err = d.refreshToken()
} else {
err = d.getShareToken()
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
return d.Link(ctx, file, args) return d.Link(ctx, file, args)
} else if e.Code == "ForbiddenNoPermission.File" {
data = utils.MergeMap(data, base.Json{
// Only ten minutes valid
"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")
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 { } else {
return nil, errors.New(e.Code + ":" + e.Message) return nil, errors.New(e.Code + ": " + e.Message)
} }
} else { } else {
u = utils.Json.Get(res.Body(), "url").ToString() u = resp.DownloadUrl
} }
return &model.Link{ return &model.Link{
Header: http.Header{ Header: http.Header{
@ -145,34 +112,4 @@ func (d *AliyundriveShare) Link(ctx context.Context, file model.Obj, args model.
}, nil }, 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
}
var _ driver.Driver = (*AliyundriveShare)(nil) var _ driver.Driver = (*AliyundriveShare)(nil)

View File

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

View File

@ -6,11 +6,13 @@ import (
_ "github.com/alist-org/alist/v3/drivers/139" _ "github.com/alist-org/alist/v3/drivers/139"
_ "github.com/alist-org/alist/v3/drivers/189" _ "github.com/alist-org/alist/v3/drivers/189"
_ "github.com/alist-org/alist/v3/drivers/189pc" _ "github.com/alist-org/alist/v3/drivers/189pc"
_ "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/alist_v3"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive" _ "github.com/alist-org/alist/v3/drivers/aliyundrive"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_share" _ "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_netdisk"
_ "github.com/alist-org/alist/v3/drivers/baidu_photo" _ "github.com/alist-org/alist/v3/drivers/baidu_photo"
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
_ "github.com/alist-org/alist/v3/drivers/ftp" _ "github.com/alist-org/alist/v3/drivers/ftp"
_ "github.com/alist-org/alist/v3/drivers/google_drive" _ "github.com/alist-org/alist/v3/drivers/google_drive"
_ "github.com/alist-org/alist/v3/drivers/google_photo" _ "github.com/alist-org/alist/v3/drivers/google_photo"
@ -20,11 +22,14 @@ import (
_ "github.com/alist-org/alist/v3/drivers/mega" _ "github.com/alist-org/alist/v3/drivers/mega"
_ "github.com/alist-org/alist/v3/drivers/onedrive" _ "github.com/alist-org/alist/v3/drivers/onedrive"
_ "github.com/alist-org/alist/v3/drivers/pikpak" _ "github.com/alist-org/alist/v3/drivers/pikpak"
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
_ "github.com/alist-org/alist/v3/drivers/quark" _ "github.com/alist-org/alist/v3/drivers/quark"
_ "github.com/alist-org/alist/v3/drivers/s3" _ "github.com/alist-org/alist/v3/drivers/s3"
_ "github.com/alist-org/alist/v3/drivers/seafile"
_ "github.com/alist-org/alist/v3/drivers/sftp" _ "github.com/alist-org/alist/v3/drivers/sftp"
_ "github.com/alist-org/alist/v3/drivers/smb" _ "github.com/alist-org/alist/v3/drivers/smb"
_ "github.com/alist-org/alist/v3/drivers/teambition" _ "github.com/alist-org/alist/v3/drivers/teambition"
_ "github.com/alist-org/alist/v3/drivers/terabox"
_ "github.com/alist-org/alist/v3/drivers/thunder" _ "github.com/alist-org/alist/v3/drivers/thunder"
_ "github.com/alist-org/alist/v3/drivers/uss" _ "github.com/alist-org/alist/v3/drivers/uss"
_ "github.com/alist-org/alist/v3/drivers/virtual" _ "github.com/alist-org/alist/v3/drivers/virtual"

View File

@ -31,15 +31,10 @@ func (d *BaiduNetdisk) Config() driver.Config {
} }
func (d *BaiduNetdisk) GetAddition() driver.Additional { func (d *BaiduNetdisk) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *BaiduNetdisk) Init(ctx context.Context, storage model.Storage) error { func (d *BaiduNetdisk) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
return d.refreshToken() return d.refreshToken()
} }
@ -57,11 +52,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) { func (d *BaiduNetdisk) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if d.DownloadAPI == "crack" { if d.DownloadAPI == "crack" {
return d.linkCrack(file, args) return d.linkCrack(file, args)
@ -98,12 +88,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 { func (d *BaiduNetdisk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
dest, newname := stdpath.Split(dstDir.GetPath())
data := []base.Json{ data := []base.Json{
{ {
"path": srcObj.GetPath(), "path": srcObj.GetPath(),
"dest": dest, "dest": dstDir.GetPath(),
"newname": newname, "newname": srcObj.GetName(),
}, },
} }
_, err := d.manage("copy", data) _, err := d.manage("copy", data)
@ -175,7 +164,8 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
return err 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, ",")) 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", data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&block_list=%s&content-md5=%s&slice-md5=%s",
path, stream.GetSize(), path, stream.GetSize(),
@ -202,6 +192,9 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
} }
left = stream.GetSize() left = stream.GetSize()
for i, partseq := range precreateResp.BlockList { for i, partseq := range precreateResp.BlockList {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
byteSize := Default byteSize := Default
var byteData []byte var byteData []byte
if left < Default { if left < Default {
@ -217,7 +210,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" u := "https://d.pcs.baidu.com/rest/2.0/pcs/superfile2"
params["partseq"] = strconv.Itoa(partseq) 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 { if err != nil {
return err return err
} }
@ -226,7 +223,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
up(i * 100 / len(precreateResp.BlockList)) 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 return err
} }

View File

@ -20,10 +20,8 @@ var config = driver.Config{
DefaultRoot: "/", DefaultRoot: "/",
} }
func New() driver.Driver {
return &BaiduNetdisk{}
}
func init() { func init() {
op.RegisterDriver(config, New) op.RegisterDriver(func() driver.Driver {
return &BaiduNetdisk{}
})
} }

View File

@ -187,7 +187,7 @@ func (d *BaiduNetdisk) create(path string, size int64, isdir int, uploadid, bloc
params := map[string]string{ params := map[string]string{
"method": "create", "method": "create",
} }
data := fmt.Sprintf("path=%s&size=%d&isdir=%d", path, size, isdir) data := fmt.Sprintf("path=%s&size=%d&isdir=%d", encodeURIComponent(path), size, isdir)
if uploadid != "" { if uploadid != "" {
data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list) data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list)
} }

View File

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

@ -8,10 +8,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/pkg/utils"
) )
//Tid生成 // Tid生成
func getTid() string { func getTid() string {
return fmt.Sprintf("3%d%.0f", time.Now().Unix(), math.Floor(9000000*rand.Float64()+1000000)) return fmt.Sprintf("3%d%.0f", time.Now().Unix(), math.Floor(9000000*rand.Float64()+1000000))
} }
@ -26,82 +26,52 @@ func toTime(t int64) *time.Time {
return &tm return &tm
} }
func fsidsFormat(ids ...string) string { func fsidsFormatNotUk(ids ...int64) string {
var buf []string buf := utils.MustSliceConvert(ids, func(id int64) string {
for _, id := range ids { return fmt.Sprintf(`{"fsid":%d}`, id)
e := splitID(id) })
buf = append(buf, fmt.Sprintf(`{"fsid":%s,"uk":%s}`, e[0], e[3]))
}
return fmt.Sprintf("[%s]", strings.Join(buf, ",")) 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 { func getFileName(path string) string {
return path[strings.LastIndex(path, "/")+1:] 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 { func MustString(str string, err error) string {
return str 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"` ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
} }
func (a Addition) GetRootId() string {
return a.AlbumID
}
var config = driver.Config{ var config = driver.Config{
Name: "BaiduPhoto", Name: "BaiduPhoto",
LocalSort: true, LocalSort: true,
} }
func init() { func init() {
op.RegisterDriver(config, func() driver.Driver { op.RegisterDriver(func() driver.Driver {
return &BaiduPhoto{} return &BaiduPhoto{}
}) })
} }

View File

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

View File

@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
@ -17,6 +16,7 @@ import (
const ( const (
API_URL = "https://photo.baidu.com/youai" API_URL = "https://photo.baidu.com/youai"
USER_API_URL = API_URL + "/user/v1"
ALBUM_API_URL = API_URL + "/album/v1" ALBUM_API_URL = API_URL + "/album/v1"
FILE_API_URL_V1 = API_URL + "/file/v1" FILE_API_URL_V1 = API_URL + "/file/v1"
FILE_API_URL_V2 = API_URL + "/file/v2" FILE_API_URL_V2 = API_URL + "/file/v2"
@ -26,9 +26,9 @@ var (
ErrNotSupportName = errors.New("only chinese and english, numbers and underscores are supported, and the length is no more than 20") 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(). req := base.RestyClient.R().
SetQueryParam("access_token", p.AccessToken) SetQueryParam("access_token", d.AccessToken)
if callback != nil { if callback != nil {
callback(req) callback(req)
} }
@ -49,7 +49,7 @@ func (p *BaiduPhoto) Request(furl string, method string, callback base.ReqCallba
case 50820: case 50820:
return nil, fmt.Errorf("no shared albums found") return nil, fmt.Errorf("no shared albums found")
case -6: case -6:
if err = p.refreshToken(); err != nil { if err = d.refreshToken(); err != nil {
return nil, err return nil, err
} }
default: default:
@ -58,15 +58,15 @@ func (p *BaiduPhoto) Request(furl string, method string, callback base.ReqCallba
return res.Body(), nil return res.Body(), nil
} }
func (p *BaiduPhoto) refreshToken() error { func (d *BaiduPhoto) refreshToken() error {
u := "https://openapi.baidu.com/oauth/2.0/token" u := "https://openapi.baidu.com/oauth/2.0/token"
var resp base.TokenResp var resp base.TokenResp
var e TokenErrResp var e TokenErrResp
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{ _, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
"grant_type": "refresh_token", "grant_type": "refresh_token",
"refresh_token": p.RefreshToken, "refresh_token": d.RefreshToken,
"client_id": p.ClientID, "client_id": d.ClientID,
"client_secret": p.ClientSecret, "client_secret": d.ClientSecret,
}).Get(u) }).Get(u)
if err != nil { if err != nil {
return err return err
@ -77,25 +77,25 @@ func (p *BaiduPhoto) refreshToken() error {
if resp.RefreshToken == "" { if resp.RefreshToken == "" {
return errs.EmptyToken return errs.EmptyToken
} }
p.AccessToken, p.RefreshToken = resp.AccessToken, resp.RefreshToken d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
op.MustSaveDriverStorage(p) op.MustSaveDriverStorage(d)
return nil return nil
} }
func (p *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) { func (d *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return p.Request(furl, http.MethodGet, callback, resp) return d.Request(furl, http.MethodGet, callback, resp)
} }
func (p *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) { func (d *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return p.Request(furl, http.MethodPost, callback, resp) 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 var cursor string
for { for {
var resp FileListResp 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.SetContext(ctx)
r.SetQueryParams(map[string]string{ r.SetQueryParams(map[string]string{
"need_thumbnail": "1", "need_thumbnail": "1",
@ -116,22 +116,22 @@ func (p *BaiduPhoto) GetAllFile(ctx context.Context) (files []File, err error) {
} }
// 删除根文件 // 删除根文件
func (p *BaiduPhoto) DeleteFile(ctx context.Context, fileIDs ...string) error { func (d *BaiduPhoto) DeleteFile(ctx context.Context, file *File) error {
_, err := p.Get(FILE_API_URL_V1+"/delete", func(req *resty.Request) { _, err := d.Get(FILE_API_URL_V1+"/delete", func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
req.SetQueryParams(map[string]string{ req.SetQueryParams(map[string]string{
"fsid_list": fmt.Sprintf("[%s]", strings.Join(fileIDs, ",")), "fsid_list": fmt.Sprintf("[%d]", file.Fsid),
}) })
}, nil) }, nil)
return err 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 var cursor string
for { for {
var resp AlbumListResp 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.SetContext(ctx)
r.SetQueryParams(map[string]string{ r.SetQueryParams(map[string]string{
"need_amount": "1", "need_amount": "1",
@ -156,14 +156,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 var cursor string
for { for {
var resp AlbumFileListResp 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.SetContext(ctx)
r.SetQueryParams(map[string]string{ r.SetQueryParams(map[string]string{
"album_id": albumID, "album_id": album.AlbumID,
"need_amount": "1", "need_amount": "1",
"limit": "1000", "limit": "1000",
"passwd": passwd, "passwd": passwd,
@ -187,45 +187,52 @@ func (p *BaiduPhoto) GetAllAlbumFile(ctx context.Context, albumID, passwd string
} }
// 创建相册 // 创建相册
func (p *BaiduPhoto) CreateAlbum(ctx context.Context, name string) error { func (d *BaiduPhoto) CreateAlbum(ctx context.Context, name string) (*Album, error) {
if !checkName(name) { if !checkName(name) {
return ErrNotSupportName return nil, ErrNotSupportName
} }
_, err := p.Post(ALBUM_API_URL+"/create", func(r *resty.Request) { var resp JoinOrCreateAlbumResp
r.SetContext(ctx) _, err := d.Post(ALBUM_API_URL+"/create", func(r *resty.Request) {
r.SetContext(ctx).SetResult(&resp)
r.SetQueryParams(map[string]string{ r.SetQueryParams(map[string]string{
"title": name, "title": name,
"tid": getTid(), "tid": getTid(),
"source": "0", "source": "0",
}) })
}, nil) }, 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 { func (d *BaiduPhoto) SetAlbumName(ctx context.Context, album *Album, name string) (*Album, error) {
if !checkName(name) { if !checkName(name) {
return ErrNotSupportName return nil, ErrNotSupportName
} }
_, err := p.Post(ALBUM_API_URL+"/settitle", func(r *resty.Request) { _, err := d.Post(ALBUM_API_URL+"/settitle", func(r *resty.Request) {
r.SetContext(ctx) r.SetContext(ctx)
r.SetFormData(map[string]string{ r.SetFormData(map[string]string{
"title": name, "title": name,
"album_id": albumID, "album_id": album.AlbumID,
"tid": tID, "tid": fmt.Sprint(album.Tid),
}) })
}, nil) }, 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 { func (d *BaiduPhoto) DeleteAlbum(ctx context.Context, album *Album) error {
_, err := p.Post(ALBUM_API_URL+"/delete", func(r *resty.Request) { _, err := d.Post(ALBUM_API_URL+"/delete", func(r *resty.Request) {
r.SetContext(ctx) r.SetContext(ctx)
r.SetFormData(map[string]string{ r.SetFormData(map[string]string{
"album_id": albumID, "album_id": album.AlbumID,
"tid": tID, "tid": fmt.Sprint(album.Tid),
"delete_origin_image": "0", // 是否删除原图 0 不删除 1 删除 "delete_origin_image": "0", // 是否删除原图 0 不删除 1 删除
}) })
}, nil) }, nil)
@ -233,13 +240,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 { func (d *BaiduPhoto) DeleteAlbumFile(ctx context.Context, file *AlbumFile) error {
_, err := p.Post(ALBUM_API_URL+"/delfile", func(r *resty.Request) { _, err := d.Post(ALBUM_API_URL+"/delfile", func(r *resty.Request) {
r.SetContext(ctx) r.SetContext(ctx)
r.SetFormData(map[string]string{ r.SetFormData(map[string]string{
"album_id": albumID, "album_id": fmt.Sprint(file.AlbumID),
"tid": tID, "tid": fmt.Sprint(file.Tid),
"list": fsidsFormat(fileIDs...), "list": fmt.Sprintf(`[{"fsid":%d,"uk":%d}]`, file.Fsid, file.Uk),
"del_origin": "0", // 是否删除原图 0 不删除 1 删除 "del_origin": "0", // 是否删除原图 0 不删除 1 删除
}) })
}, nil) }, nil)
@ -247,41 +254,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 { func (d *BaiduPhoto) AddAlbumFile(ctx context.Context, album *Album, file *File) (*AlbumFile, error) {
_, err := p.Get(ALBUM_API_URL+"/addfile", func(r *resty.Request) { _, err := d.Get(ALBUM_API_URL+"/addfile", func(r *resty.Request) {
r.SetContext(ctx) r.SetContext(ctx)
r.SetQueryParams(map[string]string{ r.SetQueryParams(map[string]string{
"album_id": albumID, "album_id": fmt.Sprint(album.AlbumID),
"tid": tID, "tid": fmt.Sprint(album.Tid),
"list": fsidsFormatNotUk(fileIDs...), "list": fsidsFormatNotUk(file.Fsid),
}) })
}, nil) }, 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 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.SetContext(ctx)
r.SetFormData(map[string]string{ r.SetFormData(map[string]string{
"album_id": albumID, "album_id": file.AlbumID,
"tid": tID, "tid": fmt.Sprint(file.Tid),
"uk": uk, "uk": fmt.Sprint(file.Uk),
"list": fsidsFormatNotUk(fileID...), "list": fsidsFormatNotUk(file.Fsid),
}) })
r.SetResult(&resp) r.SetResult(&resp)
}, nil) }, nil)
if err != nil { if err != nil {
return nil, err 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 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.SetContext(ctx)
req.SetQueryParams(map[string]string{ req.SetQueryParams(map[string]string{
"pcode": code, "pcode": code,
@ -289,18 +299,37 @@ func (p *BaiduPhoto) JoinAlbum(ctx context.Context, code string) error {
}) })
}, &resp) }, &resp)
if err != nil { 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.SetContext(ctx)
req.SetQueryParams(map[string]string{ req.SetQueryParams(map[string]string{
"invite_code": resp.Pdata.InviteCode, "invite_code": resp.Pdata.InviteCode,
}) })
}, nil) }, &resp2)
return err 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{ headers := map[string]string{
"User-Agent": base.UserAgent, "User-Agent": base.UserAgent,
} }
@ -311,16 +340,15 @@ func (d *BaiduPhoto) linkAlbum(ctx context.Context, file model.Obj, args model.L
headers["X-Forwarded-For"] = args.IP headers["X-Forwarded-For"] = args.IP
} }
e := splitID(file.GetID())
res, err := base.NoRedirectClient.R(). res, err := base.NoRedirectClient.R().
SetContext(ctx). SetContext(ctx).
SetHeaders(headers). SetHeaders(headers).
SetQueryParams(map[string]string{ SetQueryParams(map[string]string{
"access_token": d.AccessToken, "access_token": d.AccessToken,
"fsid": e[0], "fsid": fmt.Sprint(file.Fsid),
"album_id": e[1], "album_id": file.AlbumID,
"tid": e[2], "tid": fmt.Sprint(file.Tid),
"uk": e[3], "uk": fmt.Sprint(file.Uk),
}). }).
Head(ALBUM_API_URL + "/download") Head(ALBUM_API_URL + "/download")
@ -328,19 +356,17 @@ func (d *BaiduPhoto) linkAlbum(ctx context.Context, file model.Obj, args model.L
return nil, err return nil, err
} }
//exp := 8 * time.Hour
link := &model.Link{ link := &model.Link{
URL: res.Header().Get("location"), URL: res.Header().Get("location"),
Header: http.Header{ Header: http.Header{
"User-Agent": []string{headers["User-Agent"]}, "User-Agent": []string{headers["User-Agent"]},
"Referer": []string{"https://photo.baidu.com/"}, "Referer": []string{"https://photo.baidu.com/"},
}, },
//Expiration: &exp,
} }
return link, nil 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{ headers := map[string]string{
"User-Agent": base.UserAgent, "User-Agent": base.UserAgent,
} }
@ -358,21 +384,31 @@ func (d *BaiduPhoto) linkFile(ctx context.Context, file model.Obj, args model.Li
r.SetContext(ctx) r.SetContext(ctx)
r.SetHeaders(headers) r.SetHeaders(headers)
r.SetQueryParams(map[string]string{ r.SetQueryParams(map[string]string{
"fsid": splitID(file.GetID())[0], "fsid": fmt.Sprint(file.Fsid),
}) })
}, &downloadUrl) }, &downloadUrl)
if err != nil { if err != nil {
return nil, err return nil, err
} }
//exp := 8 * time.Hour
link := &model.Link{ link := &model.Link{
URL: downloadUrl.Dlink, URL: downloadUrl.Dlink,
Header: http.Header{ Header: http.Header{
"User-Agent": []string{headers["User-Agent"]}, "User-Agent": []string{headers["User-Agent"]},
"Referer": []string{"https://photo.baidu.com/"}, "Referer": []string{"https://photo.baidu.com/"},
}, },
//Expiration: &exp,
} }
return link, nil 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

@ -11,7 +11,7 @@ var NoRedirectClient *resty.Client
var RestyClient = NewRestyClient() var RestyClient = NewRestyClient()
var HttpClient = &http.Client{} var 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 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 init() {
NoRedirectClient = resty.New().SetRedirectPolicy( NoRedirectClient = resty.New().SetRedirectPolicy(

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

@ -0,0 +1,184 @@
package cloudreve
import (
"context"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
"io"
"net/http"
"strconv"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
)
type Cloudreve struct {
model.Storage
Addition
Cookie string
}
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 {
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)

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

@ -0,0 +1,26 @@
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" required:"true"`
Password string `json:"password" required:"true"`
}
var config = driver.Config{
Name: "Cloudreve",
DefaultRoot: "/",
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Cloudreve{}
})
}

View File

@ -0,0 +1,54 @@
package cloudreve
import (
"github.com/alist-org/alist/v3/internal/model"
"time"
)
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",
}
}

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

@ -0,0 +1,96 @@
package cloudreve
import (
"errors"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/cookie"
"github.com/go-resty/resty/v2"
json "github.com/json-iterator/go"
"net/http"
)
// 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 {
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 {
return d.request(http.MethodPost, loginPath, func(req *resty.Request) {
req.SetBody(base.Json{
"username": d.Addition.Username,
"Password": d.Addition.Password,
"captchaCode": "",
})
}, nil)
}
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
}

View File

@ -7,7 +7,6 @@ import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/jlaffaye/ftp" "github.com/jlaffaye/ftp"
) )
@ -22,15 +21,10 @@ func (d *FTP) Config() driver.Config {
} }
func (d *FTP) GetAddition() driver.Additional { func (d *FTP) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *FTP) Init(ctx context.Context, storage model.Storage) error { func (d *FTP) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
return d.login() return d.login()
} }
@ -66,11 +60,6 @@ func (d *FTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]m
return res, nil return res, nil
} }
//func (d *FTP) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *FTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *FTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if err := d.login(); err != nil { if err := d.login(); err != nil {
return nil, err return nil, err
@ -124,6 +113,7 @@ func (d *FTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStream
if err := d.login(); err != nil { if err := d.login(); err != nil {
return err return err
} }
// TODO: support cancel
return d.conn.Stor(stdpath.Join(dstDir.GetPath(), stream.GetName()), stream) return d.conn.Stor(stdpath.Join(dstDir.GetPath(), stream.GetName()), stream)
} }

View File

@ -19,10 +19,8 @@ var config = driver.Config{
DefaultRoot: "/", DefaultRoot: "/",
} }
func New() driver.Driver {
return &FTP{}
}
func init() { func init() {
op.RegisterDriver(config, New) op.RegisterDriver(func() driver.Driver {
return &FTP{}
})
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
@ -24,14 +25,12 @@ func (d *GoogleDrive) Config() driver.Config {
} }
func (d *GoogleDrive) GetAddition() driver.Additional { func (d *GoogleDrive) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *GoogleDrive) Init(ctx context.Context, storage model.Storage) error { func (d *GoogleDrive) Init(ctx context.Context) error {
d.Storage = storage if d.ChunkSize == 0 {
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition) d.ChunkSize = 5
if err != nil {
return err
} }
return d.refreshToken() return d.refreshToken()
} }
@ -50,13 +49,12 @@ func (d *GoogleDrive) List(ctx context.Context, dir model.Obj, args model.ListAr
}) })
} }
//func (d *GoogleDrive) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *GoogleDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *GoogleDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
url := fmt.Sprintf("https://www.googleapis.com/drive/v3/files/%s?includeItemsFromAllDrives=true&supportsAllDrives=true", file.GetID()) url := fmt.Sprintf("https://www.googleapis.com/drive/v3/files/%s?includeItemsFromAllDrives=true&supportsAllDrives=true", file.GetID())
_, err := d.request(url, http.MethodGet, nil, nil)
if err != nil {
return nil, err
}
link := model.Link{ link := model.Link{
URL: url + "&alt=media", URL: url + "&alt=media",
Header: http.Header{ Header: http.Header{
@ -112,15 +110,36 @@ func (d *GoogleDrive) Remove(ctx context.Context, obj model.Obj) error {
} }
func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
data := base.Json{ obj := stream.GetOld()
"name": stream.GetName(), var (
"parents": []string{dstDir.GetID()}, e Error
url string
data base.Json
res *resty.Response
err error
)
if obj != nil {
url = fmt.Sprintf("https://www.googleapis.com/upload/drive/v3/files/%s?uploadType=resumable&supportsAllDrives=true", obj.GetID())
data = base.Json{}
} else {
data = base.Json{
"name": stream.GetName(),
"parents": []string{dstDir.GetID()},
}
url = "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true"
}
req := base.NoRedirectClient.R().
SetHeaders(map[string]string{
"Authorization": "Bearer " + d.AccessToken,
"X-Upload-Content-Type": stream.GetMimetype(),
"X-Upload-Content-Length": strconv.FormatInt(stream.GetSize(), 10),
}).
SetError(&e).SetBody(data).SetContext(ctx)
if obj != nil {
res, err = req.Patch(url)
} else {
res, err = req.Post(url)
} }
var e Error
url := "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true"
res, err := base.NoRedirectClient.R().SetHeader("Authorization", "Bearer "+d.AccessToken).
SetError(&e).SetBody(data).
Post(url)
if err != nil { if err != nil {
return err return err
} }
@ -135,9 +154,13 @@ func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors) return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
} }
putUrl := res.Header().Get("location") putUrl := res.Header().Get("location")
_, err = d.request(putUrl, http.MethodPut, func(req *resty.Request) { if stream.GetSize() < d.ChunkSize*1024*1024 {
req.SetBody(stream.GetReadCloser()) _, err = d.request(putUrl, http.MethodPut, func(req *resty.Request) {
}, nil) req.SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).SetBody(stream.GetReadCloser())
}, nil)
} else {
err = d.chunkUpload(ctx, stream, putUrl)
}
return err return err
} }

View File

@ -12,6 +12,7 @@ type Addition struct {
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"` OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"`
ClientID string `json:"client_id" required:"true" default:"202264815644.apps.googleusercontent.com"` ClientID string `json:"client_id" required:"true" default:"202264815644.apps.googleusercontent.com"`
ClientSecret string `json:"client_secret" required:"true" default:"X4Z3ca8xfWDb1Voo-F9a7ZxJ"` ClientSecret string `json:"client_secret" required:"true" default:"X4Z3ca8xfWDb1Voo-F9a7ZxJ"`
ChunkSize int64 `json:"chunk_size" type:"number" default:"5" help:"chunk size while uploading (unit: MB)"`
} }
var config = driver.Config{ var config = driver.Config{
@ -20,10 +21,8 @@ var config = driver.Config{
DefaultRoot: "root", DefaultRoot: "root",
} }
func New() driver.Driver {
return &GoogleDrive{}
}
func init() { func init() {
op.RegisterDriver(config, New) op.RegisterDriver(func() driver.Driver {
return &GoogleDrive{}
})
} }

View File

@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
log "github.com/sirupsen/logrus"
) )
type TokenError struct { type TokenError struct {
@ -18,17 +19,22 @@ type Files struct {
} }
type File struct { type File struct {
Id string `json:"id"` Id string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
MimeType string `json:"mimeType"` MimeType string `json:"mimeType"`
ModifiedTime time.Time `json:"modifiedTime"` ModifiedTime time.Time `json:"modifiedTime"`
Size string `json:"size"` Size string `json:"size"`
ThumbnailLink string `json:"thumbnailLink"` ThumbnailLink string `json:"thumbnailLink"`
ShortcutDetails struct {
TargetId string `json:"targetId"`
TargetMimeType string `json:"targetMimeType"`
} `json:"shortcutDetails"`
} }
func fileToObj(f File) *model.ObjThumb { func fileToObj(f File) *model.ObjThumb {
log.Debugf("google file: %+v", f)
size, _ := strconv.ParseInt(f.Size, 10, 64) size, _ := strconv.ParseInt(f.Size, 10, 64)
return &model.ObjThumb{ obj := &model.ObjThumb{
Object: model.Object{ Object: model.Object{
ID: f.Id, ID: f.Id,
Name: f.Name, Name: f.Name,
@ -38,6 +44,11 @@ func fileToObj(f File) *model.ObjThumb {
}, },
Thumbnail: model.Thumbnail{}, Thumbnail: model.Thumbnail{},
} }
if f.MimeType == "application/vnd.google-apps.shortcut" {
obj.ID = f.ShortcutDetails.TargetId
obj.IsFolder = f.ShortcutDetails.TargetMimeType == "application/vnd.google-apps.folder"
}
return obj
} }
type Error struct { type Error struct {

View File

@ -1,10 +1,15 @@
package google_drive package google_drive
import ( import (
"context"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strconv"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -77,7 +82,7 @@ func (d *GoogleDrive) getFiles(id string) ([]File, error) {
} }
query := map[string]string{ query := map[string]string{
"orderBy": orderBy, "orderBy": orderBy,
"fields": "files(id,name,mimeType,size,modifiedTime,thumbnailLink),nextPageToken", "fields": "files(id,name,mimeType,size,modifiedTime,thumbnailLink,shortcutDetails),nextPageToken",
"pageSize": "1000", "pageSize": "1000",
"q": fmt.Sprintf("'%s' in parents and trashed = false", id), "q": fmt.Sprintf("'%s' in parents and trashed = false", id),
//"includeItemsFromAllDrives": "true", //"includeItemsFromAllDrives": "true",
@ -95,3 +100,28 @@ func (d *GoogleDrive) getFiles(id string) ([]File, error) {
} }
return res, nil return res, nil
} }
func (d *GoogleDrive) chunkUpload(ctx context.Context, stream model.FileStreamer, url string) error {
var defaultChunkSize = d.ChunkSize * 1024 * 1024
var finish int64 = 0
for finish < stream.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
chunkSize := stream.GetSize() - finish
if chunkSize > defaultChunkSize {
chunkSize = defaultChunkSize
}
_, err := d.request(url, http.MethodPut, func(req *resty.Request) {
req.SetHeaders(map[string]string{
"Content-Length": strconv.FormatInt(chunkSize, 10),
"Content-Range": fmt.Sprintf("bytes %d-%d/%d", finish, finish+chunkSize-1, stream.GetSize()),
}).SetBody(io.LimitReader(stream.GetReadCloser(), chunkSize)).SetContext(ctx)
}, nil)
if err != nil {
return err
}
finish += chunkSize
}
return nil
}

View File

@ -26,15 +26,10 @@ func (d *GooglePhoto) Config() driver.Config {
} }
func (d *GooglePhoto) GetAddition() driver.Additional { func (d *GooglePhoto) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *GooglePhoto) Init(ctx context.Context, storage model.Storage) error { func (d *GooglePhoto) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
return d.refreshToken() return d.refreshToken()
} }
@ -52,11 +47,6 @@ func (d *GooglePhoto) List(ctx context.Context, dir model.Obj, args model.ListAr
}) })
} }
//func (d *GooglePhoto) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *GooglePhoto) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *GooglePhoto) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
f, err := d.getMedia(file.GetID()) f, err := d.getMedia(file.GetID())
if err != nil { if err != nil {
@ -134,7 +124,7 @@ func (d *GooglePhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
} }
resp, err := d.request(postUrl, http.MethodPost, func(req *resty.Request) { resp, err := d.request(postUrl, http.MethodPost, func(req *resty.Request) {
req.SetBody(stream.GetReadCloser()) req.SetBody(stream.GetReadCloser()).SetContext(ctx)
}, nil, postHeaders) }, nil, postHeaders)
if err != nil { if err != nil {

View File

@ -21,10 +21,8 @@ var config = driver.Config{
LocalSort: true, LocalSort: true,
} }
func New() driver.Driver {
return &GooglePhoto{}
}
func init() { func init() {
op.RegisterDriver(config, New) op.RegisterDriver(func() driver.Driver {
return &GooglePhoto{}
})
} }

View File

@ -2,7 +2,9 @@ package lanzou
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"regexp"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
@ -18,6 +20,7 @@ var upClient = base.NewRestyClient().SetTimeout(120 * time.Second)
type LanZou struct { type LanZou struct {
Addition Addition
model.Storage model.Storage
uid string
} }
func (d *LanZou) Config() driver.Config { func (d *LanZou) Config() driver.Config {
@ -25,63 +28,100 @@ func (d *LanZou) Config() driver.Config {
} }
func (d *LanZou) GetAddition() driver.Additional { func (d *LanZou) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *LanZou) Init(ctx context.Context, storage model.Storage) error { func (d *LanZou) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
if d.IsCookie() { if d.IsCookie() {
if d.RootFolderID == "" { if d.RootFolderID == "" {
d.RootFolderID = "-1" d.RootFolderID = "-1"
} }
ylogin := regexp.MustCompile("ylogin=(.*?);").FindStringSubmatch(d.Cookie)
if len(ylogin) < 2 {
return fmt.Errorf("cookie does not contain ylogin")
}
d.uid = ylogin[1]
} }
return nil return nil
} }
func (d *LanZou) Drop(ctx context.Context) error { func (d *LanZou) Drop(ctx context.Context) error {
d.uid = ""
return nil return nil
} }
// 获取的大小和时间不准确 // 获取的大小和时间不准确
func (d *LanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { func (d *LanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if d.IsCookie() { if d.IsCookie() {
return d.GetFiles(ctx, dir.GetID()) return d.GetAllFiles(dir.GetID())
} else { } else {
return d.GetFileOrFolderByShareUrl(ctx, dir.GetID(), d.SharePassword) return d.GetFileOrFolderByShareUrl(dir.GetID(), d.SharePassword)
} }
} }
func (d *LanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *LanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
downID := file.GetID() var (
pwd := d.SharePassword err error
if d.IsCookie() { dfile *FileOrFolderByShareUrl
share, err := d.getFileShareUrlByID(ctx, file.GetID()) )
switch file := file.(type) {
case *FileOrFolder:
// 先获取分享链接
sfile := file.GetShareInfo()
if sfile == nil {
sfile, err = d.getFileShareUrlByID(file.GetID())
if err != nil {
return nil, err
}
file.SetShareInfo(sfile)
}
// 然后获取下载链接
dfile, err = d.GetFilesByShareUrl(sfile.FID, sfile.Pwd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
downID = share.FID // 修复文件大小
pwd = share.Pwd if d.RepairFileInfo && !file.repairFlag {
size, time := d.getFileRealInfo(dfile.Url)
if size != nil {
file.size = size
file.repairFlag = true
}
if file.time != nil {
file.time = time
}
}
case *FileOrFolderByShareUrl:
dfile, err = d.GetFilesByShareUrl(file.GetID(), file.Pwd)
if err != nil {
return nil, err
}
// 修复文件大小
if d.RepairFileInfo && !file.repairFlag {
size, time := d.getFileRealInfo(dfile.Url)
if size != nil {
file.size = size
file.repairFlag = true
}
if file.time != nil {
file.time = time
}
}
} }
fileInfo, err := d.getFilesByShareUrl(ctx, downID, pwd, nil) exp := GetExpirationTime(dfile.Url)
if err != nil {
return nil, err
}
return &model.Link{ return &model.Link{
URL: fileInfo.Url, URL: dfile.Url,
Header: http.Header{ Header: http.Header{
"User-Agent": []string{base.UserAgent}, "User-Agent": []string{base.UserAgent},
}, },
Expiration: &exp,
}, nil }, nil
} }
func (d *LanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { func (d *LanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
if d.IsCookie() { if d.IsCookie() {
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) { data, err := d.doupload(func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
"task": "2", "task": "2",
@ -90,15 +130,21 @@ func (d *LanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
"folder_description": "", "folder_description": "",
}) })
}, nil) }, nil)
return err if err != nil {
return nil, err
}
return &FileOrFolder{
Name: dirName,
FolID: utils.Json.Get(data, "text").ToString(),
}, nil
} }
return errs.NotImplement return nil, errs.NotImplement
} }
func (d *LanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *LanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
if d.IsCookie() { if d.IsCookie() {
if !srcObj.IsDir() { if !srcObj.IsDir() {
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) { _, err := d.doupload(func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
"task": "20", "task": "20",
@ -106,16 +152,19 @@ func (d *LanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
"file_id": srcObj.GetID(), "file_id": srcObj.GetID(),
}) })
}, nil) }, nil)
return err if err != nil {
return nil, err
}
return srcObj, nil
} }
} }
return errs.NotImplement return nil, errs.NotImplement
} }
func (d *LanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) error { func (d *LanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
if d.IsCookie() { if d.IsCookie() {
if !srcObj.IsDir() { if !srcObj.IsDir() {
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) { _, err := d.doupload(func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
"task": "46", "task": "46",
@ -124,19 +173,19 @@ func (d *LanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) e
"type": "2", "type": "2",
}) })
}, nil) }, nil)
return err if err != nil {
return nil, err
}
srcObj.(*FileOrFolder).NameAll = newName
return srcObj, nil
} }
} }
return errs.NotImplement return nil, errs.NotImplement
}
func (d *LanZou) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
} }
func (d *LanZou) Remove(ctx context.Context, obj model.Obj) error { func (d *LanZou) Remove(ctx context.Context, obj model.Obj) error {
if d.IsCookie() { if d.IsCookie() {
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) { _, err := d.doupload(func(req *resty.Request) {
req.SetContext(ctx) req.SetContext(ctx)
if obj.IsDir() { if obj.IsDir() {
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
@ -155,17 +204,23 @@ func (d *LanZou) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotImplement return errs.NotImplement
} }
func (d *LanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *LanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
if d.IsCookie() { if d.IsCookie() {
var resp RespText[[]FileOrFolder]
_, err := d._post(d.BaseUrl+"/fileup.php", func(req *resty.Request) { _, err := d._post(d.BaseUrl+"/fileup.php", func(req *resty.Request) {
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
"task": "1", "task": "1",
"vie": "2",
"ve": "2",
"id": "WU_FILE_0", "id": "WU_FILE_0",
"name": stream.GetName(), "name": stream.GetName(),
"folder_id": dstDir.GetID(), "folder_id": dstDir.GetID(),
}).SetFileReader("upload_file", stream.GetName(), stream) }).SetFileReader("upload_file", stream.GetName(), stream).SetContext(ctx)
}, nil, true) }, &resp, true)
return err if err != nil {
return nil, err
}
return &resp.Text[0], nil
} }
return errs.NotImplement return nil, errs.NotImplement
} }

View File

@ -8,12 +8,16 @@ import (
"strings" "strings"
"time" "time"
"unicode" "unicode"
log "github.com/sirupsen/logrus"
) )
const DAY time.Duration = 84600000000000 const DAY time.Duration = 84600000000000
// 解析时间
var timeSplitReg = regexp.MustCompile("([0-9.]*)\\s*([\u4e00-\u9fa5]+)") var timeSplitReg = regexp.MustCompile("([0-9.]*)\\s*([\u4e00-\u9fa5]+)")
// 如果解析失败,则返回当前时间
func MustParseTime(str string) time.Time { func MustParseTime(str string) time.Time {
lastOpTime, err := time.ParseInLocation("2006-01-02 -07", str+" +08", time.Local) lastOpTime, err := time.ParseInLocation("2006-01-02 -07", str+" +08", time.Local)
if err != nil { if err != nil {
@ -41,8 +45,10 @@ func MustParseTime(str string) time.Time {
return lastOpTime return lastOpTime
} }
// 解析大小
var sizeSplitReg = regexp.MustCompile(`(?i)([0-9.]+)\s*([bkm]+)`) var sizeSplitReg = regexp.MustCompile(`(?i)([0-9.]+)\s*([bkm]+)`)
// 解析失败返回0
func SizeStrToInt64(size string) int64 { func SizeStrToInt64(size string) int64 {
strs := sizeSplitReg.FindStringSubmatch(size) strs := sizeSplitReg.FindStringSubmatch(size)
if len(strs) < 3 { if len(strs) < 3 {
@ -62,8 +68,13 @@ func SizeStrToInt64(size string) int64 {
} }
// 移除注释 // 移除注释
func RemoveNotes(html []byte) []byte { func RemoveNotes(html string) string {
return regexp.MustCompile(`<!--.*?-->|//.*|/\*.*?\*/`).ReplaceAll(html, []byte{}) return regexp.MustCompile(`<!--.*?-->|[^:]//.*|/\*.*?\*/`).ReplaceAllStringFunc(html, func(b string) string {
if b[1:3] == "//" {
return b[:1]
}
return "\n"
})
} }
var findAcwScV2Reg = regexp.MustCompile(`arg1='([0-9A-Z]+)'`) var findAcwScV2Reg = regexp.MustCompile(`arg1='([0-9A-Z]+)'`)
@ -71,6 +82,7 @@ var findAcwScV2Reg = regexp.MustCompile(`arg1='([0-9A-Z]+)'`)
// 在页面被过多访问或其他情况下有时候会先返回一个加密的页面其执行计算出一个acw_sc__v2后放入页面后再重新访问页面才能获得正常页面 // 在页面被过多访问或其他情况下有时候会先返回一个加密的页面其执行计算出一个acw_sc__v2后放入页面后再重新访问页面才能获得正常页面
// 若该页面进行了js加密则进行解密计算acw_sc__v2并加入cookie // 若该页面进行了js加密则进行解密计算acw_sc__v2并加入cookie
func CalcAcwScV2(html string) (string, error) { func CalcAcwScV2(html string) (string, error) {
log.Debugln("acw_sc__v2", html)
acwScV2s := findAcwScV2Reg.FindStringSubmatch(html) acwScV2s := findAcwScV2Reg.FindStringSubmatch(html)
if len(acwScV2s) != 2 { if len(acwScV2s) != 2 {
return "", fmt.Errorf("无法匹配acw_sc__v2") return "", fmt.Errorf("无法匹配acw_sc__v2")
@ -163,3 +175,18 @@ func formToMap(from string) map[string]string {
} }
return param return param
} }
var regExpirationTime = regexp.MustCompile(`e=(\d+)`)
func GetExpirationTime(url string) (etime time.Duration) {
exps := regExpirationTime.FindStringSubmatch(url)
if len(exps) < 2 {
return
}
timestamp, err := strconv.ParseInt(exps[1], 10, 64)
if err != nil {
return
}
etime = time.Duration(timestamp-time.Now().Unix()) * time.Second
return
}

View File

@ -7,11 +7,12 @@ import (
type Addition struct { type Addition struct {
Type string `json:"type" type:"select" options:"cookie,url" default:"cookie"` Type string `json:"type" type:"select" options:"cookie,url" default:"cookie"`
Cookie string `json:"cookie" required:"true" help:"about 15 days valid"` Cookie string `json:"cookie" required:"true" help:"about 15 days valid, ignore if shareUrl is used"`
driver.RootID driver.RootID
SharePassword string `json:"share_password"` SharePassword string `json:"share_password"`
BaseUrl string `json:"baseUrl" required:"true" default:"https://pc.woozooo.com"` BaseUrl string `json:"baseUrl" required:"true" default:"https://pc.woozooo.com" help:"basic URL for file operation"`
ShareUrl string `json:"shareUrl" required:"true" default:"https://pan.lanzouo.com"` ShareUrl string `json:"shareUrl" required:"true" default:"https://pan.lanzouo.com" help:"used to get the sharing page"`
RepairFileInfo bool `json:"repair_file_info" help:"To use webdav, you need to enable it"`
} }
func (a *Addition) IsCookie() bool { func (a *Addition) IsCookie() bool {
@ -25,7 +26,7 @@ var config = driver.Config{
} }
func init() { func init() {
op.RegisterDriver(config, func() driver.Driver { op.RegisterDriver(func() driver.Driver {
return &LanZou{} return &LanZou{}
}) })
} }

View File

@ -1,14 +1,20 @@
package lanzou package lanzou
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
"github.com/alist-org/alist/v3/internal/model"
) )
type FilesOrFoldersResp struct { var ErrFileShareCancel = errors.New("file sharing cancellation")
Text []FileOrFolder `json:"text"` var ErrFileNotExist = errors.New("file does not exist")
type RespText[T any] struct {
Text T `json:"text"`
}
type RespInfo[T any] struct {
Info T `json:"info"`
} }
type FileOrFolder struct { type FileOrFolder struct {
@ -34,30 +40,51 @@ type FileOrFolder struct {
FolID string `json:"fol_id"` FolID string `json:"fol_id"`
//Folderlock string `json:"folderlock"` //Folderlock string `json:"folderlock"`
//FolderDes string `json:"folder_des"` //FolderDes string `json:"folder_des"`
// 缓存字段
size *int64 `json:"-"`
time *time.Time `json:"-"`
repairFlag bool `json:"-"`
shareInfo *FileShare `json:"-"`
} }
func (f *FileOrFolder) isFloder() bool { func (f *FileOrFolder) GetID() string {
return f.FolID != "" if f.IsDir() {
} return f.FolID
func (f *FileOrFolder) ToObj() model.Obj {
obj := &model.Object{}
if f.isFloder() {
obj.ID = f.FolID
obj.Name = f.Name
obj.Modified = time.Now()
obj.IsFolder = true
} else {
obj.ID = f.ID
obj.Name = f.NameAll
obj.Modified = MustParseTime(f.Time)
obj.Size = SizeStrToInt64(f.Size)
} }
return obj return f.ID
}
func (f *FileOrFolder) GetName() string {
if f.IsDir() {
return f.Name
}
return f.NameAll
}
func (f *FileOrFolder) GetPath() string { return "" }
func (f *FileOrFolder) GetSize() int64 {
if f.size == nil {
size := SizeStrToInt64(f.Size)
f.size = &size
}
return *f.size
}
func (f *FileOrFolder) IsDir() bool { return f.FolID != "" }
func (f *FileOrFolder) ModTime() time.Time {
if f.time == nil {
time := MustParseTime(f.Time)
f.time = &time
}
return *f.time
} }
type FileShareResp struct { func (f *FileOrFolder) SetShareInfo(fs *FileShare) {
Info FileShare `json:"info"` f.shareInfo = fs
} }
func (f *FileOrFolder) GetShareInfo() *FileShare {
return f.shareInfo
}
/* 通过ID获取文件/文件夹分享信息 */
type FileShare struct { type FileShare struct {
Pwd string `json:"pwd"` Pwd string `json:"pwd"`
Onof string `json:"onof"` Onof string `json:"onof"`
@ -73,31 +100,55 @@ type FileShare struct {
Des string `json:"des"` Des string `json:"des"`
} }
/* 分享类型为文件夹 */
type FileOrFolderByShareUrlResp struct { type FileOrFolderByShareUrlResp struct {
Text []FileOrFolderByShareUrl `json:"text"` Text []FileOrFolderByShareUrl `json:"text"`
} }
type FileOrFolderByShareUrl struct { type FileOrFolderByShareUrl struct {
ID string `json:"id"` ID string `json:"id"`
NameAll string `json:"name_all"` NameAll string `json:"name_all"`
Size string `json:"size"`
Time string `json:"time"` // 文件特有
Duan string `json:"duan"` Duan string `json:"duan"`
Size string `json:"size"`
Time string `json:"time"`
//Icon string `json:"icon"` //Icon string `json:"icon"`
//PIco int `json:"p_ico"` //PIco int `json:"p_ico"`
//T int `json:"t"` //T int `json:"t"`
IsFloder bool
// 文件夹特有
IsFloder bool `json:"-"`
//
Url string `json:"-"`
Pwd string `json:"-"`
// 缓存字段
size *int64 `json:"-"`
time *time.Time `json:"-"`
repairFlag bool `json:"-"`
} }
func (f *FileOrFolderByShareUrl) ToObj() model.Obj { func (f *FileOrFolderByShareUrl) GetID() string { return f.ID }
return &model.Object{ func (f *FileOrFolderByShareUrl) GetName() string { return f.NameAll }
ID: f.ID, func (f *FileOrFolderByShareUrl) GetPath() string { return "" }
Name: f.NameAll, func (f *FileOrFolderByShareUrl) GetSize() int64 {
Size: SizeStrToInt64(f.Size), if f.size == nil {
Modified: MustParseTime(f.Time), size := SizeStrToInt64(f.Size)
IsFolder: f.IsFloder, f.size = &size
} }
return *f.size
}
func (f *FileOrFolderByShareUrl) IsDir() bool { return f.IsFloder }
func (f *FileOrFolderByShareUrl) ModTime() time.Time {
if f.time == nil {
time := MustParseTime(f.Time)
f.time = &time
}
return *f.time
} }
// 获取下载链接的响应
type FileShareInfoAndUrlResp[T string | int] struct { type FileShareInfoAndUrlResp[T string | int] struct {
Dom string `json:"dom"` Dom string `json:"dom"`
URL string `json:"url"` URL string `json:"url"`
@ -111,21 +162,3 @@ func (u *FileShareInfoAndUrlResp[T]) GetBaseUrl() string {
func (u *FileShareInfoAndUrlResp[T]) GetDownloadUrl() string { func (u *FileShareInfoAndUrlResp[T]) GetDownloadUrl() string {
return fmt.Sprint(u.GetBaseUrl(), "/", u.URL) return fmt.Sprint(u.GetBaseUrl(), "/", u.URL)
} }
// 通过分享链接获取文件信息和下载链接
type FileInfoAndUrlByShareUrl struct {
ID string
Name string
Size string
Time string
Url string
}
func (f *FileInfoAndUrlByShareUrl) ToObj() model.Obj {
return &model.Object{
ID: f.ID,
Name: f.Name,
Size: SizeStrToInt64(f.Size),
Modified: MustParseTime(f.Time),
}
}

View File

@ -1,7 +1,7 @@
package lanzou package lanzou
import ( import (
"context" "errors"
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
@ -13,8 +13,16 @@ import (
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
) )
func (d *LanZou) doupload(callback base.ReqCallback, resp interface{}) ([]byte, error) {
return d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
req.SetQueryParam("uid", d.uid)
callback(req)
}, resp)
}
func (d *LanZou) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) { func (d *LanZou) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return d.request(url, http.MethodGet, callback, false) return d.request(url, http.MethodGet, callback, false)
} }
@ -24,7 +32,16 @@ func (d *LanZou) post(url string, callback base.ReqCallback, resp interface{}) (
} }
func (d *LanZou) _post(url string, callback base.ReqCallback, resp interface{}, up bool) ([]byte, error) { func (d *LanZou) _post(url string, callback base.ReqCallback, resp interface{}, up bool) ([]byte, error) {
data, err := d.request(url, http.MethodPost, callback, up) data, err := d.request(url, http.MethodPost, func(req *resty.Request) {
req.AddRetryCondition(func(r *resty.Response, err error) bool {
if utils.Json.Get(r.Body(), "zt").ToInt() == 4 {
time.Sleep(time.Second)
return true
}
return false
})
callback(req)
}, up)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -68,7 +85,7 @@ func (d *LanZou) request(url string, method string, callback base.ReqCallback, u
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debugf("lanzou request: url=>%s ,stats=>%d ,body => %s\n", res.Request.URL, res.StatusCode(), res.String())
return res.Body(), err return res.Body(), err
} }
@ -77,31 +94,28 @@ func (d *LanZou) request(url string, method string, callback base.ReqCallback, u
*/ */
// 获取文件和文件夹,获取到的文件大小、更改时间不可信 // 获取文件和文件夹,获取到的文件大小、更改时间不可信
func (d *LanZou) GetFiles(ctx context.Context, folderID string) ([]model.Obj, error) { func (d *LanZou) GetAllFiles(folderID string) ([]model.Obj, error) {
folders, err := d.getFolders(ctx, folderID) folders, err := d.GetFolders(folderID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
files, err := d.getFiles(ctx, folderID) files, err := d.GetFiles(folderID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
objs := make([]model.Obj, 0, len(folders)+len(files)) return append(
for _, folder := range folders { utils.MustSliceConvert(folders, func(folder FileOrFolder) model.Obj {
objs = append(objs, folder.ToObj()) return &folder
} }), utils.MustSliceConvert(files, func(file FileOrFolder) model.Obj {
return &file
for _, file := range files { })...,
objs = append(objs, file.ToObj()) ), nil
}
return objs, nil
} }
// 通过ID获取文件夹 // 通过ID获取文件夹
func (d *LanZou) getFolders(ctx context.Context, folderID string) ([]FileOrFolder, error) { func (d *LanZou) GetFolders(folderID string) ([]FileOrFolder, error) {
var resp FilesOrFoldersResp var resp RespText[[]FileOrFolder]
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) { _, err := d.doupload(func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
"task": "47", "task": "47",
"folder_id": folderID, "folder_id": folderID,
@ -114,12 +128,11 @@ func (d *LanZou) getFolders(ctx context.Context, folderID string) ([]FileOrFolde
} }
// 通过ID获取文件 // 通过ID获取文件
func (d *LanZou) getFiles(ctx context.Context, folderID string) ([]FileOrFolder, error) { func (d *LanZou) GetFiles(folderID string) ([]FileOrFolder, error) {
files := make([]FileOrFolder, 0) files := make([]FileOrFolder, 0)
for pg := 1; ; pg++ { for pg := 1; ; pg++ {
var resp FilesOrFoldersResp var resp RespText[[]FileOrFolder]
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) { _, err := d.doupload(func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
"task": "5", "task": "5",
"folder_id": folderID, "folder_id": folderID,
@ -138,37 +151,33 @@ func (d *LanZou) getFiles(ctx context.Context, folderID string) ([]FileOrFolder,
} }
// 通过ID获取文件夹分享地址 // 通过ID获取文件夹分享地址
func (d *LanZou) getFolderShareUrlByID(ctx context.Context, fileID string) (share FileShare, err error) { func (d *LanZou) getFolderShareUrlByID(fileID string) (*FileShare, error) {
var resp FileShareResp var resp RespInfo[FileShare]
_, err = d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) { _, err := d.doupload(func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
"task": "18", "task": "18",
"file_id": fileID, "file_id": fileID,
}) })
}, &resp) }, &resp)
if err != nil { if err != nil {
return return nil, err
} }
share = resp.Info return &resp.Info, nil
return
} }
// 通过ID获取文件分享地址 // 通过ID获取文件分享地址
func (d *LanZou) getFileShareUrlByID(ctx context.Context, fileID string) (share FileShare, err error) { func (d *LanZou) getFileShareUrlByID(fileID string) (*FileShare, error) {
var resp FileShareResp var resp RespInfo[FileShare]
_, err = d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) { _, err := d.doupload(func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{ req.SetFormData(map[string]string{
"task": "22", "task": "22",
"file_id": fileID, "file_id": fileID,
}) })
}, &resp) }, &resp)
if err != nil { if err != nil {
return return nil, err
} }
share = resp.Info return &resp.Info, nil
return
} }
/* /*
@ -180,237 +189,252 @@ var isFileReg = regexp.MustCompile(`class="fileinfo"|id="file"|文件描述`)
var isFolderReg = regexp.MustCompile(`id="infos"`) var isFolderReg = regexp.MustCompile(`id="infos"`)
// 获取文件文件夹基础信息 // 获取文件文件夹基础信息
// 获取文件名称
var nameFindReg = regexp.MustCompile(`<title>(.+?) - 蓝奏云</title>|id="filenajax">(.+?)</div>|var filename = '(.+?)';|<div style="font-size.+?>([^<>].+?)</div>|<div class="filethetext".+?>([^<>]+?)</div>`) var nameFindReg = regexp.MustCompile(`<title>(.+?) - 蓝奏云</title>|id="filenajax">(.+?)</div>|var filename = '(.+?)';|<div style="font-size.+?>([^<>].+?)</div>|<div class="filethetext".+?>([^<>]+?)</div>`)
// 获取文件大小
var sizeFindReg = regexp.MustCompile(`(?i)大小\W*([0-9.]+\s*[bkm]+)`) var sizeFindReg = regexp.MustCompile(`(?i)大小\W*([0-9.]+\s*[bkm]+)`)
// 获取文件时间
var timeFindReg = regexp.MustCompile(`\d+\s*[秒天分小][钟时]?前|[昨前]天|\d{4}-\d{2}-\d{2}`) var timeFindReg = regexp.MustCompile(`\d+\s*[秒天分小][钟时]?前|[昨前]天|\d{4}-\d{2}-\d{2}`)
var findSubFolaerReg = regexp.MustCompile(`(folderlink|mbxfolder).+href="/(.+?)"(.+filename")?>(.+?)<`) // 查找分享文件夹子文件夹ID和名称 // 查找分享文件夹子文件夹ID和名称
var findSubFolaerReg = regexp.MustCompile(`(?i)(?:folderlink|mbxfolder).+href="/(.+?)"(?:.+filename")?>(.+?)<`)
// 获取关键数据 // 获取下载页面链接
var findDownPageParamReg = regexp.MustCompile(`<iframe.*?src="(.+?)"`) var findDownPageParamReg = regexp.MustCompile(`<iframe.*?src="(.+?)"`)
// 通过分享链接获取文件或文件夹,如果是文件则会返回下载链接 // 获取分享链接主界面
func (d *LanZou) GetFileOrFolderByShareUrl(ctx context.Context, downID, pwd string) ([]model.Obj, error) { func (d *LanZou) getShareUrlHtml(shareID string) (string, error) {
pageData, err := d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) { req.SetContext(ctx) }, nil) var vs string
for i := 0; i < 3; i++ {
firstPageData, err := d.get(fmt.Sprint(d.ShareUrl, "/", shareID),
func(req *resty.Request) {
if vs != "" {
req.SetCookie(&http.Cookie{
Name: "acw_sc__v2",
Value: vs,
})
}
}, nil)
if err != nil {
return "", err
}
firstPageDataStr := RemoveNotes(string(firstPageData))
if strings.Contains(firstPageDataStr, "取消分享") {
return "", ErrFileShareCancel
}
if strings.Contains(firstPageDataStr, "文件不存在") {
return "", ErrFileNotExist
}
// acw_sc__v2
if strings.Contains(firstPageDataStr, "acw_sc__v2") {
if vs, err = CalcAcwScV2(firstPageDataStr); err != nil {
log.Errorf("lanzou: err => acw_sc__v2 validation error ,data => %s\n", firstPageDataStr)
return "", err
}
continue
}
return firstPageDataStr, nil
}
return "", errors.New("acw_sc__v2 validation error")
}
// 通过分享链接获取文件或文件夹
func (d *LanZou) GetFileOrFolderByShareUrl(shareID, pwd string) ([]model.Obj, error) {
pageData, err := d.getShareUrlHtml(shareID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pageData = RemoveNotes(pageData)
var objs []model.Obj if !isFileReg.MatchString(pageData) {
if !isFileReg.Match(pageData) { files, err := d.getFolderByShareUrl(pwd, pageData)
files, err := d.getFolderByShareUrl(ctx, downID, pwd, pageData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
objs = make([]model.Obj, 0, len(files)) return utils.MustSliceConvert(files, func(file FileOrFolderByShareUrl) model.Obj {
for _, file := range files { return &file
objs = append(objs, file.ToObj()) }), nil
}
} else { } else {
file, err := d.getFilesByShareUrl(ctx, downID, pwd, pageData) file, err := d.getFilesByShareUrl(shareID, pwd, pageData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
objs = []model.Obj{file.ToObj()} return []model.Obj{file}, nil
} }
return objs, nil
} }
// 通过分享链接获取文件(下载链接也使用此方法) // 通过分享链接获取文件(下载链接也使用此方法)
// FileOrFolderByShareUrl 包含 pwd 和 url 字段
// 参考 https://github.com/zaxtyson/LanZouCloud-API/blob/ab2e9ec715d1919bf432210fc16b91c6775fbb99/lanzou/api/core.py#L440 // 参考 https://github.com/zaxtyson/LanZouCloud-API/blob/ab2e9ec715d1919bf432210fc16b91c6775fbb99/lanzou/api/core.py#L440
func (d *LanZou) getFilesByShareUrl(ctx context.Context, downID, pwd string, firstPageData []byte) (file FileInfoAndUrlByShareUrl, err error) { func (d *LanZou) GetFilesByShareUrl(shareID, pwd string) (file *FileOrFolderByShareUrl, err error) {
if firstPageData == nil { pageData, err := d.getShareUrlHtml(shareID)
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) { req.SetContext(ctx) }, nil) if err != nil {
if err != nil { return nil, err
return
}
firstPageData = RemoveNotes(firstPageData)
}
firstPageDataStr := string(firstPageData)
if strings.Contains(firstPageDataStr, "acw_sc__v2") {
var vs string
if vs, err = CalcAcwScV2(firstPageDataStr); err != nil {
return
}
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) {
req.SetCookie(&http.Cookie{
Name: "acw_sc__v2",
Value: vs,
})
req.SetContext(ctx)
}, nil)
if err != nil {
return
}
firstPageData = RemoveNotes(firstPageData)
firstPageDataStr = string(firstPageData)
} }
return d.getFilesByShareUrl(shareID, pwd, pageData)
}
func (d *LanZou) getFilesByShareUrl(shareID, pwd string, sharePageData string) (*FileOrFolderByShareUrl, error) {
var ( var (
param map[string]string param map[string]string
downloadUrl string downloadUrl string
baseUrl string baseUrl string
file FileOrFolderByShareUrl
) )
// 需要密码 // 需要密码
if strings.Contains(firstPageDataStr, "pwdload") || strings.Contains(firstPageDataStr, "passwddiv") { if strings.Contains(sharePageData, "pwdload") || strings.Contains(sharePageData, "passwddiv") {
param, err = htmlFormToMap(firstPageDataStr) param, err := htmlFormToMap(sharePageData)
if err != nil { if err != nil {
return return nil, err
} }
param["p"] = pwd param["p"] = pwd
var resp FileShareInfoAndUrlResp[string] var resp FileShareInfoAndUrlResp[string]
_, err = d.post(d.ShareUrl+"/ajaxm.php", func(req *resty.Request) { req.SetFormData(param).SetContext(ctx) }, &resp) _, err = d.post(d.ShareUrl+"/ajaxm.php", func(req *resty.Request) { req.SetFormData(param) }, &resp)
if err != nil { if err != nil {
return return nil, err
} }
file.Name = resp.Inf file.NameAll = resp.Inf
file.Pwd = pwd
baseUrl = resp.GetBaseUrl() baseUrl = resp.GetBaseUrl()
downloadUrl = resp.GetDownloadUrl() downloadUrl = resp.GetDownloadUrl()
} else { } else {
urlpaths := findDownPageParamReg.FindStringSubmatch(firstPageDataStr) urlpaths := findDownPageParamReg.FindStringSubmatch(sharePageData)
if len(urlpaths) != 2 { if len(urlpaths) != 2 {
err = fmt.Errorf("not find file page param") log.Errorf("lanzou: err => not find file page param ,data => %s\n", sharePageData)
return return nil, fmt.Errorf("not find file page param")
} }
var nextPageData []byte data, err := d.get(fmt.Sprint(d.ShareUrl, urlpaths[1]), nil, nil)
nextPageData, err = d.get(fmt.Sprint(d.ShareUrl, urlpaths[1]), func(req *resty.Request) { req.SetContext(ctx) }, nil)
if err != nil { if err != nil {
return return nil, err
} }
nextPageData = RemoveNotes(nextPageData) nextPageData := RemoveNotes(string(data))
nextPageDataStr := string(nextPageData)
param, err = htmlJsonToMap(nextPageDataStr) param, err = htmlJsonToMap(nextPageData)
if err != nil { if err != nil {
return return nil, err
} }
var resp FileShareInfoAndUrlResp[int] var resp FileShareInfoAndUrlResp[int]
_, err = d.post(d.ShareUrl+"/ajaxm.php", func(req *resty.Request) { req.SetFormData(param).SetContext(ctx) }, &resp) _, err = d.post(d.ShareUrl+"/ajaxm.php", func(req *resty.Request) { req.SetFormData(param) }, &resp)
if err != nil { if err != nil {
return return nil, err
} }
baseUrl = resp.GetBaseUrl() baseUrl = resp.GetBaseUrl()
downloadUrl = resp.GetDownloadUrl() downloadUrl = resp.GetDownloadUrl()
names := nameFindReg.FindStringSubmatch(firstPageDataStr) names := nameFindReg.FindStringSubmatch(sharePageData)
if len(names) > 1 { if len(names) > 1 {
for _, name := range names[1:] { for _, name := range names[1:] {
if name != "" { if name != "" {
file.Name = name file.NameAll = name
break break
} }
} }
} }
} }
sizes := sizeFindReg.FindStringSubmatch(firstPageDataStr) sizes := sizeFindReg.FindStringSubmatch(sharePageData)
if len(sizes) == 2 { if len(sizes) == 2 {
file.Size = sizes[1] file.Size = sizes[1]
} }
file.ID = downID file.ID = shareID
file.Time = timeFindReg.FindString(firstPageDataStr) file.Time = timeFindReg.FindString(sharePageData)
// 重定向获取真实链接 // 重定向获取真实链接
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{ res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
}).SetContext(ctx).Get(downloadUrl) }).Get(downloadUrl)
if err != nil { if err != nil {
return return nil, err
} }
file.Url = res.Header().Get("location") file.Url = res.Header().Get("location")
// 触发验证 // 触发验证
rPageDataStr := res.String() rPageData := res.String()
if res.StatusCode() != 302 && strings.Contains(rPageDataStr, "网络异常") { if res.StatusCode() != 302 {
param, err = htmlJsonToMap(rPageDataStr) param, err = htmlJsonToMap(rPageData)
if err != nil { if err != nil {
return return nil, err
} }
param["el"] = "2" param["el"] = "2"
time.Sleep(time.Second * 2) time.Sleep(time.Second * 2)
// 通过验证获取直连 // 通过验证获取直连
var rUrl struct { data, err := d.post(fmt.Sprint(baseUrl, "/ajax.php"), func(req *resty.Request) { req.SetFormData(param) }, nil)
Url string `json:"url"`
}
_, err = d.post(fmt.Sprint(baseUrl, "/ajax.php"), func(req *resty.Request) { req.SetContext(ctx).SetFormData(param) }, &rUrl)
if err != nil { if err != nil {
return return nil, err
} }
file.Url = rUrl.Url file.Url = utils.Json.Get(data, "url").ToString()
} }
return return &file, nil
} }
// 通过分享链接获取文件夹 // 通过分享链接获取文件夹
// 似乎子目录和文件不会加密
// 参考 https://github.com/zaxtyson/LanZouCloud-API/blob/ab2e9ec715d1919bf432210fc16b91c6775fbb99/lanzou/api/core.py#L1089 // 参考 https://github.com/zaxtyson/LanZouCloud-API/blob/ab2e9ec715d1919bf432210fc16b91c6775fbb99/lanzou/api/core.py#L1089
func (d *LanZou) getFolderByShareUrl(ctx context.Context, downID, pwd string, firstPageData []byte) ([]FileOrFolderByShareUrl, error) { func (d *LanZou) GetFolderByShareUrl(shareID, pwd string) ([]FileOrFolderByShareUrl, error) {
if firstPageData == nil { pageData, err := d.getShareUrlHtml(shareID)
var err error if err != nil {
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) { req.SetContext(ctx) }, nil) return nil, err
if err != nil { }
return nil, err return d.getFolderByShareUrl(pwd, pageData)
} }
firstPageData = RemoveNotes(firstPageData)
} func (d *LanZou) getFolderByShareUrl(pwd string, sharePageData string) ([]FileOrFolderByShareUrl, error) {
firstPageDataStr := string(firstPageData) from, err := htmlJsonToMap(sharePageData)
//
if strings.Contains(firstPageDataStr, "acw_sc__v2") {
vs, err := CalcAcwScV2(firstPageDataStr)
if err != nil {
return nil, err
}
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) {
req.SetCookie(&http.Cookie{
Name: "acw_sc__v2",
Value: vs,
})
req.SetContext(ctx)
}, nil)
if err != nil {
return nil, err
}
firstPageData = RemoveNotes(firstPageData)
firstPageDataStr = string(firstPageData)
}
from, err := htmlJsonToMap(firstPageDataStr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
from["pwd"] = pwd
files := make([]FileOrFolderByShareUrl, 0) files := make([]FileOrFolderByShareUrl, 0)
// vip获取文件夹 // vip获取文件夹
floders := findSubFolaerReg.FindAllStringSubmatch(firstPageDataStr, -1) floders := findSubFolaerReg.FindAllStringSubmatch(sharePageData, -1)
for _, floder := range floders { for _, floder := range floders {
if len(floder) == 5 { if len(floder) == 3 {
files = append(files, FileOrFolderByShareUrl{ files = append(files, FileOrFolderByShareUrl{
ID: floder[2], // Pwd: pwd, // 子文件夹不加密
NameAll: floder[4], ID: floder[1],
NameAll: floder[2],
IsFloder: true, IsFloder: true,
}) })
} }
} }
// 获取文件
from["pwd"] = pwd
for page := 1; ; page++ { for page := 1; ; page++ {
from["pg"] = strconv.Itoa(page) from["pg"] = strconv.Itoa(page)
var resp FileOrFolderByShareUrlResp var resp FileOrFolderByShareUrlResp
_, err := d.post(d.ShareUrl+"/filemoreajax.php", func(req *resty.Request) { req.SetFormData(from).SetContext(ctx) }, &resp) _, err := d.post(d.ShareUrl+"/filemoreajax.php", func(req *resty.Request) { req.SetFormData(from) }, &resp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
files = append(files, resp.Text...) /*// 文件夹中的文件也不加密
for i := 0; i < len(resp.Text); i++ {
resp.Text[i].Pwd = pwd
}*/
if len(resp.Text) == 0 { if len(resp.Text) == 0 {
break break
} }
time.Sleep(time.Millisecond * 600) files = append(files, resp.Text...)
time.Sleep(time.Second)
} }
return files, nil return files, nil
} }
// 通过下载头获取真实文件信息
func (d *LanZou) getFileRealInfo(downURL string) (*int64, *time.Time) {
res, _ := base.RestyClient.R().Head(downURL)
if res == nil {
return nil, nil
}
time, _ := http.ParseTime(res.Header().Get("Last-Modified"))
size, _ := strconv.ParseInt(res.Header().Get("Content-Length"), 10, 64)
return &size, &time
}

View File

@ -18,9 +18,11 @@ import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "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/pkg/utils"
"github.com/alist-org/alist/v3/server/common" "github.com/alist-org/alist/v3/server/common"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
_ "golang.org/x/image/webp"
) )
type Local struct { type Local struct {
@ -32,24 +34,18 @@ func (d *Local) Config() driver.Config {
return config return config
} }
func (d *Local) Init(ctx context.Context, storage model.Storage) error { func (d *Local) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
if !utils.Exists(d.GetRootPath()) { if !utils.Exists(d.GetRootPath()) {
err = fmt.Errorf("root folder %s not exists", d.GetRootPath()) return fmt.Errorf("root folder %s not exists", d.GetRootPath())
} else {
if !filepath.IsAbs(d.GetRootPath()) {
abs, err := filepath.Abs(d.GetRootPath())
if err != nil {
return err
}
d.SetRootPath(abs)
}
} }
return err if !filepath.IsAbs(d.GetRootPath()) {
abs, err := filepath.Abs(d.GetRootPath())
if err != nil {
return err
}
d.Addition.RootFolderPath = abs
}
return nil
} }
func (d *Local) Drop(ctx context.Context) error { func (d *Local) Drop(ctx context.Context) error {
@ -57,7 +53,7 @@ func (d *Local) Drop(ctx context.Context) error {
} }
func (d *Local) GetAddition() driver.Additional { func (d *Local) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
@ -75,14 +71,20 @@ func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
if d.Thumbnail && utils.GetFileType(f.Name()) == conf.IMAGE { if d.Thumbnail && utils.GetFileType(f.Name()) == conf.IMAGE {
thumb = common.GetApiUrl(nil) + stdpath.Join("/d", args.ReqPath, f.Name()) thumb = common.GetApiUrl(nil) + stdpath.Join("/d", args.ReqPath, f.Name())
thumb = utils.EncodePath(thumb, true) thumb = utils.EncodePath(thumb, true)
thumb += "?type=thumb" thumb += "?type=thumb&sign=" + sign.Sign(stdpath.Join(args.ReqPath, f.Name()))
}
isFolder := f.IsDir() || isSymlinkDir(f, fullPath)
var size int64
if !isFolder {
size = f.Size()
} }
file := model.ObjThumb{ file := model.ObjThumb{
Object: model.Object{ Object: model.Object{
Path: filepath.Join(dir.GetPath(), f.Name()),
Name: f.Name(), Name: f.Name(),
Modified: f.ModTime(), Modified: f.ModTime(),
Size: f.Size(), Size: size,
IsFolder: f.IsDir(), IsFolder: isFolder,
}, },
Thumbnail: model.Thumbnail{ Thumbnail: model.Thumbnail{
Thumbnail: thumb, Thumbnail: thumb,
@ -94,6 +96,7 @@ func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
} }
func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) { func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {
path = filepath.Join(d.GetRootPath(), path)
f, err := os.Stat(path) f, err := os.Stat(path)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "cannot find the file") { if strings.Contains(err.Error(), "cannot find the file") {
@ -101,12 +104,17 @@ func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {
} }
return nil, err return nil, err
} }
isFolder := f.IsDir() || isSymlinkDir(f, path)
size := f.Size()
if isFolder {
size = 0
}
file := model.Object{ file := model.Object{
Path: path, Path: path,
Name: f.Name(), Name: f.Name(),
Modified: f.ModTime(), Modified: f.ModTime(),
Size: f.Size(), Size: size,
IsFolder: f.IsDir(), IsFolder: isFolder,
} }
return &file, nil return &file, nil
} }
@ -153,6 +161,9 @@ func (d *Local) MakeDir(ctx context.Context, parentDir model.Obj, dirName string
func (d *Local) Move(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *Local) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
srcPath := srcObj.GetPath() srcPath := srcObj.GetPath()
dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName()) dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())
if utils.IsSubPath(srcPath, dstPath) {
return fmt.Errorf("the destination folder is a subfolder of the source folder")
}
err := os.Rename(srcPath, dstPath) err := os.Rename(srcPath, dstPath)
if err != nil { if err != nil {
return err return err
@ -173,6 +184,9 @@ func (d *Local) Rename(ctx context.Context, srcObj model.Obj, newName string) er
func (d *Local) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *Local) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
srcPath := srcObj.GetPath() srcPath := srcObj.GetPath()
dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName()) dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())
if utils.IsSubPath(srcPath, dstPath) {
return fmt.Errorf("the destination folder is a subfolder of the source folder")
}
var err error var err error
if srcObj.IsDir() { if srcObj.IsDir() {
err = utils.CopyDir(srcPath, dstPath) err = utils.CopyDir(srcPath, dstPath)

View File

@ -19,10 +19,8 @@ var config = driver.Config{
DefaultRoot: "/", DefaultRoot: "/",
} }
func New() driver.Driver {
return &Local{}
}
func init() { func init() {
op.RegisterDriver(config, New) op.RegisterDriver(func() driver.Driver {
return &Local{}
})
} }

View File

@ -1 +1,25 @@
package local package local
import (
"io/fs"
"os"
"path/filepath"
)
func isSymlinkDir(f fs.FileInfo, path string) bool {
if f.Mode()&os.ModeSymlink == os.ModeSymlink {
dst, err := os.Readlink(filepath.Join(path, f.Name()))
if err != nil {
return false
}
if !filepath.IsAbs(dst) {
dst = filepath.Join(path, dst)
}
stat, err := os.Stat(dst)
if err != nil {
return false
}
return stat.IsDir()
}
return false
}

View File

@ -8,14 +8,12 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"path"
"strconv" "strconv"
"time" "time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
@ -36,16 +34,11 @@ func (d *MediaTrack) Config() driver.Config {
} }
func (d *MediaTrack) GetAddition() driver.Additional { func (d *MediaTrack) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *MediaTrack) Init(ctx context.Context, storage model.Storage) error { func (d *MediaTrack) Init(ctx context.Context) error {
d.Storage = storage _, err := d.request("https://kayle.api.mediatrack.cn/users", http.MethodGet, nil, nil)
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
_, err = d.request("https://kayle.api.mediatrack.cn/users", http.MethodGet, nil, nil)
return err return err
} }
@ -64,7 +57,7 @@ func (d *MediaTrack) List(ctx context.Context, dir model.Obj, args model.ListArg
if f.File != nil && f.File.Cover != "" { if f.File != nil && f.File.Cover != "" {
thumb = "https://nano.mtres.cn/" + f.File.Cover thumb = "https://nano.mtres.cn/" + f.File.Cover
} }
return &model.ObjThumb{ return &Object{
Object: model.Object{ Object: model.Object{
ID: f.ID, ID: f.ID,
Name: f.Title, Name: f.Title,
@ -73,15 +66,11 @@ func (d *MediaTrack) List(ctx context.Context, dir model.Obj, args model.ListArg
Size: size, Size: size,
}, },
Thumbnail: model.Thumbnail{Thumbnail: thumb}, Thumbnail: model.Thumbnail{Thumbnail: thumb},
ParentID: dir.GetID(),
}, nil }, nil
}) })
} }
//func (d *MediaTrack) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *MediaTrack) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *MediaTrack) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
url := fmt.Sprintf("https://kayn.api.mediatrack.cn/v1/download_token/asset?asset_id=%s&source_type=project&password=&source_id=%s", url := fmt.Sprintf("https://kayn.api.mediatrack.cn/v1/download_token/asset?asset_id=%s&source_type=project&password=&source_id=%s",
file.GetID(), d.ProjectID) file.GetID(), d.ProjectID)
@ -156,16 +145,18 @@ func (d *MediaTrack) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
} }
func (d *MediaTrack) Remove(ctx context.Context, obj model.Obj) error { func (d *MediaTrack) Remove(ctx context.Context, obj model.Obj) error {
dir, err := op.Get(ctx, d, path.Dir(obj.GetPath())) var parentID string
if err != nil { if o, ok := obj.(*Object); ok {
return err parentID = o.ParentID
} else {
return fmt.Errorf("obj is not local Object")
} }
data := base.Json{ data := base.Json{
"origin_id": dir.GetID(), "origin_id": parentID,
"ids": []string{obj.GetID()}, "ids": []string{obj.GetID()},
} }
url := "https://jayce.api.mediatrack.cn/v4/assets/batch/delete" url := "https://jayce.api.mediatrack.cn/v4/assets/batch/delete"
_, err = d.request(url, http.MethodDelete, func(req *resty.Request) { _, err := d.request(url, http.MethodDelete, func(req *resty.Request) {
req.SetBody(data) req.SetBody(data)
}, nil) }, nil)
return err return err
@ -204,7 +195,7 @@ func (d *MediaTrack) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
Key: &resp.Data.Object, Key: &resp.Data.Object,
Body: tempFile, Body: tempFile,
} }
_, err = uploader.Upload(input) _, err = uploader.UploadWithContext(ctx, input)
if err != nil { if err != nil {
return err return err
} }

View File

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

View File

@ -1,6 +1,10 @@
package mediatrack package mediatrack
import "time" import (
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type BaseResp struct { type BaseResp struct {
Status string `json:"status"` Status string `json:"status"`
@ -60,3 +64,9 @@ type UploadResp struct {
TraceID string `json:"trace_id"` TraceID string `json:"trace_id"`
RequestID string `json:"requestId"` RequestID string `json:"requestId"`
} }
type Object struct {
model.Object
model.Thumbnail
ParentID string
}

View File

@ -9,7 +9,6 @@ import (
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/chanio"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/t3rm1n4l/go-mega" "github.com/t3rm1n4l/go-mega"
@ -26,15 +25,10 @@ func (d *Mega) Config() driver.Config {
} }
func (d *Mega) GetAddition() driver.Additional { func (d *Mega) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *Mega) Init(ctx context.Context, storage model.Storage) error { func (d *Mega) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
d.c = mega.New() d.c = mega.New()
return d.c.Login(d.Email, d.Password) return d.c.Login(d.Email, d.Password)
} }
@ -62,13 +56,10 @@ func (d *Mega) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
return nil, fmt.Errorf("unable to convert dir to mega node") return nil, fmt.Errorf("unable to convert dir to mega node")
} }
func (d *Mega) Get(ctx context.Context, path string) (model.Obj, error) { func (d *Mega) GetRoot(ctx context.Context) (model.Obj, error) {
if path == "/" { n := d.c.FS.GetRoot()
n := d.c.FS.GetRoot() log.Debugf("mega root: %+v", *n)
log.Debugf("mega root: %+v", *n) return &MegaNode{n}, nil
return &MegaNode{n}, nil
}
return nil, errs.NotSupport
} }
func (d *Mega) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *Mega) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
@ -85,17 +76,21 @@ func (d *Mega) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
//u := down.GetResourceUrl() //u := down.GetResourceUrl()
//u = strings.Replace(u, "http", "https", 1) //u = strings.Replace(u, "http", "https", 1)
//return &model.Link{URL: u}, nil //return &model.Link{URL: u}, nil
c := chanio.New() r, w := io.Pipe()
go func() { go func() {
defer func() { defer func() {
_ = recover() _ = recover()
}() }()
log.Debugf("chunk size: %d", down.Chunks()) log.Debugf("chunk size: %d", down.Chunks())
var (
chunk []byte
err error
)
for id := 0; id < down.Chunks(); id++ { for id := 0; id < down.Chunks(); id++ {
chunk, err := down.DownloadChunk(id) chunk, err = down.DownloadChunk(id)
if err != nil { if err != nil {
log.Errorf("mega down: %+v", err) log.Errorf("mega down: %+v", err)
return break
} }
log.Debugf("id: %d,len: %d", id, len(chunk)) log.Debugf("id: %d,len: %d", id, len(chunk))
//_, _, err = down.ChunkLocation(id) //_, _, err = down.ChunkLocation(id)
@ -104,14 +99,16 @@ func (d *Mega) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
// return // return
//} //}
//_, err = c.Write(chunk) //_, err = c.Write(chunk)
_, err = c.Write(chunk) if _, err = w.Write(chunk); err != nil {
break
}
} }
err := c.Close() err = w.CloseWithError(err)
if err != nil { if err != nil {
log.Errorf("mega down: %+v", err) log.Errorf("mega down: %+v", err)
} }
}() }()
return &model.Link{Data: c}, nil return &model.Link{Data: r}, nil
} }
return nil, fmt.Errorf("unable to convert dir to mega node") return nil, fmt.Errorf("unable to convert dir to mega node")
} }
@ -159,6 +156,9 @@ func (d *Mega) Put(ctx context.Context, dstDir model.Obj, stream model.FileStrea
} }
for id := 0; id < u.Chunks(); id++ { for id := 0; id < u.Chunks(); id++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
_, chkSize, err := u.ChunkLocation(id) _, chkSize, err := u.ChunkLocation(id)
if err != nil { if err != nil {
return err return err

View File

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

View File

@ -2,14 +2,13 @@ package onedrive
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
stdpath "path"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
) )
@ -25,15 +24,10 @@ func (d *Onedrive) Config() driver.Config {
} }
func (d *Onedrive) GetAddition() driver.Additional { func (d *Onedrive) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *Onedrive) Init(ctx context.Context, storage model.Storage) error { func (d *Onedrive) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
if d.ChunkSize < 1 { if d.ChunkSize < 1 {
d.ChunkSize = 5 d.ChunkSize = 5
} }
@ -50,7 +44,7 @@ func (d *Onedrive) List(ctx context.Context, dir model.Obj, args model.ListArgs)
return nil, err return nil, err
} }
return utils.SliceConvert(files, func(src File) (model.Obj, error) { return utils.SliceConvert(files, func(src File) (model.Obj, error) {
return fileToObj(src), nil return fileToObj(src, dir.GetID()), nil
}) })
} }
@ -95,18 +89,21 @@ func (d *Onedrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
} }
func (d *Onedrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error { func (d *Onedrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
dstDir, err := op.Get(ctx, d, stdpath.Dir(srcObj.GetPath())) //dstDir, err := op.GetUnwrap(ctx, d, stdpath.Dir(srcObj.GetPath()))
if err != nil { var parentID string
return err if o, ok := srcObj.(*Object); ok {
parentID = o.ParentID
} else {
return fmt.Errorf("srcObj is not Object")
} }
data := base.Json{ data := base.Json{
"parentReference": base.Json{ "parentReference": base.Json{
"id": dstDir.GetID(), "id": parentID,
}, },
"name": newName, "name": newName,
} }
url := d.GetMetaUrl(false, srcObj.GetPath()) url := d.GetMetaUrl(false, srcObj.GetPath())
_, err = d.Request(url, http.MethodPatch, func(req *resty.Request) { _, err := d.Request(url, http.MethodPatch, func(req *resty.Request) {
req.SetBody(data) req.SetBody(data)
}, nil) }, nil)
return err return err
@ -140,7 +137,7 @@ func (d *Onedrive) Remove(ctx context.Context, obj model.Obj) error {
func (d *Onedrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *Onedrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
var err error var err error
if stream.GetSize() <= 4*1024*1024 { if stream.GetSize() <= 4*1024*1024 {
err = d.upSmall(dstDir, stream) err = d.upSmall(ctx, dstDir, stream)
} else { } else {
err = d.upBig(ctx, dstDir, stream, up) err = d.upBig(ctx, dstDir, stream, up)
} }

View File

@ -23,10 +23,8 @@ var config = driver.Config{
DefaultRoot: "/", DefaultRoot: "/",
} }
func New() driver.Driver {
return &Onedrive{}
}
func init() { func init() {
op.RegisterDriver(config, New) op.RegisterDriver(func() driver.Driver {
return &Onedrive{}
})
} }

View File

@ -42,21 +42,29 @@ type File struct {
} `json:"parentReference"` } `json:"parentReference"`
} }
func fileToObj(f File) *model.ObjThumbURL { type Object struct {
model.ObjThumbURL
ParentID string
}
func fileToObj(f File, parentID string) *Object {
thumb := "" thumb := ""
if len(f.Thumbnails) > 0 { if len(f.Thumbnails) > 0 {
thumb = f.Thumbnails[0].Medium.Url thumb = f.Thumbnails[0].Medium.Url
} }
return &model.ObjThumbURL{ return &Object{
Object: model.Object{ ObjThumbURL: model.ObjThumbURL{
ID: f.Id, Object: model.Object{
Name: f.Name, ID: f.Id,
Size: f.Size, Name: f.Name,
Modified: f.LastModifiedDateTime, Size: f.Size,
IsFolder: f.File == nil, Modified: f.LastModifiedDateTime,
IsFolder: f.File == nil,
},
Thumbnail: model.Thumbnail{Thumbnail: thumb},
Url: model.Url{Url: f.Url},
}, },
Thumbnail: model.Thumbnail{Thumbnail: thumb}, ParentID: parentID,
Url: model.Url{Url: f.Url},
} }
} }

View File

@ -147,14 +147,14 @@ func (d *Onedrive) GetFile(path string) (*File, error) {
return &file, err return &file, err
} }
func (d *Onedrive) upSmall(dstDir model.Obj, stream model.FileStreamer) error { func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) error {
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/content" url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/content"
data, err := io.ReadAll(stream) data, err := io.ReadAll(stream)
if err != nil { if err != nil {
return err return err
} }
_, err = d.Request(url, http.MethodPut, func(req *resty.Request) { _, err = d.Request(url, http.MethodPut, func(req *resty.Request) {
req.SetBody(data) req.SetBody(data).SetContext(ctx)
}, nil) }, nil)
return err return err
} }
@ -185,6 +185,10 @@ func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.Fil
return err return err
} }
req, err := http.NewRequest("PUT", uploadUrl, bytes.NewBuffer(byteData)) req, err := http.NewRequest("PUT", uploadUrl, bytes.NewBuffer(byteData))
if err != nil {
return err
}
req = req.WithContext(ctx)
req.Header.Set("Content-Length", strconv.Itoa(int(byteSize))) req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())) req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
finish += byteSize finish += byteSize

View File

@ -35,15 +35,10 @@ func (d *PikPak) Config() driver.Config {
} }
func (d *PikPak) GetAddition() driver.Additional { func (d *PikPak) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *PikPak) Init(ctx context.Context, storage model.Storage) error { func (d *PikPak) Init(ctx context.Context) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
return d.login() return d.login()
} }
@ -194,7 +189,7 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
Key: &key, Key: &key,
Body: tempFile, Body: tempFile,
} }
_, err = uploader.Upload(input) _, err = uploader.UploadWithContext(ctx, input)
return err return err
} }

View File

@ -17,10 +17,8 @@ var config = driver.Config{
DefaultRoot: "", DefaultRoot: "",
} }
func New() driver.Driver {
return &PikPak{}
}
func init() { func init() {
op.RegisterDriver(config, New) op.RegisterDriver(func() driver.Driver {
return &PikPak{}
})
} }

View File

@ -0,0 +1,81 @@
package pikpak_share
import (
"context"
"net/http"
"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 PikPakShare struct {
model.Storage
Addition
RefreshToken string
AccessToken string
PassCodeToken string
}
func (d *PikPakShare) Config() driver.Config {
return config
}
func (d *PikPakShare) GetAddition() driver.Additional {
return &d.Addition
}
func (d *PikPakShare) Init(ctx context.Context) error {
err := d.login()
if err != nil {
return err
}
if d.SharePwd != "" {
err = d.getSharePassToken()
if err != nil {
return err
}
}
return nil
}
func (d *PikPakShare) Drop(ctx context.Context) error {
return nil
}
func (d *PikPakShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles(dir.GetID())
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
return fileToObj(src), nil
})
}
func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var resp ShareResp
query := map[string]string{
"share_id": d.ShareId,
"file_id": file.GetID(),
"pass_code_token": d.PassCodeToken,
}
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/share/file_info", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return nil, err
}
link := model.Link{
URL: resp.FileInfo.WebContentLink,
}
if len(resp.FileInfo.Medias) > 0 && resp.FileInfo.Medias[0].Link.Url != "" {
log.Debugln("use media link")
link.URL = resp.FileInfo.Medias[0].Link.Url
}
return &link, nil
}
var _ driver.Driver = (*PikPakShare)(nil)

View File

@ -0,0 +1,27 @@
package pikpak_share
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootID
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
ShareId string `json:"share_id" required:"true"`
SharePwd string `json:"share_pwd"`
}
var config = driver.Config{
Name: "PikPakShare",
LocalSort: true,
NoUpload: true,
DefaultRoot: "",
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &PikPakShare{}
})
}

View File

@ -0,0 +1,80 @@
package pikpak_share
import (
"strconv"
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type RespErr struct {
ErrorCode int `json:"error_code"`
Error string `json:"error"`
}
type ShareResp struct {
ShareStatus string `json:"share_status"`
ShareStatusText string `json:"share_status_text"`
FileInfo File `json:"file_info"`
Files []File `json:"files"`
NextPageToken string `json:"next_page_token"`
PassCodeToken string `json:"pass_code_token"`
}
type File struct {
Id string `json:"id"`
ShareId string `json:"share_id"`
Kind string `json:"kind"`
Name string `json:"name"`
ModifiedTime time.Time `json:"modified_time"`
Size string `json:"size"`
ThumbnailLink string `json:"thumbnail_link"`
WebContentLink string `json:"web_content_link"`
Medias []Media `json:"medias"`
}
func fileToObj(f File) *model.ObjThumb {
size, _ := strconv.ParseInt(f.Size, 10, 64)
return &model.ObjThumb{
Object: model.Object{
ID: f.Id,
Name: f.Name,
Size: size,
Modified: f.ModifiedTime,
IsFolder: f.Kind == "drive#folder",
},
Thumbnail: model.Thumbnail{
Thumbnail: f.ThumbnailLink,
},
}
}
type Media struct {
MediaId string `json:"media_id"`
MediaName string `json:"media_name"`
Video struct {
Height int `json:"height"`
Width int `json:"width"`
Duration int `json:"duration"`
BitRate int `json:"bit_rate"`
FrameRate int `json:"frame_rate"`
VideoCodec string `json:"video_codec"`
AudioCodec string `json:"audio_codec"`
VideoType string `json:"video_type"`
} `json:"video"`
Link struct {
Url string `json:"url"`
Token string `json:"token"`
Expire time.Time `json:"expire"`
} `json:"link"`
NeedMoreQuota bool `json:"need_more_quota"`
VipTypes []interface{} `json:"vip_types"`
RedirectLink string `json:"redirect_link"`
IconLink string `json:"icon_link"`
IsDefault bool `json:"is_default"`
Priority int `json:"priority"`
IsOrigin bool `json:"is_origin"`
ResolutionName string `json:"resolution_name"`
IsVisible bool `json:"is_visible"`
Category string `json:"category"`
}

View File

@ -0,0 +1,154 @@
package pikpak_share
import (
"errors"
"net/http"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/op"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
)
// do others that not defined in Driver interface
func (d *PikPakShare) login() error {
url := "https://user.mypikpak.com/v1/auth/signin"
var e RespErr
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
"captcha_token": "",
"client_id": "YNxT9w7GMdWvEOKa",
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
"username": d.Username,
"password": d.Password,
}).Post(url)
if err != nil {
return err
}
if e.ErrorCode != 0 {
return errors.New(e.Error)
}
data := res.Body()
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
return nil
}
func (d *PikPakShare) refreshToken() error {
url := "https://user.mypikpak.com/v1/auth/token"
var e RespErr
res, err := base.RestyClient.R().SetError(&e).
SetHeader("user-agent", "").SetBody(base.Json{
"client_id": "YNxT9w7GMdWvEOKa",
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
"grant_type": "refresh_token",
"refresh_token": d.RefreshToken,
}).Post(url)
if err != nil {
d.Status = err.Error()
op.MustSaveDriverStorage(d)
return err
}
if e.ErrorCode != 0 {
if e.ErrorCode == 4126 {
// refresh_token invalid, re-login
return d.login()
}
d.Status = e.Error
op.MustSaveDriverStorage(d)
return errors.New(e.Error)
}
data := res.Body()
d.Status = "work"
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
op.MustSaveDriverStorage(d)
return nil
}
func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
var e RespErr
req.SetError(&e)
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
if e.ErrorCode != 0 {
if e.ErrorCode == 16 {
// login / refresh token
err = d.refreshToken()
if err != nil {
return nil, err
}
return d.request(url, method, callback, resp)
}
return nil, errors.New(e.Error)
}
return res.Body(), nil
}
func (d *PikPakShare) getSharePassToken() error {
query := map[string]string{
"share_id": d.ShareId,
"pass_code": d.SharePwd,
"thumbnail_size": "SIZE_LARGE",
"limit": "100",
}
var resp ShareResp
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/share", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return err
}
d.PassCodeToken = resp.PassCodeToken
return nil
}
func (d *PikPakShare) getFiles(id string) ([]File, error) {
res := make([]File, 0)
pageToken := "first"
for pageToken != "" {
if pageToken == "first" {
pageToken = ""
}
query := map[string]string{
"parent_id": id,
"share_id": d.ShareId,
"thumbnail_size": "SIZE_LARGE",
"with_audit": "true",
"limit": "100",
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
"page_token": pageToken,
"pass_code_token": d.PassCodeToken,
}
var resp ShareResp
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/share/detail", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
return nil, err
}
if resp.ShareStatus != "OK" {
if resp.ShareStatus == "PASS_CODE_EMPTY" || resp.ShareStatus == "PASS_CODE_ERROR" {
err = d.getSharePassToken()
if err != nil {
return nil, err
}
return d.getFiles(id)
}
return nil, errors.New(resp.ShareStatusText)
}
pageToken = resp.NextPageToken
res = append(res, resp.Files...)
}
return res, nil
}

View File

@ -29,16 +29,11 @@ func (d *Quark) Config() driver.Config {
} }
func (d *Quark) GetAddition() driver.Additional { func (d *Quark) GetAddition() driver.Additional {
return d.Addition return &d.Addition
} }
func (d *Quark) Init(ctx context.Context, storage model.Storage) error { func (d *Quark) Init(ctx context.Context) error {
d.Storage = storage _, err := d.request("/config", http.MethodGet, nil, nil)
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
_, err = d.request("/config", http.MethodGet, nil, nil)
return err return err
} }
@ -56,11 +51,6 @@ func (d *Quark) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
}) })
} }
//func (d *Quark) Get(ctx context.Context, path string) (model.Obj, error) {
// // TODO this is optional
// return nil, errs.NotImplement
//}
func (d *Quark) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *Quark) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
data := base.Json{ data := base.Json{
"fids": []string{file.GetID()}, "fids": []string{file.GetID()},
@ -185,10 +175,13 @@ func (d *Quark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
var bytes []byte var bytes []byte
md5s := make([]string, 0) md5s := make([]string, 0)
defaultBytes := make([]byte, partSize) defaultBytes := make([]byte, partSize)
left := stream.GetSize() total := stream.GetSize()
left := total
partNumber := 1 partNumber := 1
sizeDivide100 := stream.GetSize() / 100
for left > 0 { for left > 0 {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
if left > int64(partSize) { if left > int64(partSize) {
bytes = defaultBytes bytes = defaultBytes
} else { } else {
@ -198,9 +191,9 @@ func (d *Quark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
if err != nil { if err != nil {
return err return err
} }
left -= int64(partSize) left -= int64(len(bytes))
log.Debugf("left: %d", left) log.Debugf("left: %d", left)
m, err := d.upPart(pre, stream.GetMimetype(), partNumber, bytes) m, err := d.upPart(ctx, pre, stream.GetMimetype(), partNumber, bytes)
//m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account, md5Str, sha1Str) //m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account, md5Str, sha1Str)
if err != nil { if err != nil {
return err return err
@ -210,7 +203,7 @@ func (d *Quark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
} }
md5s = append(md5s, m) md5s = append(md5s, m)
partNumber++ partNumber++
up(100 - int(left/sizeDivide100)) up(int(100 * (total - left) / total))
} }
err = d.upCommit(pre, md5s) err = d.upCommit(pre, md5s)
if err != nil { if err != nil {

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