Compare commits

...

355 Commits

Author SHA1 Message Date
e1ccc0b215 chore: Merge pull request #1093 from Xhofe/dev
2.5.2
2022-05-13 17:39:26 +08:00
87e339850d fix(ftp): remove dir (#1082) 2022-05-13 17:38:22 +08:00
79c9b6ac77 chore: Merge pull request #1090 from foxxorcat/dev (#1090) 2022-05-13 13:54:04 +08:00
aeb2297f1f perf(123):file thumbnail 2022-05-12 22:27:32 +08:00
3b59bb5c09 perf(123):upload 2022-05-12 21:39:55 +08:00
bc4bac921f chore: Merge pull request #1089 from foxxorcat/dev
修复迅雷一些已知问题
2022-05-12 20:42:49 +08:00
f917882a84 perf(xunlei):upload 2022-05-12 19:18:28 +08:00
6a67d1cf69 fix(xunlei):check captchaToken 2022-05-12 19:15:39 +08:00
041b3587bf fix(xunlei):turn page 2022-05-12 13:27:49 +08:00
0eef7a129c fix(xunlei):the verification code cannot be obtained from the mobile phone number or email 2022-05-12 13:26:12 +08:00
4b635f06e3 fix(189pc): delete user-agent for upload 2022-05-11 20:01:15 +08:00
279111a8e2 chore: Merge pull request #1079 from foxxorcat/dev 2022-05-10 22:35:59 +08:00
67674835da fix(alidriver):fast upload file is not close 2022-05-10 21:54:44 +08:00
732e9eb1c3 feat:add pprof 2022-05-10 21:40:43 +08:00
b6af9aa587 fix(139,189,189pc,alidrive,onedrive,yandex):http response body is not close #1072 2022-05-10 21:37:48 +08:00
a9027c0f06 fix(baidu.photo):update download api 2022-05-10 20:35:19 +08:00
d780fa18a5 fix(sftp): error while has no files(close #1078) 2022-05-10 18:18:06 +08:00
d5626d6e2f fix: cancel QueryEscape Disposition (close #1074) 2022-05-10 18:16:32 +08:00
52dcbfe1a4 fix(xunlei):missing x-client-id error in some user requests 2022-05-09 18:14:52 +08:00
bf0ee3d315 refactor(baidu.photo): add a file api of download 2022-05-09 12:43:51 +08:00
0237e78c1e chore: Merge branch 'dev' into v2 2022-05-08 14:30:01 +08:00
44b8c6abf7 fix(189): typo 2022-05-08 14:28:26 +08:00
33e1acd344 chore: Merge pull request #1060 from Xhofe/dev
Dev 2.5.1
2022-05-08 14:26:27 +08:00
c54cb61f14 chore: add debug info 2022-05-08 14:25:37 +08:00
734b204709 chore: change ocr api 2022-05-08 14:22:07 +08:00
b7d9c5e4ff chore: Merge pull request #1059 from foxxorcat/dev (close #1053)
fix baidu.photo and xunlei
2022-05-08 13:59:58 +08:00
e698b457b9 fix(baidu.photo):windows path error 2022-05-08 13:37:56 +08:00
5258c21656 fix(xunlei):login error 2022-05-08 13:36:36 +08:00
1ca9a3d14e chore: Merge pull request #1052 from Xhofe/dev
docs: add baidu.photo
2022-05-07 16:47:22 +08:00
f23bec9a35 docs: add baidu.photo [skip ci] 2022-05-07 16:43:02 +08:00
62a1acd1f4 chore: Merge pull request #1051 from Xhofe/all-contributors/add-WntFlm
docs: add WntFlm as a contributor for code
2022-05-07 16:38:33 +08:00
fa6e3fe567 docs: update .all-contributorsrc [skip ci] 2022-05-07 08:38:10 +00:00
b71b62ee35 docs: update CONTRIBUTORS.md [skip ci] 2022-05-07 08:38:09 +00:00
410b4939a4 chore: Merge pull request #1050 from Xhofe/all-contributors/add-ericarena
docs: add ericarena as a contributor for code
2022-05-07 16:37:24 +08:00
62c0071f29 docs: update .all-contributorsrc [skip ci] 2022-05-07 08:36:58 +00:00
f043a41005 docs: update CONTRIBUTORS.md [skip ci] 2022-05-07 08:36:57 +00:00
2e9da57036 chore: Merge pull request #1048 from Xhofe/all-contributors/add-Windman1320
docs: add Windman1320 as a contributor for code
2022-05-07 16:36:17 +08:00
d83cd37984 docs: update .all-contributorsrc [skip ci] 2022-05-07 08:35:50 +00:00
bad8b0ebbb docs: update CONTRIBUTORS.md [skip ci] 2022-05-07 08:35:49 +00:00
4535e65948 chore: Merge pull request #1047 from Xhofe/dev
Dev 2.5.0
2022-05-07 16:29:10 +08:00
3b413c2ee2 chore: Merge pull request #1021 from WntFlm/mimefix
fix(webdav): empty mimeType
2022-05-01 19:13:29 +08:00
427ae56333 chore: Merge pull request #1020 from foxxorcat/dev
fix(xunlei):download link speed limit
2022-05-01 13:25:54 +08:00
658fd5ad6e fix(webdav): empty mimeType
Now mimeType will always be a non-empty string, by defaulting it to "application/octet-stream".
2022-05-01 09:42:25 +08:00
11830bb51c fix(xunlei):download link speed limit 2022-04-30 21:41:15 +08:00
75c98429bf fix(webdav): wrong MIMEType (close #1007) 2022-04-29 14:09:51 +08:00
f77ea1b3a5 chore: Merge pull request #1011 from foxxorcat/dev
增加一刻相册支持,优化迅雷代码
2022-04-29 14:08:04 +08:00
0a8bd96d33 feat: support baidu.photo 2022-04-28 23:44:22 +08:00
68f37fc11f refactor(xunlei): optimized code 2022-04-28 23:15:37 +08:00
d6775cda69 fix(123): can't delete folder (close #1009) 2022-04-28 21:17:11 +08:00
43c6e07bac feat: add aria2 download settings(#1000)
* feat: add aria2 support

在右键菜单中增加了使用aria2下载的item,可以直接发送选中的文件链接到aria2,省略复制再粘贴到aria2的步骤

* feat: set default value for `Aria2 RPC url`

Co-authored-by: Xhofe <i@nn.ci>
2022-04-28 18:05:07 +08:00
4901e9080c fix(quark): file size over i32 (close #997) 2022-04-26 15:22:39 +08:00
48049a5ea3 docs: upgrade golang version [skip ci] 2022-04-25 16:05:49 +08:00
bd7260f0ff chore: base for template 2022-04-24 21:22:24 +08:00
6c0d54394f chore: Merge pull request #992 from Xhofe/dev
Dev 2.4.3
2022-04-24 17:40:26 +08:00
ce5dacbf3f build: build musl first 2022-04-24 17:39:25 +08:00
08aaa5e2c0 build: rm .git before xgo 2022-04-24 16:53:47 +08:00
42c0e438d5 fix(webdav): sharepoint upload 2022-04-24 15:38:17 +08:00
e4df146043 fix(webdav): sharepoint repeat login 2022-04-24 15:37:59 +08:00
27b7dae113 feat(webdav): support range get 2022-04-23 22:43:02 +08:00
293d574ce7 build: specify xgo version 2022-04-23 16:53:26 +08:00
56b3b35556 chore: Merge pull request #984 from Xhofe/dev
2.4.2
2022-04-21 22:34:12 +08:00
a7a0e85a46 docs: update qq group 2022-04-21 22:31:17 +08:00
95c0106fdd feat(onedrive): default redirect_uri(close #967) 2022-04-20 15:21:48 +08:00
6612338fc1 fix(189pc): InvalidSessionKey (fix #920) 2022-04-20 15:16:30 +08:00
c276a1541f chore: delete useless comment 2022-04-18 18:32:29 +08:00
cc96a5bbdb chore: add windows bin to gitignore 2022-04-18 18:31:10 +08:00
0810561a8a fix(xunlei): check err prevent stack overflow 2022-04-18 18:29:21 +08:00
82a5c43b94 chore: Merge pull request #961 from Xhofe/dev
Dev v2.4.1
2022-04-17 23:12:02 +08:00
d38f36ef44 chore: delete useless test file 2022-04-17 22:50:54 +08:00
f9533440c7 build: cancel static link for glibc 2022-04-17 22:50:28 +08:00
41a186b051 fix(native): set size of folder to 0 2022-04-17 21:12:55 +08:00
4e6a44253c chore: Merge pull request #958 from Xhofe/dev
Dev 2.4.0
2022-04-17 17:29:02 +08:00
ebda77cd43 docs: add sharepoint to webdav 2022-04-17 17:16:39 +08:00
1a1e86521f fix(quark): lost files while number of files > 100 (fix #947) 2022-04-16 21:06:33 +08:00
1b4740dae3 fix: file deduplication (fix #941) 2022-04-16 17:28:16 +08:00
91fc8df84e build: cancel static link for darwin 2022-04-16 17:08:48 +08:00
e6ecf1fa30 feat(189): add tips get page 2022-04-16 17:08:21 +08:00
183a6f1b3a build: static link for compile 2022-04-16 16:55:55 +08:00
3c2d59e272 build: use crazymax/xgo 2022-04-16 16:43:21 +08:00
fd80e3eaf7 build: Use -buildvcs=false to disable VCS stamping 2022-04-16 15:23:21 +08:00
4928c331a8 build: upgrade go version 2022-04-16 15:04:28 +08:00
3ad75e54cb refactor(baidu): add a crack api of download
* 修复百度网盘API文件大于20M问题

* refactor: keep the official api

Co-authored-by: Xhofe <i@nn.ci>
2022-04-16 14:52:36 +08:00
a2cf3ab42e workflow: add checkboxes for issue template 2022-04-14 22:16:15 +08:00
e24814ee2f chore: Merge pull request #938 from Xhofe/feature/search
Feature/search
2022-04-13 21:57:59 +08:00
37b42e6e17 fix(sftp): add port 2022-04-12 20:22:09 +08:00
30ebb0f4d4 feat: support other region sharepoint with webdav 2022-04-12 09:54:22 +08:00
8e059c64b5 feat: webdav for sharepoint online (#460) 2022-04-11 21:32:38 +08:00
395de069c2 fix: extract_folder causes sorting confusion (close #929) 2022-04-11 16:50:47 +08:00
4c22f37d54 fix(search): file type 2022-04-08 22:47:04 +08:00
a73a40133d feat: search api 2022-04-08 22:03:26 +08:00
6591af58ea feat: store search file index 2022-04-08 21:51:21 +08:00
58568d4ef6 fix(189cloud): remove empty Authorization 2022-04-07 16:56:50 +08:00
5295593bf8 fix(189cloudpc): wrong modified time (close #910) 2022-04-06 17:59:45 +08:00
24d031d578 feat: clear temp file while start 2022-04-06 16:24:31 +08:00
7141bf0358 build: static compilation for musl 2022-04-06 16:19:15 +08:00
c5d707cf0a fix: multilevel virtual path (close #904) 2022-04-06 15:23:10 +08:00
dfcf66b43e fix(native): set size of folder to 0 2022-04-06 15:11:03 +08:00
fa6ee62cf0 feat: global readme url 2022-04-05 20:17:27 +08:00
1428d90361 feat: meta readme 2022-04-05 20:17:16 +08:00
c413c22201 fix(quark): denied by Referer ACL 2022-04-04 20:55:48 +08:00
9b6adecd62 feat: sharepoint webdav (unfinished) 2022-04-04 20:55:22 +08:00
b3540cf539 docs: add SFTP in readme [skip ci] 2022-04-03 18:20:33 +08:00
f8650c9c0b fix(webdav): remove default Authorization header (close #893) 2022-04-03 18:19:15 +08:00
bf2e5768d6 feat: add rapid upload switch for 189pc and alidrive (#892)
* 189PC增加快传开关

* alidrive增加快传开关
2022-04-03 17:56:21 +08:00
18c82e79b5 feat: sftp support 2022-04-02 19:28:43 +08:00
d69d24a5b2 fix(alidrive): judge status of delete folder (close #886) 2022-04-02 14:34:34 +08:00
342729179d chore: Merge pull request #884 from Xhofe/dev
fix: some issues of webdav due to virtual path
2022-04-01 22:02:39 +08:00
0537449335 fix(webdav): virtual path no account 2022-04-01 21:57:55 +08:00
df90311453 fix(webdav): alist path not found 2022-04-01 20:40:57 +08:00
876579ea3b chore: Merge pull request #874 from Xhofe/dev
support mount to root path
2022-04-01 09:42:37 +08:00
e83081380e workflow: cancel build docker for pr 2022-04-01 09:40:53 +08:00
9daeaf7562 fix: virtual path, support mount to root path 2022-04-01 09:40:08 +08:00
e6be11c17f chore: Merge pull request #873 from Xhofe/dev
fix: load balance
2022-03-31 22:04:37 +08:00
b52e1e8be3 fix: load balance 2022-03-31 21:52:19 +08:00
948bbe9136 chore: Merge pull request #872 from Xhofe/dev
fix quark cookie, virtual path
2022-03-31 20:58:56 +08:00
ced61da33a feat: virtual path 2022-03-31 20:43:17 +08:00
a0f4383d41 feat(quark): set status 2022-03-30 14:06:50 +08:00
5a527dfa2c feat(xunlei): set timeout 2022-03-30 00:18:20 +08:00
49fc475f9f feat(s3): create placeholder file for mkdir 2022-03-30 00:18:00 +08:00
83c377270e fix(webdav): add sign for webdav proxy 2022-03-29 16:34:22 +08:00
7ffaef0de6 fix: audio and video types 2022-03-28 21:53:57 +08:00
dd151480a8 fix(alidrive): change response of move and copy 2022-03-28 21:51:24 +08:00
ad3121d367 fix(quark): __puus expired (close #830) 2022-03-28 21:38:05 +08:00
c1525ebc69 feat: cookie operate util 2022-03-28 21:10:20 +08:00
30277cd81f docs: change blog address [skip ci] 2022-03-27 20:23:18 +08:00
466ec27ffe chore: Merge pull request #825 from Xhofe/dev
docs: add disclaimer
2022-03-27 20:17:36 +08:00
85c757b035 docs: add disclaimer 2022-03-27 20:16:06 +08:00
712687370a chore: Merge pull request #823 from Xhofe/dev
fix some issues
2022-03-26 23:54:17 +08:00
b68ba22df3 workflow: issue invalid bot 2022-03-26 23:51:25 +08:00
d9652e2a0b fix(189cloud): link force https (close #821) 2022-03-26 21:59:18 +08:00
a5b757b251 feat: customize audio/video types (close #819) 2022-03-26 17:10:37 +08:00
0bc05a60b0 feat(189pc): override upload 2022-03-23 18:51:07 +08:00
db275f885a fix: DProxyTypes judge 2022-03-22 19:53:26 +08:00
9e483d902f feat: adapt postgres (close #740) 2022-03-22 16:41:38 +08:00
801f843f8a workflow: reproduction is required [skip ci] 2022-04-05 03:24:51 +08:00
77ffb93cbe feat: multiple down proxy urls (close #793) 2022-03-20 16:53:30 +08:00
bf73ea7f5d chore: Merge pull request #787 from Xhofe/dev
fix some issues of webdav
2022-03-19 14:57:38 +08:00
9b23d0ab29 docs: add sponsors [skip ci] 2022-03-18 17:08:43 +08:00
908cdd2c78 revert: undo delete upFileMap 2022-03-17 21:57:54 +08:00
f4f61a5787 fix(webdav): nil pointer error (close #749) 2022-03-17 21:23:10 +08:00
6db09a2736 fix: xunlei upload error (#749) 2022-03-17 21:13:13 +08:00
b21801d505 fix: clear cookie for 189 cloud login 2022-03-16 18:02:11 +08:00
2dbedc245c fix: 189 family cloud upload (#761) 2022-03-16 14:22:42 +08:00
58426613f6 feat: add tls config for mysql (fix #758) 2022-03-15 17:05:54 +08:00
ef19e851e3 fix: check local ip for 123pan 2022-03-15 14:48:39 +08:00
5a1b16a601 feat: set overwrite for aliyundrive upload 2022-03-14 22:43:27 +08:00
4eef9cd9bc fix: nil pointer while delete baidu account (close #751) 2022-03-14 20:40:42 +08:00
79b5c018ea workflow: add translation for duplicate issue [skip ci] 2022-03-14 18:09:38 +08:00
15651a4356 feat: only show files (close #735) 2022-03-13 19:37:58 +08:00
7be476cce0 fix: wrong dockerfile 2022-03-13 19:00:19 +08:00
bb017c5f6d feat: remove env prefix for docker 2022-03-13 17:01:45 +08:00
c51dc4594d chore: add tips for announcement 2022-03-13 16:46:06 +08:00
8e30b02efc fix: cache config env typo 2022-03-13 16:38:12 +08:00
0aa438dce4 feat: add announcement setting 2022-03-12 21:09:33 +08:00
9c2fc8e860 feat: read config from environment 2022-03-12 20:38:22 +08:00
b1d7a980d9 feat: echo password while start every time 2022-03-12 00:24:55 +08:00
19d0a88b55 fix: cookie lanzou file with password 2022-03-11 19:48:32 +08:00
40567dee0e fix: lanzou url password 2022-03-11 19:16:21 +08:00
4b540a2297 feat: skip creating an existing folder 2022-03-11 18:12:13 +08:00
8a62d55efe feat(google): add default client 2022-03-10 20:08:10 +08:00
10fce6c0fe fix(xunlei): some issues about page turning(#716) 2022-03-09 22:48:15 +08:00
d31d49a9bb fix(189pc): some minor issues 2022-03-09 21:09:21 +08:00
2e91f5ffa5 feat: support 189 family cloud (close #612) 2022-03-09 20:30:56 +08:00
8f19c45a81 feat: pikpak video use media link 2022-03-09 15:11:12 +08:00
c63e05983d fix: 189cloud big file download (close #683) 2022-03-07 15:04:20 +08:00
678a982535 feat: add sleep for lanzou request (close #690) 2022-03-07 14:36:25 +08:00
b2c02e6c5e feat: add driver template 2022-03-06 21:33:58 +08:00
7e05b0317f chore: Merge pull request #689 from Xhofe/dev
docs: add Contributing and move Contributors
2022-03-06 20:49:14 +08:00
19f06dfaed docs: fix typo [skip ci] 2022-03-06 20:44:37 +08:00
1680a18578 docs: move CONTRIBUTORS [skip ci] 2022-03-06 20:38:30 +08:00
e8f440ca5c docs: create CONTRIBUTING.md [skip ci] 2022-03-06 20:30:17 +08:00
7deff76f49 chore: Merge pull request #687 from Xhofe/all-contributors/add-Clansty
docs: add Clansty as a contributor for doc
2022-03-06 17:36:13 +08:00
cd0afb9536 docs: update .all-contributorsrc [skip ci] 2022-03-06 09:35:08 +00:00
668a953cd8 docs: update README_cn.md [skip ci] 2022-03-06 09:35:07 +00:00
8bfbaa74f6 docs: update README.md [skip ci] 2022-03-06 09:35:06 +00:00
3ccf5ee620 fix: ipa plist key 2022-03-05 15:33:04 +08:00
b44243c021 feat: ipa name decode 2022-03-05 15:13:19 +08:00
4ae81b5a79 feat: aliyundrive fast upload (#652) 2022-03-05 13:14:57 +08:00
189f4c19a5 release: release v2.1.1 2022-03-04 21:00:39 +08:00
92a3d74af5 chore: Merge pull request #675 from Xhofe/all-contributors/add-vg-land [skip ci]
docs: add vg-land as a contributor for code
2022-03-04 20:16:17 +08:00
bb73a10332 docs: update .all-contributorsrc [skip ci] 2022-03-04 12:14:55 +00:00
3baf1e8c7b docs: update README_cn.md [skip ci] 2022-03-04 12:14:54 +00:00
fdb49f5fb4 docs: update README.md [skip ci] 2022-03-04 12:14:53 +00:00
2eedcc1626 feat: opus preview (#638) 2022-03-04 10:05:15 +08:00
6faecbd5d8 feat: add cache for xunlei 2022-03-04 09:55:37 +08:00
34ed05c62f style: go mod tidy 2022-03-04 09:52:08 +08:00
ce83d6eb40 build: fix dev build 2022-03-04 09:50:47 +08:00
2271cb6c7c docs: replace preview image [skip ci] 2022-03-03 23:30:45 +08:00
a42b30c96e docs: update .all-contributorsrc [skip ci] 2022-03-03 23:28:37 +08:00
ce25d16222 docs: update README_cn.md [skip ci] 2022-03-03 23:28:37 +08:00
b392e093e3 docs: update README.md [skip ci] 2022-03-03 23:28:37 +08:00
0d5b7298db docs: update .all-contributorsrc [skip ci] 2022-03-03 23:27:19 +08:00
2063ebb74d docs: update README_cn.md [skip ci] 2022-03-03 23:27:19 +08:00
0408d7ab5d docs: update README.md [skip ci] 2022-03-03 23:27:19 +08:00
d52451f9d2 docs: update .all-contributorsrc [skip ci] 2022-03-03 23:24:46 +08:00
ca9f77006a docs: update README_cn.md [skip ci] 2022-03-03 23:24:46 +08:00
e8e8d925f3 docs: update README.md [skip ci] 2022-03-03 23:24:46 +08:00
623aab4c28 docs: move all contributors 2022-03-03 23:17:13 +08:00
3bc81d471e docs: create .all-contributorsrc [skip ci] 2022-03-03 23:00:34 +08:00
dfddb5cfa1 docs: update README.md [skip ci] 2022-03-03 23:00:34 +08:00
80f5bde0cb build: just upx linux/amd64 2022-03-03 19:44:13 +08:00
9de072161e docs: add xunlei cloud (#659) 2022-03-03 19:38:59 +08:00
d08a7440bc 🎇 import uss 2022-03-03 19:33:40 +08:00
7a4bb2496d add welcome bot 2022-03-03 19:16:30 +08:00
f68ab40d26 🔀 Merge pull request #659 from foxxorcat/dev
 xunleicloud support
2022-03-03 17:03:23 +08:00
796d490fb7 🐛 fix #658 onedrive file/folder judge 2022-03-03 16:01:24 +08:00
2964d5a6db xunleicloud support 2022-03-03 15:45:33 +08:00
90b57dacee 🎇 remove set Content-Type for native 2022-03-02 19:27:40 +08:00
6af17e2509 🔒 fix #645 xss vulnerability 2022-03-01 20:09:25 +08:00
5193b2aa7d 🎨 fix some warning 2022-02-27 20:28:42 +08:00
3f644f07db ✏️ fix readme logo 2022-02-27 20:26:05 +08:00
d988f98b81 💚 fix upx 2022-02-26 00:12:00 +08:00
10634c7b77 👷 add build for macos 2022-02-25 23:59:27 +08:00
135d505192 back to cgo sqlite3 2022-02-25 23:55:57 +08:00
3f2be8a6ca 🔧 change default assets path 2022-02-25 22:08:12 +08:00
79bef09ee7 🔀 Merge branch 'feature/upyun' into dev 2022-02-25 21:06:49 +08:00
3534f6afac 💚 fix cal md5 2022-02-24 23:07:35 +08:00
106c1d069c 💚 change build platform 2022-02-24 22:55:50 +08:00
8ed0afe80d 🎇 add unupx version 2022-02-24 22:42:15 +08:00
6a6e3944d5 🔀 Merge branch 'feature/purego' into dev 2022-02-24 16:26:20 +08:00
94d5b5e47e direct but proxy types 2022-02-24 16:25:17 +08:00
e61b0f8e34 🔥 remove cgo to pure go 2022-02-24 16:08:49 +08:00
f7fbe1de6c 👷 build for all branch 2022-02-23 20:17:50 +08:00
01de01630e 🔥 remove placeholder for uss 2022-02-23 20:16:57 +08:00
f9f92e2198 ✏️ fix typo 2022-02-23 20:13:52 +08:00
7d5f50b04a 👷 build for all branch 2022-02-23 20:12:21 +08:00
72b5d25e4c ✏️ fix label typo 2022-02-23 20:07:45 +08:00
cae7f36531 🎇 refresh one folder 2022-02-23 19:16:33 +08:00
aa79f49e25 🐛 fix #600 aliyundrive move file 2022-02-23 14:56:17 +08:00
b4ad301d53 🐛 fix #599 lanzou url without password 2022-02-23 11:18:51 +08:00
00ed54c4c9 upyun uss support 2022-02-23 11:07:19 +08:00
ffa52794db 🍺 change login http method 2022-02-22 15:57:39 +08:00
24058d0c36 💚 fix dev build 2022-02-21 21:47:40 +08:00
641ca67671 💚 fix web replace 2022-02-21 20:31:01 +08:00
52ee2e0a8b 🐛 close #581 teambition update time error 2022-02-21 17:20:05 +08:00
724fc7f37e 💚 fix build web 2022-02-20 16:46:47 +08:00
9d279b104b dynamic public path 2022-02-20 15:14:18 +08:00
eb61f70164 🐛 fix show balance account 2022-02-20 13:06:59 +08:00
b3a8201768 🐛 fix that only two accounts can be load balanced 2022-02-19 21:49:37 +08:00
185795954b 🔊 add reason of failed to auto migrate model 2022-02-19 17:25:50 +08:00
cc62cc99d2 🐛 fix plist ipa name 2022-02-18 19:01:30 +08:00
270349f37c 🐛 fix write status sequence 2022-02-18 18:58:02 +08:00
977888070a 🐛 close #558 fix local file can't download 2022-02-18 18:50:01 +08:00
192d0f2bf3 🐛 fix update can't start 2022-02-18 18:49:40 +08:00
f2ec7884ec 🐛 fix only proxy webdav_direct 2022-02-17 17:49:03 +08:00
815975a4d2 🐛 fix 139 delete dir 2022-02-17 17:37:45 +08:00
f96a0238fc 👷 change issues-month-statistics 2022-02-17 17:29:45 +08:00
f695bd0959 🔧 change bundle-version 2022-02-17 16:54:34 +08:00
efe8f46e17 ✏️ fix typo 2022-02-17 11:56:00 +08:00
515daa22a9 🐛 fix #551 add S3ForcePathStyle config 2022-02-17 11:30:34 +08:00
f11e22deaf 🚧 change base64 characters 2022-02-17 09:14:21 +08:00
5be976169f 🚧 fix bundle-identifier 2022-02-17 00:44:30 +08:00
a6e08f3bf4 📝 update readme 2022-02-17 00:08:37 +08:00
944e68a979 🚧 plist generate 2022-02-17 00:06:10 +08:00
b3a6e33ce1 🎇 quark support 2022-02-16 20:20:39 +08:00
cb53ddc8e8 🐛 fix nil pointer 2022-02-16 16:10:39 +08:00
693417be4f 🔧 add hide files setting item 2022-02-15 14:51:50 +08:00
5c3f91bb55 support empty password 2022-02-15 14:42:24 +08:00
8a219d0732 👷 add issue bot 2022-02-14 20:03:42 +08:00
146a544af3 ✏️ fix typo 2022-02-14 19:43:43 +08:00
48dccc6c0b 📝 update readme 2022-02-14 15:41:20 +08:00
ce1740cec4 ✏️ fix typo 2022-02-14 15:36:21 +08:00
d40dbeae3e 💬 add issue template 2022-02-14 15:33:48 +08:00
5094b673c4 👷 add some issue bot 2022-02-14 15:32:25 +08:00
228e6d10e7 🔧 close #519 customize temp dir 2022-02-14 15:06:57 +08:00
e90b979d15 close #535 request set timeout 2022-02-14 14:59:00 +08:00
fb05a6ca48 🐛 fix #533 only encode fileName 2022-02-14 14:31:09 +08:00
e055ed3afa 🎇 lanzou proxy add user-agent 2022-02-13 17:46:07 +08:00
4371c470b3 webdav direct proxy 2022-02-13 15:57:42 +08:00
7bb237d0ef 🐛 fix #527 189 upload file name contains % 2022-02-13 13:03:34 +08:00
5c42354b01 🐛 fix #527 189 upload name contains + 2022-02-12 20:27:38 +08:00
387e8af422 🐛 fix 189 upload while filename contains & 2022-02-12 13:08:46 +08:00
5dca777caf 🔒 fix baidu direct link 2022-02-12 12:04:10 +08:00
0814778a14 🐛 fix no account error while only one 2022-02-11 17:11:02 +08:00
6827af3997 🎇 close #522 hide account for guest webdav 2022-02-11 16:19:55 +08:00
435bdea8f7 🔧 #523 add some default setting 2022-02-11 16:17:07 +08:00
4f81735af6 🎇 close #512 favicon redirect 2022-02-08 18:07:13 +08:00
bef3d2f88d 🐛 Fix the temp folder is not created at the first startup 2022-02-08 16:22:53 +08:00
ba99c7dc03 🐛 fix multiple accounts with the same prefix cannot be load balanced 2022-02-08 16:02:47 +08:00
f5c5162a9b load balance 2022-02-08 15:51:58 +08:00
a22903533e 🐛 set random seed 2022-02-04 16:08:15 +08:00
86cda58b22 🚧 echo password 2022-02-04 14:58:48 +08:00
7804cf9d5c 🔒 random webdav admin password 2022-02-04 14:39:11 +08:00
2bb7036110 🔧 change logo 2022-02-03 21:28:13 +08:00
ba545555cf 📝 update readme 2022-02-03 21:00:08 +08:00
be55ca690c 🔧 change default logo 2022-02-03 20:43:33 +08:00
9013add749 🔒 random initial password 2022-02-03 12:27:50 +08:00
3201b6da76 🐛 fix #376 windows webdav upload 2022-02-02 18:48:34 +08:00
feb42f1f4b 🔥 delete proxy interface 2022-02-02 18:03:54 +08:00
6f14d0eb5c 🎇 baidu disk support 2022-02-02 17:32:11 +08:00
7530d8f5b2 🐛 fix lanzou for download change 2022-02-01 22:33:19 +08:00
e25fe05a53 yandex disk support 2022-02-01 17:15:11 +08:00
8e0ab8f780 🔥 remove onedrive refresh token cron 2022-02-01 15:20:17 +08:00
cb2a3c2b42 🎨 change proxy interface 2022-02-01 14:28:21 +08:00
1b6ec94f33 🐛 fix s3 custom host 2022-02-01 11:34:32 +08:00
cb23edc1fe 🐛 fix #462 check connect while get ftp client 2022-01-31 11:13:29 +08:00
6fd05d7d72 🐛 fix connMap not init 2022-01-30 00:55:12 +08:00
f26ac57569 🐛 fix ftp conn not store 2022-01-30 00:04:31 +08:00
2434ac54d0 🐛 fix webdav 2022-01-29 18:36:22 +08:00
f25b557327 📝 update readme 2022-01-29 15:31:06 +08:00
81a0706d01 189 chunk upload 2022-01-29 11:16:40 +08:00
5f6b576cbf 🔧 switch cdn to jsdelivr 2022-01-28 23:29:38 +08:00
549877f71e 🔥 Delete Text 2022-01-28 23:21:11 +08:00
c6a5ba9b91 🚧 189 chunk upload 2022-01-28 19:51:54 +08:00
1a69d80489 🎨 Pull away Path 2022-01-28 11:04:56 +08:00
b797f4302c 🎇 use tempFile to cal md5 2022-01-27 23:48:29 +08:00
bf9aa5c3d3 🔒 not allowed use relative path of native 2022-01-27 15:10:33 +08:00
7390e19a7a 🔒 not allowed down with relative path 2022-01-27 15:05:17 +08:00
b31a12a0cc 🔒 not allowed access using relative path for native 2022-01-27 14:54:20 +08:00
26ce001782 🎇 add ocr for 189 2022-01-27 12:34:49 +08:00
a2c7ff3262 ✏️ Invalid Token 2022-01-26 16:27:38 +08:00
8fc7c716c0 copy api 2022-01-26 14:07:51 +08:00
c70fc3fc4b finish teambition chunk upload 2022-01-26 14:05:35 +08:00
df513b7dc0 teambition upload (<= 20 MB) 2022-01-23 14:03:04 +08:00
2a9598f4c6 🐛 fix #407 189cloud use local sort 2022-01-21 19:01:33 +08:00
224c20779c 🔧 add webp to image types 2022-01-20 23:01:59 +08:00
5d722298cb 📝 update readme 2022-01-20 22:38:53 +08:00
4bcc6359e3 💚 fix docker build 2022-01-20 20:52:14 +08:00
4144afcc92 🐛 fix #397 139yun file size overflow int32 2022-01-20 20:35:01 +08:00
2ad27046fb 🎨 split build and docker 2022-01-20 20:30:58 +08:00
9516ac6718 🎇 finish move api 2022-01-20 19:58:25 +08:00
de638c7c36 🐛 fix 123pan move 2022-01-20 19:58:10 +08:00
c6b34a033b 🔧 add swf to image types 2022-01-20 14:40:59 +08:00
31de3399d2 💚 fix musl prebuilt 2022-01-20 14:23:31 +08:00
0dc2ca019f 💚 fix musl prebuilt 2022-01-20 13:47:21 +08:00
04724f7f0f 👷 add prebuilt for musl-libc 2022-01-20 12:53:49 +08:00
75a983a965 🐛 fix webdav can't get file with password 2022-01-19 18:46:32 +08:00
e12d8bb8ca ⬆️ upgrade gorm 2022-01-19 09:15:00 +08:00
68f1ccfed4 add sslmode for postgres 2022-01-19 09:14:31 +08:00
54272db59c 🚧 add folder api 2022-01-18 19:08:44 +08:00
6d34e88360 🎇 hide files while webdav visitor 2022-01-18 18:48:08 +08:00
0a901a2eb0 🐛 fix can't find zhimg for dev 2022-01-18 18:19:58 +08:00
e1671a0511 🚧 add move api 2022-01-18 16:13:07 +08:00
dcb4ec695f rename and mkdir api 2022-01-18 14:31:52 +08:00
4a21b6fe1d 🔥 close res.body for proxy 2022-01-18 11:29:32 +08:00
96a237902b 🐛 close #379 fix google drive can't get text file 2022-01-17 09:54:19 +08:00
cfb51e9f80 Extract folder 2022-01-16 16:38:41 +08:00
e952f1c243 🔥 Optimize 189 cloud get client 2022-01-16 16:05:32 +08:00
07d6ca27db 🐛 Forbid MediaTrack to create a new folder with the same name 2022-01-16 13:21:29 +08:00
8245da485a 🐛 fix s3 for #362 2022-01-15 20:24:57 +08:00
5c759217cf 🐛 fix some lanzou can't down #360 2022-01-15 20:10:42 +08:00
0648fdebc2 ♻️ solve circular dependency 2022-01-15 19:59:24 +08:00
ed670e528f 🐛 fix 139Yun error phone close #365 2022-01-15 19:44:22 +08:00
2473309a51 🎇 execute save while delete account 2022-01-15 19:36:37 +08:00
21ca2f11b7 🔧 change default config 2022-01-14 21:20:45 +08:00
ccaa28a323 🔧 change default cdn 2022-01-14 20:56:20 +08:00
fea8b376f8 🔧 change config 2022-01-14 20:10:35 +08:00
55d244b726 🐛 fix mediatrack can't mkdir #351 2022-01-14 18:19:58 +08:00
1640a52789 cancel hide file for admin #343 2022-01-14 18:00:47 +08:00
424ec10692 🐛 fix ftp download error 2022-01-13 22:56:07 +08:00
b472c2ee18 🔒 not allowed delete root folder 2022-01-13 21:23:27 +08:00
65a01251e9 🐛 fix proxy not set status 2022-01-13 20:09:11 +08:00
96be6bbbd1 🐛 fix #334 proxy video can't use range 2022-01-13 19:49:02 +08:00
6f7465aab7 🐛 fix 139Yun no user error 2022-01-13 18:01:54 +08:00
c9bc8227bb 💚 switch back to techknowlogick/xgo 2022-01-12 11:50:26 +08:00
173 changed files with 13200 additions and 1436 deletions

89
.all-contributorsrc Normal file
View File

@ -0,0 +1,89 @@
{
"files": [
"CONTRIBUTORS.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "Xhofe",
"name": "Xhofe",
"avatar_url": "https://avatars.githubusercontent.com/u/36558727?v=4",
"profile": "http://nn.ci",
"contributions": [
"code",
"ideas",
"doc"
]
},
{
"login": "foxxorcat",
"name": "foxxorcat",
"avatar_url": "https://avatars.githubusercontent.com/u/95907542?v=4",
"profile": "https://github.com/foxxorcat",
"contributions": [
"code"
]
},
{
"login": "DaoChen6",
"name": "道辰",
"avatar_url": "https://avatars.githubusercontent.com/u/63903027?v=4",
"profile": "https://www.iflu.cf/",
"contributions": [
"doc"
]
},
{
"login": "vg-land",
"name": "vg-land",
"avatar_url": "https://avatars.githubusercontent.com/u/16739728?v=4",
"profile": "https://vg-land.github.io/",
"contributions": [
"code"
]
},
{
"login": "Clansty",
"name": "凌莞~(=^▽^=)",
"avatar_url": "https://avatars.githubusercontent.com/u/18461360?v=4",
"profile": "https://c5y.moe",
"contributions": [
"doc"
]
},
{
"login": "Windman1320",
"name": "Windman",
"avatar_url": "https://avatars.githubusercontent.com/u/9999486?v=4",
"profile": "https://github.com/Windman1320",
"contributions": [
"code"
]
},
{
"login": "ericarena",
"name": "ericarena",
"avatar_url": "https://avatars.githubusercontent.com/u/4518927?v=4",
"profile": "https://github.com/ericarena",
"contributions": [
"code"
]
},
{
"login": "WntFlm",
"name": "WntFlm",
"avatar_url": "https://avatars.githubusercontent.com/u/34620278?v=4",
"profile": "https://github.com/WntFlm",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"projectName": "alist",
"projectOwner": "Xhofe",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
}

View File

@ -5,7 +5,16 @@ body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Thanks for taking the time to fill out this bug report, please **confirm that your issue is not a duplicate issue and not because of your operation or version issues**
感谢您花时间填写此错误报告,请**务必确认您的issue不是重复的且不是因为您的操作或版本问题**
- type: checkboxes
attributes:
label: Please make sure of the following things
description: You may select more than one, even select all.
options:
- label: I have read the [documentation](https://alist-doc.nn.ci).
- label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure it's due to `alist` and not something else(such as `Dependencies` or `Operational`).
- type: input
id: version
attributes:
@ -28,11 +37,11 @@ body:
Please provide a link to a repo that can reproduce the problem you ran into.
请提供能复现此问题的链接
validations:
required: false
required: true
- type: textarea
id: logs
attributes:
label: 日志 / Logs
label: Logs / 日志
description: |
Please copy and paste any relevant log output.
请复制粘贴错误日志,或者截图

View File

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

View File

@ -0,0 +1,33 @@
name: "Feature request"
description: Feature request
labels: ["enhancement: pending triage"]
body:
- type: checkboxes
attributes:
label: Please make sure of the following things
description: You may select more than one, even select all.
options:
- label: I have read the [documentation](https://alist-doc.nn.ci).
- label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure this feature is not implemented.
- label: I'm sure it's a reasonable and popular requirement.
- type: textarea
id: feature-description
attributes:
label: Description of the feature / 需求描述
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested solution / 实现思路
description: |
Solutions to achieve this requirement.
实现此需求的解决思路。
- type: textarea
id: additional-context
attributes:
label: Additional context / 附件
description: |
Any other context or screenshots about the feature request here, or information you find helpful.
相关的任何其他上下文或截图,或者你觉得有帮助的信息

21
.github/config.yml vendored Normal file
View File

@ -0,0 +1,21 @@
# Configuration for welcome - https://github.com/behaviorbot/welcome
# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
# Comment to be posted to on first time issues
newIssueWelcomeComment: >
Thanks for opening your first issue here! Be sure to follow the issue template!
# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: >
Thanks for opening this pull request! Please check out our contributing guidelines.
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request! We here at behavior bot are proud of you!
# It is recommend to include as many gifs and emojis as possible

View File

@ -2,16 +2,16 @@ name: build
on:
push:
branches: [ v2 ]
branches: [ '**' ]
pull_request:
branches: [ v2 ]
branches: [ '**' ]
jobs:
build:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [1.17]
go-version: [1.18]
name: Build
runs-on: ${{ matrix.platform }}
steps:
@ -28,13 +28,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
with:
ref: v2
path: alist
- name: Set up xgo
- name: Install dependencies
run: |
docker pull crazymax/xgo:latest
go install github.com/crazy-max/xgo@latest
docker pull techknowlogick/xgo:latest
go install src.techknowlogick.com/xgo@latest
sudo apt install upx
- name: Build
@ -49,40 +48,3 @@ jobs:
with:
name: artifact
path: alist/build
docker:
name: Docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: xhofe/alist
- name: Set up Node
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Build web
run: |
bash build.sh web
mv dist/* public
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x

44
.github/workflows/build_docker.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: build_docker
on:
push:
branches: [ v2 ]
jobs:
build_docker:
name: Docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: xhofe/alist
- name: Set up Node
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Build web
run: |
bash build.sh web
mv dist/* public
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x

View File

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

25
.github/workflows/issue_duplicate.yml vendored Normal file
View File

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

25
.github/workflows/issue_invalid.yml vendored Normal file
View File

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

View File

@ -0,0 +1,16 @@
name: Issue Month Statistics
on:
schedule:
- cron: "0 1 1 * *"
jobs:
month-statistics:
runs-on: ubuntu-latest
steps:
- name: month-statistics
uses: actions-cool/issues-month-statistics@v1
with:
count-lables: true
count-comments: true
emoji: 'eyes'

20
.github/workflows/issue_question.yml vendored Normal file
View File

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

25
.github/workflows/issue_wontfix.yml vendored Normal file
View File

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

View File

@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [1.17]
go-version: [1.18]
name: Release
runs-on: ${{ matrix.platform }}
steps:
@ -27,10 +27,10 @@ jobs:
persist-credentials: false
fetch-depth: 0
- name: Set up xgo
- name: Install upx
run: |
docker pull crazymax/xgo:latest
go install github.com/crazy-max/xgo@latest
docker pull techknowlogick/xgo:latest
go install src.techknowlogick.com/xgo@latest
sudo apt install upx
- name: Build
@ -44,40 +44,3 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: alist/build/compress/*
docker:
name: Docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: xhofe/alist
- name: Set up Node
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Build web
run: |
bash build.sh cdn
mv dist/* public
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x

45
.github/workflows/release_docker.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: release_docker
on:
push:
tags:
- '*'
jobs:
docker_release:
name: Docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: xhofe/alist
- name: Set up Node
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Build web
run: |
bash build.sh cdn
mv dist/* public
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: xhofe
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x

3
.gitignore vendored
View File

@ -22,8 +22,9 @@ dist/
# vendor/
bin/*
/alist
/alist.exe
*.json
public/index.html
public/*.html
public/assets/
public/public/
data/

105
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,105 @@
# Contributing
## Setup your machine
`alist` is written in [Go](https://golang.org/) and [React](https://reactjs.org/).
Prerequisites:
- [git](https://nodejs.org/zh-cn/)
- [Go 1.18+](https://golang.org/doc/install)
- [gcc](https://gcc.gnu.org/)
- [nodejs](https://nodejs.org/)
Clone `alist` and `alist-web` anywhere:
```shell
$ git clone https://github.com/Xhofe/alist.git
$ git clone https://github.com/Xhofe/alist-web.git
```
You should switch to the dev branch for development.
## Preview your change
### backend
```shell
$ go run alist.go
```
### frontend
```shell
$ yarn dev
```
## Create a commit
Commit messages should be well formatted, and to make that "standardized".
### Commit Message Format
Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
format that includes a **type**, a **scope** and a **subject**:
```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
The **header** is mandatory and the **scope** of the header is optional.
Any line of the commit message cannot be longer than 100 characters! This allows the message to be easier
to read on GitHub as well as in various git tools.
### Revert
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header
of the reverted commit.
In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit
being reverted.
### Type
Must be one of the following:
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **perf**: A code change that improves performance
* **test**: Adding missing or correcting existing tests
* **build**: Affects project builds or dependency modifications
* **revert**: Restore the previous commit
* **ci**: Continuous integration of related file modifications
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
* **release**: Release a new version
* **workflow**: Workflow related file modification
### Scope
The scope could be anything specifying place of the commit change. For example `$location`,
`$browser`, `$compile`, `$rootScope`, `ngHref`, `ngClick`, `ngView`, etc...
You can use `*` when the change affects more than a single scope.
### Subject
The subject contains succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end
### Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.
### Footer
The footer should contain any information about **Breaking Changes** and is also the place to
[reference GitHub issues that this commit closes](https://help.github.com/articles/closing-issues-via-commit-messages/).
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines.
The rest of the commit message is then used for this.
## Submit a pull request
Push your branch to your `alist` fork and open a pull request against the
`dev` branch.

32
CONTRIBUTORS.md Normal file
View File

@ -0,0 +1,32 @@
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://nn.ci"><img src="https://avatars.githubusercontent.com/u/36558727?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Xhofe</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=Xhofe" title="Code">💻</a> <a href="#ideas-Xhofe" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/Xhofe/alist/commits?author=Xhofe" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/foxxorcat"><img src="https://avatars.githubusercontent.com/u/95907542?v=4?s=100" width="100px;" alt=""/><br /><sub><b>foxxorcat</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=foxxorcat" title="Code">💻</a></td>
<td align="center"><a href="https://www.iflu.cf/"><img src="https://avatars.githubusercontent.com/u/63903027?v=4?s=100" width="100px;" alt=""/><br /><sub><b>道辰</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=DaoChen6" title="Documentation">📖</a></td>
<td align="center"><a href="https://vg-land.github.io/"><img src="https://avatars.githubusercontent.com/u/16739728?v=4?s=100" width="100px;" alt=""/><br /><sub><b>vg-land</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=vg-land" title="Code">💻</a></td>
<td align="center"><a href="https://c5y.moe"><img src="https://avatars.githubusercontent.com/u/18461360?v=4?s=100" width="100px;" alt=""/><br /><sub><b>凌莞~(=^▽^=)</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=Clansty" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Windman1320"><img src="https://avatars.githubusercontent.com/u/9999486?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Windman</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=Windman1320" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ericarena"><img src="https://avatars.githubusercontent.com/u/4518927?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ericarena</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=ericarena" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/WntFlm"><img src="https://avatars.githubusercontent.com/u/34620278?v=4?s=100" width="100px;" alt=""/><br /><sub><b>WntFlm</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=WntFlm" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@ -3,7 +3,7 @@ LABEL stage=go-builder
WORKDIR /app/
COPY ./ ./
RUN apk add --no-cache bash git go gcc musl-dev; \
sh build.sh docker
bash build.sh docker
FROM alpine:edge
LABEL MAINTAINER="i@nn.ci"
@ -11,4 +11,4 @@ VOLUME /opt/alist/data/
WORKDIR /opt/alist/
COPY --from=builder /app/bin/alist ./
EXPOSE 5244
CMD [ "./alist" ]
CMD [ "./alist", "-docker" ]

View File

@ -1,5 +1,5 @@
<div align="center">
<h3><a href="https://alist.nn.ci">Alist</a></h3>
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂Another file list program that supports multiple storage, powered by Gin and React.</em></p>
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/release/Xhofe/alist?style=flat-square" alt="latest version"></a>
<a href="https://github.com/Xhofe/alist/discussions"><img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936&style=flat-square" alt="discussions"></a>
@ -11,10 +11,9 @@
</a>
</div>
---
English | [中文](./README_cn.md)
English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md)
## Features
@ -22,7 +21,7 @@ English | [中文](./README_cn.md)
- [x] Local storage
- [x] [Aliyundrive](https://www.aliyundrive.com/)
- [x] OneDrive / Sharepoint ([global](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us)
- [x] [189cloud](https://cloud.189.cn)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com/)
- [x] [123pan](https://www.123pan.com/)
- [x] [Lanzou](https://pc.woozooo.com/)
@ -31,10 +30,16 @@ English | [中文](./README_cn.md)
- [x] [PikPak](https://www.mypikpak.com/)
- [x] [ShandianPan](https://shandianpan.com/)
- [x] [S3](https://aws.amazon.com/s3/)
- [x] WebDav
- [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
- [x] [Mediatrack](https://www.mediatrack.cn/)
- [x] [139yun](https://yun.139.com/) (Personal, Family)
- [x] [Yandex.Disk](https://disk.yandex.com/)
- [x] [Baidu Disk](http://pan.baidu.com/)
- [x] [Quark](https://pan.quark.cn)
- [x] [XunleiCloud](https://pan.xunlei.com/)
- [x] SFTP
- [x] [Baidu.Photo](https://photo.baidu.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
@ -50,26 +55,37 @@ English | [中文](./README_cn.md)
- [x] Cloudflare workers proxy
- [x] File/Folder package download
- [x] Support video list playback and subtitles(ass,srt,vtt)
- [x] Web upload(Can allow visitors to upload) and delete
- [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy
## Discussion
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports only.**
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports and feature request only.**
## Demo
Available at: <https://alist.nn.ci>.
![demo](https://inews.gtimg.com/newsapp_ls/0/14256614096/0)
![demo](https://store.heytapimage.com/cdo-portal/feedback/202202/20/b271627971e29f0c7c9d59935b6ef381.png)
## Document
<https://alist-doc.nn.ci/en/>
## Special sponsors
- [Find Resources - Aliyundrive Resource Search Engine](https://zhaoziyuan.la/)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
## License
The `AList` is open-source software licensed under the AGPL-3.0 license.
## Disclaimer
- This program is a free and open source project. It is designed to share files on the network disk, which is convenient for downloading and learning golang. Please abide by relevant laws and regulations when using it, and do not abuse it;
- This program is implemented by calling the official sdk/interface, without destroying the official interface behavior;
- This program only does 302 redirect/traffic forwarding, and does not intercept, store, or tamper with any user data;
- Before using this program, you should understand and bear the corresponding risks, including but not limited to account ban, download speed limit, etc., which is none of this program's business;
- If there is any infringement, please contact me by [email](mailto:i@nn.ci), and it will be dealt with in time.
---
> [@Blog](https://www.nn.ci/) · [@GitHub](https://github.com/Xhofe)
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@QQGroup](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)

View File

@ -1,5 +1,5 @@
<div align="center">
<h3><a href="https://alist.nn.ci">Alist</a></h3>
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂️一个支持多存储的文件列表程序,使用 Gin 和 React 。</em></p>
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/release/Xhofe/alist?style=flat-square" alt="latest version"></a>
<a href="https://github.com/Xhofe/alist/discussions"><img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936&style=flat-square" alt="discussions"></a>
@ -13,7 +13,7 @@
---
[English](./README.md) | 中文
[English](./README.md) | 中文 | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md)
## 支持
@ -21,7 +21,7 @@
- [x] 本地存储
- [x] [阿里云盘](https://www.aliyundrive.com/)
- [x] OneDrive / Sharepoint[国际版](https://www.office.com/), [世纪互联](https://portal.partner.microsoftonline.cn),de,us
- [x] [天翼云盘](https://cloud.189.cn)
- [x] [天翼云盘](https://cloud.189.cn) (个人云, 家庭云)
- [x] [GoogleDrive](https://drive.google.com/)
- [x] [123云盘](https://www.123pan.com/)
- [x] [蓝奏云](https://pc.woozooo.com/)
@ -30,10 +30,16 @@
- [x] [PikPak](https://www.mypikpak.com/)
- [x] [闪电盘](https://shandianpan.com/)
- [x] [S3](https://aws.amazon.com/cn/s3/)
- [x] WebDav
- [x] WebDav(支持无API的OneDrive/SharePoint)
- [x] Teambition[中国](https://www.teambition.com/ )[国际](https://us.teambition.com/ )
- [x] [分秒帧](https://www.mediatrack.cn/)
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
- [x] [Yandex.Disk](https://disk.yandex.com/)
- [x] [百度网盘](http://pan.baidu.com/)
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷云盘](https://pan.xunlei.com/)
- [x] SFTP
- [x] [一刻相册](https://photo.baidu.com/)
- [x] 部署方便,开箱即用
- [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览
@ -49,26 +55,37 @@
- [x] Cloudflare workers 中转
- [x] 文件/文件夹打包下载
- [x] 支持视频列表播放和字幕(ass,srt,vtt)
- [x] 网页上传(可以允许访客上传),删除
- [x] 网页上传(可以允许访客上传),删除,新建文件夹,重命名,移动,复制
## 讨论
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) **issue仅针对错误报告。**
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) **issue仅针对错误报告和功能请求。**
## 演示
<https://alist.nn.ci>
![演示](https://inews.gtimg.com/newsapp_ls/0/14256614096/0)
![演示](https://store.heytapimage.com/cdo-portal/feedback/202202/20/b271627971e29f0c7c9d59935b6ef381.png)
## 文档
<https://alist-doc.nn.ci/>
## 特别赞助
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.la/)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
## 许可
`AList` 是在 AGPL-3.0 许可下许可的开源软件。
## 免责声明
- 本程序为免费开源项目旨在分享网盘文件方便下载以及学习golang使用时请遵守相关法律法规请勿滥用
- 本程序通过调用官方sdk/接口实现,无破坏官方接口行为;
- 本程序仅做302重定向/流量转发,不拦截、存储、篡改任何用户数据;
- 在使用本程序之前你应了解并承担相应的风险包括但不限于账号被ban下载限速等与本程序无关
- 如有侵权,请通过[邮件](mailto:i@nn.ci)与我联系,会及时处理。
---
> [@Blog](https://www.nn.ci/) · [@GitHub](https://github.com/Xhofe)
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@QQ群](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)

View File

@ -12,7 +12,6 @@ import (
)
func Init() bool {
//bootstrap.InitLog()
bootstrap.InitConf()
bootstrap.InitCron()
bootstrap.InitModel()
@ -22,7 +21,7 @@ func Init() bool {
log.Errorf(err.Error())
return false
}
log.Infof("current password: %s", pass.Value)
fmt.Printf("your password: %s\n", pass.Value)
return false
}
server.InitIndex()
@ -34,7 +33,8 @@ func Init() bool {
func main() {
if conf.Version {
fmt.Printf("Built At: %s\nGo Version: %s\nAuthor: %s\nCommit ID: %s\nVersion: %s\n", conf.BuiltAt, conf.GoVersion, conf.GitAuthor, conf.GitCommit, conf.GitTag)
fmt.Printf("Built At: %s\nGo Version: %s\nAuthor: %s\nCommit ID: %s\nVersion: %s\nWebVersion: %s\n",
conf.BuiltAt, conf.GoVersion, conf.GitAuthor, conf.GitCommit, conf.GitTag, conf.WebTag)
return
}
if !Init() {

View File

@ -1,12 +0,0 @@
package main
import (
"fmt"
"net/url"
"testing"
)
func TestUrl(t *testing.T) {
s,_ := url.QueryUnescape("/ali/%E7%8C%AA%E5%A4%B4%E7%9A%84%E6%96%87%E4%BB%B6%5B%E5%98%BF%E5%98%BF%5D/%E9%82%B9%E9%82%B9%E7%9A%84%E6%96%87%E4%BB%B6/%E6%A1%8C%E9%9D%A2%E5%A3%81%E7%BA%B8/v2-e8f266ba17ae387eefed1cb22b2b5e4e_r.jpg")
fmt.Print(s)
}

View File

@ -3,6 +3,7 @@ package bootstrap
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/drivers/operate"
"github.com/Xhofe/alist/model"
log "github.com/sirupsen/logrus"
)
@ -19,7 +20,9 @@ func InitAccounts() {
if !ok {
log.Errorf("no [%s] driver", account.Type)
} else {
err := driver.Save(&accounts[i], nil)
log.Infof("start init account: [%s], type: [%s]", account.Name, account.Type)
//err := driver.Save(&accounts[i], nil)
err := operate.Save(driver, &accounts[i], nil)
if err != nil {
log.Errorf("init account [%s] error:[%s]", account.Name, err.Error())
} else {

View File

@ -3,8 +3,11 @@ package bootstrap
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/utils"
"github.com/caarlos0/env/v6"
log "github.com/sirupsen/logrus"
"io/ioutil"
"os"
"path/filepath"
)
// InitConf init config
@ -20,20 +23,19 @@ func InitConf() {
if !utils.WriteToJson(conf.ConfigFile, conf.Conf) {
log.Fatalf("failed to create default config file")
}
return
}
} else {
config, err := ioutil.ReadFile(conf.ConfigFile)
if err != nil {
log.Fatalf("reading config file error:%s", err.Error())
}
conf.Conf = new(conf.Config)
conf.Conf = conf.DefaultConfig()
err = utils.Json.Unmarshal(config, conf.Conf)
if err != nil {
log.Fatalf("load config error: %s", err.Error())
}
log.Debugf("config:%+v", conf.Conf)
// update config.json struct
confBody, err := utils.Json.Marshal(conf.Conf)
confBody, err := utils.Json.MarshalIndent(conf.Conf, "", " ")
if err != nil {
log.Fatalf("marshal config error:%s", err.Error())
}
@ -41,4 +43,29 @@ func InitConf() {
if err != nil {
log.Fatalf("update config struct error: %s", err.Error())
}
}
if !conf.Conf.Force {
confFromEnv()
}
err := os.RemoveAll(filepath.Join(conf.Conf.TempDir))
if err != nil {
log.Errorln("failed delete temp file:", err)
}
err = os.MkdirAll(conf.Conf.TempDir, 0700)
if err != nil {
log.Fatalf("create temp dir error: %s", err.Error())
}
log.Debugf("config: %+v", conf.Conf)
}
func confFromEnv() {
prefix := "ALIST_"
if conf.Docker {
prefix = ""
}
if err := env.Parse(conf.Conf, env.Options{
Prefix: prefix,
}); err != nil {
log.Fatalf("load config from env error: %s", err.Error())
}
}

View File

@ -12,6 +12,9 @@ func InitLog() {
log.SetLevel(log.DebugLevel)
log.SetReportCaller(true)
}
if conf.Password || conf.Version {
log.SetLevel(log.WarnLevel)
}
log.SetFormatter(&log.TextFormatter{
//DisableColors: true,
ForceColors: true,
@ -27,6 +30,7 @@ func init() {
flag.BoolVar(&conf.Debug, "debug", false, "start with debug mode")
flag.BoolVar(&conf.Version, "version", false, "print version info")
flag.BoolVar(&conf.Password, "password", false, "print current password")
flag.BoolVar(&conf.Docker, "docker", false, "is using docker")
flag.Parse()
InitLog()
}

View File

@ -50,8 +50,8 @@ func InitModel() {
}
case "mysql":
{
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
databaseConfig.User, databaseConfig.Password, databaseConfig.Host, databaseConfig.Port, databaseConfig.Name)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&tls=%s",
databaseConfig.User, databaseConfig.Password, databaseConfig.Host, databaseConfig.Port, databaseConfig.Name, databaseConfig.SslMode)
db, err := gorm.Open(mysql.Open(dsn), gormConfig)
if err != nil {
log.Fatalf("failed to connect database:%s", err.Error())
@ -60,14 +60,13 @@ func InitModel() {
}
case "postgres":
{
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
databaseConfig.Host, databaseConfig.User, databaseConfig.Password, databaseConfig.Name, databaseConfig.Port)
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=Asia/Shanghai",
databaseConfig.Host, databaseConfig.User, databaseConfig.Password, databaseConfig.Name, databaseConfig.Port, databaseConfig.SslMode)
db, err := gorm.Open(postgres.Open(dsn), gormConfig)
if err != nil {
log.Errorf("failed to connect database:%s", err.Error())
}
conf.DB = db
}
default:
log.Fatalf("not supported database type: %s", databaseConfig.Type)
@ -75,11 +74,11 @@ func InitModel() {
log.Infof("auto migrate model...")
if databaseConfig.Type == "mysql" {
err = conf.DB.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4").
AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{})
AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{}, &model.SearchFile{})
} else {
err = conf.DB.AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{})
err = conf.DB.AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{}, &model.SearchFile{})
}
if err != nil {
log.Fatalf("failed to auto migrate")
log.Fatalf("failed to auto migrate: %s", err.Error())
}
}

View File

@ -3,6 +3,7 @@ package bootstrap
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"strings"
@ -27,7 +28,7 @@ func InitSettings() {
},
{
Key: "password",
Value: "alist",
Value: utils.RandomStr(8),
Description: "password",
Type: "string",
Access: model.PRIVATE,
@ -35,7 +36,7 @@ func InitSettings() {
},
{
Key: "logo",
Value: "https://store.heytapimage.com/cdo-portal/feedback/202112/05/1542f45f86b8609495b69c5380753135.png",
Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/can_circle.svg",
Description: "logo",
Type: "string",
Access: model.PUBLIC,
@ -43,7 +44,7 @@ func InitSettings() {
},
{
Key: "favicon",
Value: "https://store.heytapimage.com/cdo-portal/feedback/202112/05/1542f45f86b8609495b69c5380753135.png",
Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg",
Description: "favicon",
Type: "string",
Access: model.PUBLIC,
@ -57,6 +58,14 @@ func InitSettings() {
Access: model.PUBLIC,
Group: model.FRONT,
},
{
Key: "announcement",
Value: "This is a test announcement.",
Description: "announcement message (support markdown)",
Type: "text",
Access: model.PUBLIC,
Group: model.FRONT,
},
{
Key: "text types",
Value: strings.Join(conf.TextTypes, ","),
@ -65,15 +74,37 @@ func InitSettings() {
Group: model.FRONT,
},
{
Key: "hide readme file",
Value: "true",
Type: "bool",
Description: "hide readme file? ",
Key: "audio types",
Value: strings.Join(conf.AudioTypes, ","),
Type: "string",
Description: "audio type extensions",
Group: model.FRONT,
},
{
Key: "video types",
Value: strings.Join(conf.VideoTypes, ","),
Type: "string",
Description: "video type extensions",
Group: model.FRONT,
},
{
Key: "d_proxy types",
Value: strings.Join(conf.DProxyTypes, ","),
Type: "string",
Description: "/d but proxy",
Access: model.PRIVATE,
Group: model.BACK,
},
{
Key: "hide files",
Value: "/\\/README.md/i",
Type: "text",
Description: "hide files, support RegExp, one per line",
Group: model.FRONT,
},
{
Key: "music cover",
Value: "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png",
Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/circle_center.svg",
Description: "music cover image",
Type: "string",
Access: model.PUBLIC,
@ -87,8 +118,8 @@ func InitSettings() {
Group: model.FRONT,
},
{
Key: "home readme url",
Description: "when have multiple, the readme file to show",
Key: "global readme url",
Description: "Default display when directory has no readme",
Type: "string",
Access: model.PUBLIC,
Group: model.FRONT,
@ -157,7 +188,7 @@ func InitSettings() {
},
{
Key: "WebDAV username",
Value: "alist_admin",
Value: "admin",
Description: "WebDAV username",
Type: "string",
Access: model.PRIVATE,
@ -165,7 +196,7 @@ func InitSettings() {
},
{
Key: "WebDAV password",
Value: "alist_admin",
Value: utils.RandomStr(8),
Description: "WebDAV password",
Type: "string",
Access: model.PRIVATE,
@ -189,7 +220,7 @@ func InitSettings() {
},
{
Key: "Visitor WebDAV username",
Value: "alist_visitor",
Value: "guest",
Description: "Visitor WebDAV username",
Type: "string",
Access: model.PRIVATE,
@ -197,7 +228,7 @@ func InitSettings() {
},
{
Key: "Visitor WebDAV password",
Value: "alist_visitor",
Value: "guest",
Description: "Visitor WebDAV password",
Type: "string",
Access: model.PRIVATE,
@ -219,6 +250,38 @@ func InitSettings() {
Access: model.PUBLIC,
Group: model.FRONT,
},
{
Key: "ocr api",
Value: "https://api.nn.ci/ocr/file/json",
Description: "Used to identify verification codes",
Type: "string",
Access: model.PRIVATE,
Group: model.BACK,
},
{
Key: "enable search",
Value: "false",
Type: "bool",
Access: model.PUBLIC,
Group: model.BACK,
Description: "Experimental function, not recommended as it's still under development",
},
{
Key: "Aria2 RPC url",
Value: "http://localhost:6800/jsonrpc",
Description: "Aria2 RPC url, e.g. 'http://aria2.example.com:6800/jsonrpc'",
Type: "string",
Access: model.PRIVATE,
Group: model.BACK,
},
{
Key: "Aria2 RPC secret",
Value: "",
Description: "Aria2 RPC secret, e.g. '123456'",
Type: "string",
Access: model.PRIVATE,
Group: model.BACK,
},
}
for i, _ := range settings {
v := settings[i]
@ -227,6 +290,9 @@ func InitSettings() {
if err != nil {
if err == gorm.ErrRecordNotFound {
err = model.SaveSetting(v)
if v.Key == "password" {
log.Infof("Initial password: %s", conf.C.Sprintf(v.Value))
}
if err != nil {
log.Fatalf("failed write setting: %s", err.Error())
}
@ -241,6 +307,9 @@ func InitSettings() {
if err != nil {
log.Fatalf("failed write setting: %s", err.Error())
}
if v.Key == "password" {
log.Infof("Your password: %s", conf.C.Sprintf(v.Value))
}
}
}
model.LoadSettings()

View File

@ -6,8 +6,11 @@ BUILD_WEB() {
cd alist-web
yarn
yarn build
sed -i -e "s/\/CDN_URL\//\//g" dist/index.html
sed -i -e "s/assets/\/assets/g" dist/index.html
rm -f dist/index.html-e
mv dist ..
cd ..
cd .. || exit
rm -rf alist-web
}
@ -25,6 +28,7 @@ BUILD_DOCKER() {
gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
gitCommit=$(git log --pretty=format:"%h" -1)
gitTag=$(git describe --long --tags --dirty --always)
webTag=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
ldflags="\
-w -s \
-X 'github.com/Xhofe/alist/conf.BuiltAt=$builtAt' \
@ -32,6 +36,7 @@ BUILD_DOCKER() {
-X 'github.com/Xhofe/alist/conf.GitAuthor=$gitAuthor' \
-X 'github.com/Xhofe/alist/conf.GitCommit=$gitCommit' \
-X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \
-X 'github.com/Xhofe/alist/conf.WebTag=$webTag' \
"
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter alist.go
}
@ -44,7 +49,9 @@ BUILD() {
gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
gitCommit=$(git log --pretty=format:"%h" -1)
gitTag=$(git describe --long --tags --dirty --always)
webTag=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
echo "build version: $gitTag"
ldflags="\
-w -s \
-X 'github.com/Xhofe/alist/conf.BuiltAt=$builtAt' \
@ -52,24 +59,73 @@ BUILD() {
-X 'github.com/Xhofe/alist/conf.GitAuthor=$gitAuthor' \
-X 'github.com/Xhofe/alist/conf.GitCommit=$gitCommit' \
-X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \
-X 'github.com/Xhofe/alist/conf.WebTag=$webTag' \
"
rm -rf .git/
if [ "$1" == "release" ]; then
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
else
xgo -targets=linux/amd64,windows/amd64 -out alist -ldflags="$ldflags" -tags=jsoniter .
xgo -targets=linux/amd64,windows/amd64,darwin/amd64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
fi
mkdir "build"
mkdir -p "build"
mv alist-* build
if [ "$1" != "release" ]; then
cd build
upx -9 ./*
upx -9 ./alist-linux*
upx -9 ./alist-windows*
find . -type f -print0 | xargs -0 md5sum >md5.txt
cat md5.txt
cd ../..
cd .. || exit
fi
cd .. || exit
}
BUILD_MUSL() {
BASE="https://musl.cc/"
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross arm-linux-musleabihf-cross mips-linux-musl-cross mips64-linux-musl-cross mips64el-linux-musl-cross mipsel-linux-musl-cross powerpc64le-linux-musl-cross s390x-linux-musl-cross)
for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz"
curl -L -o "${i}.tgz" "${url}"
sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
done
cd alist
appName="alist"
builtAt="$(date +'%F %T %z')"
goVersion=$(go version | sed 's/go version //')
gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
gitCommit=$(git log --pretty=format:"%h" -1)
gitTag=$(git describe --long --tags --dirty --always)
webTag=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
ldflags="\
-w -s --extldflags '-static -fpic' \
-X 'github.com/Xhofe/alist/conf.BuiltAt=$builtAt' \
-X 'github.com/Xhofe/alist/conf.GoVersion=$goVersion' \
-X 'github.com/Xhofe/alist/conf.GitAuthor=$gitAuthor' \
-X 'github.com/Xhofe/alist/conf.GitCommit=$gitCommit' \
-X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \
-X 'github.com/Xhofe/alist/conf.WebTag=$webTag' \
"
OS_ARCHES=(linux-musl-amd64 linux-musl-arm64 linux-musl-arm linux-musl-mips linux-musl-mips64 linux-musl-mips64le linux-musl-mipsle linux-musl-ppc64le linux-musl-s390x)
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc arm-linux-musleabihf-gcc mips-linux-musl-gcc mips64-linux-musl-gcc mips64el-linux-musl-gcc mipsel-linux-musl-gcc powerpc64le-linux-musl-gcc s390x-linux-musl-gcc)
for i in "${!OS_ARCHES[@]}"; do
os_arch=${OS_ARCHES[$i]}
cgo_cc=${CGO_ARGS[$i]}
echo building for ${os_arch}
export GOOS=${os_arch%%-*}
export GOARCH=${os_arch##*-}
export CC=${cgo_cc}
export CGO_ENABLED=1
go build -o ./build/$appName-$os_arch -ldflags="$ldflags" -tags=jsoniter alist.go
done
cd .. || exit
}
RELEASE() {
cd alist/build
upx -9 ./alist-linux-amd64
upx -9 ./alist-windows*
find . -type f -print0 | xargs -0 md5sum >md5.txt
cat md5.txt
mkdir compress
mv md5.txt compress
for i in $(find . -type f -name "$appName-linux-*"); do
@ -81,7 +137,7 @@ RELEASE() {
for i in $(find . -type f -name "$appName-windows-*"); do
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip "$i"
done
cd ../..
cd ../.. || exit
}
if [ "$1" = "web" ]; then
@ -93,8 +149,9 @@ elif [ "$1" = "docker" ]; then
elif [ "$1" = "build" ]; then
BUILD build
elif [ "$1" = "release" ]; then
BUILD_MUSL
BUILD release
RELEASE
else
echo -e "${RED_COLOR} 错误的命令${RES}"
echo -e "${RED_COLOR} Parameter error ${RES}"
fi

View File

@ -1,40 +1,45 @@
package conf
type Database struct {
Type string `json:"type"`
User string `json:"user"`
Password string `json:"password"`
Host string `json:"host"`
Port int `json:"port"`
Name string `json:"name"`
TablePrefix string `json:"table_prefix"`
DBFile string `json:"db_file"`
Type string `json:"type" env:"DB_TYPE"`
Host string `json:"host" env:"DB_HOST"`
Port int `json:"port" env:"DB_PORT"`
User string `json:"user" env:"DB_USER"`
Password string `json:"password" env:"DB_PASS"`
Name string `json:"name" env:"DB_NAME"`
DBFile string `json:"db_file" env:"DB_FILE"`
TablePrefix string `json:"table_prefix" env:"DB_TABLE_PREFIX"`
SslMode string `json:"ssl_mode" env:"DB_SLL_MODE"`
}
type Scheme struct {
Https bool `json:"https"`
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
Https bool `json:"https" env:"HTTPS"`
CertFile string `json:"cert_file" env:"CERT_FILE"`
KeyFile string `json:"key_file" env:"KEY_FILE"`
}
type CacheConfig struct {
Expiration int64 `json:"expiration"`
CleanupInterval int64 `json:"cleanup_interval"`
Expiration int64 `json:"expiration" env:"CACHE_EXPIRATION"`
CleanupInterval int64 `json:"cleanup_interval" env:"CLEANUP_INTERVAL"`
}
type Config struct {
Address string `json:"address"`
Port int `json:"port"`
Local bool `json:"local"`
Force bool `json:"force"`
Address string `json:"address" env:"ADDR"`
Port int `json:"port" env:"PORT"`
Assets string `json:"assets" env:"ASSETS"`
Database Database `json:"database"`
Scheme Scheme `json:"scheme"`
Cache CacheConfig `json:"cache"`
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
}
func DefaultConfig() *Config {
return &Config{
Address: "0.0.0.0",
Port: 5244,
Assets: "https://npm.elemecdn.com/alist-web@$version/dist",
TempDir: "data/temp",
Database: Database{
Type: "sqlite3",
Port: 0,

View File

@ -3,6 +3,7 @@ package conf
import (
"context"
"github.com/eko/gocache/v2/cache"
"github.com/fatih/color"
"github.com/robfig/cron/v3"
"gorm.io/gorm"
"strconv"
@ -14,6 +15,7 @@ var (
GitAuthor string
GitCommit string
GitTag string = "dev"
WebTag string
)
var (
@ -22,24 +24,28 @@ var (
Debug bool
Version bool
Password bool
Docker bool
DB *gorm.DB
Cache *cache.Cache
Ctx = context.TODO()
Cron *cron.Cron
C = color.New(color.FgHiBlue, color.Bold, color.BgHiWhite, color.Underline)
)
var (
TextTypes = []string{"txt", "htm", "html", "xml", "java", "properties", "sql",
"js", "md", "json", "conf", "ini", "vue", "php", "py", "bat", "gitignore", "yml",
"go", "sh", "c", "cpp", "h", "hpp", "tsx", "vtt", "srt", "ass"}
DProxyTypes = []string{"m3u8"}
OfficeTypes = []string{"doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf"}
VideoTypes = []string{"mp4", "mkv", "avi", "mov", "rmvb", "webm", "flv"}
AudioTypes = []string{"mp3", "flac", "ogg", "m4a", "wav"}
ImageTypes = []string{"jpg", "tiff", "jpeg", "png", "gif", "bmp", "svg", "ico"}
AudioTypes = []string{"mp3", "flac", "ogg", "m4a", "wav", "opus"}
ImageTypes = []string{"jpg", "tiff", "jpeg", "png", "gif", "bmp", "svg", "ico", "swf", "webp"}
)
var settingsMap = make(map[string]string, 0)
var settingsMap = make(map[string]string)
func Set(key string, value string) {
settingsMap[key] = value
@ -78,6 +84,8 @@ var (
"check parent folder", "check down link", "WebDAV username", "WebDAV password",
"Visitor WebDAV username", "Visitor WebDAV password",
"default page size", "load type",
"ocr api", "favicon",
"enable search",
}
)

View File

@ -3,67 +3,17 @@ package _23
import (
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"path/filepath"
"strconv"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"path/filepath"
"strconv"
"time"
)
type BaseResp struct {
Code int `json:"code"`
Message string `json:"message"`
}
type Pan123TokenResp struct {
BaseResp
Data struct {
Token string `json:"token"`
} `json:"data"`
}
type Pan123File struct {
FileName string `json:"FileName"`
Size int64 `json:"Size"`
UpdateAt *time.Time `json:"UpdateAt"`
FileId int64 `json:"FileId"`
Type int `json:"Type"`
Etag string `json:"Etag"`
S3KeyFlag string `json:"S3KeyFlag"`
}
type Pan123Files struct {
BaseResp
Data struct {
InfoList []Pan123File `json:"InfoList"`
Next string `json:"Next"`
} `json:"data"`
}
type Pan123DownResp struct {
BaseResp
Data struct {
DownloadUrl string `json:"DownloadUrl"`
} `json:"data"`
}
type UploadResp struct {
BaseResp
Data struct {
AccessKeyId string `json:"AccessKeyId"`
Bucket string `json:"Bucket"`
Key string `json:"Key"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken"`
FileId int64 `json:"FileId"`
} `json:"data"`
}
func (driver Pan123) Login(account *model.Account) error {
url := "https://www.123pan.com/api/user/sign_in"
if account.APIProxyUrl != "" {
@ -90,25 +40,22 @@ func (driver Pan123) Login(account *model.Account) error {
return err
}
func (driver Pan123) FormatFile(file *Pan123File) *model.File {
func (driver Pan123) FormatFile(file *File) *model.File {
f := &model.File{
Id: strconv.FormatInt(file.FileId, 10),
Name: file.FileName,
Size: file.Size,
Driver: driver.Config().Name,
UpdatedAt: file.UpdateAt,
Thumbnail: file.DownloadUrl,
}
if file.Type == 1 {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(filepath.Ext(file.FileName))
}
f.Type = file.GetType()
return f
}
func (driver Pan123) GetFiles(parentId string, account *model.Account) ([]Pan123File, error) {
func (driver Pan123) GetFiles(parentId string, account *model.Account) ([]File, error) {
next := "0"
res := make([]Pan123File, 0)
res := make([]File, 0)
for next != "-1" {
var resp Pan123Files
query := map[string]string{
@ -120,7 +67,7 @@ func (driver Pan123) GetFiles(parentId string, account *model.Account) ([]Pan123
"parentFileId": parentId,
"trashed": "false",
}
_, err := driver.Request("https://www.123pan.com/api/file/list",
_, err := driver.Request("https://www.123pan.com/api/file/list/new",
base.Get, nil, query, nil, &resp, false, account)
if err != nil {
return nil, err
@ -193,7 +140,7 @@ func (driver Pan123) Request(url string, method int, headers, query map[string]s
// return body, nil
//}
func (driver Pan123) GetFile(path string, account *model.Account) (*Pan123File, error) {
func (driver Pan123) GetFile(path string, account *model.Account) (*File, error) {
dir, name := filepath.Split(path)
dir = utils.ParsePath(dir)
_, err := driver.Files(dir, account)
@ -201,14 +148,15 @@ func (driver Pan123) GetFile(path string, account *model.Account) (*Pan123File,
return nil, err
}
parentFiles_, _ := base.GetCache(dir, account)
parentFiles, _ := parentFiles_.([]Pan123File)
parentFiles, _ := parentFiles_.([]File)
for _, file := range parentFiles {
if file.FileName == name {
if file.Type != conf.FOLDER {
return &file, err
} else {
return nil, base.ErrNotFile
}
//if file.Type != conf.FOLDER {
// return &file, err
//} else {
// return nil, base.ErrNotFile
//}
return &file, nil
}
}
return nil, base.ErrPathNotFound

View File

@ -1,7 +1,16 @@
package _23
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strconv"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
@ -10,11 +19,7 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"net/url"
"path/filepath"
"strconv"
)
type Pan123 struct{}
@ -53,6 +58,7 @@ func (driver Pan123) Items() []base.Item {
Type: base.TypeSelect,
Values: "name,fileId,updateAt,createAt",
Required: true,
Default: "name",
},
{
Name: "order_direction",
@ -60,11 +66,15 @@ func (driver Pan123) Items() []base.Item {
Type: base.TypeSelect,
Values: "asc,desc",
Required: true,
Default: "asc",
},
}
}
func (driver Pan123) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
if account.RootFolder == "" {
account.RootFolder = "0"
}
@ -99,10 +109,10 @@ func (driver Pan123) File(path string, account *model.Account) (*model.File, err
func (driver Pan123) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
var rawFiles []Pan123File
var rawFiles []File
cache, err := base.GetCache(path, account)
if err == nil {
rawFiles, _ = cache.([]Pan123File)
rawFiles, _ = cache.([]File)
} else {
file, err := driver.File(path, account)
if err != nil {
@ -116,7 +126,7 @@ func (driver Pan123) Files(path string, account *model.Account) ([]model.File, e
_ = base.SetCache(path, rawFiles, account)
}
}
files := make([]model.File, 0)
files := make([]model.File, 0, len(rawFiles))
for _, file := range rawFiles {
files = append(files, *driver.FormatFile(&file))
}
@ -131,7 +141,7 @@ func (driver Pan123) Link(args base.Args, account *model.Account) (*base.Link, e
}
var resp Pan123DownResp
var headers map[string]string
if args.IP != "" && args.IP != "::1" {
if !utils.IsLocalIPAddr(args.IP) {
headers = map[string]string{
//"X-Real-IP": "1.1.1.1",
"X-Forwarded-For": args.IP,
@ -189,9 +199,9 @@ func (driver Pan123) Path(path string, account *model.Account) (*model.File, []m
return nil, files, nil
}
func (driver Pan123) Proxy(c *gin.Context, account *model.Account) {
c.Request.Header.Del("origin")
}
//func (driver Pan123) Proxy(r *http.Request, account *model.Account) {
// r.Header.Del("origin")
//}
func (driver Pan123) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
@ -234,7 +244,7 @@ func (driver Pan123) Move(src string, dst string, account *model.Account) error
}
parentFileId, _ := strconv.Atoi(dstDirFile.Id)
data := base.Json{
"fileId": fileId,
"fileIdList": []base.Json{{"FileId": fileId}},
"parentFileId": parentFileId,
}
_, err = driver.Request("https://www.123pan.com/api/file/mod_pid",
@ -269,12 +279,13 @@ func (driver Pan123) Delete(path string, account *model.Account) error {
if err != nil {
return err
}
log.Debugln("delete 123 file: ", file)
data := base.Json{
"driveId": 0,
"operation": true,
"fileTrashInfoList": file,
"fileTrashInfoList": []File{*file},
}
_, err = driver.Request("https://www.123pan.com/api/file/trash",
_, err = driver.Request("https://www.123pan.com/b/api/file/trash",
base.Post, nil, nil, &data, nil, false, account)
return err
}
@ -290,23 +301,43 @@ func (driver Pan123) Upload(file *model.FileStream, account *model.Account) erro
if !parentFile.IsDir() {
return base.ErrNotFolder
}
parentFileId, _ := strconv.Atoi(parentFile.Id)
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
h := md5.New()
if _, err = io.Copy(io.MultiWriter(tempFile, h), file); err != nil {
return err
}
etag := hex.EncodeToString(h.Sum(nil))
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
data := base.Json{
"driveId": 0,
"duplicate": true,
"etag": "836aae6cac845e17fce51919594737d0", //maybe file's md5
"duplicate": 2, // 2->覆盖 1->重命名 0->默认
"etag": etag,
"fileName": file.GetFileName(),
"parentFileId": parentFileId,
"parentFileId": parentFile.Id,
"size": file.GetSize(),
"type": 0,
}
var resp UploadResp
_, err = driver.Request("https://www.123pan.com/api/file/upload_request",
base.Post, nil, nil, &data, &resp, false, account)
base.Post, map[string]string{"app-version": "1.1"}, nil, &data, &resp, false, account)
//res, err := driver.Post("https://www.123pan.com/api/file/upload_request", data, account)
if err != nil {
return err
}
if resp.Data.Key == "" {
return nil
}
cfg := &aws.Config{
Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),
Region: aws.String("123pan"),
@ -321,7 +352,7 @@ func (driver Pan123) Upload(file *model.FileStream, account *model.Account) erro
input := &s3manager.UploadInput{
Bucket: &resp.Data.Bucket,
Key: &resp.Data.Key,
Body: file,
Body: tempFile,
}
_, err = uploader.Upload(input)
if err != nil {

74
drivers/123/types.go Normal file
View File

@ -0,0 +1,74 @@
package _23
import (
"path"
"time"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/utils"
)
type File struct {
FileName string `json:"FileName"`
Size int64 `json:"Size"`
UpdateAt *time.Time `json:"UpdateAt"`
FileId int64 `json:"FileId"`
Type int `json:"Type"`
Etag string `json:"Etag"`
S3KeyFlag string `json:"S3KeyFlag"`
DownloadUrl string `json:"DownloadUrl"`
}
func (f File) GetSize() uint64 {
return uint64(f.Size)
}
func (f File) GetName() string {
return f.FileName
}
func (f File) GetType() int {
if f.Type == 1 {
return conf.FOLDER
}
return utils.GetFileType(path.Ext(f.FileName))
}
type BaseResp struct {
Code int `json:"code"`
Message string `json:"message"`
}
type Pan123TokenResp struct {
BaseResp
Data struct {
Token string `json:"token"`
} `json:"data"`
}
type Pan123Files struct {
BaseResp
Data struct {
InfoList []File `json:"InfoList"`
Next string `json:"Next"`
} `json:"data"`
}
type Pan123DownResp struct {
BaseResp
Data struct {
DownloadUrl string `json:"DownloadUrl"`
} `json:"data"`
}
type UploadResp struct {
BaseResp
Data struct {
AccessKeyId string `json:"AccessKeyId"`
Bucket string `json:"Bucket"`
Key string `json:"Key"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken"`
FileId int64 `json:"FileId"`
} `json:"data"`
}

View File

@ -17,7 +17,7 @@ import (
func (driver Cloud139) Request(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
url := "https://yun.139.com" + pathname
req := base.RestyClient.R()
randStr := randomStr(16)
randStr := utils.RandomStr(16)
ts := time.Now().Format("2006-01-02 15:04:05")
log.Debugf("%+v", data)
body, err := utils.Json.Marshal(data)
@ -136,7 +136,7 @@ func (driver Cloud139) GetFiles(catalogID string, account *model.Account) ([]mod
f := model.File{
Id: content.ContentID,
Name: content.ContentName,
Size: int64(content.ContentSize),
Size: content.ContentSize,
Type: utils.GetFileType(path.Ext(content.ContentName)),
Driver: driver.Config().Name,
UpdatedAt: getTime(content.UpdateTime),
@ -158,7 +158,7 @@ func (driver Cloud139) GetLink(contentId string, account *model.Account) (string
"appName": "",
"contentID": contentId,
"commonAccountInfo": base.Json{
"account": "18627147660",
"account": account.Username,
"accountType": 1,
},
}

View File

@ -7,7 +7,6 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"io"
"math"
@ -64,6 +63,9 @@ func (driver Cloud139) Items() []base.Item {
}
func (driver Cloud139) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
_, err := driver.Request("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Post, nil, nil, nil, base.Json{
"qryUserExternInfoReq": base.Json{
"commonAccountInfo": base.Json{
@ -131,7 +133,12 @@ func (driver Cloud139) Link(args base.Args, account *model.Account) (*base.Link,
if err != nil {
return nil, err
}
u, err := driver.GetLink(file.Id, account)
var u string
//if isFamily(account) {
// u, err = driver.familyLink(file.Id, account)
//} else {
u, err = driver.GetLink(file.Id, account)
//}
if err != nil {
return nil, err
}
@ -155,9 +162,9 @@ func (driver Cloud139) Path(path string, account *model.Account) (*model.File, [
return nil, files, nil
}
func (driver Cloud139) Proxy(c *gin.Context, account *model.Account) {
}
//func (driver Cloud139) Proxy(r *http.Request, account *model.Account) {
//
//}
func (driver Cloud139) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
@ -224,7 +231,7 @@ func (driver Cloud139) Move(src string, dst string, account *model.Account) erro
"newCatalogID": dstParentFile.Id,
},
"commonAccountInfo": base.Json{
"account": "18627147660",
"account": account.Username,
"accountType": 1,
},
},
@ -249,7 +256,7 @@ func (driver Cloud139) Rename(src string, dst string, account *model.Account) er
"catalogID": srcFile.Id,
"catalogName": utils.Base(dst),
"commonAccountInfo": base.Json{
"account": "18627147660",
"account": account.Username,
"accountType": 1,
},
}
@ -259,7 +266,7 @@ func (driver Cloud139) Rename(src string, dst string, account *model.Account) er
"contentID": srcFile.Id,
"contentName": utils.Base(dst),
"commonAccountInfo": base.Json{
"account": "18627147660",
"account": account.Username,
"accountType": 1,
},
}
@ -298,7 +305,7 @@ func (driver Cloud139) Copy(src string, dst string, account *model.Account) erro
"newCatalogID": dstParentFile.Id,
},
"commonAccountInfo": base.Json{
"account": "18627147660",
"account": account.Username,
"accountType": 1,
},
},
@ -327,10 +334,10 @@ func (driver Cloud139) Delete(path string, account *model.Account) error {
"taskInfo": base.Json{
"newCatalogID": "",
"contentInfoList": contentInfoList,
"catalogInfoList": contentInfoList,
"catalogInfoList": catalogInfoList,
},
"commonAccountInfo": base.Json{
"account": "18627147660",
"account": account.Username,
"accountType": 1,
},
},
@ -341,7 +348,7 @@ func (driver Cloud139) Delete(path string, account *model.Account) error {
"catalogList": catalogInfoList,
"contentList": contentInfoList,
"commonAccountInfo": base.Json{
"account": "18627147660",
"account": account.Username,
"accountType": 1,
},
"sourceCatalogType": 1002,
@ -377,7 +384,7 @@ func (driver Cloud139) Upload(file *model.FileStream, account *model.Account) er
"parentCatalogID": parentFile.Id,
"newCatalogName": "",
"commonAccountInfo": base.Json{
"account": "18627147660",
"account": account.Username,
"accountType": 1,
},
}
@ -441,6 +448,7 @@ func (driver Cloud139) Upload(file *model.FileStream, account *model.Account) er
return err
}
log.Debugf("%+v", res)
res.Body.Close()
start += byteSize
}
return nil

View File

@ -43,7 +43,7 @@ func (driver Cloud139) familyGetFiles(catalogID string, account *model.Account)
f := model.File{
Id: content.ContentID,
Name: content.ContentName,
Size: int64(content.ContentSize),
Size: content.ContentSize,
Type: utils.GetFileType(path.Ext(content.ContentName)),
Driver: driver.Config().Name,
UpdatedAt: getTime(content.LastUpdateTime),

View File

@ -40,7 +40,7 @@ type Content struct {
ContentID string `json:"contentID"`
ContentName string `json:"contentName"`
//ContentSuffix string `json:"contentSuffix"`
ContentSize int `json:"contentSize"`
ContentSize int64 `json:"contentSize"`
//ContentDesc string `json:"contentDesc"`
//ContentType int `json:"contentType"`
//ContentOrigin int `json:"contentOrigin"`
@ -133,42 +133,42 @@ type UploadResp struct {
type CloudContent struct {
ContentID string `json:"contentID"`
Modifier string `json:"modifier"`
Nickname string `json:"nickname"`
CloudNickName string `json:"cloudNickName"`
//Modifier string `json:"modifier"`
//Nickname string `json:"nickname"`
//CloudNickName string `json:"cloudNickName"`
ContentName string `json:"contentName"`
ContentType int `json:"contentType"`
ContentSuffix string `json:"contentSuffix"`
ContentSize int `json:"contentSize"`
ContentDesc string `json:"contentDesc"`
CreateTime string `json:"createTime"`
Shottime interface{} `json:"shottime"`
//ContentType int `json:"contentType"`
//ContentSuffix string `json:"contentSuffix"`
ContentSize int64 `json:"contentSize"`
//ContentDesc string `json:"contentDesc"`
//CreateTime string `json:"createTime"`
//Shottime interface{} `json:"shottime"`
LastUpdateTime string `json:"lastUpdateTime"`
ThumbnailURL string `json:"thumbnailURL"`
MidthumbnailURL string `json:"midthumbnailURL"`
BigthumbnailURL string `json:"bigthumbnailURL"`
PresentURL string `json:"presentURL"`
PresentLURL string `json:"presentLURL"`
PresentHURL string `json:"presentHURL"`
ParentCatalogID string `json:"parentCatalogID"`
Uploader string `json:"uploader"`
UploaderNickName string `json:"uploaderNickName"`
TreeInfo interface{} `json:"treeInfo"`
UpdateTime interface{} `json:"updateTime"`
ExtInfo struct {
Uploader string `json:"uploader"`
} `json:"extInfo"`
EtagOprType interface{} `json:"etagOprType"`
//MidthumbnailURL string `json:"midthumbnailURL"`
//BigthumbnailURL string `json:"bigthumbnailURL"`
//PresentURL string `json:"presentURL"`
//PresentLURL string `json:"presentLURL"`
//PresentHURL string `json:"presentHURL"`
//ParentCatalogID string `json:"parentCatalogID"`
//Uploader string `json:"uploader"`
//UploaderNickName string `json:"uploaderNickName"`
//TreeInfo interface{} `json:"treeInfo"`
//UpdateTime interface{} `json:"updateTime"`
//ExtInfo struct {
// Uploader string `json:"uploader"`
//} `json:"extInfo"`
//EtagOprType interface{} `json:"etagOprType"`
}
type CloudCatalog struct {
CatalogID string `json:"catalogID"`
CatalogName string `json:"catalogName"`
CloudID string `json:"cloudID"`
CreateTime string `json:"createTime"`
//CloudID string `json:"cloudID"`
//CreateTime string `json:"createTime"`
LastUpdateTime string `json:"lastUpdateTime"`
Creator string `json:"creator"`
CreatorNickname string `json:"creatorNickname"`
//Creator string `json:"creator"`
//CreatorNickname string `json:"creatorNickname"`
}
type QueryContentListResp struct {

View File

@ -5,7 +5,6 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"math/rand"
"net/url"
"sort"
"strconv"
@ -13,16 +12,6 @@ import (
"time"
)
func randomStr(n int) string {
builder := strings.Builder{}
t := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for i := 0; i < n; i++ {
r := rand.Intn(len(t))
builder.WriteString(t[r : r+1])
}
return builder.String()
}
func encodeURIComponent(str string) string {
r := url.QueryEscape(str)
r = strings.Replace(r, "+", "%20", -1)

View File

@ -2,16 +2,9 @@ package _89
import (
"bytes"
"crypto/aes"
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
@ -19,15 +12,12 @@ import (
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"io"
"math"
mathRand "math/rand"
"net/http"
"net/url"
"path/filepath"
"net/http/cookiejar"
"regexp"
"strconv"
"strings"
@ -35,6 +25,23 @@ import (
)
var client189Map map[string]*resty.Client
var infoMap = make(map[string]Rsa)
func (driver Cloud189) getClient(account *model.Account) (*resty.Client, error) {
client, ok := client189Map[account.Name]
if ok {
return client, nil
}
err := driver.Login(account)
if err != nil {
return nil, err
}
client, ok = client189Map[account.Name]
if !ok {
return nil, fmt.Errorf("can't find [%s] client", account.Name)
}
return client, nil
}
func (driver Cloud189) FormatFile(file *Cloud189File) *model.File {
f := &model.File{
@ -52,11 +59,9 @@ func (driver Cloud189) FormatFile(file *Cloud189File) *model.File {
f.UpdatedAt = &lastOpTime
}
if file.Size == -1 {
f.Type = conf.FOLDER
f.Size = 0
} else {
f.Type = utils.GetFileType(filepath.Ext(file.Name))
}
f.Type = file.GetType()
return f
}
@ -81,12 +86,6 @@ func (driver Cloud189) FormatFile(file *Cloud189File) *model.File {
// return nil, ErrPathNotFound
//}
type Cloud189Down struct {
ResCode int `json:"res_code"`
ResMessage string `json:"res_message"`
FileDownloadUrl string `json:"fileDownloadUrl"`
}
type LoginResp struct {
Msg string `json:"msg"`
Result int `json:"result"`
@ -100,15 +99,21 @@ func (driver Cloud189) Login(account *model.Account) error {
//cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
client = resty.New()
//client.SetCookieJar(cookieJar)
client.SetTimeout(base.DefaultTimeout)
client.SetRetryCount(3)
client.SetHeader("Referer", "https://cloud.189.cn/")
}
// clear cookie
jar, _ := cookiejar.New(nil)
client.SetCookieJar(jar)
url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
b := ""
lt := ""
ltText := regexp.MustCompile(`lt = "(.+?)"`)
var res *resty.Response
var err error
for i := 0; i < 3; i++ {
res, err := client.R().Get(url)
res, err = client.R().Get(url)
if err != nil {
return err
}
@ -126,7 +131,7 @@ func (driver Cloud189) Login(account *model.Account) error {
}
}
if lt == "" {
return fmt.Errorf("get empty login page")
return fmt.Errorf("get page: %s \nstatus: %d \nrequest url: %s", b, res.StatusCode(), res.RawResponse.Request.URL.String())
}
captchaToken := regexp.MustCompile(`captchaToken' value='(.+?)'`).FindStringSubmatch(b)[1]
returnUrl := regexp.MustCompile(`returnUrl = '(.+?)'`).FindStringSubmatch(b)[1]
@ -137,12 +142,36 @@ func (driver Cloud189) Login(account *model.Account) error {
vCodeRS := ""
if vCodeID != "" {
// need ValidateCode
log.Debugf("try to identify verification codes")
timeStamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
u := "https://open.e.189.cn/api/logbox/oauth2/picCaptcha.do?token=" + vCodeID + timeStamp
imgRes, err := client.R().SetHeaders(map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/76.0",
"Referer": "https://open.e.189.cn/api/logbox/oauth2/unifyAccountLogin.do",
"Sec-Fetch-Dest": "image",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "same-origin",
}).Get(u)
if err != nil {
return err
}
userRsa := RsaEncode([]byte(account.Username), jRsakey)
passwordRsa := RsaEncode([]byte(account.Password), jRsakey)
vRes, err := client.R().SetMultipartField(
"image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
Post(conf.GetStr("ocr api"))
if err != nil {
return err
}
if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 {
return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString())
}
vCodeRS = jsoniter.Get(vRes.Body(), "result").ToString()
log.Debugln("code: ", vCodeRS)
}
userRsa := RsaEncode([]byte(account.Username), jRsakey, true)
passwordRsa := RsaEncode([]byte(account.Password), jRsakey, true)
url = "https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do"
var loginResp LoginResp
res, err := client.R().
res, err = client.R().
SetHeaders(map[string]string{
"lt": lt,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
@ -183,39 +212,6 @@ func (driver Cloud189) Login(account *model.Account) error {
return nil
}
type Cloud189Error struct {
ErrorCode string `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
}
type Cloud189File struct {
Id int64 `json:"id"`
LastOpTime string `json:"lastOpTime"`
Name string `json:"name"`
Size int64 `json:"size"`
Icon struct {
SmallUrl string `json:"smallUrl"`
//LargeUrl string `json:"largeUrl"`
} `json:"icon"`
Url string `json:"url"`
}
type Cloud189Folder struct {
Id int64 `json:"id"`
LastOpTime string `json:"lastOpTime"`
Name string `json:"name"`
}
type Cloud189Files struct {
ResCode int `json:"res_code"`
ResMessage string `json:"res_message"`
FileListAO struct {
Count int `json:"count"`
FileList []Cloud189File `json:"fileList"`
FolderList []Cloud189Folder `json:"folderList"`
} `json:"fileListAO"`
}
func (driver Cloud189) isFamily(account *model.Account) bool {
return account.InternalType == "Family"
}
@ -233,8 +229,8 @@ func (driver Cloud189) GetFiles(fileId string, account *model.Account) ([]Cloud1
"mediaType": "0",
"folderId": fileId,
"iconOption": "5",
"orderBy": account.OrderBy,
"descending": account.OrderDirection,
"orderBy": "lastOpTime", //account.OrderBy
"descending": "true", //account.OrderDirection
}, nil, nil, account)
if err != nil {
return nil, err
@ -264,9 +260,9 @@ func (driver Cloud189) GetFiles(fileId string, account *model.Account) ([]Cloud1
}
func (driver Cloud189) Request(url string, method int, query, form map[string]string, headers map[string]string, account *model.Account) ([]byte, error) {
client, ok := client189Map[account.Name]
if !ok {
return nil, fmt.Errorf("can't find [%s] client", account.Name)
client, err := driver.getClient(account)
if err != nil {
return nil, err
}
//var resp base.Json
if driver.isFamily(account) {
@ -293,7 +289,6 @@ func (driver Cloud189) Request(url string, method int, query, form map[string]st
if headers != nil {
req = req.SetHeaders(headers)
}
var err error
var res *resty.Response
switch method {
case base.Get:
@ -306,7 +301,7 @@ func (driver Cloud189) Request(url string, method int, query, form map[string]st
if err != nil {
return nil, err
}
log.Debug(res.String())
//log.Debug(res.String())
if e.ErrorCode != "" {
if e.ErrorCode == "InvalidSessionKey" {
err = driver.Login(account)
@ -323,61 +318,198 @@ func (driver Cloud189) Request(url string, method int, query, form map[string]st
}
func (driver Cloud189) GetSessionKey(account *model.Account) (string, error) {
//info, ok := infoMap[account.Name]
//if !ok {
// info = Info{}
// infoMap[account.Name] = info
//} else {
// log.Debugf("hit")
//}
//if info.SessionKey != "" {
// return info.SessionKey, nil
//}
resp, err := driver.Request("https://cloud.189.cn/v2/getUserBriefInfo.action", base.Get, nil, nil, nil, account)
if err != nil {
return "", err
}
return jsoniter.Get(resp, "sessionKey").ToString(), nil
sessionKey := jsoniter.Get(resp, "sessionKey").ToString()
//info.SessionKey = sessionKey
return sessionKey, nil
}
func (driver Cloud189) GetResKey(account *model.Account) (string, string, error) {
rsa, ok := infoMap[account.Name]
if !ok {
rsa = Rsa{}
infoMap[account.Name] = rsa
}
now := time.Now().UnixMilli()
if rsa.Expire > now {
return rsa.PubKey, rsa.PkId, nil
}
resp, err := driver.Request("https://cloud.189.cn/api/security/generateRsaKey.action", base.Get, nil, nil, nil, account)
if err != nil {
return "", "", err
}
return jsoniter.Get(resp, "pubKey").ToString(), jsoniter.Get(resp, "pkId").ToString(), nil
pubKey, pkId := jsoniter.Get(resp, "pubKey").ToString(), jsoniter.Get(resp, "pkId").ToString()
rsa.PubKey, rsa.PkId = pubKey, pkId
rsa.Expire = jsoniter.Get(resp, "expire").ToInt64()
return pubKey, pkId, nil
}
func (driver Cloud189) UploadRequest(url string, form map[string]string, account *model.Account) ([]byte, error) {
sessionKey, err := driver.GetSessionKey(account)
if err != nil {
return nil, err
}
//func (driver Cloud189) UploadRequest1(uri string, form map[string]string, account *model.Account, resp interface{}) ([]byte, error) {
// //sessionKey, err := driver.GetSessionKey(account)
// //if err != nil {
// // return nil, err
// //}
// sessionKey := account.DriveId
// pubKey, pkId, err := driver.GetResKey(account)
// log.Debugln(sessionKey, pubKey, pkId)
// if err != nil {
// return nil, err
// }
// xRId := Random("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx")
// pkey := Random("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx")[0 : 16+int(16*mathRand.Float32())]
// params := hex.EncodeToString(AesEncrypt([]byte(qs(form)), []byte(pkey[0:16])))
// date := strconv.FormatInt(time.Now().Unix(), 10)
// a := make(url.Values)
// a.Set("SessionKey", sessionKey)
// a.Set("Operate", http.MethodGet)
// a.Set("RequestURI", uri)
// a.Set("Date", date)
// a.Set("params", params)
// signature := hex.EncodeToString(SHA1(EncodeParam(a), pkey))
// encryptionText := RsaEncode([]byte(pkey), pubKey, false)
// headers := map[string]string{
// "signature": signature,
// "sessionKey": sessionKey,
// "encryptionText": encryptionText,
// "pkId": pkId,
// "x-request-id": xRId,
// "x-request-date": date,
// }
// req := base.RestyClient.R().SetHeaders(headers).SetQueryParam("params", params)
// if resp != nil {
// req.SetResult(resp)
// }
// res, err := req.Get("https://upload.cloud.189.cn" + uri)
// if err != nil {
// return nil, err
// }
// //log.Debug(res.String())
// data := res.Body()
// if jsoniter.Get(data, "code").ToString() != "SUCCESS" {
// return nil, errors.New(uri + "---" + jsoniter.Get(data, "msg").ToString())
// }
// return data, nil
//}
//
//func (driver Cloud189) UploadRequest2(uri string, form map[string]string, account *model.Account, resp interface{}) ([]byte, error) {
// c := strconv.FormatInt(time.Now().UnixMilli(), 10)
// r := Random("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx")
// l := Random("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx")
// l = l[0 : 16+int(16*mathRand.Float32())]
//
// e := qs(form)
// data := AesEncrypt([]byte(e), []byte(l[0:16]))
// h := hex.EncodeToString(data)
//
// sessionKey := account.DriveId
// a := make(url.Values)
// a.Set("SessionKey", sessionKey)
// a.Set("Operate", http.MethodGet)
// a.Set("RequestURI", uri)
// a.Set("Date", c)
// a.Set("params", h)
// g := SHA1(EncodeParam(a), l)
//
// pubKey, pkId, err := driver.GetResKey(account)
// if err != nil {
// return nil, err
// }
// b := RsaEncode([]byte(l), pubKey, false)
// client, err := driver.getClient(account)
// if err != nil {
// return nil, err
// }
// req := client.R()
// req.Header.Set("accept", "application/json;charset=UTF-8")
// req.Header.Set("SessionKey", sessionKey)
// req.Header.Set("Signature", hex.EncodeToString(g))
// req.Header.Set("X-Request-Date", c)
// req.Header.Set("X-Request-ID", r)
// req.Header.Set("EncryptionText", b)
// req.Header.Set("PkId", pkId)
// if resp != nil {
// req.SetResult(resp)
// }
// res, err := req.Get("https://upload.cloud.189.cn" + uri + "?params=" + h)
// if err != nil {
// return nil, err
// }
// //log.Debug(res.String())
// data = res.Body()
// if jsoniter.Get(data, "code").ToString() != "SUCCESS" {
// return nil, errors.New(uri + "---" + jsoniter.Get(data, "msg").ToString())
// }
// return data, nil
//}
func (driver Cloud189) UploadRequest(uri string, form map[string]string, account *model.Account, resp interface{}) ([]byte, error) {
c := strconv.FormatInt(time.Now().UnixMilli(), 10)
r := Random("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx")
l := Random("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx")
l = l[0 : 16+int(16*utils.Rand.Float32())]
e := qs(form)
data := AesEncrypt([]byte(e), []byte(l[0:16]))
h := hex.EncodeToString(data)
sessionKey := account.DriveId
signature := hmacSha1(fmt.Sprintf("SessionKey=%s&Operate=GET&RequestURI=%s&Date=%s&params=%s", sessionKey, uri, c, h), l)
pubKey, pkId, err := driver.GetResKey(account)
if err != nil {
return nil, err
}
xRId := uuid.New().String()
pkey := strings.ReplaceAll(xRId, "-", "")[:mathRand.Intn(16)+16]
params := aesEncrypt(qs(form), pkey[:16])
date := strconv.FormatInt(time.Now().Unix(), 10)
signature := hmacSha1(fmt.Sprintf("SessionKey=%s&Operate=GET&RequestURI=%s&Date=%s&params=%s", sessionKey, url, date, params), pkey)
encryptionText := RsaEncode([]byte(pkey), pubKey)
headers := map[string]string{
"signature": signature,
"sessionKey": sessionKey,
"encryptionText": encryptionText,
"pkId": pkId,
"x-request-id": xRId,
"x-request-date": date,
"origin": "https://cloud.189.cn",
"referer": "https://cloud.189.cn/",
}
log.Debugf("%+v\n%s", headers, params)
res, err := base.RestyClient.R().SetHeaders(headers).SetQueryParam("params", params).Get("https://upload.cloud.189.cn" + url)
b := RsaEncode([]byte(l), pubKey, false)
client, err := driver.getClient(account)
if err != nil {
return nil, err
}
log.Debug(res.String())
data := res.Body()
req := client.R()
req.Header.Set("accept", "application/json;charset=UTF-8")
req.Header.Set("SessionKey", sessionKey)
req.Header.Set("Signature", signature)
req.Header.Set("X-Request-Date", c)
req.Header.Set("X-Request-ID", r)
req.Header.Set("EncryptionText", b)
req.Header.Set("PkId", pkId)
if resp != nil {
req.SetResult(resp)
}
res, err := req.Get("https://upload.cloud.189.cn" + uri + "?params=" + h)
if err != nil {
return nil, err
}
//log.Debug(res.String())
data = res.Body()
if jsoniter.Get(data, "code").ToString() != "SUCCESS" {
return nil, errors.New(jsoniter.Get(data, "msg").ToString())
return nil, errors.New(uri + "---" + jsoniter.Get(data, "msg").ToString())
}
return data, nil
}
// Upload Error: decrypt encryptionText failed
// NewUpload Error: signature check false
func (driver Cloud189) NewUpload(file *model.FileStream, account *model.Account) error {
sessionKey, err := driver.GetSessionKey(account)
if err != nil {
account.Status = err.Error()
} else {
account.Status = "work"
account.DriveId = sessionKey
}
_ = model.SaveAccount(account)
const DEFAULT uint64 = 10485760
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
var finish uint64 = 0
@ -390,15 +522,18 @@ func (driver Cloud189) NewUpload(file *model.FileStream, account *model.Account)
}
res, err := driver.UploadRequest("/person/initMultiUpload", map[string]string{
"parentFolderId": parentFile.Id,
"fileName": file.Name,
"fileName": encode(file.Name),
"fileSize": strconv.FormatInt(int64(file.Size), 10),
"sliceSize": strconv.FormatInt(int64(DEFAULT), 10),
"lazyCheck": "1",
}, account)
}, account, nil)
if err != nil {
return err
}
uploadFileId := jsoniter.Get(res, "data.uploadFileId").ToString()
uploadFileId := jsoniter.Get(res, "data", "uploadFileId").ToString()
//_, err = driver.UploadRequest("/person/getUploadedPartsInfo", map[string]string{
// "uploadFileId": uploadFileId,
//}, account, nil)
var i int64
var byteSize uint64
md5s := make([]string, 0)
@ -408,180 +543,83 @@ func (driver Cloud189) NewUpload(file *model.FileStream, account *model.Account)
if DEFAULT < byteSize {
byteSize = DEFAULT
}
log.Debugf("%d,%d", byteSize, finish)
//log.Debugf("%d,%d", byteSize, finish)
byteData := make([]byte, byteSize)
n, err := io.ReadFull(file, byteData)
log.Debug(err, n)
//log.Debug(err, n)
if err != nil {
return err
}
finish += uint64(n)
md5Bytes := getMd5(byteData)
md5Str := hex.EncodeToString(md5Bytes)
md5Hex := hex.EncodeToString(md5Bytes)
md5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)
md5s = append(md5s, md5Str)
md5s = append(md5s, strings.ToUpper(md5Hex))
md5Sum.Write(byteData)
//log.Debugf("md5Bytes: %+v,md5Str:%s,md5Base64:%s", md5Bytes, md5Hex, md5Base64)
var resp UploadUrlsResp
res, err = driver.UploadRequest("/person/getMultiUploadUrls", map[string]string{
"partInfo": fmt.Sprintf("%s-%s", strconv.FormatInt(i, 10), md5Base64),
"uploadFileId": uploadFileId,
}, account)
}, account, &resp)
if err != nil {
return err
}
uploadData := jsoniter.Get(res, "uploadUrls.partNumber_"+strconv.FormatInt(i, 10))
headers := strings.Split(uploadData.Get("requestHeader").ToString(), "&")
req, err := http.NewRequest("PUT", uploadData.Get("requestURL").ToString(), bytes.NewBuffer(byteData))
uploadData := resp.UploadUrls["partNumber_"+strconv.FormatInt(i, 10)]
log.Debugf("uploadData: %+v", uploadData)
requestURL := uploadData.RequestURL
uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&")
req, _ := http.NewRequest(http.MethodPut, requestURL, bytes.NewReader(byteData))
for _, v := range uploadHeaders {
i := strings.Index(v, "=")
req.Header.Set(v[0:i], v[i+1:])
}
r, err := base.HttpClient.Do(req)
log.Debugf("%+v %+v", r, r.Request.Header)
r.Body.Close()
if err != nil {
return err
}
for _, header := range headers {
kv := strings.Split(header, "=")
req.Header.Set(kv[0], strings.Join(kv[1:], "="))
}
res, err := base.HttpClient.Do(req)
if err != nil {
return err
fileMd5 := hex.EncodeToString(md5Sum.Sum(nil))
sliceMd5 := fileMd5
if file.GetSize() > DEFAULT {
sliceMd5 = utils.GetMD5Encode(strings.Join(md5s, "\n"))
}
log.Debugf("%+v", res)
}
id := md5Sum.Sum(nil)
res, err = driver.UploadRequest("/person/commitMultiUploadFile", map[string]string{
"uploadFileId": uploadFileId,
"fileMd5": hex.EncodeToString(id),
"sliceMd5": utils.GetMD5Encode(strings.Join(md5s, "\n")),
"fileMd5": fileMd5,
"sliceMd5": sliceMd5,
"lazyCheck": "1",
}, account)
}, account, nil)
account.DriveId, _ = driver.GetSessionKey(account)
return err
}
func random() string {
return fmt.Sprintf("0.%17v", mathRand.New(mathRand.NewSource(time.Now().UnixNano())).Int63n(100000000000000000))
}
func RsaEncode(origData []byte, j_rsakey string) string {
publicKey := []byte("-----BEGIN PUBLIC KEY-----\n" + j_rsakey + "\n-----END PUBLIC KEY-----")
block, _ := pem.Decode(publicKey)
pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
pub := pubInterface.(*rsa.PublicKey)
b, err := rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
func (driver Cloud189) OldUpload(file *model.FileStream, account *model.Account) error {
//return base.ErrNotImplement
client, err := driver.getClient(account)
if err != nil {
log.Errorf("err: %s", err.Error())
return err
}
return b64tohex(base64.StdEncoding.EncodeToString(b))
}
var b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"
func int2char(a int) string {
return strings.Split(BI_RM, "")[a]
}
func b64tohex(a string) string {
d := ""
e := 0
c := 0
for i := 0; i < len(a); i++ {
m := strings.Split(a, "")[i]
if m != "=" {
v := strings.Index(b64map, m)
if 0 == e {
e = 1
d += int2char(v >> 2)
c = 3 & v
} else if 1 == e {
e = 2
d += int2char(c<<2 | v>>4)
c = 15 & v
} else if 2 == e {
e = 3
d += int2char(c)
d += int2char(v >> 2)
c = 3 & v
} else {
e = 0
d += int2char(c<<2 | v>>4)
d += int2char(15 & v)
}
}
}
if e == 1 {
d += int2char(c << 2)
}
return d
}
func qs(form map[string]string) string {
strList := make([]string, 0)
for k, v := range form {
strList = append(strList, fmt.Sprintf("%s=%s", k, url.QueryEscape(v)))
}
return strings.Join(strList, "&")
}
func aesEncrypt(data, key string) string {
encrypted := AesEncryptECB([]byte(data), []byte(key))
//return string(encrypted)
return hex.EncodeToString(encrypted)
}
func hmacSha1(data string, secret string) string {
h := hmac.New(sha1.New, []byte(secret))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
func AesEncryptECB(origData []byte, key []byte) (encrypted []byte) {
cipher, _ := aes.NewCipher(generateKey(key))
length := (len(origData) + aes.BlockSize) / aes.BlockSize
plain := make([]byte, length*aes.BlockSize)
copy(plain, origData)
pad := byte(len(plain) - len(origData))
for i := len(origData); i < len(plain); i++ {
plain[i] = pad
}
encrypted = make([]byte, len(plain))
// 分组分块加密
for bs, be := 0, cipher.BlockSize(); bs <= len(origData); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
cipher.Encrypt(encrypted[bs:be], plain[bs:be])
}
return encrypted
}
func AesDecryptECB(encrypted []byte, key []byte) (decrypted []byte) {
cipher, _ := aes.NewCipher(generateKey(key))
decrypted = make([]byte, len(encrypted))
//
for bs, be := 0, cipher.BlockSize(); bs < len(encrypted); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
cipher.Decrypt(decrypted[bs:be], encrypted[bs:be])
}
trim := 0
if len(decrypted) > 0 {
trim = len(decrypted) - int(decrypted[len(decrypted)-1])
}
return decrypted[:trim]
}
func generateKey(key []byte) (genKey []byte) {
genKey = make([]byte, 16)
copy(genKey, key)
for i := 16; i < len(key); {
for j := 0; j < 16 && i < len(key); j, i = j+1, i+1 {
genKey[j] ^= key[i]
}
}
return genKey
}
func getMd5(data []byte) []byte {
h := md5.New()
h.Write(data)
return h.Sum(nil)
}
func init() {
base.RegisterDriver(&Cloud189{})
client189Map = make(map[string]*resty.Client, 0)
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
}
// api refer to PanIndex
res, err := client.R().SetMultipartFormData(map[string]string{
"parentId": parentFile.Id,
"sessionKey": account.DriveId,
"opertype": "1",
"fname": file.GetFileName(),
}).SetMultipartField("Filedata", file.GetFileName(), file.GetMIMEType(), file).Post("https://hb02.upload.cloud.189.cn/v1/DCIWebUploadAction")
if err != nil {
return err
}
if jsoniter.Get(res.Body(), "MD5").ToString() != "" {
return nil
}
log.Debugf(res.String())
return errors.New(res.String())
}

View File

@ -1,16 +1,16 @@
package _89
import (
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"net/http"
"path/filepath"
"strings"
)
type Cloud189 struct{}
@ -18,6 +18,7 @@ type Cloud189 struct{}
func (driver Cloud189) Config() base.DriverConfig {
return base.DriverConfig{
Name: "189Cloud",
LocalSort: true,
}
}
@ -55,27 +56,30 @@ func (driver Cloud189) Items() []base.Item {
// Label: "family id",
// Type: base.TypeString,
//},
{
Name: "order_by",
Label: "order_by",
Type: base.TypeSelect,
Values: "name,size,lastOpTime,createdDate",
Required: true,
},
{
Name: "order_direction",
Label: "desc",
Type: base.TypeSelect,
Values: "true,false",
Required: true,
},
//{
// Name: "order_by",
// Label: "order_by",
// Type: base.TypeSelect,
// Values: "name,size,lastOpTime,createdDate",
// Required: true,
//},
//{
// Name: "order_direction",
// Label: "desc",
// Type: base.TypeSelect,
// Values: "true,false",
// Required: true,
//},
}
}
func (driver Cloud189) Save(account *model.Account, old *model.Account) error {
if old != nil && old.Name != account.Name {
if old != nil {
delete(client189Map, old.Name)
}
if account == nil {
return nil
}
if err := driver.Login(account); err != nil {
account.Status = err.Error()
_ = model.SaveAccount(account)
@ -151,14 +155,15 @@ func (driver Cloud189) Link(args base.Args, account *model.Account) (*base.Link,
if file.Type == conf.FOLDER {
return nil, base.ErrNotFile
}
var resp Cloud189Down
u := "https://cloud.189.cn/api/open/file/getFileDownloadUrl.action"
var resp DownResp
u := "https://cloud.189.cn/api/portal/getFileInfo.action"
body, err := driver.Request(u, base.Get, map[string]string{
"fileId": file.Id,
}, nil, nil, account)
if err != nil {
return nil, err
}
log.Debugln(string(body))
err = utils.Json.Unmarshal(body, &resp)
if err != nil {
return nil, err
@ -166,16 +171,35 @@ func (driver Cloud189) Link(args base.Args, account *model.Account) (*base.Link,
if resp.ResCode != 0 {
return nil, fmt.Errorf(resp.ResMessage)
}
res, err := base.NoRedirectClient.R().Get(resp.FileDownloadUrl)
client, err := driver.getClient(account)
if err != nil {
return nil, err
}
link := base.Link{}
client = resty.NewWithClient(client.GetClient()).SetRedirectPolicy(
resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}))
res, err := client.R().Get("https:" + resp.FileDownloadUrl)
if err != nil {
return nil, err
}
log.Debugln(res.Status())
link := base.Link{
Headers: []base.Header{
{Name: "User-Agent", Value: base.UserAgent},
//{Name: "Authorization", Value: ""},
},
}
if res.StatusCode() == 302 {
link.Url = res.Header().Get("location")
res, err = client.R().Get(link.Url)
if res.StatusCode() == 302 {
link.Url = res.Header().Get("location")
}
} else {
link.Url = resp.FileDownloadUrl
}
link.Url = strings.Replace(link.Url, "http://", "https://", 1)
return &link, nil
}
@ -196,9 +220,9 @@ func (driver Cloud189) Path(path string, account *model.Account) (*model.File, [
return nil, files, nil
}
func (driver Cloud189) Proxy(ctx *gin.Context, account *model.Account) {
ctx.Request.Header.Del("Origin")
}
//func (driver Cloud189) Proxy(r *http.Request, account *model.Account) {
// r.Header.Del("Origin")
//}
func (driver Cloud189) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
@ -346,29 +370,8 @@ func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) er
if file == nil {
return base.ErrEmptyFile
}
client, ok := client189Map[account.Name]
if !ok {
return fmt.Errorf("can't find [%s] client", account.Name)
}
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
}
// api refer to PanIndex
res, err := client.R().SetMultipartFormData(map[string]string{
"parentId": parentFile.Id,
"sessionKey": account.DriveId,
"opertype": "1",
"fname": file.GetFileName(),
}).SetMultipartField("Filedata", file.GetFileName(), file.GetMIMEType(), file).Post("https://hb02.upload.cloud.189.cn/v1/DCIWebUploadAction")
if err != nil {
return err
}
if jsoniter.Get(res.Body(), "MD5").ToString() != "" {
return nil
}
log.Debugf(res.String())
return errors.New(res.String())
return driver.NewUpload(file, account)
//return driver.OldUpload(file, account)
}
var _ base.Driver = (*Cloud189)(nil)

91
drivers/189/types.go Normal file
View File

@ -0,0 +1,91 @@
package _89
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/utils"
"path"
)
type Cloud189Error struct {
ErrorCode string `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
}
type Cloud189File struct {
Id int64 `json:"id"`
LastOpTime string `json:"lastOpTime"`
Name string `json:"name"`
Size int64 `json:"size"`
Icon struct {
SmallUrl string `json:"smallUrl"`
//LargeUrl string `json:"largeUrl"`
} `json:"icon"`
Url string `json:"url"`
}
func (f Cloud189File) GetSize() uint64 {
if f.Size == -1 {
return 0
}
return uint64(f.Size)
}
func (f Cloud189File) GetName() string {
return f.Name
}
func (f Cloud189File) GetType() int {
if f.Size == -1 {
return conf.FOLDER
}
return utils.GetFileType(path.Ext(f.Name))
}
type Cloud189Folder struct {
Id int64 `json:"id"`
LastOpTime string `json:"lastOpTime"`
Name string `json:"name"`
}
type Cloud189Files struct {
ResCode int `json:"res_code"`
ResMessage string `json:"res_message"`
FileListAO struct {
Count int `json:"count"`
FileList []Cloud189File `json:"fileList"`
FolderList []Cloud189Folder `json:"folderList"`
} `json:"fileListAO"`
}
type UploadUrlsResp struct {
Code string `json:"code"`
UploadUrls map[string]Part `json:"uploadUrls"`
}
type Part struct {
RequestURL string `json:"requestURL"`
RequestHeader string `json:"requestHeader"`
}
//type Info struct {
// SessionKey string
// Rsa Rsa
//}
type Rsa struct {
Expire int64 `json:"expire"`
PkId string `json:"pkId"`
PubKey string `json:"pubKey"`
}
type Cloud189Down struct {
ResCode int `json:"res_code"`
ResMessage string `json:"res_message"`
FileDownloadUrl string `json:"fileDownloadUrl"`
}
type DownResp struct {
ResCode int `json:"res_code"`
ResMessage string `json:"res_message"`
FileDownloadUrl string `json:"downloadUrl"`
}

199
drivers/189/util.go Normal file
View File

@ -0,0 +1,199 @@
package _89
import (
"bytes"
"crypto/aes"
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"fmt"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"net/url"
"regexp"
"strconv"
"strings"
)
func random() string {
return fmt.Sprintf("0.%17v", utils.Rand.Int63n(100000000000000000))
}
func RsaEncode(origData []byte, j_rsakey string, hex bool) string {
publicKey := []byte("-----BEGIN PUBLIC KEY-----\n" + j_rsakey + "\n-----END PUBLIC KEY-----")
block, _ := pem.Decode(publicKey)
pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
pub := pubInterface.(*rsa.PublicKey)
b, err := rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
if err != nil {
log.Errorf("err: %s", err.Error())
}
res := base64.StdEncoding.EncodeToString(b)
if hex {
return b64tohex(res)
}
return res
}
var b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"
func int2char(a int) string {
return strings.Split(BI_RM, "")[a]
}
func b64tohex(a string) string {
d := ""
e := 0
c := 0
for i := 0; i < len(a); i++ {
m := strings.Split(a, "")[i]
if m != "=" {
v := strings.Index(b64map, m)
if 0 == e {
e = 1
d += int2char(v >> 2)
c = 3 & v
} else if 1 == e {
e = 2
d += int2char(c<<2 | v>>4)
c = 15 & v
} else if 2 == e {
e = 3
d += int2char(c)
d += int2char(v >> 2)
c = 3 & v
} else {
e = 0
d += int2char(c<<2 | v>>4)
d += int2char(15 & v)
}
}
}
if e == 1 {
d += int2char(c << 2)
}
return d
}
func qs(form map[string]string) string {
f := make(url.Values)
for k, v := range form {
f.Set(k, v)
}
return EncodeParam(f)
//strList := make([]string, 0)
//for k, v := range form {
// strList = append(strList, fmt.Sprintf("%s=%s", k, url.QueryEscape(v)))
//}
//return strings.Join(strList, "&")
}
func EncodeParam(v url.Values) string {
if v == nil {
return ""
}
var buf strings.Builder
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
for _, k := range keys {
vs := v[k]
for _, v := range vs {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
//if k == "fileName" {
// buf.WriteString(encode(v))
//} else {
buf.WriteString(v)
//}
}
}
return buf.String()
}
func encode(str string) string {
//str = strings.ReplaceAll(str, "%", "%25")
//str = strings.ReplaceAll(str, "&", "%26")
//str = strings.ReplaceAll(str, "+", "%2B")
//return str
return url.QueryEscape(str)
}
func AesEncrypt(data, key []byte) []byte {
block, _ := aes.NewCipher(key)
if block == nil {
return []byte{}
}
data = PKCS7Padding(data, block.BlockSize())
decrypted := make([]byte, len(data))
size := block.BlockSize()
for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
block.Encrypt(decrypted[bs:be], data[bs:be])
}
return decrypted
}
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func hmacSha1(data string, secret string) string {
h := hmac.New(sha1.New, []byte(secret))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
func getMd5(data []byte) []byte {
h := md5.New()
h.Write(data)
return h.Sum(nil)
}
func init() {
base.RegisterDriver(&Cloud189{})
client189Map = make(map[string]*resty.Client)
}
func decodeURIComponent(str string) string {
r, _ := url.PathUnescape(str)
//r = strings.ReplaceAll(r, " ", "+")
return r
}
func Random(v string) string {
reg := regexp.MustCompilePOSIX("[xy]")
data := reg.ReplaceAllFunc([]byte(v), func(msg []byte) []byte {
var i int64
t := int64(16 * utils.Rand.Float32())
if msg[0] == 120 {
i = t
} else {
i = 3&t | 8
}
return []byte(strconv.FormatInt(i, 16))
})
return string(data)
}
//func SHA1(v, l string) []byte {
// key := []byte(l)
// mac := hmac.New(sha1.New, key)
// mac.Write([]byte(v))
// return mac.Sum(nil)
//}

318
drivers/189pc/189.go Normal file
View File

@ -0,0 +1,318 @@
package _189
import (
"bytes"
"errors"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"regexp"
"sync"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
var userStateCache = struct {
sync.Mutex
States map[string]*State
}{States: make(map[string]*State)}
func GetState(account *model.Account) *State {
userStateCache.Lock()
defer userStateCache.Unlock()
if v, ok := userStateCache.States[account.Username]; ok && v != nil {
return v
}
state := &State{client: resty.New().
SetHeaders(map[string]string{
"Accept": "application/json;charset=UTF-8",
"User-Agent": base.UserAgent,
}).SetTimeout(base.DefaultTimeout),
}
userStateCache.States[account.Username] = state
return state
}
type State struct {
sync.Mutex
client *resty.Client
RsaPublicKey string
SessionKey string
SessionSecret string
FamilySessionKey string
FamilySessionSecret string
AccessToken string
//怎么刷新的???
RefreshToken string
}
func (s *State) login(account *model.Account) error {
// 清除cookie
jar, _ := cookiejar.New(nil)
s.client.SetCookieJar(jar)
var err error
var res *resty.Response
defer func() {
account.Status = "work"
if err != nil {
account.Status = err.Error()
}
model.SaveAccount(account)
if res != nil {
log.Debug(res.String())
}
}()
var param *LoginParam
param, err = s.getLoginParam()
if err != nil {
return err
}
// 提交登录
s.RsaPublicKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", param.jRsaKey)
res, err = s.client.R().
SetHeaders(map[string]string{
"Referer": AUTH_URL,
"REQID": param.ReqId,
"lt": param.Lt,
}).
SetFormData(map[string]string{
"appKey": APP_ID,
"accountType": "02",
"userName": "{RSA}" + rsaEncrypt(s.RsaPublicKey, account.Username),
"password": "{RSA}" + rsaEncrypt(s.RsaPublicKey, account.Password),
"validateCode": param.vCodeRS,
"captchaToken": param.CaptchaToken,
"returnUrl": RETURN_URL,
"mailSuffix": "@189.cn",
"dynamicCheck": "FALSE",
"clientType": CLIENT_TYPE,
"cb_SaveName": "1",
"isOauth2": "false",
"state": "",
"paramId": param.ParamId,
}).
Post(AUTH_URL + "/api/logbox/oauth2/loginSubmit.do")
if err != nil {
return err
}
toUrl := utils.Json.Get(res.Body(), "toUrl").ToString()
if toUrl == "" {
log.Error(res.String())
return fmt.Errorf(res.String())
}
// 获取Session
var erron Erron
var sessionResp appSessionResp
res, err = s.client.R().
SetResult(&sessionResp).SetError(&erron).
SetQueryParams(clientSuffix()).
SetQueryParam("redirectURL", url.QueryEscape(toUrl)).
Post(API_URL + "/getSessionForPC.action")
if err != nil {
return err
}
if erron.ResCode != "" {
err = fmt.Errorf(erron.ResMessage)
return err
}
if sessionResp.ResCode != 0 {
err = fmt.Errorf(sessionResp.ResMessage)
return err
}
s.SessionKey = sessionResp.SessionKey
s.SessionSecret = sessionResp.SessionSecret
s.FamilySessionKey = sessionResp.FamilySessionKey
s.FamilySessionSecret = sessionResp.FamilySessionSecret
s.AccessToken = sessionResp.AccessToken
s.RefreshToken = sessionResp.RefreshToken
return err
}
func (s *State) getLoginParam() (*LoginParam, error) {
res, err := s.client.R().
SetQueryParams(map[string]string{
"appId": APP_ID,
"clientType": CLIENT_TYPE,
"returnURL": RETURN_URL,
"timeStamp": fmt.Sprint(timestamp()),
}).
Get(WEB_URL + "/api/portal/unifyLoginForPC.action")
if err != nil {
return nil, err
}
log.Debug(res.String())
param := &LoginParam{
CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],
Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1],
ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1],
ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1],
jRsaKey: regexp.MustCompile(`"j_rsaKey" value="(.+?)"`).FindStringSubmatch(res.String())[1],
vCodeID: regexp.MustCompile(`token=([A-Za-z0-9&=]+)`).FindStringSubmatch(res.String())[1],
}
imgRes, err := s.client.R().Get(fmt.Sprint(AUTH_URL, "/api/logbox/oauth2/picCaptcha.do?token=", param.vCodeID, timestamp()))
if err != nil {
return nil, err
}
if len(imgRes.Body()) > 0 {
vRes, err := resty.New().R().
SetMultipartField("image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
Post(conf.GetStr("ocr api"))
if err != nil {
return nil, err
}
if utils.Json.Get(vRes.Body(), "status").ToInt() != 200 {
return nil, errors.New("ocr error:" + utils.Json.Get(vRes.Body(), "msg").ToString())
}
param.vCodeRS = utils.Json.Get(vRes.Body(), "result").ToString()
log.Debugln("code: ", param.vCodeRS)
}
return param, nil
}
func (s *State) refreshSession(account *model.Account) error {
var erron Erron
var userSessionResp UserSessionResp
res, err := s.client.R().
SetResult(&userSessionResp).SetError(&erron).
SetQueryParams(clientSuffix()).
SetQueryParams(map[string]string{
"appId": APP_ID,
"accessToken": s.AccessToken,
}).
SetHeader("X-Request-ID", uuid.NewString()).
Get(API_URL + "/getSessionForPC.action")
if err != nil {
return err
}
log.Debug(res.String())
if erron.ResCode != "" {
return fmt.Errorf(erron.ResMessage)
}
switch userSessionResp.ResCode {
case 0:
s.SessionKey = userSessionResp.SessionKey
s.SessionSecret = userSessionResp.SessionSecret
s.FamilySessionKey = userSessionResp.FamilySessionKey
s.FamilySessionSecret = userSessionResp.FamilySessionSecret
case 11, 18:
return s.login(account)
default:
account.Status = userSessionResp.ResMessage
_ = model.SaveAccount(account)
return fmt.Errorf(userSessionResp.ResMessage)
}
return nil
}
func (s *State) IsLogin(account *model.Account) bool {
_, err := s.Request(http.MethodGet, API_URL+"/getUserInfo.action", nil, func(r *resty.Request) { r.SetQueryParams(clientSuffix()) }, account)
return err == nil
}
func (s *State) Login(account *model.Account) error {
s.Lock()
defer s.Unlock()
return s.login(account)
}
func (s *State) RefreshSession(account *model.Account) error {
s.Lock()
defer s.Unlock()
return s.refreshSession(account)
}
func (s *State) Request(method string, fullUrl string, params Params, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
s.Lock()
dateOfGmt := getHttpDateStr()
sessionKey := s.SessionKey
sessionSecret := s.SessionSecret
if isFamily(account) {
sessionKey = s.FamilySessionKey
sessionSecret = s.FamilySessionSecret
}
req := s.client.R()
req.SetHeaders(map[string]string{
"Date": dateOfGmt,
"SessionKey": sessionKey,
"X-Request-ID": uuid.NewString(),
})
// 设置params
var paramsData string
if params != nil {
paramsData = AesECBEncrypt(params.Encode(), s.SessionSecret[:16])
req.SetQueryParam("params", paramsData)
}
req.SetHeader("Signature", signatureOfHmac(sessionSecret, sessionKey, method, fullUrl, dateOfGmt, paramsData))
if callback != nil {
callback(req)
}
s.Unlock()
res, err := req.Execute(method, fullUrl)
if err != nil {
return nil, err
}
log.Debug(res.String())
var erron Erron
utils.Json.Unmarshal(res.Body(), &erron)
if erron.ResCode != "" {
return nil, fmt.Errorf(erron.ResMessage)
}
if erron.Code != "" && erron.Code != "SUCCESS" {
if erron.Msg == "" {
if erron.Message == "" {
return nil, fmt.Errorf(res.String())
}
return nil, fmt.Errorf(erron.Message)
}
return nil, fmt.Errorf(erron.Msg)
}
if erron.ErrorCode != "" {
switch erron.ErrorCode {
case "InvalidSessionKey":
if err := s.RefreshSession(account); err != nil {
return nil, err
}
return s.Request(method, fullUrl, params, callback, account)
}
return nil, fmt.Errorf(erron.ErrorMsg)
}
switch utils.Json.Get(res.Body(), "res_code").ToInt64() {
case 11, 18:
if err := s.RefreshSession(account); err != nil {
return nil, err
}
return s.Request(method, fullUrl, params, callback, account)
case 0:
if res.StatusCode() == http.StatusOK {
return res, nil
}
return nil, fmt.Errorf(res.String())
default:
return nil, fmt.Errorf(utils.Json.Get(res.Body(), "res_message").ToString())
}
}

918
drivers/189pc/driver.go Normal file
View File

@ -0,0 +1,918 @@
package _189
import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
func init() {
base.RegisterDriver(new(Cloud189))
}
type Cloud189 struct {
}
func (driver Cloud189) Config() base.DriverConfig {
return base.DriverConfig{
Name: "189CloudPC",
}
}
func (driver Cloud189) Items() []base.Item {
return []base.Item{
{
Name: "username",
Label: "username",
Type: base.TypeString,
Required: true,
Description: "account username/phone number",
},
{
Name: "password",
Label: "password",
Type: base.TypeString,
Required: true,
Description: "account password",
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
},
{
Name: "internal_type",
Label: "189cloud type",
Type: base.TypeSelect,
Required: true,
Values: "Personal,Family",
},
{
Name: "site_id",
Label: "family id",
Type: base.TypeString,
},
{
Name: "order_by",
Label: "order_by",
Type: base.TypeSelect,
Values: "filename,filesize,lastOpTime",
Required: true,
},
{
Name: "order_direction",
Label: "desc",
Type: base.TypeSelect,
Values: "true,false",
Required: true,
},
{
Name: "bool_1",
Label: "fast upload",
Type: base.TypeBool,
},
}
}
func (driver Cloud189) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
if !isFamily(account) && account.RootFolder == "" {
account.RootFolder = "-11"
account.SiteId = ""
}
if isFamily(account) && account.RootFolder == "-11" {
account.RootFolder = ""
}
state := GetState(account)
if !state.IsLogin(account) {
if err := state.Login(account); err != nil {
return err
}
}
if isFamily(account) {
list, err := driver.getFamilyInfoList(account)
if err != nil {
return err
}
for _, l := range list {
if account.SiteId == "" {
account.SiteId = fmt.Sprint(l.FamilyID)
}
log.Infof("天翼家庭云 用户名:%s FamilyID %d\n", l.RemarkName, l.FamilyID)
}
}
account.Status = "work"
model.SaveAccount(account)
return nil
}
func (driver Cloud189) getFamilyInfoList(account *model.Account) ([]FamilyInfoResp, error) {
var resp FamilyInfoListResp
_, err := GetState(account).Request(http.MethodGet, API_URL+"/family/manage/getFamilyList.action", nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetResult(&resp)
}, account)
if err != nil {
return nil, err
}
return resp.FamilyInfoResp, nil
}
func (driver Cloud189) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver Cloud189) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
cache, err := base.GetCache(path, account)
if err == nil {
files, _ := cache.([]model.File)
return files, nil
}
file, err := driver.File(path, account)
if err != nil {
return nil, err
}
fullUrl := API_URL
if isFamily(account) {
fullUrl += "/family/file"
}
fullUrl += "/listFiles.action"
files := make([]model.File, 0)
client := GetState(account)
for pageNum := 1; ; pageNum++ {
var resp Cloud189FilesResp
_, err = client.Request(http.MethodGet, fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix()).
SetQueryParams(map[string]string{
"folderId": file.Id,
"fileType": "0",
"mediaAttr": "0",
"iconOption": "5",
"pageNum": fmt.Sprint(pageNum),
"pageSize": "130",
})
if isFamily(account) {
r.SetQueryParams(map[string]string{
"familyId": account.SiteId,
"orderBy": toFamilyOrderBy(account.OrderBy),
"descending": account.OrderDirection,
})
} else {
r.SetQueryParams(map[string]string{
"recursive": "0",
"orderBy": account.OrderBy,
"descending": account.OrderDirection,
})
}
r.SetResult(&resp)
}, account)
if err != nil {
return nil, err
}
// 获取完毕跳出
if resp.FileListAO.Count == 0 {
break
}
for _, folder := range resp.FileListAO.FolderList {
files = append(files, model.File{
Id: fmt.Sprint(folder.ID),
Name: folder.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: MustParseTime(folder.LastOpTime),
})
}
for _, file := range resp.FileListAO.FileList {
files = append(files, model.File{
Id: fmt.Sprint(file.ID),
Name: file.Name,
Size: file.Size,
Type: utils.GetFileType(filepath.Ext(file.Name)),
Driver: driver.Config().Name,
UpdatedAt: MustParseTime(file.LastOpTime),
Thumbnail: file.Icon.SmallUrl,
})
}
}
if len(files) > 0 {
_ = base.SetCache(path, files, account)
}
return files, nil
}
func (driver Cloud189) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
log.Debugf("189PC path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver Cloud189) Link(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(utils.ParsePath(args.Path), account)
if err != nil {
return nil, err
}
if file.Type == conf.FOLDER {
return nil, base.ErrNotFile
}
fullUrl := API_URL
if isFamily(account) {
fullUrl += "/family/file"
}
fullUrl += "/getFileDownloadUrl.action"
var downloadUrl struct {
URL string `json:"fileDownloadUrl"`
}
_, err = GetState(account).Request(http.MethodGet, fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix()).SetQueryParam("fileId", file.Id)
if isFamily(account) {
r.SetQueryParams(map[string]string{
"familyId": account.SiteId,
})
} else {
r.SetQueryParams(map[string]string{
"dt": "3",
"flag": "1",
})
}
r.SetResult(&downloadUrl)
}, account)
if err != nil {
return nil, err
}
return &base.Link{
Headers: []base.Header{
{Name: "User-Agent", Value: base.UserAgent},
},
Url: strings.ReplaceAll(downloadUrl.URL, "&amp;", "&"),
}, nil
}
func (driver Cloud189) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver Cloud189) MakeDir(path string, account *model.Account) error {
dir, name := filepath.Split(path)
parentFile, err := driver.File(dir, account)
if err != nil {
return err
}
if !parentFile.IsDir() {
return base.ErrNotFolder
}
fullUrl := API_URL
if isFamily(account) {
fullUrl += "/family/file"
}
fullUrl += "/createFolder.action"
_, err = GetState(account).Request(http.MethodPost, fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix()).SetQueryParams(map[string]string{
"folderName": name,
"relativePath": "",
})
if isFamily(account) {
r.SetQueryParams(map[string]string{
"familyId": account.SiteId,
"parentId": parentFile.Id,
})
} else {
r.SetQueryParams(map[string]string{
"parentFolderId": parentFile.Id,
})
}
}, account)
return err
}
func (driver Cloud189) Move(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
dstDirFile, err := driver.File(filepath.Dir(dst), account)
if err != nil {
return err
}
_, err = GetState(account).Request(http.MethodPost, API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
"type": "MOVE",
"taskInfos": string(MustToBytes(utils.Json.Marshal(
[]*BatchTaskInfo{
{
FileId: srcFile.Id,
FileName: srcFile.Name,
IsFolder: BoolToNumber(srcFile.IsDir()),
},
}))),
"targetFolderId": dstDirFile.Id,
})
if isFamily(account) {
r.SetFormData(map[string]string{
"familyId": account.SiteId,
})
}
}, account)
return err
}
/*
func (driver Cloud189) Move(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
dstDirFile, err := driver.File(filepath.Dir(dst), account)
if err != nil {
return err
}
var queryParam map[string]string
fullUrl := API_URL
method := http.MethodPost
if isFamily(account) {
fullUrl += "/family/file"
method = http.MethodGet
}
if srcFile.IsDir() {
fullUrl += "/moveFolder.action"
queryParam = map[string]string{
"folderId": srcFile.Id,
"destFolderName": srcFile.Name,
}
} else {
fullUrl += "/moveFile.action"
queryParam = map[string]string{
"fileId": srcFile.Id,
"destFileName": srcFile.Name,
}
}
_, err = GetState(account).Request(method, fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(queryParam).SetQueryParams(clientSuffix())
if isFamily(account) {
r.SetQueryParams(map[string]string{
"familyId": account.SiteId,
"destParentId": dstDirFile.Id,
})
} else {
r.SetQueryParam("destParentFolderId", dstDirFile.Id)
}
}, account)
return err
}*/
func (driver Cloud189) Rename(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
var queryParam map[string]string
fullUrl := API_URL
method := http.MethodPost
if isFamily(account) {
fullUrl += "/family/file"
method = http.MethodGet
}
if srcFile.IsDir() {
fullUrl += "/renameFolder.action"
queryParam = map[string]string{
"folderId": srcFile.Id,
"destFolderName": filepath.Base(dst),
}
} else {
fullUrl += "/renameFile.action"
queryParam = map[string]string{
"fileId": srcFile.Id,
"destFileName": filepath.Base(dst),
}
}
_, err = GetState(account).Request(method, fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(queryParam).SetQueryParams(clientSuffix())
if isFamily(account) {
r.SetQueryParam("familyId", account.SiteId)
}
}, account)
return err
}
func (driver Cloud189) Copy(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
dstDirFile, err := driver.File(filepath.Dir(dst), account)
if err != nil {
return err
}
_, err = GetState(account).Request(http.MethodPost, API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
"type": "COPY",
"taskInfos": string(MustToBytes(utils.Json.Marshal(
[]*BatchTaskInfo{
{
FileId: srcFile.Id,
FileName: srcFile.Name,
IsFolder: BoolToNumber(srcFile.IsDir()),
},
}))),
"targetFolderId": dstDirFile.Id,
"targetFileName": filepath.Base(dst),
})
if isFamily(account) {
r.SetFormData(map[string]string{
"familyId": account.SiteId,
})
}
}, account)
return err
}
func (driver Cloud189) Delete(path string, account *model.Account) error {
path = utils.ParsePath(path)
srcFile, err := driver.File(path, account)
if err != nil {
return err
}
_, err = GetState(account).Request(http.MethodPost, API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
"type": "DELETE",
"taskInfos": string(MustToBytes(utils.Json.Marshal(
[]*BatchTaskInfo{
{
FileId: srcFile.Id,
FileName: srcFile.Name,
IsFolder: BoolToNumber(srcFile.IsDir()),
},
}))),
})
if isFamily(account) {
r.SetFormData(map[string]string{
"familyId": account.SiteId,
})
}
}, account)
return err
}
func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) error {
if file == nil {
return base.ErrEmptyFile
}
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
}
if !parentFile.IsDir() {
return base.ErrNotFolder
}
if account.Bool1 {
return driver.FastUpload(file, parentFile, account)
}
return driver.CommonUpload(file, parentFile, account)
/*
if isFamily(account) {
return driver.uploadFamily(file, parentFile, account)
}
return driver.uploadPerson(file, parentFile, account)
*/
}
func (driver Cloud189) CommonUpload(file *model.FileStream, parentFile *model.File, account *model.Account) error {
// 初始化上传
state := GetState(account)
const DEFAULT int64 = 10485760
count := int(math.Ceil(float64(file.Size) / float64(DEFAULT)))
params := Params{
"parentFolderId": parentFile.Id,
"fileName": url.PathEscape(file.Name),
"fileSize": fmt.Sprint(file.Size),
"sliceSize": fmt.Sprint(DEFAULT),
"lazyCheck": "1",
}
fullUrl := UPLOAD_URL
if isFamily(account) {
params.Set("familyId", account.SiteId)
fullUrl += "/family"
} else {
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
fullUrl += "/person"
}
var initMultiUpload InitMultiUploadResp
_, err := state.Request(http.MethodGet, fullUrl+"/initMultiUpload", params, func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&initMultiUpload) }, account)
if err != nil {
return err
}
fileMd5 := md5.New()
silceMd5 := md5.New()
silceMd5Hexs := make([]string, 0, count)
byteData := bytes.NewBuffer(make([]byte, DEFAULT))
for i := 1; i <= count; i++ {
byteData.Reset()
silceMd5.Reset()
if n, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5, byteData), file, DEFAULT); err != io.EOF && n == 0 {
return err
}
md5Bytes := silceMd5.Sum(nil)
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Bytes)))
silceMd5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)
var uploadUrl UploadUrlsResp
_, err = state.Request(http.MethodGet, fullUrl+"/getMultiUploadUrls",
Params{"partInfo": fmt.Sprintf("%d-%s", i, silceMd5Base64), "uploadFileId": initMultiUpload.Data.UploadFileID},
func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&uploadUrl) },
account)
if err != nil {
return err
}
uploadData := uploadUrl.UploadUrls[fmt.Sprint("partNumber_", i)]
req, _ := http.NewRequest(http.MethodPut, uploadData.RequestURL, byteData)
req.Header.Del("User-Agent")
for k, v := range ParseHttpHeader(uploadData.RequestHeader) {
req.Header.Set(k, v)
}
for k, v := range clientSuffix() {
req.URL.RawQuery += fmt.Sprintf("&%s=%s", k, v)
}
r, err := base.HttpClient.Do(req)
if err != nil {
return err
}
if r.StatusCode != http.StatusOK {
data, _ := io.ReadAll(r.Body)
r.Body.Close()
return fmt.Errorf(string(data))
}
r.Body.Close()
}
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
sliceMd5Hex := fileMd5Hex
if int64(file.Size) > DEFAULT {
sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n")))
}
_, err = state.Request(http.MethodGet, fullUrl+"/commitMultiUploadFile",
Params{
"uploadFileId": initMultiUpload.Data.UploadFileID,
"fileMd5": fileMd5Hex,
"sliceMd5": sliceMd5Hex,
"lazyCheck": "1",
"isLog": "0",
"opertype": "3",
},
func(r *resty.Request) { r.SetQueryParams(clientSuffix()) }, account)
return err
}
func (driver Cloud189) FastUpload(file *model.FileStream, parentFile *model.File, account *model.Account) error {
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
// 初始化上传
state := GetState(account)
const DEFAULT int64 = 10485760
count := int(math.Ceil(float64(file.Size) / float64(DEFAULT)))
// 优先计算所需信息
fileMd5 := md5.New()
silceMd5 := md5.New()
silceMd5Hexs := make([]string, 0, count)
silceMd5Base64s := make([]string, 0, count)
for i := 1; i <= count; i++ {
silceMd5.Reset()
if n, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5, tempFile), file, DEFAULT); err != nil && n == 0 {
return err
}
md5Byte := silceMd5.Sum(nil)
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte)))
silceMd5Base64s = append(silceMd5Base64s, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte)))
}
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
sliceMd5Hex := fileMd5Hex
if int64(file.Size) > DEFAULT {
sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n")))
}
params := Params{
"parentFolderId": parentFile.Id,
"fileName": url.PathEscape(file.Name),
"fileSize": fmt.Sprint(file.Size),
"fileMd5": fileMd5Hex,
"sliceSize": fmt.Sprint(DEFAULT),
"sliceMd5": sliceMd5Hex,
}
fullUrl := UPLOAD_URL
if isFamily(account) {
params.Set("familyId", account.SiteId)
fullUrl += "/family"
} else {
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
fullUrl += "/person"
}
var uploadInfo InitMultiUploadResp
_, err = state.Request(http.MethodGet, fullUrl+"/initMultiUpload", params, func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&uploadInfo) }, account)
if err != nil {
return err
}
if uploadInfo.Data.FileDataExists != 1 {
var uploadUrls UploadUrlsResp
_, err := state.Request(http.MethodGet, fullUrl+"/getMultiUploadUrls",
Params{
"uploadFileId": uploadInfo.Data.UploadFileID,
"partInfo": strings.Join(silceMd5Base64s, ","),
},
func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&uploadUrls) },
account)
if err != nil {
return err
}
for i := 1; i <= count; i++ {
uploadData := uploadUrls.UploadUrls[fmt.Sprint("partNumber_", i)]
req, _ := http.NewRequest(http.MethodPut, uploadData.RequestURL, io.NewSectionReader(tempFile, int64(i-1)*DEFAULT, DEFAULT))
req.Header.Del("User-Agent")
for k, v := range ParseHttpHeader(uploadData.RequestHeader) {
req.Header.Set(k, v)
}
for k, v := range clientSuffix() {
req.URL.RawQuery += fmt.Sprintf("&%s=%s", k, v)
}
r, err := base.HttpClient.Do(req)
if err != nil {
return err
}
if r.StatusCode != http.StatusOK {
data, _ := io.ReadAll(r.Body)
r.Body.Close()
return fmt.Errorf(string(data))
}
r.Body.Close()
}
}
_, err = state.Request(http.MethodGet, fullUrl+"/commitMultiUploadFile",
Params{
"uploadFileId": uploadInfo.Data.UploadFileID,
"isLog": "0",
"opertype": "3",
},
func(r *resty.Request) { r.SetQueryParams(clientSuffix()) },
account)
return err
}
/*
func (driver Cloud189) uploadFamily(file *model.FileStream, parentFile *model.File, account *model.Account) error {
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
fileMd5 := md5.New()
if _, err = io.Copy(io.MultiWriter(fileMd5, tempFile), file); err != nil {
return err
}
client := GetState(account)
var createUpload CreateUploadFileResult
_, err = client.Request(http.MethodGet, API_URL+"/family/file/createFamilyFile.action", nil, func(r *resty.Request) {
r.SetQueryParams(map[string]string{
"fileMd5": hex.EncodeToString(fileMd5.Sum(nil)),
"fileName": file.Name,
"familyId": account.SiteId,
"parentId": parentFile.Id,
"resumePolicy": "1",
"fileSize": fmt.Sprint(file.Size),
})
r.SetQueryParams(clientSuffix())
r.SetResult(&createUpload)
}, account)
if err != nil {
return err
}
if createUpload.FileDataExists != 1 {
if createUpload.UploadFileId, err = driver.uploadFileData(file, tempFile, createUpload, account); err != nil {
return err
}
}
_, err = client.Request(http.MethodGet, createUpload.FileCommitUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetHeaders(map[string]string{
"FamilyId": account.SiteId,
"uploadFileId": fmt.Sprint(createUpload.UploadFileId),
"ResumePolicy": "1",
})
}, account)
return err
}
func (driver Cloud189) uploadPerson(file *model.FileStream, parentFile *model.File, account *model.Account) error {
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
fileMd5 := md5.New()
if _, err = io.Copy(io.MultiWriter(fileMd5, tempFile), file); err != nil {
return err
}
client := GetState(account)
var createUpload CreateUploadFileResult
_, err = client.Request(http.MethodPost, API_URL+"/createUploadFile.action", nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
"parentFolderId": parentFile.Id,
"baseFileId": "",
"fileName": file.Name,
"size": fmt.Sprint(file.Size),
"md5": hex.EncodeToString(fileMd5.Sum(nil)),
// "lastWrite": param.LastWrite,
// "localPath": strings.ReplaceAll(file.ParentPath, "\\", "/"),
"opertype": "1",
"flag": "1",
"resumePolicy": "1",
"isLog": "0",
"fileExt": "",
})
r.SetResult(&createUpload)
}, account)
if err != nil {
return err
}
if createUpload.FileDataExists != 1 {
if createUpload.UploadFileId, err = driver.uploadFileData(file, tempFile, createUpload, account); err != nil {
return err
}
}
_, err = client.Request(http.MethodPost, createUpload.FileCommitUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetFormData(map[string]string{
"uploadFileId": fmt.Sprint(createUpload.UploadFileId),
"opertype": "5", //5 覆盖 1 重命名
"ResumePolicy": "1",
"isLog": "0",
})
}, account)
return err
}
func (driver Cloud189) uploadFileData(file *model.FileStream, tempFile *os.File, createUpload CreateUploadFileResult, account *model.Account) (int64, error) {
uploadFileState, err := driver.getUploadFileState(createUpload.UploadFileId, account)
if err != nil {
return 0, err
}
if uploadFileState.FileDataExists == 1 || uploadFileState.DataSize == int64(file.Size) {
return uploadFileState.UploadFileId, nil
}
if _, err = tempFile.Seek(uploadFileState.DataSize, io.SeekStart); err != nil {
return 0, err
}
_, err = GetState(account).Request("PUT", uploadFileState.FileUploadUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetHeaders(map[string]string{
"Content-Type": "application/octet-stream",
"ResumePolicy": "1",
"Edrive-UploadFileRange": fmt.Sprintf("bytes=%d-%d", uploadFileState.DataSize, file.Size),
"Expect": "100-continue",
})
if isFamily(account) {
r.SetHeaders(map[string]string{
"familyId": account.SiteId,
"UploadFileId": fmt.Sprint(uploadFileState.UploadFileId),
})
} else {
r.SetHeader("Edrive-UploadFileId", fmt.Sprint(uploadFileState.UploadFileId))
}
r.SetBody(tempFile)
}, account)
return uploadFileState.UploadFileId, err
}
func (driver Cloud189) getUploadFileState(uploadFileId int64, account *model.Account) (*UploadFileStatusResult, error) {
fullUrl := API_URL
if isFamily(account) {
fullUrl += "/family/file/getFamilyFileStatus.action"
} else {
fullUrl += "/getUploadFileStatus.action"
}
var uploadFileState UploadFileStatusResult
_, err := GetState(account).Request(http.MethodGet, fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetQueryParams(map[string]string{
"uploadFileId": fmt.Sprint(uploadFileId),
"resumePolicy": "1",
})
if isFamily(account) {
r.SetQueryParam("familyId", account.SiteId)
}
r.SetResult(&uploadFileState)
}, account)
if err != nil {
return nil, err
}
return &uploadFileState, nil
}*/
var _ base.Driver = (*Cloud189)(nil)

180
drivers/189pc/type.go Normal file
View File

@ -0,0 +1,180 @@
package _189
import "encoding/xml"
type LoginParam struct {
CaptchaToken string
Lt string
ParamId string
ReqId string
jRsaKey string
vCodeID string
vCodeRS string
}
// 居然有四种返回方式
type Erron struct {
ResCode string `json:"res_code"`
ResMessage string `json:"res_message"`
XMLName xml.Name `xml:"error"`
Code string `json:"code" xml:"code"`
Message string `json:"message" xml:"message"`
// Code string `json:"code"`
Msg string `json:"msg"`
ErrorCode string `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
}
// 刷新session返回
type UserSessionResp struct {
ResCode int `json:"res_code"`
ResMessage string `json:"res_message"`
LoginName string `json:"loginName"`
KeepAlive int `json:"keepAlive"`
GetFileDiffSpan int `json:"getFileDiffSpan"`
GetUserInfoSpan int `json:"getUserInfoSpan"`
// 个人云
SessionKey string `json:"sessionKey"`
SessionSecret string `json:"sessionSecret"`
// 家庭云
FamilySessionKey string `json:"familySessionKey"`
FamilySessionSecret string `json:"familySessionSecret"`
}
//登录返回
type appSessionResp struct {
UserSessionResp
IsSaveName string `json:"isSaveName"`
// 会话刷新Token
AccessToken string `json:"accessToken"`
//Token刷新
RefreshToken string `json:"refreshToken"`
}
type FamilyInfoListResp struct {
FamilyInfoResp []FamilyInfoResp `json:"familyInfoResp"`
}
type FamilyInfoResp struct {
Count int `json:"count"`
CreateTime string `json:"createTime"`
FamilyID int `json:"familyId"`
RemarkName string `json:"remarkName"`
Type int `json:"type"`
UseFlag int `json:"useFlag"`
UserRole int `json:"userRole"`
}
/*文件部分*/
// 文件
type Cloud189File struct {
CreateDate string `json:"createDate"`
FileCata int64 `json:"fileCata"`
Icon struct {
//iconOption 5
SmallUrl string `json:"smallUrl"`
LargeUrl string `json:"largeUrl"`
// iconOption 10
Max600 string `json:"max600"`
MediumURL string `json:"mediumUrl"`
} `json:"icon"`
ID int64 `json:"id"`
LastOpTime string `json:"lastOpTime"`
Md5 string `json:"md5"`
MediaType int `json:"mediaType"`
Name string `json:"name"`
Orientation int64 `json:"orientation"`
Rev string `json:"rev"`
Size int64 `json:"size"`
StarLabel int64 `json:"starLabel"`
}
// 文件夹
type Cloud189Folder struct {
ID int64 `json:"id"`
ParentID int64 `json:"parentId"`
Name string `json:"name"`
FileCata int64 `json:"fileCata"`
FileCount int64 `json:"fileCount"`
LastOpTime string `json:"lastOpTime"`
CreateDate string `json:"createDate"`
FileListSize int64 `json:"fileListSize"`
Rev string `json:"rev"`
StarLabel int64 `json:"starLabel"`
}
type Cloud189FilesResp struct {
//ResCode int `json:"res_code"`
//ResMessage string `json:"res_message"`
FileListAO struct {
Count int `json:"count"`
FileList []Cloud189File `json:"fileList"`
FolderList []Cloud189Folder `json:"folderList"`
} `json:"fileListAO"`
}
// TaskInfo 任务信息
type BatchTaskInfo struct {
// FileId 文件ID
FileId string `json:"fileId"`
// FileName 文件名
FileName string `json:"fileName"`
// IsFolder 是否是文件夹0-否1-是
IsFolder int `json:"isFolder"`
// SrcParentId 文件所在父目录ID
//SrcParentId string `json:"srcParentId"`
}
/*
type CreateUploadFileResult struct {
// UploadFileId 上传文件请求ID
UploadFileId int64 `json:"uploadFileId"`
// FileUploadUrl 上传文件数据的URL路径
FileUploadUrl string `json:"fileUploadUrl"`
// FileCommitUrl 上传文件完成后确认路径
FileCommitUrl string `json:"fileCommitUrl"`
// FileDataExists 文件是否已存在云盘中0-未存在1-已存在
FileDataExists int `json:"fileDataExists"`
}
type UploadFileStatusResult struct {
// 上传文件的ID
UploadFileId int64 `json:"uploadFileId"`
// 已上传的大小
DataSize int64 `json:"dataSize"`
FileUploadUrl string `json:"fileUploadUrl"`
FileCommitUrl string `json:"fileCommitUrl"`
FileDataExists int `json:"fileDataExists"`
}
*/
type InitMultiUploadResp struct {
//Code string `json:"code"`
Data struct {
UploadType int `json:"uploadType"`
UploadHost string `json:"uploadHost"`
UploadFileID string `json:"uploadFileId"`
FileDataExists int `json:"fileDataExists"`
} `json:"data"`
}
type UploadUrlsResp struct {
Code string `json:"code"`
UploadUrls map[string]Part `json:"uploadUrls"`
}
type Part struct {
RequestURL string `json:"requestURL"`
RequestHeader string `json:"requestHeader"`
}

177
drivers/189pc/util.go Normal file
View File

@ -0,0 +1,177 @@
package _189
import (
"bytes"
"crypto/aes"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"fmt"
rand2 "math/rand"
"net/http"
"net/url"
"sort"
"strings"
"time"
"github.com/Xhofe/alist/model"
)
const (
APP_ID = "8025431004"
CLIENT_TYPE = "10020"
VERSION = "6.2"
WEB_URL = "https://cloud.189.cn"
AUTH_URL = "https://open.e.189.cn"
API_URL = "https://api.cloud.189.cn"
UPLOAD_URL = "https://upload.cloud.189.cn"
RETURN_URL = "https://m.cloud.189.cn/zhuanti/2020/loginErrorPc/index.html"
PC = "TELEPC"
MAC = "TELEMAC"
CHANNEL_ID = "web_cloud.189.cn"
)
func clientSuffix() map[string]string {
return map[string]string{
"clientType": PC,
"version": VERSION,
"channelId": CHANNEL_ID,
"rand": fmt.Sprintf("%d_%d", rand2.Int63n(1e5), rand2.Int63n(1e10)),
}
}
// 带params的SignatureOfHmac HMAC签名
func signatureOfHmac(sessionSecret, sessionKey, operate, fullUrl, dateOfGmt, param string) string {
u, _ := url.Parse(fullUrl)
mac := hmac.New(sha1.New, []byte(sessionSecret))
data := fmt.Sprintf("SessionKey=%s&Operate=%s&RequestURI=%s&Date=%s", sessionKey, operate, u.Path, dateOfGmt)
if param != "" {
data += fmt.Sprintf("&params=%s", param)
}
mac.Write([]byte(data))
return strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))
}
// 获取http规范的时间
func getHttpDateStr() string {
return time.Now().UTC().Format(http.TimeFormat)
}
// RAS 加密用户名密码
func rsaEncrypt(publicKey, origData string) string {
block, _ := pem.Decode([]byte(publicKey))
pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
data, _ := rsa.EncryptPKCS1v15(rand.Reader, pubInterface.(*rsa.PublicKey), []byte(origData))
return base64ToHex(base64.StdEncoding.EncodeToString(data))
}
// aes 加密params
func AesECBEncrypt(data, key string) string {
block, _ := aes.NewCipher([]byte(key))
paddingData := PKCS7Padding([]byte(data), block.BlockSize())
decrypted := make([]byte, len(paddingData))
size := block.BlockSize()
for src, dst := paddingData, decrypted; len(src) > 0; src, dst = src[size:], dst[size:] {
block.Encrypt(dst[:size], src[:size])
}
return strings.ToUpper(hex.EncodeToString(decrypted))
}
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
// 时间戳
func timestamp() int64 {
return time.Now().UTC().UnixNano() / 1e6
}
func base64ToHex(a string) string {
v, _ := base64.StdEncoding.DecodeString(a)
return strings.ToUpper(hex.EncodeToString(v))
}
func isFamily(account *model.Account) bool {
return account.InternalType == "Family"
}
func toFamilyOrderBy(o string) string {
switch o {
case "filename":
return "1"
case "filesize":
return "2"
case "lastOpTime":
return "3"
default:
return "1"
}
}
func ParseHttpHeader(str string) map[string]string {
header := make(map[string]string)
for _, value := range strings.Split(str, "&") {
i := strings.Index(value, "=")
header[strings.TrimSpace(value[0:i])] = strings.TrimSpace(value[i+1:])
}
return header
}
func MustString(str string, err error) string {
return str
}
func MustToBytes(b []byte, err error) []byte {
return b
}
func BoolToNumber(b bool) int {
if b {
return 1
}
return 0
}
func MustParseTime(str string) *time.Time {
loc, _ := time.LoadLocation("Local")
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", str, loc)
return &lastOpTime
}
type Params map[string]string
func (p Params) Set(k, v string) {
p[k] = v
}
func (p Params) Encode() string {
if p == nil {
return ""
}
var buf strings.Builder
keys := make([]string, 0, len(p))
for k := range p {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
buf.WriteString(p[k])
}
return buf.String()
}

View File

@ -3,44 +3,17 @@ package alidrive
import (
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"path/filepath"
"strings"
"time"
)
var aliClient = resty.New()
type AliRespError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type AliFiles struct {
Items []AliFile `json:"items"`
NextMarker string `json:"next_marker"`
}
type AliFile struct {
DriveId string `json:"drive_id"`
CreatedAt *time.Time `json:"created_at"`
FileExtension string `json:"file_extension"`
FileId string `json:"file_id"`
Type string `json:"type"`
Name string `json:"name"`
Category string `json:"category"`
ParentFileId string `json:"parent_file_id"`
UpdatedAt *time.Time `json:"updated_at"`
Size int64 `json:"size"`
Thumbnail string `json:"thumbnail"`
Url string `json:"url"`
}
func (driver AliDrive) FormatFile(file *AliFile) *model.File {
f := &model.File{
Id: file.FileId,
@ -51,17 +24,7 @@ func (driver AliDrive) FormatFile(file *AliFile) *model.File {
Driver: driver.Config().Name,
Url: file.Url,
}
if file.Type == "folder" {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(file.FileExtension)
}
if file.Category == "video" {
f.Type = conf.VIDEO
}
if file.Category == "image" {
f.Type = conf.IMAGE
}
f.Type = file.GetType()
return f
}
@ -190,7 +153,7 @@ func (driver AliDrive) rename(fileId, name string, account *model.Account) error
return fmt.Errorf("%+v", resp)
}
func (driver AliDrive) batch(srcId, dstId string, account *model.Account) error {
func (driver AliDrive) batch(srcId, dstId string, url string, account *model.Account) error {
var e AliRespError
res, err := aliClient.R().SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
@ -208,6 +171,7 @@ func (driver AliDrive) batch(srcId, dstId string, account *model.Account) error
"to_drive_id": account.DriveId,
"to_parent_file_id": dstId,
},
"url": url,
},
},
"resource": "file",
@ -222,12 +186,13 @@ func (driver AliDrive) batch(srcId, dstId string, account *model.Account) error
return err
} else {
_ = model.SaveAccount(account)
return driver.batch(srcId, dstId, account)
return driver.batch(srcId, dstId, url, account)
}
}
return fmt.Errorf("%s", e.Message)
}
if strings.Contains(res.String(), `"status":200`) {
status := jsoniter.Get(res.Body(), "responses", 0, "status").ToInt()
if status < 400 && status >= 100 {
return nil
}
return errors.New(res.String())
@ -236,6 +201,7 @@ func (driver AliDrive) batch(srcId, dstId string, account *model.Account) error
func init() {
base.RegisterDriver(&AliDrive{})
aliClient.
SetTimeout(base.DefaultTimeout).
SetRetryCount(3).
SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36").
SetHeader("content-type", "application/json").

View File

@ -2,19 +2,25 @@ package alidrive
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"math/big"
"net/http"
"os"
"path/filepath"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
"io"
"math"
"net/http"
"path/filepath"
)
type AliDrive struct{}
@ -60,6 +66,11 @@ func (driver AliDrive) Items() []base.Item {
Required: false,
Description: ">0 and <=200",
},
{
Name: "bool_1",
Label: "fast upload",
Type: base.TypeBool,
},
}
}
@ -67,6 +78,9 @@ func (driver AliDrive) Save(account *model.Account, old *model.Account) error {
if old != nil {
conf.Cron.Remove(cron.EntryID(old.CronId))
}
if account == nil {
return nil
}
if account.RootFolder == "" {
account.RootFolder = "root"
}
@ -85,15 +99,15 @@ func (driver AliDrive) Save(account *model.Account, old *model.Account) error {
log.Debugf("user info: %+v", resp)
account.DriveId = resp["default_drive_id"].(string)
cronId, err := conf.Cron.AddFunc("@every 2h", func() {
name := account.Name
log.Debugf("ali account name: %s", name)
newAccount, ok := model.GetAccount(name)
id := account.ID
log.Debugf("ali account id: %d", id)
newAccount, err := model.GetAccountById(id)
log.Debugf("ali account: %+v", newAccount)
if !ok {
if err != nil {
return
}
err = driver.RefreshToken(&newAccount)
_ = model.SaveAccount(&newAccount)
err = driver.RefreshToken(newAccount)
_ = model.SaveAccount(newAccount)
})
if err != nil {
return err
@ -188,6 +202,12 @@ func (driver AliDrive) Link(args base.Args, account *model.Account) (*base.Link,
return nil, fmt.Errorf("%s", e.Message)
}
return &base.Link{
Headers: []base.Header{
{
Name: "Referer",
Value: "https://www.aliyundrive.com/",
},
},
Url: resp["url"].(string),
}, nil
}
@ -209,10 +229,10 @@ func (driver AliDrive) Path(path string, account *model.Account) (*model.File, [
return nil, files, nil
}
func (driver AliDrive) Proxy(c *gin.Context, account *model.Account) {
c.Request.Header.Del("Origin")
c.Request.Header.Set("Referer", "https://www.aliyundrive.com/")
}
//func (driver AliDrive) Proxy(r *http.Request, account *model.Account) {
// r.Header.Del("Origin")
// r.Header.Set("Referer", "https://www.aliyundrive.com/")
//}
func (driver AliDrive) Preview(path string, account *model.Account) (interface{}, error) {
file, err := driver.GetFile(path, account)
@ -301,7 +321,7 @@ func (driver AliDrive) Move(src string, dst string, account *model.Account) erro
if err != nil {
return err
}
err = driver.batch(srcFile.Id, dstDirFile.Id, account)
err = driver.batch(srcFile.Id, dstDirFile.Id, "/file/move", account)
return err
}
@ -316,7 +336,17 @@ func (driver AliDrive) Rename(src string, dst string, account *model.Account) er
}
func (driver AliDrive) Copy(src string, dst string, account *model.Account) error {
return base.ErrNotSupport
dstDir, _ := filepath.Split(dst)
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
dstDirFile, err := driver.File(dstDir, account)
if err != nil {
return err
}
err = driver.batch(srcFile.Id, dstDirFile.Id, "/file/copy", account)
return err
}
func (driver AliDrive) Delete(path string, account *model.Account) error {
@ -346,7 +376,7 @@ func (driver AliDrive) Delete(path string, account *model.Account) error {
}
return fmt.Errorf("%s", e.Message)
}
if res.StatusCode() == 204 {
if res.StatusCode() < 400 {
return nil
}
return errors.New(res.String())
@ -358,15 +388,15 @@ type UploadResp struct {
PartInfoList []struct {
UploadUrl string `json:"upload_url"`
} `json:"part_info_list"`
RapidUpload bool `json:"rapid_upload"`
}
func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) error {
if file == nil {
return base.ErrEmptyFile
}
const DEFAULT uint64 = 10485760
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
var finish uint64 = 0
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
@ -374,32 +404,51 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
if !parentFile.IsDir() {
return base.ErrNotFolder
}
var resp UploadResp
var e AliRespError
partInfoList := make([]base.Json, 0)
var i int64
for i = 0; i < count; i++ {
partInfoList = append(partInfoList, base.Json{
"part_number": i + 1,
})
const DEFAULT int64 = 10485760
var count = int(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
partInfoList := make([]base.Json, 0, count)
for i := 1; i <= count; i++ {
partInfoList = append(partInfoList, base.Json{"part_number": i})
}
_, err = aliClient.R().SetResult(&resp).SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(base.Json{
"check_name_mode": "auto_rename",
// content_hash
"content_hash_name": "none",
reqBody := base.Json{
"check_name_mode": "overwrite",
"drive_id": account.DriveId,
"name": file.GetFileName(),
"parent_file_id": parentFile.Id,
"part_info_list": partInfoList,
//proof_code
"proof_version": "v1",
"size": file.GetSize(),
"type": "file",
}).Post("https://api.aliyundrive.com/adrive/v2/file/createWithFolders") // /v2/file/create_with_proof
//log.Debugf("%+v\n%+v", resp, e)
if e.Code != "" {
}
if account.Bool1 {
buf := bytes.NewBuffer(make([]byte, 0, 1024))
io.CopyN(buf, file, 1024)
reqBody["pre_hash"] = utils.GetSHA1Encode(buf.String())
// 把头部拼接回去
file.File = struct {
io.Reader
io.Closer
}{
Reader: io.MultiReader(buf, file.File),
Closer: file.File,
}
} else {
reqBody["content_hash_name"] = "none"
reqBody["proof_version"] = "v1"
}
var resp UploadResp
var e AliRespError
client := aliClient.R().SetResult(&resp).SetError(&e).SetHeader("authorization", "Bearer\t"+account.AccessToken).SetBody(reqBody)
_, err = client.Post("https://api.aliyundrive.com/adrive/v2/file/createWithFolders")
if err != nil {
return err
}
if e.Code != "" && e.Code != "PreHashMatched" {
if e.Code == "AccessTokenInvalid" {
err = driver.RefreshToken(account)
if err != nil {
@ -411,26 +460,61 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
}
return fmt.Errorf("%s", e.Message)
}
var byteSize uint64
for i = 0; i < count; i++ {
byteSize = file.GetSize() - finish
if DEFAULT < byteSize {
byteSize = DEFAULT
}
log.Debugf("%d,%d", byteSize, finish)
byteData := make([]byte, byteSize)
n, err := io.ReadFull(file, byteData)
//n, err := file.Read(byteData)
//byteData, err := io.ReadAll(file)
//n := len(byteData)
log.Debug(err, n)
if account.Bool1 && e.Code == "PreHashMatched" {
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
finish += uint64(n)
defer tempFile.Close()
defer os.Remove(tempFile.Name())
req, err := http.NewRequest("PUT", resp.PartInfoList[i].UploadUrl, bytes.NewBuffer(byteData))
delete(reqBody, "pre_hash")
h := sha1.New()
if _, err = io.Copy(io.MultiWriter(tempFile, h), file.File); err != nil {
return err
}
reqBody["content_hash"] = hex.EncodeToString(h.Sum(nil))
reqBody["content_hash_name"] = "sha1"
reqBody["proof_version"] = "v1"
/*
js 隐性转换太坑不知道有没有bug
var n = e.access_token
r = new BigNumber('0x'.concat(md5(n).slice(0, 16)))
i = new BigNumber(t.file.size)
o = i ? r.mod(i) : new gt.BigNumber(0);
(t.file.slice(o.toNumber(), Math.min(o.plus(8).toNumber(), t.file.size)))
*/
buf := make([]byte, 8)
r, _ := new(big.Int).SetString(utils.GetMD5Encode(account.AccessToken)[:16], 16)
i := new(big.Int).SetUint64(file.Size)
o := r.Mod(r, i)
n, _ := io.NewSectionReader(tempFile, o.Int64(), 8).Read(buf[:8])
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
_, err = client.Post("https://api.aliyundrive.com/adrive/v2/file/createWithFolders")
if err != nil {
return err
}
if e.Code != "" && e.Code != "PreHashMatched" {
return fmt.Errorf("%s", e.Message)
}
if resp.RapidUpload {
return nil
}
// 秒传失败
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return err
}
file.File = tempFile
}
for _, partInfo := range resp.PartInfoList {
req, err := http.NewRequest("PUT", partInfo.UploadUrl, io.LimitReader(file.File, DEFAULT))
if err != nil {
return err
}
@ -439,6 +523,7 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
return err
}
log.Debugf("%+v", res)
res.Body.Close()
//res, err := base.BaseClient.R().
// SetHeader("Content-Type","").
// SetBody(byteData).Put(resp.PartInfoList[i].UploadUrl)
@ -455,7 +540,10 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
"file_id": resp.FileId,
"upload_id": resp.UploadId,
}).Post("https://api.aliyundrive.com/v2/file/complete")
if e.Code != "" {
if err != nil {
return err
}
if e.Code != "" && e.Code != "PreHashMatched" {
//if e.Code == "AccessTokenInvalid" {
// err = driver.RefreshToken(account)
// if err != nil {

53
drivers/alidrive/types.go Normal file
View File

@ -0,0 +1,53 @@
package alidrive
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/utils"
"time"
)
type AliRespError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type AliFiles struct {
Items []AliFile `json:"items"`
NextMarker string `json:"next_marker"`
}
type AliFile struct {
DriveId string `json:"drive_id"`
CreatedAt *time.Time `json:"created_at"`
FileExtension string `json:"file_extension"`
FileId string `json:"file_id"`
Type string `json:"type"`
Name string `json:"name"`
Category string `json:"category"`
ParentFileId string `json:"parent_file_id"`
UpdatedAt *time.Time `json:"updated_at"`
Size int64 `json:"size"`
Thumbnail string `json:"thumbnail"`
Url string `json:"url"`
}
func (f AliFile) GetSize() uint64 {
return uint64(f.Size)
}
func (f AliFile) GetName() string {
return f.Name
}
func (f AliFile) GetType() int {
if f.Type == "folder" {
return conf.FOLDER
}
if f.Category == "video" {
return conf.VIDEO
}
if f.Category == "image" {
return conf.IMAGE
}
return utils.GetFileType(f.FileExtension)
}

View File

@ -7,7 +7,6 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
"path/filepath"
"strings"
"time"
@ -48,6 +47,9 @@ func (driver Alist) Items() []base.Item {
}
func (driver Alist) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
account.SiteUrl = strings.TrimRight(account.SiteUrl, "/")
if account.RootFolder == "" {
account.RootFolder = "/"
@ -145,7 +147,7 @@ func (driver Alist) Path(path string, account *model.Account) (*model.File, []mo
return nil, resp.Data.Files, nil
}
func (driver Alist) Proxy(c *gin.Context, account *model.Account) {}
//func (driver Alist) Proxy(r *http.Request, account *model.Account) {}
func (driver Alist) Preview(path string, account *model.Account) (interface{}, error) {
var resp PathResp

View File

@ -4,8 +4,10 @@ import (
_ "github.com/Xhofe/alist/drivers/123"
_ "github.com/Xhofe/alist/drivers/139"
_ "github.com/Xhofe/alist/drivers/189"
_ "github.com/Xhofe/alist/drivers/189pc"
_ "github.com/Xhofe/alist/drivers/alidrive"
_ "github.com/Xhofe/alist/drivers/alist"
_ "github.com/Xhofe/alist/drivers/baidu"
"github.com/Xhofe/alist/drivers/base"
_ "github.com/Xhofe/alist/drivers/ftp"
_ "github.com/Xhofe/alist/drivers/google"
@ -14,10 +16,16 @@ import (
_ "github.com/Xhofe/alist/drivers/native"
_ "github.com/Xhofe/alist/drivers/onedrive"
_ "github.com/Xhofe/alist/drivers/pikpak"
_ "github.com/Xhofe/alist/drivers/quark"
_ "github.com/Xhofe/alist/drivers/s3"
_ "github.com/Xhofe/alist/drivers/sftp"
_ "github.com/Xhofe/alist/drivers/shandian"
_ "github.com/Xhofe/alist/drivers/teambition"
_ "github.com/Xhofe/alist/drivers/uss"
_ "github.com/Xhofe/alist/drivers/webdav"
_ "github.com/Xhofe/alist/drivers/xunlei"
_ "github.com/Xhofe/alist/drivers/yandex"
_ "github.com/Xhofe/alist/drivers/baiduphoto"
log "github.com/sirupsen/logrus"
"strings"
)

185
drivers/baidu/baidu.go Normal file
View File

@ -0,0 +1,185 @@
package baidu
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
"path"
"strconv"
)
func (driver Baidu) RefreshToken(account *model.Account) error {
err := driver.refreshToken(account)
if err != nil && err == base.ErrEmptyToken {
err = driver.refreshToken(account)
}
if err != nil {
account.Status = err.Error()
}
_ = model.SaveAccount(account)
return err
}
func (driver Baidu) refreshToken(account *model.Account) error {
u := "https://openapi.baidu.com/oauth/2.0/token"
var resp base.TokenResp
var e TokenErrResp
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
"grant_type": "refresh_token",
"refresh_token": account.RefreshToken,
"client_id": account.ClientId,
"client_secret": account.ClientSecret,
}).Get(u)
if err != nil {
return err
}
if e.Error != "" {
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
}
if resp.RefreshToken == "" {
return base.ErrEmptyToken
}
account.Status = "work"
account.AccessToken, account.RefreshToken = resp.AccessToken, resp.RefreshToken
return nil
}
func (driver Baidu) Request(fullurl string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
req := base.RestyClient.R()
req.SetQueryParam("access_token", account.AccessToken)
if headers != nil {
req.SetHeaders(headers)
}
if query != nil {
req.SetQueryParams(query)
}
if form != nil {
req.SetFormData(form)
}
if data != nil {
req.SetBody(data)
}
if resp != nil {
req.SetResult(resp)
}
var res *resty.Response
var err error
switch method {
case base.Get:
res, err = req.Get(fullurl)
case base.Post:
res, err = req.Post(fullurl)
case base.Patch:
res, err = req.Patch(fullurl)
case base.Delete:
res, err = req.Delete(fullurl)
case base.Put:
res, err = req.Put(fullurl)
default:
return nil, base.ErrNotSupport
}
if err != nil {
return nil, err
}
//log.Debug(res.String())
errno := jsoniter.Get(res.Body(), "errno").ToInt()
if errno != 0 {
if errno == -6 {
err = driver.RefreshToken(account)
if err != nil {
return nil, err
}
return driver.Request(fullurl, method, headers, query, form, data, resp, account)
}
return nil, fmt.Errorf("errno: %d, refer to https://pan.baidu.com/union/doc/", errno)
}
return res.Body(), nil
}
func (driver Baidu) Get(pathname string, params map[string]string, resp interface{}, account *model.Account) ([]byte, error) {
return driver.Request("https://pan.baidu.com/rest/2.0"+pathname, base.Get, nil, params, nil, nil, resp, account)
}
func (driver Baidu) Post(pathname string, params map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
return driver.Request("https://pan.baidu.com/rest/2.0"+pathname, base.Post, nil, params, nil, data, resp, account)
}
func (driver Baidu) manage(opera string, filelist interface{}, account *model.Account) ([]byte, error) {
params := map[string]string{
"method": "filemanager",
"opera": opera,
}
marshal, err := utils.Json.Marshal(filelist)
if err != nil {
return nil, err
}
data := fmt.Sprintf("async=0&filelist=%s&ondup=newcopy", string(marshal))
return driver.Post("/xpan/file", params, data, nil, account)
}
func (driver Baidu) GetFiles(dir string, account *model.Account) ([]model.File, error) {
dir = utils.Join(account.RootFolder, dir)
start := 0
limit := 200
params := map[string]string{
"method": "list",
"dir": dir,
"web": "web",
}
if account.OrderBy != "" {
params["order"] = account.OrderBy
if account.OrderDirection == "desc" {
params["desc"] = "1"
}
}
res := make([]model.File, 0)
for {
params["start"] = strconv.Itoa(start)
params["limit"] = strconv.Itoa(limit)
start += limit
var resp ListResp
_, err := driver.Get("/xpan/file", params, &resp, account)
if err != nil {
return nil, err
}
if len(resp.List) == 0 {
break
}
for _, f := range resp.List {
file := model.File{
Id: strconv.FormatInt(f.FsId, 10),
Name: f.ServerFilename,
Size: f.Size,
Driver: driver.Config().Name,
UpdatedAt: getTime(f.ServerMtime),
Thumbnail: f.Thumbs.Url3,
}
if f.Isdir == 1 {
file.Type = conf.FOLDER
} else {
file.Type = utils.GetFileType(path.Ext(f.ServerFilename))
}
res = append(res, file)
}
}
return res, nil
}
func (driver Baidu) create(path string, size uint64, isdir int, uploadid, block_list string, account *model.Account) ([]byte, error) {
params := map[string]string{
"method": "create",
}
data := fmt.Sprintf("path=%s&size=%d&isdir=%d", path, size, isdir)
if uploadid != "" {
data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list)
}
return driver.Post("/xpan/file", params, data, nil, account)
}
func init() {
base.RegisterDriver(&Baidu{})
}

399
drivers/baidu/driver.go Normal file
View File

@ -0,0 +1,399 @@
package baidu
import (
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
"math"
"os"
"path/filepath"
"strconv"
"strings"
)
type Baidu struct{}
func (driver Baidu) Config() base.DriverConfig {
return base.DriverConfig{
Name: "Baidu.Disk",
}
}
func (driver Baidu) Items() []base.Item {
return []base.Item{
{
Name: "refresh_token",
Label: "refresh token",
Type: base.TypeString,
Required: true,
},
{
Name: "root_folder",
Label: "root folder path",
Type: base.TypeString,
Default: "/",
Required: true,
},
{
Name: "order_by",
Label: "order_by",
Type: base.TypeSelect,
Default: "name",
Values: "name,time,size",
Required: false,
},
{
Name: "order_direction",
Label: "order_direction",
Type: base.TypeSelect,
Values: "asc,desc",
Default: "asc",
Required: false,
},
{
Name: "internal_type",
Label: "download api",
Type: base.TypeSelect,
Required: true,
Values: "official,crack",
Default: "official",
},
{
Name: "client_id",
Label: "client id",
Default: "iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v",
Type: base.TypeString,
Required: true,
},
{
Name: "client_secret",
Label: "client secret",
Default: "jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG",
Type: base.TypeString,
Required: true,
},
}
}
func (driver Baidu) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
return driver.RefreshToken(account)
}
func (driver Baidu) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver Baidu) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
cache, err := base.GetCache(path, account)
if err == nil {
files, _ := cache.([]model.File)
return files, nil
}
files, err := driver.GetFiles(path, account)
if err != nil {
return nil, err
}
if len(files) > 0 {
_ = base.SetCache(path, files, account)
}
return files, nil
}
func (driver Baidu) Link(args base.Args, account *model.Account) (*base.Link, error) {
if account.InternalType == "crack" {
return driver.LinkCrack(args, account)
}
return driver.LinkOfficial(args, account)
}
func (driver Baidu) LinkOfficial(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(args.Path, account)
if err != nil {
return nil, err
}
if file.IsDir() {
return nil, base.ErrNotFile
}
var resp DownloadResp
params := map[string]string{
"method": "filemetas",
"fsids": fmt.Sprintf("[%s]", file.Id),
"dlink": "1",
}
_, err = driver.Get("/xpan/multimedia", params, &resp, account)
if err != nil {
return nil, err
}
u := fmt.Sprintf("%s&access_token=%s", resp.List[0].Dlink, account.AccessToken)
res, err := base.NoRedirectClient.R().SetHeader("User-Agent", "pan.baidu.com").Head(u)
if err != nil {
return nil, err
}
//if res.StatusCode() == 302 {
u = res.Header().Get("location")
//}
return &base.Link{
Url: u,
Headers: []base.Header{
{Name: "User-Agent", Value: "pan.baidu.com"},
}}, nil
}
func (driver Baidu) LinkCrack(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(args.Path, account)
if err != nil {
return nil, err
}
if file.IsDir() {
return nil, base.ErrNotFile
}
var resp DownloadResp2
param := map[string]string{
"target": fmt.Sprintf("[\"%s\"]", utils.Join(account.RootFolder, args.Path)),
"dlink": "1",
"web": "5",
"origin": "dlna",
}
_, err = driver.Request("https://pan.baidu.com/api/filemetas", base.Get, nil, param, nil, nil, &resp, account)
if err != nil {
return nil, err
}
return &base.Link{
Url: resp.Info[0].Dlink,
Headers: []base.Header{
{Name: "User-Agent", Value: "pan.baidu.com"},
}}, nil
}
func (driver Baidu) Path(path string, account *model.Account) (*model.File, []model.File, error) {
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
//func (driver Baidu) Proxy(r *http.Request, account *model.Account) {
// r.Header.Set("User-Agent", "pan.baidu.com")
//}
func (driver Baidu) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver Baidu) MakeDir(path string, account *model.Account) error {
_, err := driver.create(utils.Join(account.RootFolder, path), 0, 1, "", "", account)
return err
}
func (driver Baidu) Move(src string, dst string, account *model.Account) error {
path := utils.Join(account.RootFolder, src)
dest, newname := utils.Split(utils.Join(account.RootFolder, dst))
data := []base.Json{
{
"path": path,
"dest": dest,
"newname": newname,
},
}
_, err := driver.manage("move", data, account)
return err
}
func (driver Baidu) Rename(src string, dst string, account *model.Account) error {
path := utils.Join(account.RootFolder, src)
newname := utils.Base(dst)
data := []base.Json{
{
"path": path,
"newname": newname,
},
}
_, err := driver.manage("rename", data, account)
return err
}
func (driver Baidu) Copy(src string, dst string, account *model.Account) error {
path := utils.Join(account.RootFolder, src)
dest, newname := utils.Split(utils.Join(account.RootFolder, dst))
data := []base.Json{
{
"path": path,
"dest": dest,
"newname": newname,
},
}
_, err := driver.manage("copy", data, account)
return err
}
func (driver Baidu) Delete(path string, account *model.Account) error {
path = utils.Join(account.RootFolder, path)
data := []string{path}
_, err := driver.manage("delete", data, account)
return err
}
func (driver Baidu) Upload(file *model.FileStream, account *model.Account) error {
if file == nil {
return base.ErrEmptyFile
}
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
_, err = io.Copy(tempFile, file)
if err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
var Default uint64 = 4 * 1024 * 1024
defaultByteData := make([]byte, Default)
count := int(math.Ceil(float64(file.GetSize()) / float64(Default)))
var SliceSize uint64 = 256 * 1024
// cal md5
h1 := md5.New()
h2 := md5.New()
block_list := make([]string, 0)
content_md5 := ""
slice_md5 := ""
left := file.GetSize()
for i := 0; i < count; i++ {
byteSize := Default
var byteData []byte
if left < Default {
byteSize = left
byteData = make([]byte, byteSize)
} else {
byteData = defaultByteData
}
left -= byteSize
_, err = io.ReadFull(tempFile, byteData)
if err != nil {
return err
}
h1.Write(byteData)
h2.Write(byteData)
block_list = append(block_list, fmt.Sprintf("\"%s\"", hex.EncodeToString(h2.Sum(nil))))
h2.Reset()
}
content_md5 = hex.EncodeToString(h1.Sum(nil))
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
if file.GetSize() <= SliceSize {
slice_md5 = content_md5
} else {
sliceData := make([]byte, SliceSize)
_, err = io.ReadFull(tempFile, sliceData)
if err != nil {
return err
}
h2.Write(sliceData)
slice_md5 = hex.EncodeToString(h2.Sum(nil))
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
}
path := encodeURIComponent(utils.Join(account.RootFolder, file.ParentPath, file.Name))
block_list_str := fmt.Sprintf("[%s]", strings.Join(block_list, ","))
data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&block_list=%s&content-md5=%s&slice-md5=%s",
path, file.GetSize(),
block_list_str,
content_md5, slice_md5)
params := map[string]string{
"method": "precreate",
}
var precreateResp PrecreateResp
_, err = driver.Post("/xpan/file", params, data, &precreateResp, account)
if err != nil {
return err
}
log.Debugf("%+v", precreateResp)
if precreateResp.ReturnType == 2 {
return nil
}
params = map[string]string{
"method": "upload",
"access_token": account.AccessToken,
"type": "tmpfile",
"path": path,
"uploadid": precreateResp.Uploadid,
}
left = file.GetSize()
for _, partseq := range precreateResp.BlockList {
byteSize := Default
var byteData []byte
if left < Default {
byteSize = left
byteData = make([]byte, byteSize)
} else {
byteData = defaultByteData
}
left -= byteSize
_, err = io.ReadFull(tempFile, byteData)
if err != nil {
return err
}
u := "https://d.pcs.baidu.com/rest/2.0/pcs/superfile2"
params["partseq"] = strconv.Itoa(partseq)
res, err := base.RestyClient.R().SetQueryParams(params).SetFileReader("file", file.Name, bytes.NewReader(byteData)).Post(u)
if err != nil {
return err
}
log.Debugln(res.String())
}
_, err = driver.create(path, file.GetSize(), 0, precreateResp.Uploadid, block_list_str, account)
return err
}
var _ base.Driver = (*Baidu)(nil)

143
drivers/baidu/types.go Normal file
View File

@ -0,0 +1,143 @@
package baidu
type TokenErrResp struct {
ErrorDescription string `json:"error_description"`
Error string `json:"error"`
}
type File struct {
//TkbindId int `json:"tkbind_id"`
//OwnerType int `json:"owner_type"`
//Category int `json:"category"`
//RealCategory string `json:"real_category"`
FsId int64 `json:"fs_id"`
ServerMtime int64 `json:"server_mtime"`
//OperId int `json:"oper_id"`
//ServerCtime int `json:"server_ctime"`
Thumbs struct {
//Icon string `json:"icon"`
Url3 string `json:"url3"`
//Url2 string `json:"url2"`
//Url1 string `json:"url1"`
} `json:"thumbs"`
//Wpfile int `json:"wpfile"`
//LocalMtime int `json:"local_mtime"`
Size int64 `json:"size"`
//ExtentTinyint7 int `json:"extent_tinyint7"`
Path string `json:"path"`
//Share int `json:"share"`
//ServerAtime int `json:"server_atime"`
//Pl int `json:"pl"`
//LocalCtime int `json:"local_ctime"`
ServerFilename string `json:"server_filename"`
//Md5 string `json:"md5"`
//OwnerId int `json:"owner_id"`
//Unlist int `json:"unlist"`
Isdir int `json:"isdir"`
}
type ListResp struct {
Errno int `json:"errno"`
GuidInfo string `json:"guid_info"`
List []File `json:"list"`
RequestId int64 `json:"request_id"`
Guid int `json:"guid"`
}
type DownloadResp struct {
Errmsg string `json:"errmsg"`
Errno int `json:"errno"`
List []struct {
//Category int `json:"category"`
//DateTaken int `json:"date_taken,omitempty"`
Dlink string `json:"dlink"`
//Filename string `json:"filename"`
//FsId int64 `json:"fs_id"`
//Height int `json:"height,omitempty"`
//Isdir int `json:"isdir"`
//Md5 string `json:"md5"`
//OperId int `json:"oper_id"`
//Path string `json:"path"`
//ServerCtime int `json:"server_ctime"`
//ServerMtime int `json:"server_mtime"`
//Size int `json:"size"`
//Thumbs struct {
// Icon string `json:"icon,omitempty"`
// Url1 string `json:"url1,omitempty"`
// Url2 string `json:"url2,omitempty"`
// Url3 string `json:"url3,omitempty"`
//} `json:"thumbs"`
//Width int `json:"width,omitempty"`
} `json:"list"`
//Names struct {
//} `json:"names"`
RequestId string `json:"request_id"`
}
type DownloadResp2 struct {
Errno int `json:"errno"`
Info []struct {
//ExtentTinyint4 int `json:"extent_tinyint4"`
//ExtentTinyint1 int `json:"extent_tinyint1"`
//Bitmap string `json:"bitmap"`
//Category int `json:"category"`
//Isdir int `json:"isdir"`
//Videotag int `json:"videotag"`
Dlink string `json:"dlink"`
//OperID int64 `json:"oper_id"`
//PathMd5 int `json:"path_md5"`
//Wpfile int `json:"wpfile"`
//LocalMtime int `json:"local_mtime"`
/*Thumbs struct {
Icon string `json:"icon"`
URL3 string `json:"url3"`
URL2 string `json:"url2"`
URL1 string `json:"url1"`
} `json:"thumbs"`*/
//PlaySource int `json:"play_source"`
//Share int `json:"share"`
//FileKey string `json:"file_key"`
//Errno int `json:"errno"`
//LocalCtime int `json:"local_ctime"`
//Rotate int `json:"rotate"`
//Metadata time.Time `json:"metadata"`
//Height int `json:"height"`
//SampleRate int `json:"sample_rate"`
//Width int `json:"width"`
//OwnerType int `json:"owner_type"`
//Privacy int `json:"privacy"`
//ExtentInt3 int64 `json:"extent_int3"`
//RealCategory string `json:"real_category"`
//SrcLocation string `json:"src_location"`
//MetaInfo string `json:"meta_info"`
//ID string `json:"id"`
//Duration int `json:"duration"`
//FileSize string `json:"file_size"`
//Channels int `json:"channels"`
//UseSegment int `json:"use_segment"`
//ServerCtime int `json:"server_ctime"`
//Resolution string `json:"resolution"`
//OwnerID int `json:"owner_id"`
//ExtraInfo string `json:"extra_info"`
//Size int `json:"size"`
//FsID int64 `json:"fs_id"`
//ExtentTinyint3 int `json:"extent_tinyint3"`
//Md5 string `json:"md5"`
//Path string `json:"path"`
//FrameRate int `json:"frame_rate"`
//ExtentTinyint2 int `json:"extent_tinyint2"`
//ServerFilename string `json:"server_filename"`
//ServerMtime int `json:"server_mtime"`
//TkbindID int `json:"tkbind_id"`
} `json:"info"`
RequestID int64 `json:"request_id"`
}
type PrecreateResp struct {
Path string `json:"path"`
Uploadid string `json:"uploadid"`
ReturnType int `json:"return_type"`
BlockList []int `json:"block_list"`
Errno int `json:"errno"`
RequestId int64 `json:"request_id"`
}

18
drivers/baidu/util.go Normal file
View File

@ -0,0 +1,18 @@
package baidu
import (
"net/url"
"strings"
"time"
)
func getTime(t int64) *time.Time {
tm := time.Unix(t, 0)
return &tm
}
func encodeURIComponent(str string) string {
r := url.QueryEscape(str)
r = strings.ReplaceAll(r, "+", "%20")
return r
}

259
drivers/baiduphoto/baidu.go Normal file
View File

@ -0,0 +1,259 @@
package baiduphoto
import (
"fmt"
"net/http"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
func (driver Baidu) RefreshToken(account *model.Account) error {
err := driver.refreshToken(account)
if err != nil && err == base.ErrEmptyToken {
err = driver.refreshToken(account)
}
if err != nil {
account.Status = err.Error()
}
_ = model.SaveAccount(account)
return err
}
func (driver Baidu) refreshToken(account *model.Account) error {
u := "https://openapi.baidu.com/oauth/2.0/token"
var resp base.TokenResp
var e TokenErrResp
_, err := base.RestyClient.R().
SetResult(&resp).
SetError(&e).
SetQueryParams(map[string]string{
"grant_type": "refresh_token",
"refresh_token": account.RefreshToken,
"client_id": account.ClientId,
"client_secret": account.ClientSecret,
}).Get(u)
if err != nil {
return err
}
if e.ErrorMsg != "" {
return &e
}
if resp.RefreshToken == "" {
return base.ErrEmptyToken
}
account.Status = "work"
account.AccessToken, account.RefreshToken = resp.AccessToken, resp.RefreshToken
return nil
}
func (driver Baidu) Request(method string, url string, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
req := base.RestyClient.R()
req.SetQueryParam("access_token", account.AccessToken)
if callback != nil {
callback(req)
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
log.Debug(res.String())
var erron Erron
if err = utils.Json.Unmarshal(res.Body(), &erron); err != nil {
return nil, err
}
switch erron.Errno {
case 0:
return res, nil
case -6:
if err = driver.RefreshToken(account); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron.Errno)
}
return driver.Request(method, url, callback, account)
}
// 获取所有根文件
func (driver Baidu) GetAllFile(account *model.Account) (files []File, err error) {
var cursor string
for {
var resp FileListResp
_, err = driver.Request(http.MethodGet, FILE_API_URL_V1+"/list", func(r *resty.Request) {
r.SetQueryParams(map[string]string{
"need_thumbnail": "1",
"need_filter_hidden": "0",
"cursor": cursor,
})
r.SetResult(&resp)
}, account)
if err != nil {
return
}
cursor = resp.Cursor
files = append(files, resp.List...)
if !resp.HasNextPage() {
return
}
}
}
// 获取所有相册
func (driver Baidu) GetAllAlbum(account *model.Account) (albums []Album, err error) {
var cursor string
for {
var resp AlbumListResp
_, err = driver.Request(http.MethodGet, ALBUM_API_URL+"/list", func(r *resty.Request) {
r.SetQueryParams(map[string]string{
"need_amount": "1",
"limit": "100",
"cursor": cursor,
})
r.SetResult(&resp)
}, account)
if err != nil {
return
}
if albums == nil {
albums = make([]Album, 0, resp.TotalCount)
}
cursor = resp.Cursor
albums = append(albums, resp.List...)
if !resp.HasNextPage() {
return
}
}
}
// 获取相册中所有文件
func (driver Baidu) GetAllAlbumFile(albumID string, account *model.Account) (files []AlbumFile, err error) {
var cursor string
for {
var resp AlbumFileListResp
_, err = driver.Request(http.MethodGet, ALBUM_API_URL+"/listfile", func(r *resty.Request) {
r.SetQueryParams(map[string]string{
"album_id": splitID(albumID)[0],
"need_amount": "1",
"limit": "1000",
"cursor": cursor,
})
r.SetResult(&resp)
}, account)
if err != nil {
return
}
if files == nil {
files = make([]AlbumFile, 0, resp.TotalCount)
}
cursor = resp.Cursor
files = append(files, resp.List...)
if !resp.HasNextPage() {
return
}
}
}
// 创建相册
func (driver Baidu) CreateAlbum(name string, account *model.Account) error {
if !checkName(name) {
return ErrNotSupportName
}
_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/create", func(r *resty.Request) {
r.SetQueryParams(map[string]string{
"title": name,
"tid": getTid(),
"source": "0",
})
}, account)
return err
}
// 相册改名
func (driver Baidu) SetAlbumName(albumID string, name string, account *model.Account) error {
if !checkName(name) {
return ErrNotSupportName
}
e := splitID(albumID)
_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/settitle", func(r *resty.Request) {
r.SetFormData(map[string]string{
"title": name,
"album_id": e[0],
"tid": e[1],
})
}, account)
return err
}
// 删除相册
func (driver Baidu) DeleteAlbum(albumID string, account *model.Account) error {
e := splitID(albumID)
_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/delete", func(r *resty.Request) {
r.SetFormData(map[string]string{
"album_id": e[0],
"tid": e[1],
"delete_origin_image": "0", // 是否删除原图 0 不删除
})
}, account)
return err
}
// 删除相册文件
func (driver Baidu) DeleteAlbumFile(albumID string, account *model.Account, fileIDs ...string) error {
e := splitID(albumID)
_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/delfile", func(r *resty.Request) {
r.SetFormData(map[string]string{
"album_id": e[0],
"tid": e[1],
"list": fsidsFormat(fileIDs...),
"del_origin": "0", // 是否删除原图 0 不删除 1 删除
})
}, account)
return err
}
// 增加相册文件
func (driver Baidu) AddAlbumFile(albumID string, account *model.Account, fileIDs ...string) error {
e := splitID(albumID)
_, err := driver.Request(http.MethodGet, ALBUM_API_URL+"/addfile", func(r *resty.Request) {
r.SetQueryParams(map[string]string{
"album_id": e[0],
"tid": e[1],
"list": fsidsFormatNotUk(fileIDs...),
})
}, account)
return err
}
// 保存相册文件为根文件
func (driver Baidu) CopyAlbumFile(albumID string, account *model.Account, fileID string) (*CopyFile, error) {
var resp CopyFileResp
e := splitID(fileID)
_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/copyfile", func(r *resty.Request) {
r.SetFormData(map[string]string{
"album_id": splitID(albumID)[0],
"tid": e[2],
"uk": e[1],
"list": fsidsFormatNotUk(fileID),
})
r.SetResult(&resp)
}, account)
if err != nil {
return nil, err
}
return &resp.List[0], err
}

View File

@ -0,0 +1,502 @@
package baiduphoto
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"os"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
)
type Baidu struct{}
func init() {
base.RegisterDriver(new(Baidu))
}
func (driver Baidu) Config() base.DriverConfig {
return base.DriverConfig{
Name: "Baidu.Photo",
LocalSort: true,
}
}
func (driver Baidu) Items() []base.Item {
return []base.Item{
{
Name: "refresh_token",
Label: "refresh token",
Type: base.TypeString,
Required: true,
},
{
Name: "root_folder",
Label: "album_id",
Type: base.TypeString,
},
{
Name: "internal_type",
Label: "download api",
Type: base.TypeSelect,
Required: true,
Values: "file,album",
Default: "album",
},
{
Name: "client_id",
Label: "client id",
Default: "iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v",
Type: base.TypeString,
Required: true,
},
{
Name: "client_secret",
Label: "client secret",
Default: "jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG",
Type: base.TypeString,
Required: true,
},
}
}
func (driver Baidu) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
return driver.RefreshToken(account)
}
func (driver Baidu) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := utils.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver Baidu) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
var files []model.File
cache, err := base.GetCache(path, account)
if err == nil {
files, _ = cache.([]model.File)
return files, nil
}
file, err := driver.File(path, account)
if err != nil {
return nil, err
}
if IsAlbum(file) {
albumFiles, err := driver.GetAllAlbumFile(file.Id, account)
if err != nil {
return nil, err
}
files = make([]model.File, 0, len(albumFiles))
for _, file := range albumFiles {
var thumbnail string
if len(file.Thumburl) > 0 {
thumbnail = file.Thumburl[0]
}
files = append(files, model.File{
Id: joinID(file.Fsid, file.Uk, file.Tid),
Name: file.Name(),
Size: file.Size,
Type: utils.GetFileType(utils.Ext(file.Path)),
Driver: driver.Config().Name,
UpdatedAt: getTime(file.Mtime),
Thumbnail: thumbnail,
})
}
} else if IsRoot(file) {
albums, err := driver.GetAllAlbum(account)
if err != nil {
return nil, err
}
files = make([]model.File, 0, len(albums))
for _, album := range albums {
files = append(files, model.File{
Id: joinID(album.AlbumID, album.Tid),
Name: album.Title,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: getTime(album.Mtime),
})
}
} else {
return nil, base.ErrNotSupport
}
if len(files) > 0 {
_ = base.SetCache(path, files, account)
}
return files, nil
}
func (driver Baidu) Link(args base.Args, account *model.Account) (*base.Link, error) {
if account.InternalType == "file" {
return driver.LinkFile(args, account)
}
return driver.LinkAlbum(args, account)
}
func (driver Baidu) LinkAlbum(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(args.Path, account)
if err != nil {
return nil, err
}
if !IsAlbumFile(file) {
return nil, base.ErrNotSupport
}
album, err := driver.File(utils.Dir(utils.ParsePath(args.Path)), account)
if err != nil {
return nil, err
}
e := splitID(file.Id)
res, err := base.NoRedirectClient.R().
SetQueryParams(map[string]string{
"access_token": account.AccessToken,
"album_id": splitID(album.Id)[0],
"tid": e[2],
"fsid": e[0],
"uk": e[1],
}).
Head(ALBUM_API_URL + "/download")
if err != nil {
return nil, err
}
return &base.Link{
Headers: []base.Header{
{Name: "User-Agent", Value: base.UserAgent},
},
Url: res.Header().Get("location"),
}, nil
}
func (driver Baidu) LinkFile(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(args.Path, account)
if err != nil {
return nil, err
}
if !IsAlbumFile(file) {
return nil, base.ErrNotSupport
}
album, err := driver.File(utils.Dir(utils.ParsePath(args.Path)), account)
if err != nil {
return nil, err
}
// 拷贝到根目录
cfile, err := driver.CopyAlbumFile(album.Id, account, file.Id)
if err != nil {
return nil, err
}
res, err := driver.Request(http.MethodGet, FILE_API_URL_V2+"/download", func(r *resty.Request) {
r.SetQueryParams(map[string]string{
"fsid": fmt.Sprint(cfile.Fsid),
})
}, account)
if err != nil {
return nil, err
}
return &base.Link{
Headers: []base.Header{
{Name: "User-Agent", Value: base.UserAgent},
},
Url: utils.Json.Get(res.Body(), "dlink").ToString(),
}, nil
}
func (driver Baidu) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver Baidu) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver Baidu) Rename(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
if IsAlbum(srcFile) {
return driver.SetAlbumName(srcFile.Id, utils.Base(dst), account)
}
return base.ErrNotSupport
}
func (driver Baidu) MakeDir(path string, account *model.Account) error {
dir, name := utils.Split(path)
parentFile, err := driver.File(dir, account)
if err != nil {
return err
}
if !IsRoot(parentFile) {
return base.ErrNotSupport
}
return driver.CreateAlbum(name, account)
}
func (driver Baidu) Move(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
if IsAlbumFile(srcFile) {
// 移动相册文件
dstAlbum, err := driver.File(utils.Dir(dst), account)
if err != nil {
return err
}
if !IsAlbum(dstAlbum) {
return base.ErrNotSupport
}
srcAlbum, err := driver.File(utils.Dir(src), account)
if err != nil {
return err
}
newFile, err := driver.CopyAlbumFile(srcAlbum.Id, account, srcFile.Id)
if err != nil {
return err
}
err = driver.DeleteAlbumFile(srcAlbum.Id, account, srcFile.Id)
if err != nil {
return err
}
err = driver.AddAlbumFile(dstAlbum.Id, account, joinID(newFile.Fsid))
if err != nil {
return err
}
return nil
}
return base.ErrNotSupport
}
func (driver Baidu) Copy(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
if IsAlbumFile(srcFile) {
// 复制相册文件
dstAlbum, err := driver.File(utils.Dir(dst), account)
if err != nil {
return err
}
if !IsAlbum(dstAlbum) {
return base.ErrNotSupport
}
srcAlbum, err := driver.File(utils.Dir(src), account)
if err != nil {
return err
}
newFile, err := driver.CopyAlbumFile(srcAlbum.Id, account, srcFile.Id)
if err != nil {
return err
}
err = driver.AddAlbumFile(dstAlbum.Id, account, joinID(newFile.Fsid))
if err != nil {
return err
}
return nil
}
return base.ErrNotSupport
}
func (driver Baidu) Delete(path string, account *model.Account) error {
file, err := driver.File(path, account)
if err != nil {
return err
}
// 删除相册
if IsAlbum(file) {
return driver.DeleteAlbum(file.Id, account)
}
// 生成相册文件
if IsAlbumFile(file) {
// 删除相册文件
album, err := driver.File(utils.Dir(path), account)
if err != nil {
return err
}
return driver.DeleteAlbumFile(album.Id, account, file.Id)
}
return base.ErrNotSupport
}
func (driver Baidu) Upload(file *model.FileStream, account *model.Account) error {
if file == nil {
return base.ErrEmptyFile
}
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
}
if !IsAlbum(parentFile) {
return base.ErrNotSupport
}
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer func() {
tempFile.Close()
os.Remove(tempFile.Name())
}()
// 计算需要的数据
const DEFAULT = 1 << 22
const SliceSize = 1 << 18
count := int(math.Ceil(float64(file.Size) / float64(DEFAULT)))
sliceMD5List := make([]string, 0, count)
fileMd5 := md5.New()
sliceMd5 := md5.New()
for i := 1; i <= count; i++ {
if n, err := io.CopyN(io.MultiWriter(fileMd5, sliceMd5, tempFile), file, DEFAULT); err != io.EOF && n == 0 {
return err
}
sliceMD5List = append(sliceMD5List, hex.EncodeToString(sliceMd5.Sum(nil)))
sliceMd5.Reset()
}
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return err
}
content_md5 := hex.EncodeToString(fileMd5.Sum(nil))
slice_md5 := content_md5
if file.GetSize() > SliceSize {
sliceData := make([]byte, SliceSize)
if _, err = io.ReadFull(tempFile, sliceData); err != nil {
return err
}
sliceMd5.Write(sliceData)
slice_md5 = hex.EncodeToString(sliceMd5.Sum(nil))
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return err
}
}
// 开始执行上传
params := map[string]string{
"autoinit": "1",
"isdir": "0",
"rtype": "1",
"ctype": "11",
"path": utils.ParsePath(file.Name),
"size": fmt.Sprint(file.Size),
"slice-md5": slice_md5,
"content-md5": content_md5,
"block_list": MustString(utils.Json.MarshalToString(sliceMD5List)),
}
// 预上传
var precreateResp PrecreateResp
_, err = driver.Request(http.MethodPost, FILE_API_URL_V1+"/precreate", func(r *resty.Request) {
r.SetFormData(params)
r.SetResult(&precreateResp)
}, account)
if err != nil {
return err
}
switch precreateResp.ReturnType {
case 1: // 上传文件
uploadParams := map[string]string{
"method": "upload",
"path": params["path"],
"uploadid": precreateResp.UploadID,
}
for i := 0; i < count; i++ {
uploadParams["partseq"] = fmt.Sprint(i)
_, err = driver.Request(http.MethodPost, "https://c3.pcs.baidu.com/rest/2.0/pcs/superfile2", func(r *resty.Request) {
r.SetQueryParams(uploadParams)
r.SetFileReader("file", file.Name, io.LimitReader(tempFile, DEFAULT))
}, account)
if err != nil {
return err
}
}
fallthrough
case 2: // 创建文件
params["uploadid"] = precreateResp.UploadID
_, err = driver.Request(http.MethodPost, FILE_API_URL_V1+"/create", func(r *resty.Request) {
r.SetFormData(params)
r.SetResult(&precreateResp)
}, account)
if err != nil {
return err
}
fallthrough
case 3: // 增加到相册
err = driver.AddAlbumFile(parentFile.Id, account, joinID(precreateResp.Data.FsID))
if err != nil {
return err
}
}
return nil
}
var _ base.Driver = (*Baidu)(nil)

126
drivers/baiduphoto/types.go Normal file
View File

@ -0,0 +1,126 @@
package baiduphoto
import (
"fmt"
"github.com/Xhofe/alist/utils"
)
type TokenErrResp struct {
ErrorDescription string `json:"error_description"`
ErrorMsg string `json:"error"`
}
func (e *TokenErrResp) Error() string {
return fmt.Sprint(e.ErrorMsg, " : ", e.ErrorDescription)
}
type Erron struct {
Errno int `json:"errno"`
RequestID int `json:"request_id"`
}
type Page struct {
HasMore int `json:"has_more"`
Cursor string `json:"cursor"`
}
func (p Page) HasNextPage() bool {
return p.HasMore == 1
}
type (
FileListResp struct {
Page
List []File `json:"list"`
}
File struct {
Fsid int64 `json:"fsid"` // 文件ID
Path string `json:"path"` // 文件路径
Size int64 `json:"size"`
Ctime int64 `json:"ctime"` // 创建时间 s
Mtime int64 `json:"mtime"` // 修改时间 s
Thumburl []string `json:"thumburl"`
}
)
func (f File) Name() string {
return utils.Base(f.Path)
}
/*相册部分*/
type (
AlbumListResp struct {
Page
List []Album `json:"list"`
Reset int64 `json:"reset"`
TotalCount int64 `json:"total_count"`
}
Album struct {
AlbumID string `json:"album_id"`
Tid int64 `json:"tid"`
Title string `json:"title"`
JoinTime int64 `json:"join_time"`
CreateTime int64 `json:"create_time"`
Mtime int64 `json:"mtime"`
}
AlbumFileListResp struct {
Page
List []AlbumFile `json:"list"`
Reset int64 `json:"reset"`
TotalCount int64 `json:"total_count"`
}
AlbumFile struct {
File
Tid int64 `json:"tid"`
Uk int64 `json:"uk"`
}
)
type (
CopyFileResp struct {
List []CopyFile `json:"list"`
}
CopyFile struct {
FromFsid int64 `json:"from_fsid"` // 源ID
Fsid int64 `json:"fsid"` // 目标ID
Path string `json:"path"`
ShootTime int `json:"shoot_time"`
}
)
/*上传部分*/
type (
UploadFile struct {
FsID int64 `json:"fs_id"`
Size int64 `json:"size"`
Md5 string `json:"md5"`
ServerFilename string `json:"server_filename"`
Path string `json:"path"`
Ctime int `json:"ctime"`
Mtime int `json:"mtime"`
Isdir int `json:"isdir"`
Category int `json:"category"`
ServerMd5 string `json:"server_md5"`
ShootTime int `json:"shoot_time"`
}
CreateFileResp struct {
Data UploadFile `json:"data"`
}
PrecreateResp struct {
ReturnType int `json:"return_type"` //存在返回2 不存在返回1 已经保存3
//存在返回
CreateFileResp
//不存在返回
Path string `json:"path"`
UploadID string `json:"uploadid"`
Blocklist []int64 `json:"block_list"`
}
)

View File

@ -0,0 +1,84 @@
package baiduphoto
import (
"errors"
"fmt"
"math"
"math/rand"
"regexp"
"strings"
"time"
"github.com/Xhofe/alist/model"
)
const (
API_URL = "https://photo.baidu.com/youai"
ALBUM_API_URL = API_URL + "/album/v1"
FILE_API_URL_V1 = API_URL + "/file/v1"
FILE_API_URL_V2 = API_URL + "/file/v2"
)
var (
ErrNotSupportName = errors.New("only chinese and english, numbers and underscores are supported, and the length is no more than 20")
)
//Tid生成
func getTid() string {
return fmt.Sprintf("3%d%.0f", time.Now().Unix(), math.Floor(9000000*rand.Float64()+1000000))
}
// 检查名称
func checkName(name string) bool {
return len(name) <= 20 && regexp.MustCompile("[\u4e00-\u9fa5A-Za-z0-9_]").MatchString(name)
}
func getTime(t int64) *time.Time {
tm := time.Unix(t, 0)
return &tm
}
func fsidsFormat(ids ...string) string {
var buf []string
for _, id := range ids {
e := strings.Split(id, "|")
buf = append(buf, fmt.Sprintf("{\"fsid\":%s,\"uk\":%s}", e[0], e[1]))
}
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}", strings.Split(id, "|")[0]))
}
return fmt.Sprintf("[%s]", strings.Join(buf, ","))
}
func splitID(id string) []string {
return strings.SplitN(id, "|", 3)[:3]
}
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 IsAlbum(file *model.File) bool {
return file.Id != "" && file.IsDir()
}
func IsAlbumFile(file *model.File) bool {
return file.Id != "" && !file.IsDir()
}
func IsRoot(file *model.File) bool {
return file.Id == "" && file.IsDir()
}
func MustString(str string, err error) string {
return str
}

61
drivers/base/base.go Normal file
View File

@ -0,0 +1,61 @@
package base
import "github.com/Xhofe/alist/model"
type Base struct{}
func (b Base) Config() DriverConfig {
return DriverConfig{}
}
func (b Base) Items() []Item {
return nil
}
func (b Base) Save(account *model.Account, old *model.Account) error {
return ErrNotImplement
}
func (b Base) File(path string, account *model.Account) (*model.File, error) {
return nil, ErrNotImplement
}
func (b Base) Files(path string, account *model.Account) ([]model.File, error) {
return nil, ErrNotImplement
}
func (b Base) Link(args Args, account *model.Account) (*Link, error) {
return nil, ErrNotImplement
}
func (b Base) Path(path string, account *model.Account) (*model.File, []model.File, error) {
return nil, nil, ErrNotImplement
}
func (b Base) Preview(path string, account *model.Account) (interface{}, error) {
return nil, ErrNotImplement
}
func (b Base) MakeDir(path string, account *model.Account) error {
return ErrNotImplement
}
func (b Base) Move(src string, dst string, account *model.Account) error {
return ErrNotImplement
}
func (b Base) Rename(src string, dst string, account *model.Account) error {
return ErrNotImplement
}
func (b Base) Copy(src string, dst string, account *model.Account) error {
return ErrNotImplement
}
func (b Base) Delete(path string, account *model.Account) error {
return ErrNotImplement
}
func (b Base) Upload(file *model.FileStream, account *model.Account) error {
return ErrNotImplement
}

View File

@ -1,7 +1,6 @@
package base
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
@ -9,12 +8,39 @@ import (
)
func KeyCache(path string, account *model.Account) string {
path = utils.ParsePath(path)
return fmt.Sprintf("%s%s", account.Name, path)
//path = utils.ParsePath(path)
key := utils.ParsePath(utils.Join(account.Name, path))
log.Debugln("cache key: ", key)
return key
}
func SetCache(path string, obj interface{}, account *model.Account) error {
return conf.Cache.Set(conf.Ctx, KeyCache(path, account), obj, nil)
func SaveSearchFiles[T model.ISearchFile](key string, obj []T) {
err := model.DeleteSearchFilesByPath(key)
if err != nil {
log.Errorln("failed create search files", err)
return
}
files := make([]model.SearchFile, len(obj))
for i := 0; i < len(obj); i++ {
files[i] = model.SearchFile{
Path: key,
Name: obj[i].GetName(),
Size: obj[i].GetSize(),
Type: obj[i].GetType(),
}
}
err = model.CreateSearchFiles(files)
if err != nil {
log.Errorln("failed create search files", err)
}
}
func SetCache[T model.ISearchFile](path string, obj []T, account *model.Account) error {
key := KeyCache(path, account)
if conf.GetBool("enable search") {
go SaveSearchFiles(key, obj)
}
return conf.Cache.Set(conf.Ctx, key, obj, nil)
}
func GetCache(path string, account *model.Account) (interface{}, error) {

View File

@ -2,10 +2,10 @@ package base
import (
"github.com/Xhofe/alist/model"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"net/http"
"time"
)
type DriverConfig struct {
@ -21,6 +21,7 @@ type DriverConfig struct {
type Args struct {
Path string
IP string
Header http.Header
}
type Driver interface {
@ -38,8 +39,8 @@ type Driver interface {
Link(args Args, account *model.Account) (*Link, error)
// Path 取路径(文件或文件夹)
Path(path string, account *model.Account) (*model.File, []model.File, error)
// Proxy 代理处理
Proxy(c *gin.Context, account *model.Account)
// Deprecated Proxy 代理处理
//Proxy(r *http.Request, account *model.Account)
// Preview 预览
Preview(path string, account *model.Account) (interface{}, error)
// MakeDir 创建文件夹
@ -62,6 +63,7 @@ type Item struct {
Name string `json:"name"`
Label string `json:"label"`
Type string `json:"type"`
Default string `json:"default"`
Values string `json:"values"`
Required bool `json:"required"`
Description string `json:"description"`
@ -84,19 +86,21 @@ func GetDriversMap() map[string]Driver {
}
func GetDrivers() map[string][]Item {
res := make(map[string][]Item, 0)
res := make(map[string][]Item)
for k, v := range driversMap {
webdavDirect := Item{
Name: "webdav_direct",
Label: "webdav direct",
Type: TypeBool,
Required: true,
Description: "Transfer the WebDAV of this account through the native",
}
if v.Config().OnlyProxy {
res[k] = v.Items()
res[k] = append([]Item{
webdavDirect,
}, v.Items()...)
} else {
res[k] = append([]Item{
//{
// Name: "allow_proxy",
// Label: "allow_proxy",
// Type: TypeBool,
// Required: true,
// Description: "allow proxy",
//},
{
Name: "proxy",
Label: "proxy",
@ -111,13 +115,20 @@ func GetDrivers() map[string][]Item {
Required: true,
Description: "Transfer the WebDAV of this account through the server",
},
webdavDirect,
}, v.Items()...)
}
res[k] = append([]Item{
{
Name: "down_proxy_url",
Label: "down_proxy_url",
Type: TypeString,
Type: TypeText,
},
{
Name: "extract_folder",
Label: "extract_folder",
Values: "front,back",
Type: TypeSelect,
},
}, res[k]...)
if v.Config().ApiProxy {
@ -154,6 +165,8 @@ func GetDrivers() map[string][]Item {
var NoRedirectClient *resty.Client
var RestyClient = resty.New()
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 DefaultTimeout = time.Second * 20
func init() {
NoRedirectClient = resty.New().SetRedirectPolicy(
@ -161,8 +174,8 @@ func init() {
return http.ErrUseLastResponse
}),
)
userAgent := "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
NoRedirectClient.SetHeader("user-agent", userAgent)
RestyClient.SetHeader("user-agent", userAgent)
NoRedirectClient.SetHeader("user-agent", UserAgent)
RestyClient.SetHeader("user-agent", UserAgent)
RestyClient.SetRetryCount(3)
RestyClient.SetTimeout(DefaultTimeout)
}

View File

@ -3,6 +3,7 @@ package base
import (
"errors"
"io"
"net/http"
)
var (
@ -12,6 +13,8 @@ var (
ErrNotSupport = errors.New("not support")
ErrNotFolder = errors.New("not a folder")
ErrEmptyFile = errors.New("empty file")
ErrRelativePath = errors.New("access using relative path is not allowed")
ErrEmptyToken = errors.New("empty token")
)
const (
@ -19,6 +22,7 @@ const (
TypeSelect = "select"
TypeBool = "bool"
TypeNumber = "number"
TypeText = "text"
)
const (
@ -45,4 +49,7 @@ type Link struct {
Url string `json:"url"`
Headers []Header `json:"headers"`
Data io.ReadCloser
FilePath string `json:"path"` // for native
Status int
Header http.Header
}

View File

@ -5,7 +5,6 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
"github.com/jlaffaye/ftp"
log "github.com/sirupsen/logrus"
"path/filepath"
@ -53,15 +52,25 @@ func (driver FTP) Items() []base.Item {
}
func (driver FTP) Save(account *model.Account, old *model.Account) error {
if old != nil {
conn, ok := connMap[old.Name]
if ok {
err := conn.Quit()
log.Error("ftp:", err)
delete(connMap, old.Name)
}
}
if account == nil {
return nil
}
if account.RootFolder == "" {
account.RootFolder = "/"
}
conn, err := driver.Login(account)
_, err := driver.Login(account)
if err != nil {
account.Status = err.Error()
} else {
account.Status = "work"
_ = conn.Quit()
}
_ = model.SaveAccount(account)
return err
@ -106,7 +115,7 @@ func (driver FTP) Files(path string, account *model.Account) ([]model.File, erro
if err != nil {
return nil, err
}
defer func() { _ = conn.Quit() }()
//defer func() { _ = conn.Quit() }()
entries, err := conn.List(realPath)
if err != nil {
return nil, err
@ -144,7 +153,7 @@ func (driver FTP) Link(args base.Args, account *model.Account) (*base.Link, erro
if err != nil {
return nil, err
}
defer func() { _ = conn.Quit() }()
//defer func() { _ = conn.Quit() }()
resp, err := conn.Retr(realPath)
if err != nil {
return nil, err
@ -176,9 +185,9 @@ func (driver FTP) Path(path string, account *model.Account) (*model.File, []mode
return nil, files, nil
}
func (driver FTP) Proxy(c *gin.Context, account *model.Account) {
}
//func (driver FTP) Proxy(r *http.Request, account *model.Account) {
//
//}
func (driver FTP) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
@ -191,7 +200,7 @@ func (driver FTP) MakeDir(path string, account *model.Account) error {
if err != nil {
return err
}
defer func() { _ = conn.Quit() }()
//defer func() { _ = conn.Quit() }()
err = conn.MakeDir(realPath)
return err
}
@ -203,7 +212,7 @@ func (driver FTP) Move(src string, dst string, account *model.Account) error {
if err != nil {
return err
}
defer func() { _ = conn.Quit() }()
//defer func() { _ = conn.Quit() }()
err = conn.Rename(realSrc, realDst)
return err
}
@ -218,13 +227,21 @@ func (driver FTP) Copy(src string, dst string, account *model.Account) error {
func (driver FTP) Delete(path string, account *model.Account) error {
path = utils.ParsePath(path)
file, err := driver.File(path, account)
if err != nil {
return err
}
realPath := utils.Join(account.RootFolder, path)
conn, err := driver.Login(account)
if err != nil {
return err
}
defer func() { _ = conn.Quit() }()
//defer func() { _ = conn.Quit() }()
if file.IsDir() {
err = conn.RemoveDirRecur(realPath)
} else {
err = conn.Delete(realPath)
}
return err
}
@ -237,7 +254,7 @@ func (driver FTP) Upload(file *model.FileStream, account *model.Account) error {
if err != nil {
return err
}
defer func() { _ = conn.Quit() }()
//defer func() { _ = conn.Quit() }()
err = conn.Stor(realPath, file)
return err
}

View File

@ -6,7 +6,18 @@ import (
"github.com/jlaffaye/ftp"
)
var connMap map[string]*ftp.ServerConn
func (driver FTP) Login(account *model.Account) (*ftp.ServerConn, error) {
conn, ok := connMap[account.Name]
if ok {
_, err := conn.CurrentDir()
if err == nil {
return conn, nil
} else {
delete(connMap, account.Name)
}
}
conn, err := ftp.Connect(account.SiteUrl)
if err != nil {
return nil, err
@ -15,9 +26,11 @@ func (driver FTP) Login(account *model.Account) (*ftp.ServerConn, error) {
if err != nil {
return nil, err
}
connMap[account.Name] = conn
return conn, nil
}
func init() {
base.RegisterDriver(&FTP{})
connMap = make(map[string]*ftp.ServerConn)
}

View File

@ -6,7 +6,6 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"io/ioutil"
"path/filepath"
@ -30,12 +29,14 @@ func (driver GoogleDrive) Items() []base.Item {
Label: "client id",
Type: base.TypeString,
Required: true,
Default: "202264815644.apps.googleusercontent.com",
},
{
Name: "client_secret",
Label: "client secret",
Type: base.TypeString,
Required: true,
Default: "X4Z3ca8xfWDb1Voo-F9a7ZxJ",
},
{
Name: "refresh_token",
@ -67,6 +68,9 @@ func (driver GoogleDrive) Items() []base.Item {
}
func (driver GoogleDrive) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
account.Proxy = true
err := driver.RefreshToken(account)
if err != nil {
@ -175,9 +179,9 @@ func (driver GoogleDrive) Path(path string, account *model.Account) (*model.File
return nil, files, nil
}
func (driver GoogleDrive) Proxy(c *gin.Context, account *model.Account) {
c.Request.Header.Add("Authorization", "Bearer "+account.AccessToken)
}
//func (driver GoogleDrive) Proxy(r *http.Request, account *model.Account) {
// r.Header.Add("Authorization", "Bearer "+account.AccessToken)
//}
func (driver GoogleDrive) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport

View File

@ -2,15 +2,10 @@ package google
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"path/filepath"
"strconv"
"time"
)
type TokenError struct {
@ -44,19 +39,6 @@ func (driver GoogleDrive) RefreshToken(account *model.Account) error {
return nil
}
type File struct {
Id string `json:"id"`
Name string `json:"name"`
MimeType string `json:"mimeType"`
ModifiedTime *time.Time `json:"modifiedTime"`
Size string `json:"size"`
ThumbnailLink string `json:"thumbnailLink"`
}
func (driver GoogleDrive) IsDir(mimeType string) bool {
return mimeType == "application/vnd.google-apps.folder" || mimeType == "application/vnd.google-apps.shortcut"
}
func (driver GoogleDrive) FormatFile(file *File, account *model.Account) *model.File {
f := &model.File{
Id: file.Id,
@ -65,16 +47,11 @@ func (driver GoogleDrive) FormatFile(file *File, account *model.Account) *model.
UpdatedAt: file.ModifiedTime,
Url: "",
}
if driver.IsDir(file.MimeType) {
f.Type = conf.FOLDER
} else {
size, _ := strconv.ParseInt(file.Size, 10, 64)
f.Size = size
f.Type = utils.GetFileType(filepath.Ext(file.Name))
}
f.Size = int64(file.GetSize())
f.Type = file.GetType()
if file.ThumbnailLink != "" {
if account.DownProxyUrl != "" {
f.Thumbnail = fmt.Sprintf("%s/%s", account.DownProxyUrl, file.ThumbnailLink)
if account.APIProxyUrl != "" {
f.Thumbnail = fmt.Sprintf("%s/%s", account.APIProxyUrl, file.ThumbnailLink)
} else {
f.Thumbnail = file.ThumbnailLink
}

38
drivers/google/types.go Normal file
View File

@ -0,0 +1,38 @@
package google
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/utils"
"path"
"strconv"
"time"
)
type File struct {
Id string `json:"id"`
Name string `json:"name"`
MimeType string `json:"mimeType"`
ModifiedTime *time.Time `json:"modifiedTime"`
Size string `json:"size"`
ThumbnailLink string `json:"thumbnailLink"`
}
func (f File) GetSize() uint64 {
if f.GetType() == conf.FOLDER {
return 0
}
size, _ := strconv.ParseUint(f.Size, 10, 64)
return size
}
func (f File) GetName() string {
return f.Name
}
func (f File) GetType() int {
mimeType := f.MimeType
if mimeType == "application/vnd.google-apps.folder" || mimeType == "application/vnd.google-apps.shortcut" {
return conf.FOLDER
}
return utils.GetFileType(path.Ext(f.Name))
}

View File

@ -5,7 +5,6 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"path/filepath"
)
@ -33,16 +32,18 @@ func (driver Lanzou) Items() []base.Item {
Label: "cookie",
Type: base.TypeString,
Description: "about 15 days valid",
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
Required: true,
},
{
Name: "site_url",
Label: "share url",
Type: base.TypeString,
Required: true,
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
},
{
Name: "password",
@ -53,6 +54,9 @@ func (driver Lanzou) Items() []base.Item {
}
func (driver Lanzou) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
if account.InternalType == "cookie" {
if account.RootFolder == "" {
account.RootFolder = "-1"
@ -121,18 +125,27 @@ func (driver Lanzou) Link(args base.Args, account *model.Account) (*base.Link, e
}
log.Debugf("down file: %+v", file)
downId := file.Id
pwd := ""
if account.InternalType == "cookie" {
downId, err = driver.GetDownPageId(file.Id, account)
downId, pwd, err = driver.GetDownPageId(file.Id, account)
if err != nil {
return nil, err
}
}
url, err := driver.GetLink(downId)
var url string
//if pwd != "" {
//url, err = driver.GetLinkWithPassword(downId, pwd, account)
//} else {
url, err = driver.GetLink(downId, pwd, account)
//}
if err != nil {
return nil, err
}
link := base.Link{
Url: url,
Headers: []base.Header{
{Name: "User-Agent", Value: base.UserAgent},
},
}
return &link, nil
}
@ -154,9 +167,9 @@ func (driver Lanzou) Path(path string, account *model.Account) (*model.File, []m
return nil, files, nil
}
func (driver Lanzou) Proxy(c *gin.Context, account *model.Account) {
c.Request.Header.Del("Origin")
}
//func (driver Lanzou) Proxy(r *http.Request, account *model.Account) {
// r.Header.Del("Origin")
//}
func (driver Lanzou) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport

View File

@ -2,30 +2,15 @@ package lanzou
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"path/filepath"
"net/url"
"regexp"
"strconv"
"time"
)
var lanzouClient = resty.New()
type LanZouFile struct {
Name string `json:"name"`
NameAll string `json:"name_all"`
Id string `json:"id"`
FolId string `json:"fol_id"`
Size string `json:"size"`
Time string `json:"time"`
Folder bool
}
func (driver *Lanzou) FormatFile(file *LanZouFile) *model.File {
now := time.Now()
f := &model.File{
@ -37,12 +22,11 @@ func (driver *Lanzou) FormatFile(file *LanZouFile) *model.File {
UpdatedAt: &now,
}
if file.Folder {
f.Type = conf.FOLDER
f.Id = file.FolId
} else {
f.Name = file.NameAll
f.Type = utils.GetFileType(filepath.Ext(file.NameAll))
}
f.Type = file.GetType()
return f
}
@ -57,7 +41,7 @@ func (driver *Lanzou) GetFiles(folderId string, account *model.Account) ([]LanZo
files := make([]LanZouFile, 0)
var resp LanZouFilesResp
// folders
res, err := lanzouClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
res, err := base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
SetFormData(map[string]string{
"task": "47",
"folder_id": folderId,
@ -76,7 +60,7 @@ func (driver *Lanzou) GetFiles(folderId string, account *model.Account) ([]LanZo
// files
pg := 1
for {
_, err = lanzouClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
_, err = base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
SetFormData(map[string]string{
"task": "5",
"folder_id": folderId,
@ -103,7 +87,11 @@ func (driver *Lanzou) GetFiles(folderId string, account *model.Account) ([]LanZo
func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error) {
files := make([]LanZouFile, 0)
shareUrl := account.SiteUrl
res, err := lanzouClient.R().Get(shareUrl)
u, err := url.Parse(shareUrl)
if err != nil {
return nil, err
}
res, err := base.RestyClient.R().Get(shareUrl)
if err != nil {
return nil, err
}
@ -116,7 +104,10 @@ func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error
uid := regexp.MustCompile(`'uid':'(.+?)',`).FindStringSubmatch(res.String())[1]
rep := regexp.MustCompile(`'rep':'(.+?)',`).FindStringSubmatch(res.String())[1]
up := regexp.MustCompile(`'up':(.+?),`).FindStringSubmatch(res.String())[1]
ls := regexp.MustCompile(`'ls':(.+?),`).FindStringSubmatch(res.String())[1]
ls := ""
if account.Password != "" {
ls = regexp.MustCompile(`'ls':(.+?),`).FindStringSubmatch(res.String())[1]
}
tName := regexp.MustCompile(`'t':(.+?),`).FindStringSubmatch(res.String())[1]
kName := regexp.MustCompile(`'k':(.+?),`).FindStringSubmatch(res.String())[1]
t := regexp.MustCompile(`var ` + tName + ` = '(.+?)';`).FindStringSubmatch(res.String())[1]
@ -124,7 +115,7 @@ func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error
pg := 1
for {
var resp LanZouFilesResp
res, err = lanzouClient.R().SetResult(&resp).SetFormData(map[string]string{
res, err = base.RestyClient.R().SetResult(&resp).SetFormData(map[string]string{
"lx": lx,
"fid": fid,
"uid": uid,
@ -135,7 +126,7 @@ func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error
"up": up,
"ls": ls,
"pwd": account.Password,
}).Post("https://wwa.lanzouo.com/filemoreajax.php")
}).Post(fmt.Sprintf("https://%s/filemoreajax.php", u.Host))
if err != nil {
log.Debug(err)
break
@ -148,6 +139,7 @@ func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error
break
}
pg++
time.Sleep(time.Second)
files = append(files, resp.Text...)
}
return files, nil
@ -158,30 +150,22 @@ func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error
// IsNewd string `json:"is_newd"`
//}
// 获取下载页面的ID
func (driver *Lanzou) GetDownPageId(fileId string, account *model.Account) (string, error) {
var resp LanZouFilesResp
res, err := lanzouClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
// GetDownPageId 获取下载页面的ID
func (driver *Lanzou) GetDownPageId(fileId string, account *model.Account) (string, string, error) {
var resp DownPageResp
res, err := base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
SetFormData(map[string]string{
"task": "22",
"file_id": fileId,
}).Post("https://pc.woozooo.com/doupload.php")
if err != nil {
return "", err
return "", "", err
}
log.Debug(res.String())
if resp.Zt != 1 {
return "", fmt.Errorf("%v", resp.Info)
return "", "", fmt.Errorf("%v", resp.Info)
}
info, ok := resp.Info.(map[string]interface{})
if !ok {
return "", fmt.Errorf("%v", resp.Info)
}
fid, ok := info["f_id"].(string)
if !ok {
return "", fmt.Errorf("%v", info["f_id"])
}
return fid, nil
return resp.Info.FId, resp.Info.Pwd, nil
}
type LanzouLinkResp struct {
@ -190,50 +174,98 @@ type LanzouLinkResp struct {
Zt int `json:"zt"`
}
func (driver *Lanzou) GetLink(downId string) (string, error) {
res, err := lanzouClient.R().Get("https://wwa.lanzouo.com/" + downId)
func (driver *Lanzou) GetLink(downId string, pwd string, account *model.Account) (string, error) {
shareUrl := account.SiteUrl
u, err := url.Parse(shareUrl)
if err != nil {
return "", err
}
log.Debugln(fmt.Sprintf("https://%s/%s", u.Host, downId))
res, err := base.RestyClient.R().Get(fmt.Sprintf("https://%s/%s", u.Host, downId))
if err != nil {
return "", err
}
iframe := regexp.MustCompile(`<iframe class="ifr2" name=".{2,20}" src="(.+?)"`).FindStringSubmatch(res.String())
if len(iframe) == 0 {
return "", fmt.Errorf("get down empty page")
return driver.GetLinkWithPassword(downId, pwd, res.String(), account)
}
iframeUrl := "https://wwa.lanzouo.com" + iframe[1]
res, err = lanzouClient.R().Get(iframeUrl)
iframeUrl := fmt.Sprintf("https://%s%s", u.Host, iframe[1])
res, err = base.RestyClient.R().Get(iframeUrl)
if err != nil {
return "", err
}
log.Debugln(res.String())
ajaxdata := regexp.MustCompile(`var ajaxdata = '(.+?)'`).FindStringSubmatch(res.String())
if len(ajaxdata) == 0 {
return "", fmt.Errorf("get iframe empty page")
}
signs := ajaxdata[1]
sign := regexp.MustCompile(`var ispostdowns = '(.+?)';`).FindStringSubmatch(res.String())[1]
websignkey := regexp.MustCompile(`'websignkey':'(.+?)'`).FindStringSubmatch(res.String())[1]
//sign := regexp.MustCompile(`var ispostdowns = '(.+?)';`).FindStringSubmatch(res.String())[1]
sign := regexp.MustCompile(`'sign':'(.+?)',`).FindStringSubmatch(res.String())[1]
//websign := regexp.MustCompile(`'websign':'(.+?)'`).FindStringSubmatch(res.String())[1]
websign := ""
websignR := regexp.MustCompile(`var websign = '(.+?)'`).FindStringSubmatch(res.String())
if len(websignR) > 1 {
websign = websignR[1]
}
//websign := ""
//websignkey := regexp.MustCompile(`'websignkey':'(.+?)'`).FindStringSubmatch(res.String())[1]
websignkey := regexp.MustCompile(`var websignkey = '(.+?)';`).FindStringSubmatch(res.String())[1]
var resp LanzouLinkResp
form := map[string]string{
"action": "downprocess",
"signs": signs,
"sign": sign,
"ves": "1",
"websign": "",
"websign": websign,
"websignkey": websignkey,
}
log.Debugf("form: %+v", form)
_, err = lanzouClient.R().SetResult(&resp).
SetHeader("origin", "https://wwa.lanzouo.com").
res, err = base.RestyClient.R().SetResult(&resp).
SetHeader("origin", "https://"+u.Host).
SetHeader("referer", iframeUrl).
SetFormData(form).Post("https://wwa.lanzouo.com/ajaxm.php")
SetFormData(form).Post(fmt.Sprintf("https://%s/ajaxm.php", u.Host))
log.Debug(res.String())
if err != nil {
return "", err
}
if resp.Zt == 1 {
return resp.Dom + "/file/" + resp.Url, nil
}
return "", fmt.Errorf("can't get link")
return "", fmt.Errorf("failed get link")
}
func (driver *Lanzou) GetLinkWithPassword(downId string, pwd string, html string, account *model.Account) (string, error) {
shareUrl := account.SiteUrl
u, err := url.Parse(shareUrl)
if err != nil {
return "", err
}
if html == "" {
log.Debugln(fmt.Sprintf("https://%s/%s", u.Host, downId))
res, err := base.RestyClient.R().Get(fmt.Sprintf("https://%s/%s", u.Host, downId))
if err != nil {
return "", err
}
html = res.String()
}
data := regexp.MustCompile(`data : '(.+?)'\+pwd,`).FindStringSubmatch(html)[1] + pwd
var resp LanzouLinkResp
_, err = base.RestyClient.R().SetResult(&resp).SetHeaders(map[string]string{
"Referer": fmt.Sprintf("https://%s/%s", u.Host, downId),
"Origin": "https://" + u.Host,
"content-type": "application/x-www-form-urlencoded",
}).SetBody(data).Post(fmt.Sprintf("https://%s/ajaxm.php", u.Host))
if err != nil {
return "", err
}
if resp.Zt == 1 {
return resp.Dom + "/file/" + resp.Url, nil
}
return "", fmt.Errorf("failed get link with password")
}
func init() {
base.RegisterDriver(&Lanzou{})
lanzouClient.
SetRetryCount(3).
SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36")
}

47
drivers/lanzou/types.go Normal file
View File

@ -0,0 +1,47 @@
package lanzou
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/utils"
"path"
)
type LanZouFile struct {
Name string `json:"name"`
NameAll string `json:"name_all"`
Id string `json:"id"`
FolId string `json:"fol_id"`
Size string `json:"size"`
Time string `json:"time"`
Folder bool
}
func (f LanZouFile) GetSize() uint64 {
return 0
}
func (f LanZouFile) GetName() string {
if f.Folder {
return f.Name
}
return f.NameAll
}
func (f LanZouFile) GetType() int {
if f.Folder {
return conf.FOLDER
}
return utils.GetFileType(path.Ext(f.NameAll))
}
type DownPageResp struct {
Zt int `json:"zt"`
Info struct {
Pwd string `json:"pwd"`
Onof string `json:"onof"`
FId string `json:"f_id"`
Taoc string `json:"taoc"`
IsNewd string `json:"is_newd"`
} `json:"info"`
Text interface{} `json:"text"`
}

View File

@ -1,7 +1,6 @@
package mediatrack
import (
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
@ -13,11 +12,12 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
"os"
"path/filepath"
)
@ -36,11 +36,13 @@ func (driver MediaTrack) Items() []base.Item {
Label: "Token",
Type: base.TypeString,
Description: "Unknown expiration time",
Required: true,
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
Required: true,
},
{
Name: "order_by",
@ -48,6 +50,7 @@ func (driver MediaTrack) Items() []base.Item {
Type: base.TypeSelect,
Values: "updated_at,title,size",
Required: true,
Description: "title",
},
{
Name: "order_direction",
@ -55,11 +58,15 @@ func (driver MediaTrack) Items() []base.Item {
Type: base.TypeSelect,
Values: "true,false",
Required: true,
Default: "false",
},
}
}
func (driver MediaTrack) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
return nil
}
@ -142,9 +149,9 @@ func (driver MediaTrack) Path(path string, account *model.Account) (*model.File,
return nil, files, nil
}
func (driver MediaTrack) Proxy(c *gin.Context, account *model.Account) {
}
//func (driver MediaTrack) Proxy(r *http.Request, account *model.Account) {
//
//}
func (driver MediaTrack) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotImplement
@ -155,7 +162,7 @@ func (driver MediaTrack) MakeDir(path string, account *model.Account) error {
if err != nil {
return err
}
url := fmt.Sprintf("https://jayce.api.mediatrack.cn/v4/assets/%s/children", parentFile.Id)
url := fmt.Sprintf("https://jayce.api.mediatrack.cn/v3/assets/%s/children", parentFile.Id)
_, err = driver.Request(url, base.Post, nil, nil, nil, base.Json{
"type": 1,
"title": utils.Base(path),
@ -256,21 +263,39 @@ func (driver MediaTrack) Upload(file *model.FileStream, account *model.Account)
if err != nil {
return err
}
var buf bytes.Buffer
read := io.TeeReader(file, &buf)
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
_, err = io.Copy(tempFile, file)
if err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
uploader := s3manager.NewUploader(s)
input := &s3manager.UploadInput{
Bucket: &resp.Data.Bucket,
Key: &resp.Data.Object,
Body: read,
Body: tempFile,
}
_, err = uploader.Upload(input)
if err != nil {
return err
}
url := fmt.Sprintf("https://jayce.api.mediatrack.cn/v3/assets/%s/children", parentFile.Id)
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
h := md5.New()
_, err = io.Copy(h, &buf)
_, err = io.Copy(h, tempFile)
if err != nil {
return err
}

View File

@ -8,6 +8,7 @@ import (
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"path"
"strconv"
"time"
@ -58,6 +59,7 @@ func (driver MediaTrack) Request(url string, method int, headers, query, form ma
if err != nil {
return nil, err
}
log.Debugln(res.String())
if e.Status != "SUCCESS" {
return nil, errors.New(e.Message)
}

View File

@ -6,7 +6,6 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
@ -39,6 +38,9 @@ func (driver Native) Items() []base.Item {
}
func (driver Native) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
log.Debugf("save a account: [%s]", account.Name)
if !utils.Exists(account.RootFolder) {
account.Status = fmt.Sprintf("[%s] not exist", account.RootFolder)
@ -55,6 +57,9 @@ func (driver Native) Save(account *model.Account, old *model.Account) error {
}
func (driver Native) File(path string, account *model.Account) (*model.File, error) {
if utils.IsContain(strings.Split(path, "/"), "..") {
return nil, base.ErrRelativePath
}
fullPath := filepath.Join(account.RootFolder, path)
if !utils.Exists(fullPath) {
return nil, base.ErrPathNotFound
@ -66,7 +71,6 @@ func (driver Native) File(path string, account *model.Account) (*model.File, err
time := f.ModTime()
file := &model.File{
Name: f.Name(),
Size: f.Size(),
UpdatedAt: &time,
Driver: driver.Config().Name,
}
@ -74,11 +78,15 @@ func (driver Native) File(path string, account *model.Account) (*model.File, err
file.Type = conf.FOLDER
} else {
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
file.Size = f.Size()
}
return file, nil
}
func (driver Native) Files(path string, account *model.Account) ([]model.File, error) {
if utils.IsContain(strings.Split(path, "/"), "..") {
return nil, base.ErrRelativePath
}
fullPath := filepath.Join(account.RootFolder, path)
if !utils.Exists(fullPath) {
return nil, base.ErrPathNotFound
@ -95,7 +103,6 @@ func (driver Native) Files(path string, account *model.Account) ([]model.File, e
time := f.ModTime()
file := model.File{
Name: f.Name(),
Size: f.Size(),
Type: 0,
UpdatedAt: &time,
Driver: driver.Config().Name,
@ -104,14 +111,22 @@ func (driver Native) Files(path string, account *model.Account) ([]model.File, e
file.Type = conf.FOLDER
} else {
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
file.Size = f.Size()
}
files = append(files, file)
}
model.SortFiles(files, account)
_, err = base.GetCache(path, account)
if len(files) != 0 && err != nil {
_ = base.SetCache(path, files, account)
}
return files, nil
}
func (driver Native) Link(args base.Args, account *model.Account) (*base.Link, error) {
_, err := driver.File(args.Path, account)
if err != nil {
return nil, err
}
fullPath := filepath.Join(account.RootFolder, args.Path)
s, err := os.Stat(fullPath)
if err != nil {
@ -121,7 +136,7 @@ func (driver Native) Link(args base.Args, account *model.Account) (*base.Link, e
return nil, base.ErrNotFile
}
link := base.Link{
Url: fullPath,
FilePath: fullPath,
}
return &link, nil
}
@ -144,21 +159,27 @@ func (driver Native) Path(path string, account *model.Account) (*model.File, []m
return nil, files, nil
}
func (driver Native) Proxy(c *gin.Context, account *model.Account) {
// unnecessary
}
//func (driver Native) Proxy(r *http.Request, account *model.Account) {
// // unnecessary
//}
func (driver Native) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver Native) MakeDir(path string, account *model.Account) error {
if utils.IsContain(strings.Split(path, "/"), "..") {
return base.ErrRelativePath
}
fullPath := filepath.Join(account.RootFolder, path)
err := os.MkdirAll(fullPath, 0700)
return err
}
func (driver Native) Move(src string, dst string, account *model.Account) error {
if utils.IsContain(strings.Split(src+"/"+dst, "/"), "..") {
return base.ErrRelativePath
}
fullSrc := filepath.Join(account.RootFolder, src)
fullDst := filepath.Join(account.RootFolder, dst)
return os.Rename(fullSrc, fullDst)
@ -169,6 +190,9 @@ func (driver Native) Rename(src string, dst string, account *model.Account) erro
}
func (driver Native) Copy(src string, dst string, account *model.Account) error {
if utils.IsContain(strings.Split(src+"/"+dst, "/"), "..") {
return base.ErrRelativePath
}
fullSrc := filepath.Join(account.RootFolder, src)
fullDst := filepath.Join(account.RootFolder, dst)
srcFile, err := driver.File(src, account)
@ -188,6 +212,9 @@ func (driver Native) Copy(src string, dst string, account *model.Account) error
}
func (driver Native) Delete(path string, account *model.Account) error {
if utils.IsContain(strings.Split(path, "/"), "..") {
return base.ErrRelativePath
}
fullPath := filepath.Join(account.RootFolder, path)
file, err := driver.File(path, account)
if err != nil {
@ -203,6 +230,9 @@ func (driver Native) Upload(file *model.FileStream, account *model.Account) erro
if file == nil {
return base.ErrEmptyFile
}
if utils.IsContain(strings.Split(file.ParentPath, "/"), "..") {
return base.ErrRelativePath
}
fullPath := filepath.Join(account.RootFolder, file.ParentPath, file.Name)
_, err := driver.File(filepath.Join(file.ParentPath, file.Name), account)
if err == nil {
@ -222,6 +252,16 @@ func (driver Native) Upload(file *model.FileStream, account *model.Account) erro
defer func() {
_ = out.Close()
}()
//var buf bytes.Buffer
//reader := io.TeeReader(file, &buf)
//h := md5.New()
//_, err = io.Copy(h, reader)
//if err != nil {
// return err
//}
//hash := hex.EncodeToString(h.Sum(nil))
//log.Debugln("md5:", hash)
//_, err = io.Copy(out, &buf)
_, err = io.Copy(out, file)
return err
}

View File

@ -6,8 +6,6 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
"path/filepath"
)
@ -55,6 +53,7 @@ func (driver Onedrive) Items() []base.Item {
Label: "redirect uri",
Type: base.TypeString,
Required: true,
Default: "https://tool.nn.ci/onedrive/callback",
},
{
Name: "refresh_token",
@ -92,37 +91,37 @@ func (driver Onedrive) Items() []base.Item {
}
func (driver Onedrive) Save(account *model.Account, old *model.Account) error {
//if old != nil {
// conf.Cron.Remove(cron.EntryID(old.CronId))
//}
if account == nil {
return nil
}
_, ok := onedriveHostMap[account.Zone]
if !ok {
return fmt.Errorf("no [%s] zone", account.Zone)
}
if old != nil {
conf.Cron.Remove(cron.EntryID(old.CronId))
}
account.RootFolder = utils.ParsePath(account.RootFolder)
err := driver.RefreshToken(account)
_ = model.SaveAccount(account)
if err != nil {
return err
}
cronId, err := conf.Cron.AddFunc("@every 1h", func() {
name := account.Name
log.Debugf("onedrive account name: %s", name)
newAccount, ok := model.GetAccount(name)
log.Debugf("onedrive account: %+v", newAccount)
if !ok {
return
}
err = driver.RefreshToken(&newAccount)
_ = model.SaveAccount(&newAccount)
})
if err != nil {
return err
}
account.CronId = int(cronId)
err = model.SaveAccount(account)
if err != nil {
return err
}
//cronId, err := conf.Cron.AddFunc("@every 1h", func() {
// name := account.Name
// log.Debugf("onedrive account name: %s", name)
// newAccount, ok := model.GetAccount(name)
// log.Debugf("onedrive account: %+v", newAccount)
// if !ok {
// return
// }
// err = driver.RefreshToken(&newAccount)
// _ = model.SaveAccount(&newAccount)
//})
//if err != nil {
// return err
//}
//account.CronId = int(cronId)
return nil
}
@ -177,7 +176,7 @@ func (driver Onedrive) Link(args base.Args, account *model.Account) (*base.Link,
if err != nil {
return nil, err
}
if file.File.MimeType == "" {
if file.File == nil {
return nil, base.ErrNotFile
}
link := base.Link{
@ -202,9 +201,9 @@ func (driver Onedrive) Path(path string, account *model.Account) (*model.File, [
return nil, files, nil
}
func (driver Onedrive) Proxy(c *gin.Context, account *model.Account) {
c.Request.Header.Del("Origin")
}
//func (driver Onedrive) Proxy(r *http.Request, account *model.Account) {
// r.Header.Del("Origin")
//}
func (driver Onedrive) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport

View File

@ -19,8 +19,6 @@ import (
"time"
)
var oneClient = resty.New()
type Host struct {
Oauth string
Api string
@ -47,7 +45,7 @@ var onedriveHostMap = map[string]Host{
func (driver Onedrive) GetMetaUrl(account *model.Account, auth bool, path string) string {
path = filepath.Join(account.RootFolder, path)
log.Debugf(path)
//log.Debugf(path)
host, _ := onedriveHostMap[account.Zone]
if auth {
return host.Oauth
@ -81,7 +79,7 @@ type OneTokenErr struct {
func (driver Onedrive) RefreshToken(account *model.Account) error {
err := driver.refreshToken(account)
if err != nil && err.Error() == "empty refresh_token" {
if err != nil && err == base.ErrEmptyToken {
return driver.refreshToken(account)
}
return err
@ -91,7 +89,7 @@ func (driver Onedrive) refreshToken(account *model.Account) error {
url := driver.GetMetaUrl(account, true, "") + "/common/oauth2/v2.0/token"
var resp base.TokenResp
var e OneTokenErr
_, err := oneClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
"grant_type": "refresh_token",
"client_id": account.ClientId,
"client_secret": account.ClientSecret,
@ -109,8 +107,8 @@ func (driver Onedrive) refreshToken(account *model.Account) error {
account.Status = "work"
}
if resp.RefreshToken == "" {
account.Status = "empty refresh_token"
return errors.New("empty refresh_token")
account.Status = base.ErrEmptyToken.Error()
return base.ErrEmptyToken
}
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
return nil
@ -122,7 +120,7 @@ type OneFile struct {
Size int64 `json:"size"`
LastModifiedDateTime *time.Time `json:"lastModifiedDateTime"`
Url string `json:"@microsoft.graph.downloadUrl"`
File struct {
File *struct {
MimeType string `json:"mimeType"`
} `json:"file"`
Thumbnails []struct {
@ -159,7 +157,7 @@ func (driver Onedrive) FormatFile(file *OneFile) *model.File {
if len(file.Thumbnails) > 0 {
f.Thumbnail = file.Thumbnails[0].Medium.Url
}
if file.File.MimeType == "" {
if file.File == nil {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(filepath.Ext(file.Name))
@ -178,16 +176,17 @@ func (driver Onedrive) GetFiles(account *model.Account, path string) ([]OneFile,
}
for nextLink != "" {
var files OneFiles
var e OneRespErr
_, err := oneClient.R().SetResult(&files).SetError(&e).
SetHeader("Authorization", "Bearer "+account.AccessToken).
Get(nextLink)
_, err := driver.Request(nextLink, base.Get, nil, nil, nil, nil, &files, account)
//var e OneRespErr
//_, err := oneClient.R().SetResult(&files).SetError(&e).
// SetHeader("Authorization", "Bearer "+account.AccessToken).
// Get(nextLink)
if err != nil {
return nil, err
}
if e.Error.Code != "" {
return nil, fmt.Errorf("%s", e.Error.Message)
}
//if e.Error.Code != "" {
// return nil, fmt.Errorf("%s", e.Error.Message)
//}
res = append(res, files.Value...)
nextLink = files.NextLink
}
@ -196,16 +195,18 @@ func (driver Onedrive) GetFiles(account *model.Account, path string) ([]OneFile,
func (driver Onedrive) GetFile(account *model.Account, path string) (*OneFile, error) {
var file OneFile
var e OneRespErr
_, err := oneClient.R().SetResult(&file).SetError(&e).
SetHeader("Authorization", "Bearer "+account.AccessToken).
Get(driver.GetMetaUrl(account, false, path))
//var e OneRespErr
u := driver.GetMetaUrl(account, false, path)
_, err := driver.Request(u, base.Get, nil, nil, nil, nil, &file, account)
//_, err := oneClient.R().SetResult(&file).SetError(&e).
// SetHeader("Authorization", "Bearer "+account.AccessToken).
// Get(driver.GetMetaUrl(account, false, path))
if err != nil {
return nil, err
}
if e.Error.Code != "" {
return nil, fmt.Errorf("%s", e.Error.Message)
}
//if e.Error.Code != "" {
// return nil, fmt.Errorf("%s", e.Error.Message)
//}
return &file, nil
}
@ -252,7 +253,7 @@ func (driver Onedrive) Request(url string, method int, headers, query, form map[
if err != nil {
return nil, err
}
log.Debug(res.String())
//log.Debug(res.String())
if e.Error.Code != "" {
if e.Error.Code == "InvalidAuthenticationToken" {
err = driver.RefreshToken(account)
@ -306,13 +307,14 @@ func (driver Onedrive) UploadBig(file *model.FileStream, account *model.Account)
res, err := base.HttpClient.Do(req)
if res.StatusCode != 201 && res.StatusCode != 202 {
data, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return errors.New(string(data))
}
res.Body.Close()
}
return nil
}
func init() {
base.RegisterDriver(&Onedrive{})
oneClient.SetRetryCount(3)
}

View File

@ -4,17 +4,51 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
log "github.com/sirupsen/logrus"
"runtime/debug"
)
func Save(driver base.Driver, account, old *model.Account) error {
return driver.Save(account, old)
}
func Path(driver base.Driver, account *model.Account, path string) (*model.File, []model.File, error) {
return driver.Path(path, account)
}
func Files(driver base.Driver, account *model.Account, path string) ([]model.File, error) {
_, files, err := Path(driver, account, path)
if err != nil {
return nil, err
}
if files == nil {
return nil, base.ErrNotFolder
}
return files, nil
}
func File(driver base.Driver, account *model.Account, path string) (*model.File, error) {
return driver.File(path, account)
}
func MakeDir(driver base.Driver, account *model.Account, path string, clearCache bool) error {
err := driver.MakeDir(path, account)
log.Debugf("mkdir: %s", path)
_, err := Files(driver, account, path)
if err != base.ErrPathNotFound {
return nil
}
err = driver.MakeDir(path, account)
if err == nil && clearCache {
_ = base.DeleteCache(utils.Dir(path), account)
}
if err != nil {
log.Errorf("mkdir error: %s", err.Error())
}
return err
}
func Move(driver base.Driver, account *model.Account, src, dst string, clearCache bool) error {
log.Debugf("move %s to %s", src, dst)
rename := false
if utils.Dir(src) == utils.Dir(dst) {
rename = true
@ -31,22 +65,33 @@ func Move(driver base.Driver, account *model.Account, src, dst string, clearCach
_ = base.DeleteCache(utils.Dir(dst), account)
}
}
if err != nil {
log.Errorf("move error: %s", err.Error())
}
return err
}
func Copy(driver base.Driver, account *model.Account, src, dst string, clearCache bool) error {
log.Debugf("copy %s to %s", src, dst)
err := driver.Copy(src, dst, account)
if err == nil && clearCache {
_ = base.DeleteCache(utils.Dir(dst), account)
}
if err != nil {
log.Errorf("copy error: %s", err.Error())
}
return err
}
func Delete(driver base.Driver, account *model.Account, path string, clearCache bool) error {
log.Debugf("delete %s", path)
err := driver.Delete(path, account)
if err == nil && clearCache {
_ = base.DeleteCache(utils.Dir(path), account)
}
if err != nil {
log.Errorf("delete error: %s", err.Error())
}
return err
}
@ -58,5 +103,9 @@ func Upload(driver base.Driver, account *model.Account, file *model.FileStream,
if err == nil && clearCache {
_ = base.DeleteCache(file.ParentPath, account)
}
if err != nil {
log.Errorf("upload error: %s", err.Error())
}
debug.FreeOSMemory()
return err
}

View File

@ -10,7 +10,6 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"path/filepath"
@ -51,6 +50,9 @@ func (driver PikPak) Items() []base.Item {
}
func (driver PikPak) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
err := driver.Login(account)
return err
}
@ -117,9 +119,14 @@ func (driver PikPak) Link(args base.Args, account *model.Account) (*base.Link, e
if err != nil {
return nil, err
}
return &base.Link{
link := base.Link{
Url: resp.WebContentLink,
}, nil
}
if len(resp.Medias) > 0 && resp.Medias[0].Link.Url != "" {
log.Debugln("use media link")
link.Url = resp.Medias[0].Link.Url
}
return &link, nil
}
func (driver PikPak) Path(path string, account *model.Account) (*model.File, []model.File, error) {
@ -139,9 +146,9 @@ func (driver PikPak) Path(path string, account *model.Account) (*model.File, []m
return nil, files, nil
}
func (driver PikPak) Proxy(c *gin.Context, account *model.Account) {
}
//func (driver PikPak) Proxy(r *http.Request, account *model.Account) {
//
//}
func (driver PikPak) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport

View File

@ -144,6 +144,37 @@ type File struct {
Size string `json:"size"`
ThumbnailLink string `json:"thumbnail_link"`
WebContentLink string `json:"web_content_link"`
Medias []Media `json:"medias"`
}
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"`
}
func (driver PikPak) FormatFile(file *File) *model.File {

327
drivers/quark/driver.go Normal file
View File

@ -0,0 +1,327 @@
package quark
import (
"crypto/md5"
"crypto/sha1"
"encoding/hex"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
"os"
"path/filepath"
)
type Quark struct{}
func (driver Quark) Config() base.DriverConfig {
return base.DriverConfig{
Name: "Quark",
OnlyProxy: true,
}
}
func (driver Quark) Items() []base.Item {
return []base.Item{
{
Name: "access_token",
Label: "Cookie",
Type: base.TypeString,
Required: true,
Description: "Unknown expiration time",
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
Required: true,
Default: "0",
},
{
Name: "order_by",
Label: "order_by",
Type: base.TypeSelect,
Values: "file_type,file_name,updated_at",
Required: true,
Default: "file_name",
},
{
Name: "order_direction",
Label: "order_direction",
Type: base.TypeSelect,
Values: "asc,desc",
Required: true,
Default: "asc",
},
}
}
func (driver Quark) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
_, err := driver.Get("/config", nil, nil, account)
if err == nil {
account.Status = "work"
} else {
account.Status = err.Error()
}
_ = model.SaveAccount(account)
return err
}
func (driver Quark) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver Quark) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
var files []model.File
cache, err := base.GetCache(path, account)
if err == nil {
files, _ = cache.([]model.File)
} else {
file, err := driver.File(path, account)
if err != nil {
return nil, err
}
files, err = driver.GetFiles(file.Id, account)
if err != nil {
return nil, err
}
if len(files) > 0 {
_ = base.SetCache(path, files, account)
}
}
return files, nil
}
func (driver Quark) Link(args base.Args, account *model.Account) (*base.Link, error) {
path := args.Path
file, err := driver.File(path, account)
if err != nil {
return nil, err
}
data := base.Json{
"fids": []string{file.Id},
}
var resp DownResp
_, err = driver.Post("/file/download", data, &resp, account)
if err != nil {
return nil, err
}
return &base.Link{
Url: resp.Data[0].DownloadUrl,
Headers: []base.Header{
{Name: "Cookie", Value: account.AccessToken},
{Name: "Referer", Value: "https://pan.quark.cn"},
},
}, nil
}
func (driver Quark) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
log.Debugf("quark path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver Quark) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver Quark) MakeDir(path string, account *model.Account) error {
parentFile, err := driver.File(utils.Dir(path), account)
if err != nil {
return err
}
data := base.Json{
"dir_init_lock": false,
"dir_path": "",
"file_name": utils.Base(path),
"pdir_fid": parentFile.Id,
}
_, err = driver.Post("/file", data, nil, account)
return err
}
func (driver Quark) Move(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
dstParentFile, err := driver.File(utils.Dir(dst), account)
if err != nil {
return err
}
data := base.Json{
"action_type": 1,
"exclude_fids": []string{},
"filelist": []string{srcFile.Id},
"to_pdir_fid": dstParentFile.Id,
}
_, err = driver.Post("/file/move", data, nil, account)
return err
}
func (driver Quark) Rename(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
data := base.Json{
"fid": srcFile.Id,
"file_name": utils.Base(dst),
}
_, err = driver.Post("/file/rename", data, nil, account)
return err
}
func (driver Quark) Copy(src string, dst string, account *model.Account) error {
return base.ErrNotSupport
}
func (driver Quark) Delete(path string, account *model.Account) error {
srcFile, err := driver.File(path, account)
if err != nil {
return err
}
data := base.Json{
"action_type": 1,
"exclude_fids": []string{},
"filelist": []string{srcFile.Id},
}
_, err = driver.Post("/file/delete", data, nil, account)
return err
}
func (driver Quark) Upload(file *model.FileStream, account *model.Account) error {
if file == nil {
return base.ErrEmptyFile
}
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
}
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
_, err = io.Copy(tempFile, file)
if err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
m := md5.New()
_, err = io.Copy(m, tempFile)
if err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
md5Str := hex.EncodeToString(m.Sum(nil))
s := sha1.New()
_, err = io.Copy(s, tempFile)
if err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
sha1Str := hex.EncodeToString(s.Sum(nil))
// pre
pre, err := driver.UpPre(file, parentFile.Id, account)
if err != nil {
return err
}
log.Debugln("hash: ", md5Str, sha1Str)
// hash
finish, err := driver.UpHash(md5Str, sha1Str, pre.Data.TaskId, account)
if err != nil {
return err
}
if finish {
return nil
}
// part up
partSize := pre.Metadata.PartSize
var bytes []byte
md5s := make([]string, 0)
defaultBytes := make([]byte, partSize)
left := int64(file.GetSize())
partNumber := 1
for left > 0 {
if left > int64(partSize) {
bytes = defaultBytes
} else {
bytes = make([]byte, left)
}
_, err := io.ReadFull(tempFile, bytes)
if err != nil {
return err
}
left -= int64(partSize)
log.Debugf("left: %d", left)
m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account)
//m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account, md5Str, sha1Str)
if err != nil {
return err
}
if m == "finish" {
return nil
}
md5s = append(md5s, m)
partNumber++
}
err = driver.UpCommit(pre, md5s, account)
if err != nil {
return err
}
return driver.UpFinish(pre, account)
}
var _ base.Driver = (*Quark)(nil)

272
drivers/quark/quark.go Normal file
View File

@ -0,0 +1,272 @@
package quark
import (
"crypto/md5"
"encoding/base64"
"errors"
"fmt"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/Xhofe/alist/utils/cookie"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"net/http"
"strconv"
"strings"
"time"
)
func (driver Quark) Request(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
u := "https://drive.quark.cn/1/clouddrive" + pathname
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"Cookie": account.AccessToken,
"Accept": "application/json, text/plain, */*",
"Referer": "https://pan.quark.cn/",
})
req.SetQueryParam("pr", "ucpro")
req.SetQueryParam("fr", "pc")
if headers != nil {
req.SetHeaders(headers)
}
if query != nil {
req.SetQueryParams(query)
}
if form != nil {
req.SetFormData(form)
}
if data != nil {
req.SetBody(data)
}
if resp != nil {
req.SetResult(resp)
}
var e Resp
var err error
var res *resty.Response
req.SetError(&e)
switch method {
case base.Get:
res, err = req.Get(u)
case base.Post:
res, err = req.Post(u)
case base.Delete:
res, err = req.Delete(u)
case base.Patch:
res, err = req.Patch(u)
case base.Put:
res, err = req.Put(u)
default:
return nil, base.ErrNotSupport
}
if err != nil {
return nil, err
}
__puus := cookie.GetCookie(res.Cookies(), "__puus")
if __puus != nil {
account.AccessToken = cookie.SetStr(account.AccessToken, "__puus", __puus.Value)
_ = model.SaveAccount(account)
}
//log.Debugf("%s response: %s", pathname, res.String())
if e.Status >= 400 || e.Code != 0 {
return nil, errors.New(e.Message)
}
return res.Body(), nil
}
func (driver Quark) Get(pathname string, query map[string]string, resp interface{}, account *model.Account) ([]byte, error) {
return driver.Request(pathname, base.Get, nil, query, nil, nil, resp, account)
}
func (driver Quark) Post(pathname string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
return driver.Request(pathname, base.Post, nil, nil, nil, data, resp, account)
}
func (driver Quark) GetFiles(parent string, account *model.Account) ([]model.File, error) {
files := make([]model.File, 0)
page := 1
size := 100
query := map[string]string{
"pdir_fid": parent,
"_size": strconv.Itoa(size),
"_fetch_total": "1",
"_sort": "file_type:asc," + account.OrderBy + ":" + account.OrderDirection,
}
for {
query["_page"] = strconv.Itoa(page)
var resp SortResp
_, err := driver.Get("/file/sort", query, &resp, account)
if err != nil {
return nil, err
}
for _, f := range resp.Data.List {
files = append(files, *driver.formatFile(&f))
}
if page*size >= resp.Metadata.Total {
break
}
page++
}
return files, nil
}
func (driver Quark) UpPre(file *model.FileStream, parentId string, account *model.Account) (UpPreResp, error) {
now := time.Now()
data := base.Json{
"ccp_hash_update": true,
"dir_name": "",
"file_name": file.Name,
"format_type": file.MIMEType,
"l_created_at": now.UnixMilli(),
"l_updated_at": now.UnixMilli(),
"pdir_fid": parentId,
"size": file.Size,
}
log.Debugf("uppre data: %+v", data)
var resp UpPreResp
_, err := driver.Post("/file/upload/pre", data, &resp, account)
return resp, err
}
func (driver Quark) UpHash(md5, sha1, taskId string, account *model.Account) (bool, error) {
data := base.Json{
"md5": md5,
"sha1": sha1,
"task_id": taskId,
}
log.Debugf("hash: %+v", data)
var resp HashResp
_, err := driver.Post("/file/update/hash", data, &resp, account)
return resp.Data.Finish, err
}
func (driver Quark) UpPart(pre UpPreResp, mineType string, partNumber int, bytes []byte, account *model.Account) (string, error) {
//func (driver Quark) UpPart(pre UpPreResp, mineType string, partNumber int, bytes []byte, account *model.Account, md5Str, sha1Str string) (string, error) {
timeStr := time.Now().UTC().Format(http.TimeFormat)
data := base.Json{
"auth_info": pre.Data.AuthInfo,
"auth_meta": fmt.Sprintf(`PUT
%s
%s
x-oss-date:%s
x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit
/%s/%s?partNumber=%d&uploadId=%s`,
mineType, timeStr, timeStr, pre.Data.Bucket, pre.Data.ObjKey, partNumber, pre.Data.UploadId),
"task_id": pre.Data.TaskId,
}
var resp UpAuthResp
_, err := driver.Post("/file/upload/auth", data, &resp, account)
if err != nil {
return "", err
}
//if partNumber == 1 {
// finish, err := driver.UpHash(md5Str, sha1Str, pre.Data.TaskId, account)
// if err != nil {
// return "", err
// }
// if finish {
// return "finish", nil
// }
//}
u := fmt.Sprintf("https://%s.%s/%s", pre.Data.Bucket, pre.Data.UploadUrl[7:], pre.Data.ObjKey)
res, err := base.RestyClient.R().
SetHeaders(map[string]string{
"Authorization": resp.Data.AuthKey,
"Content-Type": mineType,
"Referer": "https://pan.quark.cn/",
"x-oss-date": timeStr,
"x-oss-user-agent": "aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit",
}).
SetQueryParams(map[string]string{
"partNumber": strconv.Itoa(partNumber),
"uploadId": pre.Data.UploadId,
}).SetBody(bytes).Put(u)
if res.StatusCode() != 200 {
return "", fmt.Errorf("up status: %d, error: %s", res.StatusCode(), res.String())
}
return res.Header().Get("ETag"), nil
}
func (driver Quark) UpCommit(pre UpPreResp, md5s []string, account *model.Account) error {
timeStr := time.Now().UTC().Format(http.TimeFormat)
log.Debugf("md5s: %+v", md5s)
bodyBuilder := strings.Builder{}
bodyBuilder.WriteString(`<?xml version="1.0" encoding="UTF-8"?>
<CompleteMultipartUpload>
`)
for i, m := range md5s {
bodyBuilder.WriteString(fmt.Sprintf(`<Part>
<PartNumber>%d</PartNumber>
<ETag>%s</ETag>
</Part>
`, i+1, m))
}
bodyBuilder.WriteString("</CompleteMultipartUpload>")
body := bodyBuilder.String()
m := md5.New()
m.Write([]byte(body))
contentMd5 := base64.StdEncoding.EncodeToString(m.Sum(nil))
callbackBytes, err := utils.Json.Marshal(pre.Data.Callback)
if err != nil {
return err
}
callbackBase64 := base64.StdEncoding.EncodeToString(callbackBytes)
data := base.Json{
"auth_info": pre.Data.AuthInfo,
"auth_meta": fmt.Sprintf(`POST
%s
application/xml
%s
x-oss-callback:%s
x-oss-date:%s
x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit
/%s/%s?uploadId=%s`,
contentMd5, timeStr, callbackBase64, timeStr,
pre.Data.Bucket, pre.Data.ObjKey, pre.Data.UploadId),
"task_id": pre.Data.TaskId,
}
log.Debugf("xml: %s", body)
log.Debugf("auth data: %+v", data)
var resp UpAuthResp
_, err = driver.Post("/file/upload/auth", data, &resp, account)
if err != nil {
return err
}
u := fmt.Sprintf("https://%s.%s/%s", pre.Data.Bucket, pre.Data.UploadUrl[7:], pre.Data.ObjKey)
res, err := base.RestyClient.R().
SetHeaders(map[string]string{
"Authorization": resp.Data.AuthKey,
"Content-MD5": contentMd5,
"Content-Type": "application/xml",
"Referer": "https://pan.quark.cn/",
"x-oss-callback": callbackBase64,
"x-oss-date": timeStr,
"x-oss-user-agent": "aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit",
}).
SetQueryParams(map[string]string{
"uploadId": pre.Data.UploadId,
}).SetBody(body).Post(u)
if res.StatusCode() != 200 {
return fmt.Errorf("up status: %d, error: %s", res.StatusCode(), res.String())
}
return nil
}
func (driver Quark) UpFinish(pre UpPreResp, account *model.Account) error {
data := base.Json{
"obj_key": pre.Data.ObjKey,
"task_id": pre.Data.TaskId,
}
_, err := driver.Post("/file/upload/finish", data, nil, account)
if err != nil {
return err
}
time.Sleep(time.Second)
return nil
}
func init() {
base.RegisterDriver(&Quark{})
}

134
drivers/quark/types.go Normal file
View File

@ -0,0 +1,134 @@
package quark
type Resp struct {
Status int `json:"status"`
Code int `json:"code"`
Message string `json:"message"`
//ReqId string `json:"req_id"`
//Timestamp int `json:"timestamp"`
}
type File struct {
Fid string `json:"fid"`
FileName string `json:"file_name"`
//PdirFid string `json:"pdir_fid"`
//Category int `json:"category"`
//FileType int `json:"file_type"`
Size int64 `json:"size"`
//FormatType string `json:"format_type"`
//Status int `json:"status"`
//Tags string `json:"tags,omitempty"`
//LCreatedAt int64 `json:"l_created_at"`
LUpdatedAt int64 `json:"l_updated_at"`
//NameSpace int `json:"name_space"`
//IncludeItems int `json:"include_items,omitempty"`
//RiskType int `json:"risk_type"`
//BackupSign int `json:"backup_sign"`
//Duration int `json:"duration"`
//FileSource string `json:"file_source"`
File bool `json:"file"`
//CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
//PrivateExtra struct {} `json:"_private_extra"`
//ObjCategory string `json:"obj_category,omitempty"`
//Thumbnail string `json:"thumbnail,omitempty"`
}
type SortResp struct {
Resp
Data struct {
List []File `json:"list"`
} `json:"data"`
Metadata struct {
Size int `json:"_size"`
Page int `json:"_page"`
Count int `json:"_count"`
Total int `json:"_total"`
Way string `json:"way"`
} `json:"metadata"`
}
type DownResp struct {
Resp
Data []struct {
//Fid string `json:"fid"`
//FileName string `json:"file_name"`
//PdirFid string `json:"pdir_fid"`
//Category int `json:"category"`
//FileType int `json:"file_type"`
//Size int `json:"size"`
//FormatType string `json:"format_type"`
//Status int `json:"status"`
//Tags string `json:"tags"`
//LCreatedAt int64 `json:"l_created_at"`
//LUpdatedAt int64 `json:"l_updated_at"`
//NameSpace int `json:"name_space"`
//Thumbnail string `json:"thumbnail"`
DownloadUrl string `json:"download_url"`
//Md5 string `json:"md5"`
//RiskType int `json:"risk_type"`
//RangeSize int `json:"range_size"`
//BackupSign int `json:"backup_sign"`
//ObjCategory string `json:"obj_category"`
//Duration int `json:"duration"`
//FileSource string `json:"file_source"`
//File bool `json:"file"`
//CreatedAt int64 `json:"created_at"`
//UpdatedAt int64 `json:"updated_at"`
//PrivateExtra struct {
//} `json:"_private_extra"`
} `json:"data"`
//Metadata struct {
// Acc2 string `json:"acc2"`
// Acc1 string `json:"acc1"`
//} `json:"metadata"`
}
type UpPreResp struct {
Resp
Data struct {
TaskId string `json:"task_id"`
Finish bool `json:"finish"`
UploadId string `json:"upload_id"`
ObjKey string `json:"obj_key"`
UploadUrl string `json:"upload_url"`
Fid string `json:"fid"`
Bucket string `json:"bucket"`
Callback struct {
CallbackUrl string `json:"callbackUrl"`
CallbackBody string `json:"callbackBody"`
} `json:"callback"`
FormatType string `json:"format_type"`
Size int `json:"size"`
AuthInfo string `json:"auth_info"`
} `json:"data"`
Metadata struct {
PartThread int `json:"part_thread"`
Acc2 string `json:"acc2"`
Acc1 string `json:"acc1"`
PartSize int `json:"part_size"` // 分片大小
} `json:"metadata"`
}
type HashResp struct {
Resp
Data struct {
Finish bool `json:"finish"`
Fid string `json:"fid"`
Thumbnail string `json:"thumbnail"`
FormatType string `json:"format_type"`
} `json:"data"`
Metadata struct {
} `json:"metadata"`
}
type UpAuthResp struct {
Resp
Data struct {
AuthKey string `json:"auth_key"`
Speed int `json:"speed"`
Headers []interface{} `json:"headers"`
} `json:"data"`
Metadata struct {
} `json:"metadata"`
}

31
drivers/quark/util.go Normal file
View File

@ -0,0 +1,31 @@
package quark
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"path"
"time"
)
func getTime(t int64) *time.Time {
tm := time.UnixMilli(t)
//log.Debugln(tm)
return &tm
}
func (driver Quark) formatFile(f *File) *model.File {
file := model.File{
Id: f.Fid,
Name: f.FileName,
Size: f.Size,
Driver: driver.Config().Name,
UpdatedAt: getTime(f.UpdatedAt),
}
if f.File {
file.Type = utils.GetFileType(path.Ext(f.FileName))
} else {
file.Type = conf.FOLDER
}
return &file
}

View File

@ -1,6 +1,7 @@
package s3
import (
"bytes"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
@ -8,11 +9,10 @@ import (
"github.com/Xhofe/alist/utils"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"io/ioutil"
"net/url"
"path/filepath"
"strings"
"time"
)
@ -71,14 +71,36 @@ func (driver S3) Items() []base.Item {
},
{
Name: "limit",
Label: "url expire time(hours)",
Label: "Sign url expire time(hours)",
Type: base.TypeNumber,
Description: "default 4 hours",
},
{
Name: "zone",
Label: "placeholder filename",
Type: base.TypeString,
Description: "default empty string",
Default: defaultPlaceholderName,
},
{
Name: "bool_1",
Label: "S3ForcePathStyle",
Type: base.TypeBool,
},
{
Name: "internal_type",
Label: "ListObject Version",
Type: base.TypeSelect,
Values: "v1,v2",
Default: "v1",
},
}
}
func (driver S3) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
if account.Limit == 0 {
account.Limit = 4
}
@ -124,8 +146,12 @@ func (driver S3) Files(path string, account *model.Account) ([]model.File, error
cache, err := base.GetCache(path, account)
if err == nil {
files, _ = cache.([]model.File)
} else {
if account.InternalType == "v2" {
files, err = driver.ListV2(path, account)
} else {
files, err = driver.List(path, account)
}
if err == nil && len(files) > 0 {
_ = base.SetCache(path, files, account)
}
@ -134,19 +160,28 @@ func (driver S3) Files(path string, account *model.Account) ([]model.File, error
}
func (driver S3) Link(args base.Args, account *model.Account) (*base.Link, error) {
client, err := driver.GetClient(account)
client, err := driver.GetClient(account, true)
if err != nil {
return nil, err
}
path := strings.TrimPrefix(args.Path, "/")
path := driver.GetKey(args.Path, account, false)
disposition := fmt.Sprintf(`attachment;filename="%s"`, url.QueryEscape(utils.Base(path)))
input := &s3.GetObjectInput{
Bucket: &account.Bucket,
Key: &path,
ResponseContentDisposition: &disposition,
//ResponseContentDisposition: &disposition,
}
if account.CustomHost == "" {
input.ResponseContentDisposition = &disposition
}
req, _ := client.GetObjectRequest(input)
link, err := req.Presign(time.Hour * time.Duration(account.Limit))
var link string
if account.CustomHost != "" {
err = req.Build()
link = req.HTTPRequest.URL.String()
} else {
link, err = req.Presign(time.Hour * time.Duration(account.Limit))
}
if err != nil {
return nil, err
}
@ -172,17 +207,28 @@ func (driver S3) Path(path string, account *model.Account) (*model.File, []model
return nil, files, nil
}
func (driver S3) Proxy(c *gin.Context, account *model.Account) {
}
//func (driver S3) Proxy(r *http.Request, account *model.Account) {
//
//}
func (driver S3) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver S3) MakeDir(path string, account *model.Account) error {
// not support, default as success
// not support, generate a placeholder file
_, err := driver.File(path, account)
// exist
if err != base.ErrPathNotFound {
return nil
}
return driver.Upload(&model.FileStream{
File: ioutil.NopCloser(bytes.NewReader([]byte{})),
Size: 0,
ParentPath: path,
Name: getPlaceholderName(account.Zone),
MIMEType: "application/octet-stream",
}, account)
}
func (driver S3) Move(src string, dst string, account *model.Account) error {
@ -198,7 +244,7 @@ func (driver S3) Rename(src string, dst string, account *model.Account) error {
}
func (driver S3) Copy(src string, dst string, account *model.Account) error {
client, err := driver.GetClient(account)
client, err := driver.GetClient(account, false)
if err != nil {
return err
}
@ -218,7 +264,7 @@ func (driver S3) Copy(src string, dst string, account *model.Account) error {
}
func (driver S3) Delete(path string, account *model.Account) error {
client, err := driver.GetClient(account)
client, err := driver.GetClient(account, false)
if err != nil {
return err
}
@ -245,6 +291,7 @@ func (driver S3) Upload(file *model.FileStream, account *model.Account) error {
}
uploader := s3manager.NewUploader(s)
key := driver.GetKey(utils.Join(file.ParentPath, file.GetFileName()), account, false)
log.Debugln("key:", key)
input := &s3manager.UploadInput{
Bucket: &account.Bucket,
Key: &key,

View File

@ -1,6 +1,7 @@
package s3
import (
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
@ -25,17 +26,18 @@ func (driver S3) NewSession(account *model.Account) (*session.Session, error) {
Credentials: credentials.NewStaticCredentials(account.AccessKey, account.AccessSecret, ""),
Region: &account.Region,
Endpoint: &account.Endpoint,
S3ForcePathStyle: aws.Bool(account.Bool1),
}
return session.NewSession(cfg)
}
func (driver S3) GetClient(account *model.Account) (*s3.S3, error) {
func (driver S3) GetClient(account *model.Account, link bool) (*s3.S3, error) {
s, ok := sessionsMap[account.Name]
if !ok {
return nil, fmt.Errorf("can't find [%s] session", account.Name)
}
client := s3.New(s)
if account.CustomHost != "" {
if link && account.CustomHost != "" {
cURL, err := url.Parse(account.CustomHost)
if err != nil {
return nil, err
@ -54,7 +56,7 @@ func (driver S3) GetClient(account *model.Account) (*s3.S3, error) {
func (driver S3) List(prefix string, account *model.Account) ([]model.File, error) {
prefix = driver.GetKey(prefix, account, true)
log.Debugf("list: %s", prefix)
client, err := driver.GetClient(account)
client, err := driver.GetClient(account, false)
if err != nil {
return nil, err
}
@ -73,9 +75,10 @@ func (driver S3) List(prefix string, account *model.Account) ([]model.File, erro
return nil, err
}
for _, object := range listObjectsResult.CommonPrefixes {
name := utils.Base(strings.Trim(*object.Prefix, "/"))
file := model.File{
//Id: *object.Key,
Name: utils.Base(strings.Trim(*object.Prefix, "/")),
Name: name,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
TimeStr: "-",
@ -84,9 +87,13 @@ func (driver S3) List(prefix string, account *model.Account) ([]model.File, erro
files = append(files, file)
}
for _, object := range listObjectsResult.Contents {
name := utils.Base(*object.Key)
if name == getPlaceholderName(account.Zone) {
continue
}
file := model.File{
//Id: *object.Key,
Name: utils.Base(*object.Key),
Name: name,
Size: *object.Size,
Driver: driver.Config().Name,
UpdatedAt: object.LastModified,
@ -94,6 +101,9 @@ func (driver S3) List(prefix string, account *model.Account) ([]model.File, erro
}
files = append(files, file)
}
if listObjectsResult.IsTruncated == nil {
return nil, errors.New("IsTruncated nil")
}
if *listObjectsResult.IsTruncated {
marker = *listObjectsResult.NextMarker
} else {
@ -103,6 +113,73 @@ func (driver S3) List(prefix string, account *model.Account) ([]model.File, erro
return files, nil
}
func (driver S3) ListV2(prefix string, account *model.Account) ([]model.File, error) {
prefix = driver.GetKey(prefix, account, true)
//if prefix == "" {
// prefix = "/"
//}
log.Debugf("list: %s", prefix)
client, err := driver.GetClient(account, false)
if err != nil {
return nil, err
}
files := make([]model.File, 0)
var continuationToken, startAfter *string
for {
input := &s3.ListObjectsV2Input{
Bucket: &account.Bucket,
ContinuationToken: continuationToken,
Prefix: &prefix,
Delimiter: aws.String("/"),
StartAfter: startAfter,
}
listObjectsResult, err := client.ListObjectsV2(input)
if err != nil {
return nil, err
}
log.Debugf("resp: %+v", listObjectsResult)
for _, object := range listObjectsResult.CommonPrefixes {
name := utils.Base(strings.Trim(*object.Prefix, "/"))
file := model.File{
//Id: *object.Key,
Name: name,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
TimeStr: "-",
Type: conf.FOLDER,
}
files = append(files, file)
}
for _, object := range listObjectsResult.Contents {
name := utils.Base(*object.Key)
if name == getPlaceholderName(account.Zone) {
continue
}
file := model.File{
//Id: *object.Key,
Name: name,
Size: *object.Size,
Driver: driver.Config().Name,
UpdatedAt: object.LastModified,
Type: utils.GetFileType(path.Ext(*object.Key)),
}
files = append(files, file)
}
if !aws.BoolValue(listObjectsResult.IsTruncated) {
break
}
if listObjectsResult.NextContinuationToken != nil {
continuationToken = listObjectsResult.NextContinuationToken
continue
}
if len(listObjectsResult.Contents) == 0 {
break
}
startAfter = listObjectsResult.Contents[len(listObjectsResult.Contents)-1].Key
}
return files, nil
}
func (driver S3) GetKey(path string, account *model.Account, dir bool) string {
path = utils.Join(account.RootFolder, path)
path = strings.TrimPrefix(path, "/")
@ -113,6 +190,6 @@ func (driver S3) GetKey(path string, account *model.Account, dir bool) string {
}
func init() {
sessionsMap = make(map[string]*session.Session, 0)
sessionsMap = make(map[string]*session.Session)
base.RegisterDriver(&S3{})
}

10
drivers/s3/util.go Normal file
View File

@ -0,0 +1,10 @@
package s3
var defaultPlaceholderName = ".placeholder"
func getPlaceholderName(placeholder string) string {
if placeholder == "" {
return defaultPlaceholderName
}
return placeholder
}

220
drivers/sftp/driver.go Normal file
View File

@ -0,0 +1,220 @@
package template
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"io"
"path"
"path/filepath"
)
type SFTP struct {
}
func (driver SFTP) Config() base.DriverConfig {
return base.DriverConfig{
Name: "SFTP",
OnlyProxy: true,
OnlyLocal: true,
LocalSort: true,
}
}
func (driver SFTP) Items() []base.Item {
// TODO fill need info
return []base.Item{
{
Name: "site_url",
Label: "ip/host",
Type: base.TypeString,
Required: true,
},
{
Name: "limit",
Label: "port",
Type: base.TypeNumber,
Required: true,
Default: "22",
},
{
Name: "username",
Label: "username",
Type: base.TypeString,
Required: true,
},
{
Name: "password",
Label: "password",
Type: base.TypeString,
Required: true,
},
{
Name: "root_folder",
Label: "root folder path",
Type: base.TypeString,
Default: "/",
Required: true,
},
}
}
func (driver SFTP) Save(account *model.Account, old *model.Account) error {
if old != nil {
clientsMap.Lock()
defer clientsMap.Unlock()
delete(clientsMap.clients, old.Name)
}
if account == nil {
return nil
}
_, err := GetClient(account)
if err != nil {
account.Status = err.Error()
} else {
account.Status = "work"
}
_ = model.SaveAccount(account)
return err
}
func (driver SFTP) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver SFTP) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
remotePath := utils.Join(account.RootFolder, path)
cache, err := base.GetCache(path, account)
if err == nil {
files, _ := cache.([]model.File)
return files, nil
}
client, err := GetClient(account)
if err != nil {
return nil, err
}
files := make([]model.File, 0)
rawFiles, err := client.Files(remotePath)
if err != nil {
return nil, err
}
for i := 0; i < len(rawFiles); i++ {
files = append(files, driver.formatFile(rawFiles[i]))
}
if len(files) > 0 {
_ = base.SetCache(path, files, account)
}
return files, nil
}
func (driver SFTP) Link(args base.Args, account *model.Account) (*base.Link, error) {
client, err := GetClient(account)
if err != nil {
return nil, err
}
remoteFileName := utils.Join(account.RootFolder, args.Path)
remoteFile, err := client.Open(remoteFileName)
if err != nil {
return nil, err
}
return &base.Link{
Data: remoteFile,
}, nil
}
func (driver SFTP) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver SFTP) Preview(path string, account *model.Account) (interface{}, error) {
//TODO preview interface if driver support
return nil, base.ErrNotImplement
}
func (driver SFTP) MakeDir(path string, account *model.Account) error {
client, err := GetClient(account)
if err != nil {
return err
}
return client.MkdirAll(utils.Join(account.RootFolder, path))
}
func (driver SFTP) Move(src string, dst string, account *model.Account) error {
return driver.Rename(src, dst, account)
}
func (driver SFTP) Rename(src string, dst string, account *model.Account) error {
client, err := GetClient(account)
if err != nil {
return err
}
return client.Rename(utils.Join(account.RootFolder, src), utils.Join(account.RootFolder, dst))
}
func (driver SFTP) Copy(src string, dst string, account *model.Account) error {
return base.ErrNotSupport
}
func (driver SFTP) Delete(path string, account *model.Account) error {
client, err := GetClient(account)
if err != nil {
return err
}
return client.Remove(utils.Join(account.RootFolder, path))
}
func (driver SFTP) Upload(file *model.FileStream, account *model.Account) error {
if file == nil {
return base.ErrEmptyFile
}
client, err := GetClient(account)
if err != nil {
return err
}
dstFile, err := client.Create(path.Join(account.RootFolder, file.ParentPath, file.Name))
if err != nil {
return err
}
defer func() {
_ = dstFile.Close()
}()
_, err = io.Copy(dstFile, file)
return err
}
var _ base.Driver = (*SFTP)(nil)

110
drivers/sftp/sftp.go Normal file
View File

@ -0,0 +1,110 @@
package template
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"os"
"path"
"sync"
)
var clientsMap = struct {
sync.Mutex
clients map[string]*Client
}{clients: make(map[string]*Client)}
func GetClient(account *model.Account) (*Client, error) {
clientsMap.Lock()
defer clientsMap.Unlock()
if v, ok := clientsMap.clients[account.Name]; ok {
return v, nil
}
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", account.SiteUrl, account.Limit), &ssh.ClientConfig{
User: account.Username,
Auth: []ssh.AuthMethod{ssh.Password(account.Password)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
if err != nil {
return nil, err
}
client, err := sftp.NewClient(conn)
if err != nil {
return nil, err
}
c := &Client{client}
clientsMap.clients[account.Name] = c
return c, nil
}
type Client struct {
*sftp.Client
}
func (client *Client) Files(remotePath string) ([]os.FileInfo, error) {
return client.ReadDir(remotePath)
}
func (client *Client) Remove(remotePath string) error {
f, err := client.Stat(remotePath)
if err != nil {
return nil
}
if f.IsDir() {
return client.removeDirectory(remotePath)
} else {
return client.removeFile(remotePath)
}
}
func (client *Client) removeDirectory(remotePath string) error {
//打不开,说明要么文件路径错误了,要么是第一次部署
remoteFiles, err := client.ReadDir(remotePath)
if err != nil {
return err
}
for _, backupDir := range remoteFiles {
remoteFilePath := path.Join(remotePath, backupDir.Name())
if backupDir.IsDir() {
err := client.removeDirectory(remoteFilePath)
if err != nil {
return err
}
} else {
err := client.Remove(path.Join(remoteFilePath))
if err != nil {
return err
}
}
}
return client.RemoveDirectory(remotePath)
}
func (client *Client) removeFile(remotePath string) error {
return client.Remove(utils.Join(remotePath))
}
func (driver SFTP) formatFile(f os.FileInfo) model.File {
t := f.ModTime()
file := model.File{
//Id: f.Id,
Name: f.Name(),
Size: f.Size(),
Driver: driver.Config().Name,
UpdatedAt: &t,
}
if f.IsDir() {
file.Type = conf.FOLDER
} else {
file.Type = utils.GetFileType(path.Ext(f.Name()))
}
return file
}
func init() {
base.RegisterDriver(&SFTP{})
}

18
drivers/sftp/types.go Normal file
View File

@ -0,0 +1,18 @@
package template
import "time"
// write all struct here
type Resp struct {
Code int `json:"code"`
Message string `json:"message"`
}
type File struct {
Id string `json:"id"`
FileName string `json:"file_name"`
Size int64 `json:"size"`
File bool `json:"file"`
UpdatedAt *time.Time `json:"updated_at"`
}

3
drivers/sftp/util.go Normal file
View File

@ -0,0 +1,3 @@
package template
// write util func here, such as cal sign

View File

@ -6,7 +6,6 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"path/filepath"
"strconv"
@ -48,6 +47,9 @@ func (driver Shandian) Items() []base.Item {
}
func (driver Shandian) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
if account.RootFolder == "" {
account.RootFolder = "0"
}
@ -151,9 +153,9 @@ func (driver Shandian) Path(path string, account *model.Account) (*model.File, [
return nil, files, nil
}
func (driver Shandian) Proxy(c *gin.Context, account *model.Account) {
}
//func (driver Shandian) Proxy(r *http.Request, account *model.Account) {
//
//}
func (driver Shandian) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport

View File

@ -5,7 +5,6 @@ import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"path/filepath"
)
@ -52,6 +51,7 @@ func (driver Teambition) Items() []base.Item {
Type: base.TypeSelect,
Values: "fileName,fileSize,updated,created",
Required: true,
Default: "fileName",
},
{
Name: "order_direction",
@ -59,11 +59,15 @@ func (driver Teambition) Items() []base.Item {
Type: base.TypeSelect,
Values: "Asc,Desc",
Required: true,
Default: "Asc",
},
}
}
func (driver Teambition) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
_, err := driver.Request("/api/v2/roles", base.Get, nil, nil, nil, nil, nil, account)
return err
}
@ -146,9 +150,9 @@ func (driver Teambition) Path(path string, account *model.Account) (*model.File,
return nil, files, nil
}
func (driver Teambition) Proxy(c *gin.Context, account *model.Account) {
}
//func (driver Teambition) Proxy(r *http.Request, account *model.Account) {
//
//}
func (driver Teambition) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
@ -249,7 +253,34 @@ func (driver Teambition) Delete(path string, account *model.Account) error {
}
func (driver Teambition) Upload(file *model.FileStream, account *model.Account) error {
return base.ErrNotImplement
if file == nil {
return base.ErrEmptyFile
}
parentFile, err := driver.File(file.ParentPath, account)
if !parentFile.IsDir() {
return base.ErrNotFolder
}
if err != nil {
return err
}
res, err := driver.Request("/projects", base.Get, nil, nil, nil, nil, nil, account)
if err != nil {
return err
}
token := GetBetweenStr(string(res), "strikerAuth&quot;:&quot;", "&quot;,&quot;phoneForLogin")
var newFile *FileUpload
if file.Size <= 20971520 {
// post upload
newFile, err = driver.upload(file, token, account)
} else {
// chunk upload
//err = base.ErrNotImplement
newFile, err = driver.chunkUpload(file, token, account)
}
if err != nil {
return err
}
return driver.finishUpload(newFile, parentFile.Id, account)
}
var _ base.Driver = (*Teambition)(nil)

View File

@ -2,11 +2,14 @@ package teambition
import (
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"io"
"path"
"strconv"
"time"
@ -66,25 +69,6 @@ func (driver Teambition) Request(pathname string, method int, headers, query, fo
return res.Body(), nil
}
type Collection struct {
ID string `json:"_id"`
Title string `json:"title"`
Updated time.Time `json:"updated"`
}
type Work struct {
ID string `json:"_id"`
FileName string `json:"fileName"`
FileSize int64 `json:"fileSize"`
FileKey string `json:"fileKey"`
FileCategory string `json:"fileCategory"`
DownloadURL string `json:"downloadUrl"`
ThumbnailURL string `json:"thumbnailUrl"`
Thumbnail string `json:"thumbnail"`
Updated time.Time `json:"updated"`
PreviewURL string `json:"previewUrl"`
}
func (driver Teambition) GetFiles(parentId string, account *model.Account) ([]model.File, error) {
files := make([]model.File, 0)
page := 1
@ -114,7 +98,7 @@ func (driver Teambition) GetFiles(parentId string, account *model.Account) ([]mo
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: &collection.Updated,
UpdatedAt: collection.Updated,
})
}
}
@ -142,7 +126,7 @@ func (driver Teambition) GetFiles(parentId string, account *model.Account) ([]mo
Size: work.FileSize,
Type: utils.GetFileType(path.Ext(work.FileName)),
Driver: driver.Config().Name,
UpdatedAt: &work.Updated,
UpdatedAt: work.Updated,
Thumbnail: work.Thumbnail,
Url: work.DownloadURL,
})
@ -151,6 +135,103 @@ func (driver Teambition) GetFiles(parentId string, account *model.Account) ([]mo
return files, nil
}
func (driver Teambition) upload(file *model.FileStream, token string, account *model.Account) (*FileUpload, error) {
prefix := "tcs"
if account.InternalType == "International" {
prefix = "us-tcs"
}
var newFile FileUpload
_, err := base.RestyClient.R().SetResult(&newFile).SetHeader("Authorization", token).
SetMultipartFormData(map[string]string{
"name": file.GetFileName(),
"type": file.GetMIMEType(),
"size": strconv.FormatUint(file.GetSize(), 10),
//"lastModifiedDate": "",
}).SetMultipartField("file", file.GetFileName(), file.GetMIMEType(), file).
Post(fmt.Sprintf("https://%s.teambition.net/upload", prefix))
if err != nil {
return nil, err
}
return &newFile, nil
}
func (driver Teambition) chunkUpload(file *model.FileStream, token string, account *model.Account) (*FileUpload, error) {
prefix := "tcs"
referer := "https://www.teambition.com/"
if account.InternalType == "International" {
prefix = "us-tcs"
referer = "https://us.teambition.com/"
}
var newChunk ChunkUpload
_, err := base.RestyClient.R().SetResult(&newChunk).SetHeader("Authorization", token).
SetBody(base.Json{
"fileName": file.GetFileName(),
"fileSize": file.GetSize(),
"lastUpdated": time.Now(),
}).Post(fmt.Sprintf("https://%s.teambition.net/upload/chunk", prefix))
if err != nil {
return nil, err
}
for i := 0; i < newChunk.Chunks; i++ {
chunkSize := newChunk.ChunkSize
if i == newChunk.Chunks-1 {
chunkSize = int(file.GetSize()) - i*chunkSize
}
log.Debugf("%d : %d", i, chunkSize)
chunkData := make([]byte, chunkSize)
_, err = io.ReadFull(file, chunkData)
if err != nil {
return nil, err
}
u := fmt.Sprintf("https://%s.teambition.net/upload/chunk/%s?chunk=%d&chunks=%d",
prefix, newChunk.FileKey, i+1, newChunk.Chunks)
log.Debugf("url: %s", u)
res, err := base.RestyClient.R().SetHeaders(map[string]string{
"Authorization": token,
"Content-Type": "application/octet-stream",
"Referer": referer,
}).SetBody(chunkData).Post(u)
if err != nil {
return nil, err
}
log.Debug(res.Status(), res.String())
//req, err := http.NewRequest("POST",
// u,
// bytes.NewBuffer(chunkData))
//if err != nil {
// return nil, err
//}
//req.Header.Set("Authorization", token)
//req.Header.Set("Content-Type", "application/octet-stream")
//req.Header.Set("Referer", "https://www.teambition.com/")
//resp, err := base.HttpClient.Do(req)
//res, _ := ioutil.ReadAll(resp.Body)
//log.Debugf("chunk upload status: %s, res: %s", resp.Status, string(res))
if err != nil {
return nil, err
}
}
res, err := base.RestyClient.R().SetHeader("Authorization", token).Post(
fmt.Sprintf("https://%s.teambition.net/upload/chunk/%s",
prefix, newChunk.FileKey))
log.Debug(res.Status(), res.String())
if err != nil {
return nil, err
}
return &newChunk.FileUpload, nil
}
func (driver Teambition) finishUpload(file *FileUpload, parentId string, account *model.Account) error {
file.InvolveMembers = []interface{}{}
file.Visible = "members"
file.ParentId = parentId
_, err := driver.Request("/api/works", base.Post, nil, nil, nil, base.Json{
"works": []FileUpload{*file},
"_parentId": parentId,
}, nil, account)
return err
}
func init() {
base.RegisterDriver(&Teambition{})
}

View File

@ -0,0 +1,63 @@
package teambition
import "time"
type Collection struct {
ID string `json:"_id"`
Title string `json:"title"`
Updated *time.Time `json:"updated"`
}
type Work struct {
ID string `json:"_id"`
FileName string `json:"fileName"`
FileSize int64 `json:"fileSize"`
FileKey string `json:"fileKey"`
FileCategory string `json:"fileCategory"`
DownloadURL string `json:"downloadUrl"`
ThumbnailURL string `json:"thumbnailUrl"`
Thumbnail string `json:"thumbnail"`
Updated *time.Time `json:"updated"`
PreviewURL string `json:"previewUrl"`
}
type FileUpload struct {
FileKey string `json:"fileKey"`
FileName string `json:"fileName"`
FileType string `json:"fileType"`
FileSize int `json:"fileSize"`
FileCategory string `json:"fileCategory"`
ImageWidth int `json:"imageWidth"`
ImageHeight int `json:"imageHeight"`
InvolveMembers []interface{} `json:"involveMembers"`
Source string `json:"source"`
Visible string `json:"visible"`
ParentId string `json:"_parentId"`
}
type ChunkUpload struct {
FileUpload
Storage string `json:"storage"`
MimeType string `json:"mimeType"`
Chunks int `json:"chunks"`
ChunkSize int `json:"chunkSize"`
Created time.Time `json:"created"`
FileMD5 string `json:"fileMD5"`
LastUpdated time.Time `json:"lastUpdated"`
UploadedChunks []interface{} `json:"uploadedChunks"`
Token struct {
AppID string `json:"AppID"`
OrganizationID string `json:"OrganizationID"`
UserID string `json:"UserID"`
Exp time.Time `json:"Exp"`
Storage string `json:"Storage"`
Resource string `json:"Resource"`
Speed int `json:"Speed"`
} `json:"token"`
DownloadUrl string `json:"downloadUrl"`
ThumbnailUrl string `json:"thumbnailUrl"`
PreviewUrl string `json:"previewUrl"`
ImmPreviewUrl string `json:"immPreviewUrl"`
PreviewExt string `json:"previewExt"`
LastUploadTime interface{} `json:"lastUploadTime"`
}

View File

@ -0,0 +1,18 @@
package teambition
import "strings"
func GetBetweenStr(str, start, end string) string {
n := strings.Index(str, start)
if n == -1 {
return ""
}
n = n + len(start)
str = string([]byte(str)[n:])
m := strings.Index(str, end)
if m == -1 {
return ""
}
str = string([]byte(str)[:m])
return str
}

151
drivers/template/driver.go Normal file
View File

@ -0,0 +1,151 @@
package template
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"path/filepath"
)
type Template struct {
base.Base
}
func (driver Template) Config() base.DriverConfig {
return base.DriverConfig{
Name: "Template",
OnlyProxy: false,
OnlyLocal: false,
ApiProxy: false,
NoNeedSetLink: false,
NoCors: false,
LocalSort: false,
}
}
func (driver Template) Items() []base.Item {
// TODO fill need info
return []base.Item{
{
Name: "refresh_token",
Label: "refresh token",
Type: base.TypeString,
Required: true,
},
{
Name: "root_folder",
Label: "root folder path",
Type: base.TypeString,
Default: "/",
Required: true,
},
}
}
func (driver Template) Save(account *model.Account, old *model.Account) error {
// TODO test available or init
return nil
}
func (driver Template) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver Template) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
cache, err := base.GetCache(path, account)
if err == nil {
files, _ := cache.([]model.File)
return files, nil
}
var files []model.File
// TODO get files
if err != nil {
return nil, err
}
if len(files) > 0 {
_ = base.SetCache(path, files, account)
}
return files, nil
}
func (driver Template) Link(args base.Args, account *model.Account) (*base.Link, error) {
// TODO get file link
return nil, base.ErrNotImplement
}
func (driver Template) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
// Optional function
//func (driver Template) Preview(path string, account *model.Account) (interface{}, error) {
// //TODO preview interface if driver support
// return nil, base.ErrNotImplement
//}
//
//func (driver Template) MakeDir(path string, account *model.Account) error {
// //TODO make dir
// return base.ErrNotImplement
//}
//
//func (driver Template) Move(src string, dst string, account *model.Account) error {
// //TODO move file/dir
// return base.ErrNotImplement
//}
//
//func (driver Template) Rename(src string, dst string, account *model.Account) error {
// //TODO rename file/dir
// return base.ErrNotImplement
//}
//
//func (driver Template) Copy(src string, dst string, account *model.Account) error {
// //TODO copy file/dir
// return base.ErrNotImplement
//}
//
//func (driver Template) Delete(path string, account *model.Account) error {
// //TODO delete file/dir
// return base.ErrNotImplement
//}
//
//func (driver Template) Upload(file *model.FileStream, account *model.Account) error {
// //TODO upload file
// return base.ErrNotImplement
//}
var _ base.Driver = (*Template)(nil)

View File

@ -0,0 +1,90 @@
package template
import (
"errors"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
"path"
)
func LoginOrRefreshToken(account *model.Account) error {
return nil
}
func Request(u string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"Authorization": "Bearer" + account.AccessToken,
"Accept": "application/json, text/plain, */*",
})
if headers != nil {
req.SetHeaders(headers)
}
if query != nil {
req.SetQueryParams(query)
}
if form != nil {
req.SetFormData(form)
}
if data != nil {
req.SetBody(data)
}
if resp != nil {
req.SetResult(resp)
}
var e Resp
var err error
var res *resty.Response
req.SetError(&e)
switch method {
case base.Get:
res, err = req.Get(u)
case base.Post:
res, err = req.Post(u)
case base.Delete:
res, err = req.Delete(u)
case base.Patch:
res, err = req.Patch(u)
case base.Put:
res, err = req.Put(u)
default:
return nil, base.ErrNotSupport
}
if err != nil {
return nil, err
}
if e.Code >= 400 {
if e.Code == 401 {
err = LoginOrRefreshToken(account)
if err != nil {
return nil, err
}
return Request(u, method, headers, query, form, data, resp, account)
}
return nil, errors.New(e.Message)
}
return res.Body(), nil
}
func (driver Template) formatFile(f *File) *model.File {
file := model.File{
Id: f.Id,
Name: f.FileName,
Size: f.Size,
Driver: driver.Config().Name,
UpdatedAt: f.UpdatedAt,
}
if f.File {
file.Type = utils.GetFileType(path.Ext(f.FileName))
} else {
file.Type = conf.FOLDER
}
return &file
}
func init() {
base.RegisterDriver(&Template{})
}

18
drivers/template/types.go Normal file
View File

@ -0,0 +1,18 @@
package template
import "time"
// write all struct here
type Resp struct {
Code int `json:"code"`
Message string `json:"message"`
}
type File struct {
Id string `json:"id"`
FileName string `json:"file_name"`
Size int64 `json:"size"`
File bool `json:"file"`
UpdatedAt *time.Time `json:"updated_at"`
}

3
drivers/template/util.go Normal file
View File

@ -0,0 +1,3 @@
package template
// write util func here, such as cal sign

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