mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-11-25 03:15:19 +08:00
Compare commits
439 Commits
v2.0.0-bet
...
v2.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c63e05983d | ||
|
|
678a982535 | ||
|
|
b2c02e6c5e | ||
|
|
7e05b0317f | ||
|
|
19f06dfaed | ||
|
|
1680a18578 | ||
|
|
e8f440ca5c | ||
|
|
7deff76f49 | ||
|
|
cd0afb9536 | ||
|
|
668a953cd8 | ||
|
|
8bfbaa74f6 | ||
|
|
3ccf5ee620 | ||
|
|
b44243c021 | ||
|
|
4ae81b5a79 | ||
|
|
189f4c19a5 | ||
|
|
92a3d74af5 | ||
|
|
bb73a10332 | ||
|
|
3baf1e8c7b | ||
|
|
fdb49f5fb4 | ||
|
|
2eedcc1626 | ||
|
|
6faecbd5d8 | ||
|
|
34ed05c62f | ||
|
|
ce83d6eb40 | ||
|
|
2271cb6c7c | ||
|
|
a42b30c96e | ||
|
|
ce25d16222 | ||
|
|
b392e093e3 | ||
|
|
0d5b7298db | ||
|
|
2063ebb74d | ||
|
|
0408d7ab5d | ||
|
|
d52451f9d2 | ||
|
|
ca9f77006a | ||
|
|
e8e8d925f3 | ||
|
|
623aab4c28 | ||
|
|
3bc81d471e | ||
|
|
dfddb5cfa1 | ||
|
|
80f5bde0cb | ||
|
|
9de072161e | ||
|
|
d08a7440bc | ||
|
|
7a4bb2496d | ||
|
|
f68ab40d26 | ||
|
|
796d490fb7 | ||
|
|
2964d5a6db | ||
|
|
90b57dacee | ||
|
|
6af17e2509 | ||
|
|
5193b2aa7d | ||
|
|
3f644f07db | ||
|
|
d988f98b81 | ||
|
|
10634c7b77 | ||
|
|
135d505192 | ||
|
|
3f2be8a6ca | ||
|
|
79bef09ee7 | ||
|
|
3534f6afac | ||
|
|
106c1d069c | ||
|
|
8ed0afe80d | ||
|
|
6a6e3944d5 | ||
|
|
94d5b5e47e | ||
|
|
e61b0f8e34 | ||
|
|
f7fbe1de6c | ||
|
|
01de01630e | ||
|
|
f9f92e2198 | ||
|
|
7d5f50b04a | ||
|
|
72b5d25e4c | ||
|
|
cae7f36531 | ||
|
|
aa79f49e25 | ||
|
|
b4ad301d53 | ||
|
|
00ed54c4c9 | ||
|
|
ffa52794db | ||
|
|
24058d0c36 | ||
|
|
641ca67671 | ||
|
|
52ee2e0a8b | ||
|
|
724fc7f37e | ||
|
|
9d279b104b | ||
|
|
eb61f70164 | ||
|
|
b3a8201768 | ||
|
|
185795954b | ||
|
|
cc62cc99d2 | ||
|
|
270349f37c | ||
|
|
977888070a | ||
|
|
192d0f2bf3 | ||
|
|
f2ec7884ec | ||
|
|
815975a4d2 | ||
|
|
f96a0238fc | ||
|
|
f695bd0959 | ||
|
|
efe8f46e17 | ||
|
|
515daa22a9 | ||
|
|
f11e22deaf | ||
|
|
5be976169f | ||
|
|
a6e08f3bf4 | ||
|
|
944e68a979 | ||
|
|
b3a6e33ce1 | ||
|
|
cb53ddc8e8 | ||
|
|
693417be4f | ||
|
|
5c3f91bb55 | ||
|
|
8a219d0732 | ||
|
|
146a544af3 | ||
|
|
48dccc6c0b | ||
|
|
ce1740cec4 | ||
|
|
d40dbeae3e | ||
|
|
5094b673c4 | ||
|
|
228e6d10e7 | ||
|
|
e90b979d15 | ||
|
|
fb05a6ca48 | ||
|
|
e055ed3afa | ||
|
|
4371c470b3 | ||
|
|
7bb237d0ef | ||
|
|
5c42354b01 | ||
|
|
387e8af422 | ||
|
|
5dca777caf | ||
|
|
0814778a14 | ||
|
|
6827af3997 | ||
|
|
435bdea8f7 | ||
|
|
4f81735af6 | ||
|
|
bef3d2f88d | ||
|
|
ba99c7dc03 | ||
|
|
f5c5162a9b | ||
|
|
a22903533e | ||
|
|
86cda58b22 | ||
|
|
7804cf9d5c | ||
|
|
2bb7036110 | ||
|
|
ba545555cf | ||
|
|
be55ca690c | ||
|
|
9013add749 | ||
|
|
3201b6da76 | ||
|
|
feb42f1f4b | ||
|
|
6f14d0eb5c | ||
|
|
7530d8f5b2 | ||
|
|
e25fe05a53 | ||
|
|
8e0ab8f780 | ||
|
|
cb2a3c2b42 | ||
|
|
1b6ec94f33 | ||
|
|
cb23edc1fe | ||
|
|
6fd05d7d72 | ||
|
|
f26ac57569 | ||
|
|
2434ac54d0 | ||
|
|
f25b557327 | ||
|
|
81a0706d01 | ||
|
|
5f6b576cbf | ||
|
|
549877f71e | ||
|
|
c6a5ba9b91 | ||
|
|
1a69d80489 | ||
|
|
b797f4302c | ||
|
|
bf9aa5c3d3 | ||
|
|
7390e19a7a | ||
|
|
b31a12a0cc | ||
|
|
26ce001782 | ||
|
|
a2c7ff3262 | ||
|
|
8fc7c716c0 | ||
|
|
c70fc3fc4b | ||
|
|
df513b7dc0 | ||
|
|
2a9598f4c6 | ||
|
|
224c20779c | ||
|
|
5d722298cb | ||
|
|
4bcc6359e3 | ||
|
|
4144afcc92 | ||
|
|
2ad27046fb | ||
|
|
9516ac6718 | ||
|
|
de638c7c36 | ||
|
|
c6b34a033b | ||
|
|
31de3399d2 | ||
|
|
0dc2ca019f | ||
|
|
04724f7f0f | ||
|
|
75a983a965 | ||
|
|
e12d8bb8ca | ||
|
|
68f1ccfed4 | ||
|
|
54272db59c | ||
|
|
6d34e88360 | ||
|
|
0a901a2eb0 | ||
|
|
e1671a0511 | ||
|
|
dcb4ec695f | ||
|
|
4a21b6fe1d | ||
|
|
96a237902b | ||
|
|
cfb51e9f80 | ||
|
|
e952f1c243 | ||
|
|
07d6ca27db | ||
|
|
8245da485a | ||
|
|
5c759217cf | ||
|
|
0648fdebc2 | ||
|
|
ed670e528f | ||
|
|
2473309a51 | ||
|
|
21ca2f11b7 | ||
|
|
ccaa28a323 | ||
|
|
fea8b376f8 | ||
|
|
55d244b726 | ||
|
|
1640a52789 | ||
|
|
424ec10692 | ||
|
|
b472c2ee18 | ||
|
|
65a01251e9 | ||
|
|
96be6bbbd1 | ||
|
|
6f7465aab7 | ||
|
|
c9bc8227bb | ||
|
|
4ea9371c00 | ||
|
|
326c74cdc6 | ||
|
|
f11c2efa8c | ||
|
|
6dd8102a82 | ||
|
|
ddf6a4955f | ||
|
|
602994e213 | ||
|
|
7c7306bf96 | ||
|
|
c24894b5de | ||
|
|
e10412c530 | ||
|
|
5c2491b6c3 | ||
|
|
036373032c | ||
|
|
db58dabd31 | ||
|
|
7ef98c05fa | ||
|
|
6351f43b9b | ||
|
|
6613c8a6c1 | ||
|
|
2b97882b42 | ||
|
|
925f386bed | ||
|
|
0fbbd54b0c | ||
|
|
12ba1fed00 | ||
|
|
2826bac53c | ||
|
|
3e7e9f354f | ||
|
|
86ff80885d | ||
|
|
be03e34406 | ||
|
|
83231becba | ||
|
|
876ee49fb0 | ||
|
|
10bec31033 | ||
|
|
fc48f29575 | ||
|
|
f0d9a452bb | ||
|
|
c03a4f83d1 | ||
|
|
10f06fde5c | ||
|
|
36b533cb16 | ||
|
|
6ff2cdab98 | ||
|
|
ef5cad1bf0 | ||
|
|
b60c7ecd9e | ||
|
|
68aaa8fee2 | ||
|
|
67bf14d428 | ||
|
|
c22ff77c89 | ||
|
|
84fc0ab1bd | ||
|
|
beb06f2f7f | ||
|
|
7cf30836bf | ||
|
|
e7ba289d06 | ||
|
|
d255ff4fd0 | ||
|
|
bcf19f4f3e | ||
|
|
efeee0e276 | ||
|
|
e789873eca | ||
|
|
14b9b76e87 | ||
|
|
38323fd24f | ||
|
|
366148a450 | ||
|
|
bc364cee0d | ||
|
|
f433277227 | ||
|
|
35fc1c87d2 | ||
|
|
9da2af8c49 | ||
|
|
103e049f22 | ||
|
|
cc217df924 | ||
|
|
939c9cd5ac | ||
|
|
6f0959a98e | ||
|
|
d71ed4d775 | ||
|
|
0af3e95f1f | ||
|
|
582f7bbfee | ||
|
|
cf2506901f | ||
|
|
db06b627cc | ||
|
|
5f2621eca9 | ||
|
|
3331462229 | ||
|
|
0f079827e5 | ||
|
|
ba66e33913 | ||
|
|
6a54ed87f3 | ||
|
|
88a9edb90a | ||
|
|
606134f39c | ||
|
|
3c03344ef1 | ||
|
|
6f5914ae6f | ||
|
|
4c00866249 | ||
|
|
04752f7473 | ||
|
|
26b4766da7 | ||
|
|
12af9cb89f | ||
|
|
958d793725 | ||
|
|
36f07ee194 | ||
|
|
91c2c21522 | ||
|
|
d6d2f52922 | ||
|
|
9162e782a0 | ||
|
|
dc41ceb99b | ||
|
|
c5e274f52a | ||
|
|
3781043c78 | ||
|
|
1485ab2677 | ||
|
|
337bf08cd3 | ||
|
|
22665aa19a | ||
|
|
1ab6b4e201 | ||
|
|
d97afb691b | ||
|
|
b63e65880f | ||
|
|
44cbe0522c | ||
|
|
fedab86c30 | ||
|
|
731dbf6c3a | ||
|
|
d00f75c814 | ||
|
|
f5b8815a84 | ||
|
|
99d06c7449 | ||
|
|
8e7b2c5837 | ||
|
|
3d3a97288a | ||
|
|
c2142cc03a | ||
|
|
3ce94de823 | ||
|
|
c64c003257 | ||
|
|
1c65588b4a | ||
|
|
d49f92b542 | ||
|
|
92a0453e00 | ||
|
|
bd7d27efc7 | ||
|
|
b2055777e0 | ||
|
|
fe79f9518b | ||
|
|
a7e9bb9e9a | ||
|
|
73d85d96f1 | ||
|
|
ff91d7a37d | ||
|
|
78f81ddc3b | ||
|
|
2f8258053f | ||
|
|
511efce624 | ||
|
|
14ff3450ab | ||
|
|
bbba161d55 | ||
|
|
6b61f8e9cc | ||
|
|
a295e7024a | ||
|
|
b36eaf08f0 | ||
|
|
bb6e520ab5 | ||
|
|
9b64e2e045 | ||
|
|
ee7c12c30f | ||
|
|
96d6d58910 | ||
|
|
2bf235a5ac | ||
|
|
236f9969c0 | ||
|
|
09e63027d9 | ||
|
|
a15dae291e | ||
|
|
efaaeedfb8 | ||
|
|
190c8001a5 | ||
|
|
b8698700ef | ||
|
|
985b81826f | ||
|
|
74d8fa3919 | ||
|
|
43e4928bb9 | ||
|
|
03580fd76c | ||
|
|
6e8d551420 | ||
|
|
28998d6f8c | ||
|
|
1779617cb9 | ||
|
|
7dfe48339c | ||
|
|
9c5627a382 | ||
|
|
809850321a | ||
|
|
bdc1f68746 | ||
|
|
9aaef6c3a3 | ||
|
|
bb50c52d0e | ||
|
|
6041e5a0fa | ||
|
|
308a86c36e | ||
|
|
ba7c4fc230 | ||
|
|
d81ec0637d | ||
|
|
1d7d37e642 | ||
|
|
b62a716267 | ||
|
|
944941db10 | ||
|
|
bd91acc5d0 | ||
|
|
cd50227835 | ||
|
|
50226f66e3 | ||
|
|
9dcaa9b07a | ||
|
|
fa6c0f78bc | ||
|
|
7f35dc6ade | ||
|
|
5d6463b75a | ||
|
|
733b38b435 | ||
|
|
50a02a7af7 | ||
|
|
71b1517de7 | ||
|
|
ffdd88ec66 | ||
|
|
4ff2756572 | ||
|
|
d955038ebc | ||
|
|
72d5e4e691 | ||
|
|
b1e662cd34 | ||
|
|
0f0e1104a4 | ||
|
|
3041da35ab | ||
|
|
9eab54a7c8 | ||
|
|
0b8d3a0a2c | ||
|
|
f9945a14a8 | ||
|
|
c39752ceb4 | ||
|
|
53b383d2cf | ||
|
|
e76fc3e616 | ||
|
|
eb21b87020 | ||
|
|
f577d82242 | ||
|
|
98691b2aa8 | ||
|
|
4fe6ed6c3e | ||
|
|
fe73ece57d | ||
|
|
59b8f1084a | ||
|
|
2f669ac45c | ||
|
|
d03d91d518 | ||
|
|
fe981f67ec | ||
|
|
8cfabfd0f5 | ||
|
|
163ee1159e | ||
|
|
e31402e94f | ||
|
|
5500980d63 | ||
|
|
b1695445e0 | ||
|
|
5db1ad4adf | ||
|
|
725f5b0c55 | ||
|
|
87a74394b3 | ||
|
|
a41c820525 | ||
|
|
cd53dc6d24 | ||
|
|
cfe16b5ed2 | ||
|
|
3d9746485d | ||
|
|
0b7f2fee7d | ||
|
|
36d52e0b75 | ||
|
|
e4d254e4b0 | ||
|
|
e8d27a30b4 | ||
|
|
69514668cc | ||
|
|
e5f8f59c87 | ||
|
|
f87ee1ed9e | ||
|
|
07155cfd01 | ||
|
|
5e982980dc | ||
|
|
8987958e26 | ||
|
|
4466cb19a5 | ||
|
|
da74e29b26 | ||
|
|
82272fcbf5 | ||
|
|
74d86f8cc4 | ||
|
|
c03646dedf | ||
|
|
c0d1888e25 | ||
|
|
f4942e89bd | ||
|
|
27e61c9eb8 | ||
|
|
aeb72320ca | ||
|
|
caddba05e9 | ||
|
|
0dffb9aaa1 | ||
|
|
0f93d2bfed | ||
|
|
b785945210 | ||
|
|
48a65784c7 | ||
|
|
5f34b8ab80 | ||
|
|
959ef620fb | ||
|
|
a73501463f | ||
|
|
cf07b3921c | ||
|
|
65ec4e3611 | ||
|
|
7799c1f3bc | ||
|
|
e902c2ded7 | ||
|
|
2c675ae909 | ||
|
|
98c017730f | ||
|
|
f4affb2c69 | ||
|
|
17af21079f | ||
|
|
f7d35ec925 | ||
|
|
a8730e82b5 | ||
|
|
6275e27d1b | ||
|
|
e70353704f | ||
|
|
be5b1e42d4 | ||
|
|
7d08cbc4a9 | ||
|
|
1542878d66 | ||
|
|
ac8f5d5737 | ||
|
|
9ed5b6e581 | ||
|
|
db7bff2d61 | ||
|
|
7970e737f0 | ||
|
|
d4523d52ee | ||
|
|
ac8476702c | ||
|
|
12f68eaed9 | ||
|
|
6a51f02845 | ||
|
|
e7071e1093 | ||
|
|
11b141b190 | ||
|
|
7e099b39cf | ||
|
|
b46bf0dfc9 | ||
|
|
91f64161b2 | ||
|
|
8255ef4346 | ||
|
|
254b6c6f79 |
62
.all-contributorsrc
Normal file
62
.all-contributorsrc
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "alist",
|
||||
"projectOwner": "Xhofe",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true
|
||||
}
|
||||
39
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: "Bug report"
|
||||
description: Bug report
|
||||
labels: [pending triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report, please confirm that your issue is not a duplicate issue
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Alist Version / Alist 版本
|
||||
description: What version of our software are you running?
|
||||
placeholder: v2.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the bug / 问题描述
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction / 复现链接
|
||||
description: |
|
||||
Please provide a link to a repo that can reproduce the problem you ran into.
|
||||
请提供能复现此问题的链接
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 日志 / Logs
|
||||
description: |
|
||||
Please copy and paste any relevant log output.
|
||||
请复制粘贴错误日志,或者截图
|
||||
render: shell
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/Xhofe/alist/discussions
|
||||
about: Use GitHub discussions for message-board style questions and discussions.
|
||||
24
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: "Feature request"
|
||||
description: Feature request
|
||||
labels: ["enhancement: pending triage"]
|
||||
body:
|
||||
- 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
21
.github/config.yml
vendored
Normal 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
|
||||
37
.github/workflows/build.yml
vendored
37
.github/workflows/build.yml
vendored
@@ -2,15 +2,15 @@ name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ v2 ]
|
||||
branches: [ '**' ]
|
||||
pull_request:
|
||||
branches: [ v2 ]
|
||||
branches: [ '**' ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [macos-latest]
|
||||
platform: [ubuntu-latest]
|
||||
go-version: [1.17]
|
||||
name: Build
|
||||
runs-on: ${{ matrix.platform }}
|
||||
@@ -25,43 +25,26 @@ jobs:
|
||||
with:
|
||||
node-version: '16'
|
||||
|
||||
- name: Setup docker
|
||||
uses: docker-practice/actions-setup-docker@master
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: v2
|
||||
path: alist
|
||||
|
||||
- name: Checkout web repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: Xhofe/alist-web
|
||||
ref: v2
|
||||
path: alist-web
|
||||
|
||||
- name: Set up xgo
|
||||
- name: Install upx
|
||||
run: |
|
||||
docker pull techknowlogick/xgo:latest
|
||||
go install src.techknowlogick.com/xgo@latest
|
||||
brew install upx
|
||||
|
||||
- name: Build web
|
||||
run: |
|
||||
cd alist-web
|
||||
yarn
|
||||
yarn build
|
||||
cd ..
|
||||
sudo apt install upx
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd alist
|
||||
bash build.sh
|
||||
cd ..
|
||||
mv alist/build.sh .
|
||||
bash build.sh web
|
||||
mv dist/* alist/public
|
||||
bash build.sh build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: artifact
|
||||
path: alist/build
|
||||
path: alist/build
|
||||
46
.github/workflows/build_docker.yml
vendored
Normal file
46
.github/workflows/build_docker.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: build_docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ v2 ]
|
||||
pull_request:
|
||||
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
|
||||
20
.github/workflows/issue_check_question.yml
vendored
Normal file
20
.github/workflows/issue_check_question.yml
vendored
Normal 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
25
.github/workflows/issue_duplicate.yml
vendored
Normal 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 }}
|
||||
16
.github/workflows/issue_month_statistics.yml
vendored
Normal file
16
.github/workflows/issue_month_statistics.yml
vendored
Normal 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
20
.github/workflows/issue_question.yml
vendored
Normal 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
25
.github/workflows/issue_wontfix.yml
vendored
Normal 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 }}
|
||||
35
.github/workflows/release.yml
vendored
35
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
release:
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [macos-latest]
|
||||
platform: [ubuntu-latest]
|
||||
go-version: [1.17]
|
||||
name: Release
|
||||
runs-on: ${{ matrix.platform }}
|
||||
@@ -19,45 +19,26 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Setup docker
|
||||
uses: docker-practice/actions-setup-docker@master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: v2
|
||||
path: alist
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout web repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: Xhofe/alist-web
|
||||
ref: v2
|
||||
path: alist-web
|
||||
|
||||
- name: Set up xgo
|
||||
- name: Install upx
|
||||
run: |
|
||||
docker pull techknowlogick/xgo:latest
|
||||
go install src.techknowlogick.com/xgo@latest
|
||||
brew install upx
|
||||
|
||||
- name: Build web
|
||||
run: |
|
||||
cd alist-web
|
||||
yarn
|
||||
yarn build
|
||||
cd ..
|
||||
sudo apt install upx
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd alist
|
||||
mv alist/build.sh .
|
||||
bash build.sh cdn
|
||||
mv dist/* alist/public
|
||||
bash build.sh release
|
||||
cd ..
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
|
||||
45
.github/workflows/release_docker.yml
vendored
Normal file
45
.github/workflows/release_docker.yml
vendored
Normal 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
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -21,7 +21,9 @@ dist/
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
bin/*
|
||||
alist
|
||||
/alist
|
||||
*.json
|
||||
public/index.html
|
||||
public/assets/
|
||||
public/*.html
|
||||
public/assets/
|
||||
public/public/
|
||||
data/
|
||||
105
CONTRIBUTING.md
Normal file
105
CONTRIBUTING.md
Normal 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.17+](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.
|
||||
27
CONTRIBUTORS.md
Normal file
27
CONTRIBUTORS.md
Normal file
@@ -0,0 +1,27 @@
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#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>
|
||||
</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!
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM alpine:edge as builder
|
||||
LABEL stage=go-builder
|
||||
WORKDIR /app/
|
||||
COPY ./ ./
|
||||
RUN apk add --no-cache bash git go gcc musl-dev; \
|
||||
bash build.sh docker
|
||||
|
||||
FROM alpine:edge
|
||||
LABEL MAINTAINER="i@nn.ci"
|
||||
VOLUME /opt/alist/data/
|
||||
WORKDIR /opt/alist/
|
||||
COPY --from=builder /app/bin/alist ./
|
||||
EXPOSE 5244
|
||||
CMD [ "./alist" ]
|
||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
81
README.md
Normal file → Executable file
81
README.md
Normal file → Executable file
@@ -1,43 +1,78 @@
|
||||
<h2 align="center">Alist</h2>
|
||||
<p align="center">
|
||||
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/release/Xhofe/alist?style=flat-square" alt="Release version"></a>
|
||||
<div align="center">
|
||||
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||
<p><em>🗂️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>
|
||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build?style=flat-square" alt="Build status"></a>
|
||||
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/downloads/Xhofe/alist/total?style=flat-square" alt="Downloads"></a>
|
||||
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Xhofe/alist?style=flat-square" alt="License"></a>
|
||||
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/downloads/Xhofe/alist/total?style=flat-square&color=%239F7AEA" alt="Downloads"></a>
|
||||
<a href="https://github.com/Xhofe/alist/blob/v2/LICENSE"><img src="https://img.shields.io/github/license/Xhofe/alist?style=flat-square" alt="License"></a>
|
||||
<a href="https://pay.xhofe.top">
|
||||
<img src="https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat-square" alt="donate">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
### 这是什么?
|
||||
English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md)
|
||||
|
||||
一款支持多种存储的目录文件列表程序,后端基于`go-fiber`,前端使用`react`。
|
||||
## Features
|
||||
|
||||
### 前端项目地址
|
||||
- [x] Multiple storage
|
||||
- [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] [GoogleDrive](https://drive.google.com/)
|
||||
- [x] [123pan](https://www.123pan.com/)
|
||||
- [x] [Lanzou](https://pc.woozooo.com/)
|
||||
- [x] [Alist](https://github.com/Xhofe/alist)
|
||||
- [x] FTP
|
||||
- [x] [PikPak](https://www.mypikpak.com/)
|
||||
- [x] [ShandianPan](https://shandianpan.com/)
|
||||
- [x] [S3](https://aws.amazon.com/s3/)
|
||||
- [x] WebDav
|
||||
- [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] Easy to deploy and out-of-the-box
|
||||
- [x] File preview (PDF, markdown, code, plain text, ...)
|
||||
- [x] Image preview in gallery mode
|
||||
- [x] Video and audio preview (mp4, mp3, ...)
|
||||
- [x] Office documents preview (docx, pptx, xlsx, ...)
|
||||
- [x] `README.md` preview rendering
|
||||
- [x] File permalink copy and direct file download
|
||||
- [x] Dark mode
|
||||
- [x] I18n
|
||||
- [x] Protected routes (password protection and authentication)
|
||||
- [x] WebDav (see https://alist-doc.nn.ci/en/docs/webdav for details)
|
||||
- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist)
|
||||
- [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), delete, mkdir, rename, move and copy
|
||||
|
||||
- https://github.com/Xhofe/alist-web
|
||||
## Discussion
|
||||
|
||||
### 演示地址
|
||||
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports and feature request only.**
|
||||
|
||||
- https://alist.nn.ci
|
||||
## Demo
|
||||
|
||||
### 预览
|
||||
Available at: <https://alist.nn.ci>.
|
||||
|
||||
<a href="https://alist.nn.ci/"><img src="https://store.heytapimage.com/cdo-portal/feedback/202111/03/695ef77854a144e928518efde38db97a.png"></a>
|
||||

|
||||
|
||||
### 支持的存储
|
||||
## Document
|
||||
|
||||
- 本地存储
|
||||
- 阿里云盘
|
||||
- Onedrive
|
||||
- ...
|
||||
<https://alist-doc.nn.ci/en/>
|
||||
|
||||
### 如何使用
|
||||
## License
|
||||
|
||||
- https://www.nn.ci/archives/alist.html
|
||||
The `AList` is open-source software licensed under the AGPL-3.0 license.
|
||||
|
||||
### License
|
||||
---
|
||||
|
||||
The `AList` is open-source software licensed under the MIT license.
|
||||
> [@Blog](https://www.nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@QQGroup](https://jq.qq.com/?_wv=1027&k=OVPJcv2b)
|
||||
78
README_cn.md
Normal file
78
README_cn.md
Normal file
@@ -0,0 +1,78 @@
|
||||
<div align="center">
|
||||
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||
<p><em>🗂️一个支持多存储的文件列表程序,使用 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>
|
||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build?style=flat-square" alt="Build status"></a>
|
||||
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/downloads/Xhofe/alist/total?style=flat-square&color=%239F7AEA" alt="Downloads"></a>
|
||||
<a href="https://github.com/Xhofe/alist/blob/v2/LICENSE"><img src="https://img.shields.io/github/license/Xhofe/alist?style=flat-square" alt="License"></a>
|
||||
<a href="https://pay.xhofe.top">
|
||||
<img src="https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat-square" alt="donate">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
[English](./README.md) | 中文 | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md)
|
||||
|
||||
## 支持
|
||||
|
||||
- [x] 多种存储
|
||||
- [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] [GoogleDrive](https://drive.google.com/)
|
||||
- [x] [123云盘](https://www.123pan.com/)
|
||||
- [x] [蓝奏云](https://pc.woozooo.com/)
|
||||
- [x] [Alist](https://github.com/Xhofe/alist)
|
||||
- [x] FTP
|
||||
- [x] [PikPak](https://www.mypikpak.com/)
|
||||
- [x] [闪电盘](https://shandianpan.com/)
|
||||
- [x] [S3](https://aws.amazon.com/cn/s3/)
|
||||
- [x] WebDav
|
||||
- [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] 部署方便,开箱即用
|
||||
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
||||
- [x] 画廊模式下的图像预览
|
||||
- [x] 视频和音频预览(mp4、mp3 等)
|
||||
- [x] Office 文档预览(docx、pptx、xlsx、...)
|
||||
- [x] `README.md` 预览渲染
|
||||
- [x] 文件永久链接复制和直接文件下载
|
||||
- [x] 黑暗模式
|
||||
- [x] 国际化
|
||||
- [x] 受保护的路由(密码保护和身份验证)
|
||||
- [x] WebDav(具体见https://alist-doc.nn.ci/docs/webdav )
|
||||
- [x] [Docker 部署](https://hub.docker.com/r/xhofe/alist)
|
||||
- [x] Cloudflare workers 中转
|
||||
- [x] 文件/文件夹打包下载
|
||||
- [x] 支持视频列表播放和字幕(ass,srt,vtt)
|
||||
- [x] 网页上传(可以允许访客上传),删除,新建文件夹,重命名,移动,复制
|
||||
|
||||
## 讨论
|
||||
|
||||
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) ,**issue仅针对错误报告和功能请求。**
|
||||
|
||||
## 演示
|
||||
|
||||
<https://alist.nn.ci>。
|
||||
|
||||

|
||||
|
||||
## 文档
|
||||
|
||||
<https://alist-doc.nn.ci/>
|
||||
|
||||
## 许可
|
||||
|
||||
`AList` 是在 AGPL-3.0 许可下许可的开源软件。
|
||||
|
||||
---
|
||||
|
||||
> [@Blog](https://www.nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@QQGroup](https://jq.qq.com/?_wv=1027&k=OVPJcv2b)
|
||||
60
alist.go
60
alist.go
@@ -1,47 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/bootstrap"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/public"
|
||||
_ "github.com/Xhofe/alist/drivers"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/server"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&conf.ConfigFile, "conf", "config.json", "config file")
|
||||
flag.BoolVar(&conf.Debug, "debug", false, "start with debug mode")
|
||||
flag.BoolVar(&conf.Version, "version", false, "print version info")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func Init() {
|
||||
bootstrap.InitLog()
|
||||
func Init() bool {
|
||||
//bootstrap.InitLog()
|
||||
bootstrap.InitConf()
|
||||
bootstrap.InitCron()
|
||||
bootstrap.InitModel()
|
||||
if conf.Password {
|
||||
pass, err := model.GetSettingByKey("password")
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
return false
|
||||
}
|
||||
fmt.Printf("your password: %s\n", pass.Value)
|
||||
return false
|
||||
}
|
||||
server.InitIndex()
|
||||
bootstrap.InitSettings()
|
||||
bootstrap.InitAccounts()
|
||||
bootstrap.InitCache()
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Init()
|
||||
app := fiber.New()
|
||||
server.InitApiRouter(app)
|
||||
app.Use("/", filesystem.New(filesystem.Config{
|
||||
Root: http.FS(public.Public),
|
||||
NotFoundFile: "index.html",
|
||||
}))
|
||||
log.Info("starting server")
|
||||
err := app.Listen(fmt.Sprintf(":%d", conf.Conf.Port))
|
||||
if !Init() {
|
||||
return
|
||||
}
|
||||
if !conf.Debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
r := gin.Default()
|
||||
server.InitApiRouter(r)
|
||||
base := fmt.Sprintf("%s:%d", conf.Conf.Address, conf.Conf.Port)
|
||||
log.Infof("start server @ %s", base)
|
||||
var err error
|
||||
if conf.Conf.Scheme.Https {
|
||||
err = r.RunTLS(base, conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||
} else {
|
||||
err = r.Run(base)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("failed to start: %s", err.Error())
|
||||
}
|
||||
|
||||
31
bootstrap/account.go
Normal file
31
bootstrap/account.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func InitAccounts() {
|
||||
log.Infof("init accounts...")
|
||||
var accounts []model.Account
|
||||
if err := conf.DB.Find(&accounts).Error; err != nil {
|
||||
log.Fatalf("failed sync init accounts")
|
||||
}
|
||||
for i, account := range accounts {
|
||||
model.RegisterAccount(account)
|
||||
driver, ok := base.GetDriver(account.Type)
|
||||
if !ok {
|
||||
log.Errorf("no [%s] driver", account.Type)
|
||||
} else {
|
||||
log.Infof("start init account: [%s], type: [%s]", account.Name, account.Type)
|
||||
err := driver.Save(&accounts[i], nil)
|
||||
if err != nil {
|
||||
log.Errorf("init account [%s] error:[%s]", account.Name, err.Error())
|
||||
} else {
|
||||
log.Infof("success init account: %s, type: %s", account.Name, account.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,11 @@ import (
|
||||
// InitCache init cache
|
||||
func InitCache() {
|
||||
log.Infof("init cache...")
|
||||
goCacheClient := goCache.New(60*time.Minute, 120*time.Minute)
|
||||
c := conf.Conf.Cache
|
||||
if c.Expiration == 0 {
|
||||
c.Expiration, c.CleanupInterval = 60, 120
|
||||
}
|
||||
goCacheClient := goCache.New(time.Duration(c.Expiration)*time.Minute, time.Duration(c.CleanupInterval)*time.Minute)
|
||||
goCacheStore := store.NewGoCache(goCacheClient, nil)
|
||||
conf.Cache = cache.New(goCacheStore)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// InitConf init config
|
||||
@@ -13,20 +13,37 @@ func InitConf() {
|
||||
log.Infof("reading config file: %s", conf.ConfigFile)
|
||||
if !utils.Exists(conf.ConfigFile) {
|
||||
log.Infof("config file not exists, creating default config file")
|
||||
_, err := utils.CreatNestedFile(conf.ConfigFile)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create config file")
|
||||
}
|
||||
conf.Conf = conf.DefaultConfig()
|
||||
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 = 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.MarshalIndent(conf.Conf, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("marshal config error:%s", err.Error())
|
||||
}
|
||||
err = ioutil.WriteFile(conf.ConfigFile, confBody, 0777)
|
||||
if err != nil {
|
||||
log.Fatalf("update config struct error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
config, err := ioutil.ReadFile(conf.ConfigFile)
|
||||
err := os.MkdirAll(conf.Conf.TempDir, 0700)
|
||||
if err != nil {
|
||||
log.Fatalf("reading config file error:%s", err.Error())
|
||||
log.Fatalf("create temp dir error: %s", err.Error())
|
||||
}
|
||||
conf.Conf = new(conf.Config)
|
||||
err = json.Unmarshal(config, conf.Conf)
|
||||
if err != nil {
|
||||
log.Fatalf("load config error: %s", err.Error())
|
||||
}
|
||||
log.Debugf("config:%+v", conf.Conf)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// initLog init log
|
||||
// InitLog init log
|
||||
func InitLog() {
|
||||
if conf.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetReportCaller(true)
|
||||
}
|
||||
if conf.Password || conf.Version {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
//DisableColors: true,
|
||||
ForceColors: true,
|
||||
@@ -18,4 +22,14 @@ func InitLog() {
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
FullTimestamp: true,
|
||||
})
|
||||
}
|
||||
log.Infof("init log...")
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&conf.ConfigFile, "conf", "data/config.json", "config file")
|
||||
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.Parse()
|
||||
InitLog()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package bootstrap
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers"
|
||||
"github.com/Xhofe/alist/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/driver/mysql"
|
||||
@@ -20,6 +19,7 @@ import (
|
||||
|
||||
func InitModel() {
|
||||
log.Infof("init model...")
|
||||
var err error
|
||||
databaseConfig := conf.Conf.Database
|
||||
newLogger := logger.New(
|
||||
log2.New(os.Stdout, "\r\n", log2.LstdFlags),
|
||||
@@ -60,129 +60,25 @@ 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)
|
||||
}
|
||||
log.Infof("auto migrate model")
|
||||
err := conf.DB.AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{})
|
||||
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{})
|
||||
} else {
|
||||
err = conf.DB.AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{})
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed to auto migrate")
|
||||
}
|
||||
|
||||
// TODO init filetype
|
||||
initAccounts()
|
||||
initSettings()
|
||||
}
|
||||
|
||||
func initAccounts() {
|
||||
log.Infof("init accounts...")
|
||||
var accounts []model.Account
|
||||
if err := conf.DB.Find(&accounts).Error; err != nil {
|
||||
log.Fatalf("failed sync init accounts")
|
||||
}
|
||||
for _, account := range accounts {
|
||||
model.RegisterAccount(account)
|
||||
driver, ok := drivers.GetDriver(account.Type)
|
||||
if !ok {
|
||||
log.Errorf("no [%s] driver", driver)
|
||||
} else {
|
||||
err := driver.Save(&account, nil)
|
||||
if err != nil {
|
||||
log.Errorf("init account [%s] error:[%s]", account.Name, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initSettings() {
|
||||
log.Infof("init settings...")
|
||||
version := model.SettingItem{
|
||||
Key: "version",
|
||||
Value: conf.GitTag,
|
||||
Description: "version",
|
||||
Group: model.CONST,
|
||||
}
|
||||
|
||||
_ = model.SaveSetting(version)
|
||||
|
||||
settings := []model.SettingItem{
|
||||
{
|
||||
Key: "title",
|
||||
Value: "Alist",
|
||||
Description: "title",
|
||||
Group: model.PUBLIC,
|
||||
},
|
||||
{
|
||||
Key: "password",
|
||||
Value: "alist",
|
||||
Description: "password",
|
||||
Group: model.PRIVATE,
|
||||
},
|
||||
{
|
||||
Key: "logo",
|
||||
Value: "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png",
|
||||
Description: "logo",
|
||||
Group: model.PUBLIC,
|
||||
},
|
||||
{
|
||||
Key: "favicon",
|
||||
Value: "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png",
|
||||
Description: "favicon",
|
||||
Group: model.PUBLIC,
|
||||
},
|
||||
{
|
||||
Key: "icon color",
|
||||
Value: "teal.300",
|
||||
Description: "icon's color",
|
||||
Group: model.PUBLIC,
|
||||
},
|
||||
{
|
||||
Key: "text types",
|
||||
Value: "txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,go,sh,c,cpp,h,hpp",
|
||||
Description: "text type extensions",
|
||||
},
|
||||
{
|
||||
Key: "readme file",
|
||||
Value: "hide",
|
||||
Description: "hide readme file? (show/hide)",
|
||||
},
|
||||
{
|
||||
Key: "music cover",
|
||||
Value: "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png",
|
||||
Description: "music cover image",
|
||||
Group: model.PUBLIC,
|
||||
},
|
||||
{
|
||||
Key: "site beian",
|
||||
Description: "chinese beian info",
|
||||
Group: model.PUBLIC,
|
||||
},
|
||||
{
|
||||
Key: "home readme url",
|
||||
Description: "when have multiple, the readme file to show",
|
||||
Group: model.PUBLIC,
|
||||
},
|
||||
}
|
||||
for _, v := range settings {
|
||||
_, err := model.GetSettingByKey(v.Key)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
err = model.SaveSetting(v)
|
||||
if err != nil {
|
||||
log.Fatalf("failed write setting: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
textTypes, err := model.GetSettingByKey("text types")
|
||||
if err == nil {
|
||||
conf.TextTypes = strings.Split(textTypes.Value, ",")
|
||||
log.Fatalf("failed to auto migrate: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
267
bootstrap/setting.go
Normal file
267
bootstrap/setting.go
Normal file
@@ -0,0 +1,267 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func InitSettings() {
|
||||
log.Infof("init settings...")
|
||||
|
||||
err := model.SaveSetting(model.Version)
|
||||
if err != nil {
|
||||
log.Fatalf("failed write setting: %s", err.Error())
|
||||
}
|
||||
|
||||
settings := []model.SettingItem{
|
||||
{
|
||||
Key: "title",
|
||||
Value: "Alist",
|
||||
Description: "title",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "password",
|
||||
Value: utils.RandomStr(8),
|
||||
Description: "password",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "logo",
|
||||
Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/can_circle.svg",
|
||||
Description: "logo",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "favicon",
|
||||
Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg",
|
||||
Description: "favicon",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "icon color",
|
||||
Value: "#1890ff",
|
||||
Description: "icon's color",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "text types",
|
||||
Value: strings.Join(conf.TextTypes, ","),
|
||||
Type: "string",
|
||||
Description: "text 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://cdn.jsdelivr.net/gh/alist-org/logo@main/circle_center.svg",
|
||||
Description: "music cover image",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "site beian",
|
||||
Description: "chinese beian info",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "home readme url",
|
||||
Description: "when have multiple, the readme file to show",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "autoplay video",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "autoplay audio",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "check parent folder",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Description: "check parent folder password",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "customize head",
|
||||
Value: "",
|
||||
Type: "text",
|
||||
Description: "Customize head, placed at the beginning of the head",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "customize body",
|
||||
Value: "",
|
||||
Type: "text",
|
||||
Description: "Customize script, placed at the end of the body",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "home emoji",
|
||||
Value: "🏠",
|
||||
Type: "string",
|
||||
Description: "emoji in front of home in nav",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "animation",
|
||||
Value: "true",
|
||||
Type: "bool",
|
||||
Description: "when there are a lot of files, the animation will freeze when opening",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "check down link",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Description: "check down link password, your link will be 'https://alist.com/d/filename?pw=xxx'",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "WebDAV username",
|
||||
Value: "admin",
|
||||
Description: "WebDAV username",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "WebDAV password",
|
||||
Value: utils.RandomStr(8),
|
||||
Description: "WebDAV password",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "artplayer whitelist",
|
||||
Value: "*",
|
||||
Description: "refer to https://artplayer.org/document/options#whitelist",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "artplayer autoSize",
|
||||
Value: "true",
|
||||
Description: "refer to https://artplayer.org/document/options#autosize",
|
||||
Type: "bool",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "Visitor WebDAV username",
|
||||
Value: "guest",
|
||||
Description: "Visitor WebDAV username",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "Visitor WebDAV password",
|
||||
Value: "guest",
|
||||
Description: "Visitor WebDAV password",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "load type",
|
||||
Value: "all",
|
||||
Type: "select",
|
||||
Values: "all,load more,auto load more,pagination",
|
||||
Description: "Not recommended to choose to auto load more, it has bugs now",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "default page size",
|
||||
Value: "30",
|
||||
Type: "number",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "ocr api",
|
||||
Value: "https://api.xhofe.top/ocr/file/json",
|
||||
Description: "Used to identify verification codes",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
}
|
||||
for i, _ := range settings {
|
||||
v := settings[i]
|
||||
v.Version = conf.GitTag
|
||||
o, err := model.GetSettingByKey(v.Key)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
err = model.SaveSetting(v)
|
||||
if v.Key == "password" {
|
||||
log.Infof("Initial password: %s", v.Value)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed write setting: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Fatalf("can't get setting: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
//o.Version = conf.GitTag
|
||||
//err = model.SaveSetting(*o)
|
||||
v.Value = o.Value
|
||||
err = model.SaveSetting(v)
|
||||
if err != nil {
|
||||
log.Fatalf("failed write setting: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
model.LoadSettings()
|
||||
}
|
||||
179
build.sh
179
build.sh
@@ -1,53 +1,156 @@
|
||||
#!/bin/bash
|
||||
|
||||
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)
|
||||
# 构建前端,在当前目录产生一个dist文件夹
|
||||
BUILD_WEB() {
|
||||
git clone https://github.com/alist-org/alist-web.git
|
||||
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 .. || exit
|
||||
rm -rf alist-web
|
||||
}
|
||||
|
||||
if [ "$1" == "release" ]; then
|
||||
gitTag=$(git describe --abbrev=0 --tags)
|
||||
else
|
||||
gitTag=$(git describe --abbrev=0 --tags)-next
|
||||
fi
|
||||
CDN_WEB() {
|
||||
curl -L https://github.com/alist-org/alist-web/releases/latest/download/dist.tar.gz -o dist.tar.gz
|
||||
tar -zxvf dist.tar.gz
|
||||
rm -f dist.tar.gz
|
||||
}
|
||||
|
||||
ldflags="\
|
||||
# 在DOCKER中构建
|
||||
BUILD_DOCKER() {
|
||||
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 \
|
||||
-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' \
|
||||
"
|
||||
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter alist.go
|
||||
}
|
||||
|
||||
BUILD() {
|
||||
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')
|
||||
echo "build version: $gitTag"
|
||||
ldflags="\
|
||||
-w -s \
|
||||
-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' \
|
||||
"
|
||||
|
||||
cp -R ../alist-web/dist/* public
|
||||
if [ "$1" == "release" ]; then
|
||||
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
||||
else
|
||||
xgo -targets=linux/amd64,windows/amd64,darwin/amd64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
||||
fi
|
||||
mkdir -p "build"
|
||||
mv alist-* build
|
||||
if [ "$1" != "release" ]; then
|
||||
cd build
|
||||
upx -9 ./alist-linux*
|
||||
upx -9 ./alist-windows*
|
||||
find . -type f -print0 | xargs -0 md5sum >md5.txt
|
||||
cat md5.txt
|
||||
cd .. || exit
|
||||
fi
|
||||
cd .. || exit
|
||||
}
|
||||
|
||||
if [ "$1" == "release" ]; then
|
||||
xgo -out alist -ldflags="$ldflags" .
|
||||
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 \
|
||||
-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
|
||||
tar -czvf compress/"$i".tar.gz "$i"
|
||||
done
|
||||
for i in $(find . -type f -name "$appName-darwin-*"); do
|
||||
tar -czvf compress/"$i".tar.gz "$i"
|
||||
done
|
||||
for i in $(find . -type f -name "$appName-windows-*"); do
|
||||
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip "$i"
|
||||
done
|
||||
cd ../.. || exit
|
||||
}
|
||||
|
||||
if [ "$1" = "web" ]; then
|
||||
BUILD_WEB
|
||||
elif [ "$1" = "cdn" ]; then
|
||||
CDN_WEB
|
||||
elif [ "$1" = "docker" ]; then
|
||||
BUILD_DOCKER
|
||||
elif [ "$1" = "build" ]; then
|
||||
BUILD build
|
||||
elif [ "$1" = "release" ]; then
|
||||
BUILD release
|
||||
BUILD_MUSL
|
||||
RELEASE
|
||||
else
|
||||
xgo -targets=linux/amd64,windows/amd64,darwin/amd64 -out alist -ldflags="$ldflags" .
|
||||
fi
|
||||
mkdir "build"
|
||||
mv alist-* build
|
||||
cd build || exit
|
||||
upx -9 ./*
|
||||
find . -type f -print0 | xargs -0 md5sum > md5.txt
|
||||
|
||||
# compress file (release)
|
||||
if [ "$1" == "release" ]; then
|
||||
mkdir compress
|
||||
mv md5.txt compress
|
||||
for i in `find . -type f -name "$appName-linux-*"`
|
||||
do
|
||||
tar -czvf compress/"$i".tar.gz "$i"
|
||||
done
|
||||
for i in `find . -type f -name "$appName-darwin-*"`
|
||||
do
|
||||
tar -czvf compress/"$i".tar.gz "$i"
|
||||
done
|
||||
for i in `find . -type f -name "$appName-windows-*"`
|
||||
do
|
||||
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip "$i"
|
||||
done
|
||||
echo -e "${RED_COLOR} Parameter error ${RES}"
|
||||
fi
|
||||
@@ -9,26 +9,46 @@ type Database struct {
|
||||
Name string `json:"name"`
|
||||
TablePrefix string `json:"table_prefix"`
|
||||
DBFile string `json:"db_file"`
|
||||
SslMode string `json:"ssl_mode"`
|
||||
}
|
||||
|
||||
type Scheme struct {
|
||||
Https bool `json:"https"`
|
||||
CertFile string `json:"cert_file"`
|
||||
KeyFile string `json:"key_file"`
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Expiration int64 `json:"expiration"`
|
||||
CleanupInterval int64 `json:"cleanup_interval"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Database Database `json:"database"`
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Assets string `json:"assets"`
|
||||
Database Database `json:"database"`
|
||||
Scheme Scheme `json:"scheme"`
|
||||
Cache CacheConfig `json:"cache"`
|
||||
TempDir string `json:"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",
|
||||
User: "",
|
||||
Password: "",
|
||||
Host: "",
|
||||
Port: 0,
|
||||
Name: "",
|
||||
TablePrefix: "x_",
|
||||
DBFile: "data.db",
|
||||
DBFile: "data/data.db",
|
||||
SslMode: "disable",
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
Expiration: 60,
|
||||
CleanupInterval: 120,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
73
conf/var.go
73
conf/var.go
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/eko/gocache/v2/cache"
|
||||
"github.com/robfig/cron/v3"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -12,7 +13,8 @@ var (
|
||||
GoVersion string
|
||||
GitAuthor string
|
||||
GitCommit string
|
||||
GitTag string
|
||||
GitTag string = "dev"
|
||||
WebTag string
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -20,6 +22,7 @@ var (
|
||||
Conf *Config
|
||||
Debug bool
|
||||
Version bool
|
||||
Password bool
|
||||
|
||||
DB *gorm.DB
|
||||
Cache *cache.Cache
|
||||
@@ -28,9 +31,69 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
TextTypes = []string{"txt", "go", "md"}
|
||||
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"}
|
||||
AudioTypes = []string{"mp3", "flac", "ogg"}
|
||||
ImageTypes = []string{"jpg", "tiff", "jpeg", "png", "gif", "bmp", "svg"}
|
||||
VideoTypes = []string{"mp4", "mkv", "avi", "mov", "rmvb", "webm", "flv"}
|
||||
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)
|
||||
|
||||
func Set(key string, value string) {
|
||||
settingsMap[key] = value
|
||||
}
|
||||
|
||||
func GetStr(key string) string {
|
||||
value, ok := settingsMap[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func GetBool(key string) bool {
|
||||
value, ok := settingsMap[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return value == "true"
|
||||
}
|
||||
|
||||
func GetInt(key string, defaultV int) int {
|
||||
value, ok := settingsMap[key]
|
||||
if !ok {
|
||||
return defaultV
|
||||
}
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return defaultV
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
var (
|
||||
LoadSettings = []string{
|
||||
"check parent folder", "check down link", "WebDAV username", "WebDAV password",
|
||||
"Visitor WebDAV username", "Visitor WebDAV password",
|
||||
"default page size", "load type",
|
||||
"ocr api", "favicon",
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
RawIndexHtml string
|
||||
ManageHtml string
|
||||
IndexHtml string
|
||||
Token string
|
||||
|
||||
//CheckParent bool
|
||||
//CheckDown bool
|
||||
//DavUsername string
|
||||
//DavPassword string
|
||||
//VisitorDavUsername string
|
||||
//VisitorDavPassword string
|
||||
)
|
||||
|
||||
230
drivers/123/123.go
Normal file
230
drivers/123/123.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package _23
|
||||
|
||||
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"
|
||||
"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 != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
var resp Pan123TokenResp
|
||||
_, err := base.RestyClient.R().
|
||||
SetResult(&resp).
|
||||
SetBody(base.Json{
|
||||
"passport": account.Username,
|
||||
"password": account.Password,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
err = fmt.Errorf(resp.Message)
|
||||
account.Status = resp.Message
|
||||
} else {
|
||||
account.Status = "work"
|
||||
account.AccessToken = resp.Data.Token
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) FormatFile(file *Pan123File) *model.File {
|
||||
f := &model.File{
|
||||
Id: strconv.FormatInt(file.FileId, 10),
|
||||
Name: file.FileName,
|
||||
Size: file.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: file.UpdateAt,
|
||||
}
|
||||
if file.Type == 1 {
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
f.Type = utils.GetFileType(filepath.Ext(file.FileName))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (driver Pan123) GetFiles(parentId string, account *model.Account) ([]Pan123File, error) {
|
||||
next := "0"
|
||||
res := make([]Pan123File, 0)
|
||||
for next != "-1" {
|
||||
var resp Pan123Files
|
||||
query := map[string]string{
|
||||
"driveId": "0",
|
||||
"limit": "100",
|
||||
"next": next,
|
||||
"orderBy": account.OrderBy,
|
||||
"orderDirection": account.OrderDirection,
|
||||
"parentFileId": parentId,
|
||||
"trashed": "false",
|
||||
}
|
||||
_, err := driver.Request("https://www.123pan.com/api/file/list",
|
||||
base.Get, nil, query, nil, &resp, false, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next = resp.Data.Next
|
||||
res = append(res, resp.Data.InfoList...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver Pan123) Request(url string, method int, headers, query map[string]string, data *base.Json, resp interface{}, proxy bool, account *model.Account) ([]byte, error) {
|
||||
rawUrl := url
|
||||
if account.APIProxyUrl != "" && proxy {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+account.AccessToken)
|
||||
if headers != nil {
|
||||
req.SetHeaders(headers)
|
||||
}
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
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(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
body := res.Body()
|
||||
code := jsoniter.Get(body, "code").ToInt()
|
||||
if code != 0 {
|
||||
if code == 401 {
|
||||
err := driver.Login(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return driver.Request(rawUrl, method, headers, query, data, resp, proxy, account)
|
||||
}
|
||||
return nil, errors.New(jsoniter.Get(body, "message").ToString())
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
//func (driver Pan123) Post(url string, data base.Json, account *model.Account) ([]byte, error) {
|
||||
// res, err := pan123Client.R().
|
||||
// SetHeader("authorization", "Bearer "+account.AccessToken).
|
||||
// SetBody(data).Post(url)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// body := res.Body()
|
||||
// if jsoniter.Get(body, "code").ToInt() != 0 {
|
||||
// return nil, errors.New(jsoniter.Get(body, "message").ToString())
|
||||
// }
|
||||
// return body, nil
|
||||
//}
|
||||
|
||||
func (driver Pan123) GetFile(path string, account *model.Account) (*Pan123File, error) {
|
||||
dir, name := filepath.Split(path)
|
||||
dir = utils.ParsePath(dir)
|
||||
_, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentFiles_, _ := base.GetCache(dir, account)
|
||||
parentFiles, _ := parentFiles_.([]Pan123File)
|
||||
for _, file := range parentFiles {
|
||||
if file.FileName == name {
|
||||
if file.Type != conf.FOLDER {
|
||||
return &file, err
|
||||
} else {
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
//func HMAC(message string, secret string) string {
|
||||
// key := []byte(secret)
|
||||
// h := hmac.New(sha256.New, key)
|
||||
// h.Write([]byte(message))
|
||||
// // fmt.Println(h.Sum(nil))
|
||||
// //sha := hex.EncodeToString(h.Sum(nil))
|
||||
// // fmt.Println(sha)
|
||||
// //return sha
|
||||
// return string(h.Sum(nil))
|
||||
//}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Pan123{})
|
||||
}
|
||||
439
drivers/123/driver.go
Normal file
439
drivers/123/driver.go
Normal file
@@ -0,0 +1,439 @@
|
||||
package _23
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"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"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Pan123 struct{}
|
||||
|
||||
func (driver Pan123) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "123Pan",
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Pan123) 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,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "name,fileId,updateAt,createAt",
|
||||
Required: true,
|
||||
Default: "name",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
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"
|
||||
}
|
||||
err := driver.Login(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) 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 Pan123) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var rawFiles []Pan123File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
rawFiles, _ = cache.([]Pan123File)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawFiles, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawFiles) > 0 {
|
||||
_ = base.SetCache(path, rawFiles, account)
|
||||
}
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Pan123) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
log.Debugf("%+v", args)
|
||||
file, err := driver.GetFile(utils.ParsePath(args.Path), account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp Pan123DownResp
|
||||
var headers map[string]string
|
||||
if args.IP != "" && args.IP != "::1" {
|
||||
headers = map[string]string{
|
||||
//"X-Real-IP": "1.1.1.1",
|
||||
"X-Forwarded-For": args.IP,
|
||||
}
|
||||
}
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"etag": file.Etag,
|
||||
"fileId": file.FileId,
|
||||
"fileName": file.FileName,
|
||||
"s3keyFlag": file.S3KeyFlag,
|
||||
"size": file.Size,
|
||||
"type": file.Type,
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/download_info",
|
||||
base.Post, headers, nil, &data, &resp, false, account)
|
||||
//_, err = pan123Client.R().SetResult(&resp).SetHeader("authorization", "Bearer "+account.AccessToken).
|
||||
// SetBody().Post("https://www.123pan.com/api/file/download_info")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := url.Parse(resp.Data.DownloadUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u_ := fmt.Sprintf("https://%s%s", u.Host, u.Path)
|
||||
res, err := base.NoRedirectClient.R().SetQueryParamsFromValues(u.Query()).Get(u_)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
link := base.Link{
|
||||
Url: resp.Data.DownloadUrl,
|
||||
}
|
||||
if res.StatusCode() == 302 {
|
||||
link.Url = res.Header().Get("location")
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Pan123) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("pan123 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 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
|
||||
}
|
||||
|
||||
func (driver Pan123) 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
|
||||
}
|
||||
parentFileId, _ := strconv.Atoi(parentFile.Id)
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"etag": "",
|
||||
"fileName": name,
|
||||
"parentFileId": parentFileId,
|
||||
"size": 0,
|
||||
"type": 1,
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/upload_request",
|
||||
base.Post, nil, nil, &data, nil, false, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) Move(src string, dst string, account *model.Account) error {
|
||||
dstDir, _ := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileId, _ := strconv.Atoi(srcFile.Id)
|
||||
|
||||
dstDirFile, err := driver.File(dstDir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parentFileId, _ := strconv.Atoi(dstDirFile.Id)
|
||||
data := base.Json{
|
||||
"fileIdList": []base.Json{{"FileId": fileId}},
|
||||
"parentFileId": parentFileId,
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/mod_pid",
|
||||
base.Post, nil, nil, &data, nil, false, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) Rename(src string, dst string, account *model.Account) error {
|
||||
_, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileId, _ := strconv.Atoi(srcFile.Id)
|
||||
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"fileId": fileId,
|
||||
"fileName": dstName,
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/rename",
|
||||
base.Post, nil, nil, &data, nil, false, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Pan123) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.GetFile(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"operation": true,
|
||||
"fileTrashInfoList": file,
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/trash",
|
||||
base.Post, nil, nil, &data, nil, false, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) 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
|
||||
}
|
||||
parentFileId, _ := strconv.Atoi(parentFile.Id)
|
||||
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
|
||||
}
|
||||
h := md5.New()
|
||||
_, err = io.Copy(h, tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
etag := hex.EncodeToString(h.Sum(nil))
|
||||
log.Debugln("md5:", etag)
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"duplicate": true,
|
||||
"etag": etag,
|
||||
"fileName": file.GetFileName(),
|
||||
"parentFileId": parentFileId,
|
||||
"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)
|
||||
//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"),
|
||||
Endpoint: aws.String("file.123pan.com"),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
}
|
||||
s, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: &resp.Data.Bucket,
|
||||
Key: &resp.Data.Key,
|
||||
Body: tempFile,
|
||||
}
|
||||
_, err = uploader.Upload(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/upload_complete", base.Post, nil, nil, &base.Json{
|
||||
"fileId": resp.Data.FileId,
|
||||
}, nil, false, account)
|
||||
return err
|
||||
}
|
||||
|
||||
//type UploadResp struct {
|
||||
// XMLName xml.Name `xml:"InitiateMultipartUploadResult"`
|
||||
// Bucket string `xml:"Bucket"`
|
||||
// Key string `xml:"Key"`
|
||||
// UploadId string `xml:"UploadId"`
|
||||
//}
|
||||
|
||||
// TODO unfinished
|
||||
//func (driver Pan123) Upload(file *model.FileStream, account *model.Account) error {
|
||||
// return base.ErrNotImplement
|
||||
// parentFile, err := driver.File(file.ParentPath, account)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if !parentFile.IsDir() {
|
||||
// return base.ErrNotFolder
|
||||
// }
|
||||
// parentFileId, _ := strconv.Atoi(parentFile.Id)
|
||||
// data := base.Json{
|
||||
// "driveId": 0,
|
||||
// "duplicate": true,
|
||||
// "etag": RandStr(32), //maybe file's md5
|
||||
// "fileName": file.GetFileName(),
|
||||
// "parentFileId": parentFileId,
|
||||
// "size": file.GetSize(),
|
||||
// "type": 0,
|
||||
// }
|
||||
// res, err := driver.Request("https://www.123pan.com/api/file/upload_request",
|
||||
// base.Post, nil, nil, &data, nil, false, account)
|
||||
// //res, err := driver.Post("https://www.123pan.com/api/file/upload_request", data, account)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// baseUrl := fmt.Sprintf("https://file.123pan.com/%s/%s", jsoniter.Get(res, "data.Bucket").ToString(), jsoniter.Get(res, "data.Key").ToString())
|
||||
// var resp UploadResp
|
||||
// kSecret := jsoniter.Get(res, "data.SecretAccessKey").ToString()
|
||||
// nowTimeStr := time.Now().String()
|
||||
// Date := strings.ReplaceAll(strings.Split(nowTimeStr, "T")[0], "-", "")
|
||||
//
|
||||
// StringToSign := fmt.Sprintf("%s\n%s\n%s\n%s",
|
||||
// "AWS4-HMAC-SHA256",
|
||||
// nowTimeStr,
|
||||
// fmt.Sprintf("%s/us-east-1/s3/aws4_request", Date),
|
||||
// )
|
||||
//
|
||||
// kDate := HMAC("AWS4"+kSecret, Date)
|
||||
// kRegion := HMAC(kDate, "us-east-1")
|
||||
// kService := HMAC(kRegion, "s3")
|
||||
// kSigning := HMAC(kService, "aws4_request")
|
||||
// _, err = base.RestyClient.R().SetResult(&resp).SetHeaders(map[string]string{
|
||||
// "Authorization": fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=%s",
|
||||
// jsoniter.Get(res, "data.AccessKeyId"),
|
||||
// Date,
|
||||
// hex.EncodeToString([]byte(HMAC(StringToSign, kSigning)))),
|
||||
// "X-Amz-Content-Sha256": "UNSIGNED-PAYLOAD",
|
||||
// "X-Amz-Date": nowTimeStr,
|
||||
// "x-amz-security-token": jsoniter.Get(res, "data.SessionToken").ToString(),
|
||||
// }).Post(fmt.Sprintf("%s?uploads", baseUrl))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return base.ErrNotImplement
|
||||
//}
|
||||
|
||||
var _ base.Driver = (*Pan123)(nil)
|
||||
175
drivers/139/139.go
Normal file
175
drivers/139/139.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package _39
|
||||
|
||||
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"
|
||||
"time"
|
||||
)
|
||||
|
||||
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 := utils.RandomStr(16)
|
||||
ts := time.Now().Format("2006-01-02 15:04:05")
|
||||
log.Debugf("%+v", data)
|
||||
body, err := utils.Json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sign := calSign(string(body), ts, randStr)
|
||||
svcType := "1"
|
||||
if isFamily(account) {
|
||||
svcType = "2"
|
||||
}
|
||||
req.SetHeaders(map[string]string{
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"CMS-DEVICE": "default",
|
||||
"Cookie": account.AccessToken,
|
||||
"mcloud-channel": "1000101",
|
||||
"mcloud-client": "10701",
|
||||
//"mcloud-route": "001",
|
||||
"mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
|
||||
//"mcloud-skey":"",
|
||||
"mcloud-version": "6.6.0",
|
||||
"Origin": "https://yun.139.com",
|
||||
"Referer": "https://yun.139.com/w/",
|
||||
"x-DeviceInfo": "||9|6.6.0|chrome|95.0.4638.69|uwIy75obnsRPIwlJSd7D9GhUvFwG96ce||macos 10.15.2||zh-CN|||",
|
||||
"x-huawei-channelSrc": "10000034",
|
||||
"x-inner-ntwk": "2",
|
||||
"x-m4c-caller": "PC",
|
||||
"x-m4c-src": "10002",
|
||||
"x-SvcType": svcType,
|
||||
})
|
||||
if headers != nil {
|
||||
req.SetHeaders(headers)
|
||||
}
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
if form != nil {
|
||||
req.SetFormData(form)
|
||||
}
|
||||
if data != nil {
|
||||
req.SetBody(data)
|
||||
}
|
||||
var e BaseResp
|
||||
//var err error
|
||||
var res *resty.Response
|
||||
req.SetResult(&e)
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
case base.Put:
|
||||
res, err = req.Put(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugln(res.String())
|
||||
if !e.Success {
|
||||
return nil, errors.New(e.Message)
|
||||
}
|
||||
if resp != nil {
|
||||
err = utils.Json.Unmarshal(res.Body(), resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (driver Cloud139) 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 Cloud139) GetFiles(catalogID string, account *model.Account) ([]model.File, error) {
|
||||
start := 0
|
||||
limit := 100
|
||||
files := make([]model.File, 0)
|
||||
for {
|
||||
data := base.Json{
|
||||
"catalogID": catalogID,
|
||||
"sortDirection": 1,
|
||||
"startNumber": start + 1,
|
||||
"endNumber": start + limit,
|
||||
"filterType": 0,
|
||||
"catalogSortType": 0,
|
||||
"contentSortType": 0,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
var resp GetDiskResp
|
||||
_, err := driver.Post("/orchestration/personalCloud/catalog/v1.0/getDisk", data, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, catalog := range resp.Data.GetDiskResult.CatalogList {
|
||||
f := model.File{
|
||||
Id: catalog.CatalogID,
|
||||
Name: catalog.CatalogName,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(catalog.UpdateTime),
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
for _, content := range resp.Data.GetDiskResult.ContentList {
|
||||
f := model.File{
|
||||
Id: content.ContentID,
|
||||
Name: content.ContentName,
|
||||
Size: content.ContentSize,
|
||||
Type: utils.GetFileType(path.Ext(content.ContentName)),
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(content.UpdateTime),
|
||||
Thumbnail: content.ThumbnailURL,
|
||||
//Thumbnail: content.BigthumbnailURL,
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
if start+limit >= resp.Data.GetDiskResult.NodeCount {
|
||||
break
|
||||
}
|
||||
start += limit
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Cloud139) GetLink(contentId string, account *model.Account) (string, error) {
|
||||
data := base.Json{
|
||||
"appName": "",
|
||||
"contentID": contentId,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
res, err := driver.Post("/orchestration/personalCloud/uploadAndDownload/v1.0/downloadRequest",
|
||||
data, nil, account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Cloud139{})
|
||||
}
|
||||
456
drivers/139/driver.go
Normal file
456
drivers/139/driver.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package _39
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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"
|
||||
"math"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Cloud139 struct{}
|
||||
|
||||
func (driver Cloud139) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "139Yun",
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Cloud139) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "username",
|
||||
Label: "phone",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "phone number",
|
||||
},
|
||||
{
|
||||
Name: "access_token",
|
||||
Label: "Cookie",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "Unknown expiration time",
|
||||
},
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "139yun type",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "Personal,Family",
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "site_id",
|
||||
Label: "cloud_id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
},
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) 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 Cloud139) 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
|
||||
}
|
||||
if isFamily(account) {
|
||||
files, err = driver.familyGetFiles(file.Id, account)
|
||||
} else {
|
||||
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 Cloud139) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
return &base.Link{Url: u}, nil
|
||||
}
|
||||
|
||||
func (driver Cloud139) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("139 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 Cloud139) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver Cloud139) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Cloud139) MakeDir(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"createCatalogExtReq": base.Json{
|
||||
"parentCatalogID": parentFile.Id,
|
||||
"newCatalogName": utils.Base(path),
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt"
|
||||
if isFamily(account) {
|
||||
data = base.Json{
|
||||
"cloudID": account.SiteId,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
"docLibName": utils.Base(path),
|
||||
}
|
||||
pathname = "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc"
|
||||
}
|
||||
_, err = driver.Post(pathname,
|
||||
data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) Move(src string, dst string, account *model.Account) error {
|
||||
if isFamily(account) {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstParentFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var contentInfoList []string
|
||||
var catalogInfoList []string
|
||||
if srcFile.IsDir() {
|
||||
catalogInfoList = append(catalogInfoList, srcFile.Id)
|
||||
} else {
|
||||
contentInfoList = append(contentInfoList, srcFile.Id)
|
||||
}
|
||||
data := base.Json{
|
||||
"createBatchOprTaskReq": base.Json{
|
||||
"taskType": 3,
|
||||
"actionType": "304",
|
||||
"taskInfo": base.Json{
|
||||
"contentInfoList": contentInfoList,
|
||||
"catalogInfoList": catalogInfoList,
|
||||
"newCatalogID": dstParentFile.Id,
|
||||
},
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
|
||||
_, err = driver.Post(pathname, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) Rename(src string, dst string, account *model.Account) error {
|
||||
if isFamily(account) {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var data base.Json
|
||||
var pathname string
|
||||
if srcFile.IsDir() {
|
||||
data = base.Json{
|
||||
"catalogID": srcFile.Id,
|
||||
"catalogName": utils.Base(dst),
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo"
|
||||
} else {
|
||||
data = base.Json{
|
||||
"contentID": srcFile.Id,
|
||||
"contentName": utils.Base(dst),
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname = "/orchestration/personalCloud/catalog/v1.0/updateContentInfo"
|
||||
}
|
||||
_, err = driver.Post(pathname, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) Copy(src string, dst string, account *model.Account) error {
|
||||
if isFamily(account) {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstParentFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var contentInfoList []string
|
||||
var catalogInfoList []string
|
||||
if srcFile.IsDir() {
|
||||
catalogInfoList = append(catalogInfoList, srcFile.Id)
|
||||
} else {
|
||||
contentInfoList = append(contentInfoList, srcFile.Id)
|
||||
}
|
||||
data := base.Json{
|
||||
"createBatchOprTaskReq": base.Json{
|
||||
"taskType": 3,
|
||||
"actionType": 309,
|
||||
"taskInfo": base.Json{
|
||||
"contentInfoList": contentInfoList,
|
||||
"catalogInfoList": catalogInfoList,
|
||||
"newCatalogID": dstParentFile.Id,
|
||||
},
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
|
||||
_, err = driver.Post(pathname, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var contentInfoList []string
|
||||
var catalogInfoList []string
|
||||
if file.IsDir() {
|
||||
catalogInfoList = append(catalogInfoList, file.Id)
|
||||
} else {
|
||||
contentInfoList = append(contentInfoList, file.Id)
|
||||
}
|
||||
data := base.Json{
|
||||
"createBatchOprTaskReq": base.Json{
|
||||
"taskType": 2,
|
||||
"actionType": 201,
|
||||
"taskInfo": base.Json{
|
||||
"newCatalogID": "",
|
||||
"contentInfoList": contentInfoList,
|
||||
"catalogInfoList": catalogInfoList,
|
||||
},
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
|
||||
if isFamily(account) {
|
||||
data = base.Json{
|
||||
"catalogList": catalogInfoList,
|
||||
"contentList": contentInfoList,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
"sourceCatalogType": 1002,
|
||||
"taskType": 2,
|
||||
}
|
||||
pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask"
|
||||
}
|
||||
_, err = driver.Post(pathname, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) 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
|
||||
}
|
||||
data := base.Json{
|
||||
"manualRename": 2,
|
||||
"operation": 0,
|
||||
"fileCount": 1,
|
||||
"totalSize": file.Size,
|
||||
"uploadContentList": []base.Json{{
|
||||
"contentName": file.Name,
|
||||
"contentSize": file.Size,
|
||||
// "digest": "5a3231986ce7a6b46e408612d385bafa"
|
||||
}},
|
||||
"parentCatalogID": parentFile.Id,
|
||||
"newCatalogName": "",
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
|
||||
if isFamily(account) {
|
||||
data = newJson(base.Json{
|
||||
"fileCount": 1,
|
||||
"manualRename": 2,
|
||||
"operation": 0,
|
||||
"path": "",
|
||||
"seqNo": "",
|
||||
"totalSize": file.Size,
|
||||
"uploadContentList": []base.Json{{
|
||||
"contentName": file.Name,
|
||||
"contentSize": file.Size,
|
||||
// "digest": "5a3231986ce7a6b46e408612d385bafa"
|
||||
}},
|
||||
}, account)
|
||||
pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL"
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
var resp UploadResp
|
||||
_, err = driver.Post(pathname, data, &resp, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var Default uint64 = 10485760
|
||||
part := int(math.Ceil(float64(file.Size) / float64(Default)))
|
||||
var start uint64 = 0
|
||||
for i := 0; i < part; i++ {
|
||||
byteSize := file.Size - start
|
||||
if byteSize > Default {
|
||||
byteSize = Default
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
_, err = io.ReadFull(file, byteData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, bytes.NewBuffer(byteData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headers := map[string]string{
|
||||
"Accept": "*/*",
|
||||
"Content-Type": "text/plain;name=" + unicode(file.Name),
|
||||
"contentSize": strconv.FormatUint(file.Size, 10),
|
||||
"range": fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1),
|
||||
"content-length": strconv.FormatUint(byteSize, 10),
|
||||
"uploadtaskID": resp.Data.UploadResult.UploadTaskID,
|
||||
"rangeType": "0",
|
||||
"Referer": "https://yun.139.com/",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.44",
|
||||
"x-SvcType": "1",
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("%+v", res)
|
||||
start += byteSize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Cloud139)(nil)
|
||||
74
drivers/139/family.go
Normal file
74
drivers/139/family.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package _39
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"path"
|
||||
)
|
||||
|
||||
func (driver Cloud139) familyGetFiles(catalogID string, account *model.Account) ([]model.File, error) {
|
||||
pageNum := 1
|
||||
files := make([]model.File, 0)
|
||||
for {
|
||||
data := newJson(base.Json{
|
||||
"catalogID": catalogID,
|
||||
"contentSortType": 0,
|
||||
"pageInfo": base.Json{
|
||||
"pageNum": pageNum,
|
||||
"pageSize": 100,
|
||||
},
|
||||
"sortDirection": 1,
|
||||
}, account)
|
||||
|
||||
var resp QueryContentListResp
|
||||
_, err := driver.Post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, catalog := range resp.Data.CloudCatalogList {
|
||||
f := model.File{
|
||||
Id: catalog.CatalogID,
|
||||
Name: catalog.CatalogName,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(catalog.LastUpdateTime),
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
for _, content := range resp.Data.CloudContentList {
|
||||
f := model.File{
|
||||
Id: content.ContentID,
|
||||
Name: content.ContentName,
|
||||
Size: content.ContentSize,
|
||||
Type: utils.GetFileType(path.Ext(content.ContentName)),
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(content.LastUpdateTime),
|
||||
Thumbnail: content.ThumbnailURL,
|
||||
//Thumbnail: content.BigthumbnailURL,
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
if 100*pageNum > resp.Data.TotalCount {
|
||||
break
|
||||
}
|
||||
pageNum++
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Cloud139) familyLink(contentId string, account *model.Account) (string, error) {
|
||||
data := newJson(base.Json{
|
||||
"contentID": contentId,
|
||||
//"path":"",
|
||||
}, account)
|
||||
res, err := driver.Post("/orchestration/familyCloud/content/v1.0/getFileDownLoadURL",
|
||||
data, nil, account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||
}
|
||||
187
drivers/139/types.go
Normal file
187
drivers/139/types.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package _39
|
||||
|
||||
type BaseResp struct {
|
||||
Success bool `json:"success"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Catalog struct {
|
||||
CatalogID string `json:"catalogID"`
|
||||
CatalogName string `json:"catalogName"`
|
||||
//CatalogType int `json:"catalogType"`
|
||||
//CreateTime string `json:"createTime"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
//IsShared bool `json:"isShared"`
|
||||
//CatalogLevel int `json:"catalogLevel"`
|
||||
//ShareDoneeCount int `json:"shareDoneeCount"`
|
||||
//OpenType int `json:"openType"`
|
||||
//ParentCatalogID string `json:"parentCatalogId"`
|
||||
//DirEtag int `json:"dirEtag"`
|
||||
//Tombstoned int `json:"tombstoned"`
|
||||
//ProxyID interface{} `json:"proxyID"`
|
||||
//Moved int `json:"moved"`
|
||||
//IsFixedDir int `json:"isFixedDir"`
|
||||
//IsSynced interface{} `json:"isSynced"`
|
||||
//Owner string `json:"owner"`
|
||||
//Modifier interface{} `json:"modifier"`
|
||||
//Path string `json:"path"`
|
||||
//ShareType int `json:"shareType"`
|
||||
//SoftLink interface{} `json:"softLink"`
|
||||
//ExtProp1 interface{} `json:"extProp1"`
|
||||
//ExtProp2 interface{} `json:"extProp2"`
|
||||
//ExtProp3 interface{} `json:"extProp3"`
|
||||
//ExtProp4 interface{} `json:"extProp4"`
|
||||
//ExtProp5 interface{} `json:"extProp5"`
|
||||
//ETagOprType int `json:"ETagOprType"`
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
ContentID string `json:"contentID"`
|
||||
ContentName string `json:"contentName"`
|
||||
//ContentSuffix string `json:"contentSuffix"`
|
||||
ContentSize int64 `json:"contentSize"`
|
||||
//ContentDesc string `json:"contentDesc"`
|
||||
//ContentType int `json:"contentType"`
|
||||
//ContentOrigin int `json:"contentOrigin"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
//CommentCount int `json:"commentCount"`
|
||||
ThumbnailURL string `json:"thumbnailURL"`
|
||||
//BigthumbnailURL string `json:"bigthumbnailURL"`
|
||||
//PresentURL string `json:"presentURL"`
|
||||
//PresentLURL string `json:"presentLURL"`
|
||||
//PresentHURL string `json:"presentHURL"`
|
||||
//ContentTAGList interface{} `json:"contentTAGList"`
|
||||
//ShareDoneeCount int `json:"shareDoneeCount"`
|
||||
//Safestate int `json:"safestate"`
|
||||
//Transferstate int `json:"transferstate"`
|
||||
//IsFocusContent int `json:"isFocusContent"`
|
||||
//UpdateShareTime interface{} `json:"updateShareTime"`
|
||||
//UploadTime string `json:"uploadTime"`
|
||||
//OpenType int `json:"openType"`
|
||||
//AuditResult int `json:"auditResult"`
|
||||
//ParentCatalogID string `json:"parentCatalogId"`
|
||||
//Channel string `json:"channel"`
|
||||
//GeoLocFlag string `json:"geoLocFlag"`
|
||||
//Digest string `json:"digest"`
|
||||
//Version string `json:"version"`
|
||||
//FileEtag string `json:"fileEtag"`
|
||||
//FileVersion string `json:"fileVersion"`
|
||||
//Tombstoned int `json:"tombstoned"`
|
||||
//ProxyID string `json:"proxyID"`
|
||||
//Moved int `json:"moved"`
|
||||
//MidthumbnailURL string `json:"midthumbnailURL"`
|
||||
//Owner string `json:"owner"`
|
||||
//Modifier string `json:"modifier"`
|
||||
//ShareType int `json:"shareType"`
|
||||
//ExtInfo struct {
|
||||
// Uploader string `json:"uploader"`
|
||||
// Address string `json:"address"`
|
||||
//} `json:"extInfo"`
|
||||
//Exif struct {
|
||||
// CreateTime string `json:"createTime"`
|
||||
// Longitude interface{} `json:"longitude"`
|
||||
// Latitude interface{} `json:"latitude"`
|
||||
// LocalSaveTime interface{} `json:"localSaveTime"`
|
||||
//} `json:"exif"`
|
||||
//CollectionFlag interface{} `json:"collectionFlag"`
|
||||
//TreeInfo interface{} `json:"treeInfo"`
|
||||
//IsShared bool `json:"isShared"`
|
||||
//ETagOprType int `json:"ETagOprType"`
|
||||
}
|
||||
|
||||
type GetDiskResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Result struct {
|
||||
ResultCode string `json:"resultCode"`
|
||||
ResultDesc interface{} `json:"resultDesc"`
|
||||
} `json:"result"`
|
||||
GetDiskResult struct {
|
||||
ParentCatalogID string `json:"parentCatalogID"`
|
||||
NodeCount int `json:"nodeCount"`
|
||||
CatalogList []Catalog `json:"catalogList"`
|
||||
ContentList []Content `json:"contentList"`
|
||||
IsCompleted int `json:"isCompleted"`
|
||||
} `json:"getDiskResult"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Result struct {
|
||||
ResultCode string `json:"resultCode"`
|
||||
ResultDesc interface{} `json:"resultDesc"`
|
||||
} `json:"result"`
|
||||
UploadResult struct {
|
||||
UploadTaskID string `json:"uploadTaskID"`
|
||||
RedirectionURL string `json:"redirectionUrl"`
|
||||
NewContentIDList []struct {
|
||||
ContentID string `json:"contentID"`
|
||||
ContentName string `json:"contentName"`
|
||||
IsNeedUpload string `json:"isNeedUpload"`
|
||||
FileEtag int64 `json:"fileEtag"`
|
||||
FileVersion int64 `json:"fileVersion"`
|
||||
OverridenFlag int `json:"overridenFlag"`
|
||||
} `json:"newContentIDList"`
|
||||
CatalogIDList interface{} `json:"catalogIDList"`
|
||||
IsSlice interface{} `json:"isSlice"`
|
||||
} `json:"uploadResult"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type CloudContent struct {
|
||||
ContentID string `json:"contentID"`
|
||||
//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 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"`
|
||||
}
|
||||
|
||||
type CloudCatalog struct {
|
||||
CatalogID string `json:"catalogID"`
|
||||
CatalogName string `json:"catalogName"`
|
||||
//CloudID string `json:"cloudID"`
|
||||
//CreateTime string `json:"createTime"`
|
||||
LastUpdateTime string `json:"lastUpdateTime"`
|
||||
//Creator string `json:"creator"`
|
||||
//CreatorNickname string `json:"creatorNickname"`
|
||||
}
|
||||
|
||||
type QueryContentListResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Result struct {
|
||||
ResultCode string `json:"resultCode"`
|
||||
ResultDesc string `json:"resultDesc"`
|
||||
} `json:"result"`
|
||||
Path string `json:"path"`
|
||||
CloudContentList []CloudContent `json:"cloudContentList"`
|
||||
CloudCatalogList []CloudCatalog `json:"cloudCatalogList"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
RecallContent interface{} `json:"recallContent"`
|
||||
} `json:"data"`
|
||||
}
|
||||
70
drivers/139/util.go
Normal file
70
drivers/139/util.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package _39
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func encodeURIComponent(str string) string {
|
||||
r := url.QueryEscape(str)
|
||||
r = strings.Replace(r, "+", "%20", -1)
|
||||
return r
|
||||
}
|
||||
|
||||
func calSign(body, ts, randStr string) string {
|
||||
body = strings.ReplaceAll(body, "\n", "")
|
||||
body = strings.ReplaceAll(body, " ", "")
|
||||
body = encodeURIComponent(body)
|
||||
strs := strings.Split(body, "")
|
||||
sort.Strings(strs)
|
||||
body = strings.Join(strs, "")
|
||||
body = base64.StdEncoding.EncodeToString([]byte(body))
|
||||
res := utils.GetMD5Encode(body) + utils.GetMD5Encode(ts+":"+randStr)
|
||||
res = strings.ToUpper(utils.GetMD5Encode(res))
|
||||
return res
|
||||
}
|
||||
|
||||
func getTime(t string) *time.Time {
|
||||
stamp, _ := time.ParseInLocation("20060102150405", t, time.Local)
|
||||
return &stamp
|
||||
}
|
||||
|
||||
func isFamily(account *model.Account) bool {
|
||||
return account.InternalType == "Family"
|
||||
}
|
||||
|
||||
func unicode(str string) string {
|
||||
textQuoted := strconv.QuoteToASCII(str)
|
||||
textUnquoted := textQuoted[1 : len(textQuoted)-1]
|
||||
return textUnquoted
|
||||
}
|
||||
|
||||
func MergeMap(mObj ...map[string]interface{}) map[string]interface{} {
|
||||
newObj := map[string]interface{}{}
|
||||
for _, m := range mObj {
|
||||
for k, v := range m {
|
||||
newObj[k] = v
|
||||
}
|
||||
}
|
||||
return newObj
|
||||
}
|
||||
|
||||
func newJson(data map[string]interface{}, account *model.Account) map[string]interface{} {
|
||||
common := map[string]interface{}{
|
||||
"catalogType": 3,
|
||||
"cloudID": account.SiteId,
|
||||
"cloudType": 1,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
return MergeMap(data, common)
|
||||
}
|
||||
621
drivers/189/189.go
Normal file
621
drivers/189/189.go
Normal file
@@ -0,0 +1,621 @@
|
||||
package _89
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"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"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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{
|
||||
Id: strconv.FormatInt(file.Id, 10),
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: nil,
|
||||
Thumbnail: file.Icon.SmallUrl,
|
||||
Url: file.Url,
|
||||
}
|
||||
loc, _ := time.LoadLocation("Local")
|
||||
lastOpTime, err := time.ParseInLocation("2006-01-02 15:04:05", file.LastOpTime, loc)
|
||||
if err == nil {
|
||||
f.UpdatedAt = &lastOpTime
|
||||
}
|
||||
if file.Size == -1 {
|
||||
f.Type = conf.FOLDER
|
||||
f.Size = 0
|
||||
} else {
|
||||
f.Type = utils.GetFileType(filepath.Ext(file.Name))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
//func (c Cloud189) GetFile(path string, account *model.Account) (*Cloud189File, error) {
|
||||
// dir, name := filepath.Split(path)
|
||||
// dir = utils.ParsePath(dir)
|
||||
// _, _, err := c.ParentPath(dir, account)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// parentFiles_, _ := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, dir))
|
||||
// parentFiles, _ := parentFiles_.([]Cloud189File)
|
||||
// for _, file := range parentFiles {
|
||||
// if file.Name == name {
|
||||
// if file.Size != -1 {
|
||||
// return &file, err
|
||||
// } else {
|
||||
// return nil, ErrNotFile
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return nil, ErrPathNotFound
|
||||
//}
|
||||
|
||||
type LoginResp struct {
|
||||
Msg string `json:"msg"`
|
||||
Result int `json:"result"`
|
||||
ToUrl string `json:"toUrl"`
|
||||
}
|
||||
|
||||
// Login refer to PanIndex
|
||||
func (driver Cloud189) Login(account *model.Account) error {
|
||||
client, ok := client189Map[account.Name]
|
||||
if !ok {
|
||||
//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/")
|
||||
}
|
||||
url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
|
||||
b := ""
|
||||
lt := ""
|
||||
ltText := regexp.MustCompile(`lt = "(.+?)"`)
|
||||
for i := 0; i < 3; i++ {
|
||||
res, err := client.R().Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 已经登陆
|
||||
if res.RawResponse.Request.URL.String() == "https://cloud.189.cn/web/main" {
|
||||
return nil
|
||||
}
|
||||
b = res.String()
|
||||
ltTextArr := ltText.FindStringSubmatch(b)
|
||||
if len(ltTextArr) > 0 {
|
||||
lt = ltTextArr[1]
|
||||
break
|
||||
} else {
|
||||
<-time.After(time.Second)
|
||||
}
|
||||
}
|
||||
if lt == "" {
|
||||
return fmt.Errorf("get empty login page")
|
||||
}
|
||||
captchaToken := regexp.MustCompile(`captchaToken' value='(.+?)'`).FindStringSubmatch(b)[1]
|
||||
returnUrl := regexp.MustCompile(`returnUrl = '(.+?)'`).FindStringSubmatch(b)[1]
|
||||
paramId := regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(b)[1]
|
||||
//reqId := regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(b)[1]
|
||||
jRsakey := regexp.MustCompile(`j_rsaKey" value="(\S+)"`).FindStringSubmatch(b)[1]
|
||||
vCodeID := regexp.MustCompile(`picCaptcha\.do\?token\=([A-Za-z0-9\&\=]+)`).FindStringSubmatch(b)[1]
|
||||
vCodeRS := ""
|
||||
if vCodeID != "" {
|
||||
// need ValidateCode
|
||||
log.Debugf("try to identify verification codes")
|
||||
timeStamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
|
||||
u := "https://open.e.189.cn/api/logbox/oauth2/picCaptcha.do?token=" + vCodeID + timeStamp
|
||||
imgRes, err := 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
|
||||
}
|
||||
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().
|
||||
SetHeaders(map[string]string{
|
||||
"lt": lt,
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
|
||||
"Referer": "https://open.e.189.cn/",
|
||||
"accept": "application/json;charset=UTF-8",
|
||||
}).SetFormData(map[string]string{
|
||||
"appKey": "cloud",
|
||||
"accountType": "01",
|
||||
"userName": "{RSA}" + userRsa,
|
||||
"password": "{RSA}" + passwordRsa,
|
||||
"validateCode": vCodeRS,
|
||||
"captchaToken": captchaToken,
|
||||
"returnUrl": returnUrl,
|
||||
"mailSuffix": "@pan.cn",
|
||||
"paramId": paramId,
|
||||
"clientType": "10010",
|
||||
"dynamicCheck": "FALSE",
|
||||
"cb_SaveName": "1",
|
||||
"isOauth2": "false",
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = utils.Json.Unmarshal(res.Body(), &loginResp)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
if loginResp.Result != 0 {
|
||||
return fmt.Errorf(loginResp.Msg)
|
||||
}
|
||||
_, err = client.R().Get(loginResp.ToUrl)
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
return err
|
||||
}
|
||||
client189Map[account.Name] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) isFamily(account *model.Account) bool {
|
||||
return account.InternalType == "Family"
|
||||
}
|
||||
|
||||
func (driver Cloud189) GetFiles(fileId string, account *model.Account) ([]Cloud189File, error) {
|
||||
res := make([]Cloud189File, 0)
|
||||
pageNum := 1
|
||||
|
||||
for {
|
||||
var resp Cloud189Files
|
||||
body, err := driver.Request("https://cloud.189.cn/api/open/file/listFiles.action", base.Get, map[string]string{
|
||||
//"noCache": random(),
|
||||
"pageSize": "60",
|
||||
"pageNum": strconv.Itoa(pageNum),
|
||||
"mediaType": "0",
|
||||
"folderId": fileId,
|
||||
"iconOption": "5",
|
||||
"orderBy": "lastOpTime", //account.OrderBy
|
||||
"descending": "true", //account.OrderDirection
|
||||
}, nil, nil, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = utils.Json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.ResCode != 0 {
|
||||
return nil, fmt.Errorf(resp.ResMessage)
|
||||
}
|
||||
if resp.FileListAO.Count == 0 {
|
||||
break
|
||||
}
|
||||
for _, folder := range resp.FileListAO.FolderList {
|
||||
res = append(res, Cloud189File{
|
||||
Id: folder.Id,
|
||||
LastOpTime: folder.LastOpTime,
|
||||
Name: folder.Name,
|
||||
Size: -1,
|
||||
})
|
||||
}
|
||||
res = append(res, resp.FileListAO.FileList...)
|
||||
pageNum++
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) Request(url string, method int, query, form map[string]string, headers map[string]string, account *model.Account) ([]byte, error) {
|
||||
client, err := driver.getClient(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//var resp base.Json
|
||||
if driver.isFamily(account) {
|
||||
url = strings.Replace(url, "/api/open", "/api/open/family", 1)
|
||||
if query != nil {
|
||||
query["familyId"] = account.SiteId
|
||||
}
|
||||
if form != nil {
|
||||
form["familyId"] = account.SiteId
|
||||
}
|
||||
}
|
||||
var e Cloud189Error
|
||||
req := client.R().SetError(&e).
|
||||
SetHeader("Accept", "application/json;charset=UTF-8").
|
||||
SetQueryParams(map[string]string{
|
||||
"noCache": random(),
|
||||
})
|
||||
if query != nil {
|
||||
req = req.SetQueryParams(query)
|
||||
}
|
||||
if form != nil {
|
||||
req = req.SetFormData(form)
|
||||
}
|
||||
if headers != nil {
|
||||
req = req.SetHeaders(headers)
|
||||
}
|
||||
var res *resty.Response
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//log.Debug(res.String())
|
||||
if e.ErrorCode != "" {
|
||||
if e.ErrorCode == "InvalidSessionKey" {
|
||||
err = driver.Login(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return driver.Request(url, method, query, form, nil, account)
|
||||
}
|
||||
}
|
||||
if jsoniter.Get(res.Body(), "res_code").ToInt() != 0 {
|
||||
err = errors.New(jsoniter.Get(res.Body(), "res_message").ToString())
|
||||
}
|
||||
return res.Body(), err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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) 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¶ms=%s", sessionKey, uri, c, h), 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", 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(uri + "---" + jsoniter.Get(data, "msg").ToString())
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
res, err := driver.UploadRequest("/person/initMultiUpload", map[string]string{
|
||||
"parentFolderId": parentFile.Id,
|
||||
"fileName": encode(file.Name),
|
||||
"fileSize": strconv.FormatInt(int64(file.Size), 10),
|
||||
"sliceSize": strconv.FormatInt(int64(DEFAULT), 10),
|
||||
"lazyCheck": "1",
|
||||
}, account, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
md5Sum := md5.New()
|
||||
for i = 1; 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)
|
||||
//log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
finish += uint64(n)
|
||||
md5Bytes := getMd5(byteData)
|
||||
md5Hex := hex.EncodeToString(md5Bytes)
|
||||
md5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)
|
||||
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, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fileMd5 := hex.EncodeToString(md5Sum.Sum(nil))
|
||||
sliceMd5 := fileMd5
|
||||
if file.GetSize() > DEFAULT {
|
||||
sliceMd5 = utils.GetMD5Encode(strings.Join(md5s, "\n"))
|
||||
}
|
||||
res, err = driver.UploadRequest("/person/commitMultiUploadFile", map[string]string{
|
||||
"uploadFileId": uploadFileId,
|
||||
"fileMd5": fileMd5,
|
||||
"sliceMd5": sliceMd5,
|
||||
"lazyCheck": "1",
|
||||
}, account, nil)
|
||||
account.DriveId, _ = driver.GetSessionKey(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) OldUpload(file *model.FileStream, account *model.Account) error {
|
||||
//return base.ErrNotImplement
|
||||
client, err := driver.getClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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())
|
||||
}
|
||||
375
drivers/189/driver.go
Normal file
375
drivers/189/driver.go
Normal file
@@ -0,0 +1,375 @@
|
||||
package _89
|
||||
|
||||
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"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Cloud189 struct{}
|
||||
|
||||
func (driver Cloud189) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "189Cloud",
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
Required: true,
|
||||
},
|
||||
//{
|
||||
// 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: "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 {
|
||||
delete(client189Map, old.Name)
|
||||
}
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if err := driver.Login(account); err != nil {
|
||||
account.Status = err.Error()
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
sessionKey, err := driver.GetSessionKey(account)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
} else {
|
||||
account.Status = "work"
|
||||
account.DriveId = sessionKey
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
var rawFiles []Cloud189File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
rawFiles, _ = cache.([]Cloud189File)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawFiles, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawFiles) > 0 {
|
||||
_ = base.SetCache(path, rawFiles, account)
|
||||
}
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
return 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
|
||||
}
|
||||
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
|
||||
}
|
||||
if resp.ResCode != 0 {
|
||||
return nil, fmt.Errorf(resp.ResMessage)
|
||||
}
|
||||
client, err := driver.getClient(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("189 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) 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
|
||||
}
|
||||
|
||||
func (driver Cloud189) MakeDir(path string, account *model.Account) error {
|
||||
dir, name := filepath.Split(path)
|
||||
parent, err := driver.File(dir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parent.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
form := map[string]string{
|
||||
"parentFolderId": parent.Id,
|
||||
"folderName": name,
|
||||
}
|
||||
_, err = driver.Request("https://cloud.189.cn/api/open/file/createFolder.action", base.Post, nil, form, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Move(src string, dst string, account *model.Account) error {
|
||||
dstDir, dstName := 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
|
||||
}
|
||||
isFolder := 0
|
||||
if srcFile.IsDir() {
|
||||
isFolder = 1
|
||||
}
|
||||
taskInfos := []base.Json{
|
||||
{
|
||||
"fileId": srcFile.Id,
|
||||
"fileName": dstName,
|
||||
"isFolder": isFolder,
|
||||
},
|
||||
}
|
||||
taskInfosBytes, err := utils.Json.Marshal(taskInfos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form := map[string]string{
|
||||
"type": "MOVE",
|
||||
"targetFolderId": dstDirFile.Id,
|
||||
"taskInfos": string(taskInfosBytes),
|
||||
}
|
||||
_, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", base.Post, nil, form, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Rename(src string, dst string, account *model.Account) error {
|
||||
_, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := "https://cloud.189.cn/api/open/file/renameFile.action"
|
||||
idKey := "fileId"
|
||||
nameKey := "destFileName"
|
||||
if srcFile.IsDir() {
|
||||
url = "https://cloud.189.cn/api/open/file/renameFolder.action"
|
||||
idKey = "folderId"
|
||||
nameKey = "destFolderName"
|
||||
}
|
||||
form := map[string]string{
|
||||
idKey: srcFile.Id,
|
||||
nameKey: dstName,
|
||||
}
|
||||
_, err = driver.Request(url, base.Post, nil, form, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Copy(src string, dst string, account *model.Account) error {
|
||||
dstDir, dstName := 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
|
||||
}
|
||||
isFolder := 0
|
||||
if srcFile.IsDir() {
|
||||
isFolder = 1
|
||||
}
|
||||
taskInfos := []base.Json{
|
||||
{
|
||||
"fileId": srcFile.Id,
|
||||
"fileName": dstName,
|
||||
"isFolder": isFolder,
|
||||
},
|
||||
}
|
||||
taskInfosBytes, err := utils.Json.Marshal(taskInfos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form := map[string]string{
|
||||
"type": "COPY",
|
||||
"targetFolderId": dstDirFile.Id,
|
||||
"taskInfos": string(taskInfosBytes),
|
||||
}
|
||||
_, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", base.Post, nil, form, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Delete(path string, account *model.Account) error {
|
||||
path = utils.ParsePath(path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isFolder := 0
|
||||
if file.IsDir() {
|
||||
isFolder = 1
|
||||
}
|
||||
taskInfos := []base.Json{
|
||||
{
|
||||
"fileId": file.Id,
|
||||
"fileName": file.Name,
|
||||
"isFolder": isFolder,
|
||||
},
|
||||
}
|
||||
taskInfosBytes, err := utils.Json.Marshal(taskInfos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form := map[string]string{
|
||||
"type": "DELETE",
|
||||
"targetFolderId": "",
|
||||
"taskInfos": string(taskInfosBytes),
|
||||
}
|
||||
_, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", base.Post, nil, form, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) error {
|
||||
//return base.ErrNotImplement
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
return driver.NewUpload(file, account)
|
||||
//return driver.OldUpload(file, account)
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Cloud189)(nil)
|
||||
67
drivers/189/types.go
Normal file
67
drivers/189/types.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package _89
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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
199
drivers/189/util.go
Normal 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)
|
||||
//}
|
||||
@@ -1,373 +0,0 @@
|
||||
package drivers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/robfig/cron/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
var aliClient = resty.New()
|
||||
|
||||
func init() {
|
||||
RegisterDriver("AliDrive", &AliDrive{})
|
||||
aliClient.
|
||||
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").
|
||||
SetHeader("origin", "https://aliyundrive.com")
|
||||
}
|
||||
|
||||
type AliDrive struct{}
|
||||
|
||||
func (a AliDrive) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
file, err := a.GetFile(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// office
|
||||
var resp Json
|
||||
var e AliRespError
|
||||
var url string
|
||||
req := Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": file.FileId,
|
||||
}
|
||||
switch file.Category {
|
||||
case "doc":
|
||||
{
|
||||
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
|
||||
req["access_token"] = account.AccessToken
|
||||
}
|
||||
case "video":
|
||||
{
|
||||
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
|
||||
req["category"] = "live_transcoding"
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("don't support")
|
||||
}
|
||||
_, err = aliClient.R().SetResult(&resp).SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(req).Post(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Code != "" {
|
||||
return nil, fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (a AliDrive) Items() []Item {
|
||||
return []Item{
|
||||
{
|
||||
Name: "refresh_token",
|
||||
Label: "refresh token",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: "string",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "limit",
|
||||
Label: "limit",
|
||||
Type: "number",
|
||||
Required: false,
|
||||
Description: ">0 and <=200",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a AliDrive) Proxy(ctx *fiber.Ctx) {
|
||||
ctx.Request().Header.Del("Origin")
|
||||
ctx.Request().Header.Set("Referer", "https://www.aliyundrive.com/")
|
||||
}
|
||||
|
||||
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 (a AliDrive) FormatFile(file *AliFile) *model.File {
|
||||
f := &model.File{
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
UpdatedAt: file.UpdatedAt,
|
||||
Thumbnail: file.Thumbnail,
|
||||
Driver: "AliDrive",
|
||||
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
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (a AliDrive) GetFiles(fileId string, account *model.Account) ([]AliFile, error) {
|
||||
marker := "first"
|
||||
res := make([]AliFile, 0)
|
||||
for marker != "" {
|
||||
if marker == "first" {
|
||||
marker = ""
|
||||
}
|
||||
var resp AliFiles
|
||||
var e AliRespError
|
||||
_, err := aliClient.R().
|
||||
SetResult(&resp).
|
||||
SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(Json{
|
||||
"drive_id": account.DriveId,
|
||||
"fields": "*",
|
||||
"image_thumbnail_process": "image/resize,w_400/format,jpeg",
|
||||
"image_url_process": "image/resize,w_1920/format,jpeg",
|
||||
"limit": account.Limit,
|
||||
"marker": marker,
|
||||
"order_by": account.OrderBy,
|
||||
"order_direction": account.OrderDirection,
|
||||
"parent_file_id": fileId,
|
||||
"video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300",
|
||||
"url_expire_sec": 14400,
|
||||
}).Post("https://api.aliyundrive.com/v2/file/list")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = a.RefreshToken(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
_ = model.SaveAccount(*account)
|
||||
return a.GetFiles(fileId, account)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
marker = resp.NextMarker
|
||||
res = append(res, resp.Items...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (a AliDrive) GetFile(path string, account *model.Account) (*AliFile, error) {
|
||||
dir, name := filepath.Split(path)
|
||||
dir = utils.ParsePath(dir)
|
||||
_, _, err := a.Path(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentFiles_, _ := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, dir))
|
||||
parentFiles, _ := parentFiles_.([]AliFile)
|
||||
for _, file := range parentFiles {
|
||||
if file.Name == name {
|
||||
if file.Type == "file" {
|
||||
return &file, err
|
||||
} else {
|
||||
return nil, fmt.Errorf("not file")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("path not found")
|
||||
}
|
||||
|
||||
// path: /aaa/bbb
|
||||
func (a AliDrive) Path(path string, account *model.Account) (*model.File, []*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("ali path: %s", path)
|
||||
cache, err := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path))
|
||||
if err == nil {
|
||||
file, ok := cache.(AliFile)
|
||||
if ok {
|
||||
return a.FormatFile(&file), nil, nil
|
||||
} else {
|
||||
files, _ := cache.([]AliFile)
|
||||
res := make([]*model.File, 0)
|
||||
for _, file = range files {
|
||||
res = append(res, a.FormatFile(&file))
|
||||
}
|
||||
return nil, res, nil
|
||||
}
|
||||
} else {
|
||||
fileId := account.RootFolder
|
||||
if path != "/" {
|
||||
dir, name := filepath.Split(path)
|
||||
dir = utils.ParsePath(dir)
|
||||
_, _, err = a.Path(dir, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
parentFiles_, _ := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, dir))
|
||||
parentFiles, _ := parentFiles_.([]AliFile)
|
||||
found := false
|
||||
for _, file := range parentFiles {
|
||||
if file.Name == name {
|
||||
found = true
|
||||
if file.Type == "file" {
|
||||
url, err := a.Link(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
file.Url = url
|
||||
return a.FormatFile(&file), nil, nil
|
||||
} else {
|
||||
fileId = file.FileId
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, nil, fmt.Errorf("path not found")
|
||||
}
|
||||
}
|
||||
files, err := a.GetFiles(fileId, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_ = conf.Cache.Set(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path), files, nil)
|
||||
res := make([]*model.File, 0)
|
||||
for _, file := range files {
|
||||
res = append(res, a.FormatFile(&file))
|
||||
}
|
||||
return nil, res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a AliDrive) Link(path string, account *model.Account) (string, error) {
|
||||
file, err := a.GetFile(utils.ParsePath(path), account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var resp Json
|
||||
var e AliRespError
|
||||
_, err = aliClient.R().SetResult(&resp).
|
||||
SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": file.FileId,
|
||||
"expire_sec": 14400,
|
||||
}).Post("https://api.aliyundrive.com/v2/file/get_download_url")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = a.RefreshToken(account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
_ = model.SaveAccount(*account)
|
||||
return a.Link(path, account)
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
return resp["url"].(string), nil
|
||||
}
|
||||
|
||||
func (a AliDrive) RefreshToken(account *model.Account) error {
|
||||
url := "https://auth.aliyundrive.com/v2/account/token"
|
||||
var resp TokenResp
|
||||
var e AliRespError
|
||||
_, err := aliClient.R().
|
||||
//ForceContentType("application/json").
|
||||
SetBody(Json{"refresh_token": account.RefreshToken, "grant_type": "refresh_token"}).
|
||||
SetResult(&resp).
|
||||
SetError(&e).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("%+v,%+v", resp, e)
|
||||
if e.Code != "" {
|
||||
return fmt.Errorf("failed to refresh token: %s", e.Message)
|
||||
}
|
||||
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AliDrive) Save(account *model.Account, old *model.Account) error {
|
||||
if old != nil {
|
||||
conf.Cron.Remove(cron.EntryID(old.CronId))
|
||||
}
|
||||
if account.RootFolder == "" {
|
||||
account.RootFolder = "root"
|
||||
}
|
||||
if account.Limit == 0 {
|
||||
account.Limit = 200
|
||||
}
|
||||
err := a.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var resp Json
|
||||
_, _ = aliClient.R().SetResult(&resp).
|
||||
SetBody("{}").
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
Post("https://api.aliyundrive.com/v2/user/get")
|
||||
log.Debugf("user info: %+v", resp)
|
||||
account.DriveId = resp["default_drive_id"].(string)
|
||||
cronId, err := conf.Cron.AddFunc("@every 2h", func() {
|
||||
name := account.Name
|
||||
newAccount, ok := model.GetAccount(name)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err = a.RefreshToken(&newAccount)
|
||||
if err != nil {
|
||||
newAccount.Status = err.Error()
|
||||
}
|
||||
_ = model.SaveAccount(newAccount)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.CronId = int(cronId)
|
||||
err = model.SaveAccount(*account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ Driver = (*AliDrive)(nil)
|
||||
246
drivers/alidrive/alidrive.go
Normal file
246
drivers/alidrive/alidrive.go
Normal file
@@ -0,0 +1,246 @@
|
||||
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"
|
||||
"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,
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
UpdatedAt: file.UpdatedAt,
|
||||
Thumbnail: file.Thumbnail,
|
||||
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
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (driver AliDrive) GetFiles(fileId string, account *model.Account) ([]AliFile, error) {
|
||||
marker := "first"
|
||||
res := make([]AliFile, 0)
|
||||
for marker != "" {
|
||||
if marker == "first" {
|
||||
marker = ""
|
||||
}
|
||||
var resp AliFiles
|
||||
var e AliRespError
|
||||
_, err := aliClient.R().
|
||||
SetResult(&resp).
|
||||
SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"fields": "*",
|
||||
"image_thumbnail_process": "image/resize,w_400/format,jpeg",
|
||||
"image_url_process": "image/resize,w_1920/format,jpeg",
|
||||
"limit": account.Limit,
|
||||
"marker": marker,
|
||||
"order_by": account.OrderBy,
|
||||
"order_direction": account.OrderDirection,
|
||||
"parent_file_id": fileId,
|
||||
"video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300",
|
||||
"url_expire_sec": 14400,
|
||||
}).Post("https://api.aliyundrive.com/v2/file/list")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.GetFiles(fileId, account)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
marker = resp.NextMarker
|
||||
res = append(res, resp.Items...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) GetFile(path string, account *model.Account) (*AliFile, error) {
|
||||
dir, name := filepath.Split(path)
|
||||
dir = utils.ParsePath(dir)
|
||||
_, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentFiles_, _ := base.GetCache(dir, account)
|
||||
parentFiles, _ := parentFiles_.([]AliFile)
|
||||
for _, file := range parentFiles {
|
||||
if file.Name == name {
|
||||
if file.Type == "file" {
|
||||
return &file, err
|
||||
} else {
|
||||
return nil, fmt.Errorf("not file")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver AliDrive) RefreshToken(account *model.Account) error {
|
||||
url := "https://auth.aliyundrive.com/v2/account/token"
|
||||
var resp base.TokenResp
|
||||
var e AliRespError
|
||||
_, err := aliClient.R().
|
||||
//ForceContentType("application/json").
|
||||
SetBody(base.Json{"refresh_token": account.RefreshToken, "grant_type": "refresh_token"}).
|
||||
SetResult(&resp).
|
||||
SetError(&e).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
return err
|
||||
}
|
||||
log.Debugf("%+v,%+v", resp, e)
|
||||
if e.Code != "" {
|
||||
account.Status = e.Message
|
||||
return fmt.Errorf("failed to refresh token: %s", e.Message)
|
||||
} else {
|
||||
account.Status = "work"
|
||||
}
|
||||
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) rename(fileId, name string, account *model.Account) error {
|
||||
var resp base.Json
|
||||
var e AliRespError
|
||||
_, err := aliClient.R().SetResult(&resp).SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"check_name_mode": "refuse",
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": fileId,
|
||||
"name": name,
|
||||
}).Post("https://api.aliyundrive.com/v3/file/update")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.rename(fileId, name, account)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
if resp["name"] == name {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%+v", resp)
|
||||
}
|
||||
|
||||
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).
|
||||
SetBody(base.Json{
|
||||
"requests": []base.Json{
|
||||
{
|
||||
"headers": base.Json{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
"method": "POST",
|
||||
"id": srcId,
|
||||
"body": base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": srcId,
|
||||
"to_drive_id": account.DriveId,
|
||||
"to_parent_file_id": dstId,
|
||||
},
|
||||
"url": url,
|
||||
},
|
||||
},
|
||||
"resource": "file",
|
||||
}).Post("https://api.aliyundrive.com/v3/batch")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.batch(srcId, dstId, url, account)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
status := jsoniter.Get(res.Body(), "status").ToInt()
|
||||
if status < 400 && status >= 100 {
|
||||
return nil
|
||||
}
|
||||
return errors.New(res.String())
|
||||
}
|
||||
|
||||
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").
|
||||
SetHeader("origin", "https://www.aliyundrive.com")
|
||||
}
|
||||
544
drivers/alidrive/driver.go
Normal file
544
drivers/alidrive/driver.go
Normal file
@@ -0,0 +1,544 @@
|
||||
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/robfig/cron/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AliDrive struct{}
|
||||
|
||||
func (driver AliDrive) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "AliDrive",
|
||||
}
|
||||
}
|
||||
|
||||
func (driver AliDrive) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "refresh_token",
|
||||
Label: "refresh token",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "name,size,updated_at,created_at",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "ASC,DESC",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "limit",
|
||||
Label: "limit",
|
||||
Type: base.TypeNumber,
|
||||
Required: false,
|
||||
Description: ">0 and <=200",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
if account.Limit == 0 {
|
||||
account.Limit = 200
|
||||
}
|
||||
err := driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var resp base.Json
|
||||
_, _ = aliClient.R().SetResult(&resp).
|
||||
SetBody("{}").
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
Post("https://api.aliyundrive.com/v2/user/get")
|
||||
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)
|
||||
log.Debugf("ali 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
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) 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 AliDrive) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var rawFiles []AliFile
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
rawFiles, _ = cache.([]AliFile)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawFiles, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawFiles) > 0 {
|
||||
_ = base.SetCache(path, rawFiles, account)
|
||||
}
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp base.Json
|
||||
var e AliRespError
|
||||
_, err = aliClient.R().SetResult(&resp).
|
||||
SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": file.Id,
|
||||
"expire_sec": 14400,
|
||||
}).Post("https://api.aliyundrive.com/v2/file/get_download_url")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.Link(args, account)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (driver AliDrive) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("ali 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 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// office
|
||||
var resp base.Json
|
||||
var e AliRespError
|
||||
var url string
|
||||
req := base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": file.FileId,
|
||||
}
|
||||
switch file.Category {
|
||||
case "doc":
|
||||
{
|
||||
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
|
||||
req["access_token"] = account.AccessToken
|
||||
}
|
||||
case "video":
|
||||
{
|
||||
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
|
||||
req["category"] = "live_transcoding"
|
||||
}
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
_, err = aliClient.R().SetResult(&resp).SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(req).Post(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Code != "" {
|
||||
return nil, fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) 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
|
||||
}
|
||||
var resp base.Json
|
||||
var e AliRespError
|
||||
_, err = aliClient.R().SetResult(&resp).SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"check_name_mode": "refuse",
|
||||
"drive_id": account.DriveId,
|
||||
"name": name,
|
||||
"parent_file_id": parentFile.Id,
|
||||
"type": "folder",
|
||||
}).Post("https://api.aliyundrive.com/adrive/v2/file/createWithFolders")
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.MakeDir(path, account)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
if resp["file_name"] == name {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%+v", resp)
|
||||
}
|
||||
|
||||
func (driver AliDrive) Move(src string, dst string, account *model.Account) error {
|
||||
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/move", account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver AliDrive) Rename(src string, dst string, account *model.Account) error {
|
||||
_, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = driver.rename(srcFile.Id, dstName, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver AliDrive) Copy(src string, dst string, account *model.Account) error {
|
||||
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 {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var e AliRespError
|
||||
res, err := aliClient.R().SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": file.Id,
|
||||
}).Post("https://api.aliyundrive.com/v2/recyclebin/trash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.Delete(path, account)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
if res.StatusCode() == 204 {
|
||||
return nil
|
||||
}
|
||||
return errors.New(res.String())
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
FileId string `json:"file_id"`
|
||||
UploadId string `json:"upload_id"`
|
||||
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 int64 = 10485760
|
||||
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
|
||||
partInfoList := make([]base.Json, 0, count)
|
||||
var i int64
|
||||
for i = 0; i < count; i++ {
|
||||
partInfoList = append(partInfoList, base.Json{
|
||||
"part_number": i + 1,
|
||||
})
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, _ := file.Read(buf[:])
|
||||
reqBody := base.Json{
|
||||
"check_name_mode": "auto_rename",
|
||||
"drive_id": account.DriveId,
|
||||
"name": file.GetFileName(),
|
||||
"parent_file_id": parentFile.Id,
|
||||
"part_info_list": partInfoList,
|
||||
"size": file.GetSize(),
|
||||
"type": "file",
|
||||
"pre_hash": utils.GetSHA1Encode(string(buf[:n])),
|
||||
}
|
||||
fileReader := io.MultiReader(bytes.NewReader(buf[:n]), file.File)
|
||||
|
||||
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 {
|
||||
return err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.Upload(file, account)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
|
||||
if e.Code == "PreHashMatched" {
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer tempFile.Close()
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
delete(reqBody, "pre_hash")
|
||||
h := sha1.New()
|
||||
if _, err = io.Copy(tempFile, io.TeeReader(fileReader, h)); 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)))
|
||||
*/
|
||||
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
|
||||
}
|
||||
fileReader = tempFile
|
||||
}
|
||||
|
||||
for i = 0; i < count; i++ {
|
||||
req, err := http.NewRequest("PUT", resp.PartInfoList[i].UploadUrl, io.LimitReader(fileReader, DEFAULT))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("%+v", res)
|
||||
//res, err := base.BaseClient.R().
|
||||
// SetHeader("Content-Type","").
|
||||
// SetBody(byteData).Put(resp.PartInfoList[i].UploadUrl)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//log.Debugf("put to %s : %d,%s", resp.PartInfoList[i].UploadUrl, res.StatusCode(),res.String())
|
||||
}
|
||||
var resp2 base.Json
|
||||
_, err = aliClient.R().SetResult(&resp2).SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": resp.FileId,
|
||||
"upload_id": resp.UploadId,
|
||||
}).Post("https://api.aliyundrive.com/v2/file/complete")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" {
|
||||
//if e.Code == "AccessTokenInvalid" {
|
||||
// err = driver.RefreshToken(account)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// } else {
|
||||
// _ = model.SaveAccount(account)
|
||||
// return driver.Upload(file, account)
|
||||
// }
|
||||
//}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
if resp2["file_id"] == resp.FileId {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%+v", resp2)
|
||||
}
|
||||
|
||||
var _ base.Driver = (*AliDrive)(nil)
|
||||
44
drivers/alist/alist.go
Normal file
44
drivers/alist/alist.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package alist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
)
|
||||
|
||||
type BaseResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type PathResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Type string `json:"type"`
|
||||
//Meta Meta `json:"meta"`
|
||||
Files []model.File `json:"files"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type PreviewResp struct {
|
||||
BaseResp
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func (driver *Alist) Login(account *model.Account) error {
|
||||
var resp BaseResp
|
||||
_, err := base.RestyClient.R().SetResult(&resp).
|
||||
SetHeader("Authorization", account.AccessToken).
|
||||
Get(account.SiteUrl + "/api/admin/login")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
return errors.New(resp.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Alist{})
|
||||
}
|
||||
192
drivers/alist/driver.go
Normal file
192
drivers/alist/driver.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package alist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Alist struct{}
|
||||
|
||||
func (driver Alist) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Alist",
|
||||
NoNeedSetLink: true,
|
||||
NoCors: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Alist) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "site_url",
|
||||
Label: "alist site url",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "access_token",
|
||||
Label: "token",
|
||||
Type: base.TypeString,
|
||||
Description: "admin token",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 = "/"
|
||||
}
|
||||
err := driver.Login(account)
|
||||
if err == nil {
|
||||
account.Status = "work"
|
||||
} else {
|
||||
account.Status = err.Error()
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Alist) File(path string, account *model.Account) (*model.File, error) {
|
||||
now := time.Now()
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: "root",
|
||||
Name: "root",
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: &now,
|
||||
}, nil
|
||||
}
|
||||
_, files, err := driver.Path(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if files == nil {
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
name := utils.Base(path)
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Alist) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
//return nil, base.ErrNotImplement
|
||||
_, files, err := driver.Path(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if files == nil {
|
||||
return nil, base.ErrNotFolder
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Alist) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
path := args.Path
|
||||
path = utils.ParsePath(path)
|
||||
name := utils.Base(path)
|
||||
flag := "d"
|
||||
if utils.GetFileType(filepath.Ext(path)) == conf.TEXT {
|
||||
flag = "p"
|
||||
}
|
||||
link := base.Link{}
|
||||
link.Url = fmt.Sprintf("%s/%s%s?sign=%s", account.SiteUrl, flag, path, utils.SignWithToken(name, conf.Token))
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Alist) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
path = filepath.Join(account.RootFolder, path)
|
||||
path = strings.ReplaceAll(path, "\\", "/")
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files := cache.([]model.File)
|
||||
return nil, files, nil
|
||||
}
|
||||
var resp PathResp
|
||||
_, err = base.RestyClient.R().SetResult(&resp).
|
||||
SetHeader("Authorization", account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"path": path,
|
||||
}).Post(account.SiteUrl + "/api/public/path")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
return nil, nil, errors.New(resp.Message)
|
||||
}
|
||||
if resp.Data.Type == "file" {
|
||||
return &resp.Data.Files[0], nil, nil
|
||||
}
|
||||
if len(resp.Data.Files) > 0 {
|
||||
_ = base.SetCache(path, resp.Data.Files, account)
|
||||
}
|
||||
return nil, resp.Data.Files, nil
|
||||
}
|
||||
|
||||
//func (driver Alist) Proxy(r *http.Request, account *model.Account) {}
|
||||
|
||||
func (driver Alist) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
var resp PathResp
|
||||
_, err := base.RestyClient.R().SetResult(&resp).
|
||||
SetHeader("Authorization", account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"path": path,
|
||||
}).Post(account.SiteUrl + "/api/public/preview")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
return nil, errors.New(resp.Message)
|
||||
}
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
func (driver Alist) MakeDir(path string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver Alist) Move(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver Alist) Rename(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver Alist) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver Alist) Delete(path string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver Alist) Upload(file *model.FileStream, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Alist)(nil)
|
||||
49
drivers/all.go
Normal file
49
drivers/all.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package drivers
|
||||
|
||||
import (
|
||||
_ "github.com/Xhofe/alist/drivers/123"
|
||||
_ "github.com/Xhofe/alist/drivers/139"
|
||||
_ "github.com/Xhofe/alist/drivers/189"
|
||||
_ "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"
|
||||
_ "github.com/Xhofe/alist/drivers/lanzou"
|
||||
_ "github.com/Xhofe/alist/drivers/mediatrack"
|
||||
_ "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/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"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var NoCors string
|
||||
var NoUpload string
|
||||
|
||||
func GetConfig() {
|
||||
for k, v := range base.GetDriversMap() {
|
||||
if v.Config().NoCors {
|
||||
NoCors += k + ","
|
||||
}
|
||||
if v.Upload(nil, nil) != base.ErrEmptyFile {
|
||||
NoUpload += k + ","
|
||||
}
|
||||
}
|
||||
NoCors = strings.Trim(NoCors, ",")
|
||||
NoUpload += "root"
|
||||
}
|
||||
|
||||
func init() {
|
||||
log.Debug("all init")
|
||||
GetConfig()
|
||||
}
|
||||
186
drivers/baidu/baidu.go
Normal file
186
drivers/baidu/baidu.go
Normal file
@@ -0,0 +1,186 @@
|
||||
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(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
u := "https://pan.baidu.com/rest/2.0" + pathname
|
||||
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(u)
|
||||
case base.Post:
|
||||
res, err = req.Post(u)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(u)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(u)
|
||||
case base.Put:
|
||||
res, err = req.Put(u)
|
||||
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(pathname, 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(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(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{})
|
||||
}
|
||||
355
drivers/baidu/driver.go
Normal file
355
drivers/baidu/driver.go
Normal file
@@ -0,0 +1,355 @@
|
||||
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: "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 {
|
||||
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) {
|
||||
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) 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)
|
||||
84
drivers/baidu/types.go
Normal file
84
drivers/baidu/types.go
Normal file
@@ -0,0 +1,84 @@
|
||||
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 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
18
drivers/baidu/util.go
Normal 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
|
||||
}
|
||||
28
drivers/base/cache.go
Normal file
28
drivers/base/cache.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func KeyCache(path string, account *model.Account) string {
|
||||
path = utils.ParsePath(path)
|
||||
return fmt.Sprintf("%s%s", account.Name, path)
|
||||
}
|
||||
|
||||
func SetCache(path string, obj interface{}, account *model.Account) error {
|
||||
return conf.Cache.Set(conf.Ctx, KeyCache(path, account), obj, nil)
|
||||
}
|
||||
|
||||
func GetCache(path string, account *model.Account) (interface{}, error) {
|
||||
return conf.Cache.Get(conf.Ctx, KeyCache(path, account))
|
||||
}
|
||||
|
||||
func DeleteCache(path string, account *model.Account) error {
|
||||
err := conf.Cache.Delete(conf.Ctx, KeyCache(path, account))
|
||||
log.Debugf("delete cache %s: %+v", path, err)
|
||||
return err
|
||||
}
|
||||
180
drivers/base/driver.go
Normal file
180
drivers/base/driver.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DriverConfig struct {
|
||||
Name string
|
||||
OnlyProxy bool // 必须使用代理(本机或者其他机器)
|
||||
OnlyLocal bool // 必须本机返回的
|
||||
ApiProxy bool // 使用API中转的
|
||||
NoNeedSetLink bool // 不需要设置链接的
|
||||
NoCors bool // 不可以跨域
|
||||
LocalSort bool // 本地排序
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
Path string
|
||||
IP string
|
||||
}
|
||||
|
||||
type Driver interface {
|
||||
// Config 配置
|
||||
Config() DriverConfig
|
||||
// Items 账号所需参数
|
||||
Items() []Item
|
||||
// Save 保存时处理
|
||||
Save(account *model.Account, old *model.Account) error
|
||||
// File 取文件
|
||||
File(path string, account *model.Account) (*model.File, error)
|
||||
// Files 取文件夹
|
||||
Files(path string, account *model.Account) ([]model.File, error)
|
||||
// Link 取链接
|
||||
Link(args Args, account *model.Account) (*Link, error)
|
||||
// Path 取路径(文件或文件夹)
|
||||
Path(path string, account *model.Account) (*model.File, []model.File, error)
|
||||
// Deprecated Proxy 代理处理
|
||||
//Proxy(r *http.Request, account *model.Account)
|
||||
// Preview 预览
|
||||
Preview(path string, account *model.Account) (interface{}, error)
|
||||
// MakeDir 创建文件夹
|
||||
MakeDir(path string, account *model.Account) error
|
||||
// Move 移动/改名
|
||||
Move(src string, dst string, account *model.Account) error
|
||||
// Rename 改名
|
||||
Rename(src string, dst string, account *model.Account) error
|
||||
// Copy 拷贝
|
||||
Copy(src string, dst string, account *model.Account) error
|
||||
// Delete 删除
|
||||
Delete(path string, account *model.Account) error
|
||||
// Upload 上传
|
||||
Upload(file *model.FileStream, account *model.Account) error
|
||||
// TODO
|
||||
//Search(path string, keyword string, account *model.Account) ([]*model.File, error)
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
var driversMap = map[string]Driver{}
|
||||
|
||||
func RegisterDriver(driver Driver) {
|
||||
log.Infof("register driver: [%s]", driver.Config().Name)
|
||||
driversMap[driver.Config().Name] = driver
|
||||
}
|
||||
|
||||
func GetDriver(name string) (driver Driver, ok bool) {
|
||||
driver, ok = driversMap[name]
|
||||
return
|
||||
}
|
||||
|
||||
func GetDriversMap() map[string]Driver {
|
||||
return driversMap
|
||||
}
|
||||
|
||||
func GetDrivers() map[string][]Item {
|
||||
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] = append([]Item{
|
||||
webdavDirect,
|
||||
}, v.Items()...)
|
||||
} else {
|
||||
res[k] = append([]Item{
|
||||
{
|
||||
Name: "proxy",
|
||||
Label: "proxy",
|
||||
Type: TypeBool,
|
||||
Required: true,
|
||||
Description: "web proxy",
|
||||
},
|
||||
{
|
||||
Name: "webdav_proxy",
|
||||
Label: "webdav proxy",
|
||||
Type: TypeBool,
|
||||
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,
|
||||
},
|
||||
{
|
||||
Name: "extract_folder",
|
||||
Label: "extract_folder",
|
||||
Values: "front,back",
|
||||
Type: TypeSelect,
|
||||
},
|
||||
}, res[k]...)
|
||||
if v.Config().ApiProxy {
|
||||
res[k] = append([]Item{
|
||||
{
|
||||
Name: "api_proxy_url",
|
||||
Label: "api_proxy_url",
|
||||
Type: TypeString,
|
||||
},
|
||||
}, res[k]...)
|
||||
}
|
||||
if v.Config().LocalSort {
|
||||
res[k] = append(res[k], []Item{
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: TypeSelect,
|
||||
Values: "name,size,updated_at",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: TypeSelect,
|
||||
Values: "ASC,DESC",
|
||||
Required: false,
|
||||
},
|
||||
}...)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
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(
|
||||
resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}),
|
||||
)
|
||||
NoRedirectClient.SetHeader("user-agent", UserAgent)
|
||||
RestyClient.SetHeader("user-agent", UserAgent)
|
||||
RestyClient.SetRetryCount(3)
|
||||
RestyClient.SetTimeout(DefaultTimeout)
|
||||
}
|
||||
51
drivers/base/types.go
Normal file
51
drivers/base/types.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPathNotFound = errors.New("path not found")
|
||||
ErrNotFile = errors.New("not file")
|
||||
ErrNotImplement = errors.New("not implement")
|
||||
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 (
|
||||
TypeString = "string"
|
||||
TypeSelect = "select"
|
||||
TypeBool = "bool"
|
||||
TypeNumber = "number"
|
||||
)
|
||||
|
||||
const (
|
||||
Get = iota
|
||||
Post
|
||||
Put
|
||||
Delete
|
||||
Patch
|
||||
)
|
||||
|
||||
type Json map[string]interface{}
|
||||
|
||||
type TokenResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Url string `json:"url"`
|
||||
Headers []Header `json:"headers"`
|
||||
Data io.ReadCloser
|
||||
FilePath string `json:"path"` // for native
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package drivers
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Driver interface {
|
||||
Items() []Item
|
||||
Path(path string, account *model.Account) (*model.File, []*model.File, error)
|
||||
Link(path string, account *model.Account) (string, error)
|
||||
Save(account *model.Account, old *model.Account) error
|
||||
Proxy(ctx *fiber.Ctx)
|
||||
Preview(path string, account *model.Account) (interface{}, error)
|
||||
// TODO
|
||||
//MakeDir(path string, account *model.Account) error
|
||||
//Move(src string, des string, account *model.Account) error
|
||||
//Delete(path string) error
|
||||
//Upload(file *fs.File, path string, account *model.Account) error
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"`
|
||||
Values string `json:"values"`
|
||||
Required bool `json:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type TokenResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
var driversMap = map[string]Driver{}
|
||||
|
||||
func RegisterDriver(name string, driver Driver) {
|
||||
driversMap[name] = driver
|
||||
}
|
||||
|
||||
func GetDriver(name string) (driver Driver, ok bool) {
|
||||
driver, ok = driversMap[name]
|
||||
return
|
||||
}
|
||||
|
||||
func GetDrivers() map[string][]Item {
|
||||
res := make(map[string][]Item, 0)
|
||||
for k, v := range driversMap {
|
||||
res[k] = v.Items()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type Json map[string]interface{}
|
||||
254
drivers/ftp/driver.go
Normal file
254
drivers/ftp/driver.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/jlaffaye/ftp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type FTP struct{}
|
||||
|
||||
func (driver FTP) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "FTP",
|
||||
OnlyProxy: true,
|
||||
OnlyLocal: true,
|
||||
NoNeedSetLink: true,
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver FTP) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "site_url",
|
||||
Label: "ftp host url",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
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,
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 = "/"
|
||||
}
|
||||
_, err := driver.Login(account)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
} else {
|
||||
account.Status = "work"
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver FTP) File(path string, account *model.Account) (*model.File, error) {
|
||||
log.Debugf("file: %s", path)
|
||||
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 FTP) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
log.Debugf("files: %s", path)
|
||||
path = utils.ParsePath(path)
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ := cache.([]model.File)
|
||||
return files, nil
|
||||
}
|
||||
realPath := utils.Join(account.RootFolder, path)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
entries, err := conn.List(realPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make([]model.File, 0)
|
||||
for i, _ := range entries {
|
||||
entry := entries[i]
|
||||
if entry.Name == "." || entry.Name == ".." {
|
||||
continue
|
||||
}
|
||||
f := model.File{
|
||||
Name: entry.Name,
|
||||
Size: int64(entry.Size),
|
||||
UpdatedAt: &entry.Time,
|
||||
Driver: driver.Config().Name,
|
||||
}
|
||||
if entry.Type == ftp.EntryTypeFolder {
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
f.Type = utils.GetFileType(filepath.Ext(entry.Name))
|
||||
}
|
||||
res = append(res, f)
|
||||
}
|
||||
if len(res) > 0 {
|
||||
_ = base.SetCache(path, res, account)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver FTP) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
path := args.Path
|
||||
path = utils.ParsePath(path)
|
||||
realPath := utils.Join(account.RootFolder, path)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
resp, err := conn.Retr(realPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//defer func() { _ = resp.Close() }()
|
||||
//data, err := ioutil.ReadAll(resp)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
return &base.Link{
|
||||
Data: resp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver FTP) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
log.Debugf("ftp path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
//file.Url, _ = driver.Link(path, account)
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//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
|
||||
}
|
||||
|
||||
func (driver FTP) MakeDir(path string, account *model.Account) error {
|
||||
path = utils.ParsePath(path)
|
||||
realPath := utils.Join(account.RootFolder, path)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
err = conn.MakeDir(realPath)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver FTP) Move(src string, dst string, account *model.Account) error {
|
||||
realSrc := utils.Join(account.RootFolder, src)
|
||||
realDst := utils.Join(account.RootFolder, dst)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
err = conn.Rename(realSrc, realDst)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver FTP) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver FTP) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver FTP) Delete(path string, account *model.Account) error {
|
||||
path = utils.ParsePath(path)
|
||||
realPath := utils.Join(account.RootFolder, path)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
err = conn.Delete(realPath)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver FTP) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
realPath := utils.Join(account.RootFolder, file.ParentPath, file.Name)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
err = conn.Stor(realPath, file)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*FTP)(nil)
|
||||
36
drivers/ftp/ftp.go
Normal file
36
drivers/ftp/ftp.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"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
|
||||
}
|
||||
err = conn.Login(account.Username, account.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connMap[account.Name] = conn
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&FTP{})
|
||||
connMap = make(map[string]*ftp.ServerConn)
|
||||
}
|
||||
287
drivers/google/driver.go
Normal file
287
drivers/google/driver.go
Normal file
@@ -0,0 +1,287 @@
|
||||
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"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type GoogleDrive struct{}
|
||||
|
||||
func (driver GoogleDrive) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "GoogleDrive",
|
||||
OnlyProxy: true,
|
||||
ApiProxy: true,
|
||||
NoNeedSetLink: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "client_id",
|
||||
Label: "client id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "client_secret",
|
||||
Label: "client secret",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "refresh_token",
|
||||
Label: "refresh token",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
Description: "such as: folder,name,modifiedTime",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "asc,desc",
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
account.Status = err.Error()
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
if account.RootFolder == "" {
|
||||
account.RootFolder = "root"
|
||||
}
|
||||
account.Status = "work"
|
||||
_ = model.SaveAccount(account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) 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 GoogleDrive) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var rawFiles []File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
rawFiles, _ = cache.([]File)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawFiles, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawFiles) > 0 {
|
||||
_ = base.SetCache(path, rawFiles, account)
|
||||
}
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file, account))
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.Type == conf.FOLDER {
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
url := fmt.Sprintf("https://www.googleapis.com/drive/v3/files/%s?includeItemsFromAllDrives=true&supportsAllDrives=true", file.Id)
|
||||
_, err = driver.Request(url, base.Get, nil, nil, nil, nil, nil, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link := base.Link{
|
||||
Url: url + "&alt=media",
|
||||
Headers: []base.Header{
|
||||
{
|
||||
Name: "Authorization",
|
||||
Value: "Bearer " + account.AccessToken,
|
||||
},
|
||||
},
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("google 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 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
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) MakeDir(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"name": utils.Base(path),
|
||||
"parents": []string{parentFile.Id},
|
||||
"mimeType": "application/vnd.google-apps.folder",
|
||||
}
|
||||
_, err = driver.Request("https://www.googleapis.com/drive/v3/files", base.Post, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Move(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
url := "https://www.googleapis.com/drive/v3/files/" + srcFile.Id
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstParentFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query := map[string]string{
|
||||
"addParents": dstParentFile.Id,
|
||||
"removeParents": "root",
|
||||
}
|
||||
_, err = driver.Request(url, base.Patch, nil, query, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Rename(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
url := "https://www.googleapis.com/drive/v3/files/" + srcFile.Id
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"name": utils.Base(dst),
|
||||
}
|
||||
_, err = driver.Request(url, base.Patch, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.File(path, account)
|
||||
url := "https://www.googleapis.com/drive/v3/files/" + file.Id
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request(url, base.Delete, nil, nil, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) 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
|
||||
}
|
||||
data := base.Json{
|
||||
"name": file.Name,
|
||||
"parents": []string{parentFile.Id},
|
||||
}
|
||||
var e Error
|
||||
url := "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true"
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
res, err := base.NoRedirectClient.R().SetHeader("Authorization", "Bearer "+account.AccessToken).
|
||||
SetError(&e).SetBody(data).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Error.Code != 0 {
|
||||
if e.Error.Code == 401 {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
return driver.Upload(file, account)
|
||||
}
|
||||
return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
|
||||
}
|
||||
putUrl := res.Header().Get("location")
|
||||
byteData, _ := ioutil.ReadAll(file)
|
||||
_, err = driver.Request(putUrl, base.Put, nil, nil, nil, byteData, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*GoogleDrive)(nil)
|
||||
198
drivers/google/googledrive.go
Normal file
198
drivers/google/googledrive.go
Normal file
@@ -0,0 +1,198 @@
|
||||
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 {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) RefreshToken(account *model.Account) error {
|
||||
url := "https://www.googleapis.com/oauth2/v4/token"
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
var resp base.TokenResp
|
||||
var e TokenError
|
||||
res, err := base.RestyClient.R().SetResult(&resp).SetError(&e).
|
||||
SetFormData(map[string]string{
|
||||
"client_id": account.ClientId,
|
||||
"client_secret": account.ClientSecret,
|
||||
"refresh_token": account.RefreshToken,
|
||||
"grant_type": "refresh_token",
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if e.Error != "" {
|
||||
return fmt.Errorf(e.Error)
|
||||
}
|
||||
account.AccessToken = resp.AccessToken
|
||||
account.Status = "work"
|
||||
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,
|
||||
Name: file.Name,
|
||||
Driver: driver.Config().Name,
|
||||
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))
|
||||
}
|
||||
if file.ThumbnailLink != "" {
|
||||
if account.DownProxyUrl != "" {
|
||||
f.Thumbnail = fmt.Sprintf("%s/%s", account.DownProxyUrl, file.ThumbnailLink)
|
||||
} else {
|
||||
f.Thumbnail = file.ThumbnailLink
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type Files struct {
|
||||
NextPageToken string `json:"nextPageToken"`
|
||||
Files []File `json:"files"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Error struct {
|
||||
Errors []struct {
|
||||
Domain string `json:"domain"`
|
||||
Reason string `json:"reason"`
|
||||
Message string `json:"message"`
|
||||
LocationType string `json:"location_type"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) GetFiles(id string, account *model.Account) ([]File, error) {
|
||||
pageToken := "first"
|
||||
res := make([]File, 0)
|
||||
for pageToken != "" {
|
||||
if pageToken == "first" {
|
||||
pageToken = ""
|
||||
}
|
||||
var resp Files
|
||||
orderBy := "folder,name,modifiedTime desc"
|
||||
if account.OrderBy != "" {
|
||||
orderBy = account.OrderBy + " " + account.OrderDirection
|
||||
}
|
||||
query := map[string]string{
|
||||
"orderBy": orderBy,
|
||||
"fields": "files(id,name,mimeType,size,modifiedTime,thumbnailLink),nextPageToken",
|
||||
"pageSize": "1000",
|
||||
"q": fmt.Sprintf("'%s' in parents and trashed = false", id),
|
||||
//"includeItemsFromAllDrives": "true",
|
||||
//"supportsAllDrives": "true",
|
||||
"pageToken": pageToken,
|
||||
}
|
||||
_, err := driver.Request("https://www.googleapis.com/drive/v3/files",
|
||||
base.Get, nil, query, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pageToken = resp.NextPageToken
|
||||
res = append(res, resp.Files...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Request(url string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
rawUrl := url
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+account.AccessToken)
|
||||
req.SetQueryParam("includeItemsFromAllDrives", "true")
|
||||
req.SetQueryParam("supportsAllDrives", "true")
|
||||
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
|
||||
var e Error
|
||||
req.SetError(&e)
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
case base.Put:
|
||||
res, err = req.Put(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if e.Error.Code != 0 {
|
||||
if e.Error.Code == 401 {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
_ = model.SaveAccount(account)
|
||||
return nil, err
|
||||
}
|
||||
return driver.Request(rawUrl, method, headers, query, form, data, resp, account)
|
||||
}
|
||||
return nil, fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&GoogleDrive{})
|
||||
}
|
||||
196
drivers/lanzou/driver.go
Normal file
196
drivers/lanzou/driver.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package lanzou
|
||||
|
||||
import (
|
||||
"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"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Lanzou struct{}
|
||||
|
||||
func (driver Lanzou) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Lanzou",
|
||||
NoCors: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Lanzou) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "lanzou type",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "cookie,url",
|
||||
},
|
||||
{
|
||||
Name: "access_token",
|
||||
Label: "cookie",
|
||||
Type: base.TypeString,
|
||||
Description: "about 15 days valid",
|
||||
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",
|
||||
Label: "share password",
|
||||
Type: base.TypeString,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
account.Status = "work"
|
||||
_ = model.SaveAccount(account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Lanzou) 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 Lanzou) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var rawFiles []LanZouFile
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
rawFiles, _ = cache.([]LanZouFile)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawFiles, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawFiles) > 0 {
|
||||
_ = base.SetCache(path, rawFiles, account)
|
||||
}
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Lanzou) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("down file: %+v", file)
|
||||
downId := file.Id
|
||||
if account.InternalType == "cookie" {
|
||||
downId, err = driver.GetDownPageId(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
url, err := driver.GetLink(downId, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link := base.Link{
|
||||
Url: url,
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: base.UserAgent},
|
||||
},
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Lanzou) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("lanzou 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 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
|
||||
}
|
||||
|
||||
func (driver *Lanzou) MakeDir(path string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver *Lanzou) Move(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver *Lanzou) Rename(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver *Lanzou) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver *Lanzou) Delete(path string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver *Lanzou) Upload(file *model.FileStream, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Lanzou)(nil)
|
||||
257
drivers/lanzou/lanzou.go
Normal file
257
drivers/lanzou/lanzou.go
Normal file
@@ -0,0 +1,257 @@
|
||||
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"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
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{
|
||||
Id: file.Id,
|
||||
Name: file.Name,
|
||||
Driver: driver.Config().Name,
|
||||
SizeStr: file.Size,
|
||||
TimeStr: file.Time,
|
||||
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))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type LanZouFilesResp struct {
|
||||
Zt int `json:"zt"`
|
||||
Info interface{} `json:"info"`
|
||||
Text []LanZouFile `json:"text"`
|
||||
}
|
||||
|
||||
func (driver *Lanzou) GetFiles(folderId string, account *model.Account) ([]LanZouFile, error) {
|
||||
if account.InternalType == "cookie" {
|
||||
files := make([]LanZouFile, 0)
|
||||
var resp LanZouFilesResp
|
||||
// folders
|
||||
res, err := base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
SetFormData(map[string]string{
|
||||
"task": "47",
|
||||
"folder_id": folderId,
|
||||
}).Post("https://pc.woozooo.com/doupload.php")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if resp.Zt != 1 && resp.Zt != 2 {
|
||||
return nil, fmt.Errorf("%v", resp.Info)
|
||||
}
|
||||
for _, file := range resp.Text {
|
||||
file.Folder = true
|
||||
files = append(files, file)
|
||||
}
|
||||
// files
|
||||
pg := 1
|
||||
for {
|
||||
_, err = base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
SetFormData(map[string]string{
|
||||
"task": "5",
|
||||
"folder_id": folderId,
|
||||
"pg": strconv.Itoa(pg),
|
||||
}).Post("https://pc.woozooo.com/doupload.php")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Zt != 1 {
|
||||
return nil, fmt.Errorf("%v", resp.Info)
|
||||
}
|
||||
if len(resp.Text) == 0 {
|
||||
break
|
||||
}
|
||||
files = append(files, resp.Text...)
|
||||
pg++
|
||||
}
|
||||
return files, nil
|
||||
} else {
|
||||
return driver.GetFilesByUrl(account)
|
||||
}
|
||||
}
|
||||
|
||||
func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error) {
|
||||
files := make([]LanZouFile, 0)
|
||||
shareUrl := account.SiteUrl
|
||||
u, err := url.Parse(shareUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := base.RestyClient.R().Get(shareUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lxArr := regexp.MustCompile(`'lx':(.+?),`).FindStringSubmatch(res.String())
|
||||
if len(lxArr) == 0 {
|
||||
return nil, fmt.Errorf("get empty page")
|
||||
}
|
||||
lx := lxArr[1]
|
||||
fid := regexp.MustCompile(`'fid':(.+?),`).FindStringSubmatch(res.String())[1]
|
||||
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 := ""
|
||||
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]
|
||||
k := regexp.MustCompile(`var ` + kName + ` = '(.+?)';`).FindStringSubmatch(res.String())[1]
|
||||
pg := 1
|
||||
for {
|
||||
var resp LanZouFilesResp
|
||||
res, err = base.RestyClient.R().SetResult(&resp).SetFormData(map[string]string{
|
||||
"lx": lx,
|
||||
"fid": fid,
|
||||
"uid": uid,
|
||||
"pg": strconv.Itoa(pg),
|
||||
"rep": rep,
|
||||
"t": t,
|
||||
"k": k,
|
||||
"up": up,
|
||||
"ls": ls,
|
||||
"pwd": account.Password,
|
||||
}).Post(fmt.Sprintf("https://%s/filemoreajax.php", u.Host))
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
break
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if resp.Zt != 1 {
|
||||
return nil, fmt.Errorf("%v", resp.Info)
|
||||
}
|
||||
if len(resp.Text) == 0 {
|
||||
break
|
||||
}
|
||||
pg++
|
||||
time.Sleep(time.Second)
|
||||
files = append(files, resp.Text...)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
//type LanzouDownInfo struct {
|
||||
// FId string `json:"f_id"`
|
||||
// IsNewd string `json:"is_newd"`
|
||||
//}
|
||||
|
||||
// GetDownPageId 获取下载页面的ID
|
||||
func (driver *Lanzou) GetDownPageId(fileId string, account *model.Account) (string, error) {
|
||||
var resp LanZouFilesResp
|
||||
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
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if resp.Zt != 1 {
|
||||
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
|
||||
}
|
||||
|
||||
type LanzouLinkResp struct {
|
||||
Dom string `json:"dom"`
|
||||
Url string `json:"url"`
|
||||
Zt int `json:"zt"`
|
||||
}
|
||||
|
||||
func (driver *Lanzou) GetLink(downId string, account *model.Account) (string, error) {
|
||||
shareUrl := account.SiteUrl
|
||||
u, err := url.Parse(shareUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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")
|
||||
}
|
||||
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]
|
||||
sign := regexp.MustCompile(`'sign':'(.+?)',`).FindStringSubmatch(res.String())[1]
|
||||
//websign := regexp.MustCompile(`'websign':'(.+?)'`).FindStringSubmatch(res.String())[1]
|
||||
//websign := regexp.MustCompile(`var websign = '(.+?)'`).FindStringSubmatch(res.String())[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,
|
||||
"websignkey": websignkey,
|
||||
}
|
||||
log.Debugf("form: %+v", form)
|
||||
res, err = base.RestyClient.R().SetResult(&resp).
|
||||
SetHeader("origin", "https://"+u.Host).
|
||||
SetHeader("referer", iframeUrl).
|
||||
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")
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Lanzou{})
|
||||
}
|
||||
321
drivers/mediatrack/driver.go
Normal file
321
drivers/mediatrack/driver.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package mediatrack
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"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/google/uuid"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type MediaTrack struct{}
|
||||
|
||||
func (driver MediaTrack) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "MediaTrack",
|
||||
}
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "access_token",
|
||||
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",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "updated_at,title,size",
|
||||
Required: true,
|
||||
Description: "title",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "desc",
|
||||
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
|
||||
}
|
||||
|
||||
func (driver MediaTrack) 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 MediaTrack) 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 MediaTrack) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pre := "https://jayce.api.mediatrack.cn/v3/assets/" + file.Id
|
||||
body, err := driver.Request(pre+"/token", base.Get, nil, nil, nil, nil, nil, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := pre + "/raw"
|
||||
res, err := base.NoRedirectClient.R().SetQueryParam("token", jsoniter.Get(body, "data").ToString()).Get(url)
|
||||
return &base.Link{Url: res.Header().Get("location")}, nil
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("MediaTrack 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 MediaTrack) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver MediaTrack) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver MediaTrack) MakeDir(path string, account *model.Account) error {
|
||||
_, err := driver.File(path, account)
|
||||
if err != base.ErrPathNotFound {
|
||||
return nil
|
||||
}
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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),
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver MediaTrack) 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{
|
||||
"parent_id": dstParentFile.Id,
|
||||
"ids": []string{srcFile.Id},
|
||||
}
|
||||
url := "https://jayce.api.mediatrack.cn/v4/assets/batch/move"
|
||||
_, err = driver.Request(url, base.Post, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Rename(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := "https://jayce.api.mediatrack.cn/v3/assets/" + srcFile.Id
|
||||
data := base.Json{
|
||||
"title": utils.Base(dst),
|
||||
}
|
||||
_, err = driver.Request(url, base.Put, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Copy(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{
|
||||
"parent_id": dstParentFile.Id,
|
||||
"ids": []string{srcFile.Id},
|
||||
}
|
||||
url := "https://jayce.api.mediatrack.cn/v4/assets/batch/clone"
|
||||
_, err = driver.Request(url, base.Post, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Delete(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"origin_id": parentFile.Id,
|
||||
"ids": []string{file.Id},
|
||||
}
|
||||
url := "https://jayce.api.mediatrack.cn/v4/assets/batch/delete"
|
||||
_, err = driver.Request(url, base.Delete, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver MediaTrack) 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
|
||||
}
|
||||
src := "assets/" + uuid.New().String()
|
||||
var resp UploadResp
|
||||
_, err = driver.Request("https://jayce.api.mediatrack.cn/v3/storage/tokens/asset", base.Get, nil, map[string]string{
|
||||
"src": src,
|
||||
}, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
credential := resp.Data.Credentials
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(credential.TmpSecretID, credential.TmpSecretKey, credential.Token),
|
||||
Region: &resp.Data.Region,
|
||||
Endpoint: aws.String("cos.accelerate.myqcloud.com"),
|
||||
}
|
||||
s, err := session.NewSession(cfg)
|
||||
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
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: &resp.Data.Bucket,
|
||||
Key: &resp.Data.Object,
|
||||
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, tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash := hex.EncodeToString(h.Sum(nil))
|
||||
data := base.Json{
|
||||
"category": 0,
|
||||
"description": file.GetFileName(),
|
||||
"hash": hash,
|
||||
"mime": file.MIMEType,
|
||||
"size": file.GetSize(),
|
||||
"src": src,
|
||||
"title": file.GetFileName(),
|
||||
"type": 0,
|
||||
}
|
||||
_, err = driver.Request(url, base.Post, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*MediaTrack)(nil)
|
||||
183
drivers/mediatrack/mediatrack.go
Normal file
183
drivers/mediatrack/mediatrack.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package mediatrack
|
||||
|
||||
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"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseResp struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type T struct {
|
||||
BaseResp
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Request(url string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+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)
|
||||
}
|
||||
var e BaseResp
|
||||
req.SetResult(&e)
|
||||
var err error
|
||||
var res *resty.Response
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
case base.Put:
|
||||
res, err = req.Put(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugln(res.String())
|
||||
if e.Status != "SUCCESS" {
|
||||
return nil, errors.New(e.Message)
|
||||
}
|
||||
if resp != nil {
|
||||
err = utils.Json.Unmarshal(res.Body(), resp)
|
||||
}
|
||||
return res.Body(), err
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Category int `json:"category"`
|
||||
ChildAssets []interface{} `json:"childAssets"`
|
||||
CommentCount int `json:"comment_count"`
|
||||
CoverAsset interface{} `json:"cover_asset"`
|
||||
CoverAssetID string `json:"cover_asset_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt string `json:"deleted_at"`
|
||||
Description string `json:"description"`
|
||||
File *struct {
|
||||
Cover string `json:"cover"`
|
||||
Src string `json:"src"`
|
||||
} `json:"file"`
|
||||
//FileID string `json:"file_id"`
|
||||
ID string `json:"id"`
|
||||
|
||||
Size string `json:"size"`
|
||||
Thumbnails []interface{} `json:"thumbnails"`
|
||||
Title string `json:"title"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (driver MediaTrack) formatFile(file *File) *model.File {
|
||||
f := model.File{
|
||||
Id: file.ID,
|
||||
Name: file.Title,
|
||||
Size: 0,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: &file.UpdatedAt,
|
||||
}
|
||||
if file.File == nil {
|
||||
// folder
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
size, _ := strconv.ParseInt(file.Size, 10, 64)
|
||||
f.Size = size
|
||||
f.Type = utils.GetFileType(path.Ext(file.Title))
|
||||
if file.File.Cover != "" {
|
||||
f.Thumbnail = "https://nano.mtres.cn/" + file.File.Cover
|
||||
}
|
||||
}
|
||||
return &f
|
||||
}
|
||||
|
||||
type ChildrenResp struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Total int `json:"total"`
|
||||
Assets []File `json:"assets"`
|
||||
} `json:"data"`
|
||||
Path string `json:"path"`
|
||||
TraceID string `json:"trace_id"`
|
||||
RequestID string `json:"requestId"`
|
||||
}
|
||||
|
||||
func (driver MediaTrack) GetFiles(parentId string, account *model.Account) ([]model.File, error) {
|
||||
files := make([]model.File, 0)
|
||||
url := fmt.Sprintf("https://jayce.api.mediatrack.cn/v4/assets/%s/children", parentId)
|
||||
sort := ""
|
||||
if account.OrderBy != "" {
|
||||
if account.OrderDirection == "true" {
|
||||
sort = "-"
|
||||
}
|
||||
sort += account.OrderBy
|
||||
}
|
||||
page := 1
|
||||
for {
|
||||
var resp ChildrenResp
|
||||
_, err := driver.Request(url, base.Get, nil, map[string]string{
|
||||
"page": strconv.Itoa(page),
|
||||
"size": "50",
|
||||
"sort": sort,
|
||||
}, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.Data.Assets) == 0 {
|
||||
break
|
||||
}
|
||||
page++
|
||||
for _, file := range resp.Data.Assets {
|
||||
files = append(files, *driver.formatFile(&file))
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Credentials struct {
|
||||
TmpSecretID string `json:"TmpSecretId"`
|
||||
TmpSecretKey string `json:"TmpSecretKey"`
|
||||
Token string `json:"Token"`
|
||||
ExpiredTime int `json:"ExpiredTime"`
|
||||
Expiration time.Time `json:"Expiration"`
|
||||
StartTime int `json:"StartTime"`
|
||||
} `json:"credentials"`
|
||||
Object string `json:"object"`
|
||||
Bucket string `json:"bucket"`
|
||||
Region string `json:"region"`
|
||||
URL string `json:"url"`
|
||||
Size string `json:"size"`
|
||||
} `json:"data"`
|
||||
Path string `json:"path"`
|
||||
TraceID string `json:"trace_id"`
|
||||
RequestID string `json:"requestId"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&MediaTrack{})
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package drivers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Native struct {
|
||||
}
|
||||
|
||||
func (n Native) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil,fmt.Errorf("no need")
|
||||
}
|
||||
|
||||
func (n Native) Items() []Item {
|
||||
return []Item{
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (n Native) Proxy(ctx *fiber.Ctx) {
|
||||
// unnecessary
|
||||
}
|
||||
|
||||
func (n Native) Save(account *model.Account, old *model.Account) error {
|
||||
log.Debugf("save a account: [%s]", account.Name)
|
||||
if !utils.Exists(account.RootFolder) {
|
||||
return fmt.Errorf("[%s] not exist", account.RootFolder)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO sort files
|
||||
func (n Native) Path(path string, account *model.Account) (*model.File, []*model.File, error) {
|
||||
fullPath := filepath.Join(account.RootFolder, path)
|
||||
log.Debugf("%s-%s-%s", account.RootFolder, path, fullPath)
|
||||
if !utils.Exists(fullPath) {
|
||||
return nil, nil, fmt.Errorf("path not found")
|
||||
}
|
||||
if utils.IsDir(fullPath) {
|
||||
result := make([]*model.File, 0)
|
||||
files, err := ioutil.ReadDir(fullPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, f := range files {
|
||||
if strings.HasPrefix(f.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
time := f.ModTime()
|
||||
file := &model.File{
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
Type: 0,
|
||||
UpdatedAt: &time,
|
||||
Driver: "Native",
|
||||
}
|
||||
if f.IsDir() {
|
||||
file.Type = conf.FOLDER
|
||||
} else {
|
||||
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
|
||||
}
|
||||
result = append(result, file)
|
||||
}
|
||||
return nil, result, nil
|
||||
}
|
||||
f, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
time := f.ModTime()
|
||||
file := &model.File{
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
Type: utils.GetFileType(filepath.Ext(f.Name())),
|
||||
UpdatedAt: &time,
|
||||
Driver: "Native",
|
||||
}
|
||||
return file, nil, nil
|
||||
}
|
||||
|
||||
func (n Native) Link(path string, account *model.Account) (string, error) {
|
||||
fullPath := filepath.Join(account.RootFolder, path)
|
||||
s, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if s.IsDir() {
|
||||
return "", fmt.Errorf("can't down folder")
|
||||
}
|
||||
return fullPath, nil
|
||||
}
|
||||
|
||||
var _ Driver = (*Native)(nil)
|
||||
|
||||
func init() {
|
||||
RegisterDriver("Native", &Native{})
|
||||
}
|
||||
265
drivers/native/driver.go
Normal file
265
drivers/native/driver.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package native
|
||||
|
||||
import (
|
||||
"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"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Native struct{}
|
||||
|
||||
func (driver Native) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Native",
|
||||
OnlyProxy: true,
|
||||
OnlyLocal: true,
|
||||
NoNeedSetLink: true,
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Native) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
_ = model.SaveAccount(account)
|
||||
return fmt.Errorf("[%s] not exist", account.RootFolder)
|
||||
}
|
||||
account.Status = "work"
|
||||
account.Proxy = true
|
||||
err := model.SaveAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
f, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
time := f.ModTime()
|
||||
file := &model.File{
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
UpdatedAt: &time,
|
||||
Driver: driver.Config().Name,
|
||||
}
|
||||
if f.IsDir() {
|
||||
file.Type = conf.FOLDER
|
||||
} else {
|
||||
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
|
||||
}
|
||||
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
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
rawFiles, err := ioutil.ReadDir(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range rawFiles {
|
||||
if strings.HasPrefix(f.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
time := f.ModTime()
|
||||
file := model.File{
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
Type: 0,
|
||||
UpdatedAt: &time,
|
||||
Driver: driver.Config().Name,
|
||||
}
|
||||
if f.IsDir() {
|
||||
file.Type = conf.FOLDER
|
||||
} else {
|
||||
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if s.IsDir() {
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
link := base.Link{
|
||||
FilePath: fullPath,
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Native) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
log.Debugf("native path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
//file.Url, _ = driver.Link(path, account)
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
//model.SortFiles(files, account)
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
func (driver Native) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstFile, err := driver.File(dst, account)
|
||||
if err == nil {
|
||||
if !dstFile.IsDir() {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
}
|
||||
if srcFile.IsDir() {
|
||||
return driver.CopyDir(fullSrc, fullDst)
|
||||
}
|
||||
return driver.CopyFile(fullSrc, fullDst)
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if file.IsDir() {
|
||||
return os.RemoveAll(fullPath)
|
||||
}
|
||||
return os.Remove(fullPath)
|
||||
}
|
||||
|
||||
func (driver Native) Upload(file *model.FileStream, account *model.Account) error {
|
||||
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 {
|
||||
// TODO overwrite?
|
||||
}
|
||||
basePath := filepath.Dir(fullPath)
|
||||
if !utils.Exists(basePath) {
|
||||
err := os.MkdirAll(basePath, 0744)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
out, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Native)(nil)
|
||||
74
drivers/native/native.go
Normal file
74
drivers/native/native.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package native
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
// File copies a single file from src to dst
|
||||
func (driver *Native) CopyFile(src, dst string) error {
|
||||
var err error
|
||||
var srcfd *os.File
|
||||
var dstfd *os.File
|
||||
var srcinfo os.FileInfo
|
||||
|
||||
if srcfd, err = os.Open(src); err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcfd.Close()
|
||||
|
||||
if dstfd, err = os.Create(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstfd.Close()
|
||||
|
||||
if _, err = io.Copy(dstfd, srcfd); err != nil {
|
||||
return err
|
||||
}
|
||||
if srcinfo, err = os.Stat(src); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(dst, srcinfo.Mode())
|
||||
}
|
||||
|
||||
// Dir copies a whole directory recursively
|
||||
func (driver *Native) CopyDir(src string, dst string) error {
|
||||
var err error
|
||||
var fds []os.FileInfo
|
||||
var srcinfo os.FileInfo
|
||||
|
||||
if srcinfo, err = os.Stat(src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fds, err = ioutil.ReadDir(src); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fd := range fds {
|
||||
srcfp := path.Join(src, fd.Name())
|
||||
dstfp := path.Join(dst, fd.Name())
|
||||
|
||||
if fd.IsDir() {
|
||||
if err = driver.CopyDir(srcfp, dstfp); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
} else {
|
||||
if err = driver.CopyFile(srcfp, dstfp); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Native{})
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
package drivers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/robfig/cron/v3"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Onedrive struct{}
|
||||
|
||||
var oneClient = resty.New()
|
||||
|
||||
type OnedriveHost struct {
|
||||
Oauth string
|
||||
Api string
|
||||
}
|
||||
|
||||
var onedriveHostMap = map[string]OnedriveHost{
|
||||
"global": {
|
||||
Oauth: "https://login.microsoftonline.com",
|
||||
Api: "https://graph.microsoft.com",
|
||||
},
|
||||
"cn": {
|
||||
Oauth: "https://login.chinacloudapi.cn",
|
||||
Api: "https://microsoftgraph.chinacloudapi.cn",
|
||||
},
|
||||
"us": {
|
||||
Oauth: "https://login.microsoftonline.us",
|
||||
Api: "https://graph.microsoft.us",
|
||||
},
|
||||
"de": {
|
||||
Oauth: "https://login.microsoftonline.de",
|
||||
Api: "https://graph.microsoft.de",
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterDriver("Onedrive", &Onedrive{})
|
||||
oneClient.SetRetryCount(3)
|
||||
}
|
||||
|
||||
func (o Onedrive) GetMetaUrl(account *model.Account, auth bool, path string) string {
|
||||
path = filepath.Join(account.RootFolder, path)
|
||||
host, _ := onedriveHostMap[account.Zone]
|
||||
if auth {
|
||||
return host.Oauth
|
||||
}
|
||||
switch account.OnedriveType {
|
||||
case "onedrive":
|
||||
{
|
||||
if path == "/" {
|
||||
return fmt.Sprintf("%s/v1.0/me/drive/root", host.Api)
|
||||
} else {
|
||||
return fmt.Sprintf("%s/v1.0/me/drive/root:%s:", host.Api, path)
|
||||
}
|
||||
}
|
||||
case "sharepoint":
|
||||
{
|
||||
if path == "/" {
|
||||
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root", host.Api, account.SiteId)
|
||||
} else {
|
||||
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root:%s:", host.Api, account.SiteId, path)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (o Onedrive) Items() []Item {
|
||||
return []Item{
|
||||
{
|
||||
Name: "zone",
|
||||
Label: "zone",
|
||||
Type: "select",
|
||||
Required: true,
|
||||
Values: "global,cn,us,de",
|
||||
Description: "",
|
||||
},
|
||||
{
|
||||
Name: "onedrive_type",
|
||||
Label: "onedrive type",
|
||||
Type: "select",
|
||||
Required: true,
|
||||
Values: "onedrive,sharepoint",
|
||||
},
|
||||
{
|
||||
Name: "client_id",
|
||||
Label: "client id",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "client_secret",
|
||||
Label: "client secret",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "redirect_uri",
|
||||
Label: "redirect uri",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "refresh_token",
|
||||
Label: "refresh token",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "site_id",
|
||||
Label: "site id",
|
||||
Type: "string",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: "string",
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type OneTokenErr struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
func (o Onedrive) RefreshToken(account *model.Account) error {
|
||||
url := o.GetMetaUrl(account, true, "") + "/common/oauth2/v2.0/token"
|
||||
var resp TokenResp
|
||||
var e OneTokenErr
|
||||
_, err := oneClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": account.ClientId,
|
||||
"client_secret": account.ClientSecret,
|
||||
"redirect_uri": account.RedirectUri,
|
||||
"refresh_token": account.RefreshToken,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Error != "" {
|
||||
return fmt.Errorf("%s", e.ErrorDescription)
|
||||
}
|
||||
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
|
||||
return nil
|
||||
}
|
||||
|
||||
type OneFile struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
LastModifiedDateTime *time.Time `json:"lastModifiedDateTime"`
|
||||
Url string `json:"@microsoft.graph.downloadUrl"`
|
||||
File struct {
|
||||
MimeType string `json:"mimeType"`
|
||||
} `json:"file"`
|
||||
}
|
||||
|
||||
type OneFiles struct {
|
||||
Value []OneFile `json:"value"`
|
||||
}
|
||||
|
||||
type OneRespErr struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (o Onedrive) FormatFile(file *OneFile) *model.File {
|
||||
f := &model.File{
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
UpdatedAt: file.LastModifiedDateTime,
|
||||
Driver: "OneDrive",
|
||||
Url: file.Url,
|
||||
}
|
||||
if file.File.MimeType == "" {
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
f.Type = utils.GetFileType(filepath.Ext(file.Name))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (o Onedrive) GetFiles(account *model.Account, path string) ([]OneFile, error) {
|
||||
var files OneFiles
|
||||
var e OneRespErr
|
||||
_, err := oneClient.R().SetResult(&files).SetError(&e).
|
||||
SetHeader("Authorization", "Bearer "+account.AccessToken).
|
||||
Get(o.GetMetaUrl(account, false, path) + "/children")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Error.Code != "" {
|
||||
return nil, fmt.Errorf("%s", e.Error.Message)
|
||||
}
|
||||
return files.Value, nil
|
||||
}
|
||||
|
||||
func (o 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(o.GetMetaUrl(account, false, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Error.Code != "" {
|
||||
return nil, fmt.Errorf("%s", e.Error.Message)
|
||||
}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (o Onedrive) Path(path string, account *model.Account) (*model.File, []*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
cache, err := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path))
|
||||
if err == nil {
|
||||
files, _ := cache.([]*model.File)
|
||||
return nil, files, nil
|
||||
}
|
||||
file, err := o.GetFile(account, path)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if file.File.MimeType != "" {
|
||||
return o.FormatFile(file), nil, nil
|
||||
} else {
|
||||
files, err := o.GetFiles(account, path)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
res := make([]*model.File, 0)
|
||||
for _, file := range files {
|
||||
res = append(res, o.FormatFile(&file))
|
||||
}
|
||||
_ = conf.Cache.Set(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path), res, nil)
|
||||
return nil, res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (o Onedrive) Link(path string, account *model.Account) (string, error) {
|
||||
file, err := o.GetFile(account, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if file.File.MimeType == "" {
|
||||
return "", fmt.Errorf("can't down folder")
|
||||
}
|
||||
return file.Url, nil
|
||||
}
|
||||
|
||||
func (o Onedrive) Save(account *model.Account, old *model.Account) error {
|
||||
_, 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 := o.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cronId, err := conf.Cron.AddFunc("@every 1h", func() {
|
||||
name := account.Name
|
||||
newAccount, ok := model.GetAccount(name)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err = o.RefreshToken(&newAccount)
|
||||
if err != nil {
|
||||
newAccount.Status = err.Error()
|
||||
}
|
||||
_ = model.SaveAccount(newAccount)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.CronId = int(cronId)
|
||||
err = model.SaveAccount(*account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o Onedrive) Proxy(ctx *fiber.Ctx) {
|
||||
|
||||
}
|
||||
|
||||
func (o Onedrive) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var _ Driver = (*Onedrive)(nil)
|
||||
278
drivers/onedrive/driver.go
Normal file
278
drivers/onedrive/driver.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package onedrive
|
||||
|
||||
import (
|
||||
"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"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Onedrive struct{}
|
||||
|
||||
func (driver Onedrive) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Onedrive",
|
||||
NoNeedSetLink: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Onedrive) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "zone",
|
||||
Label: "zone",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "global,cn,us,de",
|
||||
Description: "",
|
||||
},
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "onedrive type",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "onedrive,sharepoint",
|
||||
},
|
||||
{
|
||||
Name: "client_id",
|
||||
Label: "client id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "client_secret",
|
||||
Label: "client secret",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "redirect_uri",
|
||||
Label: "redirect uri",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "refresh_token",
|
||||
Label: "refresh token",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "site_id",
|
||||
Label: "site id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "name,size,lastModifiedDateTime",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "asc,desc",
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) 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 Onedrive) 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
|
||||
}
|
||||
rawFiles, err := driver.GetFiles(account, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.GetFile(account, args.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.File == nil {
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
link := base.Link{
|
||||
Url: file.Url,
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
log.Debugf("onedrive 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 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
|
||||
}
|
||||
|
||||
func (driver Onedrive) MakeDir(path string, account *model.Account) error {
|
||||
url := driver.GetMetaUrl(account, false, utils.Dir(path)) + "/children"
|
||||
data := base.Json{
|
||||
"name": utils.Base(path),
|
||||
"folder": base.Json{},
|
||||
"@microsoft.graph.conflictBehavior": "rename",
|
||||
}
|
||||
_, err := driver.Request(url, base.Post, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) Move(src string, dst string, account *model.Account) error {
|
||||
dstParentFile, err := driver.GetFile(account, utils.Dir(dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"parentReference": base.Json{
|
||||
"id": dstParentFile.Id,
|
||||
},
|
||||
"name": utils.Base(dst),
|
||||
}
|
||||
url := driver.GetMetaUrl(account, false, src)
|
||||
_, err = driver.Request(url, base.Patch, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver Onedrive) Copy(src string, dst string, account *model.Account) error {
|
||||
dstParentFile, err := driver.GetFile(account, utils.Dir(dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"parentReference": base.Json{
|
||||
"driveId": dstParentFile.ParentReference.DriveId,
|
||||
"id": dstParentFile.Id,
|
||||
},
|
||||
"name": utils.Base(dst),
|
||||
}
|
||||
url := driver.GetMetaUrl(account, false, src) + "/copy"
|
||||
_, err = driver.Request(url, base.Post, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) Delete(path string, account *model.Account) error {
|
||||
url := driver.GetMetaUrl(account, false, path)
|
||||
_, err := driver.Request(url, base.Delete, nil, nil, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
var err error
|
||||
if file.GetSize() <= 4*1024*1024 {
|
||||
err = driver.UploadSmall(file, account)
|
||||
} else {
|
||||
err = driver.UploadBig(file, account)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Onedrive)(nil)
|
||||
318
drivers/onedrive/onedrive.go
Normal file
318
drivers/onedrive/onedrive.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package onedrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Host struct {
|
||||
Oauth string
|
||||
Api string
|
||||
}
|
||||
|
||||
var onedriveHostMap = map[string]Host{
|
||||
"global": {
|
||||
Oauth: "https://login.microsoftonline.com",
|
||||
Api: "https://graph.microsoft.com",
|
||||
},
|
||||
"cn": {
|
||||
Oauth: "https://login.chinacloudapi.cn",
|
||||
Api: "https://microsoftgraph.chinacloudapi.cn",
|
||||
},
|
||||
"us": {
|
||||
Oauth: "https://login.microsoftonline.us",
|
||||
Api: "https://graph.microsoft.us",
|
||||
},
|
||||
"de": {
|
||||
Oauth: "https://login.microsoftonline.de",
|
||||
Api: "https://graph.microsoft.de",
|
||||
},
|
||||
}
|
||||
|
||||
func (driver Onedrive) GetMetaUrl(account *model.Account, auth bool, path string) string {
|
||||
path = filepath.Join(account.RootFolder, path)
|
||||
//log.Debugf(path)
|
||||
host, _ := onedriveHostMap[account.Zone]
|
||||
if auth {
|
||||
return host.Oauth
|
||||
}
|
||||
switch account.InternalType {
|
||||
case "onedrive":
|
||||
{
|
||||
if path == "/" || path == "\\" {
|
||||
return fmt.Sprintf("%s/v1.0/me/drive/root", host.Api)
|
||||
} else {
|
||||
return fmt.Sprintf("%s/v1.0/me/drive/root:%s:", host.Api, path)
|
||||
}
|
||||
}
|
||||
case "sharepoint":
|
||||
{
|
||||
if path == "/" || path == "\\" {
|
||||
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root", host.Api, account.SiteId)
|
||||
} else {
|
||||
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root:%s:", host.Api, account.SiteId, path)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type OneTokenErr struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
func (driver Onedrive) RefreshToken(account *model.Account) error {
|
||||
err := driver.refreshToken(account)
|
||||
if err != nil && err == base.ErrEmptyToken {
|
||||
return driver.refreshToken(account)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
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 := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": account.ClientId,
|
||||
"client_secret": account.ClientSecret,
|
||||
"redirect_uri": account.RedirectUri,
|
||||
"refresh_token": account.RefreshToken,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
return err
|
||||
}
|
||||
if e.Error != "" {
|
||||
account.Status = e.ErrorDescription
|
||||
return fmt.Errorf("%s", e.ErrorDescription)
|
||||
} else {
|
||||
account.Status = "work"
|
||||
}
|
||||
if resp.RefreshToken == "" {
|
||||
account.Status = base.ErrEmptyToken.Error()
|
||||
return base.ErrEmptyToken
|
||||
}
|
||||
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
|
||||
return nil
|
||||
}
|
||||
|
||||
type OneFile struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
LastModifiedDateTime *time.Time `json:"lastModifiedDateTime"`
|
||||
Url string `json:"@microsoft.graph.downloadUrl"`
|
||||
File *struct {
|
||||
MimeType string `json:"mimeType"`
|
||||
} `json:"file"`
|
||||
Thumbnails []struct {
|
||||
Medium struct {
|
||||
Url string `json:"url"`
|
||||
} `json:"medium"`
|
||||
} `json:"thumbnails"`
|
||||
ParentReference struct {
|
||||
DriveId string `json:"driveId"`
|
||||
} `json:"parentReference"`
|
||||
}
|
||||
|
||||
type OneFiles struct {
|
||||
Value []OneFile `json:"value"`
|
||||
NextLink string `json:"@odata.nextLink"`
|
||||
}
|
||||
|
||||
type OneRespErr struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (driver Onedrive) FormatFile(file *OneFile) *model.File {
|
||||
f := &model.File{
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
UpdatedAt: file.LastModifiedDateTime,
|
||||
Driver: driver.Config().Name,
|
||||
Url: file.Url,
|
||||
Id: file.Id,
|
||||
}
|
||||
if len(file.Thumbnails) > 0 {
|
||||
f.Thumbnail = file.Thumbnails[0].Medium.Url
|
||||
}
|
||||
if file.File == nil {
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
f.Type = utils.GetFileType(filepath.Ext(file.Name))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (driver Onedrive) GetFiles(account *model.Account, path string) ([]OneFile, error) {
|
||||
var res []OneFile
|
||||
nextLink := driver.GetMetaUrl(account, false, path) + "/children?$expand=thumbnails"
|
||||
if account.OrderBy != "" {
|
||||
nextLink += fmt.Sprintf("&orderby=%s", account.OrderBy)
|
||||
if account.OrderDirection != "" {
|
||||
nextLink += fmt.Sprintf("%%20%s", account.OrderDirection)
|
||||
}
|
||||
}
|
||||
for nextLink != "" {
|
||||
var files OneFiles
|
||||
_, 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)
|
||||
//}
|
||||
res = append(res, files.Value...)
|
||||
nextLink = files.NextLink
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) GetFile(account *model.Account, path string) (*OneFile, error) {
|
||||
var file OneFile
|
||||
//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)
|
||||
//}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) Request(url string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
rawUrl := url
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+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
|
||||
var e OneRespErr
|
||||
req.SetError(&e)
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(url)
|
||||
case base.Put:
|
||||
res, err = req.Put(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//log.Debug(res.String())
|
||||
if e.Error.Code != "" {
|
||||
if e.Error.Code == "InvalidAuthenticationToken" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
_ = model.SaveAccount(account)
|
||||
return nil, err
|
||||
}
|
||||
return driver.Request(rawUrl, method, headers, query, form, data, resp, account)
|
||||
}
|
||||
return nil, errors.New(e.Error.Message)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) UploadSmall(file *model.FileStream, account *model.Account) error {
|
||||
url := driver.GetMetaUrl(account, false, utils.Join(file.ParentPath, file.Name)) + "/content"
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request(url, base.Put, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) UploadBig(file *model.FileStream, account *model.Account) error {
|
||||
url := driver.GetMetaUrl(account, false, utils.Join(file.ParentPath, file.Name)) + "/createUploadSession"
|
||||
res, err := driver.Request(url, base.Post, nil, nil, nil, nil, nil, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
|
||||
var finish uint64 = 0
|
||||
const DEFAULT = 4 * 1024 * 1024
|
||||
for finish < file.GetSize() {
|
||||
log.Debugf("upload: %d", finish)
|
||||
var byteSize uint64 = DEFAULT
|
||||
left := file.GetSize() - finish
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(file, byteData)
|
||||
log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("PUT", uploadUrl, bytes.NewBuffer(byteData))
|
||||
req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, file.Size))
|
||||
finish += byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if res.StatusCode != 201 && res.StatusCode != 202 {
|
||||
data, _ := ioutil.ReadAll(res.Body)
|
||||
return errors.New(string(data))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Onedrive{})
|
||||
}
|
||||
103
drivers/operate/operate.go
Normal file
103
drivers/operate/operate.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package operate
|
||||
|
||||
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 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 {
|
||||
log.Debugf("mkdir: %s", path)
|
||||
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
|
||||
}
|
||||
var err error
|
||||
if rename {
|
||||
err = driver.Rename(src, dst, account)
|
||||
} else {
|
||||
err = driver.Move(src, dst, account)
|
||||
}
|
||||
if err == nil && clearCache {
|
||||
_ = base.DeleteCache(utils.Dir(src), account)
|
||||
if !rename {
|
||||
_ = 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
|
||||
}
|
||||
|
||||
func Upload(driver base.Driver, account *model.Account, file *model.FileStream, clearCache bool) error {
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
err := driver.Upload(file, account)
|
||||
if err == nil && clearCache {
|
||||
_ = base.DeleteCache(file.ParentPath, account)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("upload error: %s", err.Error())
|
||||
}
|
||||
debug.FreeOSMemory()
|
||||
return err
|
||||
}
|
||||
325
drivers/pikpak/driver.go
Normal file
325
drivers/pikpak/driver.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package pikpak
|
||||
|
||||
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/aws/aws-sdk-go/aws"
|
||||
"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"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PikPak struct{}
|
||||
|
||||
func (driver PikPak) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "PikPak",
|
||||
ApiProxy: true,
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver PikPak) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "username",
|
||||
Label: "username",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "password",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver PikPak) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
err := driver.Login(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) 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 PikPak) 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
|
||||
}
|
||||
rawFiles, err := driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver PikPak) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp File
|
||||
_, err = driver.Request(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.Id),
|
||||
base.Get, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{
|
||||
Url: resp.WebContentLink,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver PikPak) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("pikpak 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 PikPak) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver PikPak) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver PikPak) MakeDir(path string, account *model.Account) error {
|
||||
path = utils.ParsePath(path)
|
||||
dir, name := filepath.Split(path)
|
||||
parentFile, err := driver.File(dir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Post, nil, &base.Json{
|
||||
"kind": "drive#folder",
|
||||
"parent_id": parentFile.Id,
|
||||
"name": name,
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) Move(src string, dst string, account *model.Account) error {
|
||||
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.Request("https://api-drive.mypikpak.com/drive/v1/files:batchMove", base.Post, nil, &base.Json{
|
||||
"ids": []string{srcFile.Id},
|
||||
"to": base.Json{
|
||||
"parent_id": dstDirFile.Id,
|
||||
},
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) Rename(src string, dst string, account *model.Account) error {
|
||||
_, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files/"+srcFile.Id, base.Patch, nil, &base.Json{
|
||||
"name": dstName,
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) Copy(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstDirFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchCopy", base.Post, nil, &base.Json{
|
||||
"ids": []string{srcFile.Id},
|
||||
"to": base.Json{
|
||||
"parent_id": dstDirFile.Id,
|
||||
},
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchTrash", base.Post, nil, &base.Json{
|
||||
"ids": []string{file.Id},
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) 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
|
||||
}
|
||||
data := base.Json{
|
||||
"kind": "drive#file",
|
||||
"name": file.GetFileName(),
|
||||
"size": file.GetSize(),
|
||||
"hash": "1CF254FBC456E1B012CD45C546636AA62CF8350E",
|
||||
"upload_type": "UPLOAD_TYPE_RESUMABLE",
|
||||
"objProvider": base.Json{"provider": "UPLOAD_TYPE_UNKNOWN"},
|
||||
"parent_id": parentFile.Id,
|
||||
}
|
||||
res, err := driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Post, nil, &data, nil, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params := jsoniter.Get(res, "resumable").Get("params")
|
||||
endpoint := params.Get("endpoint").ToString()
|
||||
endpointS := strings.Split(endpoint, ".")
|
||||
endpoint = strings.Join(endpointS[1:], ".")
|
||||
accessKeyId := params.Get("access_key_id").ToString()
|
||||
accessKeySecret := params.Get("access_key_secret").ToString()
|
||||
securityToken := params.Get("security_token").ToString()
|
||||
key := params.Get("key").ToString()
|
||||
bucket := params.Get("bucket").ToString()
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyId, accessKeySecret, securityToken),
|
||||
Region: aws.String("pikpak"),
|
||||
Endpoint: &endpoint,
|
||||
}
|
||||
s, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
Body: file,
|
||||
}
|
||||
_, err = uploader.Upload(input)
|
||||
return err
|
||||
}
|
||||
|
||||
// use aliyun-oss-sdk
|
||||
//func (driver PikPak) 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
|
||||
// }
|
||||
// data := base.Json{
|
||||
// "kind": "drive#file",
|
||||
// "name": file.GetFileName(),
|
||||
// "size": file.GetSize(),
|
||||
// "hash": "1CF254FBC456E1B012CD45C546636AA62CF8350E",
|
||||
// "upload_type": "UPLOAD_TYPE_RESUMABLE",
|
||||
// "objProvider": base.Json{"provider": "UPLOAD_TYPE_UNKNOWN"},
|
||||
// "parent_id": parentFile.Id,
|
||||
// }
|
||||
// res, err := driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Post, nil, &data, nil, account)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// params := jsoniter.Get(res, "resumable").Get("params")
|
||||
// endpoint := params.Get("endpoint").ToString()
|
||||
// endpointS := strings.Split(endpoint, ".")
|
||||
// endpoint = strings.Join(endpointS[1:], ".")
|
||||
// accessKeyId := params.Get("access_key_id").ToString()
|
||||
// accessKeySecret := params.Get("access_key_secret").ToString()
|
||||
// securityToken := params.Get("security_token").ToString()
|
||||
// client, err := oss.New("https://"+endpoint, accessKeyId,
|
||||
// accessKeySecret, oss.SecurityToken(securityToken))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// bucket, err := client.Bucket(params.Get("bucket").ToString())
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// signedURL, err := bucket.SignURL(params.Get("key").ToString(), oss.HTTPPut, 60)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// err = bucket.PutObjectWithURL(signedURL, file)
|
||||
// return err
|
||||
//}
|
||||
|
||||
var _ base.Driver = (*PikPak)(nil)
|
||||
201
drivers/pikpak/pikpak.go
Normal file
201
drivers/pikpak/pikpak.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package pikpak
|
||||
|
||||
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"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RespErr struct {
|
||||
ErrorCode int `json:"error_code"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func (driver PikPak) Login(account *model.Account) error {
|
||||
url := "https://user.mypikpak.com/v1/auth/signin"
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
|
||||
"captcha_token": "",
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"username": account.Username,
|
||||
"password": account.Password,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if e.ErrorCode != 0 {
|
||||
account.Status = e.Error
|
||||
err = errors.New(e.Error)
|
||||
} else {
|
||||
data := res.Body()
|
||||
account.Status = "work"
|
||||
account.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
account.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver PikPak) RefreshToken(account *model.Account) error {
|
||||
url := "https://user.mypikpak.com/v1/auth/token"
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).
|
||||
SetHeader("user-agent", "").SetBody(base.Json{
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": account.RefreshToken,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 4126 {
|
||||
// refresh_token 失效,重新登陆
|
||||
return driver.Login(account)
|
||||
}
|
||||
account.Status = e.Error
|
||||
_ = model.SaveAccount(account)
|
||||
return errors.New(e.Error)
|
||||
}
|
||||
data := res.Body()
|
||||
account.Status = "work"
|
||||
account.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
account.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
log.Debugf("%s\n %+v", res.String(), account)
|
||||
_ = model.SaveAccount(account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver PikPak) Request(url string, method int, query map[string]string, data *base.Json, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
rawUrl := url
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+account.AccessToken)
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
if data != nil {
|
||||
req.SetBody(data)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var e RespErr
|
||||
req.SetError(&e)
|
||||
var res *resty.Response
|
||||
var err error
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 16 {
|
||||
// login / refresh token
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return driver.Request(rawUrl, method, query, data, resp, account)
|
||||
} else {
|
||||
return nil, errors.New(e.Error)
|
||||
}
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Id string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
ModifiedTime *time.Time `json:"modified_time"`
|
||||
Size string `json:"size"`
|
||||
ThumbnailLink string `json:"thumbnail_link"`
|
||||
WebContentLink string `json:"web_content_link"`
|
||||
}
|
||||
|
||||
func (driver PikPak) FormatFile(file *File) *model.File {
|
||||
size, _ := strconv.ParseInt(file.Size, 10, 64)
|
||||
f := &model.File{
|
||||
Id: file.Id,
|
||||
Name: file.Name,
|
||||
Size: size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: file.ModifiedTime,
|
||||
Thumbnail: file.ThumbnailLink,
|
||||
}
|
||||
if file.Kind == "drive#folder" {
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
f.Type = utils.GetFileType(filepath.Ext(file.Name))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type Files struct {
|
||||
Files []File `json:"files"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
}
|
||||
|
||||
func (driver PikPak) GetFiles(id string, account *model.Account) ([]File, error) {
|
||||
res := make([]File, 0)
|
||||
pageToken := "first"
|
||||
for pageToken != "" {
|
||||
if pageToken == "first" {
|
||||
pageToken = ""
|
||||
}
|
||||
query := map[string]string{
|
||||
"parent_id": id,
|
||||
"thumbnail_size": "SIZE_LARGE",
|
||||
"with_audit": "true",
|
||||
"limit": "100",
|
||||
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
|
||||
"page_token": pageToken,
|
||||
}
|
||||
var resp Files
|
||||
_, err := driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Get, query, nil, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("%+v", resp)
|
||||
pageToken = resp.NextPageToken
|
||||
res = append(res, resp.Files...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&PikPak{})
|
||||
}
|
||||
320
drivers/quark/driver.go
Normal file
320
drivers/quark/driver.go
Normal file
@@ -0,0 +1,320 @@
|
||||
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)
|
||||
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},
|
||||
},
|
||||
}, 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)
|
||||
266
drivers/quark/quark.go
Normal file
266
drivers/quark/quark.go
Normal file
@@ -0,0 +1,266 @@
|
||||
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/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
|
||||
}
|
||||
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.Count {
|
||||
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
134
drivers/quark/types.go
Normal 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
31
drivers/quark/util.go
Normal 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
|
||||
}
|
||||
289
drivers/s3/driver.go
Normal file
289
drivers/s3/driver.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package s3
|
||||
|
||||
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/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type S3 struct {
|
||||
}
|
||||
|
||||
func (driver S3) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "S3",
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver S3) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "bucket",
|
||||
Label: "Bucket",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "endpoint",
|
||||
Label: "Endpoint",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "region",
|
||||
Label: "Region",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "access_key",
|
||||
Label: "Access Key",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "access_secret",
|
||||
Label: "Access Secret",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "custom_host",
|
||||
Label: "Custom Host",
|
||||
Type: base.TypeString,
|
||||
},
|
||||
{
|
||||
Name: "limit",
|
||||
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",
|
||||
},
|
||||
{
|
||||
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
|
||||
}
|
||||
client, err := driver.NewSession(account)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
} else {
|
||||
sessionsMap[account.Name] = client
|
||||
account.Status = "work"
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver S3) 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 S3) 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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
return files, err
|
||||
}
|
||||
|
||||
func (driver S3) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
client, err := driver.GetClient(account, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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,
|
||||
}
|
||||
if account.CustomHost == "" {
|
||||
input.ResponseContentDisposition = &disposition
|
||||
}
|
||||
req, _ := client.GetObjectRequest(input)
|
||||
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
|
||||
}
|
||||
return &base.Link{
|
||||
Url: link,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver S3) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("s3 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 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
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver S3) Move(src string, dst string, account *model.Account) error {
|
||||
err := driver.Copy(src, dst, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return driver.Delete(src, account)
|
||||
}
|
||||
|
||||
func (driver S3) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver S3) Copy(src string, dst string, account *model.Account) error {
|
||||
client, err := driver.GetClient(account, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcKey := driver.GetKey(src, account, srcFile.IsDir())
|
||||
dstKey := driver.GetKey(dst, account, srcFile.IsDir())
|
||||
input := &s3.CopyObjectInput{
|
||||
Bucket: &account.Bucket,
|
||||
CopySource: &srcKey,
|
||||
Key: &dstKey,
|
||||
}
|
||||
_, err = client.CopyObject(input)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver S3) Delete(path string, account *model.Account) error {
|
||||
client, err := driver.GetClient(account, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := driver.GetKey(path, account, file.IsDir())
|
||||
input := &s3.DeleteObjectInput{
|
||||
Bucket: &account.Bucket,
|
||||
Key: &key,
|
||||
}
|
||||
_, err = client.DeleteObject(input)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver S3) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
s, ok := sessionsMap[account.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("can't find [%s] session", account.Name)
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
key := driver.GetKey(utils.Join(file.ParentPath, file.GetFileName()), account, false)
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: &account.Bucket,
|
||||
Key: &key,
|
||||
Body: file,
|
||||
}
|
||||
_, err := uploader.Upload(input)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*S3)(nil)
|
||||
195
drivers/s3/s3.go
Normal file
195
drivers/s3/s3.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package s3
|
||||
|
||||
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/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var sessionsMap map[string]*session.Session
|
||||
|
||||
func (driver S3) NewSession(account *model.Account) (*session.Session, error) {
|
||||
cfg := &aws.Config{
|
||||
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, 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 link && account.CustomHost != "" {
|
||||
cURL, err := url.Parse(account.CustomHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.Handlers.Build.PushBack(func(r *request.Request) {
|
||||
if r.HTTPRequest.Method != http.MethodGet {
|
||||
return
|
||||
}
|
||||
r.HTTPRequest.URL.Scheme = cURL.Scheme
|
||||
r.HTTPRequest.URL.Host = cURL.Host
|
||||
})
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
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, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
marker := ""
|
||||
for {
|
||||
input := &s3.ListObjectsInput{
|
||||
Bucket: &account.Bucket,
|
||||
Marker: &marker,
|
||||
Prefix: &prefix,
|
||||
Delimiter: aws.String("/"),
|
||||
}
|
||||
listObjectsResult, err := client.ListObjects(input)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 == 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 listObjectsResult.IsTruncated == nil {
|
||||
return nil, errors.New("IsTruncated nil")
|
||||
}
|
||||
if *listObjectsResult.IsTruncated {
|
||||
marker = *listObjectsResult.NextMarker
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
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 == 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, "/")
|
||||
if path != "" && dir {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func init() {
|
||||
sessionsMap = make(map[string]*session.Session)
|
||||
base.RegisterDriver(&S3{})
|
||||
}
|
||||
278
drivers/shandian/driver.go
Normal file
278
drivers/shandian/driver.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package shandian
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Shandian struct{}
|
||||
|
||||
func (driver Shandian) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "ShandianPan",
|
||||
NoNeedSetLink: true,
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Shandian) 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,
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Shandian) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if account.RootFolder == "" {
|
||||
account.RootFolder = "0"
|
||||
}
|
||||
return driver.Login(account)
|
||||
}
|
||||
|
||||
func (driver Shandian) 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 Shandian) 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
|
||||
}
|
||||
rawFiles, err := driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Shandian) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
log.Debugf("shandian link")
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var e Resp
|
||||
res, err := base.NoRedirectClient.R().SetError(&e).SetHeader("Accept", "application/json").SetQueryParams(map[string]string{
|
||||
"id": file.Id,
|
||||
"token": account.AccessToken,
|
||||
}).Get("https://shandianpan.com/api/pan/file-download")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Code != 0 {
|
||||
if e.Code == 10 {
|
||||
err = driver.Login(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return driver.Link(args, account)
|
||||
}
|
||||
return nil, errors.New(e.Msg)
|
||||
}
|
||||
return &base.Link{
|
||||
Url: res.Header().Get("location"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver Shandian) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("shandian 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 Shandian) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver Shandian) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Shandian) MakeDir(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"id": parentFile.Id,
|
||||
"name": utils.Base(path),
|
||||
}
|
||||
_, err = driver.Post("https://shandianpan.com/api/pan/mkdir", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Shandian) 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 := map[string]interface{}{
|
||||
"id": srcFile.Id,
|
||||
"to_id": dstParentFile.Id,
|
||||
}
|
||||
_, err = driver.Post("https://shandianpan.com/api/pan/move", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Shandian) Rename(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"id": srcFile.Id,
|
||||
"name": utils.Base(dst),
|
||||
}
|
||||
_, err = driver.Post("https://shandianpan.com/api/pan/change", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Shandian) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Shandian) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"id": file.Id,
|
||||
}
|
||||
_, err = driver.Post("https://shandianpan.com/api/pan/recycle-in", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Shandian) 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
|
||||
}
|
||||
var resp UploadResp
|
||||
parentId, err := strconv.Atoi(parentFile.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"id": parentId,
|
||||
"name": file.GetFileName(),
|
||||
}
|
||||
res, err := driver.Post("https://shandianpan.com/api/pan/upload", data, nil, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = utils.Json.Unmarshal(res, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
if resp.Code == 10 {
|
||||
err = driver.Login(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return driver.Upload(file, account)
|
||||
}
|
||||
return errors.New(resp.Msg)
|
||||
}
|
||||
var r Resp
|
||||
_, err = base.RestyClient.R().SetMultipartFormData(map[string]string{
|
||||
"token": account.AccessToken,
|
||||
"id": "0",
|
||||
"key": resp.Data.Key,
|
||||
"ossAccessKeyId": resp.Data.Accessid,
|
||||
"policy": resp.Data.Policy,
|
||||
"signature": resp.Data.Signature,
|
||||
"callback": resp.Data.Callback,
|
||||
}).SetMultipartField("file", file.GetFileName(), file.GetMIMEType(), file).
|
||||
SetResult(&r).SetError(&r).Post("https:" + resp.Data.Host + "/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Code == 0 {
|
||||
return nil
|
||||
}
|
||||
return errors.New(r.Msg)
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Shandian)(nil)
|
||||
150
drivers/shandian/shandian.go
Normal file
150
drivers/shandian/shandian.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package shandian
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Resp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type LoginResp struct {
|
||||
Resp
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func (driver Shandian) Login(account *model.Account) error {
|
||||
var resp LoginResp
|
||||
_, err := base.RestyClient.R().SetResult(&resp).SetHeader("Accept", "application/json").SetBody(base.Json{
|
||||
"mobile": account.Username,
|
||||
"password": account.Password,
|
||||
"smscode": "",
|
||||
}).Post("https://shandianpan.com/api/login")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
account.Status = resp.Msg
|
||||
err = errors.New(resp.Msg)
|
||||
} else {
|
||||
account.Status = "work"
|
||||
account.AccessToken = resp.Data.Token
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Id int64 `json:"id"`
|
||||
Type int `json:"type"`
|
||||
Name string `json:"name"`
|
||||
UpdateTime int64 `json:"update_time"`
|
||||
Size int64 `json:"size"`
|
||||
Ext string `json:"ext"`
|
||||
}
|
||||
|
||||
func (driver Shandian) FormatFile(file *File) *model.File {
|
||||
t := time.Unix(file.UpdateTime, 0)
|
||||
f := &model.File{
|
||||
Id: strconv.FormatInt(file.Id, 10),
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: &t,
|
||||
}
|
||||
if file.Type == 1 {
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
f.Type = utils.GetFileType(file.Ext)
|
||||
if file.Ext != "" {
|
||||
f.Name += "." + file.Ext
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (driver Shandian) Post(url string, data map[string]interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Accept", "application/json")
|
||||
data["token"] = account.AccessToken
|
||||
req.SetBody(data)
|
||||
var e Resp
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
} else {
|
||||
req.SetResult(&e)
|
||||
}
|
||||
req.SetError(&e)
|
||||
res, err := req.Post(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if e.Code != 0 {
|
||||
if e.Code == 10 {
|
||||
err = driver.Login(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return driver.Post(url, data, resp, account)
|
||||
}
|
||||
return nil, errors.New(e.Msg)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
type FilesResp struct {
|
||||
Resp
|
||||
Data []File `json:"data"`
|
||||
}
|
||||
|
||||
func (driver Shandian) GetFiles(id string, account *model.Account) ([]File, error) {
|
||||
// TODO page not wok
|
||||
//res := make([]File, 0)
|
||||
page := 1
|
||||
//for {
|
||||
data := map[string]interface{}{
|
||||
"id": id,
|
||||
"page": page,
|
||||
"page_size": 100,
|
||||
}
|
||||
var resp FilesResp
|
||||
_, err := driver.Post("https://shandianpan.com/api/pan", data, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//res = append(res, resp.Data...)
|
||||
// if len(resp.Data) == 0 {
|
||||
// break
|
||||
// }
|
||||
//}
|
||||
//return res, nil
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
Resp
|
||||
Data struct {
|
||||
Accessid string `json:"accessid"`
|
||||
Policy string `json:"policy"`
|
||||
Expire int `json:"expire"`
|
||||
Callback string `json:"callback"`
|
||||
Key string `json:"key"`
|
||||
Host string `json:"host"`
|
||||
Signature string `json:"signature"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Shandian{})
|
||||
}
|
||||
286
drivers/teambition/driver.go
Normal file
286
drivers/teambition/driver.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package teambition
|
||||
|
||||
import (
|
||||
"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"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Teambition struct{}
|
||||
|
||||
func (driver Teambition) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Teambition",
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Teambition) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "Teambition type",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "China,International",
|
||||
},
|
||||
{
|
||||
Name: "access_token",
|
||||
Label: "Cookie",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "Unknown expiration time",
|
||||
},
|
||||
{
|
||||
Name: "zone",
|
||||
Label: "Project id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "fileName,fileSize,updated,created",
|
||||
Required: true,
|
||||
Default: "fileName",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
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
|
||||
}
|
||||
|
||||
func (driver Teambition) 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 Teambition) 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 Teambition) 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
|
||||
}
|
||||
url := file.Url
|
||||
res, err := base.NoRedirectClient.R().Get(url)
|
||||
if res.StatusCode() == 302 {
|
||||
url = res.Header().Get("location")
|
||||
}
|
||||
return &base.Link{Url: url}, nil
|
||||
}
|
||||
|
||||
func (driver Teambition) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("teambition 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 Teambition) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver Teambition) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Teambition) MakeDir(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"objectType": "collection",
|
||||
"_projectId": account.Zone,
|
||||
"_creatorId": "",
|
||||
"created": "",
|
||||
"updated": "",
|
||||
"title": utils.Base(path),
|
||||
"color": "blue",
|
||||
"description": "",
|
||||
"workCount": 0,
|
||||
"collectionType": "",
|
||||
"recentWorks": []interface{}{},
|
||||
"_parentId": parentFile.Id,
|
||||
"subCount": nil,
|
||||
}
|
||||
_, err = driver.Request("/api/collections", base.Post, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Teambition) 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
|
||||
}
|
||||
pre := "/api/works/"
|
||||
if srcFile.IsDir() {
|
||||
pre = "/api/collections/"
|
||||
}
|
||||
_, err = driver.Request(pre+srcFile.Id+"/move", base.Put, nil, nil, nil, &base.Json{
|
||||
"_parentId": dstParentFile.Id,
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Teambition) Rename(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pre := "/api/works/"
|
||||
data := base.Json{
|
||||
"fileName": utils.Base(dst),
|
||||
}
|
||||
if srcFile.IsDir() {
|
||||
pre = "/api/collections/"
|
||||
data = base.Json{
|
||||
"title": utils.Base(dst),
|
||||
}
|
||||
}
|
||||
_, err = driver.Request(pre+srcFile.Id, base.Put, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Teambition) Copy(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
|
||||
}
|
||||
pre := "/api/works/"
|
||||
if srcFile.IsDir() {
|
||||
pre = "/api/collections/"
|
||||
}
|
||||
_, err = driver.Request(pre+srcFile.Id+"/fork", base.Put, nil, nil, nil, &base.Json{
|
||||
"_parentId": dstParentFile.Id,
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Teambition) Delete(path string, account *model.Account) error {
|
||||
srcFile, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pre := "/api/works/"
|
||||
if srcFile.IsDir() {
|
||||
pre = "/api/collections/"
|
||||
}
|
||||
_, err = driver.Request(pre+srcFile.Id+"/archive", base.Post, nil, nil, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Teambition) Upload(file *model.FileStream, account *model.Account) error {
|
||||
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":"", "","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)
|
||||
237
drivers/teambition/teambition.go
Normal file
237
drivers/teambition/teambition.go
Normal file
@@ -0,0 +1,237 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type ErrResp struct {
|
||||
Name string `json:"name"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (driver Teambition) Request(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
url := "https://www.teambition.com" + pathname
|
||||
if account.InternalType == "International" {
|
||||
url = "https://us.teambition.com" + pathname
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Cookie", 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 e ErrResp
|
||||
var err error
|
||||
var res *resty.Response
|
||||
req.SetError(&e)
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
case base.Put:
|
||||
res, err = req.Put(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Name != "" {
|
||||
return nil, errors.New(e.Message)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (driver Teambition) GetFiles(parentId string, account *model.Account) ([]model.File, error) {
|
||||
files := make([]model.File, 0)
|
||||
page := 1
|
||||
for {
|
||||
var collections []Collection
|
||||
_, err := driver.Request("/api/collections", base.Get, nil, map[string]string{
|
||||
"_parentId": parentId,
|
||||
"_projectId": account.Zone,
|
||||
"order": account.OrderBy + account.OrderDirection,
|
||||
"count": "50",
|
||||
"page": strconv.Itoa(page),
|
||||
}, nil, nil, &collections, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(collections) == 0 {
|
||||
break
|
||||
}
|
||||
page++
|
||||
for _, collection := range collections {
|
||||
if collection.Title == "" {
|
||||
continue
|
||||
}
|
||||
files = append(files, model.File{
|
||||
Id: collection.ID,
|
||||
Name: collection.Title,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: collection.Updated,
|
||||
})
|
||||
}
|
||||
}
|
||||
page = 1
|
||||
for {
|
||||
var works []Work
|
||||
_, err := driver.Request("/api/works", base.Get, nil, map[string]string{
|
||||
"_parentId": parentId,
|
||||
"_projectId": account.Zone,
|
||||
"order": account.OrderBy + account.OrderDirection,
|
||||
"count": "50",
|
||||
"page": strconv.Itoa(page),
|
||||
}, nil, nil, &works, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(works) == 0 {
|
||||
break
|
||||
}
|
||||
page++
|
||||
for _, work := range works {
|
||||
files = append(files, model.File{
|
||||
Id: work.ID,
|
||||
Name: work.FileName,
|
||||
Size: work.FileSize,
|
||||
Type: utils.GetFileType(path.Ext(work.FileName)),
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: work.Updated,
|
||||
Thumbnail: work.Thumbnail,
|
||||
Url: work.DownloadURL,
|
||||
})
|
||||
}
|
||||
}
|
||||
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{})
|
||||
}
|
||||
63
drivers/teambition/types.go
Normal file
63
drivers/teambition/types.go
Normal 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"`
|
||||
}
|
||||
18
drivers/teambition/util.go
Normal file
18
drivers/teambition/util.go
Normal 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
|
||||
}
|
||||
149
drivers/template/driver.go
Normal file
149
drivers/template/driver.go
Normal file
@@ -0,0 +1,149 @@
|
||||
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 {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
90
drivers/template/template.go
Normal file
90
drivers/template/template.go
Normal 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
18
drivers/template/types.go
Normal 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
3
drivers/template/util.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package template
|
||||
|
||||
// write util func here, such as cal sign
|
||||
237
drivers/uss/driver.go
Normal file
237
drivers/uss/driver.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package uss
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/upyun/go-sdk/v3/upyun"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type USS struct {
|
||||
}
|
||||
|
||||
func (driver USS) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "USS",
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver USS) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "bucket",
|
||||
Label: "Bucket",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "endpoint",
|
||||
Label: "Endpoint",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "access_key",
|
||||
Label: "Operator Name",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "access_secret",
|
||||
Label: "Operator Password",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "custom_host",
|
||||
Label: "Custom Host",
|
||||
Type: base.TypeString,
|
||||
},
|
||||
{
|
||||
Name: "limit",
|
||||
Label: "Sign url expire time(hours)",
|
||||
Type: base.TypeNumber,
|
||||
Default: "4",
|
||||
Description: "default 4 hours",
|
||||
},
|
||||
//{
|
||||
// Name: "zone",
|
||||
// Label: "placeholder filename",
|
||||
// Type: base.TypeString,
|
||||
// Description: "default empty string",
|
||||
//},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver USS) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if account.Limit == 0 {
|
||||
account.Limit = 4
|
||||
}
|
||||
client, err := driver.NewUpYun(account)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
} else {
|
||||
clientsMap[account.Name] = client
|
||||
account.Status = "work"
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver USS) 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 USS) 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 {
|
||||
files, err = driver.List(path, account)
|
||||
if err == nil && len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
}
|
||||
return files, err
|
||||
}
|
||||
|
||||
func (driver USS) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
key := driver.GetKey(args.Path, account, false)
|
||||
host := account.CustomHost
|
||||
if host == "" {
|
||||
host = account.Endpoint
|
||||
}
|
||||
if strings.Contains(host, "://") {
|
||||
host = "https://" + host
|
||||
}
|
||||
u := fmt.Sprintf("%s/%s", host, key)
|
||||
downExp := time.Hour * time.Duration(account.Limit)
|
||||
expireAt := time.Now().Add(downExp).Unix()
|
||||
upd := url.QueryEscape(utils.Base(args.Path))
|
||||
signStr := strings.Join([]string{account.AccessSecret, fmt.Sprint(expireAt), fmt.Sprintf("/%s", key)}, "&")
|
||||
upt := utils.GetMD5Encode(signStr)[12:20] + fmt.Sprint(expireAt)
|
||||
link := fmt.Sprintf("%s?_upd=%s&_upt=%s", u, upd, upt)
|
||||
return &base.Link{Url: link}, nil
|
||||
}
|
||||
|
||||
func (driver USS) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("s3 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 USS) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver USS) MakeDir(path string, account *model.Account) error {
|
||||
client, err := driver.GetClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Mkdir(driver.GetKey(path, account, true))
|
||||
}
|
||||
|
||||
func (driver USS) Move(src string, dst string, account *model.Account) error {
|
||||
client, err := driver.GetClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Move(&upyun.MoveObjectConfig{
|
||||
SrcPath: driver.GetKey(src, account, false),
|
||||
DestPath: driver.GetKey(dst, account, false),
|
||||
})
|
||||
}
|
||||
|
||||
func (driver USS) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver USS) Copy(src string, dst string, account *model.Account) error {
|
||||
client, err := driver.GetClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Copy(&upyun.CopyObjectConfig{
|
||||
SrcPath: driver.GetKey(src, account, false),
|
||||
DestPath: driver.GetKey(dst, account, false),
|
||||
})
|
||||
}
|
||||
|
||||
func (driver USS) Delete(path string, account *model.Account) error {
|
||||
client, err := driver.GetClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Delete(&upyun.DeleteObjectConfig{
|
||||
Path: driver.GetKey(path, account, false),
|
||||
Async: false,
|
||||
})
|
||||
}
|
||||
|
||||
func (driver USS) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
client, err := driver.GetClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Put(&upyun.PutObjectConfig{
|
||||
Path: driver.GetKey(utils.Join(file.ParentPath, file.GetFileName()), account, false),
|
||||
Reader: file,
|
||||
})
|
||||
}
|
||||
|
||||
var _ base.Driver = (*USS)(nil)
|
||||
82
drivers/uss/uss.go
Normal file
82
drivers/uss/uss.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package uss
|
||||
|
||||
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/upyun/go-sdk/v3/upyun"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var clientsMap map[string]*upyun.UpYun
|
||||
|
||||
func (driver USS) NewUpYun(account *model.Account) (*upyun.UpYun, error) {
|
||||
return upyun.NewUpYun(&upyun.UpYunConfig{
|
||||
Bucket: account.Bucket,
|
||||
Operator: account.AccessKey,
|
||||
Password: account.AccessToken,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (driver USS) GetClient(account *model.Account) (*upyun.UpYun, error) {
|
||||
client, ok := clientsMap[account.Name]
|
||||
if ok {
|
||||
return client, nil
|
||||
}
|
||||
return nil, errors.New("can't get client")
|
||||
}
|
||||
|
||||
func (driver USS) List(prefix string, account *model.Account) ([]model.File, error) {
|
||||
prefix = driver.GetKey(prefix, account, true)
|
||||
client, err := driver.GetClient(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objsChan := make(chan *upyun.FileInfo, 10)
|
||||
defer close(objsChan)
|
||||
go func() {
|
||||
err = client.List(&upyun.GetObjectsConfig{
|
||||
Path: prefix,
|
||||
ObjectsChan: objsChan,
|
||||
MaxListObjects: 0,
|
||||
MaxListLevel: 1,
|
||||
})
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make([]model.File, 0)
|
||||
for obj := range objsChan {
|
||||
t := obj.Time
|
||||
f := model.File{
|
||||
Name: obj.Name,
|
||||
Size: obj.Size,
|
||||
UpdatedAt: &t,
|
||||
Driver: driver.Config().Name,
|
||||
}
|
||||
if obj.IsDir {
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
f.Type = utils.GetFileType(path.Ext(obj.Name))
|
||||
}
|
||||
res = append(res, f)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (driver USS) GetKey(path string, account *model.Account, dir bool) string {
|
||||
path = utils.Join(account.RootFolder, path)
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
if dir {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func init() {
|
||||
clientsMap = make(map[string]*upyun.UpYun)
|
||||
base.RegisterDriver(&USS{})
|
||||
}
|
||||
185
drivers/webdav/driver.go
Normal file
185
drivers/webdav/driver.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package webdav
|
||||
|
||||
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 WebDav struct{}
|
||||
|
||||
func (driver WebDav) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "WebDav",
|
||||
OnlyProxy: true,
|
||||
OnlyLocal: true,
|
||||
NoNeedSetLink: true,
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver WebDav) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "site_url",
|
||||
Label: "webdav root url",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "username",
|
||||
Label: "username",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "password",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver WebDav) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
account.Status = "work"
|
||||
_ = model.SaveAccount(account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver WebDav) File(path string, account *model.Account) (*model.File, error) {
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: "/",
|
||||
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 WebDav) 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
|
||||
}
|
||||
c := driver.NewClient(account)
|
||||
rawFiles, err := c.ReadDir(driver.WebDavPath(path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
if len(rawFiles) == 0 {
|
||||
return files, nil
|
||||
}
|
||||
for _, f := range rawFiles {
|
||||
t := f.ModTime()
|
||||
file := model.File{
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: &t,
|
||||
}
|
||||
if f.IsDir() {
|
||||
file.Type = conf.FOLDER
|
||||
} else {
|
||||
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
_ = base.SetCache(path, files, account)
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver WebDav) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
path := args.Path
|
||||
c := driver.NewClient(account)
|
||||
reader, err := c.ReadStream(driver.WebDavPath(path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{Data: reader}, nil
|
||||
}
|
||||
|
||||
func (driver WebDav) 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 WebDav) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver WebDav) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver WebDav) MakeDir(path string, account *model.Account) error {
|
||||
c := driver.NewClient(account)
|
||||
err := c.MkdirAll(driver.WebDavPath(path), 0644)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver WebDav) Move(src string, dst string, account *model.Account) error {
|
||||
c := driver.NewClient(account)
|
||||
err := c.Rename(driver.WebDavPath(src), driver.WebDavPath(dst), true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver WebDav) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver WebDav) Copy(src string, dst string, account *model.Account) error {
|
||||
c := driver.NewClient(account)
|
||||
err := c.Copy(driver.WebDavPath(src), driver.WebDavPath(dst), true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver WebDav) Delete(path string, account *model.Account) error {
|
||||
c := driver.NewClient(account)
|
||||
err := c.RemoveAll(driver.WebDavPath(path))
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver WebDav) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
c := driver.NewClient(account)
|
||||
path := utils.Join(file.ParentPath, file.Name)
|
||||
err := c.WriteStream(driver.WebDavPath(path), file, 0644)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*WebDav)(nil)
|
||||
23
drivers/webdav/webdav.go
Normal file
23
drivers/webdav/webdav.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/studio-b12/gowebdav"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (driver WebDav) NewClient(account *model.Account) *gowebdav.Client {
|
||||
return gowebdav.NewClient(account.SiteUrl, account.Username, account.Password)
|
||||
}
|
||||
|
||||
func (driver WebDav) WebDavPath(path string) string {
|
||||
path = utils.ParsePath(path)
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
return path
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&WebDav{})
|
||||
}
|
||||
282
drivers/xunlei/driver.go
Normal file
282
drivers/xunlei/driver.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package xunlei
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type XunLeiCloud struct{}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(new(XunLeiCloud))
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "XunLeiCloud",
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) 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,
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
return GetState(account).Login(account)
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) 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 XunLeiCloud) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ := cache.([]model.File)
|
||||
return files, nil
|
||||
}
|
||||
file, err := driver.File(utils.ParsePath(path), account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fileList FileList
|
||||
u := fmt.Sprintf("https://api-pan.xunlei.com/drive/v1/files?parent_id=%s&page_token=%s&with_audit=true&filters=%s", file.Id, "", url.QueryEscape(`{"phase": {"eq": "PHASE_TYPE_COMPLETE"}, "trashed":{"eq":false}}`))
|
||||
if err = GetState(account).Request("GET", u, nil, &fileList, account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files := make([]model.File, 0, len(fileList.Files))
|
||||
for _, file := range fileList.Files {
|
||||
if file.Kind == FOLDER || (file.Kind == FILE && file.Audit.Status == "STATUS_OK") {
|
||||
files = append(files, *driver.formatFile(&file))
|
||||
}
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) formatFile(file *Files) *model.File {
|
||||
size, _ := strconv.ParseInt(file.Size, 10, 64)
|
||||
tp := conf.FOLDER
|
||||
if file.Kind == FILE {
|
||||
tp = utils.GetFileType(file.FileExtension)
|
||||
}
|
||||
return &model.File{
|
||||
Id: file.ID,
|
||||
Name: file.Name,
|
||||
Size: size,
|
||||
Type: tp,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: file.CreatedTime,
|
||||
Thumbnail: file.ThumbnailLink,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) 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
|
||||
}
|
||||
var lFile Files
|
||||
if err = GetState(account).Request("GET", fmt.Sprintf("https://api-pan.xunlei.com/drive/v1/files/%s?&with_audit=true", file.Id), nil, &lFile, account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: base.UserAgent},
|
||||
},
|
||||
Url: lFile.WebContentLink,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("xunlei 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 XunLeiCloud) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) 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
|
||||
}
|
||||
return GetState(account).Request("POST", "https://api-pan.xunlei.com/drive/v1/files", &base.Json{"kind": FOLDER, "name": name, "parent_id": parentFile.Id}, nil, account)
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) 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
|
||||
}
|
||||
return GetState(account).Request("POST", "https://api-pan.xunlei.com/drive/v1/files:batchMove", &base.Json{"to": base.Json{"parent_id": dstDirFile.Id}, "ids": []string{srcFile.Id}}, nil, account)
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) 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
|
||||
}
|
||||
return GetState(account).Request("POST", "https://api-pan.xunlei.com/drive/v1/files:batchCopy", &base.Json{"to": base.Json{"parent_id": dstDirFile.Id}, "ids": []string{srcFile.Id}}, nil, account)
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) Delete(path string, account *model.Account) error {
|
||||
srcFile, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return GetState(account).Request("PATCH", fmt.Sprintf("https://api-pan.xunlei.com/drive/v1/files/%s/trash", srcFile.Id), &base.Json{}, nil, account)
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) 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 tempFile.Close()
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
gcid, err := getGcid(io.TeeReader(file, tempFile), int64(file.Size))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rep UploadTaskResponse
|
||||
err = GetState(account).Request("POST", "https://api-pan.xunlei.com/drive/v1/files", &base.Json{
|
||||
"kind": FILE,
|
||||
"parent_id": parentFile.Id,
|
||||
"name": file.Name,
|
||||
"size": fmt.Sprint(file.Size),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
}, &rep, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
param := rep.Resumable.Params
|
||||
client, err := oss.New(param.Endpoint, param.AccessKeyID, param.AccessKeySecret, oss.SecurityToken(param.SecurityToken), oss.EnableMD5(true), oss.HTTPClient(xunleiClient.GetClient()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bucket, err := client.Bucket(param.Bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.UploadFile(param.Key, tempFile.Name(), 4*1024*1024, oss.Routines(3), oss.Checkpoint(true, ""), oss.Expires(param.Expiration))
|
||||
}
|
||||
|
||||
func (driver XunLeiCloud) Rename(src string, dst string, account *model.Account) error {
|
||||
_, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return GetState(account).Request("PATCH", fmt.Sprintf("https://api-pan.xunlei.com/drive/v1/files/%s", srcFile.Id), &base.Json{"name": dstName}, nil, account)
|
||||
}
|
||||
|
||||
var _ base.Driver = (*XunLeiCloud)(nil)
|
||||
154
drivers/xunlei/types.go
Normal file
154
drivers/xunlei/types.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package xunlei
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Erron struct {
|
||||
Error string `json:"error"`
|
||||
ErrorCode int64 `json:"error_code"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
// ErrorDetails interface{} `json:"error_details"`
|
||||
}
|
||||
|
||||
type CaptchaTokenRequest struct {
|
||||
Action string `json:"action"`
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
ClientID string `json:"client_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Meta map[string]string `json:"meta"`
|
||||
//RedirectUri string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type CaptchaTokenResponse struct {
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
TokenType string `json:"token_type"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
|
||||
Sub string `json:"sub"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type SignInRequest struct {
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type FileList struct {
|
||||
Kind string `json:"kind"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
Files []Files `json:"files"`
|
||||
Version string `json:"version"`
|
||||
VersionOutdated bool `json:"version_outdated"`
|
||||
}
|
||||
|
||||
type Files struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
ParentID string `json:"parent_id"`
|
||||
Name string `json:"name"`
|
||||
UserID string `json:"user_id"`
|
||||
Size string `json:"size"`
|
||||
Revision string `json:"revision"`
|
||||
FileExtension string `json:"file_extension"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Starred bool `json:"starred"`
|
||||
WebContentLink string `json:"web_content_link"`
|
||||
CreatedTime *time.Time `json:"created_time"`
|
||||
ModifiedTime *time.Time `json:"modified_time"`
|
||||
IconLink string `json:"icon_link"`
|
||||
ThumbnailLink string `json:"thumbnail_link"`
|
||||
Md5Checksum string `json:"md5_checksum"`
|
||||
Hash string `json:"hash"`
|
||||
//Links struct{} `json:"links"`
|
||||
Phase string `json:"phase"`
|
||||
Audit struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Title string `json:"title"`
|
||||
} `json:"audit"`
|
||||
/* Medias []struct {
|
||||
Category string `json:"category"`
|
||||
IconLink string `json:"icon_link"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsOrigin bool `json:"is_origin"`
|
||||
IsVisible bool `json:"is_visible"`
|
||||
//Link interface{} `json:"link"`
|
||||
MediaID string `json:"media_id"`
|
||||
MediaName string `json:"media_name"`
|
||||
NeedMoreQuota bool `json:"need_more_quota"`
|
||||
Priority int `json:"priority"`
|
||||
RedirectLink string `json:"redirect_link"`
|
||||
ResolutionName string `json:"resolution_name"`
|
||||
Video struct {
|
||||
AudioCodec string `json:"audio_codec"`
|
||||
BitRate int `json:"bit_rate"`
|
||||
Duration int `json:"duration"`
|
||||
FrameRate int `json:"frame_rate"`
|
||||
Height int `json:"height"`
|
||||
VideoCodec string `json:"video_codec"`
|
||||
VideoType string `json:"video_type"`
|
||||
Width int `json:"width"`
|
||||
} `json:"video"`
|
||||
VipTypes []string `json:"vip_types"`
|
||||
} `json:"medias"` */
|
||||
Trashed bool `json:"trashed"`
|
||||
DeleteTime string `json:"delete_time"`
|
||||
OriginalURL string `json:"original_url"`
|
||||
//Params struct{} `json:"params"`
|
||||
OriginalFileIndex int `json:"original_file_index"`
|
||||
Space string `json:"space"`
|
||||
//Apps []interface{} `json:"apps"`
|
||||
Writable bool `json:"writable"`
|
||||
FolderType string `json:"folder_type"`
|
||||
//Collection interface{} `json:"collection"`
|
||||
}
|
||||
|
||||
type UploadTaskResponse struct {
|
||||
UploadType string `json:"upload_type"`
|
||||
|
||||
/*//UPLOAD_TYPE_FORM
|
||||
Form struct {
|
||||
//Headers struct{} `json:"headers"`
|
||||
Kind string `json:"kind"`
|
||||
Method string `json:"method"`
|
||||
MultiParts struct {
|
||||
OSSAccessKeyID string `json:"OSSAccessKeyId"`
|
||||
Signature string `json:"Signature"`
|
||||
Callback string `json:"callback"`
|
||||
Key string `json:"key"`
|
||||
Policy string `json:"policy"`
|
||||
XUserData string `json:"x:user_data"`
|
||||
} `json:"multi_parts"`
|
||||
URL string `json:"url"`
|
||||
} `json:"form"`*/
|
||||
|
||||
//UPLOAD_TYPE_RESUMABLE
|
||||
Resumable struct {
|
||||
Kind string `json:"kind"`
|
||||
Params struct {
|
||||
AccessKeyID string `json:"access_key_id"`
|
||||
AccessKeySecret string `json:"access_key_secret"`
|
||||
Bucket string `json:"bucket"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
Key string `json:"key"`
|
||||
SecurityToken string `json:"security_token"`
|
||||
} `json:"params"`
|
||||
Provider string `json:"provider"`
|
||||
} `json:"resumable"`
|
||||
|
||||
File Files `json:"file"`
|
||||
}
|
||||
103
drivers/xunlei/util.go
Normal file
103
drivers/xunlei/util.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package xunlei
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/Xhofe/alist/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// 小米浏览器
|
||||
CLIENT_ID = "X7MtiU0Gb5YqWv-6"
|
||||
CLIENT_SECRET = "84MYEih3Eeu2HF4RrGce3Q"
|
||||
CLIENT_VERSION = "5.1.0.51045"
|
||||
|
||||
ALG_VERSION = "1"
|
||||
PACKAGE_NAME = "com.xunlei.xcloud.lib"
|
||||
)
|
||||
|
||||
var Algorithms = []string{
|
||||
"",
|
||||
"BXza40wm+P4zw8rEFpHA",
|
||||
"UfZLfKfYRmKTA0",
|
||||
"OMBGVt/9Wcaln1XaBz",
|
||||
"Jn217F4rk5FPPWyhoeV",
|
||||
"w5OwkGo0pGpb0Xe/XZ5T3",
|
||||
"5guM3DNiY4F78x49zQ97q75",
|
||||
"QXwn4D2j884wJgrYXjGClM/IVrJX",
|
||||
"NXBRosYvbHIm6w8vEB",
|
||||
"2kZ8Ie1yW2ib4O2iAkNpJobP",
|
||||
"11CoVJJQEc",
|
||||
"xf3QWysVwnVsNv5DCxU+cgNT1rK",
|
||||
"9eEfKkrqkfw",
|
||||
"T78dnANexYRbiZy",
|
||||
}
|
||||
|
||||
const (
|
||||
FOLDER = "drive#folder"
|
||||
FILE = "drive#file"
|
||||
|
||||
RESUMABLE = "drive#resumable"
|
||||
)
|
||||
|
||||
const (
|
||||
UPLOAD_TYPE_UNKNOWN = "UPLOAD_TYPE_UNKNOWN"
|
||||
//UPLOAD_TYPE_FORM = "UPLOAD_TYPE_FORM"
|
||||
UPLOAD_TYPE_RESUMABLE = "UPLOAD_TYPE_RESUMABLE"
|
||||
UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL"
|
||||
)
|
||||
|
||||
func captchaSign(driverID string, time int64) string {
|
||||
str := fmt.Sprint(CLIENT_ID, CLIENT_VERSION, PACKAGE_NAME, driverID, time)
|
||||
for _, algorithm := range Algorithms {
|
||||
str = utils.GetMD5Encode(fmt.Sprint(str, algorithm))
|
||||
}
|
||||
return fmt.Sprint(ALG_VERSION, ".", str)
|
||||
}
|
||||
|
||||
func getAction(method string, u string) string {
|
||||
c, _ := url.Parse(u)
|
||||
return fmt.Sprint(method, ":", c.Path)
|
||||
}
|
||||
|
||||
func getGcid(r io.Reader, size int64) (string, error) {
|
||||
calcBlockSize := func(j int64) int64 {
|
||||
if j >= 0 && j <= 134217728 {
|
||||
return 262144
|
||||
}
|
||||
if j <= 134217728 || j > 268435456 {
|
||||
if j <= 268435456 || j > 536870912 {
|
||||
return 2097152
|
||||
}
|
||||
return 1048576
|
||||
}
|
||||
return 524288
|
||||
}
|
||||
/*
|
||||
calcBlockSize := func(j int64) int64 {
|
||||
psize := int64(0x40000)
|
||||
for j/psize > 0x200 {
|
||||
psize <<= 1
|
||||
}
|
||||
return psize
|
||||
}
|
||||
*/
|
||||
|
||||
hash1 := sha1.New()
|
||||
hash2 := sha1.New()
|
||||
for {
|
||||
hash2.Reset()
|
||||
if n, err := io.CopyN(hash2, r, calcBlockSize(size)); err != nil && n == 0 {
|
||||
if err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
}
|
||||
hash1.Write(hash2.Sum(nil))
|
||||
}
|
||||
return hex.EncodeToString(hash1.Sum(nil)), nil
|
||||
}
|
||||
288
drivers/xunlei/xunlei.go
Normal file
288
drivers/xunlei/xunlei.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package xunlei
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var xunleiClient = resty.New().SetTimeout(120 * time.Second)
|
||||
|
||||
// 一个账户只允许登陆一次
|
||||
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 := new(State).Init()
|
||||
userStateCache.States[account.Username] = state
|
||||
return state
|
||||
}
|
||||
|
||||
type State struct {
|
||||
sync.Mutex
|
||||
captchaToken string
|
||||
captchaTokenExpiresTime int64
|
||||
|
||||
tokenType string
|
||||
accessToken string
|
||||
refreshToken string
|
||||
tokenExpiresTime int64 //Milli
|
||||
|
||||
userID string
|
||||
}
|
||||
|
||||
func (s *State) init() *State {
|
||||
s.captchaToken = ""
|
||||
s.captchaTokenExpiresTime = 0
|
||||
s.tokenType = ""
|
||||
s.accessToken = ""
|
||||
s.refreshToken = ""
|
||||
s.tokenExpiresTime = 0
|
||||
s.userID = "0"
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *State) getToken(account *model.Account) (string, error) {
|
||||
if s.isTokensExpires() {
|
||||
if err := s.refreshToken_(account); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return fmt.Sprint(s.tokenType, " ", s.accessToken), nil
|
||||
}
|
||||
|
||||
func (s *State) getCaptchaToken(action string, account *model.Account) (string, error) {
|
||||
if s.isCaptchaTokenExpires() {
|
||||
return s.newCaptchaToken(action, nil, account)
|
||||
}
|
||||
return s.captchaToken, nil
|
||||
}
|
||||
|
||||
func (s *State) isCaptchaTokenExpires() bool {
|
||||
return time.Now().UnixMilli() >= s.captchaTokenExpiresTime || s.captchaToken == "" || s.tokenType == ""
|
||||
}
|
||||
|
||||
func (s *State) isTokensExpires() bool {
|
||||
return time.Now().UnixMilli() >= s.tokenExpiresTime || s.accessToken == ""
|
||||
}
|
||||
|
||||
func (s *State) newCaptchaToken(action string, meta map[string]string, account *model.Account) (string, error) {
|
||||
ctime := time.Now().UnixMilli()
|
||||
driverID := utils.GetMD5Encode(account.Username)
|
||||
creq := CaptchaTokenRequest{
|
||||
Action: action,
|
||||
CaptchaToken: s.captchaToken,
|
||||
ClientID: CLIENT_ID,
|
||||
DeviceID: driverID,
|
||||
Meta: map[string]string{
|
||||
"captcha_sign": captchaSign(driverID, ctime),
|
||||
"client_version": CLIENT_VERSION,
|
||||
"package_name": PACKAGE_NAME,
|
||||
"timestamp": fmt.Sprint(ctime),
|
||||
"user_id": s.userID,
|
||||
},
|
||||
}
|
||||
for k, v := range meta {
|
||||
creq.Meta[k] = v
|
||||
}
|
||||
|
||||
var e Erron
|
||||
var resp CaptchaTokenResponse
|
||||
_, err := xunleiClient.R().
|
||||
SetHeader("X-Device-Id", driverID).
|
||||
SetBody(&creq).
|
||||
SetError(&e).
|
||||
SetResult(&resp).
|
||||
Post("https://xluser-ssl.xunlei.com/v1/shield/captcha/init?client_id=" + CLIENT_ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
log.Debugf("%+v\n %+v", e, account)
|
||||
return "", fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
|
||||
}
|
||||
if resp.Url != "" {
|
||||
return "", fmt.Errorf("需要验证验证码")
|
||||
}
|
||||
|
||||
s.captchaTokenExpiresTime = (ctime + resp.ExpiresIn*1000) - 30000
|
||||
s.captchaToken = resp.CaptchaToken
|
||||
log.Debugf("%+v\n %+v", s.captchaToken, account)
|
||||
return s.captchaToken, nil
|
||||
}
|
||||
|
||||
func (s *State) refreshToken_(account *model.Account) error {
|
||||
var e Erron
|
||||
var resp TokenResponse
|
||||
_, err := xunleiClient.R().
|
||||
SetResult(&resp).SetError(&e).
|
||||
SetBody(&base.Json{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": s.refreshToken,
|
||||
"client_id": CLIENT_ID,
|
||||
"client_secret": CLIENT_SECRET,
|
||||
}).
|
||||
SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)).SetQueryParam("client_id", CLIENT_ID).
|
||||
Post("https://xluser-ssl.xunlei.com/v1/auth/token")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch e.ErrorCode {
|
||||
case 4122, 4121:
|
||||
return s.login(account)
|
||||
case 0:
|
||||
s.tokenExpiresTime = (time.Now().UnixMilli() + resp.ExpiresIn*1000) - 30000
|
||||
s.tokenType = resp.TokenType
|
||||
s.accessToken = resp.AccessToken
|
||||
s.refreshToken = resp.RefreshToken
|
||||
s.userID = resp.UserID
|
||||
return nil
|
||||
default:
|
||||
log.Debugf("%+v\n %+v", e, account)
|
||||
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) login(account *model.Account) error {
|
||||
s.init()
|
||||
ctime := time.Now().UnixMilli()
|
||||
url := "https://xluser-ssl.xunlei.com/v1/auth/signin"
|
||||
captchaToken, err := s.newCaptchaToken(getAction("POST", url), map[string]string{"username": account.Username}, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signReq := SignInRequest{
|
||||
CaptchaToken: captchaToken,
|
||||
ClientID: CLIENT_ID,
|
||||
ClientSecret: CLIENT_SECRET,
|
||||
Username: account.Username,
|
||||
Password: account.Password,
|
||||
}
|
||||
|
||||
var e Erron
|
||||
var resp TokenResponse
|
||||
_, err = xunleiClient.R().
|
||||
SetResult(&resp).
|
||||
SetError(&e).
|
||||
SetBody(&signReq).
|
||||
SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)).
|
||||
SetQueryParam("client_id", CLIENT_ID).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer model.SaveAccount(account)
|
||||
if e.ErrorCode != 0 {
|
||||
account.Status = e.Error
|
||||
log.Debugf("%+v\n %+v", e, account)
|
||||
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
|
||||
}
|
||||
account.Status = "work"
|
||||
s.tokenExpiresTime = (ctime + resp.ExpiresIn*1000) - 30000
|
||||
s.tokenType = resp.TokenType
|
||||
s.accessToken = resp.AccessToken
|
||||
s.refreshToken = resp.RefreshToken
|
||||
s.userID = resp.UserID
|
||||
log.Debugf("%+v\n %+v", resp, account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *State) Request(method string, url string, body interface{}, resp interface{}, account *model.Account) error {
|
||||
s.Lock()
|
||||
token, err := s.getToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
captchaToken, err := s.getCaptchaToken(getAction(method, url), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Unlock()
|
||||
var e Erron
|
||||
req := xunleiClient.R().
|
||||
SetError(&e).
|
||||
SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)).
|
||||
SetHeader("Authorization", token).
|
||||
SetHeader("X-Captcha-Token", captchaToken).
|
||||
SetQueryParam("client_id", CLIENT_ID)
|
||||
|
||||
if body != nil {
|
||||
req.SetBody(body)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "GET":
|
||||
_, err = req.Get(url)
|
||||
case "POST":
|
||||
_, err = req.Post(url)
|
||||
case "DELETE":
|
||||
_, err = req.Delete(url)
|
||||
case "PATCH":
|
||||
_, err = req.Patch(url)
|
||||
case "PUT":
|
||||
_, err = req.Put(url)
|
||||
default:
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch e.ErrorCode {
|
||||
case 0:
|
||||
return nil
|
||||
case 9:
|
||||
s.newCaptchaToken(getAction(method, url), nil, account)
|
||||
fallthrough
|
||||
case 4122, 4121:
|
||||
return s.Request(method, url, body, resp, account)
|
||||
default:
|
||||
log.Debugf("%+v\n %+v", e, account)
|
||||
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) Init() *State {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.init()
|
||||
}
|
||||
|
||||
func (s *State) GetCaptchaToken(action string, account *model.Account) (string, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.getCaptchaToken(action, account)
|
||||
}
|
||||
|
||||
func (s *State) GetToken(account *model.Account) (string, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.getToken(account)
|
||||
}
|
||||
|
||||
func (s *State) Login(account *model.Account) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.login(account)
|
||||
}
|
||||
224
drivers/yandex/driver.go
Normal file
224
drivers/yandex/driver.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package yandex
|
||||
|
||||
import (
|
||||
"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"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Yandex struct{}
|
||||
|
||||
func (driver Yandex) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Yandex.Disk",
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Yandex) 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,path,created,modified,size",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "asc,desc",
|
||||
Default: "asc",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "client_id",
|
||||
Label: "client id",
|
||||
Default: "a78d5a69054042fa936f6c77f9a0ae8b",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "client_secret",
|
||||
Label: "client secret",
|
||||
Default: "9c119bbb04b346d2a52aa64401936b2b",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Yandex) Save(account *model.Account, old *model.Account) error {
|
||||
return driver.RefreshToken(account)
|
||||
}
|
||||
|
||||
func (driver Yandex) 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 Yandex) 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 Yandex) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
path := utils.Join(account.RootFolder, args.Path)
|
||||
log.Debugln("down path:", path)
|
||||
var resp DownResp
|
||||
_, err := driver.Request("/download", base.Get, nil, map[string]string{
|
||||
"path": path,
|
||||
}, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link := base.Link{
|
||||
Url: resp.Href,
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Yandex) 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 Yandex) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver Yandex) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Yandex) MakeDir(path string, account *model.Account) error {
|
||||
path = utils.Join(account.RootFolder, path)
|
||||
_, err := driver.Request("", base.Put, nil, map[string]string{
|
||||
"path": path,
|
||||
}, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Yandex) Move(src string, dst string, account *model.Account) error {
|
||||
from := utils.Join(account.RootFolder, src)
|
||||
path := utils.Join(account.RootFolder, dst)
|
||||
_, err := driver.Request("/move", base.Post, nil, map[string]string{
|
||||
"from": from,
|
||||
"path": path,
|
||||
"overwrite": "true",
|
||||
}, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Yandex) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver Yandex) Copy(src string, dst string, account *model.Account) error {
|
||||
from := utils.Join(account.RootFolder, src)
|
||||
path := utils.Join(account.RootFolder, dst)
|
||||
_, err := driver.Request("/copy", base.Post, nil, map[string]string{
|
||||
"from": from,
|
||||
"path": path,
|
||||
"overwrite": "true",
|
||||
}, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Yandex) Delete(path string, account *model.Account) error {
|
||||
path = utils.Join(account.RootFolder, path)
|
||||
_, err := driver.Request("", base.Delete, nil, map[string]string{
|
||||
"path": path,
|
||||
}, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Yandex) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
path := utils.Join(account.RootFolder, file.ParentPath, file.Name)
|
||||
var resp UploadResp
|
||||
_, err := driver.Request("/upload", base.Get, nil, map[string]string{
|
||||
"path": path,
|
||||
"overwrite": "true",
|
||||
}, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(resp.Method, resp.Href, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Length", strconv.FormatUint(file.Size, 10))
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
_, err = base.HttpClient.Do(req)
|
||||
//res, err := base.RestyClient.R().
|
||||
// SetHeader("Content-Length", strconv.FormatUint(file.Size, 10)).
|
||||
// SetBody(file).Put(resp.Href)
|
||||
//log.Debugln(res.Status(), res.String())
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Yandex)(nil)
|
||||
74
drivers/yandex/types.go
Normal file
74
drivers/yandex/types.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package yandex
|
||||
|
||||
import "time"
|
||||
|
||||
type TokenErrResp struct {
|
||||
ErrorDescription string `json:"error_description"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type ErrResp struct {
|
||||
Message string `json:"message"`
|
||||
Description string `json:"description"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
//AntivirusStatus string `json:"antivirus_status"`
|
||||
Size int64 `json:"size"`
|
||||
//CommentIds struct {
|
||||
// PrivateResource string `json:"private_resource"`
|
||||
// PublicResource string `json:"public_resource"`
|
||||
//} `json:"comment_ids"`
|
||||
Name string `json:"name"`
|
||||
//Exif struct {
|
||||
// DateTime time.Time `json:"date_time"`
|
||||
//} `json:"exif"`
|
||||
//Created time.Time `json:"created"`
|
||||
//ResourceId string `json:"resource_id"`
|
||||
Modified *time.Time `json:"modified"`
|
||||
//MimeType string `json:"mime_type"`
|
||||
File string `json:"file"`
|
||||
//MediaType string `json:"media_type"`
|
||||
Preview string `json:"preview"`
|
||||
Path string `json:"path"`
|
||||
//Sha256 string `json:"sha256"`
|
||||
Type string `json:"type"`
|
||||
//Md5 string `json:"md5"`
|
||||
//Revision int64 `json:"revision"`
|
||||
}
|
||||
|
||||
type FilesResp struct {
|
||||
Embedded struct {
|
||||
Sort string `json:"sort"`
|
||||
Items []File `json:"items"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
Path string `json:"path"`
|
||||
Total int `json:"total"`
|
||||
} `json:"_embedded"`
|
||||
Name string `json:"name"`
|
||||
Exif struct {
|
||||
} `json:"exif"`
|
||||
ResourceId string `json:"resource_id"`
|
||||
Created time.Time `json:"created"`
|
||||
Modified time.Time `json:"modified"`
|
||||
Path string `json:"path"`
|
||||
CommentIds struct {
|
||||
} `json:"comment_ids"`
|
||||
Type string `json:"type"`
|
||||
Revision int64 `json:"revision"`
|
||||
}
|
||||
|
||||
type DownResp struct {
|
||||
Href string `json:"href"`
|
||||
Method string `json:"method"`
|
||||
Templated bool `json:"templated"`
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
OperationId string `json:"operation_id"`
|
||||
Href string `json:"href"`
|
||||
Method string `json:"method"`
|
||||
Templated bool `json:"templated"`
|
||||
}
|
||||
1
drivers/yandex/util.go
Normal file
1
drivers/yandex/util.go
Normal file
@@ -0,0 +1 @@
|
||||
package yandex
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user