mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-11-25 19:37:41 +08:00
Compare commits
932 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55d3827dee | ||
|
|
1fbc9427df | ||
|
|
bb3d139a47 | ||
|
|
d227ab85d6 | ||
|
|
5342ae96d0 | ||
|
|
273e15a050 | ||
|
|
13aad2c2fa | ||
|
|
368dc65a6e | ||
|
|
8b4b6ba970 | ||
|
|
4d28e838ce | ||
|
|
3930d4789a | ||
|
|
d0c22a1ecb | ||
|
|
57fceabcf4 | ||
|
|
8c244a984d | ||
|
|
df479ba806 | ||
|
|
5ae8e96237 | ||
|
|
aa0ced47b0 | ||
|
|
ab747d9052 | ||
|
|
93c06213d4 | ||
|
|
b9b8eed285 | ||
|
|
317d190b77 | ||
|
|
52d7d819ad | ||
|
|
0483e0f868 | ||
|
|
08dae4f55f | ||
|
|
9ac0484bc0 | ||
|
|
8cf15183a0 | ||
|
|
c8f2aaaa55 | ||
|
|
1208bd0a83 | ||
|
|
6b096bcad4 | ||
|
|
58dbf088f9 | ||
|
|
05ff7908f2 | ||
|
|
a703b736c9 | ||
|
|
e458f2ab53 | ||
|
|
a5a22e7085 | ||
|
|
9469c95b14 | ||
|
|
cf912dcf7a | ||
|
|
ccd4af26e5 | ||
|
|
1682e873d6 | ||
|
|
54ae7e6d9b | ||
|
|
991da7d87f | ||
|
|
a498091aef | ||
|
|
976c82bb2b | ||
|
|
5b41a3bdff | ||
|
|
19d1a3b785 | ||
|
|
3c7b0c4999 | ||
|
|
d6867b4ab6 | ||
|
|
11cf561307 | ||
|
|
239b58f63e | ||
|
|
7da06655cb | ||
|
|
e0b3a611ba | ||
|
|
be1ad08a83 | ||
|
|
4e9c30f49d | ||
|
|
0ee31a3f36 | ||
|
|
23bddf991e | ||
|
|
da8d6607cf | ||
|
|
6134574dac | ||
|
|
b273232f87 | ||
|
|
358e4d851e | ||
|
|
e8a1ed638a | ||
|
|
4106e2a996 | ||
|
|
c2271df64e | ||
|
|
d4b8570eb8 | ||
|
|
bd297e8ccc | ||
|
|
923d282c8a | ||
|
|
4d8c4d7089 | ||
|
|
e93ab76036 | ||
|
|
a9f02ecdac | ||
|
|
93849a3b5b | ||
|
|
c2e0d0c9ce | ||
|
|
4a713363ee | ||
|
|
3da8ccb7a7 | ||
|
|
676b8cff0b | ||
|
|
57cf28fc90 | ||
|
|
8cf90e074d | ||
|
|
74c2ed8306 | ||
|
|
5f03edd683 | ||
|
|
8b65c918d4 | ||
|
|
b5f0e3e5ee | ||
|
|
179894ff37 | ||
|
|
e2fc89c637 | ||
|
|
cacf67b181 | ||
|
|
afb043e1d6 | ||
|
|
d9debb81ad | ||
|
|
4c069fddd6 | ||
|
|
b450a2104d | ||
|
|
7d0de17daf | ||
|
|
bba4fb2203 | ||
|
|
a20c2020f8 | ||
|
|
a92b5eb929 | ||
|
|
6817494a41 | ||
|
|
5a0d8ee1b8 | ||
|
|
012e51c551 | ||
|
|
59ec1dbc9b | ||
|
|
6bb28d13f9 | ||
|
|
811a862288 | ||
|
|
74d32fd4d7 | ||
|
|
cedb3d488d | ||
|
|
86324d2d6b | ||
|
|
648079ae24 | ||
|
|
e8d45398d6 | ||
|
|
0c461991f9 | ||
|
|
2a4c546a8b | ||
|
|
750d4eb3f6 | ||
|
|
cc01b410a4 | ||
|
|
e5fbe72581 | ||
|
|
283f3723d1 | ||
|
|
ad8c7b37a1 | ||
|
|
a84ffb96e9 | ||
|
|
19c6b6f930 | ||
|
|
eed3c0533c | ||
|
|
c72ba9828a | ||
|
|
4965a1b909 | ||
|
|
1bba550469 | ||
|
|
d678322b18 | ||
|
|
efd8897bdf | ||
|
|
7c7cec0993 | ||
|
|
3838ef0663 | ||
|
|
9e610af114 | ||
|
|
0177177238 | ||
|
|
a77e515c9b | ||
|
|
4af16ab009 | ||
|
|
da35423198 | ||
|
|
9612d61e60 | ||
|
|
92f396df10 | ||
|
|
9557834342 | ||
|
|
288ba2fcda | ||
|
|
f3920b02f7 | ||
|
|
2ec9dad3db | ||
|
|
e11227fe2d | ||
|
|
859931b78c | ||
|
|
b591524ac3 | ||
|
|
dc26b4fce5 | ||
|
|
f8cf02a2da | ||
|
|
a214e794f4 | ||
|
|
54d761b371 | ||
|
|
bea7a9b0e4 | ||
|
|
a46f4cff18 | ||
|
|
8eb2d600c7 | ||
|
|
ffb6c2a180 | ||
|
|
8e19a0fb07 | ||
|
|
79f4f96217 | ||
|
|
7f53390dce | ||
|
|
e83f8e197a | ||
|
|
d707f002eb | ||
|
|
c0f69f7fa7 | ||
|
|
adf914115f | ||
|
|
c166fe6127 | ||
|
|
9725e0fd76 | ||
|
|
5c4cd1b198 | ||
|
|
44f4658f37 | ||
|
|
b4997e7a7e | ||
|
|
f26892ac3c | ||
|
|
aae3851979 | ||
|
|
a17b3dc405 | ||
|
|
022614f155 | ||
|
|
874dc292ae | ||
|
|
9442013b37 | ||
|
|
862b1c3c53 | ||
|
|
52c93f2046 | ||
|
|
3d13d5213b | ||
|
|
103abc942e | ||
|
|
f0236522f3 | ||
|
|
6a3b8fab06 | ||
|
|
5c288dc763 | ||
|
|
6d0d3ac612 | ||
|
|
1ec97733e5 | ||
|
|
ded67b746b | ||
|
|
4590795cba | ||
|
|
060fd36883 | ||
|
|
76a1f99df1 | ||
|
|
38766a4cb7 | ||
|
|
bcc518cf96 | ||
|
|
3fdb2c79bf | ||
|
|
18f7a2ba0e | ||
|
|
e32cebb153 | ||
|
|
02031bd835 | ||
|
|
7726cb14a0 | ||
|
|
23cfe8090b | ||
|
|
d89d0a05b4 | ||
|
|
14d57ae2ec | ||
|
|
d5f4b687bb | ||
|
|
bdb880f9f2 | ||
|
|
22575a1c61 | ||
|
|
890297aa27 | ||
|
|
0fd602bc1b | ||
|
|
f6470af971 | ||
|
|
d695d28e13 | ||
|
|
ffc14ea14c | ||
|
|
25df3daba5 | ||
|
|
ce3cb2e31e | ||
|
|
afe23986d2 | ||
|
|
0026f0c860 | ||
|
|
9e69b2aaa3 | ||
|
|
af71deb407 | ||
|
|
fe079cf0a3 | ||
|
|
cf85d49b6c | ||
|
|
96cf2f7cf9 | ||
|
|
b0736d2d02 | ||
|
|
49213c1321 | ||
|
|
64dd3cb047 | ||
|
|
12fd52b6b7 | ||
|
|
27533d0e20 | ||
|
|
34a2eeb4a9 | ||
|
|
652e4ba1cb | ||
|
|
639b5cf7c2 | ||
|
|
b5c1386645 | ||
|
|
041868dfb8 | ||
|
|
cfbc157477 | ||
|
|
5d44806064 | ||
|
|
fc8b99c862 | ||
|
|
24560b43c0 | ||
|
|
39ca385778 | ||
|
|
ef0531ad40 | ||
|
|
12540a8abc | ||
|
|
0f5ed14fe2 | ||
|
|
ca55b89322 | ||
|
|
a3c7cb059d | ||
|
|
0f8545133b | ||
|
|
72fad1be2e | ||
|
|
b7ce7f172b | ||
|
|
248c041711 | ||
|
|
70b937e031 | ||
|
|
79521db8e0 | ||
|
|
015d3ecd00 | ||
|
|
89451b6d98 | ||
|
|
681cb6c8a4 | ||
|
|
c2d1316f65 | ||
|
|
5e8d8d070a | ||
|
|
c7c0bfe810 | ||
|
|
e9c73b52db | ||
|
|
7d24a5d45f | ||
|
|
3ab309e00e | ||
|
|
8822eef97e | ||
|
|
7613f886d0 | ||
|
|
fe02a989bd | ||
|
|
2bed40cfce | ||
|
|
87ca1b96ae | ||
|
|
5a4649c929 | ||
|
|
2e2cec05fd | ||
|
|
b1afadd129 | ||
|
|
a59ad9a84e | ||
|
|
2e889fb07d | ||
|
|
d95c4f0127 | ||
|
|
1c58d11d62 | ||
|
|
e11c390c4d | ||
|
|
2965915bed | ||
|
|
da1cfd1945 | ||
|
|
8a29790327 | ||
|
|
7cd8f648c8 | ||
|
|
b8e6083e19 | ||
|
|
3f821bdcd1 | ||
|
|
9e05c81d9c | ||
|
|
f1552b67a0 | ||
|
|
20d1d5b479 | ||
|
|
fdcc2f136e | ||
|
|
5feb86ceee | ||
|
|
ee783fa1be | ||
|
|
0bcb4fe16d | ||
|
|
4f57bd3ae6 | ||
|
|
cf42fe6a40 | ||
|
|
c4775521c6 | ||
|
|
ffa03bfda1 | ||
|
|
630cf30af5 | ||
|
|
bc5117fa4f | ||
|
|
11e7284824 | ||
|
|
b2b91a9281 | ||
|
|
f541489d7d | ||
|
|
6d9c554f6f | ||
|
|
e532ab31ef | ||
|
|
bf0705ec17 | ||
|
|
17b42b9fa4 | ||
|
|
41bdab49aa | ||
|
|
8f89c55aca | ||
|
|
b449312da8 | ||
|
|
52d4e8ec47 | ||
|
|
28e5b5759e | ||
|
|
477c43971f | ||
|
|
0a9921fa79 | ||
|
|
88abb323cb | ||
|
|
f0b1aeaf8d | ||
|
|
c8470b9a2a | ||
|
|
d0ee90cd11 | ||
|
|
544a7ea022 | ||
|
|
4f5cabc725 | ||
|
|
a2f266277c | ||
|
|
a4bfbf8a83 | ||
|
|
ddffacf07b | ||
|
|
3375c26c41 | ||
|
|
ab68faef44 | ||
|
|
2e21df0661 | ||
|
|
af18cb138b | ||
|
|
31c55a2adf | ||
|
|
465dd1703d | ||
|
|
a6304285b6 | ||
|
|
affd0cecd1 | ||
|
|
37640221c0 | ||
|
|
e4bd223d1c | ||
|
|
0cde4e73d6 | ||
|
|
7b62dcb88c | ||
|
|
c38dc6df7c | ||
|
|
5668e4a4ea | ||
|
|
1335f80362 | ||
|
|
704d3854df | ||
|
|
44cc71d354 | ||
|
|
9a9aee9ac6 | ||
|
|
4fcc3a187e | ||
|
|
10a76c701d | ||
|
|
6e13923225 | ||
|
|
32890da29f | ||
|
|
758554a40f | ||
|
|
4563aea47e | ||
|
|
35d6f3b8fc | ||
|
|
b4e6ab12d9 | ||
|
|
3499c4db87 | ||
|
|
d20f41d687 | ||
|
|
d16ba65f42 | ||
|
|
c82e632ee1 | ||
|
|
04f5525f20 | ||
|
|
28b61a93fd | ||
|
|
0126af4de0 | ||
|
|
7579d44517 | ||
|
|
5dfea714d8 | ||
|
|
370a6c15a9 | ||
|
|
2570707a06 | ||
|
|
4145734c18 | ||
|
|
646c7bcd21 | ||
|
|
cdc41595bc | ||
|
|
79bef0be9e | ||
|
|
c230f24ebe | ||
|
|
30d8c20756 | ||
|
|
3b71500f23 | ||
|
|
399336b33c | ||
|
|
36b4204623 | ||
|
|
f25be154c6 | ||
|
|
ec3fc945a3 | ||
|
|
3f9bed3d5f | ||
|
|
b9ad18bd0a | ||
|
|
0219c4e15a | ||
|
|
d983a4ebcb | ||
|
|
f795807753 | ||
|
|
6164e4577b | ||
|
|
39bde328ee | ||
|
|
779c293f04 | ||
|
|
b9f397d29f | ||
|
|
d53eecc229 | ||
|
|
f88fd83d4a | ||
|
|
226c34929a | ||
|
|
027edcbe53 | ||
|
|
fd51f34efa | ||
|
|
bdd9774aa7 | ||
|
|
258b8f520f | ||
|
|
99f39410f2 | ||
|
|
267120a8c8 | ||
|
|
5eff8cc7bf | ||
|
|
d5ec998699 | ||
|
|
23f3178f39 | ||
|
|
cafdb4d407 | ||
|
|
0d4c63e9ff | ||
|
|
5c5d8378e5 | ||
|
|
2be0c3d1a0 | ||
|
|
bdcf450203 | ||
|
|
c2633dd443 | ||
|
|
11b6a6012f | ||
|
|
59e02287b2 | ||
|
|
bb40e2e2cd | ||
|
|
ab22cf8233 | ||
|
|
880cc7abca | ||
|
|
b60da9732f | ||
|
|
e04114d102 | ||
|
|
51bcf83511 | ||
|
|
25b4b55ee1 | ||
|
|
6812ec9a6d | ||
|
|
31a7470865 | ||
|
|
687124c81d | ||
|
|
e4439e66b9 | ||
|
|
7fd4ac7851 | ||
|
|
6745dcc139 | ||
|
|
aa1082a56c | ||
|
|
ed149be84b | ||
|
|
040dc14ee6 | ||
|
|
4dce53d72b | ||
|
|
365fc40dfe | ||
|
|
5994c17b4e | ||
|
|
42243b1517 | ||
|
|
48916cdedf | ||
|
|
5ecf5e823c | ||
|
|
c218b5701e | ||
|
|
77d0c78bfd | ||
|
|
db5c601cfe | ||
|
|
221cdf3611 | ||
|
|
40b0e66efe | ||
|
|
b72e85a73a | ||
|
|
6aaf5975c6 | ||
|
|
bb2aec20e4 | ||
|
|
d7aa1608ac | ||
|
|
db99224126 | ||
|
|
b8bd14f99b | ||
|
|
331885ed64 | ||
|
|
cf58ab3a78 | ||
|
|
33ba7f1521 | ||
|
|
201e25c17f | ||
|
|
ecefa5e0eb | ||
|
|
650b03aeb1 | ||
|
|
7341846499 | ||
|
|
a3908fd9a6 | ||
|
|
2a035302b2 | ||
|
|
016e169c41 | ||
|
|
088120df82 | ||
|
|
aa45a82914 | ||
|
|
5084d98398 | ||
|
|
fa15c576f0 | ||
|
|
2d3605c684 | ||
|
|
492b49d77a | ||
|
|
94915b2148 | ||
|
|
2dec756f23 | ||
|
|
4c0cffd29b | ||
|
|
25c5e075a9 | ||
|
|
398c04386a | ||
|
|
12b429584e | ||
|
|
150dcc2147 | ||
|
|
0ba754fd40 | ||
|
|
28d2367a87 | ||
|
|
a4ad98ee3e | ||
|
|
1c01dc6839 | ||
|
|
c3c5843dce | ||
|
|
6c38c5972d | ||
|
|
0a46979c51 | ||
|
|
67c93eed2b | ||
|
|
f58de9923a | ||
|
|
2671c876f1 | ||
|
|
e707fa38f1 | ||
|
|
b803b0070e | ||
|
|
64ceb5afb6 | ||
|
|
10c7ebb1c0 | ||
|
|
d0cda62703 | ||
|
|
ce0b99a510 | ||
|
|
34a148c83d | ||
|
|
4955d8cec8 | ||
|
|
216e3909f3 | ||
|
|
a701432b8b | ||
|
|
a2dc45a80b | ||
|
|
48ac23c8de | ||
|
|
2830575490 | ||
|
|
e8538bd215 | ||
|
|
c3e43ff605 | ||
|
|
5f19d73fcc | ||
|
|
bdf4b52885 | ||
|
|
6106a2d4cc | ||
|
|
b6451451b1 | ||
|
|
f06d2c0348 | ||
|
|
b7ae56b109 | ||
|
|
5d9167d676 | ||
|
|
1b42b9627c | ||
|
|
bb58b94a10 | ||
|
|
ffce61d227 | ||
|
|
0310b70d90 | ||
|
|
73f0b135b6 | ||
|
|
8316f81e41 | ||
|
|
cdbfda8921 | ||
|
|
9667832b32 | ||
|
|
b36d38f63f | ||
|
|
c8317250c1 | ||
|
|
0242f36e1c | ||
|
|
40a68bcee6 | ||
|
|
92713ef5c4 | ||
|
|
716d33fddd | ||
|
|
c9fa3d7cd6 | ||
|
|
4874c9e43b | ||
|
|
34ada81582 | ||
|
|
ba716ae325 | ||
|
|
d4f9c4b6af | ||
|
|
b910b8917f | ||
|
|
d92744e673 | ||
|
|
868b0ec25c | ||
|
|
e21edf98e2 | ||
|
|
d2514d236f | ||
|
|
34b6785fab | ||
|
|
48f50a2ceb | ||
|
|
74887922b4 | ||
|
|
bcb24d61ea | ||
|
|
db1494455d | ||
|
|
d9a1809313 | ||
|
|
0715198c7f | ||
|
|
ef5e192c3b | ||
|
|
489b28bdf7 | ||
|
|
18176c659c | ||
|
|
4c48a816bf | ||
|
|
9af7aaab59 | ||
|
|
a54a09314f | ||
|
|
e2fcd73720 | ||
|
|
e238b90836 | ||
|
|
69e5b66b50 | ||
|
|
e8e6d71c41 | ||
|
|
4ba476e25c | ||
|
|
e5fe9ea5f6 | ||
|
|
e1906c9312 | ||
|
|
51c95ee117 | ||
|
|
1f652e2e7d | ||
|
|
8e6c1aa78d | ||
|
|
6bff5b6107 | ||
|
|
94937db491 | ||
|
|
3dc250cc37 | ||
|
|
9560799175 | ||
|
|
8f3c5b1587 | ||
|
|
285125d06a | ||
|
|
a26185fe05 | ||
|
|
a7efa3a676 | ||
|
|
d596ef5c38 | ||
|
|
34e34ef564 | ||
|
|
8032d0afb6 | ||
|
|
d3bc8993ee | ||
|
|
62ed169a39 | ||
|
|
979d0cfeee | ||
|
|
29165d8e60 | ||
|
|
2d77db6bc2 | ||
|
|
74f8295960 | ||
|
|
f2727095d9 | ||
|
|
d4285b7c6c | ||
|
|
2e4265a778 | ||
|
|
81258d3e8a | ||
|
|
a6bead90d7 | ||
|
|
87caaf2459 | ||
|
|
af9c6afd25 | ||
|
|
8b5727a0aa | ||
|
|
aeae47c9bf | ||
|
|
1aff758688 | ||
|
|
4a42bc5083 | ||
|
|
5fa70e4010 | ||
|
|
d4e3355f56 | ||
|
|
94f257e557 | ||
|
|
e5f53d6dee | ||
|
|
cbd4bef814 | ||
|
|
2d57529e77 | ||
|
|
2b74999703 | ||
|
|
fe081d0ebc | ||
|
|
5ef7a27be3 | ||
|
|
c9a18f4de6 | ||
|
|
f2a24881d0 | ||
|
|
cee00005ab | ||
|
|
049575b5a5 | ||
|
|
a93937f80d | ||
|
|
488ebaa1af | ||
|
|
8278d3875b | ||
|
|
736ba44031 | ||
|
|
a6ff6a94df | ||
|
|
17f78b948a | ||
|
|
fe1040a367 | ||
|
|
83048e6c7c | ||
|
|
9128647970 | ||
|
|
9629705100 | ||
|
|
cd663f78af | ||
|
|
3c483ace4f | ||
|
|
3e949fcf33 | ||
|
|
81b0afc349 | ||
|
|
a04da3ec50 | ||
|
|
9e0482afbb | ||
|
|
9de40f8976 | ||
|
|
ba4df55d6e | ||
|
|
de8d2d6dc0 | ||
|
|
65b423c503 | ||
|
|
ff20b5a6fb | ||
|
|
37d86ff55c | ||
|
|
4e1c67617f | ||
|
|
9bc2d340a2 | ||
|
|
60fc416d8f | ||
|
|
99c9632cdc | ||
|
|
2fb772c888 | ||
|
|
87192ad07d | ||
|
|
3746831384 | ||
|
|
80d4fbb870 | ||
|
|
92c65b450e | ||
|
|
213fc0232e | ||
|
|
33be44adad | ||
|
|
ca0d66bd01 | ||
|
|
3a3d0adfa0 | ||
|
|
ca30849e24 | ||
|
|
316f3569a5 | ||
|
|
2705877235 | ||
|
|
432901db5a | ||
|
|
227d034db8 | ||
|
|
453d7da622 | ||
|
|
29fe49fb87 | ||
|
|
fcf2683112 | ||
|
|
3a996a1a3a | ||
|
|
1b14d33b9f | ||
|
|
639b7817bf | ||
|
|
163af0515f | ||
|
|
8e2b9c681a | ||
|
|
0a8d710e01 | ||
|
|
d781f7127a | ||
|
|
85d743c5d2 | ||
|
|
5f60b51cf8 | ||
|
|
7013d1b7b8 | ||
|
|
9eec872637 | ||
|
|
037850bbd5 | ||
|
|
bbe3d4e19f | ||
|
|
78a9676c7c | ||
|
|
8bf93562eb | ||
|
|
b57afd0a98 | ||
|
|
f261ef50cc | ||
|
|
7e7b9b9b48 | ||
|
|
2313213f59 | ||
|
|
5f28532423 | ||
|
|
4cbbda8832 | ||
|
|
7bf5014417 | ||
|
|
b704bba444 | ||
|
|
eecea3febd | ||
|
|
0e246a7b0c | ||
|
|
b95df1d745 | ||
|
|
ec08ecdf6c | ||
|
|
479fc6d466 | ||
|
|
32ddab9b01 | ||
|
|
0c9dcec9cd | ||
|
|
793a4ea6ca | ||
|
|
c3c5181847 | ||
|
|
cd5a8a011d | ||
|
|
1756036a21 | ||
|
|
58c3cb3cf6 | ||
|
|
d8e190406a | ||
|
|
2880ed70ce | ||
|
|
0e86036874 | ||
|
|
e37465e67e | ||
|
|
d517adde71 | ||
|
|
8a18f47e68 | ||
|
|
cf08aa3668 | ||
|
|
9c84b6596f | ||
|
|
022e0ca292 | ||
|
|
88947f6676 | ||
|
|
b07ddfbc13 | ||
|
|
9a0a63d34c | ||
|
|
195c869272 | ||
|
|
bdfc1591bd | ||
|
|
82222840fe | ||
|
|
45e009a22c | ||
|
|
ac68079a76 | ||
|
|
2a17d0c2cd | ||
|
|
6f6a8e6dfc | ||
|
|
7d9ecba99c | ||
|
|
ae6984714d | ||
|
|
d0f88bd1cb | ||
|
|
f8b1f87a5f | ||
|
|
71e4e1ab6e | ||
|
|
7e6522c81e | ||
|
|
94a80bccfe | ||
|
|
e66abb3f58 | ||
|
|
742335f80e | ||
|
|
f1979a8bbc | ||
|
|
1f835502ba | ||
|
|
424ab2d0c0 | ||
|
|
858ba19670 | ||
|
|
0c7e47a76c | ||
|
|
53926d5cd0 | ||
|
|
47f4b05517 | ||
|
|
6d85f1b0c0 | ||
|
|
e49fda3e2a | ||
|
|
da5e35578a | ||
|
|
812f58ae6d | ||
|
|
9bd3c87bcc | ||
|
|
c82866975e | ||
|
|
aef952ae68 | ||
|
|
9222510d8d | ||
|
|
d88b54d98a | ||
|
|
85a28d9822 | ||
|
|
4f7761fe2c | ||
|
|
a8c900d09e | ||
|
|
8bccb69e8d | ||
|
|
0f29a811bf | ||
|
|
442c2f77ea | ||
|
|
ce06f394f1 | ||
|
|
e3e790f461 | ||
|
|
f0e8c0e886 | ||
|
|
86b35ae5cf | ||
|
|
4930f85b90 | ||
|
|
85fe65951d | ||
|
|
1381e8fb27 | ||
|
|
292bbe94ee | ||
|
|
bb6747de4e | ||
|
|
555ef0eb1a | ||
|
|
bff56ffd0f | ||
|
|
34b73b94f7 | ||
|
|
434892f135 | ||
|
|
e6e2d03ba1 | ||
|
|
28bb3f6310 | ||
|
|
fb729c1846 | ||
|
|
4448e08f5b | ||
|
|
8020d42b10 | ||
|
|
9d5fb7f595 | ||
|
|
126cfe9f93 | ||
|
|
fd96a7ccf4 | ||
|
|
03b9b9a119 | ||
|
|
03dbdfc0dd | ||
|
|
2683621ed7 | ||
|
|
be537aa49b | ||
|
|
6f742a68cf | ||
|
|
97a4b8321d | ||
|
|
8c432d3339 | ||
|
|
ff25e51f80 | ||
|
|
88831b5d5a | ||
|
|
b97c9173af | ||
|
|
207c7e05fe | ||
|
|
7db27e6da8 | ||
|
|
b5cc90cb5a | ||
|
|
8a427ddc49 | ||
|
|
c36644a172 | ||
|
|
45b1ff4a24 | ||
|
|
a4a9675616 | ||
|
|
8531b23382 | ||
|
|
2c15349ce4 | ||
|
|
5afd65b65c | ||
|
|
e2434029f9 | ||
|
|
bdf7abe717 | ||
|
|
2c8d003c2e | ||
|
|
a006f57637 | ||
|
|
be5d94cd11 | ||
|
|
977b3cf9ab | ||
|
|
182aacd309 | ||
|
|
57bac9e0d2 | ||
|
|
478470f609 | ||
|
|
6b8f35e7fa | ||
|
|
697a0ed2d3 | ||
|
|
299bfb4d7b | ||
|
|
3eca38e599 | ||
|
|
ab216ed170 | ||
|
|
e91c42c9dc | ||
|
|
54f7b21a73 | ||
|
|
de56f926cf | ||
|
|
6d4ab57a0e | ||
|
|
734d4b0354 | ||
|
|
74b20dedc3 | ||
|
|
83c2269330 | ||
|
|
296be88b5f | ||
|
|
026e944cbb | ||
|
|
8bdfc7ac8e | ||
|
|
e4a6b758dc | ||
|
|
66b7fe1e1b | ||
|
|
f475eb4401 | ||
|
|
b99e709bdb | ||
|
|
f4dcf4599c | ||
|
|
54e75d7287 | ||
|
|
d142fc3449 | ||
|
|
f23567199b | ||
|
|
1420492d81 | ||
|
|
b88067ea2f | ||
|
|
d5f381ef6f | ||
|
|
68af284dad | ||
|
|
d26887d211 | ||
|
|
3f405de6a9 | ||
|
|
6100647310 | ||
|
|
34746e951c | ||
|
|
b6134dc515 | ||
|
|
d455a232ef | ||
|
|
fe34d30d17 | ||
|
|
0fbb986ba9 | ||
|
|
1280070438 | ||
|
|
d7f66138eb | ||
|
|
b2890f05ab | ||
|
|
7583c4d734 | ||
|
|
11a30c5044 | ||
|
|
de9647a5fa | ||
|
|
8d5283604c | ||
|
|
867accafd1 | ||
|
|
6fc6751463 | ||
|
|
f904596cbc | ||
|
|
3d51845f57 | ||
|
|
a7421d8fc2 | ||
|
|
55a14bc271 | ||
|
|
91f51f17d0 | ||
|
|
4355dae491 | ||
|
|
da1c7a4c23 | ||
|
|
769281bd40 | ||
|
|
3bbdd4fa89 | ||
|
|
68f440abdb | ||
|
|
65c5ec0c34 | ||
|
|
a6325967d0 | ||
|
|
4dff49470a | ||
|
|
cc86d6f3d1 | ||
|
|
c0f9c8ebaf | ||
|
|
4fc0a77565 | ||
|
|
aaffaee2b5 | ||
|
|
8ef8023c20 | ||
|
|
cdfbe6dcf2 | ||
|
|
94d028743a | ||
|
|
7f7335435c | ||
|
|
b9e192b29c | ||
|
|
69a98eaef6 | ||
|
|
1ebc96a4e5 | ||
|
|
66e2324cac | ||
|
|
7600dc28df | ||
|
|
8ef89ad0a4 | ||
|
|
35d672217d | ||
|
|
1a283bb272 | ||
|
|
a008f54f4d | ||
|
|
3d7f79cba8 | ||
|
|
9ff83a7950 | ||
|
|
e719a1a456 | ||
|
|
40a6fcbdff | ||
|
|
0fd51646f6 | ||
|
|
e8958019d9 | ||
|
|
e1ef690784 | ||
|
|
4024050dd0 | ||
|
|
eb918658f0 | ||
|
|
fb13dae136 | ||
|
|
6b67a36d63 | ||
|
|
a64dd4885e | ||
|
|
0f03a747d8 | ||
|
|
30977cdc6d | ||
|
|
106cf720c1 | ||
|
|
882112ed1c | ||
|
|
2a6ab77295 | ||
|
|
f0981a0c8d | ||
|
|
57eea4db17 | ||
|
|
234852ca61 | ||
|
|
809105b67e | ||
|
|
02e8c31506 | ||
|
|
19b39a5c04 | ||
|
|
28e2731594 | ||
|
|
b1a279cbcc | ||
|
|
352a6a741a | ||
|
|
109015567a | ||
|
|
9e0fa77ca2 | ||
|
|
335b11c698 | ||
|
|
8e433355e6 | ||
|
|
3504f017b9 | ||
|
|
cd2f8077fa | ||
|
|
d5b68a91d2 | ||
|
|
623c7dcea5 | ||
|
|
ecbd6d86cd | ||
|
|
7200344ace | ||
|
|
b313ac4daa | ||
|
|
f2f312b43a | ||
|
|
6f6d20e1ba | ||
|
|
3231c3d930 | ||
|
|
b604e21c69 | ||
|
|
3c66db9845 | ||
|
|
f6ab1f7f61 | ||
|
|
8e40465e86 | ||
|
|
37dffd0fce | ||
|
|
e7c0d94b44 | ||
|
|
8102142007 | ||
|
|
7c6dec5d47 | ||
|
|
dd10c0c5d0 | ||
|
|
34fadecc2c | ||
|
|
cb8867fcc1 | ||
|
|
092ed06833 | ||
|
|
6308f1c35d | ||
|
|
ce10c9f120 | ||
|
|
6c4736fc8f | ||
|
|
b301b791c7 | ||
|
|
19d34e2eb8 | ||
|
|
a3748af772 | ||
|
|
9b765ef696 | ||
|
|
8f493cccc4 | ||
|
|
31a033dff1 | ||
|
|
8c3337b88b | ||
|
|
7238243664 | ||
|
|
ba2b15ab24 | ||
|
|
28dc8822b7 | ||
|
|
358c5055e9 | ||
|
|
b6cd40e6d3 | ||
|
|
7d96d8070d | ||
|
|
d482fb5f26 | ||
|
|
60402ce1fc | ||
|
|
1e3950c847 | ||
|
|
ed550594da | ||
|
|
3bbae29f93 | ||
|
|
3b74f8cd9a | ||
|
|
e9bdb91e01 | ||
|
|
1aa024ed6b | ||
|
|
13e8d36e1a | ||
|
|
5606c23768 | ||
|
|
0b675d6c02 | ||
|
|
c1db3a36ad | ||
|
|
c59dbb4f9e | ||
|
|
df6b306fce | ||
|
|
9d45718e5f | ||
|
|
b91ed7a78a | ||
|
|
95386d777b | ||
|
|
635809c376 | ||
|
|
af6bb2a6aa | ||
|
|
a797494aa3 | ||
|
|
353dd7f796 | ||
|
|
1c00d64952 | ||
|
|
ff5cf3f4fa | ||
|
|
5b6b2f427a | ||
|
|
7877184bee | ||
|
|
e9cb37122e | ||
|
|
a425392a2b | ||
|
|
75acbcc115 | ||
|
|
30415cefbe | ||
|
|
1d06a0019f | ||
|
|
3686075a7f | ||
|
|
6c1c7e5cc0 | ||
|
|
c4f901b201 | ||
|
|
4b7acb1389 | ||
|
|
15b7169df4 | ||
|
|
861948bcf3 | ||
|
|
e5ffd39cf2 | ||
|
|
8b353da0d2 | ||
|
|
49bde82426 | ||
|
|
3e285aaec4 | ||
|
|
355fc576b1 | ||
|
|
a69d72aa20 | ||
|
|
e5d123c5d3 | ||
|
|
220eb33f88 | ||
|
|
5238850036 | ||
|
|
81ac963567 | ||
|
|
3c21a9a520 | ||
|
|
1dc1dd1f07 | ||
|
|
c9ea9bce81 | ||
|
|
9f08353d31 | ||
|
|
ce0c3626c2 | ||
|
|
06f46206db | ||
|
|
579f0c06af | ||
|
|
b12d92acc9 | ||
|
|
e700ce15e5 | ||
|
|
7dbef7d559 | ||
|
|
7e9cdd8b07 | ||
|
|
cee6bc6b5d | ||
|
|
cfd23c05b4 | ||
|
|
0c1acd72ca | ||
|
|
e2ca06dcca | ||
|
|
0828fd787d | ||
|
|
2e23ea68d4 | ||
|
|
4afa822bec | ||
|
|
f2ca9b40db | ||
|
|
4c2535cb22 | ||
|
|
d4ea8787c9 | ||
|
|
a4de04528a | ||
|
|
f60aae7499 | ||
|
|
de8f9e9eee | ||
|
|
cace9db12f | ||
|
|
ec2fb82836 |
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,56 +0,0 @@
|
|||||||
name: "Bug report"
|
|
||||||
description: Bug report
|
|
||||||
labels: [bug]
|
|
||||||
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 and not because of your operation or version issues**
|
|
||||||
感谢您花时间填写此错误报告,请**务必确认您的issue不是重复的且不是因为您的操作或版本问题**
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Please make sure of the following things
|
|
||||||
description: You may select more than one, even select all.
|
|
||||||
options:
|
|
||||||
- label: I have read the [documentation](https://alist.nn.ci).
|
|
||||||
- label: I'm sure there are no duplicate issues or discussions.
|
|
||||||
- label: I'm sure it's due to `alist` and not something else(such as `Dependencies` or `Operational`).
|
|
||||||
- label: I'm sure I'm using the latest version
|
|
||||||
- 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: input
|
|
||||||
id: driver
|
|
||||||
attributes:
|
|
||||||
label: Driver used / 使用的存储驱动
|
|
||||||
description: What storage driver are you using?
|
|
||||||
placeholder: "for example: Onedrive"
|
|
||||||
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. Please be aware that your issue may be closed directly if you don't provide it.
|
|
||||||
请提供能复现此问题的链接,请知悉如果不提供它你的issue可能会被直接关闭。
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: logs
|
|
||||||
attributes:
|
|
||||||
label: Logs / 日志
|
|
||||||
description: |
|
|
||||||
Please copy and paste any relevant log output.
|
|
||||||
请复制粘贴错误日志,或者截图
|
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +0,0 @@
|
|||||||
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.
|
|
||||||
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,33 +0,0 @@
|
|||||||
name: "Feature request"
|
|
||||||
description: Feature request
|
|
||||||
labels: [enhancement]
|
|
||||||
body:
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Please make sure of the following things
|
|
||||||
description: You may select more than one, even select all.
|
|
||||||
options:
|
|
||||||
- label: I have read the [documentation](https://alist.nn.ci).
|
|
||||||
- label: I'm sure there are no duplicate issues or discussions.
|
|
||||||
- label: I'm sure this feature is not implemented.
|
|
||||||
- label: I'm sure it's a reasonable and popular requirement.
|
|
||||||
- type: textarea
|
|
||||||
id: feature-description
|
|
||||||
attributes:
|
|
||||||
label: Description of the feature / 需求描述
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: suggested-solution
|
|
||||||
attributes:
|
|
||||||
label: Suggested solution / 实现思路
|
|
||||||
description: |
|
|
||||||
Solutions to achieve this requirement.
|
|
||||||
实现此需求的解决思路。
|
|
||||||
- type: textarea
|
|
||||||
id: additional-context
|
|
||||||
attributes:
|
|
||||||
label: Additional context / 附件
|
|
||||||
description: |
|
|
||||||
Any other context or screenshots about the feature request here, or information you find helpful.
|
|
||||||
相关的任何其他上下文或截图,或者你觉得有帮助的信息
|
|
||||||
21
.github/config.yml
vendored
21
.github/config.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
# 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
|
|
||||||
19
.github/stale.yml
vendored
19
.github/stale.yml
vendored
@@ -1,19 +0,0 @@
|
|||||||
# Number of days of inactivity before an issue becomes stale
|
|
||||||
daysUntilStale: 44
|
|
||||||
# Number of days of inactivity before a stale issue is closed
|
|
||||||
daysUntilClose: 20
|
|
||||||
# Issues with these labels will never be considered stale
|
|
||||||
exemptLabels:
|
|
||||||
- accepted
|
|
||||||
- security
|
|
||||||
# Label to use when marking an issue as stale
|
|
||||||
staleLabel: stale
|
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
|
||||||
markComment: >
|
|
||||||
This issue has been automatically marked as stale because it has not had
|
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
|
||||||
for your contributions.
|
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
|
||||||
closeComment: >
|
|
||||||
This issue was closed due to inactive more than 52 days. You can reopen or
|
|
||||||
recreate it if you think it should continue. Thank you for your contributions again.
|
|
||||||
67
.github/workflows/auto_lang.yml
vendored
67
.github/workflows/auto_lang.yml
vendored
@@ -1,67 +0,0 @@
|
|||||||
name: auto_lang
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'main'
|
|
||||||
paths:
|
|
||||||
- 'drivers/**'
|
|
||||||
- 'internal/bootstrap/data/setting.go'
|
|
||||||
- 'internal/conf/const.go'
|
|
||||||
- 'cmd/lang.go'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto_lang:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform: [ ubuntu-latest ]
|
|
||||||
go-version: [ '1.20' ]
|
|
||||||
name: auto generate lang.json
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go-version }}
|
|
||||||
|
|
||||||
- name: Checkout alist
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
path: alist
|
|
||||||
|
|
||||||
- name: Checkout alist-web
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: 'alist-org/alist-web'
|
|
||||||
ref: main
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
path: alist-web
|
|
||||||
|
|
||||||
- name: Generate lang
|
|
||||||
run: |
|
|
||||||
cd alist
|
|
||||||
go run ./main.go lang
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Copy lang file
|
|
||||||
run: |
|
|
||||||
cp -f ./alist/lang/*.json ./alist-web/src/lang/en/ 2>/dev/null || :
|
|
||||||
|
|
||||||
- name: Commit git
|
|
||||||
run: |
|
|
||||||
cd alist-web
|
|
||||||
git add .
|
|
||||||
git config --local user.email "i@nn.ci"
|
|
||||||
git config --local user.name "Andy Hsu"
|
|
||||||
git commit -m "chore: auto update i18n file" -a 2>/dev/null || :
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Push lang files
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.MY_TOKEN }}
|
|
||||||
branch: main
|
|
||||||
directory: alist-web
|
|
||||||
repository: alist-org/alist-web
|
|
||||||
41
.github/workflows/build.yml
vendored
41
.github/workflows/build.yml
vendored
@@ -1,41 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ 'main' ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ 'main' ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform: [ubuntu-latest]
|
|
||||||
go-version: [ '1.20' ]
|
|
||||||
name: Build
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go-version }}
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo snap install zig --classic --beta
|
|
||||||
docker pull crazymax/xgo:latest
|
|
||||||
go install github.com/crazy-max/xgo@latest
|
|
||||||
sudo apt install upx
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
bash build.sh dev
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: alist
|
|
||||||
path: dist
|
|
||||||
65
.github/workflows/build_docker.yml
vendored
65
.github/workflows/build_docker.yml
vendored
@@ -1,65 +0,0 @@
|
|||||||
name: build_docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_docker:
|
|
||||||
name: Build docker
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: xhofe/alist
|
|
||||||
- name: Replace release with dev
|
|
||||||
run: |
|
|
||||||
sed -i 's/release/dev/g' Dockerfile
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: xhofe
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
|
|
||||||
build_docker_with_aria2:
|
|
||||||
needs: build_docker
|
|
||||||
name: Build docker with aria2
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: alist-org/with_aria2
|
|
||||||
ref: main
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Commit
|
|
||||||
run: |
|
|
||||||
git config --local user.email "i@nn.ci"
|
|
||||||
git config --local user.name "Noah Hsu"
|
|
||||||
git commit --allow-empty -m "Trigger build for ${{ github.sha }}"
|
|
||||||
|
|
||||||
- name: Push commit
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.MY_TOKEN }}
|
|
||||||
branch: main
|
|
||||||
repository: alist-org/with_aria2
|
|
||||||
22
.github/workflows/issue_close_question.yml
vendored
22
.github/workflows/issue_close_question.yml
vendored
@@ -1,22 +0,0 @@
|
|||||||
name: Close need info
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 */1 * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
close-need-info:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: close-issues
|
|
||||||
uses: actions-cool/issues-helper@v3
|
|
||||||
with:
|
|
||||||
actions: 'close-issues'
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
labels: 'question'
|
|
||||||
inactive-day: 3
|
|
||||||
close-reason: 'not_planned'
|
|
||||||
body: |
|
|
||||||
Hello @${{ github.event.issue.user.login }}, this issue was closed due to no activities in 3 days.
|
|
||||||
你好 @${{ github.event.issue.user.login }},此issue因超过3天未回复被关闭。
|
|
||||||
21
.github/workflows/issue_close_stale.yml
vendored
21
.github/workflows/issue_close_stale.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
name: Close inactive
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 */7 * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
close-inactive:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: close-issues
|
|
||||||
uses: actions-cool/issues-helper@v3
|
|
||||||
with:
|
|
||||||
actions: 'close-issues'
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
labels: 'stale'
|
|
||||||
inactive-day: 8
|
|
||||||
close-reason: 'not_planned'
|
|
||||||
body: |
|
|
||||||
Hello @${{ github.event.issue.user.login }}, this issue was closed due to inactive more than 52 days. You can reopen or recreate it if you think it should continue. Thank you for your contributions again.
|
|
||||||
25
.github/workflows/issue_duplicate.yml
vendored
25
.github/workflows/issue_duplicate.yml
vendored
@@ -1,25 +0,0 @@
|
|||||||
name: Issue Duplicate
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [labeled]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-comment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event.label.name == 'duplicate'
|
|
||||||
steps:
|
|
||||||
- name: Create comment
|
|
||||||
uses: actions-cool/issues-helper@v3
|
|
||||||
with:
|
|
||||||
actions: 'create-comment'
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
issue-number: ${{ github.event.issue.number }}
|
|
||||||
body: |
|
|
||||||
Hello @${{ github.event.issue.user.login }}, your issue is a duplicate and will be closed.
|
|
||||||
你好 @${{ github.event.issue.user.login }},你的issue是重复的,将被关闭。
|
|
||||||
- name: Close issue
|
|
||||||
uses: actions-cool/issues-helper@v3
|
|
||||||
with:
|
|
||||||
actions: 'close-issue'
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
25
.github/workflows/issue_invalid.yml
vendored
25
.github/workflows/issue_invalid.yml
vendored
@@ -1,25 +0,0 @@
|
|||||||
name: Issue Invalid
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [labeled]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-comment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event.label.name == 'invalid'
|
|
||||||
steps:
|
|
||||||
- name: Create comment
|
|
||||||
uses: actions-cool/issues-helper@v3
|
|
||||||
with:
|
|
||||||
actions: 'create-comment'
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
issue-number: ${{ github.event.issue.number }}
|
|
||||||
body: |
|
|
||||||
Hello @${{ github.event.issue.user.login }}, your issue is invalid and will be closed.
|
|
||||||
你好 @${{ github.event.issue.user.login }},你的issue无效,将被关闭。
|
|
||||||
- name: Close issue
|
|
||||||
uses: actions-cool/issues-helper@v3
|
|
||||||
with:
|
|
||||||
actions: 'close-issue'
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
20
.github/workflows/issue_question.yml
vendored
20
.github/workflows/issue_question.yml
vendored
@@ -1,20 +0,0 @@
|
|||||||
name: Issue Question
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [labeled]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-comment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event.label.name == 'question'
|
|
||||||
steps:
|
|
||||||
- name: Create comment
|
|
||||||
uses: actions-cool/issues-helper@v3.4.0
|
|
||||||
with:
|
|
||||||
actions: 'create-comment'
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
issue-number: ${{ github.event.issue.number }}
|
|
||||||
body: |
|
|
||||||
Hello @${{ github.event.issue.user.login }}, please input issue by template and add detail. Issues labeled by `question` will be closed if no activities in 3 days.
|
|
||||||
你好 @${{ github.event.issue.user.login }},请按照issue模板填写, 并详细说明问题/日志记录/复现步骤/复现链接/实现思路或提供更多信息等, 3天内未回复issue自动关闭。
|
|
||||||
19
.github/workflows/issue_similarity.yml
vendored
19
.github/workflows/issue_similarity.yml
vendored
@@ -1,19 +0,0 @@
|
|||||||
name: Issues Similarity Analysis
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened, edited]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
similarity-analysis:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: analysis
|
|
||||||
uses: actions-cool/issues-similarity-analysis@v1
|
|
||||||
with:
|
|
||||||
filter-threshold: 0.5
|
|
||||||
comment-title: '### See'
|
|
||||||
comment-body: '${index}. ${similarity} #${number}'
|
|
||||||
show-footer: false
|
|
||||||
show-mentioned: true
|
|
||||||
since-days: 730
|
|
||||||
13
.github/workflows/issue_translate.yml
vendored
13
.github/workflows/issue_translate.yml
vendored
@@ -1,13 +0,0 @@
|
|||||||
name: Translation Helper
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
translate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions-cool/translation-helper@v1.2.0
|
|
||||||
25
.github/workflows/issue_wontfix.yml
vendored
25
.github/workflows/issue_wontfix.yml
vendored
@@ -1,25 +0,0 @@
|
|||||||
name: Issue Wontfix
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [labeled]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lock-issue:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event.label.name == 'wontfix'
|
|
||||||
steps:
|
|
||||||
- name: Create comment
|
|
||||||
uses: actions-cool/issues-helper@v3
|
|
||||||
with:
|
|
||||||
actions: 'create-comment'
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
issue-number: ${{ github.event.issue.number }}
|
|
||||||
body: |
|
|
||||||
Hello @${{ github.event.issue.user.login }}, this issue will not be worked on and will be closed.
|
|
||||||
你好 @${{ github.event.issue.user.login }},这不会被处理,将被关闭。
|
|
||||||
- name: Close issue
|
|
||||||
uses: actions-cool/issues-helper@v3
|
|
||||||
with:
|
|
||||||
actions: 'close-issue'
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
81
.github/workflows/release.yml
vendored
81
.github/workflows/release.yml
vendored
@@ -1,81 +0,0 @@
|
|||||||
name: release
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [ published ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform: [ ubuntu-latest ]
|
|
||||||
go-version: [ '1.20' ]
|
|
||||||
name: Release
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- name: Prerelease
|
|
||||||
uses: irongut/EditRelease@v1.2.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MY_TOKEN }}
|
|
||||||
id: ${{ github.event.release.id }}
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go-version }}
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo snap install zig --classic --beta
|
|
||||||
docker pull crazymax/xgo:latest
|
|
||||||
go install github.com/crazy-max/xgo@latest
|
|
||||||
sudo apt install upx
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
bash build.sh release
|
|
||||||
|
|
||||||
- name: Release latest
|
|
||||||
uses: irongut/EditRelease@v1.2.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MY_TOKEN }}
|
|
||||||
id: ${{ github.event.release.id }}
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
- name: Upload assets
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: build/compress/*
|
|
||||||
|
|
||||||
release_desktop:
|
|
||||||
needs: release
|
|
||||||
name: Release desktop
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: alist-org/desktop-release
|
|
||||||
ref: main
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Add tag
|
|
||||||
run: |
|
|
||||||
git config --local user.email "i@nn.ci"
|
|
||||||
git config --local user.name "Andy Hsu"
|
|
||||||
version=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
|
||||||
git tag -a $version -m "release $version"
|
|
||||||
|
|
||||||
- name: Push tags
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.MY_TOKEN }}
|
|
||||||
branch: main
|
|
||||||
repository: alist-org/desktop-release
|
|
||||||
68
.github/workflows/release_docker.yml
vendored
68
.github/workflows/release_docker.yml
vendored
@@ -1,68 +0,0 @@
|
|||||||
name: release_docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release_docker:
|
|
||||||
name: Release Docker
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: xhofe/alist
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: xhofe
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
|
||||||
|
|
||||||
release_docker_with_aria2:
|
|
||||||
needs: release_docker
|
|
||||||
name: Release docker with aria2
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: alist-org/with_aria2
|
|
||||||
ref: main
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Add tag
|
|
||||||
run: |
|
|
||||||
git config --local user.email "i@nn.ci"
|
|
||||||
git config --local user.name "Andy Hsu"
|
|
||||||
git tag -a ${{ github.ref_name }} -m "release ${{ github.ref_name }}"
|
|
||||||
|
|
||||||
- name: Push tags
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.MY_TOKEN }}
|
|
||||||
branch: main
|
|
||||||
repository: alist-org/with_aria2
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,8 +24,11 @@ output/
|
|||||||
*.json
|
*.json
|
||||||
/build
|
/build
|
||||||
/data/
|
/data/
|
||||||
|
/tmp/
|
||||||
/log/
|
/log/
|
||||||
/lang/
|
/lang/
|
||||||
/daemon/
|
/daemon/
|
||||||
/public/dist/*
|
/public/dist/*
|
||||||
/!public/dist/README.md
|
/!public/dist/README.md
|
||||||
|
|
||||||
|
.VSCodeCounter
|
||||||
18
Dockerfile
18
Dockerfile
@@ -1,18 +0,0 @@
|
|||||||
FROM alpine:3.18 as builder
|
|
||||||
LABEL stage=go-builder
|
|
||||||
WORKDIR /app/
|
|
||||||
COPY ./ ./
|
|
||||||
RUN apk add --no-cache bash curl gcc git go musl-dev; \
|
|
||||||
bash build.sh release docker
|
|
||||||
|
|
||||||
FROM alpine:3.18
|
|
||||||
LABEL MAINTAINER="i@nn.ci"
|
|
||||||
VOLUME /opt/alist/data/
|
|
||||||
WORKDIR /opt/alist/
|
|
||||||
COPY --from=builder /app/bin/alist ./
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
RUN apk add --no-cache bash ca-certificates su-exec tzdata; \
|
|
||||||
chmod +x /entrypoint.sh
|
|
||||||
ENV PUID=0 PGID=0 UMASK=022
|
|
||||||
EXPOSE 5244 5245
|
|
||||||
CMD [ "/entrypoint.sh" ]
|
|
||||||
138
README.md
138
README.md
@@ -1,138 +0,0 @@
|
|||||||
<div align="center">
|
|
||||||
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
|
||||||
<p><em>🗂️A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>
|
|
||||||
<div>
|
|
||||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
|
||||||
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
|
|
||||||
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
|
|
||||||
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Xhofe/alist/releases">
|
|
||||||
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
|
||||||
</a>
|
|
||||||
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
|
||||||
<img src="https://badges.crowdin.net/alist/localized.svg">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="https://github.com/Xhofe/alist/discussions">
|
|
||||||
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
|
||||||
</a>
|
|
||||||
<a href="https://discord.gg/F4ymsH4xv2">
|
|
||||||
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Xhofe/alist/releases">
|
|
||||||
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
|
||||||
</a>
|
|
||||||
<a href="https://hub.docker.com/r/xhofe/alist">
|
|
||||||
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
|
|
||||||
</a>
|
|
||||||
<a href="https://alist.nn.ci/guide/sponsor.html">
|
|
||||||
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
|
|
||||||
|
|
||||||
## 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) (Personal, Family)
|
|
||||||
- [x] [GoogleDrive](https://drive.google.com/)
|
|
||||||
- [x] [123pan](https://www.123pan.com/)
|
|
||||||
- [x] FTP / SFTP
|
|
||||||
- [x] [PikPak](https://www.mypikpak.com/)
|
|
||||||
- [x] [S3](https://aws.amazon.com/s3/)
|
|
||||||
- [x] [Seafile](https://seafile.com/)
|
|
||||||
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
|
|
||||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
|
||||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
|
||||||
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
|
||||||
- [x] [139yun](https://yun.139.com/) (Personal, Family)
|
|
||||||
- [x] [YandexDisk](https://disk.yandex.com/)
|
|
||||||
- [x] [BaiduNetdisk](http://pan.baidu.com/)
|
|
||||||
- [x] [Terabox](https://www.terabox.com/main)
|
|
||||||
- [x] [UC](https://drive.uc.cn)
|
|
||||||
- [x] [Quark](https://pan.quark.cn)
|
|
||||||
- [x] [Thunder](https://pan.xunlei.com)
|
|
||||||
- [x] [Lanzou](https://www.lanzou.com/)
|
|
||||||
- [x] [Aliyundrive share](https://www.aliyundrive.com/)
|
|
||||||
- [x] [Google photo](https://photos.google.com/)
|
|
||||||
- [x] [Mega.nz](https://mega.nz)
|
|
||||||
- [x] [Baidu photo](https://photo.baidu.com/)
|
|
||||||
- [x] SMB
|
|
||||||
- [x] [115](https://115.com/)
|
|
||||||
- [X] Cloudreve
|
|
||||||
- [x] [Dropbox](https://www.dropbox.com/)
|
|
||||||
- [x] Easy to deploy and out-of-the-box
|
|
||||||
- [x] File preview (PDF, markdown, code, plain text, ...)
|
|
||||||
- [x] Image preview in gallery mode
|
|
||||||
- [x] Video and audio preview, support lyrics and subtitles
|
|
||||||
- [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.nn.ci/guide/webdav.html for details)
|
|
||||||
- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist)
|
|
||||||
- [x] Cloudflare workers proxy
|
|
||||||
- [x] File/Folder package download
|
|
||||||
- [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy
|
|
||||||
- [x] Offline download
|
|
||||||
- [x] Copy files between two storage
|
|
||||||
|
|
||||||
## Document
|
|
||||||
|
|
||||||
<https://alist.nn.ci/>
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<https://al.nn.ci>
|
|
||||||
|
|
||||||
## 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.**
|
|
||||||
|
|
||||||
## Sponsor
|
|
||||||
|
|
||||||
AList is an open-source software, if you happen to like this project and want me to keep going, please consider sponsoring me or providing a single donation! Thanks for all the love and support:
|
|
||||||
https://alist.nn.ci/guide/sponsor.html
|
|
||||||
|
|
||||||
### Special sponsors
|
|
||||||
|
|
||||||
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (sponsored Chinese API server)
|
|
||||||
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.la/)
|
|
||||||
- [KinhDown 百度云盘不限速下载!永久免费!已稳定运行3年!非常可靠!Q群 -> 786799372](https://kinhdown.com)
|
|
||||||
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
|
|
||||||
|
|
||||||
## Contributors
|
|
||||||
|
|
||||||
Thanks goes to these wonderful people:
|
|
||||||
|
|
||||||
[](https://github.com/alist-org/alist/graphs/contributors)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
The `AList` is open-source software licensed under the AGPL-3.0 license.
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
- This program is a free and open source project. It is designed to share files on the network disk, which is convenient for downloading and learning golang. Please abide by relevant laws and regulations when using it, and do not abuse it;
|
|
||||||
- This program is implemented by calling the official sdk/interface, without destroying the official interface behavior;
|
|
||||||
- This program only does 302 redirect/traffic forwarding, and does not intercept, store, or tamper with any user data;
|
|
||||||
- Before using this program, you should understand and bear the corresponding risks, including but not limited to account ban, download speed limit, etc., which is none of this program's business;
|
|
||||||
- If there is any infringement, please contact me by [email](mailto:i@nn.ci), and it will be dealt with in time.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
|
||||||
136
README_cn.md
136
README_cn.md
@@ -1,136 +0,0 @@
|
|||||||
<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 和 Solidjs。</em></p>
|
|
||||||
<div>
|
|
||||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
|
||||||
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
|
|
||||||
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
|
|
||||||
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Xhofe/alist/releases">
|
|
||||||
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
|
||||||
</a>
|
|
||||||
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
|
||||||
<img src="https://badges.crowdin.net/alist/localized.svg">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="https://github.com/Xhofe/alist/discussions">
|
|
||||||
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
|
||||||
</a>
|
|
||||||
<a href="https://discord.gg/F4ymsH4xv2">
|
|
||||||
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Xhofe/alist/releases">
|
|
||||||
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
|
||||||
</a>
|
|
||||||
<a href="https://hub.docker.com/r/xhofe/alist">
|
|
||||||
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
|
|
||||||
</a>
|
|
||||||
<a href="https://alist.nn.ci/zh/guide/sponsor.html">
|
|
||||||
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[English](./README.md) | 中文 | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.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] FTP / SFTP
|
|
||||||
- [x] [PikPak](https://www.mypikpak.com/)
|
|
||||||
- [x] [S3](https://aws.amazon.com/cn/s3/)
|
|
||||||
- [x] [Seafile](https://seafile.com/)
|
|
||||||
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
|
|
||||||
- [x] WebDav(支持无API的OneDrive/SharePoint)
|
|
||||||
- [x] Teambition([中国](https://www.teambition.com/ ),[国际](https://us.teambition.com/ ))
|
|
||||||
- [x] [分秒帧](https://www.mediatrack.cn/)
|
|
||||||
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
|
|
||||||
- [x] [Yandex.Disk](https://disk.yandex.com/)
|
|
||||||
- [x] [百度网盘](http://pan.baidu.com/)
|
|
||||||
- [x] [UC网盘](https://drive.uc.cn)
|
|
||||||
- [x] [夸克网盘](https://pan.quark.cn)
|
|
||||||
- [x] [迅雷网盘](https://pan.xunlei.com)
|
|
||||||
- [x] [蓝奏云](https://www.lanzou.com/)
|
|
||||||
- [x] [阿里云盘分享](https://www.aliyundrive.com/)
|
|
||||||
- [x] [谷歌相册](https://photos.google.com/)
|
|
||||||
- [x] [Mega.nz](https://mega.nz)
|
|
||||||
- [x] [一刻相册](https://photo.baidu.com/)
|
|
||||||
- [x] SMB
|
|
||||||
- [x] [115](https://115.com/)
|
|
||||||
- [X] Cloudreve
|
|
||||||
- [x] [Dropbox](https://www.dropbox.com/)
|
|
||||||
- [x] 部署方便,开箱即用
|
|
||||||
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
|
||||||
- [x] 画廊模式下的图像预览
|
|
||||||
- [x] 视频和音频预览,支持歌词和字幕
|
|
||||||
- [x] Office 文档预览(docx、pptx、xlsx、...)
|
|
||||||
- [x] `README.md` 预览渲染
|
|
||||||
- [x] 文件永久链接复制和直接文件下载
|
|
||||||
- [x] 黑暗模式
|
|
||||||
- [x] 国际化
|
|
||||||
- [x] 受保护的路由(密码保护和身份验证)
|
|
||||||
- [x] WebDav (具体见 https://alist.nn.ci/zh/guide/webdav.html)
|
|
||||||
- [x] [Docker 部署](https://hub.docker.com/r/xhofe/alist)
|
|
||||||
- [x] Cloudflare workers 中转
|
|
||||||
- [x] 文件/文件夹打包下载
|
|
||||||
- [x] 网页上传(可以允许访客上传),删除,新建文件夹,重命名,移动,复制
|
|
||||||
- [x] 离线下载
|
|
||||||
- [x] 跨存储复制文件
|
|
||||||
|
|
||||||
## 文档
|
|
||||||
|
|
||||||
<https://alist.nn.ci/zh/>
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<https://al.nn.ci>
|
|
||||||
|
|
||||||
## 讨论
|
|
||||||
|
|
||||||
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) ,**issue仅针对错误报告和功能请求。**
|
|
||||||
|
|
||||||
## 赞助
|
|
||||||
|
|
||||||
AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我继续下去,请考虑赞助我或提供一个单一的捐款!感谢所有的爱和支持:https://alist.nn.ci/zh/guide/sponsor.html
|
|
||||||
|
|
||||||
### 特别赞助
|
|
||||||
|
|
||||||
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (国内API服务器赞助)
|
|
||||||
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.la/)
|
|
||||||
- [KinhDown 百度云盘不限速下载!永久免费!已稳定运行3年!非常可靠!Q群 -> 786799372](https://kinhdown.com)
|
|
||||||
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
|
|
||||||
|
|
||||||
## 贡献者
|
|
||||||
|
|
||||||
Thanks goes to these wonderful people:
|
|
||||||
|
|
||||||
[](https://github.com/alist-org/alist/graphs/contributors)
|
|
||||||
|
|
||||||
## 许可
|
|
||||||
|
|
||||||
`AList` 是在 AGPL-3.0 许可下许可的开源软件。
|
|
||||||
|
|
||||||
## 免责声明
|
|
||||||
- 本程序为免费开源项目,旨在分享网盘文件,方便下载以及学习golang,使用时请遵守相关法律法规,请勿滥用;
|
|
||||||
- 本程序通过调用官方sdk/接口实现,无破坏官方接口行为;
|
|
||||||
- 本程序仅做302重定向/流量转发,不拦截、存储、篡改任何用户数据;
|
|
||||||
- 在使用本程序之前,你应了解并承担相应的风险,包括但不限于账号被ban,下载限速等,与本程序无关;
|
|
||||||
- 如有侵权,请通过[邮件](mailto:i@nn.ci)与我联系,会及时处理。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
|
||||||
11
buf.gen.yaml
Normal file
11
buf.gen.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
version: v1
|
||||||
|
plugins:
|
||||||
|
- plugin: buf.build/protocolbuffers/go:v1.36.7
|
||||||
|
out: .
|
||||||
|
opt:
|
||||||
|
- paths=source_relative
|
||||||
|
- plugin: buf.build/grpc/go:v1.5.1
|
||||||
|
out: .
|
||||||
|
opt:
|
||||||
|
- paths=source_relative
|
||||||
|
- require_unimplemented_servers=false
|
||||||
164
build.sh
164
build.sh
@@ -1,164 +0,0 @@
|
|||||||
appName="alist"
|
|
||||||
builtAt="$(date +'%F %T %z')"
|
|
||||||
goVersion=$(go version | sed 's/go version //')
|
|
||||||
gitAuthor="Xhofe <i@nn.ci>"
|
|
||||||
gitCommit=$(git log --pretty=format:"%h" -1)
|
|
||||||
|
|
||||||
if [ "$1" = "dev" ]; then
|
|
||||||
version="dev"
|
|
||||||
webVersion="dev"
|
|
||||||
else
|
|
||||||
version=$(git describe --abbrev=0 --tags)
|
|
||||||
webVersion=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "backend version: $version"
|
|
||||||
echo "frontend version: $webVersion"
|
|
||||||
|
|
||||||
ldflags="\
|
|
||||||
-w -s \
|
|
||||||
-X 'github.com/alist-org/alist/v3/internal/conf.BuiltAt=$builtAt' \
|
|
||||||
-X 'github.com/alist-org/alist/v3/internal/conf.GoVersion=$goVersion' \
|
|
||||||
-X 'github.com/alist-org/alist/v3/internal/conf.GitAuthor=$gitAuthor' \
|
|
||||||
-X 'github.com/alist-org/alist/v3/internal/conf.GitCommit=$gitCommit' \
|
|
||||||
-X 'github.com/alist-org/alist/v3/internal/conf.Version=$version' \
|
|
||||||
-X 'github.com/alist-org/alist/v3/internal/conf.WebVersion=$webVersion' \
|
|
||||||
"
|
|
||||||
|
|
||||||
FetchWebDev() {
|
|
||||||
curl -L https://codeload.github.com/alist-org/web-dist/tar.gz/refs/heads/dev -o web-dist-dev.tar.gz
|
|
||||||
tar -zxvf web-dist-dev.tar.gz
|
|
||||||
rm -rf public/dist
|
|
||||||
mv -f web-dist-dev/dist public
|
|
||||||
rm -rf web-dist-dev web-dist-dev.tar.gz
|
|
||||||
}
|
|
||||||
|
|
||||||
FetchWebRelease() {
|
|
||||||
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 -rf public/dist
|
|
||||||
mv -f dist public
|
|
||||||
rm -rf dist.tar.gz
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildWinArm64() {
|
|
||||||
echo building for windows-arm64
|
|
||||||
chmod +x ./wrapper/zcc-arm64
|
|
||||||
chmod +x ./wrapper/zcxx-arm64
|
|
||||||
export GOOS=windows
|
|
||||||
export GOARCH=arm64
|
|
||||||
export CC=$(pwd)/wrapper/zcc-arm64
|
|
||||||
export CXX=$(pwd)/wrapper/zcxx-arm64
|
|
||||||
go build -o "$1" -ldflags="$ldflags" -tags=jsoniter .
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildDev() {
|
|
||||||
rm -rf .git/
|
|
||||||
mkdir -p "dist"
|
|
||||||
muslflags="--extldflags '-static -fpic' $ldflags"
|
|
||||||
BASE="https://musl.nn.ci/"
|
|
||||||
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross)
|
|
||||||
for i in "${FILES[@]}"; do
|
|
||||||
url="${BASE}${i}.tgz"
|
|
||||||
curl -L -o "${i}.tgz" "${url}"
|
|
||||||
sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
|
|
||||||
done
|
|
||||||
OS_ARCHES=(linux-musl-amd64 linux-musl-arm64)
|
|
||||||
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc)
|
|
||||||
for i in "${!OS_ARCHES[@]}"; do
|
|
||||||
os_arch=${OS_ARCHES[$i]}
|
|
||||||
cgo_cc=${CGO_ARGS[$i]}
|
|
||||||
echo building for ${os_arch}
|
|
||||||
export GOOS=${os_arch%%-*}
|
|
||||||
export GOARCH=${os_arch##*-}
|
|
||||||
export CC=${cgo_cc}
|
|
||||||
export CGO_ENABLED=1
|
|
||||||
go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
|
||||||
done
|
|
||||||
xgo -targets=windows/amd64,darwin/amd64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
|
||||||
mv alist-* dist
|
|
||||||
cd dist
|
|
||||||
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
|
|
||||||
upx -9 ./alist-windows-amd64-upx.exe
|
|
||||||
find . -type f -print0 | xargs -0 md5sum >md5.txt
|
|
||||||
cat md5.txt
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildDocker() {
|
|
||||||
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildRelease() {
|
|
||||||
rm -rf .git/
|
|
||||||
mkdir -p "build"
|
|
||||||
muslflags="--extldflags '-static -fpic' $ldflags"
|
|
||||||
BASE="https://musl.nn.ci/"
|
|
||||||
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
|
|
||||||
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="$muslflags" -tags=jsoniter .
|
|
||||||
done
|
|
||||||
BuildWinArm64 ./build/alist-windows-arm64.exe
|
|
||||||
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
|
||||||
# why? Because some target platforms seem to have issues with upx compression
|
|
||||||
upx -9 ./alist-linux-amd64
|
|
||||||
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
|
|
||||||
upx -9 ./alist-windows-amd64-upx.exe
|
|
||||||
mv alist-* build
|
|
||||||
}
|
|
||||||
|
|
||||||
MakeRelease() {
|
|
||||||
cd build
|
|
||||||
mkdir compress
|
|
||||||
for i in $(find . -type f -name "$appName-linux-*"); do
|
|
||||||
cp "$i" alist
|
|
||||||
tar -czvf compress/"$i".tar.gz alist
|
|
||||||
rm -f alist
|
|
||||||
done
|
|
||||||
for i in $(find . -type f -name "$appName-darwin-*"); do
|
|
||||||
cp "$i" alist
|
|
||||||
tar -czvf compress/"$i".tar.gz alist
|
|
||||||
rm -f alist
|
|
||||||
done
|
|
||||||
for i in $(find . -type f -name "$appName-windows-*"); do
|
|
||||||
cp "$i" alist.exe
|
|
||||||
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip alist.exe
|
|
||||||
rm -f alist.exe
|
|
||||||
done
|
|
||||||
cd compress
|
|
||||||
find . -type f -print0 | xargs -0 md5sum >md5.txt
|
|
||||||
cat md5.txt
|
|
||||||
cd ../..
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "$1" = "dev" ]; then
|
|
||||||
FetchWebDev
|
|
||||||
if [ "$2" = "docker" ]; then
|
|
||||||
BuildDocker
|
|
||||||
else
|
|
||||||
BuildDev
|
|
||||||
fi
|
|
||||||
elif [ "$1" = "release" ]; then
|
|
||||||
FetchWebRelease
|
|
||||||
if [ "$2" = "docker" ]; then
|
|
||||||
BuildDocker
|
|
||||||
else
|
|
||||||
BuildRelease
|
|
||||||
MakeRelease
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e "Parameter error"
|
|
||||||
fi
|
|
||||||
40
cmd/admin.go
40
cmd/admin.go
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PasswordCmd represents the password command
|
|
||||||
var PasswordCmd = &cobra.Command{
|
|
||||||
Use: "admin",
|
|
||||||
Aliases: []string{"password"},
|
|
||||||
Short: "Show admin user's info",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
Init()
|
|
||||||
admin, err := op.GetAdmin()
|
|
||||||
if err != nil {
|
|
||||||
utils.Log.Errorf("failed get admin user: %+v", err)
|
|
||||||
} else {
|
|
||||||
utils.Log.Infof("admin user's info: \nusername: %s\npassword: %s", admin.Username, admin.Password)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
RootCmd.AddCommand(PasswordCmd)
|
|
||||||
|
|
||||||
// Here you will define your flags and configuration settings.
|
|
||||||
|
|
||||||
// Cobra supports Persistent Flags which will work for this command
|
|
||||||
// and all subcommands, e.g.:
|
|
||||||
// passwordCmd.PersistentFlags().String("foo", "", "A help for foo")
|
|
||||||
|
|
||||||
// Cobra supports local flags which will only run when this command
|
|
||||||
// is called directly, e.g.:
|
|
||||||
// passwordCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
|
||||||
}
|
|
||||||
@@ -1,44 +1,42 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"context"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/bootstrap"
|
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
||||||
"github.com/alist-org/alist/v3/internal/bootstrap/data"
|
"github.com/OpenListTeam/OpenList/v5/internal/bootstrap"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/sirupsen/logrus"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init(ctx context.Context) {
|
||||||
|
if flags.Dev {
|
||||||
|
flags.Debug = true
|
||||||
|
}
|
||||||
|
initLogrus()
|
||||||
bootstrap.InitConfig()
|
bootstrap.InitConfig()
|
||||||
bootstrap.Log()
|
bootstrap.InitDriverPlugins()
|
||||||
bootstrap.InitDB()
|
|
||||||
data.InitData()
|
|
||||||
bootstrap.InitIndex()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var pid = -1
|
func Release() {
|
||||||
var pidFile string
|
|
||||||
|
|
||||||
func initDaemon() {
|
}
|
||||||
ex, err := os.Executable()
|
|
||||||
if err != nil {
|
func initLog(l *logrus.Logger) {
|
||||||
log.Fatal(err)
|
if flags.Debug {
|
||||||
}
|
l.SetLevel(logrus.DebugLevel)
|
||||||
exPath := filepath.Dir(ex)
|
l.SetReportCaller(true)
|
||||||
_ = os.MkdirAll(filepath.Join(exPath, "daemon"), 0700)
|
} else {
|
||||||
pidFile = filepath.Join(exPath, "daemon/pid")
|
l.SetLevel(logrus.InfoLevel)
|
||||||
if utils.Exists(pidFile) {
|
l.SetReportCaller(false)
|
||||||
bytes, err := os.ReadFile(pidFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("failed to read pid file", err)
|
|
||||||
}
|
|
||||||
id, err := strconv.Atoi(string(bytes))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("failed to parse pid data", err)
|
|
||||||
}
|
|
||||||
pid = id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func initLogrus() {
|
||||||
|
formatter := logrus.TextFormatter{
|
||||||
|
ForceColors: true,
|
||||||
|
EnvironmentOverrideColors: true,
|
||||||
|
TimestampFormat: "2006-01-02 15:04:05",
|
||||||
|
FullTimestamp: true,
|
||||||
|
}
|
||||||
|
logrus.SetFormatter(&formatter)
|
||||||
|
initLog(logrus.StandardLogger())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,40 @@
|
|||||||
package flags
|
package flags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
DataDir string
|
ConfigFile string
|
||||||
Debug bool
|
Debug bool
|
||||||
NoPrefix bool
|
NoPrefix bool
|
||||||
Dev bool
|
Dev bool
|
||||||
ForceBinDir bool
|
ForceBinDir bool
|
||||||
LogStd bool
|
LogStd bool
|
||||||
|
|
||||||
|
pwd string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Program working directory
|
||||||
|
func PWD() string {
|
||||||
|
if pwd != "" {
|
||||||
|
return pwd
|
||||||
|
}
|
||||||
|
if ForceBinDir {
|
||||||
|
ex, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
pwd = filepath.Dir(ex)
|
||||||
|
return pwd
|
||||||
|
}
|
||||||
|
d, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
pwd = d
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|||||||
14
cmd/root.go
14
cmd/root.go
@@ -4,16 +4,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/cmd/flags"
|
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var RootCmd = &cobra.Command{
|
var RootCmd = &cobra.Command{
|
||||||
Use: "alist",
|
Use: "openlist",
|
||||||
Short: "A file list program that supports multiple storage.",
|
Short: "A file list program that supports multiple storage.",
|
||||||
Long: `A file list program that supports multiple storage,
|
Long: `A file list program that supports multiple storage,
|
||||||
built with love by Xhofe and friends in Go/Solid.js.
|
built with love by OpenListTeam.
|
||||||
Complete documentation is available at https://alist.nn.ci/`,
|
Complete documentation is available at https://doc.oplist.org/`,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
@@ -24,10 +24,10 @@ func Execute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "data folder")
|
RootCmd.PersistentFlags().StringVarP(&flags.ConfigFile, "config", "c", "data/config.json", "config file")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
|
RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
|
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
|
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.ForceBinDir, "force-bin-dir", false, "Force to use the directory where the binary file is located as data directory")
|
RootCmd.PersistentFlags().BoolVarP(&flags.ForceBinDir, "force-bin-dir", "f", false, "force to use the directory where the binary file is located as data directory")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "Force to log to std")
|
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "force to log to std")
|
||||||
}
|
}
|
||||||
|
|||||||
126
cmd/server.go
126
cmd/server.go
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -12,15 +13,14 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/cmd/flags"
|
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
||||||
_ "github.com/alist-org/alist/v3/drivers"
|
"github.com/OpenListTeam/OpenList/v5/internal/conf"
|
||||||
"github.com/alist-org/alist/v3/internal/bootstrap"
|
"github.com/OpenListTeam/OpenList/v5/server"
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/alist-org/alist/v3/server"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerCmd represents the server command
|
// ServerCmd represents the server command
|
||||||
@@ -29,129 +29,131 @@ var ServerCmd = &cobra.Command{
|
|||||||
Short: "Start the server at the specified address",
|
Short: "Start the server at the specified address",
|
||||||
Long: `Start the server at the specified address
|
Long: `Start the server at the specified address
|
||||||
the address is defined in config file`,
|
the address is defined in config file`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
Init()
|
serverCtx, serverCancel := context.WithCancel(context.Background())
|
||||||
if conf.Conf.DelayedStart != 0 {
|
defer serverCancel()
|
||||||
utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
|
Init(serverCtx)
|
||||||
time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
|
|
||||||
}
|
if !flags.Debug {
|
||||||
bootstrap.InitAria2()
|
|
||||||
bootstrap.InitQbittorrent()
|
|
||||||
bootstrap.LoadStorages()
|
|
||||||
if !flags.Debug && !flags.Dev {
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
|
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out))
|
||||||
|
r.Use(gin.RecoveryWithWriter(log.StandardLogger().Out))
|
||||||
server.Init(r)
|
server.Init(r)
|
||||||
|
|
||||||
|
var httpHandler http.Handler = r
|
||||||
|
if conf.Conf.Scheme.EnableH2c {
|
||||||
|
httpHandler = h2c.NewHandler(r, &http2.Server{})
|
||||||
|
}
|
||||||
var httpSrv, httpsSrv, unixSrv *http.Server
|
var httpSrv, httpsSrv, unixSrv *http.Server
|
||||||
if conf.Conf.Scheme.HttpPort != -1 {
|
if conf.Conf.Scheme.HttpPort > 0 {
|
||||||
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
|
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
|
||||||
utils.Log.Infof("start HTTP server @ %s", httpBase)
|
log.Infoln("start HTTP server", "@", httpBase)
|
||||||
httpSrv = &http.Server{Addr: httpBase, Handler: r}
|
httpSrv = &http.Server{Addr: httpBase, Handler: httpHandler}
|
||||||
go func() {
|
go func() {
|
||||||
err := httpSrv.ListenAndServe()
|
err := httpSrv.ListenAndServe()
|
||||||
if err != nil && err != http.ErrServerClosed {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
utils.Log.Fatalf("failed to start http: %s", err.Error())
|
log.Errorln("start HTTP server", ":", err)
|
||||||
|
serverCancel()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if conf.Conf.Scheme.HttpsPort != -1 {
|
if conf.Conf.Scheme.HttpsPort > 0 {
|
||||||
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
|
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
|
||||||
utils.Log.Infof("start HTTPS server @ %s", httpsBase)
|
log.Infoln("start HTTPS server", "@", httpsBase)
|
||||||
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
|
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
|
||||||
go func() {
|
go func() {
|
||||||
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||||
if err != nil && err != http.ErrServerClosed {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
utils.Log.Fatalf("failed to start https: %s", err.Error())
|
log.Errorln("start HTTPS server", ":", err)
|
||||||
|
serverCancel()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if conf.Conf.Scheme.UnixFile != "" {
|
if conf.Conf.Scheme.UnixFile != "" {
|
||||||
utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile)
|
log.Infoln("start Unix server", "@", conf.Conf.Scheme.UnixFile)
|
||||||
unixSrv = &http.Server{Handler: r}
|
unixSrv = &http.Server{Handler: httpHandler}
|
||||||
go func() {
|
go func() {
|
||||||
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
|
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log.Fatalf("failed to listen unix: %+v", err)
|
log.Errorln("start Unix server", ":", err)
|
||||||
|
serverCancel()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// set socket file permission
|
|
||||||
mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)
|
mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log.Errorf("failed to parse socket file permission: %+v", err)
|
log.Errorln("parse unix_file_perm", ":", err)
|
||||||
} else {
|
} else {
|
||||||
err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))
|
err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log.Errorf("failed to chmod socket file: %+v", err)
|
log.Errorln("chmod socket file", ":", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = unixSrv.Serve(listener)
|
err = unixSrv.Serve(listener)
|
||||||
if err != nil && err != http.ErrServerClosed {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
utils.Log.Fatalf("failed to start unix: %s", err.Error())
|
log.Errorln("start Unix server", ":", err)
|
||||||
|
serverCancel()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
// Wait for interrupt signal to gracefully shutdown the server with
|
|
||||||
// a timeout of 1 second.
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
// kill (no param) default send syscanll.SIGTERM
|
// kill (no param) default send syscanll.SIGTERM
|
||||||
// kill -2 is syscall.SIGINT
|
// kill -2 is syscall.SIGINT
|
||||||
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
|
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-quit
|
select {
|
||||||
utils.Log.Println("Shutdown server...")
|
case <-quit:
|
||||||
|
case <-serverCtx.Done():
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
log.Println("shutdown server...")
|
||||||
defer cancel()
|
Release()
|
||||||
|
|
||||||
|
quitCtx, quitCancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer quitCancel()
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
if conf.Conf.Scheme.HttpPort != -1 {
|
if httpSrv != nil {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := httpSrv.Shutdown(ctx); err != nil {
|
if err := httpSrv.Shutdown(quitCtx); err != nil {
|
||||||
utils.Log.Fatal("HTTP server shutdown err: ", err)
|
log.Errorln("shutdown HTTP server", ":", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if conf.Conf.Scheme.HttpsPort != -1 {
|
if httpsSrv != nil {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := httpsSrv.Shutdown(ctx); err != nil {
|
if err := httpsSrv.Shutdown(quitCtx); err != nil {
|
||||||
utils.Log.Fatal("HTTPS server shutdown err: ", err)
|
log.Errorln("shutdown HTTPS server", ":", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if conf.Conf.Scheme.UnixFile != "" {
|
if unixSrv != nil {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := unixSrv.Shutdown(ctx); err != nil {
|
if err := unixSrv.Shutdown(quitCtx); err != nil {
|
||||||
utils.Log.Fatal("Unix server shutdown err: ", err)
|
log.Errorln("shutdown Unix server", ":", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
utils.Log.Println("Server exit")
|
log.Println("server exit")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.AddCommand(ServerCmd)
|
RootCmd.AddCommand(ServerCmd)
|
||||||
|
|
||||||
// Here you will define your flags and configuration settings.
|
|
||||||
|
|
||||||
// Cobra supports Persistent Flags which will work for this command
|
|
||||||
// and all subcommands, e.g.:
|
|
||||||
// serverCmd.PersistentFlags().String("foo", "", "A help for foo")
|
|
||||||
|
|
||||||
// Cobra supports local flags which will only run when this command
|
|
||||||
// is called directly, e.g.:
|
|
||||||
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OutAlistInit 暴露用于外部启动server的函数
|
// OutOpenListInit 暴露用于外部启动server的函数
|
||||||
func OutAlistInit() {
|
func OutOpenListInit() {
|
||||||
var (
|
var (
|
||||||
cmd *cobra.Command
|
cmd *cobra.Command
|
||||||
args []string
|
args []string
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/db"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// storageCmd represents the storage command
|
|
||||||
var storageCmd = &cobra.Command{
|
|
||||||
Use: "storage",
|
|
||||||
Short: "Manage storage",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var mountPath string
|
|
||||||
var disable = &cobra.Command{
|
|
||||||
Use: "disable",
|
|
||||||
Short: "Disable a storage",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
Init()
|
|
||||||
storage, err := db.GetStorageByMountPath(mountPath)
|
|
||||||
if err != nil {
|
|
||||||
utils.Log.Errorf("failed to query storage: %+v", err)
|
|
||||||
} else {
|
|
||||||
storage.Disabled = true
|
|
||||||
err = db.UpdateStorage(storage)
|
|
||||||
if err != nil {
|
|
||||||
utils.Log.Errorf("failed to update storage: %+v", err)
|
|
||||||
} else {
|
|
||||||
utils.Log.Infof("Storage with mount path [%s] have been disabled", mountPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
disable.Flags().StringVarP(&mountPath, "mount-path", "m", "", "The mountPath of storage")
|
|
||||||
RootCmd.AddCommand(storageCmd)
|
|
||||||
storageCmd.AddCommand(disable)
|
|
||||||
|
|
||||||
// Here you will define your flags and configuration settings.
|
|
||||||
|
|
||||||
// Cobra supports Persistent Flags which will work for this command
|
|
||||||
// and all subcommands, e.g.:
|
|
||||||
// storageCmd.PersistentFlags().String("foo", "", "A help for foo")
|
|
||||||
|
|
||||||
// Cobra supports local flags which will only run when this command
|
|
||||||
// is called directly, e.g.:
|
|
||||||
// storageCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package _115
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Pan115 struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
client *driver115.Pan115Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) Init(ctx context.Context) error {
|
|
||||||
return d.login()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
files, err := d.getFiles(dir.GetID())
|
|
||||||
if err != nil && !errors.Is(err, driver115.ErrNotExist) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return utils.SliceConvert(files, func(src driver115.File) (model.Obj, error) {
|
|
||||||
return src, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
downloadInfo, err := d.client.
|
|
||||||
SetUserAgent(driver115.UA115Browser).
|
|
||||||
Download(file.(driver115.File).PickCode)
|
|
||||||
// recover for upload
|
|
||||||
d.client.SetUserAgent(driver115.UA115Desktop)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
link := &model.Link{
|
|
||||||
URL: downloadInfo.Url.Url,
|
|
||||||
Header: downloadInfo.Header,
|
|
||||||
}
|
|
||||||
return link, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
if _, err := d.client.Mkdir(parentDir.GetID(), dirName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
return d.client.Move(dstDir.GetID(), srcObj.GetID())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
return d.client.Rename(srcObj.GetID(), newName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
return d.client.Copy(dstDir.GetID(), srcObj.GetID())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
return d.client.Delete(obj.GetID())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
tempFile, err := utils.CreateTempFile(stream.GetReadCloser())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tempFile.Close()
|
|
||||||
_ = os.Remove(tempFile.Name())
|
|
||||||
}()
|
|
||||||
return d.client.UploadFastOrByMultipart(dstDir.GetID(), stream.GetName(), stream.GetSize(), tempFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Pan115)(nil)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package _115
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
|
|
||||||
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
|
|
||||||
PageSize int64 `json:"page_size" type:"number" default:"56" help:"list api per page size of 115 driver"`
|
|
||||||
driver.RootID
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "115 Cloud",
|
|
||||||
DefaultRoot: "0",
|
|
||||||
OnlyProxy: true,
|
|
||||||
OnlyLocal: true,
|
|
||||||
NoOverwriteUpload: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Pan115{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package _115
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/SheltonZhu/115driver/pkg/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ model.Obj = (*driver.File)(nil)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package _115
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/SheltonZhu/115driver/pkg/driver"
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var UserAgent = driver.UA115Desktop
|
|
||||||
|
|
||||||
func (d *Pan115) login() error {
|
|
||||||
var err error
|
|
||||||
opts := []driver.Option{
|
|
||||||
driver.UA(UserAgent),
|
|
||||||
}
|
|
||||||
d.client = driver.New(opts...)
|
|
||||||
d.client.SetHttpClient(base.HttpClient)
|
|
||||||
cr := &driver.Credential{}
|
|
||||||
if d.Addition.QRCodeToken != "" {
|
|
||||||
s := &driver.QRCodeSession{
|
|
||||||
UID: d.Addition.QRCodeToken,
|
|
||||||
}
|
|
||||||
if cr, err = d.client.QRCodeLogin(s); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to login by qrcode")
|
|
||||||
}
|
|
||||||
d.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
|
|
||||||
d.Addition.QRCodeToken = ""
|
|
||||||
} else if d.Addition.Cookie != "" {
|
|
||||||
if err = cr.FromCookie(d.Addition.Cookie); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to login by cookies")
|
|
||||||
}
|
|
||||||
d.client.ImportCredential(cr)
|
|
||||||
} else {
|
|
||||||
return errors.New("missing cookie or qrcode account")
|
|
||||||
}
|
|
||||||
return d.client.LoginCheck()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan115) getFiles(fileId string) ([]driver.File, error) {
|
|
||||||
res := make([]driver.File, 0)
|
|
||||||
if d.PageSize <= 0 {
|
|
||||||
d.PageSize = driver.FileListLimit
|
|
||||||
}
|
|
||||||
files, err := d.client.ListWithLimit(fileId, d.PageSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, file := range *files {
|
|
||||||
res = append(res, file)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package _123
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
Username string `json:"username" required:"true"`
|
|
||||||
Password string `json:"password" required:"true"`
|
|
||||||
driver.RootID
|
|
||||||
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
|
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
|
||||||
AccessToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "123Pan",
|
|
||||||
DefaultRoot: "0",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Pan123{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package _123
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Pan123) getS3PreSignedUrls(ctx context.Context, upReq *UploadResp, start, end int) (*S3PreSignedURLs, error) {
|
|
||||||
data := base.Json{
|
|
||||||
"bucket": upReq.Data.Bucket,
|
|
||||||
"key": upReq.Data.Key,
|
|
||||||
"partNumberEnd": end,
|
|
||||||
"partNumberStart": start,
|
|
||||||
"uploadId": upReq.Data.UploadId,
|
|
||||||
"StorageNode": upReq.Data.StorageNode,
|
|
||||||
}
|
|
||||||
var s3PreSignedUrls S3PreSignedURLs
|
|
||||||
_, err := d.request(S3PreSignedUrls, http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(data).SetContext(ctx)
|
|
||||||
}, &s3PreSignedUrls)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &s3PreSignedUrls, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp, file model.FileStreamer, isMultipart bool) error {
|
|
||||||
data := base.Json{
|
|
||||||
"StorageNode": upReq.Data.StorageNode,
|
|
||||||
"bucket": upReq.Data.Bucket,
|
|
||||||
"fileId": upReq.Data.FileId,
|
|
||||||
"fileSize": file.GetSize(),
|
|
||||||
"isMultipart": isMultipart,
|
|
||||||
"key": upReq.Data.Key,
|
|
||||||
"uploadId": upReq.Data.UploadId,
|
|
||||||
}
|
|
||||||
_, err := d.request(UploadCompleteV2, http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(data).SetContext(ctx)
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, reader io.Reader, up driver.UpdateProgress) error {
|
|
||||||
chunkSize := int64(1024 * 1024 * 5)
|
|
||||||
// fetch s3 pre signed urls
|
|
||||||
chunkCount := int(math.Ceil(float64(file.GetSize()) / float64(chunkSize)))
|
|
||||||
// upload 10 chunks each batch
|
|
||||||
batchSize := 10
|
|
||||||
for i := 1; i <= chunkCount; i += batchSize {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
start := i
|
|
||||||
end := i + batchSize
|
|
||||||
if end > chunkCount+1 {
|
|
||||||
end = chunkCount + 1
|
|
||||||
}
|
|
||||||
s3PreSignedUrls, err := d.getS3PreSignedUrls(ctx, upReq, start, end)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// upload each chunk
|
|
||||||
for j := start; j < end; j++ {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
curSize := chunkSize
|
|
||||||
if j == chunkCount {
|
|
||||||
curSize = file.GetSize() - (int64(chunkCount)-1)*chunkSize
|
|
||||||
}
|
|
||||||
err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.LimitReader(reader, chunkSize), curSize, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
up(j * 100 / chunkCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// complete s3 upload
|
|
||||||
return d.completeS3(ctx, upReq, file, chunkCount > 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSignedUrls *S3PreSignedURLs, cur, end int, reader io.Reader, curSize int64, retry bool) error {
|
|
||||||
uploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]
|
|
||||||
if uploadUrl == "" {
|
|
||||||
return fmt.Errorf("upload url is empty, s3PreSignedUrls: %+v", s3PreSignedUrls)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest("PUT", uploadUrl, reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
req.ContentLength = curSize
|
|
||||||
//req.Header.Set("Content-Length", strconv.FormatInt(curSize, 10))
|
|
||||||
res, err := base.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode == http.StatusForbidden {
|
|
||||||
if retry {
|
|
||||||
return fmt.Errorf("upload s3 chunk %d failed, status code: %d", cur, res.StatusCode)
|
|
||||||
}
|
|
||||||
// refresh s3 pre signed urls
|
|
||||||
newS3PreSignedUrls, err := d.getS3PreSignedUrls(ctx, upReq, cur, end)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls
|
|
||||||
// retry
|
|
||||||
return d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, cur, end, reader, curSize, true)
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("upload s3 chunk %d failed, status code: %d, body: %s", cur, res.StatusCode, body)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
package _123
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
)
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
|
|
||||||
const (
|
|
||||||
AApi = "https://www.123pan.com/a/api"
|
|
||||||
BApi = "https://www.123pan.com/b/api"
|
|
||||||
MainApi = AApi
|
|
||||||
SignIn = MainApi + "/user/sign_in"
|
|
||||||
Logout = MainApi + "/user/logout"
|
|
||||||
UserInfo = MainApi + "/user/info"
|
|
||||||
FileList = MainApi + "/file/list/new"
|
|
||||||
DownloadInfo = MainApi + "/file/download_info"
|
|
||||||
Mkdir = MainApi + "/file/upload_request"
|
|
||||||
Move = MainApi + "/file/mod_pid"
|
|
||||||
Rename = MainApi + "/file/rename"
|
|
||||||
Trash = MainApi + "/file/trash"
|
|
||||||
UploadRequest = MainApi + "/file/upload_request"
|
|
||||||
UploadComplete = MainApi + "/file/upload_complete"
|
|
||||||
S3PreSignedUrls = MainApi + "/file/s3_repare_upload_parts_batch"
|
|
||||||
S3Auth = MainApi + "/file/s3_upload_object/auth"
|
|
||||||
UploadCompleteV2 = MainApi + "/file/upload_complete/v2"
|
|
||||||
S3Complete = MainApi + "/file/s3_complete_multipart_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Pan123) login() error {
|
|
||||||
var body base.Json
|
|
||||||
if utils.IsEmailFormat(d.Username) {
|
|
||||||
body = base.Json{
|
|
||||||
"mail": d.Username,
|
|
||||||
"password": d.Password,
|
|
||||||
"type": 2,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body = base.Json{
|
|
||||||
"passport": d.Username,
|
|
||||||
"password": d.Password,
|
|
||||||
"remember": true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res, err := base.RestyClient.R().
|
|
||||||
SetHeaders(map[string]string{
|
|
||||||
"origin": "https://www.123pan.com",
|
|
||||||
"referer": "https://www.123pan.com/",
|
|
||||||
"platform": "web",
|
|
||||||
"app-version": "3",
|
|
||||||
"user-agent": base.UserAgent,
|
|
||||||
}).
|
|
||||||
SetBody(body).Post(SignIn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if utils.Json.Get(res.Body(), "code").ToInt() != 200 {
|
|
||||||
err = fmt.Errorf(utils.Json.Get(res.Body(), "message").ToString())
|
|
||||||
} else {
|
|
||||||
d.AccessToken = utils.Json.Get(res.Body(), "data", "token").ToString()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan123) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
req.SetHeaders(map[string]string{
|
|
||||||
"origin": "https://www.123pan.com",
|
|
||||||
"referer": "https://www.123pan.com/",
|
|
||||||
"authorization": "Bearer " + d.AccessToken,
|
|
||||||
"platform": "web",
|
|
||||||
"app-version": "3",
|
|
||||||
"user-agent": base.UserAgent,
|
|
||||||
})
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
req.SetResult(resp)
|
|
||||||
}
|
|
||||||
res, err := req.Execute(method, url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
body := res.Body()
|
|
||||||
code := utils.Json.Get(body, "code").ToInt()
|
|
||||||
if code != 0 {
|
|
||||||
if code == 401 {
|
|
||||||
err := d.login()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.request(url, method, callback, resp)
|
|
||||||
}
|
|
||||||
return nil, errors.New(jsoniter.Get(body, "message").ToString())
|
|
||||||
}
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Pan123) getFiles(parentId string) ([]File, error) {
|
|
||||||
page := 1
|
|
||||||
res := make([]File, 0)
|
|
||||||
for {
|
|
||||||
var resp Files
|
|
||||||
query := map[string]string{
|
|
||||||
"driveId": "0",
|
|
||||||
"limit": "100",
|
|
||||||
"next": "0",
|
|
||||||
"orderBy": d.OrderBy,
|
|
||||||
"orderDirection": d.OrderDirection,
|
|
||||||
"parentFileId": parentId,
|
|
||||||
"trashed": "false",
|
|
||||||
"Page": strconv.Itoa(page),
|
|
||||||
}
|
|
||||||
_, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(query)
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
page++
|
|
||||||
res = append(res, resp.Data.InfoList...)
|
|
||||||
if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
package _139
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Yun139 struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
Account string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) Init(ctx context.Context) error {
|
|
||||||
if d.Authorization == "" {
|
|
||||||
return fmt.Errorf("authorization is empty")
|
|
||||||
}
|
|
||||||
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
decodeStr := string(decode)
|
|
||||||
splits := strings.Split(decodeStr, ":")
|
|
||||||
if len(splits) < 2 {
|
|
||||||
return fmt.Errorf("authorization is invalid, splits < 2")
|
|
||||||
}
|
|
||||||
d.Account = splits[1]
|
|
||||||
_, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{
|
|
||||||
"qryUserExternInfoReq": base.Json{
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
if d.isFamily() {
|
|
||||||
return d.familyGetFiles(dir.GetID())
|
|
||||||
} else {
|
|
||||||
return d.getFiles(dir.GetID())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
u, err := d.getLink(file.GetID())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.Link{URL: u}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
data := base.Json{
|
|
||||||
"createCatalogExtReq": base.Json{
|
|
||||||
"parentCatalogID": parentDir.GetID(),
|
|
||||||
"newCatalogName": dirName,
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt"
|
|
||||||
if d.isFamily() {
|
|
||||||
data = base.Json{
|
|
||||||
"cloudID": d.CloudID,
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
"docLibName": dirName,
|
|
||||||
}
|
|
||||||
pathname = "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc"
|
|
||||||
}
|
|
||||||
_, err := d.post(pathname, data, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
if d.isFamily() {
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
var contentInfoList []string
|
|
||||||
var catalogInfoList []string
|
|
||||||
if srcObj.IsDir() {
|
|
||||||
catalogInfoList = append(catalogInfoList, srcObj.GetID())
|
|
||||||
} else {
|
|
||||||
contentInfoList = append(contentInfoList, srcObj.GetID())
|
|
||||||
}
|
|
||||||
data := base.Json{
|
|
||||||
"createBatchOprTaskReq": base.Json{
|
|
||||||
"taskType": 3,
|
|
||||||
"actionType": "304",
|
|
||||||
"taskInfo": base.Json{
|
|
||||||
"contentInfoList": contentInfoList,
|
|
||||||
"catalogInfoList": catalogInfoList,
|
|
||||||
"newCatalogID": dstDir.GetID(),
|
|
||||||
},
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
|
|
||||||
_, err := d.post(pathname, data, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
if d.isFamily() {
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
var data base.Json
|
|
||||||
var pathname string
|
|
||||||
if srcObj.IsDir() {
|
|
||||||
data = base.Json{
|
|
||||||
"catalogID": srcObj.GetID(),
|
|
||||||
"catalogName": newName,
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo"
|
|
||||||
} else {
|
|
||||||
data = base.Json{
|
|
||||||
"contentID": srcObj.GetID(),
|
|
||||||
"contentName": newName,
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo"
|
|
||||||
}
|
|
||||||
_, err := d.post(pathname, data, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
if d.isFamily() {
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
var contentInfoList []string
|
|
||||||
var catalogInfoList []string
|
|
||||||
if srcObj.IsDir() {
|
|
||||||
catalogInfoList = append(catalogInfoList, srcObj.GetID())
|
|
||||||
} else {
|
|
||||||
contentInfoList = append(contentInfoList, srcObj.GetID())
|
|
||||||
}
|
|
||||||
data := base.Json{
|
|
||||||
"createBatchOprTaskReq": base.Json{
|
|
||||||
"taskType": 3,
|
|
||||||
"actionType": 309,
|
|
||||||
"taskInfo": base.Json{
|
|
||||||
"contentInfoList": contentInfoList,
|
|
||||||
"catalogInfoList": catalogInfoList,
|
|
||||||
"newCatalogID": dstDir.GetID(),
|
|
||||||
},
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
|
|
||||||
_, err := d.post(pathname, data, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
var contentInfoList []string
|
|
||||||
var catalogInfoList []string
|
|
||||||
if obj.IsDir() {
|
|
||||||
catalogInfoList = append(catalogInfoList, obj.GetID())
|
|
||||||
} else {
|
|
||||||
contentInfoList = append(contentInfoList, obj.GetID())
|
|
||||||
}
|
|
||||||
data := base.Json{
|
|
||||||
"createBatchOprTaskReq": base.Json{
|
|
||||||
"taskType": 2,
|
|
||||||
"actionType": 201,
|
|
||||||
"taskInfo": base.Json{
|
|
||||||
"newCatalogID": "",
|
|
||||||
"contentInfoList": contentInfoList,
|
|
||||||
"catalogInfoList": catalogInfoList,
|
|
||||||
},
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
|
|
||||||
if d.isFamily() {
|
|
||||||
data = base.Json{
|
|
||||||
"catalogList": catalogInfoList,
|
|
||||||
"contentList": contentInfoList,
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
"sourceCatalogType": 1002,
|
|
||||||
"taskType": 2,
|
|
||||||
}
|
|
||||||
pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask"
|
|
||||||
}
|
|
||||||
_, err := d.post(pathname, data, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
_ = iota //ignore first value by assigning to blank identifier
|
|
||||||
KB = 1 << (10 * iota)
|
|
||||||
MB
|
|
||||||
GB
|
|
||||||
TB
|
|
||||||
)
|
|
||||||
|
|
||||||
func getPartSize(size int64) int64 {
|
|
||||||
// 网盘对于分片数量存在上限
|
|
||||||
if size/GB > 30 {
|
|
||||||
return 512 * MB
|
|
||||||
}
|
|
||||||
return 100 * MB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
data := base.Json{
|
|
||||||
"manualRename": 2,
|
|
||||||
"operation": 0,
|
|
||||||
"fileCount": 1,
|
|
||||||
"totalSize": 0, // 去除上传大小限制
|
|
||||||
"uploadContentList": []base.Json{{
|
|
||||||
"contentName": stream.GetName(),
|
|
||||||
"contentSize": 0, // 去除上传大小限制
|
|
||||||
// "digest": "5a3231986ce7a6b46e408612d385bafa"
|
|
||||||
}},
|
|
||||||
"parentCatalogID": dstDir.GetID(),
|
|
||||||
"newCatalogName": "",
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
|
|
||||||
if d.isFamily() {
|
|
||||||
data = d.newJson(base.Json{
|
|
||||||
"fileCount": 1,
|
|
||||||
"manualRename": 2,
|
|
||||||
"operation": 0,
|
|
||||||
"path": "",
|
|
||||||
"seqNo": "",
|
|
||||||
"totalSize": 0,
|
|
||||||
"uploadContentList": []base.Json{{
|
|
||||||
"contentName": stream.GetName(),
|
|
||||||
"contentSize": 0,
|
|
||||||
// "digest": "5a3231986ce7a6b46e408612d385bafa"
|
|
||||||
}},
|
|
||||||
})
|
|
||||||
pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL"
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
var resp UploadResp
|
|
||||||
_, err := d.post(pathname, data, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
p := driver.NewProgress(stream.GetSize(), up)
|
|
||||||
|
|
||||||
var partSize = getPartSize(stream.GetSize())
|
|
||||||
part := (stream.GetSize() + partSize - 1) / partSize
|
|
||||||
for i := int64(0); i < part; i++ {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
start := i * partSize
|
|
||||||
byteSize := stream.GetSize() - start
|
|
||||||
if byteSize > partSize {
|
|
||||||
byteSize = partSize
|
|
||||||
}
|
|
||||||
|
|
||||||
limitReader := io.LimitReader(stream, byteSize)
|
|
||||||
// Update Progress
|
|
||||||
r := io.TeeReader(limitReader, p)
|
|
||||||
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName()))
|
|
||||||
req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10))
|
|
||||||
req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1))
|
|
||||||
req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID)
|
|
||||||
req.Header.Set("rangeType", "0")
|
|
||||||
req.ContentLength = byteSize
|
|
||||||
|
|
||||||
res, err := base.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Debugf("%+v", res)
|
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Yun139)(nil)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package _139
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
//Account string `json:"account" required:"true"`
|
|
||||||
Authorization string `json:"authorization" type:"text" required:"true"`
|
|
||||||
driver.RootID
|
|
||||||
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
|
|
||||||
CloudID string `json:"cloud_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "139Yun",
|
|
||||||
LocalSort: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Yun139{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
package _139
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
func (d *Yun139) isFamily() bool {
|
|
||||||
return d.Type == "family"
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeURIComponent(str string) string {
|
|
||||||
r := url.QueryEscape(str)
|
|
||||||
r = strings.Replace(r, "+", "%20", -1)
|
|
||||||
r = strings.Replace(r, "%21", "!", -1)
|
|
||||||
r = strings.Replace(r, "%27", "'", -1)
|
|
||||||
r = strings.Replace(r, "%28", "(", -1)
|
|
||||||
r = strings.Replace(r, "%29", ")", -1)
|
|
||||||
r = strings.Replace(r, "%2A", "*", -1)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func calSign(body, ts, randStr string) string {
|
|
||||||
body = encodeURIComponent(body)
|
|
||||||
strs := strings.Split(body, "")
|
|
||||||
sort.Strings(strs)
|
|
||||||
body = strings.Join(strs, "")
|
|
||||||
body = base64.StdEncoding.EncodeToString([]byte(body))
|
|
||||||
res := utils.GetMD5EncodeStr(body) + utils.GetMD5EncodeStr(ts+":"+randStr)
|
|
||||||
res = strings.ToUpper(utils.GetMD5EncodeStr(res))
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTime(t string) time.Time {
|
|
||||||
stamp, _ := time.ParseInLocation("20060102150405", t, time.Local)
|
|
||||||
return stamp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
|
||||||
url := "https://yun.139.com" + pathname
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
randStr := random.String(16)
|
|
||||||
ts := time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
body, err := utils.Json.Marshal(req.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sign := calSign(string(body), ts, randStr)
|
|
||||||
svcType := "1"
|
|
||||||
if d.isFamily() {
|
|
||||||
svcType = "2"
|
|
||||||
}
|
|
||||||
req.SetHeaders(map[string]string{
|
|
||||||
"Accept": "application/json, text/plain, */*",
|
|
||||||
"CMS-DEVICE": "default",
|
|
||||||
"Authorization": "Basic " + d.Authorization,
|
|
||||||
"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,
|
|
||||||
})
|
|
||||||
|
|
||||||
var e BaseResp
|
|
||||||
req.SetResult(&e)
|
|
||||||
res, err := req.Execute(method, url)
|
|
||||||
log.Debugln(res.String())
|
|
||||||
if !e.Success {
|
|
||||||
return nil, errors.New(e.Message)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
err = utils.Json.Unmarshal(res.Body(), resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res.Body(), nil
|
|
||||||
}
|
|
||||||
func (d *Yun139) post(pathname string, data interface{}, resp interface{}) ([]byte, error) {
|
|
||||||
return d.request(pathname, http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(data)
|
|
||||||
}, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
|
|
||||||
start := 0
|
|
||||||
limit := 100
|
|
||||||
files := make([]model.Obj, 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": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var resp GetDiskResp
|
|
||||||
_, err := d.post("/orchestration/personalCloud/catalog/v1.0/getDisk", data, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, catalog := range resp.Data.GetDiskResult.CatalogList {
|
|
||||||
f := model.Object{
|
|
||||||
ID: catalog.CatalogID,
|
|
||||||
Name: catalog.CatalogName,
|
|
||||||
Size: 0,
|
|
||||||
Modified: getTime(catalog.UpdateTime),
|
|
||||||
IsFolder: true,
|
|
||||||
}
|
|
||||||
files = append(files, &f)
|
|
||||||
}
|
|
||||||
for _, content := range resp.Data.GetDiskResult.ContentList {
|
|
||||||
f := model.ObjThumb{
|
|
||||||
Object: model.Object{
|
|
||||||
ID: content.ContentID,
|
|
||||||
Name: content.ContentName,
|
|
||||||
Size: content.ContentSize,
|
|
||||||
Modified: getTime(content.UpdateTime),
|
|
||||||
},
|
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
|
||||||
//Thumbnail: content.BigthumbnailURL,
|
|
||||||
}
|
|
||||||
files = append(files, &f)
|
|
||||||
}
|
|
||||||
if start+limit >= resp.Data.GetDiskResult.NodeCount {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
start += limit
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) newJson(data map[string]interface{}) base.Json {
|
|
||||||
common := map[string]interface{}{
|
|
||||||
"catalogType": 3,
|
|
||||||
"cloudID": d.CloudID,
|
|
||||||
"cloudType": 1,
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return utils.MergeMap(data, common)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
|
||||||
pageNum := 1
|
|
||||||
files := make([]model.Obj, 0)
|
|
||||||
for {
|
|
||||||
data := d.newJson(base.Json{
|
|
||||||
"catalogID": catalogID,
|
|
||||||
"contentSortType": 0,
|
|
||||||
"pageInfo": base.Json{
|
|
||||||
"pageNum": pageNum,
|
|
||||||
"pageSize": 100,
|
|
||||||
},
|
|
||||||
"sortDirection": 1,
|
|
||||||
})
|
|
||||||
var resp QueryContentListResp
|
|
||||||
_, err := d.post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, catalog := range resp.Data.CloudCatalogList {
|
|
||||||
f := model.Object{
|
|
||||||
ID: catalog.CatalogID,
|
|
||||||
Name: catalog.CatalogName,
|
|
||||||
Size: 0,
|
|
||||||
IsFolder: true,
|
|
||||||
Modified: getTime(catalog.LastUpdateTime),
|
|
||||||
}
|
|
||||||
files = append(files, &f)
|
|
||||||
}
|
|
||||||
for _, content := range resp.Data.CloudContentList {
|
|
||||||
f := model.ObjThumb{
|
|
||||||
Object: model.Object{
|
|
||||||
ID: content.ContentID,
|
|
||||||
Name: content.ContentName,
|
|
||||||
Size: content.ContentSize,
|
|
||||||
Modified: getTime(content.LastUpdateTime),
|
|
||||||
},
|
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
|
||||||
//Thumbnail: content.BigthumbnailURL,
|
|
||||||
}
|
|
||||||
files = append(files, &f)
|
|
||||||
}
|
|
||||||
if 100*pageNum > resp.Data.TotalCount {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
pageNum++
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Yun139) getLink(contentId string) (string, error) {
|
|
||||||
data := base.Json{
|
|
||||||
"appName": "",
|
|
||||||
"contentID": contentId,
|
|
||||||
"commonAccountInfo": base.Json{
|
|
||||||
"account": d.Account,
|
|
||||||
"accountType": 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
res, err := d.post("/orchestration/personalCloud/uploadAndDownload/v1.0/downloadRequest",
|
|
||||||
data, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func unicode(str string) string {
|
|
||||||
textQuoted := strconv.QuoteToASCII(str)
|
|
||||||
textUnquoted := textQuoted[1 : len(textQuoted)-1]
|
|
||||||
return textUnquoted
|
|
||||||
}
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
package _189pc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Cloud189PC struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
|
|
||||||
identity string
|
|
||||||
|
|
||||||
client *resty.Client
|
|
||||||
|
|
||||||
loginParam *LoginParam
|
|
||||||
tokenInfo *AppSessionResp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) GetAddition() driver.Additional {
|
|
||||||
return &y.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) Init(ctx context.Context) (err error) {
|
|
||||||
// 处理个人云和家庭云参数
|
|
||||||
if y.isFamily() && y.RootFolderID == "-11" {
|
|
||||||
y.RootFolderID = ""
|
|
||||||
}
|
|
||||||
if !y.isFamily() && y.RootFolderID == "" {
|
|
||||||
y.RootFolderID = "-11"
|
|
||||||
y.FamilyID = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化请求客户端
|
|
||||||
if y.client == nil {
|
|
||||||
y.client = base.NewRestyClient().SetHeaders(map[string]string{
|
|
||||||
"Accept": "application/json;charset=UTF-8",
|
|
||||||
"Referer": WEB_URL,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 避免重复登陆
|
|
||||||
identity := utils.GetMD5EncodeStr(y.Username + y.Password)
|
|
||||||
if !y.isLogin() || y.identity != identity {
|
|
||||||
y.identity = identity
|
|
||||||
if err = y.login(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理家庭云ID
|
|
||||||
if y.isFamily() && y.FamilyID == "" {
|
|
||||||
if y.FamilyID, err = y.getFamilyID(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
return y.getFiles(ctx, dir.GetID())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
var downloadUrl struct {
|
|
||||||
URL string `json:"fileDownloadUrl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
fullUrl := API_URL
|
|
||||||
if y.isFamily() {
|
|
||||||
fullUrl += "/family/file"
|
|
||||||
}
|
|
||||||
fullUrl += "/getFileDownloadUrl.action"
|
|
||||||
|
|
||||||
_, err := y.get(fullUrl, func(r *resty.Request) {
|
|
||||||
r.SetContext(ctx)
|
|
||||||
r.SetQueryParam("fileId", file.GetID())
|
|
||||||
if y.isFamily() {
|
|
||||||
r.SetQueryParams(map[string]string{
|
|
||||||
"familyId": y.FamilyID,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
r.SetQueryParams(map[string]string{
|
|
||||||
"dt": "3",
|
|
||||||
"flag": "1",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, &downloadUrl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重定向获取真实链接
|
|
||||||
downloadUrl.URL = strings.Replace(strings.ReplaceAll(downloadUrl.URL, "&", "&"), "http://", "https://", 1)
|
|
||||||
res, err := base.NoRedirectClient.R().SetContext(ctx).Get(downloadUrl.URL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if res.StatusCode() == 302 {
|
|
||||||
downloadUrl.URL = res.Header().Get("location")
|
|
||||||
}
|
|
||||||
|
|
||||||
like := &model.Link{
|
|
||||||
URL: downloadUrl.URL,
|
|
||||||
Header: http.Header{
|
|
||||||
"User-Agent": []string{base.UserAgent},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
// 获取链接有效时常
|
|
||||||
strs := regexp.MustCompile(`(?i)expire[^=]*=([0-9]*)`).FindStringSubmatch(downloadUrl.URL)
|
|
||||||
if len(strs) == 2 {
|
|
||||||
timestamp, err := strconv.ParseInt(strs[1], 10, 64)
|
|
||||||
if err == nil {
|
|
||||||
expired := time.Duration(timestamp-time.Now().Unix()) * time.Second
|
|
||||||
like.Expiration = &expired
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return like, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
fullUrl := API_URL
|
|
||||||
if y.isFamily() {
|
|
||||||
fullUrl += "/family/file"
|
|
||||||
}
|
|
||||||
fullUrl += "/createFolder.action"
|
|
||||||
|
|
||||||
_, err := y.post(fullUrl, func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"folderName": dirName,
|
|
||||||
"relativePath": "",
|
|
||||||
})
|
|
||||||
if y.isFamily() {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"familyId": y.FamilyID,
|
|
||||||
"parentId": parentDir.GetID(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"parentFolderId": parentDir.GetID(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"type": "MOVE",
|
|
||||||
"taskInfos": MustString(utils.Json.MarshalToString(
|
|
||||||
[]BatchTaskInfo{
|
|
||||||
{
|
|
||||||
FileId: srcObj.GetID(),
|
|
||||||
FileName: srcObj.GetName(),
|
|
||||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
"targetFolderId": dstDir.GetID(),
|
|
||||||
})
|
|
||||||
if y.isFamily() {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"familyId": y.FamilyID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
queryParam := make(map[string]string)
|
|
||||||
fullUrl := API_URL
|
|
||||||
method := http.MethodPost
|
|
||||||
if y.isFamily() {
|
|
||||||
fullUrl += "/family/file"
|
|
||||||
method = http.MethodGet
|
|
||||||
queryParam["familyId"] = y.FamilyID
|
|
||||||
}
|
|
||||||
if srcObj.IsDir() {
|
|
||||||
fullUrl += "/renameFolder.action"
|
|
||||||
queryParam["folderId"] = srcObj.GetID()
|
|
||||||
queryParam["destFolderName"] = newName
|
|
||||||
} else {
|
|
||||||
fullUrl += "/renameFile.action"
|
|
||||||
queryParam["fileId"] = srcObj.GetID()
|
|
||||||
queryParam["destFileName"] = newName
|
|
||||||
}
|
|
||||||
_, err := y.request(fullUrl, method, func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
req.SetQueryParams(queryParam)
|
|
||||||
}, nil, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"type": "COPY",
|
|
||||||
"taskInfos": MustString(utils.Json.MarshalToString(
|
|
||||||
[]BatchTaskInfo{
|
|
||||||
{
|
|
||||||
FileId: srcObj.GetID(),
|
|
||||||
FileName: srcObj.GetName(),
|
|
||||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
"targetFolderId": dstDir.GetID(),
|
|
||||||
"targetFileName": dstDir.GetName(),
|
|
||||||
})
|
|
||||||
if y.isFamily() {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"familyId": y.FamilyID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"type": "DELETE",
|
|
||||||
"taskInfos": MustString(utils.Json.MarshalToString(
|
|
||||||
[]*BatchTaskInfo{
|
|
||||||
{
|
|
||||||
FileId: obj.GetID(),
|
|
||||||
FileName: obj.GetName(),
|
|
||||||
IsFolder: BoolToNumber(obj.IsDir()),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
|
|
||||||
if y.isFamily() {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"familyId": y.FamilyID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
switch y.UploadMethod {
|
|
||||||
case "stream":
|
|
||||||
return y.CommonUpload(ctx, dstDir, stream, up)
|
|
||||||
case "old":
|
|
||||||
return y.OldUpload(ctx, dstDir, stream, up)
|
|
||||||
case "rapid":
|
|
||||||
return y.FastUpload(ctx, dstDir, stream, up)
|
|
||||||
default:
|
|
||||||
return y.CommonUpload(ctx, dstDir, stream, up)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,831 +0,0 @@
|
|||||||
package _189pc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"net/http/cookiejar"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
"github.com/alist-org/alist/v3/internal/setting"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
|
|
||||||
"github.com/avast/retry-go"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ACCOUNT_TYPE = "02"
|
|
||||||
APP_ID = "8025431004"
|
|
||||||
CLIENT_TYPE = "10020"
|
|
||||||
VERSION = "6.2"
|
|
||||||
|
|
||||||
WEB_URL = "https://cloud.189.cn"
|
|
||||||
AUTH_URL = "https://open.e.189.cn"
|
|
||||||
API_URL = "https://api.cloud.189.cn"
|
|
||||||
UPLOAD_URL = "https://upload.cloud.189.cn"
|
|
||||||
|
|
||||||
RETURN_URL = "https://m.cloud.189.cn/zhuanti/2020/loginErrorPc/index.html"
|
|
||||||
|
|
||||||
PC = "TELEPC"
|
|
||||||
MAC = "TELEMAC"
|
|
||||||
|
|
||||||
CHANNEL_ID = "web_cloud.189.cn"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]string {
|
|
||||||
dateOfGmt := getHttpDateStr()
|
|
||||||
sessionKey := y.tokenInfo.SessionKey
|
|
||||||
sessionSecret := y.tokenInfo.SessionSecret
|
|
||||||
if y.isFamily() {
|
|
||||||
sessionKey = y.tokenInfo.FamilySessionKey
|
|
||||||
sessionSecret = y.tokenInfo.FamilySessionSecret
|
|
||||||
}
|
|
||||||
|
|
||||||
header := map[string]string{
|
|
||||||
"Date": dateOfGmt,
|
|
||||||
"SessionKey": sessionKey,
|
|
||||||
"X-Request-ID": uuid.NewString(),
|
|
||||||
"Signature": signatureOfHmac(sessionSecret, sessionKey, method, url, dateOfGmt, params),
|
|
||||||
}
|
|
||||||
return header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) EncryptParams(params Params) string {
|
|
||||||
sessionSecret := y.tokenInfo.SessionSecret
|
|
||||||
if y.isFamily() {
|
|
||||||
sessionSecret = y.tokenInfo.FamilySessionSecret
|
|
||||||
}
|
|
||||||
if params != nil {
|
|
||||||
return AesECBEncrypt(params.Encode(), sessionSecret[:16])
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}) ([]byte, error) {
|
|
||||||
req := y.client.R().SetQueryParams(clientSuffix())
|
|
||||||
|
|
||||||
// 设置params
|
|
||||||
paramsData := y.EncryptParams(params)
|
|
||||||
if paramsData != "" {
|
|
||||||
req.SetQueryParam("params", paramsData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signature
|
|
||||||
req.SetHeaders(y.SignatureHeader(url, method, paramsData))
|
|
||||||
|
|
||||||
var erron RespErr
|
|
||||||
req.SetError(&erron)
|
|
||||||
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
req.SetResult(resp)
|
|
||||||
}
|
|
||||||
res, err := req.Execute(method, url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(res.String(), "userSessionBO is null") {
|
|
||||||
if err = y.refreshSession(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return y.request(url, method, callback, params, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理错误
|
|
||||||
if erron.HasError() {
|
|
||||||
if erron.ErrorCode == "InvalidSessionKey" {
|
|
||||||
if err = y.refreshSession(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return y.request(url, method, callback, params, resp)
|
|
||||||
}
|
|
||||||
return nil, &erron
|
|
||||||
}
|
|
||||||
return res.Body(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
|
||||||
return y.request(url, http.MethodGet, callback, nil, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
|
||||||
return y.request(url, http.MethodPost, callback, nil, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader) ([]byte, error) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
query := req.URL.Query()
|
|
||||||
for key, value := range clientSuffix() {
|
|
||||||
query.Add(key, value)
|
|
||||||
}
|
|
||||||
req.URL.RawQuery = query.Encode()
|
|
||||||
|
|
||||||
for key, value := range headers {
|
|
||||||
req.Header.Add(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sign {
|
|
||||||
for key, value := range y.SignatureHeader(url, http.MethodPut, "") {
|
|
||||||
req.Header.Add(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := base.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var erron RespErr
|
|
||||||
jsoniter.Unmarshal(body, &erron)
|
|
||||||
xml.Unmarshal(body, &erron)
|
|
||||||
if erron.HasError() {
|
|
||||||
return nil, &erron
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, errors.Errorf("put fail,err:%s", string(body))
|
|
||||||
}
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, error) {
|
|
||||||
fullUrl := API_URL
|
|
||||||
if y.isFamily() {
|
|
||||||
fullUrl += "/family/file"
|
|
||||||
}
|
|
||||||
fullUrl += "/listFiles.action"
|
|
||||||
|
|
||||||
res := make([]model.Obj, 0, 130)
|
|
||||||
for pageNum := 1; ; pageNum++ {
|
|
||||||
var resp Cloud189FilesResp
|
|
||||||
_, err := y.get(fullUrl, func(r *resty.Request) {
|
|
||||||
r.SetContext(ctx)
|
|
||||||
r.SetQueryParams(map[string]string{
|
|
||||||
"folderId": fileId,
|
|
||||||
"fileType": "0",
|
|
||||||
"mediaAttr": "0",
|
|
||||||
"iconOption": "5",
|
|
||||||
"pageNum": fmt.Sprint(pageNum),
|
|
||||||
"pageSize": "130",
|
|
||||||
})
|
|
||||||
if y.isFamily() {
|
|
||||||
r.SetQueryParams(map[string]string{
|
|
||||||
"familyId": y.FamilyID,
|
|
||||||
"orderBy": toFamilyOrderBy(y.OrderBy),
|
|
||||||
"descending": toDesc(y.OrderDirection),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
r.SetQueryParams(map[string]string{
|
|
||||||
"recursive": "0",
|
|
||||||
"orderBy": y.OrderBy,
|
|
||||||
"descending": toDesc(y.OrderDirection),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 获取完毕跳出
|
|
||||||
if resp.FileListAO.Count == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(resp.FileListAO.FolderList); i++ {
|
|
||||||
res = append(res, &resp.FileListAO.FolderList[i])
|
|
||||||
}
|
|
||||||
for i := 0; i < len(resp.FileListAO.FileList); i++ {
|
|
||||||
res = append(res, &resp.FileListAO.FileList[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) login() (err error) {
|
|
||||||
// 初始化登陆所需参数
|
|
||||||
if y.loginParam == nil {
|
|
||||||
if err = y.initLoginParam(); err != nil {
|
|
||||||
// 验证码也通过错误返回
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
// 销毁验证码
|
|
||||||
y.VCode = ""
|
|
||||||
// 销毁登陆参数
|
|
||||||
y.loginParam = nil
|
|
||||||
// 遇到错误,重新加载登陆参数(刷新验证码)
|
|
||||||
if err != nil && y.NoUseOcr {
|
|
||||||
if err1 := y.initLoginParam(); err1 != nil {
|
|
||||||
err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
param := y.loginParam
|
|
||||||
var loginresp LoginResp
|
|
||||||
_, err = y.client.R().
|
|
||||||
ForceContentType("application/json;charset=UTF-8").SetResult(&loginresp).
|
|
||||||
SetHeaders(map[string]string{
|
|
||||||
"REQID": param.ReqId,
|
|
||||||
"lt": param.Lt,
|
|
||||||
}).
|
|
||||||
SetFormData(map[string]string{
|
|
||||||
"appKey": APP_ID,
|
|
||||||
"accountType": ACCOUNT_TYPE,
|
|
||||||
"userName": param.RsaUsername,
|
|
||||||
"password": param.RsaPassword,
|
|
||||||
"validateCode": y.VCode,
|
|
||||||
"captchaToken": param.CaptchaToken,
|
|
||||||
"returnUrl": RETURN_URL,
|
|
||||||
"mailSuffix": "@189.cn",
|
|
||||||
"dynamicCheck": "FALSE",
|
|
||||||
"clientType": CLIENT_TYPE,
|
|
||||||
"cb_SaveName": "1",
|
|
||||||
"isOauth2": "false",
|
|
||||||
"state": "",
|
|
||||||
"paramId": param.ParamId,
|
|
||||||
}).
|
|
||||||
Post(AUTH_URL + "/api/logbox/oauth2/loginSubmit.do")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if loginresp.ToUrl == "" {
|
|
||||||
return fmt.Errorf("login failed,No toUrl obtained, msg: %s", loginresp.Msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取Session
|
|
||||||
var erron RespErr
|
|
||||||
var tokenInfo AppSessionResp
|
|
||||||
_, err = y.client.R().
|
|
||||||
SetResult(&tokenInfo).SetError(&erron).
|
|
||||||
SetQueryParams(clientSuffix()).
|
|
||||||
SetQueryParam("redirectURL", url.QueryEscape(loginresp.ToUrl)).
|
|
||||||
Post(API_URL + "/getSessionForPC.action")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if erron.HasError() {
|
|
||||||
return &erron
|
|
||||||
}
|
|
||||||
if tokenInfo.ResCode != 0 {
|
|
||||||
err = fmt.Errorf(tokenInfo.ResMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
y.tokenInfo = &tokenInfo
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 初始化登陆需要的参数
|
|
||||||
* 如果遇到验证码返回错误
|
|
||||||
*/
|
|
||||||
func (y *Cloud189PC) initLoginParam() error {
|
|
||||||
// 清除cookie
|
|
||||||
jar, _ := cookiejar.New(nil)
|
|
||||||
y.client.SetCookieJar(jar)
|
|
||||||
|
|
||||||
res, err := y.client.R().
|
|
||||||
SetQueryParams(map[string]string{
|
|
||||||
"appId": APP_ID,
|
|
||||||
"clientType": CLIENT_TYPE,
|
|
||||||
"returnURL": RETURN_URL,
|
|
||||||
"timeStamp": fmt.Sprint(timestamp()),
|
|
||||||
}).
|
|
||||||
Get(WEB_URL + "/api/portal/unifyLoginForPC.action")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
param := LoginParam{
|
|
||||||
CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],
|
|
||||||
Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
|
||||||
ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
|
||||||
ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
|
||||||
// jRsaKey: regexp.MustCompile(`"j_rsaKey" value="(.+?)"`).FindStringSubmatch(res.String())[1],
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取rsa公钥
|
|
||||||
var encryptConf EncryptConfResp
|
|
||||||
_, err = y.client.R().
|
|
||||||
ForceContentType("application/json;charset=UTF-8").SetResult(&encryptConf).
|
|
||||||
SetFormData(map[string]string{"appId": APP_ID}).
|
|
||||||
Post(AUTH_URL + "/api/logbox/config/encryptConf.do")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
param.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
|
|
||||||
param.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Username)
|
|
||||||
param.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Password)
|
|
||||||
y.loginParam = ¶m
|
|
||||||
|
|
||||||
// 判断是否需要验证码
|
|
||||||
resp, err := y.client.R().
|
|
||||||
SetHeader("REQID", param.ReqId).
|
|
||||||
SetFormData(map[string]string{
|
|
||||||
"appKey": APP_ID,
|
|
||||||
"accountType": ACCOUNT_TYPE,
|
|
||||||
"userName": param.RsaUsername,
|
|
||||||
}).Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp.String() == "0" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拉取验证码
|
|
||||||
imgRes, err := y.client.R().
|
|
||||||
SetQueryParams(map[string]string{
|
|
||||||
"token": param.CaptchaToken,
|
|
||||||
"REQID": param.ReqId,
|
|
||||||
"rnd": fmt.Sprint(timestamp()),
|
|
||||||
}).
|
|
||||||
Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to obtain verification code")
|
|
||||||
}
|
|
||||||
if imgRes.Size() > 20 {
|
|
||||||
if setting.GetStr(conf.OcrApi) != "" && !y.NoUseOcr {
|
|
||||||
vRes, err := base.RestyClient.R().
|
|
||||||
SetMultipartField("image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
|
|
||||||
Post(setting.GetStr(conf.OcrApi))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if jsoniter.Get(vRes.Body(), "status").ToInt() == 200 {
|
|
||||||
y.VCode = jsoniter.Get(vRes.Body(), "result").ToString()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回验证码图片给前端
|
|
||||||
return fmt.Errorf(`need img validate code: <img src="data:image/png;base64,%s"/>`, base64.StdEncoding.EncodeToString(imgRes.Body()))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新会话
|
|
||||||
func (y *Cloud189PC) refreshSession() (err error) {
|
|
||||||
var erron RespErr
|
|
||||||
var userSessionResp UserSessionResp
|
|
||||||
_, err = y.client.R().
|
|
||||||
SetResult(&userSessionResp).SetError(&erron).
|
|
||||||
SetQueryParams(clientSuffix()).
|
|
||||||
SetQueryParams(map[string]string{
|
|
||||||
"appId": APP_ID,
|
|
||||||
"accessToken": y.tokenInfo.AccessToken,
|
|
||||||
}).
|
|
||||||
SetHeader("X-Request-ID", uuid.NewString()).
|
|
||||||
Get(API_URL + "/getSessionForPC.action")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 错误影响正常访问,下线该储存
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
y.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
|
||||||
op.MustSaveDriverStorage(y)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if erron.HasError() {
|
|
||||||
if erron.ResCode == "UserInvalidOpenToken" {
|
|
||||||
if err = y.login(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &erron
|
|
||||||
}
|
|
||||||
y.tokenInfo.UserSessionResp = userSessionResp
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 普通上传
|
|
||||||
func (y *Cloud189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (err error) {
|
|
||||||
var DEFAULT = partSize(file.GetSize())
|
|
||||||
var count = int(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
|
|
||||||
|
|
||||||
params := Params{
|
|
||||||
"parentFolderId": dstDir.GetID(),
|
|
||||||
"fileName": url.QueryEscape(file.GetName()),
|
|
||||||
"fileSize": fmt.Sprint(file.GetSize()),
|
|
||||||
"sliceSize": fmt.Sprint(DEFAULT),
|
|
||||||
"lazyCheck": "1",
|
|
||||||
}
|
|
||||||
|
|
||||||
fullUrl := UPLOAD_URL
|
|
||||||
if y.isFamily() {
|
|
||||||
params.Set("familyId", y.FamilyID)
|
|
||||||
fullUrl += "/family"
|
|
||||||
} else {
|
|
||||||
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
|
|
||||||
fullUrl += "/person"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化上传
|
|
||||||
var initMultiUpload InitMultiUploadResp
|
|
||||||
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
}, params, &initMultiUpload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fileMd5 := md5.New()
|
|
||||||
silceMd5 := md5.New()
|
|
||||||
silceMd5Hexs := make([]string, 0, count)
|
|
||||||
byteData := bytes.NewBuffer(make([]byte, DEFAULT))
|
|
||||||
for i := 1; i <= count; i++ {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取块
|
|
||||||
byteData.Reset()
|
|
||||||
silceMd5.Reset()
|
|
||||||
_, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5, byteData), file, DEFAULT)
|
|
||||||
if err != io.EOF && err != io.ErrUnexpectedEOF && err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算块md5并进行hex和base64编码
|
|
||||||
md5Bytes := silceMd5.Sum(nil)
|
|
||||||
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Bytes)))
|
|
||||||
silceMd5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)
|
|
||||||
|
|
||||||
// 获取上传链接
|
|
||||||
var uploadUrl UploadUrlsResp
|
|
||||||
_, err = y.request(fullUrl+"/getMultiUploadUrls", http.MethodGet,
|
|
||||||
func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
}, Params{
|
|
||||||
"partInfo": fmt.Sprintf("%d-%s", i, silceMd5Base64),
|
|
||||||
"uploadFileId": initMultiUpload.Data.UploadFileID,
|
|
||||||
}, &uploadUrl)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始上传
|
|
||||||
uploadData := uploadUrl.UploadUrls[fmt.Sprint("partNumber_", i)]
|
|
||||||
|
|
||||||
err = retry.Do(func() error {
|
|
||||||
_, err := y.put(ctx, uploadData.RequestURL, ParseHttpHeader(uploadData.RequestHeader), false, bytes.NewReader(byteData.Bytes()))
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
retry.Context(ctx),
|
|
||||||
retry.Attempts(3),
|
|
||||||
retry.Delay(time.Second),
|
|
||||||
retry.MaxDelay(5*time.Second))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
up(int(i * 100 / count))
|
|
||||||
}
|
|
||||||
|
|
||||||
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
|
|
||||||
sliceMd5Hex := fileMd5Hex
|
|
||||||
if file.GetSize() > DEFAULT {
|
|
||||||
sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(silceMd5Hexs, "\n")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交上传
|
|
||||||
_, err = y.request(fullUrl+"/commitMultiUploadFile", http.MethodGet,
|
|
||||||
func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
}, Params{
|
|
||||||
"uploadFileId": initMultiUpload.Data.UploadFileID,
|
|
||||||
"fileMd5": fileMd5Hex,
|
|
||||||
"sliceMd5": sliceMd5Hex,
|
|
||||||
"lazyCheck": "1",
|
|
||||||
"isLog": "0",
|
|
||||||
"opertype": "3",
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 快传
|
|
||||||
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (err error) {
|
|
||||||
// 需要获取完整文件md5,必须支持 io.Seek
|
|
||||||
tempFile, err := utils.CreateTempFile(file.GetReadCloser())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tempFile.Close()
|
|
||||||
_ = os.Remove(tempFile.Name())
|
|
||||||
}()
|
|
||||||
|
|
||||||
var DEFAULT = partSize(file.GetSize())
|
|
||||||
count := int(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
|
|
||||||
|
|
||||||
// 优先计算所需信息
|
|
||||||
fileMd5 := md5.New()
|
|
||||||
silceMd5 := md5.New()
|
|
||||||
silceMd5Hexs := make([]string, 0, count)
|
|
||||||
silceMd5Base64s := make([]string, 0, count)
|
|
||||||
for i := 1; i <= count; i++ {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
silceMd5.Reset()
|
|
||||||
if _, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5), tempFile, DEFAULT); err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
md5Byte := silceMd5.Sum(nil)
|
|
||||||
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte)))
|
|
||||||
silceMd5Base64s = append(silceMd5Base64s, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte)))
|
|
||||||
}
|
|
||||||
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
|
|
||||||
sliceMd5Hex := fileMd5Hex
|
|
||||||
if file.GetSize() > DEFAULT {
|
|
||||||
sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(silceMd5Hexs, "\n")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检测是否支持快传
|
|
||||||
params := Params{
|
|
||||||
"parentFolderId": dstDir.GetID(),
|
|
||||||
"fileName": url.QueryEscape(file.GetName()),
|
|
||||||
"fileSize": fmt.Sprint(file.GetSize()),
|
|
||||||
"fileMd5": fileMd5Hex,
|
|
||||||
"sliceSize": fmt.Sprint(DEFAULT),
|
|
||||||
"sliceMd5": sliceMd5Hex,
|
|
||||||
}
|
|
||||||
|
|
||||||
fullUrl := UPLOAD_URL
|
|
||||||
if y.isFamily() {
|
|
||||||
params.Set("familyId", y.FamilyID)
|
|
||||||
fullUrl += "/family"
|
|
||||||
} else {
|
|
||||||
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
|
|
||||||
fullUrl += "/person"
|
|
||||||
}
|
|
||||||
|
|
||||||
var uploadInfo InitMultiUploadResp
|
|
||||||
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
}, params, &uploadInfo)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 网盘中不存在该文件,开始上传
|
|
||||||
if uploadInfo.Data.FileDataExists != 1 {
|
|
||||||
var uploadUrls UploadUrlsResp
|
|
||||||
_, err = y.request(fullUrl+"/getMultiUploadUrls", http.MethodGet,
|
|
||||||
func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
}, Params{
|
|
||||||
"uploadFileId": uploadInfo.Data.UploadFileID,
|
|
||||||
"partInfo": strings.Join(silceMd5Base64s, ","),
|
|
||||||
}, &uploadUrls)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, DEFAULT)
|
|
||||||
for i := 1; i <= count; i++ {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := io.ReadFull(tempFile, buf)
|
|
||||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
uploadData := uploadUrls.UploadUrls[fmt.Sprint("partNumber_", i)]
|
|
||||||
err = retry.Do(func() error {
|
|
||||||
_, err := y.put(ctx, uploadData.RequestURL, ParseHttpHeader(uploadData.RequestHeader), false, bytes.NewReader(buf[:n]))
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
retry.Context(ctx),
|
|
||||||
retry.Attempts(3),
|
|
||||||
retry.Delay(time.Second),
|
|
||||||
retry.MaxDelay(5*time.Second))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
up(int(i * 100 / count))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交
|
|
||||||
_, err = y.request(fullUrl+"/commitMultiUploadFile", http.MethodGet,
|
|
||||||
func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
}, Params{
|
|
||||||
"uploadFileId": uploadInfo.Data.UploadFileID,
|
|
||||||
"isLog": "0",
|
|
||||||
"opertype": "3",
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (err error) {
|
|
||||||
// 需要获取完整文件md5,必须支持 io.Seek
|
|
||||||
tempFile, err := utils.CreateTempFile(file.GetReadCloser())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tempFile.Close()
|
|
||||||
_ = os.Remove(tempFile.Name())
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 计算md5
|
|
||||||
fileMd5 := md5.New()
|
|
||||||
if _, err := io.Copy(fileMd5, tempFile); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
|
|
||||||
|
|
||||||
// 创建上传会话
|
|
||||||
var uploadInfo CreateUploadFileResp
|
|
||||||
|
|
||||||
fullUrl := API_URL + "/createUploadFile.action"
|
|
||||||
if y.isFamily() {
|
|
||||||
fullUrl = API_URL + "/family/file/createFamilyFile.action"
|
|
||||||
}
|
|
||||||
_, err = y.post(fullUrl, func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
if y.isFamily() {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"familyId": y.FamilyID,
|
|
||||||
"fileMd5": fileMd5Hex,
|
|
||||||
"fileName": file.GetName(),
|
|
||||||
"fileSize": fmt.Sprint(file.GetSize()),
|
|
||||||
"parentId": dstDir.GetID(),
|
|
||||||
"resumePolicy": "1",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"parentFolderId": dstDir.GetID(),
|
|
||||||
"fileName": file.GetName(),
|
|
||||||
"size": fmt.Sprint(file.GetSize()),
|
|
||||||
"md5": fileMd5Hex,
|
|
||||||
"opertype": "3",
|
|
||||||
"flag": "1",
|
|
||||||
"resumePolicy": "1",
|
|
||||||
"isLog": "0",
|
|
||||||
// "baseFileId": "",
|
|
||||||
// "lastWrite":"",
|
|
||||||
// "localPath": strings.ReplaceAll(param.LocalPath, "\\", "/"),
|
|
||||||
// "fileExt": "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, &uploadInfo)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 网盘中不存在该文件,开始上传
|
|
||||||
status := GetUploadFileStatusResp{CreateUploadFileResp: uploadInfo}
|
|
||||||
for status.Size < file.GetSize() && status.FileDataExists != 1 {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
header := map[string]string{
|
|
||||||
"ResumePolicy": "1",
|
|
||||||
"Expect": "100-continue",
|
|
||||||
}
|
|
||||||
|
|
||||||
if y.isFamily() {
|
|
||||||
header["FamilyId"] = fmt.Sprint(y.FamilyID)
|
|
||||||
header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
|
||||||
} else {
|
|
||||||
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile))
|
|
||||||
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取断点状态
|
|
||||||
fullUrl := API_URL + "/getUploadFileStatus.action"
|
|
||||||
if y.isFamily() {
|
|
||||||
fullUrl = API_URL + "/family/file/getFamilyFileStatus.action"
|
|
||||||
}
|
|
||||||
_, err = y.get(fullUrl, func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx).SetQueryParams(map[string]string{
|
|
||||||
"uploadFileId": fmt.Sprint(status.UploadFileId),
|
|
||||||
"resumePolicy": "1",
|
|
||||||
})
|
|
||||||
if y.isFamily() {
|
|
||||||
req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
|
|
||||||
}
|
|
||||||
}, &status)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := tempFile.Seek(status.GetSize(), io.SeekStart); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
up(int(status.Size / file.GetSize()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交
|
|
||||||
var resp CommitUploadFileResp
|
|
||||||
_, err = y.post(status.FileCommitUrl, func(req *resty.Request) {
|
|
||||||
req.SetContext(ctx)
|
|
||||||
if y.isFamily() {
|
|
||||||
req.SetHeaders(map[string]string{
|
|
||||||
"ResumePolicy": "1",
|
|
||||||
"UploadFileId": fmt.Sprint(status.UploadFileId),
|
|
||||||
"FamilyId": fmt.Sprint(y.FamilyID),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"opertype": "3",
|
|
||||||
"resumePolicy": "1",
|
|
||||||
"uploadFileId": fmt.Sprint(status.UploadFileId),
|
|
||||||
"isLog": "0",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, &resp)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) isFamily() bool {
|
|
||||||
return y.Type == "family"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (y *Cloud189PC) isLogin() bool {
|
|
||||||
if y.tokenInfo == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, err := y.get(API_URL+"/getUserInfo.action", nil, nil)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取家庭云所有用户信息
|
|
||||||
func (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
|
|
||||||
var resp FamilyInfoListResp
|
|
||||||
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp.FamilyInfoResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 抽取家庭云ID
|
|
||||||
func (y *Cloud189PC) getFamilyID() (string, error) {
|
|
||||||
infos, err := y.getFamilyInfoList()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if len(infos) == 0 {
|
|
||||||
return "", fmt.Errorf("cannot get automatically,please input family_id")
|
|
||||||
}
|
|
||||||
for _, info := range infos {
|
|
||||||
if strings.Contains(y.tokenInfo.LoginName, info.RemarkName) {
|
|
||||||
return fmt.Sprint(info.FamilyID), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Sprint(infos[0].FamilyID), nil
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
package alias
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Alias struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
pathMap map[string][]string
|
|
||||||
autoFlatten bool
|
|
||||||
oneKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) Init(ctx context.Context) error {
|
|
||||||
if d.Paths == "" {
|
|
||||||
return errors.New("paths is required")
|
|
||||||
}
|
|
||||||
d.pathMap = make(map[string][]string)
|
|
||||||
for _, path := range strings.Split(d.Paths, "\n") {
|
|
||||||
path = strings.TrimSpace(path)
|
|
||||||
if path == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
k, v := getPair(path)
|
|
||||||
d.pathMap[k] = append(d.pathMap[k], v)
|
|
||||||
}
|
|
||||||
if len(d.pathMap) == 1 {
|
|
||||||
for k := range d.pathMap {
|
|
||||||
d.oneKey = k
|
|
||||||
}
|
|
||||||
d.autoFlatten = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) Drop(ctx context.Context) error {
|
|
||||||
d.pathMap = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) Get(ctx context.Context, path string) (model.Obj, error) {
|
|
||||||
if utils.PathEqual(path, "/") {
|
|
||||||
return &model.Object{
|
|
||||||
Name: "Root",
|
|
||||||
IsFolder: true,
|
|
||||||
Path: "/",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
root, sub := d.getRootAndPath(path)
|
|
||||||
dsts, ok := d.pathMap[root]
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
for _, dst := range dsts {
|
|
||||||
obj, err := d.get(ctx, path, dst, sub)
|
|
||||||
if err == nil {
|
|
||||||
return obj, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
path := dir.GetPath()
|
|
||||||
if utils.PathEqual(path, "/") && !d.autoFlatten {
|
|
||||||
return d.listRoot(), nil
|
|
||||||
}
|
|
||||||
root, sub := d.getRootAndPath(path)
|
|
||||||
dsts, ok := d.pathMap[root]
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
var objs []model.Obj
|
|
||||||
for _, dst := range dsts {
|
|
||||||
tmp, err := d.list(ctx, dst, sub)
|
|
||||||
if err == nil {
|
|
||||||
objs = append(objs, tmp...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return objs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
root, sub := d.getRootAndPath(file.GetPath())
|
|
||||||
dsts, ok := d.pathMap[root]
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
for _, dst := range dsts {
|
|
||||||
link, err := d.link(ctx, dst, sub, args)
|
|
||||||
if err == nil {
|
|
||||||
return link, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Alias)(nil)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package alias
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
// Usually one of two
|
|
||||||
// driver.RootPath
|
|
||||||
// define other
|
|
||||||
Paths string `json:"paths" required:"true" type:"text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "Alias",
|
|
||||||
LocalSort: true,
|
|
||||||
NoCache: true,
|
|
||||||
NoUpload: true,
|
|
||||||
DefaultRoot: "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Alias{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
package alias
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
stdpath "path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/fs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/internal/sign"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Alias) listRoot() []model.Obj {
|
|
||||||
var objs []model.Obj
|
|
||||||
for k, _ := range d.pathMap {
|
|
||||||
obj := model.Object{
|
|
||||||
Name: k,
|
|
||||||
IsFolder: true,
|
|
||||||
Modified: d.Modified,
|
|
||||||
}
|
|
||||||
objs = append(objs, &obj)
|
|
||||||
}
|
|
||||||
return objs
|
|
||||||
}
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
func getPair(path string) (string, string) {
|
|
||||||
//path = strings.TrimSpace(path)
|
|
||||||
if strings.Contains(path, ":") {
|
|
||||||
pair := strings.SplitN(path, ":", 2)
|
|
||||||
if !strings.Contains(pair[0], "/") {
|
|
||||||
return pair[0], pair[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return stdpath.Base(path), path
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) getRootAndPath(path string) (string, string) {
|
|
||||||
if d.autoFlatten {
|
|
||||||
return d.oneKey, path
|
|
||||||
}
|
|
||||||
path = strings.TrimPrefix(path, "/")
|
|
||||||
parts := strings.SplitN(path, "/", 2)
|
|
||||||
if len(parts) == 1 {
|
|
||||||
return parts[0], ""
|
|
||||||
}
|
|
||||||
return parts[0], parts[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) get(ctx context.Context, path string, dst, sub string) (model.Obj, error) {
|
|
||||||
obj, err := fs.Get(ctx, stdpath.Join(dst, sub), &fs.GetArgs{NoLog: true})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.Object{
|
|
||||||
Path: path,
|
|
||||||
Name: obj.GetName(),
|
|
||||||
Size: obj.GetSize(),
|
|
||||||
Modified: obj.ModTime(),
|
|
||||||
IsFolder: obj.IsDir(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) list(ctx context.Context, dst, sub string) ([]model.Obj, error) {
|
|
||||||
objs, err := fs.List(ctx, stdpath.Join(dst, sub), &fs.ListArgs{NoLog: true})
|
|
||||||
// the obj must implement the model.SetPath interface
|
|
||||||
// return objs, err
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return utils.SliceConvert(objs, func(obj model.Obj) (model.Obj, error) {
|
|
||||||
thumb, ok := model.GetThumb(obj)
|
|
||||||
objRes := model.Object{
|
|
||||||
Name: obj.GetName(),
|
|
||||||
Size: obj.GetSize(),
|
|
||||||
Modified: obj.ModTime(),
|
|
||||||
IsFolder: obj.IsDir(),
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return &objRes, nil
|
|
||||||
}
|
|
||||||
return &model.ObjThumb{
|
|
||||||
Object: objRes,
|
|
||||||
Thumbnail: model.Thumbnail{
|
|
||||||
Thumbnail: thumb,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
reqPath := stdpath.Join(dst, sub)
|
|
||||||
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if common.ShouldProxy(storage, stdpath.Base(sub)) {
|
|
||||||
return &model.Link{
|
|
||||||
URL: fmt.Sprintf("%s/p%s?sign=%s",
|
|
||||||
common.GetApiUrl(args.HttpReq),
|
|
||||||
utils.EncodePath(reqPath, true),
|
|
||||||
sign.Sign(reqPath)),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
link, _, err := fs.Link(ctx, reqPath, args)
|
|
||||||
return link, err
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package alist_v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AListV2 struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) Init(ctx context.Context) error {
|
|
||||||
if len(d.Addition.Address) > 0 && string(d.Addition.Address[len(d.Addition.Address)-1]) == "/" {
|
|
||||||
d.Addition.Address = d.Addition.Address[0 : len(d.Addition.Address)-1]
|
|
||||||
}
|
|
||||||
// TODO login / refresh token
|
|
||||||
//op.MustSaveDriverStorage(d)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
url := d.Address + "/api/public/path"
|
|
||||||
var resp common.Resp[PathResp]
|
|
||||||
_, err := base.RestyClient.R().
|
|
||||||
SetResult(&resp).
|
|
||||||
SetHeader("Authorization", d.AccessToken).
|
|
||||||
SetBody(PathReq{
|
|
||||||
PageNum: 0,
|
|
||||||
PageSize: 0,
|
|
||||||
Path: dir.GetPath(),
|
|
||||||
Password: d.Password,
|
|
||||||
}).Post(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var files []model.Obj
|
|
||||||
for _, f := range resp.Data.Files {
|
|
||||||
file := model.ObjThumb{
|
|
||||||
Object: model.Object{
|
|
||||||
Name: f.Name,
|
|
||||||
Modified: *f.UpdatedAt,
|
|
||||||
Size: f.Size,
|
|
||||||
IsFolder: f.Type == 1,
|
|
||||||
},
|
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
|
|
||||||
}
|
|
||||||
files = append(files, &file)
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
url := d.Address + "/api/public/path"
|
|
||||||
var resp common.Resp[PathResp]
|
|
||||||
_, err := base.RestyClient.R().
|
|
||||||
SetResult(&resp).
|
|
||||||
SetHeader("Authorization", d.AccessToken).
|
|
||||||
SetBody(PathReq{
|
|
||||||
PageNum: 0,
|
|
||||||
PageSize: 0,
|
|
||||||
Path: file.GetPath(),
|
|
||||||
Password: d.Password,
|
|
||||||
}).Post(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.Link{
|
|
||||||
URL: resp.Data.Files[0].Url,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV2) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
|
||||||
// return nil, errs.NotSupport
|
|
||||||
//}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*AListV2)(nil)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package alist_v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
driver.RootPath
|
|
||||||
Address string `json:"url" required:"true"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "AList V2",
|
|
||||||
LocalSort: true,
|
|
||||||
NoUpload: true,
|
|
||||||
DefaultRoot: "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &AListV2{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package alist_v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type File struct {
|
|
||||||
Id string `json:"-"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Type int `json:"type"`
|
|
||||||
Driver string `json:"driver"`
|
|
||||||
UpdatedAt *time.Time `json:"updated_at"`
|
|
||||||
Thumbnail string `json:"thumbnail"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
SizeStr string `json:"size_str"`
|
|
||||||
TimeStr string `json:"time_str"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PathResp struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
//Meta Meta `json:"meta"`
|
|
||||||
Files []File `json:"files"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PathReq struct {
|
|
||||||
PageNum int `json:"page_num"`
|
|
||||||
PageSize int `json:"page_size"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package alist_v2
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
package alist_v3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AListV3 struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) Init(ctx context.Context) error {
|
|
||||||
d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/")
|
|
||||||
var resp common.Resp[MeResp]
|
|
||||||
_, err := d.request("/me", http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetResult(&resp)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// if the username is not empty and the username is not the same as the current username, then login again
|
|
||||||
if d.Username != "" && d.Username != resp.Data.Username {
|
|
||||||
err = d.login()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// re-get the user info
|
|
||||||
_, err = d.request("/me", http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetResult(&resp)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp.Data.Role == model.GUEST {
|
|
||||||
url := d.Address + "/api/public/settings"
|
|
||||||
res, err := base.RestyClient.R().Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
allowMounted := utils.Json.Get(res.Body(), "data", conf.AllowMounted).ToString() == "true"
|
|
||||||
if !allowMounted {
|
|
||||||
return fmt.Errorf("the site does not allow mounted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
var resp common.Resp[FsListResp]
|
|
||||||
_, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetResult(&resp).SetBody(ListReq{
|
|
||||||
PageReq: model.PageReq{
|
|
||||||
Page: 1,
|
|
||||||
PerPage: 0,
|
|
||||||
},
|
|
||||||
Path: dir.GetPath(),
|
|
||||||
Password: d.MetaPassword,
|
|
||||||
Refresh: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var files []model.Obj
|
|
||||||
for _, f := range resp.Data.Content {
|
|
||||||
file := model.ObjThumb{
|
|
||||||
Object: model.Object{
|
|
||||||
Name: f.Name,
|
|
||||||
Modified: f.Modified,
|
|
||||||
Size: f.Size,
|
|
||||||
IsFolder: f.IsDir,
|
|
||||||
},
|
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
|
|
||||||
}
|
|
||||||
files = append(files, &file)
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
var resp common.Resp[FsGetResp]
|
|
||||||
_, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetResult(&resp).SetBody(FsGetReq{
|
|
||||||
Path: file.GetPath(),
|
|
||||||
Password: d.MetaPassword,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.Link{
|
|
||||||
URL: resp.Data.RawURL,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
_, err := d.request("/fs/mkdir", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(MkdirOrLinkReq{
|
|
||||||
Path: path.Join(parentDir.GetPath(), dirName),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
_, err := d.request("/fs/move", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(MoveCopyReq{
|
|
||||||
SrcDir: path.Dir(srcObj.GetPath()),
|
|
||||||
DstDir: dstDir.GetPath(),
|
|
||||||
Names: []string{srcObj.GetName()},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
_, err := d.request("/fs/rename", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(RenameReq{
|
|
||||||
Path: srcObj.GetPath(),
|
|
||||||
Name: newName,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
_, err := d.request("/fs/copy", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(MoveCopyReq{
|
|
||||||
SrcDir: path.Dir(srcObj.GetPath()),
|
|
||||||
DstDir: dstDir.GetPath(),
|
|
||||||
Names: []string{srcObj.GetName()},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
_, err := d.request("/fs/remove", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(RemoveReq{
|
|
||||||
Dir: path.Dir(obj.GetPath()),
|
|
||||||
Names: []string{obj.GetName()},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
_, err := d.request("/fs/put", http.MethodPut, func(req *resty.Request) {
|
|
||||||
req.SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
|
|
||||||
SetHeader("Password", d.MetaPassword).
|
|
||||||
SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
|
|
||||||
SetContentLength(true).
|
|
||||||
SetBody(stream.GetReadCloser())
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
|
||||||
// return nil, errs.NotSupport
|
|
||||||
//}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*AListV3)(nil)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package alist_v3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
driver.RootPath
|
|
||||||
Address string `json:"url" required:"true"`
|
|
||||||
MetaPassword string `json:"meta_password"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "AList V3",
|
|
||||||
LocalSort: true,
|
|
||||||
DefaultRoot: "/",
|
|
||||||
CheckStatus: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &AListV3{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package alist_v3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ListReq struct {
|
|
||||||
model.PageReq
|
|
||||||
Path string `json:"path" form:"path"`
|
|
||||||
Password string `json:"password" form:"password"`
|
|
||||||
Refresh bool `json:"refresh"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ObjResp struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
IsDir bool `json:"is_dir"`
|
|
||||||
Modified time.Time `json:"modified"`
|
|
||||||
Sign string `json:"sign"`
|
|
||||||
Thumb string `json:"thumb"`
|
|
||||||
Type int `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FsListResp struct {
|
|
||||||
Content []ObjResp `json:"content"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Readme string `json:"readme"`
|
|
||||||
Write bool `json:"write"`
|
|
||||||
Provider string `json:"provider"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FsGetReq struct {
|
|
||||||
Path string `json:"path" form:"path"`
|
|
||||||
Password string `json:"password" form:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FsGetResp struct {
|
|
||||||
ObjResp
|
|
||||||
RawURL string `json:"raw_url"`
|
|
||||||
Readme string `json:"readme"`
|
|
||||||
Provider string `json:"provider"`
|
|
||||||
Related []ObjResp `json:"related"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MkdirOrLinkReq struct {
|
|
||||||
Path string `json:"path" form:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MoveCopyReq struct {
|
|
||||||
SrcDir string `json:"src_dir"`
|
|
||||||
DstDir string `json:"dst_dir"`
|
|
||||||
Names []string `json:"names"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RenameReq struct {
|
|
||||||
Path string `json:"path"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoveReq struct {
|
|
||||||
Dir string `json:"dir"`
|
|
||||||
Names []string `json:"names"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginResp struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MeResp struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
BasePath string `json:"base_path"`
|
|
||||||
Role int `json:"role"`
|
|
||||||
Disabled bool `json:"disabled"`
|
|
||||||
Permission int `json:"permission"`
|
|
||||||
SsoId string `json:"sso_id"`
|
|
||||||
Otp bool `json:"otp"`
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package alist_v3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *AListV3) login() error {
|
|
||||||
var resp common.Resp[LoginResp]
|
|
||||||
_, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetResult(&resp).SetBody(base.Json{
|
|
||||||
"username": d.Username,
|
|
||||||
"password": d.Password,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.Token = resp.Data.Token
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AListV3) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
|
|
||||||
url := d.Address + "/api" + api
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
req.SetHeader("Authorization", d.Token)
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
res, err := req.Execute(method, url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Debugf("[alist_v3] response body: %s", res.String())
|
|
||||||
if res.StatusCode() >= 400 {
|
|
||||||
return nil, fmt.Errorf("request failed, status: %s", res.Status())
|
|
||||||
}
|
|
||||||
code := utils.Json.Get(res.Body(), "code").ToInt()
|
|
||||||
if code != 200 {
|
|
||||||
if (code == 401 || code == 403) && !utils.IsBool(retry...) {
|
|
||||||
err = d.login()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.request(api, method, callback, true)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString())
|
|
||||||
}
|
|
||||||
return res.Body(), nil
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
package aliyundrive_open
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AliyundriveOpen struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
base string
|
|
||||||
|
|
||||||
DriveId string
|
|
||||||
|
|
||||||
limitList func(ctx context.Context, data base.Json) (*Files, error)
|
|
||||||
limitLink func(ctx context.Context, file model.Obj) (*model.Link, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Init(ctx context.Context) error {
|
|
||||||
res, err := d.request("/adrive/v1.0/user/getDriveInfo", http.MethodPost, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.DriveId = utils.Json.Get(res, "default_drive_id").ToString()
|
|
||||||
d.limitList = utils.LimitRateCtx(d.list, time.Second/4)
|
|
||||||
d.limitLink = utils.LimitRateCtx(d.link, time.Second)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
if d.limitList == nil {
|
|
||||||
return nil, fmt.Errorf("driver not init")
|
|
||||||
}
|
|
||||||
files, err := d.getFiles(ctx, dir.GetID())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
|
||||||
return fileToObj(src), nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) link(ctx context.Context, file model.Obj) (*model.Link, error) {
|
|
||||||
res, err := d.request("/adrive/v1.0/openFile/getDownloadUrl", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"file_id": file.GetID(),
|
|
||||||
"expire_sec": 14400,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
url := utils.Json.Get(res, "url").ToString()
|
|
||||||
exp := time.Hour
|
|
||||||
return &model.Link{
|
|
||||||
URL: url,
|
|
||||||
Expiration: &exp,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
if d.limitLink == nil {
|
|
||||||
return nil, fmt.Errorf("driver not init")
|
|
||||||
}
|
|
||||||
return d.limitLink(ctx, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"parent_file_id": parentDir.GetID(),
|
|
||||||
"name": dirName,
|
|
||||||
"type": "folder",
|
|
||||||
"check_name_mode": "refuse",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
_, err := d.request("/adrive/v1.0/openFile/move", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"file_id": srcObj.GetID(),
|
|
||||||
"to_parent_file_id": dstDir.GetID(),
|
|
||||||
"check_name_mode": "refuse", // optional:ignore,auto_rename,refuse
|
|
||||||
//"new_name": "newName", // The new name to use when a file of the same name exists
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
_, err := d.request("/adrive/v1.0/openFile/update", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"file_id": srcObj.GetID(),
|
|
||||||
"name": newName,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
_, err := d.request("/adrive/v1.0/openFile/copy", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"file_id": srcObj.GetID(),
|
|
||||||
"to_parent_file_id": dstDir.GetID(),
|
|
||||||
"auto_rename": true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
uri := "/adrive/v1.0/openFile/recyclebin/trash"
|
|
||||||
if d.RemoveWay == "delete" {
|
|
||||||
uri = "/adrive/v1.0/openFile/delete"
|
|
||||||
}
|
|
||||||
_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"file_id": obj.GetID(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
return d.upload(ctx, dstDir, stream, up)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
|
||||||
var resp base.Json
|
|
||||||
var uri string
|
|
||||||
data := base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"file_id": args.Obj.GetID(),
|
|
||||||
}
|
|
||||||
switch args.Method {
|
|
||||||
case "video_preview":
|
|
||||||
uri = "/adrive/v1.0/openFile/getVideoPreviewPlayInfo"
|
|
||||||
data["category"] = "live_transcoding"
|
|
||||||
data["url_expire_sec"] = 14400
|
|
||||||
default:
|
|
||||||
return nil, errs.NotSupport
|
|
||||||
}
|
|
||||||
_, err := d.request(uri, http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(data).SetResult(&resp)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*AliyundriveOpen)(nil)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package aliyundrive_open
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
driver.RootID
|
|
||||||
RefreshToken string `json:"refresh_token" required:"true"`
|
|
||||||
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
|
|
||||||
OauthTokenURL string `json:"oauth_token_url" default:"https://api.xhofe.top/alist/ali_open/token"`
|
|
||||||
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
|
|
||||||
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
|
|
||||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
|
||||||
RapidUpload bool `json:"rapid_upload" help:"If you enable this option, the file will be uploaded to the server first, so the progress will be incorrect"`
|
|
||||||
InternalUpload bool `json:"internal_upload" help:"If you are using Aliyun ECS is located in Beijing, you can turn it on to boost the upload speed"`
|
|
||||||
AccessToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "AliyundriveOpen",
|
|
||||||
LocalSort: false,
|
|
||||||
OnlyLocal: false,
|
|
||||||
OnlyProxy: false,
|
|
||||||
NoCache: false,
|
|
||||||
NoUpload: false,
|
|
||||||
NeedMs: false,
|
|
||||||
DefaultRoot: "root",
|
|
||||||
NoOverwriteUpload: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &AliyundriveOpen{
|
|
||||||
base: "https://openapi.aliyundrive.com",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
package aliyundrive_open
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func makePartInfos(size int) []base.Json {
|
|
||||||
partInfoList := make([]base.Json, size)
|
|
||||||
for i := 0; i < size; i++ {
|
|
||||||
partInfoList[i] = base.Json{"part_number": 1 + i}
|
|
||||||
}
|
|
||||||
return partInfoList
|
|
||||||
}
|
|
||||||
|
|
||||||
func calPartSize(fileSize int64) int64 {
|
|
||||||
var partSize int64 = 20 * 1024 * 1024
|
|
||||||
if fileSize > partSize {
|
|
||||||
if fileSize > 1*1024*1024*1024*1024 { // file Size over 1TB
|
|
||||||
partSize = 5 * 1024 * 1024 * 1024 // file part size 5GB
|
|
||||||
} else if fileSize > 768*1024*1024*1024 { // over 768GB
|
|
||||||
partSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part
|
|
||||||
} else if fileSize > 512*1024*1024*1024 { // over 512GB
|
|
||||||
partSize = 82463373 // ≈ 78.6432MB
|
|
||||||
} else if fileSize > 384*1024*1024*1024 { // over 384GB
|
|
||||||
partSize = 54975582 // ≈ 52.4288MB
|
|
||||||
} else if fileSize > 256*1024*1024*1024 { // over 256GB
|
|
||||||
partSize = 41231687 // ≈ 39.3216MB
|
|
||||||
} else if fileSize > 128*1024*1024*1024 { // over 128GB
|
|
||||||
partSize = 27487791 // ≈ 26.2144MB
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return partSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) getUploadUrl(count int, fileId, uploadId string) ([]PartInfo, error) {
|
|
||||||
partInfoList := makePartInfos(count)
|
|
||||||
var resp CreateResp
|
|
||||||
_, err := d.request("/adrive/v1.0/openFile/getUploadUrl", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"file_id": fileId,
|
|
||||||
"part_info_list": partInfoList,
|
|
||||||
"upload_id": uploadId,
|
|
||||||
}).SetResult(&resp)
|
|
||||||
})
|
|
||||||
return resp.PartInfoList, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) uploadPart(ctx context.Context, i, count int, reader *utils.MultiReadable, resp *CreateResp, retry bool) error {
|
|
||||||
partInfo := resp.PartInfoList[i-1]
|
|
||||||
uploadUrl := partInfo.UploadUrl
|
|
||||||
if d.InternalUpload {
|
|
||||||
uploadUrl = strings.ReplaceAll(uploadUrl, "https://cn-beijing-data.aliyundrive.net/", "http://ccp-bj29-bj-1592982087.oss-cn-beijing-internal.aliyuncs.com/")
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest("PUT", uploadUrl, reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
res, err := base.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
if retry {
|
|
||||||
reader.Reset()
|
|
||||||
return d.uploadPart(ctx, i, count, reader, resp, false)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
res.Body.Close()
|
|
||||||
if retry && res.StatusCode == http.StatusForbidden {
|
|
||||||
resp.PartInfoList, err = d.getUploadUrl(count, resp.FileId, resp.UploadId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
reader.Reset()
|
|
||||||
return d.uploadPart(ctx, i, count, reader, resp, false)
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusConflict {
|
|
||||||
return fmt.Errorf("upload status: %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) normalUpload(ctx context.Context, stream model.FileStreamer, up driver.UpdateProgress, createResp CreateResp, count int, partSize int64) error {
|
|
||||||
log.Debugf("[aliyundive_open] normal upload")
|
|
||||||
// 2. upload
|
|
||||||
preTime := time.Now()
|
|
||||||
for i := 1; i <= len(createResp.PartInfoList); i++ {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
err := d.uploadPart(ctx, i, count, utils.NewMultiReadable(io.LimitReader(stream, partSize)), &createResp, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if count > 0 {
|
|
||||||
up(i * 100 / count)
|
|
||||||
}
|
|
||||||
// refresh upload url if 50 minutes passed
|
|
||||||
if time.Since(preTime) > 50*time.Minute {
|
|
||||||
createResp.PartInfoList, err = d.getUploadUrl(count, createResp.FileId, createResp.UploadId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
preTime = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 3. complete
|
|
||||||
_, err := d.request("/adrive/v1.0/openFile/complete", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"file_id": createResp.FileId,
|
|
||||||
"upload_id": createResp.UploadId,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProofRange struct {
|
|
||||||
Start int64
|
|
||||||
End int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func getProofRange(input string, size int64) (*ProofRange, error) {
|
|
||||||
if size == 0 {
|
|
||||||
return &ProofRange{}, nil
|
|
||||||
}
|
|
||||||
tmpStr := utils.GetMD5EncodeStr(input)[0:16]
|
|
||||||
tmpInt, err := strconv.ParseUint(tmpStr, 16, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
index := tmpInt % uint64(size)
|
|
||||||
pr := &ProofRange{
|
|
||||||
Start: int64(index),
|
|
||||||
End: int64(index) + 8,
|
|
||||||
}
|
|
||||||
if pr.End >= size {
|
|
||||||
pr.End = size
|
|
||||||
}
|
|
||||||
return pr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) calProofCode(file *os.File, fileSize int64) (string, error) {
|
|
||||||
proofRange, err := getProofRange(d.AccessToken, fileSize)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
buf := make([]byte, proofRange.End-proofRange.Start)
|
|
||||||
_, err = file.ReadAt(buf, proofRange.Start)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(buf), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
// 1. create
|
|
||||||
// Part Size Unit: Bytes, Default: 20MB,
|
|
||||||
// Maximum number of slices 10,000, ≈195.3125GB
|
|
||||||
var partSize = calPartSize(stream.GetSize())
|
|
||||||
createData := base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"parent_file_id": dstDir.GetID(),
|
|
||||||
"name": stream.GetName(),
|
|
||||||
"type": "file",
|
|
||||||
"check_name_mode": "ignore",
|
|
||||||
}
|
|
||||||
count := int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
|
|
||||||
createData["part_info_list"] = makePartInfos(count)
|
|
||||||
// rapid upload
|
|
||||||
rapidUpload := stream.GetSize() > 100*1024 && d.RapidUpload
|
|
||||||
if rapidUpload {
|
|
||||||
log.Debugf("[aliyundrive_open] start cal pre_hash")
|
|
||||||
// read 1024 bytes to calculate pre hash
|
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
|
||||||
_, err := io.CopyN(buf, stream, 1024)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
createData["size"] = stream.GetSize()
|
|
||||||
createData["pre_hash"] = utils.GetSHA1Encode(buf.Bytes())
|
|
||||||
// if support seek, seek to start
|
|
||||||
if localFile, ok := stream.(io.Seeker); ok {
|
|
||||||
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Put spliced head back to stream
|
|
||||||
stream.SetReadCloser(struct {
|
|
||||||
io.Reader
|
|
||||||
io.Closer
|
|
||||||
}{
|
|
||||||
Reader: io.MultiReader(buf, stream.GetReadCloser()),
|
|
||||||
Closer: stream.GetReadCloser(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var createResp CreateResp
|
|
||||||
_, err, e := d.requestReturnErrResp("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(createData).SetResult(&createResp)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if e.Code != "PreHashMatched" || !rapidUpload {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Debugf("[aliyundrive_open] pre_hash matched, start rapid upload")
|
|
||||||
// convert to local file
|
|
||||||
file, err := utils.CreateTempFile(stream)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = stream.GetReadCloser().Close()
|
|
||||||
stream.SetReadCloser(file)
|
|
||||||
// calculate full hash
|
|
||||||
h := sha1.New()
|
|
||||||
_, err = io.Copy(h, file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
delete(createData, "pre_hash")
|
|
||||||
createData["proof_version"] = "v1"
|
|
||||||
createData["content_hash_name"] = "sha1"
|
|
||||||
createData["content_hash"] = hex.EncodeToString(h.Sum(nil))
|
|
||||||
// seek to start
|
|
||||||
if _, err = file.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
createData["proof_code"], err = d.calProofCode(file, stream.GetSize())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cal proof code error: %s", err.Error())
|
|
||||||
}
|
|
||||||
_, err = d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(createData).SetResult(&createResp)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if createResp.RapidUpload {
|
|
||||||
log.Debugf("[aliyundrive_open] rapid upload success, file id: %s", createResp.FileId)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// failed to rapid upload, try normal upload
|
|
||||||
if _, err = file.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Debugf("[aliyundrive_open] create file success, resp: %+v", createResp)
|
|
||||||
return d.normalUpload(ctx, stream, up, createResp, count, partSize)
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
package aliyundrive_open
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) refreshToken() error {
|
|
||||||
url := d.base + "/oauth/access_token"
|
|
||||||
if d.OauthTokenURL != "" && d.ClientID == "" {
|
|
||||||
url = d.OauthTokenURL
|
|
||||||
}
|
|
||||||
var resp base.TokenResp
|
|
||||||
var e ErrResp
|
|
||||||
_, err := base.RestyClient.R().
|
|
||||||
ForceContentType("application/json").
|
|
||||||
SetBody(base.Json{
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
"client_secret": d.ClientSecret,
|
|
||||||
"grant_type": "refresh_token",
|
|
||||||
"refresh_token": d.RefreshToken,
|
|
||||||
}).
|
|
||||||
SetResult(&resp).
|
|
||||||
SetError(&e).
|
|
||||||
Post(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.Code != "" {
|
|
||||||
return fmt.Errorf("failed to refresh token: %s", e.Message)
|
|
||||||
}
|
|
||||||
if resp.RefreshToken == "" {
|
|
||||||
return errors.New("failed to refresh token: refresh token is empty")
|
|
||||||
}
|
|
||||||
d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) request(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
|
|
||||||
b, err, _ := d.requestReturnErrResp(uri, method, callback, retry...)
|
|
||||||
return b, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) requestReturnErrResp(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error, *ErrResp) {
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
// TODO check whether access_token is expired
|
|
||||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
|
||||||
if method == http.MethodPost {
|
|
||||||
req.SetHeader("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
var e ErrResp
|
|
||||||
req.SetError(&e)
|
|
||||||
res, err := req.Execute(method, d.base+uri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err, nil
|
|
||||||
}
|
|
||||||
isRetry := len(retry) > 0 && retry[0]
|
|
||||||
if e.Code != "" {
|
|
||||||
if !isRetry && (utils.SliceContains([]string{"AccessTokenInvalid", "AccessTokenExpired", "I400JD"}, e.Code) || d.AccessToken == "") {
|
|
||||||
err = d.refreshToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err, nil
|
|
||||||
}
|
|
||||||
return d.requestReturnErrResp(uri, method, callback, true)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("%s:%s", e.Code, e.Message), &e
|
|
||||||
}
|
|
||||||
return res.Body(), nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) list(ctx context.Context, data base.Json) (*Files, error) {
|
|
||||||
var resp Files
|
|
||||||
_, err := d.request("/adrive/v1.0/openFile/list", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(data).SetResult(&resp)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *AliyundriveOpen) getFiles(ctx context.Context, fileId string) ([]File, error) {
|
|
||||||
marker := "first"
|
|
||||||
res := make([]File, 0)
|
|
||||||
for marker != "" {
|
|
||||||
if marker == "first" {
|
|
||||||
marker = ""
|
|
||||||
}
|
|
||||||
data := base.Json{
|
|
||||||
"drive_id": d.DriveId,
|
|
||||||
"limit": 200,
|
|
||||||
"marker": marker,
|
|
||||||
"order_by": d.OrderBy,
|
|
||||||
"order_direction": d.OrderDirection,
|
|
||||||
"parent_file_id": fileId,
|
|
||||||
//"category": "",
|
|
||||||
//"type": "",
|
|
||||||
//"video_thumbnail_time": 120000,
|
|
||||||
//"video_thumbnail_width": 480,
|
|
||||||
//"image_thumbnail_width": 480,
|
|
||||||
}
|
|
||||||
resp, err := d.limitList(ctx, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
marker = resp.NextMarker
|
|
||||||
res = append(res, resp.Items...)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package drivers
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/115"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/123"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/139"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/189"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/189pc"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/alias"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/alist_v2"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/alist_v3"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/aliyundrive"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_open"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_share"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/baidu_share"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/dropbox"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/google_photo"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/ipfs_api"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/lanzou"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/local"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mega"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mopan"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/pikpak"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/s3"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/seafile"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/sftp"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/smb"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/teambition"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/terabox"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/thunder"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/trainbit"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/url_tree"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/uss"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/virtual"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/webdav"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/wopan"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/yandex_disk"
|
|
||||||
)
|
|
||||||
|
|
||||||
// All do nothing,just for import
|
|
||||||
// same as _ import
|
|
||||||
func All() {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
package baidu_netdisk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
stdpath "path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BaiduNetdisk struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Init(ctx context.Context) error {
|
|
||||||
res, err := d.get("/xpan/nas", map[string]string{
|
|
||||||
"method": "uinfo",
|
|
||||||
}, nil)
|
|
||||||
log.Debugf("[baidu] get uinfo: %s", string(res))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
files, err := d.getFiles(dir.GetPath())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
|
||||||
return fileToObj(src), nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
if d.DownloadAPI == "crack" {
|
|
||||||
return d.linkCrack(file, args)
|
|
||||||
}
|
|
||||||
return d.linkOfficial(file, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
_, err := d.create(stdpath.Join(parentDir.GetPath(), dirName), 0, 1, "", "")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
data := []base.Json{
|
|
||||||
{
|
|
||||||
"path": srcObj.GetPath(),
|
|
||||||
"dest": dstDir.GetPath(),
|
|
||||||
"newname": srcObj.GetName(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err := d.manage("move", data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
data := []base.Json{
|
|
||||||
{
|
|
||||||
"path": srcObj.GetPath(),
|
|
||||||
"newname": newName,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err := d.manage("rename", data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
data := []base.Json{
|
|
||||||
{
|
|
||||||
"path": srcObj.GetPath(),
|
|
||||||
"dest": dstDir.GetPath(),
|
|
||||||
"newname": srcObj.GetName(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err := d.manage("copy", data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
data := []string{obj.GetPath()}
|
|
||||||
_, err := d.manage("delete", data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
tempFile, err := utils.CreateTempFile(stream.GetReadCloser())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tempFile.Close()
|
|
||||||
_ = os.Remove(tempFile.Name())
|
|
||||||
}()
|
|
||||||
var Default int64 = 4 * 1024 * 1024
|
|
||||||
defaultByteData := make([]byte, Default)
|
|
||||||
count := int(math.Ceil(float64(stream.GetSize()) / float64(Default)))
|
|
||||||
var SliceSize int64 = 256 * 1024
|
|
||||||
// cal md5
|
|
||||||
h1 := md5.New()
|
|
||||||
h2 := md5.New()
|
|
||||||
block_list := make([]string, 0)
|
|
||||||
content_md5 := ""
|
|
||||||
slice_md5 := ""
|
|
||||||
left := stream.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 stream.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rawPath := stdpath.Join(dstDir.GetPath(), stream.GetName())
|
|
||||||
path := encodeURIComponent(rawPath)
|
|
||||||
block_list_str := fmt.Sprintf("[%s]", strings.Join(block_list, ","))
|
|
||||||
data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&block_list=%s&content-md5=%s&slice-md5=%s",
|
|
||||||
path, stream.GetSize(),
|
|
||||||
block_list_str,
|
|
||||||
content_md5, slice_md5)
|
|
||||||
params := map[string]string{
|
|
||||||
"method": "precreate",
|
|
||||||
}
|
|
||||||
var precreateResp PrecreateResp
|
|
||||||
_, err = d.post("/xpan/file", params, data, &precreateResp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Debugf("%+v", precreateResp)
|
|
||||||
if precreateResp.ReturnType == 2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
params = map[string]string{
|
|
||||||
"method": "upload",
|
|
||||||
"access_token": d.AccessToken,
|
|
||||||
"type": "tmpfile",
|
|
||||||
"path": path,
|
|
||||||
"uploadid": precreateResp.Uploadid,
|
|
||||||
}
|
|
||||||
left = stream.GetSize()
|
|
||||||
for i, partseq := range precreateResp.BlockList {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
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().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetQueryParams(params).
|
|
||||||
SetFileReader("file", stream.GetName(), bytes.NewReader(byteData)).
|
|
||||||
Post(u)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Debugln(res.String())
|
|
||||||
if len(precreateResp.BlockList) > 0 {
|
|
||||||
up(i * 100 / len(precreateResp.BlockList))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, err = d.create(rawPath, stream.GetSize(), 0, precreateResp.Uploadid, block_list_str)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*BaiduNetdisk)(nil)
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package baidu_netdisk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
RefreshToken string `json:"refresh_token" required:"true"`
|
|
||||||
driver.RootPath
|
|
||||||
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
|
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
|
||||||
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
|
|
||||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
|
||||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
|
||||||
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
|
||||||
AccessToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "BaiduNetdisk",
|
|
||||||
DefaultRoot: "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &BaiduNetdisk{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
package baidu_netdisk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) refreshToken() error {
|
|
||||||
err := d._refreshToken()
|
|
||||||
if err != nil && err == errs.EmptyToken {
|
|
||||||
err = d._refreshToken()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) _refreshToken() error {
|
|
||||||
u := "https://openapi.baidu.com/oauth/2.0/token"
|
|
||||||
var resp base.TokenResp
|
|
||||||
var e TokenErrResp
|
|
||||||
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
|
|
||||||
"grant_type": "refresh_token",
|
|
||||||
"refresh_token": d.RefreshToken,
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
"client_secret": d.ClientSecret,
|
|
||||||
}).Get(u)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.Error != "" {
|
|
||||||
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
|
|
||||||
}
|
|
||||||
if resp.RefreshToken == "" {
|
|
||||||
return errs.EmptyToken
|
|
||||||
}
|
|
||||||
d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) request(furl string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
req.SetQueryParam("access_token", d.AccessToken)
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
req.SetResult(resp)
|
|
||||||
}
|
|
||||||
res, err := req.Execute(method, furl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
errno := utils.Json.Get(res.Body(), "errno").ToInt()
|
|
||||||
if errno != 0 {
|
|
||||||
if errno == -6 {
|
|
||||||
err = d.refreshToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.request(furl, method, callback, resp)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("errno: %d, refer to https://pan.baidu.com/union/doc/", errno)
|
|
||||||
}
|
|
||||||
return res.Body(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) get(pathname string, params map[string]string, resp interface{}) ([]byte, error) {
|
|
||||||
return d.request("https://pan.baidu.com/rest/2.0"+pathname, http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(params)
|
|
||||||
}, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) post(pathname string, params map[string]string, data interface{}, resp interface{}) ([]byte, error) {
|
|
||||||
return d.request("https://pan.baidu.com/rest/2.0"+pathname, http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(params)
|
|
||||||
req.SetBody(data)
|
|
||||||
}, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) getFiles(dir string) ([]File, error) {
|
|
||||||
start := 0
|
|
||||||
limit := 200
|
|
||||||
params := map[string]string{
|
|
||||||
"method": "list",
|
|
||||||
"dir": dir,
|
|
||||||
"web": "web",
|
|
||||||
}
|
|
||||||
if d.OrderBy != "" {
|
|
||||||
params["order"] = d.OrderBy
|
|
||||||
if d.OrderDirection == "desc" {
|
|
||||||
params["desc"] = "1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res := make([]File, 0)
|
|
||||||
for {
|
|
||||||
params["start"] = strconv.Itoa(start)
|
|
||||||
params["limit"] = strconv.Itoa(limit)
|
|
||||||
start += limit
|
|
||||||
var resp ListResp
|
|
||||||
_, err := d.get("/xpan/file", params, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(resp.List) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
res = append(res, resp.List...)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) linkOfficial(file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
var resp DownloadResp
|
|
||||||
params := map[string]string{
|
|
||||||
"method": "filemetas",
|
|
||||||
"fsids": fmt.Sprintf("[%s]", file.GetID()),
|
|
||||||
"dlink": "1",
|
|
||||||
}
|
|
||||||
_, err := d.get("/xpan/multimedia", params, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
u := fmt.Sprintf("%s&access_token=%s", resp.List[0].Dlink, d.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 &model.Link{
|
|
||||||
URL: u,
|
|
||||||
Header: http.Header{
|
|
||||||
"User-Agent": []string{"pan.baidu.com"},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
var resp DownloadResp2
|
|
||||||
param := map[string]string{
|
|
||||||
"target": fmt.Sprintf("[\"%s\"]", file.GetPath()),
|
|
||||||
"dlink": "1",
|
|
||||||
"web": "5",
|
|
||||||
"origin": "dlna",
|
|
||||||
}
|
|
||||||
_, err := d.request("https://pan.baidu.com/api/filemetas", http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(param)
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.Link{
|
|
||||||
URL: resp.Info[0].Dlink,
|
|
||||||
Header: http.Header{
|
|
||||||
"User-Agent": []string{d.CustomCrackUA},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) manage(opera string, filelist interface{}) ([]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 d.post("/xpan/file", params, data, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduNetdisk) create(path string, size int64, isdir int, uploadid, block_list string) ([]byte, error) {
|
|
||||||
params := map[string]string{
|
|
||||||
"method": "create",
|
|
||||||
}
|
|
||||||
data := fmt.Sprintf("path=%s&size=%d&isdir=%d&rtype=3", encodeURIComponent(path), size, isdir)
|
|
||||||
if uploadid != "" {
|
|
||||||
data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list)
|
|
||||||
}
|
|
||||||
return d.post("/xpan/file", params, data, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeURIComponent(str string) string {
|
|
||||||
r := url.QueryEscape(str)
|
|
||||||
r = strings.ReplaceAll(r, "+", "%20")
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package baiduphoto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
RefreshToken string `json:"refresh_token" required:"true"`
|
|
||||||
ShowType string `json:"show_type" type:"select" options:"root,root_only_album,root_only_file" default:"root"`
|
|
||||||
AlbumID string `json:"album_id"`
|
|
||||||
//AlbumPassword string `json:"album_password"`
|
|
||||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
|
||||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "BaiduPhoto",
|
|
||||||
LocalSort: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &BaiduPhoto{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
package baidu_share
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BaiduShare struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
client *resty.Client
|
|
||||||
info struct {
|
|
||||||
Root string
|
|
||||||
Seckey string
|
|
||||||
Shareid string
|
|
||||||
Uk string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) Init(ctx context.Context) error {
|
|
||||||
// TODO login / refresh token
|
|
||||||
//op.MustSaveDriverStorage(d)
|
|
||||||
d.client = resty.New().
|
|
||||||
SetBaseURL("https://pan.baidu.com").
|
|
||||||
SetHeader("User-Agent", "netdisk").
|
|
||||||
SetCookie(&http.Cookie{Name: "BDUSS", Value: d.BDUSS}).
|
|
||||||
SetCookie(&http.Cookie{Name: "ndut_fmt"})
|
|
||||||
respJson := struct {
|
|
||||||
Errno int64 `json:"errno"`
|
|
||||||
Data struct {
|
|
||||||
List [1]struct {
|
|
||||||
Path string `json:"path"`
|
|
||||||
} `json:"list"`
|
|
||||||
Uk json.Number `json:"uk"`
|
|
||||||
Shareid json.Number `json:"shareid"`
|
|
||||||
Seckey string `json:"seckey"`
|
|
||||||
} `json:"data"`
|
|
||||||
}{}
|
|
||||||
resp, err := d.client.R().
|
|
||||||
SetBody(url.Values{
|
|
||||||
"pwd": {d.Pwd},
|
|
||||||
"root": {"1"},
|
|
||||||
"shorturl": {d.Surl},
|
|
||||||
}.Encode()).
|
|
||||||
SetResult(&respJson).
|
|
||||||
Post("share/wxlist?channel=weixin&version=2.2.2&clienttype=25&web=1")
|
|
||||||
if err == nil {
|
|
||||||
if resp.IsSuccess() && respJson.Errno == 0 {
|
|
||||||
d.info.Root = path.Dir(respJson.Data.List[0].Path)
|
|
||||||
d.info.Seckey = respJson.Data.Seckey
|
|
||||||
d.info.Shareid = respJson.Data.Shareid.String()
|
|
||||||
d.info.Uk = respJson.Data.Uk.String()
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf(" %s; %s; ", resp.Status(), resp.Body())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
// TODO return the files list, required
|
|
||||||
reqDir := dir.GetPath()
|
|
||||||
isRoot := "0"
|
|
||||||
if reqDir == d.RootFolderPath {
|
|
||||||
reqDir = path.Join(d.info.Root, reqDir)
|
|
||||||
}
|
|
||||||
if reqDir == d.info.Root {
|
|
||||||
isRoot = "1"
|
|
||||||
}
|
|
||||||
objs := []model.Obj{}
|
|
||||||
var err error
|
|
||||||
var page uint64 = 1
|
|
||||||
more := true
|
|
||||||
for more && err == nil {
|
|
||||||
respJson := struct {
|
|
||||||
Errno int64 `json:"errno"`
|
|
||||||
Data struct {
|
|
||||||
More bool `json:"has_more"`
|
|
||||||
List []struct {
|
|
||||||
Fsid json.Number `json:"fs_id"`
|
|
||||||
Isdir json.Number `json:"isdir"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Name string `json:"server_filename"`
|
|
||||||
Mtime json.Number `json:"server_mtime"`
|
|
||||||
Size json.Number `json:"size"`
|
|
||||||
} `json:"list"`
|
|
||||||
} `json:"data"`
|
|
||||||
}{}
|
|
||||||
resp, e := d.client.R().
|
|
||||||
SetBody(url.Values{
|
|
||||||
"dir": {reqDir},
|
|
||||||
"num": {"1000"},
|
|
||||||
"order": {"time"},
|
|
||||||
"page": {fmt.Sprint(page)},
|
|
||||||
"pwd": {d.Pwd},
|
|
||||||
"root": {isRoot},
|
|
||||||
"shorturl": {d.Surl},
|
|
||||||
}.Encode()).
|
|
||||||
SetResult(&respJson).
|
|
||||||
Post("share/wxlist?channel=weixin&version=2.2.2&clienttype=25&web=1")
|
|
||||||
err = e
|
|
||||||
if err == nil {
|
|
||||||
if resp.IsSuccess() && respJson.Errno == 0 {
|
|
||||||
page++
|
|
||||||
more = respJson.Data.More
|
|
||||||
for _, v := range respJson.Data.List {
|
|
||||||
size, _ := v.Size.Int64()
|
|
||||||
mtime, _ := v.Mtime.Int64()
|
|
||||||
objs = append(objs, &model.Object{
|
|
||||||
ID: v.Fsid.String(),
|
|
||||||
Path: v.Path,
|
|
||||||
Name: v.Name,
|
|
||||||
Size: size,
|
|
||||||
Modified: time.Unix(mtime, 0),
|
|
||||||
IsFolder: v.Isdir.String() == "1",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf(" %s; %s; ", resp.Status(), resp.Body())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return objs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
// TODO return link of file, required
|
|
||||||
link := model.Link{Header: d.client.Header}
|
|
||||||
sign := ""
|
|
||||||
stamp := ""
|
|
||||||
signJson := struct {
|
|
||||||
Errno int64 `json:"errno"`
|
|
||||||
Data struct {
|
|
||||||
Stamp json.Number `json:"timestamp"`
|
|
||||||
Sign string `json:"sign"`
|
|
||||||
} `json:"data"`
|
|
||||||
}{}
|
|
||||||
resp, err := d.client.R().
|
|
||||||
SetQueryParam("surl", d.Surl).
|
|
||||||
SetResult(&signJson).
|
|
||||||
Get("share/tplconfig?fields=sign,timestamp&channel=chunlei&web=1&app_id=250528&clienttype=0")
|
|
||||||
if err == nil {
|
|
||||||
if resp.IsSuccess() && signJson.Errno == 0 {
|
|
||||||
stamp = signJson.Data.Stamp.String()
|
|
||||||
sign = signJson.Data.Sign
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf(" %s; %s; ", resp.Status(), resp.Body())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
respJson := struct {
|
|
||||||
Errno int64 `json:"errno"`
|
|
||||||
List [1]struct {
|
|
||||||
Dlink string `json:"dlink"`
|
|
||||||
} `json:"list"`
|
|
||||||
}{}
|
|
||||||
resp, err = d.client.R().
|
|
||||||
SetQueryParam("sign", sign).
|
|
||||||
SetQueryParam("timestamp", stamp).
|
|
||||||
SetBody(url.Values{
|
|
||||||
"encrypt": {"0"},
|
|
||||||
"extra": {fmt.Sprintf(`{"sekey":"%s"}`, d.info.Seckey)},
|
|
||||||
"fid_list": {fmt.Sprintf("[%s]", file.GetID())},
|
|
||||||
"primaryid": {d.info.Shareid},
|
|
||||||
"product": {"share"},
|
|
||||||
"type": {"nolimit"},
|
|
||||||
"uk": {d.info.Uk},
|
|
||||||
}.Encode()).
|
|
||||||
SetResult(&respJson).
|
|
||||||
Post("api/sharedownload?app_id=250528&channel=chunlei&clienttype=12&web=1")
|
|
||||||
if err == nil {
|
|
||||||
if resp.IsSuccess() && respJson.Errno == 0 && respJson.List[0].Dlink != "" {
|
|
||||||
link.URL = respJson.List[0].Dlink
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf(" %s; %s; ", resp.Status(), resp.Body())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
resp, err = d.client.R().
|
|
||||||
SetDoNotParseResponse(true).
|
|
||||||
Get(link.URL)
|
|
||||||
if err == nil {
|
|
||||||
defer resp.RawBody().Close()
|
|
||||||
if resp.IsError() {
|
|
||||||
byt, _ := io.ReadAll(resp.RawBody())
|
|
||||||
err = fmt.Errorf(" %s; %s; ", resp.Status(), byt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &link, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
// TODO create folder, optional
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
// TODO move obj, optional
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
// TODO rename obj, optional
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
// TODO copy obj, optional
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
// TODO remove obj, optional
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *BaiduShare) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
// TODO upload file, optional
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
|
||||||
// return nil, errs.NotSupport
|
|
||||||
//}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*BaiduShare)(nil)
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package baidu_share
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
// Usually one of two
|
|
||||||
driver.RootPath
|
|
||||||
// driver.RootID
|
|
||||||
// define other
|
|
||||||
// Field string `json:"field" type:"select" required:"true" options:"a,b,c" default:"a"`
|
|
||||||
Surl string `json:"surl"`
|
|
||||||
Pwd string `json:"pwd"`
|
|
||||||
BDUSS string `json:"BDUSS"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "BaiduShare",
|
|
||||||
LocalSort: true,
|
|
||||||
OnlyLocal: false,
|
|
||||||
OnlyProxy: false,
|
|
||||||
NoCache: false,
|
|
||||||
NoUpload: true,
|
|
||||||
NeedMs: false,
|
|
||||||
DefaultRoot: "/",
|
|
||||||
CheckStatus: false,
|
|
||||||
Alert: "",
|
|
||||||
NoOverwriteUpload: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &BaiduShare{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package baidu_share
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package baidu_share
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HandleRange(link *model.Link, file io.ReadSeekCloser, header http.Header, size int64) {
|
|
||||||
if header.Get("Range") != "" {
|
|
||||||
r, err := http_range.ParseRange(header.Get("Range"), size)
|
|
||||||
if err == nil && len(r) > 0 {
|
|
||||||
_, err := file.Seek(r[0].Start, io.SeekStart)
|
|
||||||
if err == nil {
|
|
||||||
link.Data = utils.NewLimitReadCloser(file, func() error {
|
|
||||||
return file.Close()
|
|
||||||
}, r[0].Length)
|
|
||||||
link.Status = http.StatusPartialContent
|
|
||||||
link.Header = http.Header{
|
|
||||||
"Content-Range": []string{r[0].ContentRange(size)},
|
|
||||||
"Content-Length": []string{strconv.FormatInt(r[0].Length, 10)},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package cloudreve
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
// Usually one of two
|
|
||||||
driver.RootPath
|
|
||||||
// define other
|
|
||||||
Address string `json:"address" required:"true"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Cookie string `json:"cookie"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "Cloudreve",
|
|
||||||
DefaultRoot: "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Cloudreve{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package cloudreve
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/internal/setting"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/cookie"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
json "github.com/json-iterator/go"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
)
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
|
|
||||||
const loginPath = "/user/session"
|
|
||||||
|
|
||||||
func (d *Cloudreve) request(method string, path string, callback base.ReqCallback, out interface{}) error {
|
|
||||||
u := d.Address + "/api/v3" + path
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
req.SetHeaders(map[string]string{
|
|
||||||
"Cookie": "cloudreve-session=" + d.Cookie,
|
|
||||||
"Accept": "application/json, text/plain, */*",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
|
|
||||||
})
|
|
||||||
|
|
||||||
var r Resp
|
|
||||||
|
|
||||||
req.SetResult(&r)
|
|
||||||
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := req.Execute(method, u)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !resp.IsSuccess() {
|
|
||||||
return errors.New(resp.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Code != 0 {
|
|
||||||
|
|
||||||
// 刷新 cookie
|
|
||||||
if r.Code == http.StatusUnauthorized && path != loginPath {
|
|
||||||
if d.Username != "" && d.Password != "" {
|
|
||||||
err = d.login()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.request(method, path, callback, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New(r.Msg)
|
|
||||||
}
|
|
||||||
sess := cookie.GetCookie(resp.Cookies(), "cloudreve-session")
|
|
||||||
if sess != nil {
|
|
||||||
d.Cookie = sess.Value
|
|
||||||
}
|
|
||||||
if out != nil && r.Data != nil {
|
|
||||||
var marshal []byte
|
|
||||||
marshal, err = json.Marshal(r.Data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(marshal, out)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Cloudreve) login() error {
|
|
||||||
var siteConfig Config
|
|
||||||
err := d.request(http.MethodGet, "/site/config", nil, &siteConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
err = d.doLogin(siteConfig.LoginCaptcha)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil && err.Error() != "CAPTCHA not match." {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Cloudreve) doLogin(needCaptcha bool) error {
|
|
||||||
var captchaCode string
|
|
||||||
var err error
|
|
||||||
if needCaptcha {
|
|
||||||
var captcha string
|
|
||||||
err = d.request(http.MethodGet, "/site/captcha", nil, &captcha)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(captcha) == 0 {
|
|
||||||
return errors.New("can not get captcha")
|
|
||||||
}
|
|
||||||
i := strings.Index(captcha, ",")
|
|
||||||
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(captcha[i+1:]))
|
|
||||||
vRes, err := base.RestyClient.R().SetMultipartField(
|
|
||||||
"image", "validateCode.png", "image/png", dec).
|
|
||||||
Post(setting.GetStr(conf.OcrApi))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 {
|
|
||||||
return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString())
|
|
||||||
}
|
|
||||||
captchaCode = jsoniter.Get(vRes.Body(), "result").ToString()
|
|
||||||
}
|
|
||||||
var resp Resp
|
|
||||||
err = d.request(http.MethodPost, loginPath, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"username": d.Addition.Username,
|
|
||||||
"Password": d.Addition.Password,
|
|
||||||
"captchaCode": captchaCode,
|
|
||||||
})
|
|
||||||
}, &resp)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertSrc(obj model.Obj) map[string]interface{} {
|
|
||||||
m := make(map[string]interface{})
|
|
||||||
var dirs []string
|
|
||||||
var items []string
|
|
||||||
if obj.IsDir() {
|
|
||||||
dirs = append(dirs, obj.GetID())
|
|
||||||
} else {
|
|
||||||
items = append(items, obj.GetID())
|
|
||||||
}
|
|
||||||
m["dirs"] = dirs
|
|
||||||
m["items"] = items
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package dropbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultClientID = "76lrwrklhdn1icb"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
RefreshToken string `json:"refresh_token" required:"true"`
|
|
||||||
driver.RootPath
|
|
||||||
|
|
||||||
OauthTokenURL string `json:"oauth_token_url" default:"https://api.xhofe.top/alist/dropbox/token"`
|
|
||||||
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
|
|
||||||
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
|
|
||||||
|
|
||||||
AccessToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "Dropbox",
|
|
||||||
LocalSort: false,
|
|
||||||
OnlyLocal: false,
|
|
||||||
OnlyProxy: false,
|
|
||||||
NoCache: false,
|
|
||||||
NoUpload: false,
|
|
||||||
NeedMs: false,
|
|
||||||
DefaultRoot: "",
|
|
||||||
NoOverwriteUpload: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Dropbox{
|
|
||||||
base: "https://api.dropboxapi.com",
|
|
||||||
contentBase: "https://content.dropboxapi.com",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package ftp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
stdpath "path"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/jlaffaye/ftp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FTP struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
conn *ftp.ServerConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) Init(ctx context.Context) error {
|
|
||||||
return d.login()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) Drop(ctx context.Context) error {
|
|
||||||
if d.conn != nil {
|
|
||||||
_ = d.conn.Logout()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
if err := d.login(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
entries, err := d.conn.List(dir.GetPath())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res := make([]model.Obj, 0)
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.Name == "." || entry.Name == ".." {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f := model.Object{
|
|
||||||
Name: entry.Name,
|
|
||||||
Size: int64(entry.Size),
|
|
||||||
Modified: entry.Time,
|
|
||||||
IsFolder: entry.Type == ftp.EntryTypeFolder,
|
|
||||||
}
|
|
||||||
res = append(res, &f)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
if err := d.login(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r := NewFTPFileReader(d.conn, file.GetPath())
|
|
||||||
link := &model.Link{
|
|
||||||
Data: r,
|
|
||||||
}
|
|
||||||
base.HandleRange(link, r, args.Header, file.GetSize())
|
|
||||||
return link, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
if err := d.login(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.conn.MakeDir(stdpath.Join(parentDir.GetPath(), dirName))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
if err := d.login(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.conn.Rename(srcObj.GetPath(), stdpath.Join(dstDir.GetPath(), srcObj.GetName()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
if err := d.login(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.conn.Rename(srcObj.GetPath(), stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
if err := d.login(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if obj.IsDir() {
|
|
||||||
return d.conn.RemoveDirRecur(obj.GetPath())
|
|
||||||
} else {
|
|
||||||
return d.conn.Delete(obj.GetPath())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
if err := d.login(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// TODO: support cancel
|
|
||||||
return d.conn.Stor(stdpath.Join(dstDir.GetPath(), stream.GetName()), stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*FTP)(nil)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package ftp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
Address string `json:"address" required:"true"`
|
|
||||||
Username string `json:"username" required:"true"`
|
|
||||||
Password string `json:"password" required:"true"`
|
|
||||||
driver.RootPath
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "FTP",
|
|
||||||
LocalSort: true,
|
|
||||||
OnlyLocal: true,
|
|
||||||
DefaultRoot: "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &FTP{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
package ftp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jlaffaye/ftp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
|
|
||||||
func (d *FTP) login() error {
|
|
||||||
if d.conn != nil {
|
|
||||||
_, err := d.conn.CurrentDir()
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conn, err := ftp.Dial(d.Address, ftp.DialWithShutTimeout(10*time.Second))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = conn.Login(d.Username, d.Password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.conn = conn
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// An FTP file reader that implements io.ReadSeekCloser for seeking.
|
|
||||||
type FTPFileReader struct {
|
|
||||||
conn *ftp.ServerConn
|
|
||||||
resp *ftp.Response
|
|
||||||
offset int64
|
|
||||||
mu sync.Mutex
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFTPFileReader(conn *ftp.ServerConn, path string) *FTPFileReader {
|
|
||||||
return &FTPFileReader{
|
|
||||||
conn: conn,
|
|
||||||
path: path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FTPFileReader) Read(buf []byte) (n int, err error) {
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
|
|
||||||
if r.resp == nil {
|
|
||||||
r.resp, err = r.conn.RetrFrom(r.path, uint64(r.offset))
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err = r.resp.Read(buf)
|
|
||||||
r.offset += int64(n)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FTPFileReader) Seek(offset int64, whence int) (int64, error) {
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
|
|
||||||
oldOffset := r.offset
|
|
||||||
var newOffset int64
|
|
||||||
switch whence {
|
|
||||||
case io.SeekStart:
|
|
||||||
newOffset = offset
|
|
||||||
case io.SeekCurrent:
|
|
||||||
newOffset = oldOffset + offset
|
|
||||||
case io.SeekEnd:
|
|
||||||
size, err := r.conn.FileSize(r.path)
|
|
||||||
if err != nil {
|
|
||||||
return oldOffset, err
|
|
||||||
}
|
|
||||||
newOffset = offset + int64(size)
|
|
||||||
default:
|
|
||||||
return -1, os.ErrInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
if newOffset < 0 {
|
|
||||||
// offset out of range
|
|
||||||
return oldOffset, os.ErrInvalid
|
|
||||||
}
|
|
||||||
if newOffset == oldOffset {
|
|
||||||
// offset not changed, so return directly
|
|
||||||
return oldOffset, nil
|
|
||||||
}
|
|
||||||
r.offset = newOffset
|
|
||||||
|
|
||||||
if r.resp != nil {
|
|
||||||
// close the existing ftp data connection, otherwise the next read will be blocked
|
|
||||||
_ = r.resp.Close() // we do not care about whether it returns an error
|
|
||||||
r.resp = nil
|
|
||||||
}
|
|
||||||
return newOffset, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FTPFileReader) Close() error {
|
|
||||||
if r.resp != nil {
|
|
||||||
return r.resp.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package ipfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
stdpath "path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
shell "github.com/ipfs/go-ipfs-api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IPFS struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
sh *shell.Shell
|
|
||||||
gateURL *url.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) Init(ctx context.Context) error {
|
|
||||||
d.sh = shell.NewShell(d.Endpoint)
|
|
||||||
gateURL, err := url.Parse(d.Gateway)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.gateURL = gateURL
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
path := dir.GetPath()
|
|
||||||
if path[len(path):] != "/" {
|
|
||||||
path += "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
path_cid, err := d.sh.FilesStat(ctx, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dirs, err := d.sh.List(path_cid.Hash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
objlist := []model.Obj{}
|
|
||||||
for _, file := range dirs {
|
|
||||||
gateurl := *d.gateURL
|
|
||||||
gateurl.Path = "ipfs/" + file.Hash
|
|
||||||
gateurl.RawQuery = "filename=" + file.Name
|
|
||||||
objlist = append(objlist, &model.ObjectURL{
|
|
||||||
Object: model.Object{ID: file.Hash, Name: file.Name, Size: int64(file.Size), IsFolder: file.Type == 1},
|
|
||||||
Url: model.Url{Url: gateurl.String()},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return objlist, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + file.GetName()
|
|
||||||
return &model.Link{URL: link}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
path := parentDir.GetPath()
|
|
||||||
if path[len(path):] != "/" {
|
|
||||||
path += "/"
|
|
||||||
}
|
|
||||||
return d.sh.FilesMkdir(ctx, path+dirName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
return d.sh.FilesMv(ctx, srcObj.GetPath(), dstDir.GetPath())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
newFileName := filepath.Dir(srcObj.GetPath()) + "/" + newName
|
|
||||||
return d.sh.FilesMv(ctx, srcObj.GetPath(), strings.ReplaceAll(newFileName, "\\", "/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
// TODO copy obj, optional
|
|
||||||
fmt.Println(srcObj.GetPath())
|
|
||||||
fmt.Println(dstDir.GetPath())
|
|
||||||
newFileName := dstDir.GetPath() + "/" + filepath.Base(srcObj.GetPath())
|
|
||||||
fmt.Println(newFileName)
|
|
||||||
return d.sh.FilesCp(ctx, srcObj.GetPath(), strings.ReplaceAll(newFileName, "\\", "/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
// TODO remove obj, optional
|
|
||||||
return d.sh.FilesRm(ctx, obj.GetPath(), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IPFS) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
// TODO upload file, optional
|
|
||||||
_, err := d.sh.Add(stream, ToFiles(stdpath.Join(dstDir.GetPath(), stream.GetName())))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToFiles(dstDir string) shell.AddOpts {
|
|
||||||
return func(rb *shell.RequestBuilder) error {
|
|
||||||
rb.Option("to-files", dstDir)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
|
||||||
// return nil, errs.NotSupport
|
|
||||||
//}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*IPFS)(nil)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package lanzou
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
Type string `json:"type" type:"select" options:"cookie,url" default:"cookie"`
|
|
||||||
Cookie string `json:"cookie" required:"true" help:"about 15 days valid, ignore if shareUrl is used"`
|
|
||||||
driver.RootID
|
|
||||||
SharePassword string `json:"share_password"`
|
|
||||||
BaseUrl string `json:"baseUrl" required:"true" default:"https://pc.woozooo.com" help:"basic URL for file operation"`
|
|
||||||
ShareUrl string `json:"shareUrl" required:"true" default:"https://pan.lanzouo.com" help:"used to get the sharing page"`
|
|
||||||
RepairFileInfo bool `json:"repair_file_info" help:"To use webdav, you need to enable it"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Addition) IsCookie() bool {
|
|
||||||
return a.Type == "cookie"
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "Lanzou",
|
|
||||||
LocalSort: true,
|
|
||||||
DefaultRoot: "-1",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &LanZou{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
stdpath "path"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/internal/sign"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
|
||||||
_ "golang.org/x/image/webp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Local struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
mkdirPerm int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) Init(ctx context.Context) error {
|
|
||||||
if d.MkdirPerm == "" {
|
|
||||||
d.mkdirPerm = 0777
|
|
||||||
} else {
|
|
||||||
v, err := strconv.ParseUint(d.MkdirPerm, 8, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.mkdirPerm = int32(v)
|
|
||||||
}
|
|
||||||
if !utils.Exists(d.GetRootPath()) {
|
|
||||||
return fmt.Errorf("root folder %s not exists", d.GetRootPath())
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(d.GetRootPath()) {
|
|
||||||
abs, err := filepath.Abs(d.GetRootPath())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.Addition.RootFolderPath = abs
|
|
||||||
}
|
|
||||||
if d.ThumbCacheFolder != "" && !utils.Exists(d.ThumbCacheFolder) {
|
|
||||||
err := os.MkdirAll(d.ThumbCacheFolder, os.FileMode(d.mkdirPerm))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
fullPath := dir.GetPath()
|
|
||||||
rawFiles, err := readDir(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var files []model.Obj
|
|
||||||
for _, f := range rawFiles {
|
|
||||||
if !d.ShowHidden && strings.HasPrefix(f.Name(), ".") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
thumb := ""
|
|
||||||
if d.Thumbnail {
|
|
||||||
typeName := utils.GetFileType(f.Name())
|
|
||||||
if typeName == conf.IMAGE || typeName == conf.VIDEO {
|
|
||||||
thumb = common.GetApiUrl(nil) + stdpath.Join("/d", args.ReqPath, f.Name())
|
|
||||||
thumb = utils.EncodePath(thumb, true)
|
|
||||||
thumb += "?type=thumb&sign=" + sign.Sign(stdpath.Join(args.ReqPath, f.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isFolder := f.IsDir() || isSymlinkDir(f, fullPath)
|
|
||||||
var size int64
|
|
||||||
if !isFolder {
|
|
||||||
size = f.Size()
|
|
||||||
}
|
|
||||||
file := model.ObjThumb{
|
|
||||||
Object: model.Object{
|
|
||||||
Path: filepath.Join(dir.GetPath(), f.Name()),
|
|
||||||
Name: f.Name(),
|
|
||||||
Modified: f.ModTime(),
|
|
||||||
Size: size,
|
|
||||||
IsFolder: isFolder,
|
|
||||||
},
|
|
||||||
Thumbnail: model.Thumbnail{
|
|
||||||
Thumbnail: thumb,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
files = append(files, &file)
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {
|
|
||||||
path = filepath.Join(d.GetRootPath(), path)
|
|
||||||
f, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "cannot find the file") {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isFolder := f.IsDir() || isSymlinkDir(f, path)
|
|
||||||
size := f.Size()
|
|
||||||
if isFolder {
|
|
||||||
size = 0
|
|
||||||
}
|
|
||||||
file := model.Object{
|
|
||||||
Path: path,
|
|
||||||
Name: f.Name(),
|
|
||||||
Modified: f.ModTime(),
|
|
||||||
Size: size,
|
|
||||||
IsFolder: isFolder,
|
|
||||||
}
|
|
||||||
return &file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
fullPath := file.GetPath()
|
|
||||||
var link model.Link
|
|
||||||
if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" {
|
|
||||||
buf, thumbPath, err := d.getThumb(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
link.Header = http.Header{
|
|
||||||
"Content-Type": []string{"image/png"},
|
|
||||||
}
|
|
||||||
if thumbPath != nil {
|
|
||||||
link.FilePath = thumbPath
|
|
||||||
} else {
|
|
||||||
link.Data = io.NopCloser(buf)
|
|
||||||
link.Header.Set("Content-Length", strconv.Itoa(buf.Len()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
link.FilePath = &fullPath
|
|
||||||
}
|
|
||||||
return &link, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
fullPath := filepath.Join(parentDir.GetPath(), dirName)
|
|
||||||
err := os.MkdirAll(fullPath, os.FileMode(d.mkdirPerm))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
srcPath := srcObj.GetPath()
|
|
||||||
dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())
|
|
||||||
if utils.IsSubPath(srcPath, dstPath) {
|
|
||||||
return fmt.Errorf("the destination folder is a subfolder of the source folder")
|
|
||||||
}
|
|
||||||
err := os.Rename(srcPath, dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
srcPath := srcObj.GetPath()
|
|
||||||
dstPath := filepath.Join(filepath.Dir(srcPath), newName)
|
|
||||||
err := os.Rename(srcPath, dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
srcPath := srcObj.GetPath()
|
|
||||||
dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())
|
|
||||||
if utils.IsSubPath(srcPath, dstPath) {
|
|
||||||
return fmt.Errorf("the destination folder is a subfolder of the source folder")
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
if srcObj.IsDir() {
|
|
||||||
err = utils.CopyDir(srcPath, dstPath)
|
|
||||||
} else {
|
|
||||||
err = utils.CopyFile(srcPath, dstPath)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
var err error
|
|
||||||
if obj.IsDir() {
|
|
||||||
err = os.RemoveAll(obj.GetPath())
|
|
||||||
} else {
|
|
||||||
err = os.Remove(obj.GetPath())
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
fullPath := filepath.Join(dstDir.GetPath(), stream.GetName())
|
|
||||||
out, err := os.Create(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = out.Close()
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
_ = os.Remove(fullPath)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
err = utils.CopyWithCtx(ctx, out, stream, stream.GetSize(), up)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Local)(nil)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
driver.RootPath
|
|
||||||
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
|
|
||||||
ThumbCacheFolder string `json:"thumb_cache_folder"`
|
|
||||||
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
|
|
||||||
MkdirPerm string `json:"mkdir_perm" default:"777"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "Local",
|
|
||||||
OnlyLocal: true,
|
|
||||||
LocalSort: true,
|
|
||||||
NoCache: true,
|
|
||||||
DefaultRoot: "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Local{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
ffmpeg "github.com/u2takey/ffmpeg-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
func isSymlinkDir(f fs.FileInfo, path string) bool {
|
|
||||||
if f.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
||||||
dst, err := os.Readlink(filepath.Join(path, f.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(dst) {
|
|
||||||
dst = filepath.Join(path, dst)
|
|
||||||
}
|
|
||||||
stat, err := os.Stat(dst)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return stat.IsDir()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSnapshot(videoPath string, frameNum int) (imgData *bytes.Buffer, err error) {
|
|
||||||
srcBuf := bytes.NewBuffer(nil)
|
|
||||||
err = ffmpeg.Input(videoPath).Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}).
|
|
||||||
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
|
|
||||||
WithOutput(srcBuf, os.Stdout).
|
|
||||||
Run()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return srcBuf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readDir(dirname string) ([]fs.FileInfo, error) {
|
|
||||||
f, err := os.Open(dirname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
list, err := f.Readdir(-1)
|
|
||||||
f.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() })
|
|
||||||
return list, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {
|
|
||||||
fullPath := file.GetPath()
|
|
||||||
thumbPrefix := "alist_thumb_"
|
|
||||||
thumbName := thumbPrefix + utils.GetMD5EncodeStr(fullPath) + ".png"
|
|
||||||
if d.ThumbCacheFolder != "" {
|
|
||||||
// skip if the file is a thumbnail
|
|
||||||
if strings.HasPrefix(file.GetName(), thumbPrefix) {
|
|
||||||
return nil, &fullPath, nil
|
|
||||||
}
|
|
||||||
thumbPath := filepath.Join(d.ThumbCacheFolder, thumbName)
|
|
||||||
if utils.Exists(thumbPath) {
|
|
||||||
return nil, &thumbPath, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var srcBuf *bytes.Buffer
|
|
||||||
if utils.GetFileType(file.GetName()) == conf.VIDEO {
|
|
||||||
videoBuf, err := GetSnapshot(fullPath, 10)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
srcBuf = videoBuf
|
|
||||||
} else {
|
|
||||||
imgData, err := os.ReadFile(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
imgBuf := bytes.NewBuffer(imgData)
|
|
||||||
srcBuf = imgBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
image, err := imaging.Decode(srcBuf, imaging.AutoOrientation(true))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
thumbImg := imaging.Resize(image, 144, 0, imaging.Lanczos)
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err = imaging.Encode(&buf, thumbImg, imaging.PNG)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if d.ThumbCacheFolder != "" {
|
|
||||||
err = os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), buf.Bytes(), 0666)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &buf, nil, nil
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
package mega
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/t3rm1n4l/go-mega"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Mega struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
c *mega.Mega
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) Init(ctx context.Context) error {
|
|
||||||
d.c = mega.New()
|
|
||||||
return d.c.Login(d.Email, d.Password)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
if node, ok := dir.(*MegaNode); ok {
|
|
||||||
nodes, err := d.c.FS.GetChildren(node.Node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res := make([]model.Obj, 0)
|
|
||||||
for i := range nodes {
|
|
||||||
n := nodes[i]
|
|
||||||
if n.GetType() == mega.FILE || n.GetType() == mega.FOLDER {
|
|
||||||
res = append(res, &MegaNode{n})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
log.Errorf("can't convert: %+v", dir)
|
|
||||||
return nil, fmt.Errorf("unable to convert dir to mega node")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) GetRoot(ctx context.Context) (model.Obj, error) {
|
|
||||||
n := d.c.FS.GetRoot()
|
|
||||||
log.Debugf("mega root: %+v", *n)
|
|
||||||
return &MegaNode{n}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
if node, ok := file.(*MegaNode); ok {
|
|
||||||
//link, err := d.c.Link(node.Node, true)
|
|
||||||
//if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
//}
|
|
||||||
//return &model.Link{URL: link}, nil
|
|
||||||
down, err := d.c.NewDownload(node.Node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
//u := down.GetResourceUrl()
|
|
||||||
//u = strings.Replace(u, "http", "https", 1)
|
|
||||||
//return &model.Link{URL: u}, nil
|
|
||||||
r, w := io.Pipe()
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
_ = recover()
|
|
||||||
}()
|
|
||||||
log.Debugf("chunk size: %d", down.Chunks())
|
|
||||||
var (
|
|
||||||
chunk []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
for id := 0; id < down.Chunks(); id++ {
|
|
||||||
chunk, err = down.DownloadChunk(id)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("mega down: %+v", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
log.Debugf("id: %d,len: %d", id, len(chunk))
|
|
||||||
//_, _, err = down.ChunkLocation(id)
|
|
||||||
//if err != nil {
|
|
||||||
// log.Errorf("mega down: %+v", err)
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
//_, err = c.Write(chunk)
|
|
||||||
if _, err = w.Write(chunk); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = w.CloseWithError(err)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("mega down: %+v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return &model.Link{Data: r}, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unable to convert dir to mega node")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
if parentNode, ok := parentDir.(*MegaNode); ok {
|
|
||||||
_, err := d.c.CreateDir(dirName, parentNode.Node)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unable to convert dir to mega node")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
if srcNode, ok := srcObj.(*MegaNode); ok {
|
|
||||||
if dstNode, ok := dstDir.(*MegaNode); ok {
|
|
||||||
return d.c.Move(srcNode.Node, dstNode.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unable to convert dir to mega node")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
if srcNode, ok := srcObj.(*MegaNode); ok {
|
|
||||||
return d.c.Rename(srcNode.Node, newName)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unable to convert dir to mega node")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
if node, ok := obj.(*MegaNode); ok {
|
|
||||||
return d.c.Delete(node.Node, false)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unable to convert dir to mega node")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Mega) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
if dstNode, ok := dstDir.(*MegaNode); ok {
|
|
||||||
u, err := d.c.NewUpload(dstNode.Node, stream.GetName(), stream.GetSize())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for id := 0; id < u.Chunks(); id++ {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
_, chkSize, err := u.ChunkLocation(id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
chunk := make([]byte, chkSize)
|
|
||||||
n, err := io.ReadFull(stream, chunk)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if n != len(chunk) {
|
|
||||||
return errors.New("chunk too short")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = u.UploadChunk(id, chunk)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
up(id * 100 / u.Chunks())
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = u.Finish()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unable to convert dir to mega node")
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (d *Mega) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
|
||||||
// return nil, errs.NotSupport
|
|
||||||
//}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Mega)(nil)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package mega
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
// Usually one of two
|
|
||||||
//driver.RootPath
|
|
||||||
//driver.RootID
|
|
||||||
Email string `json:"email" required:"true"`
|
|
||||||
Password string `json:"password" required:"true"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "Mega_nz",
|
|
||||||
LocalSort: true,
|
|
||||||
OnlyLocal: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Mega{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package mega
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/t3rm1n4l/go-mega"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MegaNode struct {
|
|
||||||
*mega.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (m *MegaNode) GetSize() int64 {
|
|
||||||
// //TODO implement me
|
|
||||||
// panic("implement me")
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func (m *MegaNode) GetName() string {
|
|
||||||
// //TODO implement me
|
|
||||||
// panic("implement me")
|
|
||||||
//}
|
|
||||||
|
|
||||||
func (m *MegaNode) ModTime() time.Time {
|
|
||||||
return m.GetTimeStamp()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MegaNode) IsDir() bool {
|
|
||||||
return m.GetType() == mega.FOLDER || m.GetType() == mega.ROOT
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MegaNode) GetID() string {
|
|
||||||
return m.GetHash()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MegaNode) GetPath() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ model.Obj = (*MegaNode)(nil)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package mega
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package onedrive
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Host struct {
|
|
||||||
Oauth string
|
|
||||||
Api string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenErr struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
ErrorDescription string `json:"error_description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RespErr struct {
|
|
||||||
Error struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
} `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type File 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 Object struct {
|
|
||||||
model.ObjThumb
|
|
||||||
ParentID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileToObj(f File, parentID string) *Object {
|
|
||||||
thumb := ""
|
|
||||||
if len(f.Thumbnails) > 0 {
|
|
||||||
thumb = f.Thumbnails[0].Medium.Url
|
|
||||||
}
|
|
||||||
return &Object{
|
|
||||||
ObjThumb: model.ObjThumb{
|
|
||||||
Object: model.Object{
|
|
||||||
ID: f.Id,
|
|
||||||
Name: f.Name,
|
|
||||||
Size: f.Size,
|
|
||||||
Modified: f.LastModifiedDateTime,
|
|
||||||
IsFolder: f.File == nil,
|
|
||||||
},
|
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: thumb},
|
|
||||||
//Url: model.Url{Url: f.Url},
|
|
||||||
},
|
|
||||||
ParentID: parentID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Files struct {
|
|
||||||
Value []File `json:"value"`
|
|
||||||
NextLink string `json:"@odata.nextLink"`
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
package onedrive
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
stdpath "path"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 (d *Onedrive) GetMetaUrl(auth bool, path string) string {
|
|
||||||
host, _ := onedriveHostMap[d.Region]
|
|
||||||
path = utils.EncodePath(path, true)
|
|
||||||
if auth {
|
|
||||||
return host.Oauth
|
|
||||||
}
|
|
||||||
if d.IsSharepoint {
|
|
||||||
if path == "/" || path == "\\" {
|
|
||||||
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root", host.Api, d.SiteId)
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root:%s:", host.Api, d.SiteId, path)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Onedrive) refreshToken() error {
|
|
||||||
var err error
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
err = d._refreshToken()
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Onedrive) _refreshToken() error {
|
|
||||||
url := d.GetMetaUrl(true, "") + "/common/oauth2/v2.0/token"
|
|
||||||
var resp base.TokenResp
|
|
||||||
var e TokenErr
|
|
||||||
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
|
|
||||||
"grant_type": "refresh_token",
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
"client_secret": d.ClientSecret,
|
|
||||||
"redirect_uri": d.RedirectUri,
|
|
||||||
"refresh_token": d.RefreshToken,
|
|
||||||
}).Post(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.Error != "" {
|
|
||||||
return fmt.Errorf("%s", e.ErrorDescription)
|
|
||||||
}
|
|
||||||
if resp.RefreshToken == "" {
|
|
||||||
return errs.EmptyToken
|
|
||||||
}
|
|
||||||
d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Onedrive) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
req.SetResult(resp)
|
|
||||||
}
|
|
||||||
var e RespErr
|
|
||||||
req.SetError(&e)
|
|
||||||
res, err := req.Execute(method, url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if e.Error.Code != "" {
|
|
||||||
if e.Error.Code == "InvalidAuthenticationToken" {
|
|
||||||
err = d.refreshToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.Request(url, method, callback, resp)
|
|
||||||
}
|
|
||||||
return nil, errors.New(e.Error.Message)
|
|
||||||
}
|
|
||||||
return res.Body(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Onedrive) getFiles(path string) ([]File, error) {
|
|
||||||
var res []File
|
|
||||||
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,lastModifiedDateTime,content.downloadUrl,file,parentReference"
|
|
||||||
for nextLink != "" {
|
|
||||||
var files Files
|
|
||||||
_, err := d.Request(nextLink, http.MethodGet, nil, &files)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res = append(res, files.Value...)
|
|
||||||
nextLink = files.NextLink
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Onedrive) GetFile(path string) (*File, error) {
|
|
||||||
var file File
|
|
||||||
u := d.GetMetaUrl(false, path)
|
|
||||||
_, err := d.Request(u, http.MethodGet, nil, &file)
|
|
||||||
return &file, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) error {
|
|
||||||
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/content"
|
|
||||||
data, err := io.ReadAll(stream)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = d.Request(url, http.MethodPut, func(req *resty.Request) {
|
|
||||||
req.SetBody(data).SetContext(ctx)
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/createUploadSession"
|
|
||||||
res, err := d.Request(url, http.MethodPost, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
|
|
||||||
var finish int64 = 0
|
|
||||||
DEFAULT := d.ChunkSize * 1024 * 1024
|
|
||||||
for finish < stream.GetSize() {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
log.Debugf("upload: %d", finish)
|
|
||||||
var byteSize int64 = DEFAULT
|
|
||||||
left := stream.GetSize() - finish
|
|
||||||
if left < DEFAULT {
|
|
||||||
byteSize = left
|
|
||||||
}
|
|
||||||
byteData := make([]byte, byteSize)
|
|
||||||
n, err := io.ReadFull(stream, byteData)
|
|
||||||
log.Debug(err, n)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest("PUT", uploadUrl, bytes.NewBuffer(byteData))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
|
||||||
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
|
|
||||||
finish += byteSize
|
|
||||||
res, err := base.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if res.StatusCode != 201 && res.StatusCode != 202 {
|
|
||||||
data, _ := io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
return errors.New(string(data))
|
|
||||||
}
|
|
||||||
res.Body.Close()
|
|
||||||
up(int(finish * 100 / stream.GetSize()))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
package pikpak
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/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/go-resty/resty/v2"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PikPak struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
RefreshToken string
|
|
||||||
AccessToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) Init(ctx context.Context) error {
|
|
||||||
return d.login()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
files, err := d.getFiles(dir.GetID())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
|
||||||
return fileToObj(src), nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
var resp File
|
|
||||||
_, err := d.request(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.GetID()),
|
|
||||||
http.MethodGet, nil, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
link := model.Link{
|
|
||||||
URL: resp.WebContentLink,
|
|
||||||
}
|
|
||||||
if !d.DisableMediaLink && len(resp.Medias) > 0 && resp.Medias[0].Link.Url != "" {
|
|
||||||
log.Debugln("use media link")
|
|
||||||
link.URL = resp.Medias[0].Link.Url
|
|
||||||
}
|
|
||||||
return &link, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"kind": "drive#folder",
|
|
||||||
"parent_id": parentDir.GetID(),
|
|
||||||
"name": dirName,
|
|
||||||
})
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files:batchMove", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"ids": []string{srcObj.GetID()},
|
|
||||||
"to": base.Json{
|
|
||||||
"parent_id": dstDir.GetID(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files/"+srcObj.GetID(), http.MethodPatch, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"name": newName,
|
|
||||||
})
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files:batchCopy", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"ids": []string{srcObj.GetID()},
|
|
||||||
"to": base.Json{
|
|
||||||
"parent_id": dstDir.GetID(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/files:batchTrash", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"ids": []string{obj.GetID()},
|
|
||||||
})
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
tempFile, err := utils.CreateTempFile(stream.GetReadCloser())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tempFile.Close()
|
|
||||||
_ = os.Remove(tempFile.Name())
|
|
||||||
}()
|
|
||||||
// cal gcid
|
|
||||||
sha1Str, err := getGcid(tempFile, stream.GetSize())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tempFile.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var resp UploadTaskData
|
|
||||||
res, err := d.request("https://api-drive.mypikpak.com/drive/v1/files", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
|
||||||
"kind": "drive#file",
|
|
||||||
"name": stream.GetName(),
|
|
||||||
"size": stream.GetSize(),
|
|
||||||
"hash": strings.ToUpper(sha1Str),
|
|
||||||
"upload_type": "UPLOAD_TYPE_RESUMABLE",
|
|
||||||
"objProvider": base.Json{"provider": "UPLOAD_TYPE_UNKNOWN"},
|
|
||||||
"parent_id": dstDir.GetID(),
|
|
||||||
"folder_type": "NORMAL",
|
|
||||||
})
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 秒传成功
|
|
||||||
if resp.Resumable == nil {
|
|
||||||
log.Debugln(string(res))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
params := resp.Resumable.Params
|
|
||||||
endpoint := strings.Join(strings.Split(params.Endpoint, ".")[1:], ".")
|
|
||||||
cfg := &aws.Config{
|
|
||||||
Credentials: credentials.NewStaticCredentials(params.AccessKeyID, params.AccessKeySecret, params.SecurityToken),
|
|
||||||
Region: aws.String("pikpak"),
|
|
||||||
Endpoint: &endpoint,
|
|
||||||
}
|
|
||||||
ss, err := session.NewSession(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
uploader := s3manager.NewUploader(ss)
|
|
||||||
input := &s3manager.UploadInput{
|
|
||||||
Bucket: ¶ms.Bucket,
|
|
||||||
Key: ¶ms.Key,
|
|
||||||
Body: tempFile,
|
|
||||||
}
|
|
||||||
_, err = uploader.UploadWithContext(ctx, input)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*PikPak)(nil)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package pikpak
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
driver.RootID
|
|
||||||
Username string `json:"username" required:"true"`
|
|
||||||
Password string `json:"password" required:"true"`
|
|
||||||
DisableMediaLink bool `json:"disable_media_link"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "PikPak",
|
|
||||||
LocalSort: true,
|
|
||||||
DefaultRoot: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &PikPak{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package pikpak
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RespErr struct {
|
|
||||||
ErrorCode int `json:"error_code"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Files struct {
|
|
||||||
Files []File `json:"files"`
|
|
||||||
NextPageToken string `json:"next_page_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
Medias []Media `json:"medias"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileToObj(f File) *model.ObjThumb {
|
|
||||||
size, _ := strconv.ParseInt(f.Size, 10, 64)
|
|
||||||
return &model.ObjThumb{
|
|
||||||
Object: model.Object{
|
|
||||||
ID: f.Id,
|
|
||||||
Name: f.Name,
|
|
||||||
Size: size,
|
|
||||||
Modified: f.ModifiedTime,
|
|
||||||
IsFolder: f.Kind == "drive#folder",
|
|
||||||
},
|
|
||||||
Thumbnail: model.Thumbnail{
|
|
||||||
Thumbnail: f.ThumbnailLink,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Media struct {
|
|
||||||
MediaId string `json:"media_id"`
|
|
||||||
MediaName string `json:"media_name"`
|
|
||||||
Video struct {
|
|
||||||
Height int `json:"height"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Duration int `json:"duration"`
|
|
||||||
BitRate int `json:"bit_rate"`
|
|
||||||
FrameRate int `json:"frame_rate"`
|
|
||||||
VideoCodec string `json:"video_codec"`
|
|
||||||
AudioCodec string `json:"audio_codec"`
|
|
||||||
VideoType string `json:"video_type"`
|
|
||||||
} `json:"video"`
|
|
||||||
Link struct {
|
|
||||||
Url string `json:"url"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Expire time.Time `json:"expire"`
|
|
||||||
} `json:"link"`
|
|
||||||
NeedMoreQuota bool `json:"need_more_quota"`
|
|
||||||
VipTypes []interface{} `json:"vip_types"`
|
|
||||||
RedirectLink string `json:"redirect_link"`
|
|
||||||
IconLink string `json:"icon_link"`
|
|
||||||
IsDefault bool `json:"is_default"`
|
|
||||||
Priority int `json:"priority"`
|
|
||||||
IsOrigin bool `json:"is_origin"`
|
|
||||||
ResolutionName string `json:"resolution_name"`
|
|
||||||
IsVisible bool `json:"is_visible"`
|
|
||||||
Category string `json:"category"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UploadTaskData struct {
|
|
||||||
UploadType string `json:"upload_type"`
|
|
||||||
//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 File `json:"file"`
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
package pikpak
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
)
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
|
|
||||||
func (d *PikPak) login() error {
|
|
||||||
url := "https://user.mypikpak.com/v1/auth/signin"
|
|
||||||
var e RespErr
|
|
||||||
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
|
|
||||||
"captcha_token": "",
|
|
||||||
"client_id": "YNxT9w7GMdWvEOKa",
|
|
||||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
|
||||||
"username": d.Username,
|
|
||||||
"password": d.Password,
|
|
||||||
}).Post(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.ErrorCode != 0 {
|
|
||||||
return errors.New(e.Error)
|
|
||||||
}
|
|
||||||
data := res.Body()
|
|
||||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
|
||||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) refreshToken() error {
|
|
||||||
url := "https://user.mypikpak.com/v1/auth/token"
|
|
||||||
var e RespErr
|
|
||||||
res, err := base.RestyClient.R().SetError(&e).
|
|
||||||
SetHeader("user-agent", "").SetBody(base.Json{
|
|
||||||
"client_id": "YNxT9w7GMdWvEOKa",
|
|
||||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
|
||||||
"grant_type": "refresh_token",
|
|
||||||
"refresh_token": d.RefreshToken,
|
|
||||||
}).Post(url)
|
|
||||||
if err != nil {
|
|
||||||
d.Status = err.Error()
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.ErrorCode != 0 {
|
|
||||||
if e.ErrorCode == 4126 {
|
|
||||||
// refresh_token invalid, re-login
|
|
||||||
return d.login()
|
|
||||||
}
|
|
||||||
d.Status = e.Error
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
return errors.New(e.Error)
|
|
||||||
}
|
|
||||||
data := res.Body()
|
|
||||||
d.Status = "work"
|
|
||||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
|
||||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
req.SetResult(resp)
|
|
||||||
}
|
|
||||||
var e RespErr
|
|
||||||
req.SetError(&e)
|
|
||||||
res, err := req.Execute(method, url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if e.ErrorCode != 0 {
|
|
||||||
if e.ErrorCode == 16 {
|
|
||||||
// login / refresh token
|
|
||||||
err = d.refreshToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.request(url, method, callback, resp)
|
|
||||||
} else {
|
|
||||||
return nil, errors.New(e.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res.Body(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPak) getFiles(id string) ([]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 := d.request("https://api-drive.mypikpak.com/drive/v1/files", http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(query)
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pageToken = resp.NextPageToken
|
|
||||||
res = append(res, resp.Files...)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getGcid(r io.Reader, size int64) (string, error) {
|
|
||||||
calcBlockSize := func(j int64) int64 {
|
|
||||||
var psize int64 = 0x40000
|
|
||||||
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
|
|
||||||
psize = psize << 1
|
|
||||||
}
|
|
||||||
return psize
|
|
||||||
}
|
|
||||||
|
|
||||||
hash1 := sha1.New()
|
|
||||||
hash2 := sha1.New()
|
|
||||||
readSize := calcBlockSize(size)
|
|
||||||
for {
|
|
||||||
hash2.Reset()
|
|
||||||
if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
|
|
||||||
if err != io.EOF {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
hash1.Write(hash2.Sum(nil))
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(hash1.Sum(nil)), nil
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package pikpak_share
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PikPakShare struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
RefreshToken string
|
|
||||||
AccessToken string
|
|
||||||
PassCodeToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPakShare) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPakShare) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPakShare) Init(ctx context.Context) error {
|
|
||||||
err := d.login()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if d.SharePwd != "" {
|
|
||||||
err = d.getSharePassToken()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPakShare) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPakShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
files, err := d.getFiles(dir.GetID())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
|
||||||
return fileToObj(src), nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
var resp ShareResp
|
|
||||||
query := map[string]string{
|
|
||||||
"share_id": d.ShareId,
|
|
||||||
"file_id": file.GetID(),
|
|
||||||
"pass_code_token": d.PassCodeToken,
|
|
||||||
}
|
|
||||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/share/file_info", http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(query)
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
link := model.Link{
|
|
||||||
URL: resp.FileInfo.WebContentLink,
|
|
||||||
}
|
|
||||||
return &link, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*PikPakShare)(nil)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package pikpak_share
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
driver.RootID
|
|
||||||
Username string `json:"username" required:"true"`
|
|
||||||
Password string `json:"password" required:"true"`
|
|
||||||
ShareId string `json:"share_id" required:"true"`
|
|
||||||
SharePwd string `json:"share_pwd"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "PikPakShare",
|
|
||||||
LocalSort: true,
|
|
||||||
NoUpload: true,
|
|
||||||
DefaultRoot: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &PikPakShare{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
package pikpak_share
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
)
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
|
|
||||||
func (d *PikPakShare) login() error {
|
|
||||||
url := "https://user.mypikpak.com/v1/auth/signin"
|
|
||||||
var e RespErr
|
|
||||||
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
|
|
||||||
"captcha_token": "",
|
|
||||||
"client_id": "YNxT9w7GMdWvEOKa",
|
|
||||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
|
||||||
"username": d.Username,
|
|
||||||
"password": d.Password,
|
|
||||||
}).Post(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.ErrorCode != 0 {
|
|
||||||
return errors.New(e.Error)
|
|
||||||
}
|
|
||||||
data := res.Body()
|
|
||||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
|
||||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPakShare) refreshToken() error {
|
|
||||||
url := "https://user.mypikpak.com/v1/auth/token"
|
|
||||||
var e RespErr
|
|
||||||
res, err := base.RestyClient.R().SetError(&e).
|
|
||||||
SetHeader("user-agent", "").SetBody(base.Json{
|
|
||||||
"client_id": "YNxT9w7GMdWvEOKa",
|
|
||||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
|
||||||
"grant_type": "refresh_token",
|
|
||||||
"refresh_token": d.RefreshToken,
|
|
||||||
}).Post(url)
|
|
||||||
if err != nil {
|
|
||||||
d.Status = err.Error()
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.ErrorCode != 0 {
|
|
||||||
if e.ErrorCode == 4126 {
|
|
||||||
// refresh_token invalid, re-login
|
|
||||||
return d.login()
|
|
||||||
}
|
|
||||||
d.Status = e.Error
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
return errors.New(e.Error)
|
|
||||||
}
|
|
||||||
data := res.Body()
|
|
||||||
d.Status = "work"
|
|
||||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
|
||||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
req.SetResult(resp)
|
|
||||||
}
|
|
||||||
var e RespErr
|
|
||||||
req.SetError(&e)
|
|
||||||
res, err := req.Execute(method, url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if e.ErrorCode != 0 {
|
|
||||||
if e.ErrorCode == 16 {
|
|
||||||
// login / refresh token
|
|
||||||
err = d.refreshToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.request(url, method, callback, resp)
|
|
||||||
}
|
|
||||||
return nil, errors.New(e.Error)
|
|
||||||
}
|
|
||||||
return res.Body(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPakShare) getSharePassToken() error {
|
|
||||||
query := map[string]string{
|
|
||||||
"share_id": d.ShareId,
|
|
||||||
"pass_code": d.SharePwd,
|
|
||||||
"thumbnail_size": "SIZE_LARGE",
|
|
||||||
"limit": "100",
|
|
||||||
}
|
|
||||||
var resp ShareResp
|
|
||||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/share", http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(query)
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.PassCodeToken = resp.PassCodeToken
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PikPakShare) getFiles(id string) ([]File, error) {
|
|
||||||
res := make([]File, 0)
|
|
||||||
pageToken := "first"
|
|
||||||
for pageToken != "" {
|
|
||||||
if pageToken == "first" {
|
|
||||||
pageToken = ""
|
|
||||||
}
|
|
||||||
query := map[string]string{
|
|
||||||
"parent_id": id,
|
|
||||||
"share_id": d.ShareId,
|
|
||||||
"thumbnail_size": "SIZE_LARGE",
|
|
||||||
"with_audit": "true",
|
|
||||||
"limit": "100",
|
|
||||||
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
|
|
||||||
"page_token": pageToken,
|
|
||||||
"pass_code_token": d.PassCodeToken,
|
|
||||||
}
|
|
||||||
var resp ShareResp
|
|
||||||
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/share/detail", http.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(query)
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.ShareStatus != "OK" {
|
|
||||||
if resp.ShareStatus == "PASS_CODE_EMPTY" || resp.ShareStatus == "PASS_CODE_ERROR" {
|
|
||||||
err = d.getSharePassToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.getFiles(id)
|
|
||||||
}
|
|
||||||
return nil, errors.New(resp.ShareStatusText)
|
|
||||||
}
|
|
||||||
pageToken = resp.NextPageToken
|
|
||||||
res = append(res, resp.Files...)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
package s3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
stdpath "path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
|
||||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type S3 struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
Session *session.Session
|
|
||||||
client *s3.S3
|
|
||||||
linkClient *s3.S3
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) Init(ctx context.Context) error {
|
|
||||||
if d.Region == "" {
|
|
||||||
d.Region = "alist"
|
|
||||||
}
|
|
||||||
err := d.initSession()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.client = d.getClient(false)
|
|
||||||
d.linkClient = d.getClient(true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
if d.ListObjectVersion == "v2" {
|
|
||||||
return d.listV2(dir.GetPath(), args)
|
|
||||||
}
|
|
||||||
return d.listV1(dir.GetPath(), args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
path := getKey(file.GetPath(), false)
|
|
||||||
filename := stdpath.Base(path)
|
|
||||||
disposition := fmt.Sprintf(`attachment; filename*=UTF-8''%s`, url.PathEscape(filename))
|
|
||||||
if d.AddFilenameToDisposition {
|
|
||||||
disposition = fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, filename, url.PathEscape(filename))
|
|
||||||
}
|
|
||||||
input := &s3.GetObjectInput{
|
|
||||||
Bucket: &d.Bucket,
|
|
||||||
Key: &path,
|
|
||||||
//ResponseContentDisposition: &disposition,
|
|
||||||
}
|
|
||||||
if d.CustomHost == "" {
|
|
||||||
input.ResponseContentDisposition = &disposition
|
|
||||||
}
|
|
||||||
req, _ := d.linkClient.GetObjectRequest(input)
|
|
||||||
var link string
|
|
||||||
var err error
|
|
||||||
if d.CustomHost != "" {
|
|
||||||
err = req.Build()
|
|
||||||
link = req.HTTPRequest.URL.String()
|
|
||||||
if d.RemoveBucket {
|
|
||||||
link = strings.Replace(link, "/"+d.Bucket, "", 1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
link, err = req.Presign(time.Hour * time.Duration(d.SignURLExpire))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.Link{
|
|
||||||
URL: link,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
return d.Put(ctx, &model.Object{
|
|
||||||
Path: stdpath.Join(parentDir.GetPath(), dirName),
|
|
||||||
}, &model.FileStream{
|
|
||||||
Obj: &model.Object{
|
|
||||||
Name: getPlaceholderName(d.Placeholder),
|
|
||||||
Modified: time.Now(),
|
|
||||||
},
|
|
||||||
ReadCloser: io.NopCloser(bytes.NewReader([]byte{})),
|
|
||||||
Mimetype: "application/octet-stream",
|
|
||||||
}, func(int) {})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
err := d.Copy(ctx, srcObj, dstDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.Remove(ctx, srcObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
err := d.copy(ctx, srcObj.GetPath(), stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName), srcObj.IsDir())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.Remove(ctx, srcObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
return d.copy(ctx, srcObj.GetPath(), stdpath.Join(dstDir.GetPath(), srcObj.GetName()), srcObj.IsDir())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
if obj.IsDir() {
|
|
||||||
return d.removeDir(ctx, obj.GetPath())
|
|
||||||
}
|
|
||||||
return d.removeFile(obj.GetPath())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *S3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
uploader := s3manager.NewUploader(d.Session)
|
|
||||||
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
|
||||||
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
|
|
||||||
}
|
|
||||||
key := getKey(stdpath.Join(dstDir.GetPath(), stream.GetName()), false)
|
|
||||||
contentType := stream.GetMimetype()
|
|
||||||
log.Debugln("key:", key)
|
|
||||||
input := &s3manager.UploadInput{
|
|
||||||
Bucket: &d.Bucket,
|
|
||||||
Key: &key,
|
|
||||||
Body: stream,
|
|
||||||
ContentType: &contentType,
|
|
||||||
}
|
|
||||||
_, err := uploader.UploadWithContext(ctx, input)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*S3)(nil)
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package seafile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Seafile struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
|
|
||||||
authorization string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) Init(ctx context.Context) error {
|
|
||||||
d.Address = strings.TrimSuffix(d.Address, "/")
|
|
||||||
return d.getToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
path := dir.GetPath()
|
|
||||||
var resp []RepoDirItemResp
|
|
||||||
_, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
|
|
||||||
req.SetResult(&resp).SetQueryParams(map[string]string{
|
|
||||||
"p": path,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return utils.SliceConvert(resp, func(f RepoDirItemResp) (model.Obj, error) {
|
|
||||||
return &model.ObjThumb{
|
|
||||||
Object: model.Object{
|
|
||||||
Name: f.Name,
|
|
||||||
Modified: time.Unix(f.Modified, 0),
|
|
||||||
Size: f.Size,
|
|
||||||
IsFolder: f.Type == "dir",
|
|
||||||
},
|
|
||||||
// Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
|
|
||||||
}, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"p": file.GetPath(),
|
|
||||||
"reuse": "1",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
u := string(res)
|
|
||||||
u = u[1 : len(u)-1] // remove quotes
|
|
||||||
return &model.Link{URL: u}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"p": filepath.Join(parentDir.GetPath(), dirName),
|
|
||||||
}).SetFormData(map[string]string{
|
|
||||||
"operation": "mkdir",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"p": srcObj.GetPath(),
|
|
||||||
}).SetFormData(map[string]string{
|
|
||||||
"operation": "move",
|
|
||||||
"dst_repo": d.Addition.RepoId,
|
|
||||||
"dst_dir": dstDir.GetPath(),
|
|
||||||
})
|
|
||||||
}, true)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"p": srcObj.GetPath(),
|
|
||||||
}).SetFormData(map[string]string{
|
|
||||||
"operation": "rename",
|
|
||||||
"newname": newName,
|
|
||||||
})
|
|
||||||
}, true)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"p": srcObj.GetPath(),
|
|
||||||
}).SetFormData(map[string]string{
|
|
||||||
"operation": "copy",
|
|
||||||
"dst_repo": d.Addition.RepoId,
|
|
||||||
"dst_dir": dstDir.GetPath(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
_, err := d.request(http.MethodDelete, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"p": obj.GetPath(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/upload-link/", d.Addition.RepoId), func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"p": dstDir.GetPath(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u := string(res)
|
|
||||||
u = u[1 : len(u)-1] // remove quotes
|
|
||||||
_, err = d.request(http.MethodPost, u, func(req *resty.Request) {
|
|
||||||
req.SetFileReader("file", stream.GetName(), stream).
|
|
||||||
SetFormData(map[string]string{
|
|
||||||
"parent_dir": dstDir.GetPath(),
|
|
||||||
"replace": "1",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Seafile)(nil)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package seafile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
driver.RootPath
|
|
||||||
|
|
||||||
Address string `json:"address" required:"true"`
|
|
||||||
UserName string `json:"username" required:"true"`
|
|
||||||
Password string `json:"password" required:"true"`
|
|
||||||
RepoId string `json:"repoId" required:"true"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "Seafile",
|
|
||||||
DefaultRoot: "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Seafile{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package seafile
|
|
||||||
|
|
||||||
type AuthTokenResp struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RepoDirItemResp struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Type string `json:"type"` // dir, file
|
|
||||||
Name string `json:"name"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Modified int64 `json:"mtime"`
|
|
||||||
Permission string `json:"permission"`
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package seafile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Seafile) getToken() error {
|
|
||||||
var authResp AuthTokenResp
|
|
||||||
res, err := base.RestyClient.R().
|
|
||||||
SetResult(&authResp).
|
|
||||||
SetFormData(map[string]string{
|
|
||||||
"username": d.UserName,
|
|
||||||
"password": d.Password,
|
|
||||||
}).
|
|
||||||
Post(d.Address + "/api2/auth-token/")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if res.StatusCode() >= 400 {
|
|
||||||
return fmt.Errorf("get token failed: %s", res.String())
|
|
||||||
}
|
|
||||||
d.authorization = fmt.Sprintf("Token %s", authResp.Token)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Seafile) request(method string, pathname string, callback base.ReqCallback, noRedirect ...bool) ([]byte, error) {
|
|
||||||
full := pathname
|
|
||||||
if !strings.HasPrefix(pathname, "http") {
|
|
||||||
full = d.Address + pathname
|
|
||||||
}
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
if len(noRedirect) > 0 && noRedirect[0] {
|
|
||||||
req = base.NoRedirectClient.R()
|
|
||||||
}
|
|
||||||
req.SetHeader("Authorization", d.authorization)
|
|
||||||
callback(req)
|
|
||||||
var (
|
|
||||||
res *resty.Response
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
res, err = req.Execute(method, full)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if res.StatusCode() != 401 { // Unauthorized
|
|
||||||
break
|
|
||||||
}
|
|
||||||
err = d.getToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if res.StatusCode() >= 400 {
|
|
||||||
return nil, fmt.Errorf("request failed: %s", res.String())
|
|
||||||
}
|
|
||||||
return res.Body(), nil
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package sftp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
Address string `json:"address" required:"true"`
|
|
||||||
Username string `json:"username" required:"true"`
|
|
||||||
PrivateKey string `json:"private_key" type:"text"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
driver.RootPath
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "SFTP",
|
|
||||||
LocalSort: true,
|
|
||||||
OnlyLocal: true,
|
|
||||||
DefaultRoot: "/",
|
|
||||||
CheckStatus: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &SFTP{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package template
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Template struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) Init(ctx context.Context) error {
|
|
||||||
// TODO login / refresh token
|
|
||||||
//op.MustSaveDriverStorage(d)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
// TODO return the files list, required
|
|
||||||
return nil, errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
// TODO return link of file, required
|
|
||||||
return nil, errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
// TODO create folder, optional
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
// TODO move obj, optional
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
// TODO rename obj, optional
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
// TODO copy obj, optional
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
// TODO remove obj, optional
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Template) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
// TODO upload file, optional
|
|
||||||
return errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
|
||||||
// return nil, errs.NotSupport
|
|
||||||
//}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Template)(nil)
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
package terbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
stdpath "path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Terabox struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) Init(ctx context.Context) error {
|
|
||||||
var resp CheckLoginResp
|
|
||||||
_, err := d.get("/api/check/login", nil, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp.Errno != 0 {
|
|
||||||
if resp.Errno == 9000 {
|
|
||||||
return fmt.Errorf("terabox is not yet available in this area")
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to check login status according to cookie")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
files, err := d.getFiles(dir.GetPath())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
|
||||||
return fileToObj(src), nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
if d.DownloadAPI == "crack" {
|
|
||||||
return d.linkCrack(file, args)
|
|
||||||
}
|
|
||||||
return d.linkOfficial(file, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
_, err := d.create(stdpath.Join(parentDir.GetPath(), dirName), 0, 1, "", "")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
data := []base.Json{
|
|
||||||
{
|
|
||||||
"path": srcObj.GetPath(),
|
|
||||||
"dest": dstDir.GetPath(),
|
|
||||||
"newname": srcObj.GetName(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err := d.manage("move", data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
data := []base.Json{
|
|
||||||
{
|
|
||||||
"path": srcObj.GetPath(),
|
|
||||||
"newname": newName,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err := d.manage("rename", data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
data := []base.Json{
|
|
||||||
{
|
|
||||||
"path": srcObj.GetPath(),
|
|
||||||
"dest": dstDir.GetPath(),
|
|
||||||
"newname": srcObj.GetName(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err := d.manage("copy", data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
data := []string{obj.GetPath()}
|
|
||||||
_, err := d.manage("delete", data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Terabox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
tempFile, err := utils.CreateTempFile(stream.GetReadCloser())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tempFile.Close()
|
|
||||||
_ = os.Remove(tempFile.Name())
|
|
||||||
}()
|
|
||||||
var Default int64 = 4 * 1024 * 1024
|
|
||||||
defaultByteData := make([]byte, Default)
|
|
||||||
count := int(math.Ceil(float64(stream.GetSize()) / float64(Default)))
|
|
||||||
// cal md5
|
|
||||||
h1 := md5.New()
|
|
||||||
h2 := md5.New()
|
|
||||||
block_list := make([]string, 0)
|
|
||||||
left := stream.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()
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tempFile.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rawPath := stdpath.Join(dstDir.GetPath(), stream.GetName())
|
|
||||||
path := encodeURIComponent(rawPath)
|
|
||||||
block_list_str := fmt.Sprintf("[%s]", strings.Join(block_list, ","))
|
|
||||||
data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&block_list=%s",
|
|
||||||
path, stream.GetSize(),
|
|
||||||
block_list_str)
|
|
||||||
params := map[string]string{}
|
|
||||||
var precreateResp PrecreateResp
|
|
||||||
_, err = d.post("/api/precreate", params, data, &precreateResp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Debugf("%+v", precreateResp)
|
|
||||||
if precreateResp.ReturnType == 2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
params = map[string]string{
|
|
||||||
"method": "upload",
|
|
||||||
"path": path,
|
|
||||||
"uploadid": precreateResp.Uploadid,
|
|
||||||
"app_id": "250528",
|
|
||||||
"web": "1",
|
|
||||||
"channel": "dubox",
|
|
||||||
"clienttype": "0",
|
|
||||||
}
|
|
||||||
left = stream.GetSize()
|
|
||||||
for i, partseq := range precreateResp.BlockList {
|
|
||||||
if utils.IsCanceled(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
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://c-jp.terabox.com/rest/2.0/pcs/superfile2"
|
|
||||||
params["partseq"] = strconv.Itoa(partseq)
|
|
||||||
res, err := base.RestyClient.R().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetQueryParams(params).
|
|
||||||
SetFileReader("file", stream.GetName(), bytes.NewReader(byteData)).
|
|
||||||
SetHeader("Cookie", d.Cookie).
|
|
||||||
Post(u)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Debugln(res.String())
|
|
||||||
if len(precreateResp.BlockList) > 0 {
|
|
||||||
up(i * 100 / len(precreateResp.BlockList))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, err = d.create(rawPath, stream.GetSize(), 0, precreateResp.Uploadid, block_list_str)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Terabox)(nil)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user