mirror of
https://github.com/HChaZZY/alist.git
synced 2025-12-06 11:03:49 +08:00
Compare commits
582 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
44
.air.toml
Normal file
44
.air.toml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = ["server"]
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ."
|
||||||
|
delay = 0
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -3,7 +3,7 @@
|
|||||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
patreon: # Replace with a single Patreon username
|
patreon: # Replace with a single Patreon username
|
||||||
open_collective: # Replace with a single Open Collective username
|
open_collective: # Replace with a single Open Collective username
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
ko_fi: xhofe # Replace with a single Ko-fi username
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
liberapay: # Replace with a single Liberapay username
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -22,8 +22,8 @@ body:
|
|||||||
I'm sure there are no duplicate issues or discussions.
|
I'm sure there are no duplicate issues or discussions.
|
||||||
我确定没有重复的issue或讨论。
|
我确定没有重复的issue或讨论。
|
||||||
- label: |
|
- label: |
|
||||||
I'm sure it's due to `AList` and not something else(such as `Dependencies` or `Operational`).
|
I'm sure it's due to `AList` and not something else(such as [Network](https://alist.nn.ci/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host) ,`Dependencies` or `Operational`).
|
||||||
我确定是`AList`的问题,而不是其他原因(例如`依赖`或`操作`)。
|
我确定是`AList`的问题,而不是其他原因(例如[网络](https://alist.nn.ci/zh/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host),`依赖`或`操作`)。
|
||||||
- label: |
|
- label: |
|
||||||
I'm sure this issue is not fixed in the latest version.
|
I'm sure this issue is not fixed in the latest version.
|
||||||
我确定这个问题在最新版本中没有被修复。
|
我确定这个问题在最新版本中没有被修复。
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Questions & Discussions
|
- name: Questions & Discussions
|
||||||
url: https://github.com/Xhofe/alist/discussions
|
url: https://github.com/alist-org/alist/discussions
|
||||||
about: Use GitHub discussions for message-board style questions and discussions.
|
about: Use GitHub discussions for message-board style questions and discussions.
|
||||||
2
.github/stale.yml
vendored
2
.github/stale.yml
vendored
@@ -6,6 +6,8 @@ daysUntilClose: 20
|
|||||||
exemptLabels:
|
exemptLabels:
|
||||||
- accepted
|
- accepted
|
||||||
- security
|
- security
|
||||||
|
- working
|
||||||
|
- pr-welcome
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: stale
|
staleLabel: stale
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
|||||||
12
.github/workflows/auto_lang.yml
vendored
12
.github/workflows/auto_lang.yml
vendored
@@ -11,27 +11,31 @@ on:
|
|||||||
- 'cmd/lang.go'
|
- 'cmd/lang.go'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto_lang:
|
auto_lang:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform: [ ubuntu-latest ]
|
platform: [ ubuntu-latest ]
|
||||||
go-version: [ '1.20' ]
|
go-version: [ '1.21' ]
|
||||||
name: auto generate lang.json
|
name: auto generate lang.json
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
- name: Checkout alist
|
- name: Checkout alist
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: alist
|
path: alist
|
||||||
|
|
||||||
- name: Checkout alist-web
|
- name: Checkout alist-web
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: 'alist-org/alist-web'
|
repository: 'alist-org/alist-web'
|
||||||
ref: main
|
ref: main
|
||||||
|
|||||||
138
.github/workflows/beta_release.yml
vendored
Normal file
138
.github/workflows/beta_release.yml
vendored
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
name: beta release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ 'main' ]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changelog:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [ ubuntu-latest ]
|
||||||
|
go-version: [ '1.21' ]
|
||||||
|
name: Beta Release Changelog
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Create or update ref
|
||||||
|
id: create-or-update-ref
|
||||||
|
uses: ovsds/create-or-update-ref-action@v1
|
||||||
|
with:
|
||||||
|
ref: tags/beta
|
||||||
|
sha: ${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Delete beta tag
|
||||||
|
run: git tag -d beta
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: changelog # or changelogithub@0.12 if ensure the stable result
|
||||||
|
id: changelog
|
||||||
|
run: |
|
||||||
|
git tag -l
|
||||||
|
npx changelogithub --output CHANGELOG.md
|
||||||
|
# npx changelogen@latest --output CHANGELOG.md
|
||||||
|
|
||||||
|
- name: Upload assets
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
body_path: CHANGELOG.md
|
||||||
|
files: CHANGELOG.md
|
||||||
|
prerelease: true
|
||||||
|
tag_name: beta
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs:
|
||||||
|
- changelog
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: '!(*musl*|*windows-arm64*|*android*|*freebsd*)' # xgo
|
||||||
|
hash: "md5"
|
||||||
|
- target: 'linux-!(arm*)-musl*' #musl-not-arm
|
||||||
|
hash: "md5-linux-musl"
|
||||||
|
- target: 'linux-arm*-musl*' #musl-arm
|
||||||
|
hash: "md5-linux-musl-arm"
|
||||||
|
- target: 'windows-arm64' #win-arm64
|
||||||
|
hash: "md5-windows-arm64"
|
||||||
|
- target: 'android-*' #android
|
||||||
|
hash: "md5-android"
|
||||||
|
- target: 'freebsd-*' #freebsd
|
||||||
|
hash: "md5-freebsd"
|
||||||
|
|
||||||
|
name: Beta Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
|
||||||
|
- name: Setup web
|
||||||
|
run: bash build.sh dev web
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: go-cross/cgo-actions@v1
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
musl-target-format: $os-$musl-$arch
|
||||||
|
out-dir: build
|
||||||
|
x-flags: |
|
||||||
|
github.com/alist-org/alist/v3/internal/conf.BuiltAt=$built_at
|
||||||
|
github.com/alist-org/alist/v3/internal/conf.GitAuthor=Xhofe
|
||||||
|
github.com/alist-org/alist/v3/internal/conf.GitCommit=$git_commit
|
||||||
|
github.com/alist-org/alist/v3/internal/conf.Version=$tag
|
||||||
|
github.com/alist-org/alist/v3/internal/conf.WebVersion=dev
|
||||||
|
|
||||||
|
- name: Compress
|
||||||
|
run: |
|
||||||
|
bash build.sh zip ${{ matrix.hash }}
|
||||||
|
|
||||||
|
- name: Upload assets
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: build/compress/*
|
||||||
|
prerelease: true
|
||||||
|
tag_name: beta
|
||||||
|
|
||||||
|
desktop:
|
||||||
|
needs:
|
||||||
|
- release
|
||||||
|
name: Beta Release Desktop
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: alist-org/desktop-release
|
||||||
|
ref: main
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Commit
|
||||||
|
run: |
|
||||||
|
git config --local user.email "bot@nn.ci"
|
||||||
|
git config --local user.name "IlaBot"
|
||||||
|
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/desktop-release
|
||||||
54
.github/workflows/build.yml
vendored
54
.github/workflows/build.yml
vendored
@@ -6,36 +6,56 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ 'main' ]
|
branches: [ 'main' ]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform: [ubuntu-latest]
|
platform: [ubuntu-latest]
|
||||||
go-version: [ '1.20' ]
|
target:
|
||||||
|
- darwin-amd64
|
||||||
|
- darwin-arm64
|
||||||
|
- windows-amd64
|
||||||
|
- linux-arm64-musl
|
||||||
|
- linux-amd64-musl
|
||||||
|
- windows-arm64
|
||||||
|
- android-arm64
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go-version }}
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install dependencies
|
- uses: benjlevesque/short-sha@v3.0
|
||||||
run: |
|
id: short-sha
|
||||||
sudo snap install zig --classic --beta
|
|
||||||
docker pull crazymax/xgo:latest
|
- name: Setup Go
|
||||||
go install github.com/crazy-max/xgo@latest
|
uses: actions/setup-go@v5
|
||||||
sudo apt install upx
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
|
||||||
|
- name: Setup web
|
||||||
|
run: bash build.sh dev web
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
uses: go-cross/cgo-actions@v1
|
||||||
bash build.sh dev
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
musl-target-format: $os-$musl-$arch
|
||||||
|
out-dir: build
|
||||||
|
x-flags: |
|
||||||
|
github.com/alist-org/alist/v3/internal/conf.BuiltAt=$built_at
|
||||||
|
github.com/alist-org/alist/v3/internal/conf.GitAuthor=Xhofe
|
||||||
|
github.com/alist-org/alist/v3/internal/conf.GitCommit=$git_commit
|
||||||
|
github.com/alist-org/alist/v3/internal/conf.Version=$tag
|
||||||
|
github.com/alist-org/alist/v3/internal/conf.WebVersion=dev
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: alist
|
name: alist_${{ env.SHA }}_${{ matrix.target }}
|
||||||
path: dist
|
path: build/*
|
||||||
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 "bot@nn.ci"
|
|
||||||
git config --local user.name "IlaBot"
|
|
||||||
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
|
|
||||||
9
.github/workflows/changelog.yml
vendored
9
.github/workflows/changelog.yml
vendored
@@ -3,7 +3,7 @@ name: auto changelog
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
changelog:
|
changelog:
|
||||||
@@ -11,9 +11,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Delete beta tag
|
||||||
|
run: git tag -d beta
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{secrets.MY_TOKEN}}
|
GITHUB_TOKEN: ${{secrets.MY_TOKEN}}
|
||||||
|
|||||||
@@ -14,4 +14,4 @@ jobs:
|
|||||||
actions: 'remove-labels'
|
actions: 'remove-labels'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
issue-number: ${{ github.event.issue.number }}
|
issue-number: ${{ github.event.issue.number }}
|
||||||
labels: 'working'
|
labels: 'working,pr-welcome'
|
||||||
2
.github/workflows/issue_question.yml
vendored
2
.github/workflows/issue_question.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
if: github.event.label.name == 'question'
|
if: github.event.label.name == 'question'
|
||||||
steps:
|
steps:
|
||||||
- name: Create comment
|
- name: Create comment
|
||||||
uses: actions-cool/issues-helper@v3.5.1
|
uses: actions-cool/issues-helper@v3.6.0
|
||||||
with:
|
with:
|
||||||
actions: 'create-comment'
|
actions: 'create-comment'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
27
.github/workflows/release.yml
vendored
27
.github/workflows/release.yml
vendored
@@ -9,10 +9,27 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform: [ ubuntu-latest ]
|
platform: [ ubuntu-latest ]
|
||||||
go-version: [ '1.20' ]
|
go-version: [ '1.21' ]
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
|
- name: Free Disk Space (Ubuntu)
|
||||||
|
uses: jlumbroso/free-disk-space@main
|
||||||
|
with:
|
||||||
|
# this might remove tools that are actually needed,
|
||||||
|
# if set to "true" but frees about 6 GB
|
||||||
|
tool-cache: false
|
||||||
|
|
||||||
|
# all of these default to true, but feel free to set to
|
||||||
|
# "false" if necessary for your workflow
|
||||||
|
android: true
|
||||||
|
dotnet: true
|
||||||
|
haskell: true
|
||||||
|
large-packages: true
|
||||||
|
docker-images: true
|
||||||
|
swap-storage: true
|
||||||
|
|
||||||
- name: Prerelease
|
- name: Prerelease
|
||||||
uses: irongut/EditRelease@v1.2.0
|
uses: irongut/EditRelease@v1.2.0
|
||||||
with:
|
with:
|
||||||
@@ -21,12 +38,12 @@ jobs:
|
|||||||
prerelease: true
|
prerelease: true
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -42,7 +59,7 @@ jobs:
|
|||||||
bash build.sh release
|
bash build.sh release
|
||||||
|
|
||||||
- name: Upload assets
|
- name: Upload assets
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: build/compress/*
|
files: build/compress/*
|
||||||
prerelease: false
|
prerelease: false
|
||||||
@@ -53,7 +70,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: alist-org/desktop-release
|
repository: alist-org/desktop-release
|
||||||
ref: main
|
ref: main
|
||||||
|
|||||||
34
.github/workflows/release_android.yml
vendored
Normal file
34
.github/workflows/release_android.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: release_android
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release_android:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [ ubuntu-latest ]
|
||||||
|
go-version: [ '1.21' ]
|
||||||
|
name: Release
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
bash build.sh release android
|
||||||
|
|
||||||
|
- name: Upload assets
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: build/compress/*
|
||||||
163
.github/workflows/release_docker.yml
vendored
163
.github/workflows/release_docker.yml
vendored
@@ -3,66 +3,135 @@ name: release_docker
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- 'v*'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: 'xhofe/alist'
|
||||||
|
REGISTRY_USERNAME: 'xhofe'
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
ARTIFACT_NAME: 'binaries_docker_release'
|
||||||
|
RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64'
|
||||||
|
IMAGE_PUSH: ${{ github.event_name == 'push' }}
|
||||||
|
IMAGE_IS_PROD: ${{ github.ref_type == 'tag' }}
|
||||||
|
IMAGE_TAGS_BETA: |
|
||||||
|
type=schedule
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=ref,event=pr
|
||||||
|
type=raw,value=beta,enable={{is_default_branch}}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release_docker:
|
build_binary:
|
||||||
name: Release Docker
|
name: Build Binaries for Docker Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
|
||||||
|
- name: Cache Musl
|
||||||
|
id: cache-musl
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: build/musl-libs
|
||||||
|
key: docker-musl-libs-v2
|
||||||
|
|
||||||
|
- name: Download Musl Library
|
||||||
|
if: steps.cache-musl.outputs.cache-hit != 'true'
|
||||||
|
run: bash build.sh prepare docker-multiplatform
|
||||||
|
|
||||||
|
- name: Build go binary (beta)
|
||||||
|
if: env.IMAGE_IS_PROD != 'true'
|
||||||
|
run: bash build.sh beta docker-multiplatform
|
||||||
|
|
||||||
|
- name: Build go binary (release)
|
||||||
|
if: env.IMAGE_IS_PROD == 'true'
|
||||||
|
run: bash build.sh release docker-multiplatform
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ env.ARTIFACT_NAME }}
|
||||||
|
overwrite: true
|
||||||
|
path: |
|
||||||
|
build/
|
||||||
|
!build/*.tgz
|
||||||
|
!build/musl-libs/**
|
||||||
|
|
||||||
|
release_docker:
|
||||||
|
needs: build_binary
|
||||||
|
name: Release Docker image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
image: ["latest", "ffmpeg", "aria2", "aio"]
|
||||||
|
include:
|
||||||
|
- image: "latest"
|
||||||
|
build_arg: ""
|
||||||
|
tag_favor: ""
|
||||||
|
- image: "ffmpeg"
|
||||||
|
build_arg: INSTALL_FFMPEG=true
|
||||||
|
tag_favor: "suffix=-ffmpeg,onlatest=true"
|
||||||
|
- image: "aria2"
|
||||||
|
build_arg: INSTALL_ARIA2=true
|
||||||
|
tag_favor: "suffix=-aria2,onlatest=true"
|
||||||
|
- image: "aio"
|
||||||
|
build_arg: |
|
||||||
|
INSTALL_FFMPEG=true
|
||||||
|
INSTALL_ARIA2=true
|
||||||
|
tag_favor: "suffix=-aio,onlatest=true"
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ env.ARTIFACT_NAME }}
|
||||||
|
path: 'build/'
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: env.IMAGE_PUSH == 'true'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
logout: true
|
||||||
|
username: ${{ env.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ env.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: xhofe/alist
|
images: ${{ env.REGISTRY }}
|
||||||
|
tags: ${{ env.IMAGE_IS_PROD == 'true' && '' || env.IMAGE_TAGS_BETA }}
|
||||||
- name: Set up QEMU
|
flavor: |
|
||||||
uses: docker/setup-qemu-action@v2
|
${{ env.IMAGE_IS_PROD == 'true' && 'latest=true' || '' }}
|
||||||
|
${{ matrix.tag_favor }}
|
||||||
- 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
|
- name: Build and push
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
file: Dockerfile.ci
|
||||||
|
push: ${{ env.IMAGE_PUSH == 'true' }}
|
||||||
|
build-args: ${{ matrix.build_arg }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
platforms: ${{ env.RELEASE_PLATFORMS }}
|
||||||
|
|
||||||
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 "bot@nn.ci"
|
|
||||||
git config --local user.name "IlaBot"
|
|
||||||
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
|
|
||||||
34
.github/workflows/release_freebsd.yml
vendored
Normal file
34
.github/workflows/release_freebsd.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: release_freebsd
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release_freebsd:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [ ubuntu-latest ]
|
||||||
|
go-version: [ '1.21' ]
|
||||||
|
name: Release
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
bash build.sh release freebsd
|
||||||
|
|
||||||
|
- name: Upload assets
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: build/compress/*
|
||||||
34
.github/workflows/release_linux_musl.yml
vendored
Normal file
34
.github/workflows/release_linux_musl.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: release_linux_musl
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release_linux_musl:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [ ubuntu-latest ]
|
||||||
|
go-version: [ '1.21' ]
|
||||||
|
name: Release
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
bash build.sh release linux_musl
|
||||||
|
|
||||||
|
- name: Upload assets
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: build/compress/*
|
||||||
10
.github/workflows/release_linux_musl_arm.yml
vendored
10
.github/workflows/release_linux_musl_arm.yml
vendored
@@ -5,22 +5,22 @@ on:
|
|||||||
types: [ published ]
|
types: [ published ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release_arm:
|
release_linux_musl_arm:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform: [ ubuntu-latest ]
|
platform: [ ubuntu-latest ]
|
||||||
go-version: [ '1.20' ]
|
go-version: [ '1.21' ]
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -29,6 +29,6 @@ jobs:
|
|||||||
bash build.sh release linux_musl_arm
|
bash build.sh release linux_musl_arm
|
||||||
|
|
||||||
- name: Upload assets
|
- name: Upload assets
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: build/compress/*
|
files: build/compress/*
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,6 +24,7 @@ output/
|
|||||||
*.json
|
*.json
|
||||||
/build
|
/build
|
||||||
/data/
|
/data/
|
||||||
|
/tmp/
|
||||||
/log/
|
/log/
|
||||||
/lang/
|
/lang/
|
||||||
/daemon/
|
/daemon/
|
||||||
|
|||||||
43
Dockerfile
43
Dockerfile
@@ -1,18 +1,43 @@
|
|||||||
FROM alpine:3.18 as builder
|
FROM alpine:edge as builder
|
||||||
LABEL stage=go-builder
|
LABEL stage=go-builder
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
RUN apk add --no-cache bash curl gcc git go musl-dev
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
RUN apk add --no-cache bash curl gcc git go musl-dev; \
|
RUN bash build.sh release docker
|
||||||
bash build.sh release docker
|
|
||||||
|
|
||||||
FROM alpine:3.18
|
FROM alpine:edge
|
||||||
|
|
||||||
|
ARG INSTALL_FFMPEG=false
|
||||||
|
ARG INSTALL_ARIA2=false
|
||||||
LABEL MAINTAINER="i@nn.ci"
|
LABEL MAINTAINER="i@nn.ci"
|
||||||
VOLUME /opt/alist/data/
|
|
||||||
WORKDIR /opt/alist/
|
WORKDIR /opt/alist/
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk upgrade --no-cache && \
|
||||||
|
apk add --no-cache bash ca-certificates su-exec tzdata; \
|
||||||
|
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \
|
||||||
|
[ "$INSTALL_ARIA2" = "true" ] && apk add --no-cache curl aria2 && \
|
||||||
|
mkdir -p /opt/aria2/.aria2 && \
|
||||||
|
wget https://github.com/P3TERX/aria2.conf/archive/refs/heads/master.tar.gz -O /tmp/aria-conf.tar.gz && \
|
||||||
|
tar -zxvf /tmp/aria-conf.tar.gz -C /opt/aria2/.aria2 --strip-components=1 && rm -f /tmp/aria-conf.tar.gz && \
|
||||||
|
sed -i 's|rpc-secret|#rpc-secret|g' /opt/aria2/.aria2/aria2.conf && \
|
||||||
|
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/aria2.conf && \
|
||||||
|
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \
|
||||||
|
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \
|
||||||
|
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \
|
||||||
|
touch /opt/aria2/.aria2/aria2.session && \
|
||||||
|
/opt/aria2/.aria2/tracker.sh ; \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
COPY --from=builder /app/bin/alist ./
|
COPY --from=builder /app/bin/alist ./
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN apk add --no-cache bash ca-certificates su-exec tzdata; \
|
RUN chmod +x /opt/alist/alist && \
|
||||||
chmod +x /entrypoint.sh
|
chmod +x /entrypoint.sh && /entrypoint.sh version
|
||||||
ENV PUID=0 PGID=0 UMASK=022
|
|
||||||
|
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
|
||||||
|
VOLUME /opt/alist/data/
|
||||||
EXPOSE 5244 5245
|
EXPOSE 5244 5245
|
||||||
CMD [ "/entrypoint.sh" ]
|
CMD [ "/entrypoint.sh" ]
|
||||||
35
Dockerfile.ci
Normal file
35
Dockerfile.ci
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
FROM alpine:edge
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG INSTALL_FFMPEG=false
|
||||||
|
ARG INSTALL_ARIA2=false
|
||||||
|
LABEL MAINTAINER="i@nn.ci"
|
||||||
|
|
||||||
|
WORKDIR /opt/alist/
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk upgrade --no-cache && \
|
||||||
|
apk add --no-cache bash ca-certificates su-exec tzdata; \
|
||||||
|
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \
|
||||||
|
[ "$INSTALL_ARIA2" = "true" ] && apk add --no-cache curl aria2 && \
|
||||||
|
mkdir -p /opt/aria2/.aria2 && \
|
||||||
|
wget https://github.com/P3TERX/aria2.conf/archive/refs/heads/master.tar.gz -O /tmp/aria-conf.tar.gz && \
|
||||||
|
tar -zxvf /tmp/aria-conf.tar.gz -C /opt/aria2/.aria2 --strip-components=1 && rm -f /tmp/aria-conf.tar.gz && \
|
||||||
|
sed -i 's|rpc-secret|#rpc-secret|g' /opt/aria2/.aria2/aria2.conf && \
|
||||||
|
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/aria2.conf && \
|
||||||
|
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \
|
||||||
|
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \
|
||||||
|
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \
|
||||||
|
touch /opt/aria2/.aria2/aria2.session && \
|
||||||
|
/opt/aria2/.aria2/tracker.sh ; \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
COPY /build/${TARGETPLATFORM}/alist ./
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /opt/alist/alist && \
|
||||||
|
chmod +x /entrypoint.sh && /entrypoint.sh version
|
||||||
|
|
||||||
|
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
|
||||||
|
VOLUME /opt/alist/data/
|
||||||
|
EXPOSE 5244 5245
|
||||||
|
CMD [ "/entrypoint.sh" ]
|
||||||
43
README.md
Executable file → Normal file
43
README.md
Executable file → Normal file
@@ -1,17 +1,17 @@
|
|||||||
<div align="center">
|
<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>
|
<a href="https://alist.nn.ci"><img width="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>
|
<p><em>🗂️A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
<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" />
|
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
|
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
|
||||||
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
|
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
|
||||||
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/releases">
|
<a href="https://github.com/alist-org/alist/releases">
|
||||||
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
||||||
</a>
|
</a>
|
||||||
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
||||||
@@ -19,13 +19,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://github.com/Xhofe/alist/discussions">
|
<a href="https://github.com/alist-org/alist/discussions">
|
||||||
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/F4ymsH4xv2">
|
<a href="https://discord.gg/F4ymsH4xv2">
|
||||||
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/releases">
|
<a href="https://github.com/alist-org/alist/releases">
|
||||||
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://hub.docker.com/r/xhofe/alist">
|
<a href="https://hub.docker.com/r/xhofe/alist">
|
||||||
@@ -39,13 +39,13 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
|
English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] Multiple storage
|
- [x] Multiple storages
|
||||||
- [x] Local storage
|
- [x] Local storage
|
||||||
- [x] [Aliyundrive](https://www.aliyundrive.com/)
|
- [x] [Aliyundrive](https://www.alipan.com/)
|
||||||
- [x] OneDrive / Sharepoint ([global](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us)
|
- [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] [189cloud](https://cloud.189.cn) (Personal, Family)
|
||||||
- [x] [GoogleDrive](https://drive.google.com/)
|
- [x] [GoogleDrive](https://drive.google.com/)
|
||||||
@@ -58,7 +58,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
|||||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
- [x] WebDav(Support OneDrive/SharePoint without API)
|
||||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
||||||
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
||||||
- [x] [139yun](https://yun.139.com/) (Personal, Family)
|
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
|
||||||
- [x] [YandexDisk](https://disk.yandex.com/)
|
- [x] [YandexDisk](https://disk.yandex.com/)
|
||||||
- [x] [BaiduNetdisk](http://pan.baidu.com/)
|
- [x] [BaiduNetdisk](http://pan.baidu.com/)
|
||||||
- [x] [Terabox](https://www.terabox.com/main)
|
- [x] [Terabox](https://www.terabox.com/main)
|
||||||
@@ -66,7 +66,8 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
|||||||
- [x] [Quark](https://pan.quark.cn)
|
- [x] [Quark](https://pan.quark.cn)
|
||||||
- [x] [Thunder](https://pan.xunlei.com)
|
- [x] [Thunder](https://pan.xunlei.com)
|
||||||
- [x] [Lanzou](https://www.lanzou.com/)
|
- [x] [Lanzou](https://www.lanzou.com/)
|
||||||
- [x] [Aliyundrive share](https://www.aliyundrive.com/)
|
- [x] [ILanzou](https://www.ilanzou.com/)
|
||||||
|
- [x] [Aliyundrive share](https://www.alipan.com/)
|
||||||
- [x] [Google photo](https://photos.google.com/)
|
- [x] [Google photo](https://photos.google.com/)
|
||||||
- [x] [Mega.nz](https://mega.nz)
|
- [x] [Mega.nz](https://mega.nz)
|
||||||
- [x] [Baidu photo](https://photo.baidu.com/)
|
- [x] [Baidu photo](https://photo.baidu.com/)
|
||||||
@@ -74,6 +75,8 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
|||||||
- [x] [115](https://115.com/)
|
- [x] [115](https://115.com/)
|
||||||
- [X] Cloudreve
|
- [X] Cloudreve
|
||||||
- [x] [Dropbox](https://www.dropbox.com/)
|
- [x] [Dropbox](https://www.dropbox.com/)
|
||||||
|
- [x] [FeijiPan](https://www.feijipan.com/)
|
||||||
|
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
||||||
- [x] Easy to deploy and out-of-the-box
|
- [x] Easy to deploy and out-of-the-box
|
||||||
- [x] File preview (PDF, markdown, code, plain text, ...)
|
- [x] File preview (PDF, markdown, code, plain text, ...)
|
||||||
- [x] Image preview in gallery mode
|
- [x] Image preview in gallery mode
|
||||||
@@ -86,7 +89,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
|||||||
- [x] Protected routes (password protection and authentication)
|
- [x] Protected routes (password protection and authentication)
|
||||||
- [x] WebDav (see https://alist.nn.ci/guide/webdav.html for details)
|
- [x] WebDav (see https://alist.nn.ci/guide/webdav.html for details)
|
||||||
- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist)
|
- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist)
|
||||||
- [x] Cloudflare workers proxy
|
- [x] Cloudflare Workers proxy
|
||||||
- [x] File/Folder package download
|
- [x] File/Folder package download
|
||||||
- [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy
|
- [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy
|
||||||
- [x] Offline download
|
- [x] Offline download
|
||||||
@@ -95,7 +98,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
|||||||
|
|
||||||
## Document
|
## Document
|
||||||
|
|
||||||
<https://alist.nn.ci/>
|
<https://alistgo.com/>
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
@@ -103,7 +106,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
|||||||
|
|
||||||
## Discussion
|
## 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.**
|
Please go to our [discussion forum](https://github.com/alist-org/alist/discussions) for general questions, **issues are for bug reports and feature requests only.**
|
||||||
|
|
||||||
## Sponsor
|
## Sponsor
|
||||||
|
|
||||||
@@ -112,22 +115,22 @@ https://alist.nn.ci/guide/sponsor.html
|
|||||||
|
|
||||||
### Special sponsors
|
### Special sponsors
|
||||||
|
|
||||||
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (sponsored Chinese API server)
|
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
|
||||||
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.pw/)
|
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
||||||
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
|
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
Thanks goes to these wonderful people:
|
Thanks goes to these wonderful people:
|
||||||
|
|
||||||
[](https://github.com/alist-org/alist/graphs/contributors)
|
[](https://github.com/alist-org/alist/graphs/contributors)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The `AList` is open-source software licensed under the AGPL-3.0 license.
|
The `AList` is open-source software licensed under the AGPL-3.0 license.
|
||||||
|
|
||||||
## Disclaimer
|
## 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 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 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;
|
- 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;
|
- 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;
|
||||||
@@ -135,4 +138,4 @@ The `AList` is open-source software licensed under the AGPL-3.0 license.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
> [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||||
|
|||||||
33
README_cn.md
33
README_cn.md
@@ -1,17 +1,17 @@
|
|||||||
<div align="center">
|
<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>
|
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||||
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
|
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
<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" />
|
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
|
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
|
||||||
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
|
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
|
||||||
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/releases">
|
<a href="https://github.com/alist-org/alist/releases">
|
||||||
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
||||||
</a>
|
</a>
|
||||||
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
||||||
@@ -19,13 +19,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://github.com/Xhofe/alist/discussions">
|
<a href="https://github.com/alist-org/alist/discussions">
|
||||||
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/F4ymsH4xv2">
|
<a href="https://discord.gg/F4ymsH4xv2">
|
||||||
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/releases">
|
<a href="https://github.com/alist-org/alist/releases">
|
||||||
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://hub.docker.com/r/xhofe/alist">
|
<a href="https://hub.docker.com/r/xhofe/alist">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
- [x] 多种存储
|
- [x] 多种存储
|
||||||
- [x] 本地存储
|
- [x] 本地存储
|
||||||
- [x] [阿里云盘](https://www.aliyundrive.com/)
|
- [x] [阿里云盘](https://www.alipan.com/)
|
||||||
- [x] OneDrive / Sharepoint([国际版](https://www.office.com/), [世纪互联](https://portal.partner.microsoftonline.cn),de,us)
|
- [x] OneDrive / Sharepoint([国际版](https://www.office.com/), [世纪互联](https://portal.partner.microsoftonline.cn),de,us)
|
||||||
- [x] [天翼云盘](https://cloud.189.cn) (个人云, 家庭云)
|
- [x] [天翼云盘](https://cloud.189.cn) (个人云, 家庭云)
|
||||||
- [x] [GoogleDrive](https://drive.google.com/)
|
- [x] [GoogleDrive](https://drive.google.com/)
|
||||||
@@ -58,14 +58,15 @@
|
|||||||
- [x] WebDav(支持无API的OneDrive/SharePoint)
|
- [x] WebDav(支持无API的OneDrive/SharePoint)
|
||||||
- [x] Teambition([中国](https://www.teambition.com/ ),[国际](https://us.teambition.com/ ))
|
- [x] Teambition([中国](https://www.teambition.com/ ),[国际](https://us.teambition.com/ ))
|
||||||
- [x] [分秒帧](https://www.mediatrack.cn/)
|
- [x] [分秒帧](https://www.mediatrack.cn/)
|
||||||
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
|
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云,共享群组)
|
||||||
- [x] [Yandex.Disk](https://disk.yandex.com/)
|
- [x] [Yandex.Disk](https://disk.yandex.com/)
|
||||||
- [x] [百度网盘](http://pan.baidu.com/)
|
- [x] [百度网盘](http://pan.baidu.com/)
|
||||||
- [x] [UC网盘](https://drive.uc.cn)
|
- [x] [UC网盘](https://drive.uc.cn)
|
||||||
- [x] [夸克网盘](https://pan.quark.cn)
|
- [x] [夸克网盘](https://pan.quark.cn)
|
||||||
- [x] [迅雷网盘](https://pan.xunlei.com)
|
- [x] [迅雷网盘](https://pan.xunlei.com)
|
||||||
- [x] [蓝奏云](https://www.lanzou.com/)
|
- [x] [蓝奏云](https://www.lanzou.com/)
|
||||||
- [x] [阿里云盘分享](https://www.aliyundrive.com/)
|
- [x] [蓝奏云优享版](https://www.ilanzou.com/)
|
||||||
|
- [x] [阿里云盘分享](https://www.alipan.com/)
|
||||||
- [x] [谷歌相册](https://photos.google.com/)
|
- [x] [谷歌相册](https://photos.google.com/)
|
||||||
- [x] [Mega.nz](https://mega.nz)
|
- [x] [Mega.nz](https://mega.nz)
|
||||||
- [x] [一刻相册](https://photo.baidu.com/)
|
- [x] [一刻相册](https://photo.baidu.com/)
|
||||||
@@ -73,6 +74,8 @@
|
|||||||
- [x] [115](https://115.com/)
|
- [x] [115](https://115.com/)
|
||||||
- [X] Cloudreve
|
- [X] Cloudreve
|
||||||
- [x] [Dropbox](https://www.dropbox.com/)
|
- [x] [Dropbox](https://www.dropbox.com/)
|
||||||
|
- [x] [飞机盘](https://www.feijipan.com/)
|
||||||
|
- [x] [多吉云](https://www.dogecloud.com/product/oss)
|
||||||
- [x] 部署方便,开箱即用
|
- [x] 部署方便,开箱即用
|
||||||
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
||||||
- [x] 画廊模式下的图像预览
|
- [x] 画廊模式下的图像预览
|
||||||
@@ -102,7 +105,7 @@
|
|||||||
|
|
||||||
## 讨论
|
## 讨论
|
||||||
|
|
||||||
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) ,**issue仅针对错误报告和功能请求。**
|
一般问题请到[讨论论坛](https://github.com/alist-org/alist/discussions) ,**issue仅针对错误报告和功能请求。**
|
||||||
|
|
||||||
## 赞助
|
## 赞助
|
||||||
|
|
||||||
@@ -110,15 +113,15 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
|
|||||||
|
|
||||||
### 特别赞助
|
### 特别赞助
|
||||||
|
|
||||||
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (国内API服务器赞助)
|
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器,iPhone,iPad,Mac,Apple TV全平台支持。
|
||||||
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.pw/)
|
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
|
||||||
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
|
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||||
|
|
||||||
## 贡献者
|
## 贡献者
|
||||||
|
|
||||||
Thanks goes to these wonderful people:
|
Thanks goes to these wonderful people:
|
||||||
|
|
||||||
[](https://github.com/alist-org/alist/graphs/contributors)
|
[](https://github.com/alist-org/alist/graphs/contributors)
|
||||||
|
|
||||||
## 许可
|
## 许可
|
||||||
|
|
||||||
@@ -133,4 +136,4 @@ Thanks goes to these wonderful people:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||||
|
|||||||
33
README_ja.md
33
README_ja.md
@@ -1,17 +1,17 @@
|
|||||||
<div align="center">
|
<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>
|
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||||
<p><em>🗂️Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p>
|
<p><em>🗂️Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
<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" />
|
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
|
<a href="https://github.com/alist-org/alist/blob/main/LICENSE">
|
||||||
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
|
<a href="https://github.com/alist-org/alist/actions?query=workflow%3ABuild">
|
||||||
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
<img src="https://img.shields.io/github/actions/workflow/status/Xhofe/alist/build.yml?branch=main" alt="Build status" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/releases">
|
<a href="https://github.com/alist-org/alist/releases">
|
||||||
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
|
||||||
</a>
|
</a>
|
||||||
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
|
||||||
@@ -19,13 +19,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://github.com/Xhofe/alist/discussions">
|
<a href="https://github.com/alist-org/alist/discussions">
|
||||||
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/F4ymsH4xv2">
|
<a href="https://discord.gg/F4ymsH4xv2">
|
||||||
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Xhofe/alist/releases">
|
<a href="https://github.com/alist-org/alist/releases">
|
||||||
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://hub.docker.com/r/xhofe/alist">
|
<a href="https://hub.docker.com/r/xhofe/alist">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
- [x] マルチストレージ
|
- [x] マルチストレージ
|
||||||
- [x] ローカルストレージ
|
- [x] ローカルストレージ
|
||||||
- [x] [Aliyundrive](https://www.aliyundrive.com/)
|
- [x] [Aliyundrive](https://www.alipan.com/)
|
||||||
- [x] OneDrive / Sharepoint ([グローバル](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us)
|
- [x] OneDrive / Sharepoint ([グローバル](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us)
|
||||||
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
|
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
|
||||||
- [x] [GoogleDrive](https://drive.google.com/)
|
- [x] [GoogleDrive](https://drive.google.com/)
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
- [x] WebDav(Support OneDrive/SharePoint without API)
|
||||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
||||||
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
||||||
- [x] [139yun](https://yun.139.com/) (Personal, Family)
|
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
|
||||||
- [x] [YandexDisk](https://disk.yandex.com/)
|
- [x] [YandexDisk](https://disk.yandex.com/)
|
||||||
- [x] [BaiduNetdisk](http://pan.baidu.com/)
|
- [x] [BaiduNetdisk](http://pan.baidu.com/)
|
||||||
- [x] [Terabox](https://www.terabox.com/main)
|
- [x] [Terabox](https://www.terabox.com/main)
|
||||||
@@ -66,7 +66,8 @@
|
|||||||
- [x] [Quark](https://pan.quark.cn)
|
- [x] [Quark](https://pan.quark.cn)
|
||||||
- [x] [Thunder](https://pan.xunlei.com)
|
- [x] [Thunder](https://pan.xunlei.com)
|
||||||
- [x] [Lanzou](https://www.lanzou.com/)
|
- [x] [Lanzou](https://www.lanzou.com/)
|
||||||
- [x] [Aliyundrive share](https://www.aliyundrive.com/)
|
- [x] [ILanzou](https://www.ilanzou.com/)
|
||||||
|
- [x] [Aliyundrive share](https://www.alipan.com/)
|
||||||
- [x] [Google photo](https://photos.google.com/)
|
- [x] [Google photo](https://photos.google.com/)
|
||||||
- [x] [Mega.nz](https://mega.nz)
|
- [x] [Mega.nz](https://mega.nz)
|
||||||
- [x] [Baidu photo](https://photo.baidu.com/)
|
- [x] [Baidu photo](https://photo.baidu.com/)
|
||||||
@@ -74,6 +75,8 @@
|
|||||||
- [x] [115](https://115.com/)
|
- [x] [115](https://115.com/)
|
||||||
- [X] Cloudreve
|
- [X] Cloudreve
|
||||||
- [x] [Dropbox](https://www.dropbox.com/)
|
- [x] [Dropbox](https://www.dropbox.com/)
|
||||||
|
- [x] [FeijiPan](https://www.feijipan.com/)
|
||||||
|
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
||||||
- [x] デプロイが簡単で、すぐに使える
|
- [x] デプロイが簡単で、すぐに使える
|
||||||
- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)
|
- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)
|
||||||
- [x] ギャラリーモードでの画像プレビュー
|
- [x] ギャラリーモードでの画像プレビュー
|
||||||
@@ -103,7 +106,7 @@
|
|||||||
|
|
||||||
## ディスカッション
|
## ディスカッション
|
||||||
|
|
||||||
一般的なご質問は[ディスカッションフォーラム](https://github.com/Xhofe/alist/discussions)をご利用ください。**問題はバグレポートと機能リクエストのみです。**
|
一般的なご質問は[ディスカッションフォーラム](https://github.com/alist-org/alist/discussions)をご利用ください。**問題はバグレポートと機能リクエストのみです。**
|
||||||
|
|
||||||
## スポンサー
|
## スポンサー
|
||||||
|
|
||||||
@@ -112,15 +115,15 @@ https://alist.nn.ci/guide/sponsor.html
|
|||||||
|
|
||||||
### スペシャルスポンサー
|
### スペシャルスポンサー
|
||||||
|
|
||||||
- [亚洲云 - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商](https://www.asiayun.com/aff/QQCOOQKZ) (sponsored Chinese API server)
|
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
|
||||||
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.pw/)
|
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
||||||
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
|
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
|
||||||
|
|
||||||
## コントリビューター
|
## コントリビューター
|
||||||
|
|
||||||
これらの素晴らしい人々に感謝します:
|
これらの素晴らしい人々に感謝します:
|
||||||
|
|
||||||
[](https://github.com/alist-org/alist/graphs/contributors)
|
[](https://github.com/alist-org/alist/graphs/contributors)
|
||||||
|
|
||||||
## ライセンス
|
## ライセンス
|
||||||
|
|
||||||
@@ -135,4 +138,4 @@ https://alist.nn.ci/guide/sponsor.html
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/alist-org) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
|
||||||
|
|||||||
164
build.sh
164
build.sh
@@ -1,13 +1,16 @@
|
|||||||
appName="alist"
|
appName="alist"
|
||||||
builtAt="$(date +'%F %T %z')"
|
builtAt="$(date +'%F %T %z')"
|
||||||
goVersion=$(go version | sed 's/go version //')
|
|
||||||
gitAuthor="Xhofe <i@nn.ci>"
|
gitAuthor="Xhofe <i@nn.ci>"
|
||||||
gitCommit=$(git log --pretty=format:"%h" -1)
|
gitCommit=$(git log --pretty=format:"%h" -1)
|
||||||
|
|
||||||
if [ "$1" = "dev" ]; then
|
if [ "$1" = "dev" ]; then
|
||||||
version="dev"
|
version="dev"
|
||||||
webVersion="dev"
|
webVersion="dev"
|
||||||
|
elif [ "$1" = "beta" ]; then
|
||||||
|
version="beta"
|
||||||
|
webVersion="dev"
|
||||||
else
|
else
|
||||||
|
git tag -d beta
|
||||||
version=$(git describe --abbrev=0 --tags)
|
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')
|
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
|
fi
|
||||||
@@ -18,7 +21,6 @@ echo "frontend version: $webVersion"
|
|||||||
ldflags="\
|
ldflags="\
|
||||||
-w -s \
|
-w -s \
|
||||||
-X 'github.com/alist-org/alist/v3/internal/conf.BuiltAt=$builtAt' \
|
-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.GitAuthor=$gitAuthor' \
|
||||||
-X 'github.com/alist-org/alist/v3/internal/conf.GitCommit=$gitCommit' \
|
-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.Version=$version' \
|
||||||
@@ -49,6 +51,7 @@ BuildWinArm64() {
|
|||||||
export GOARCH=arm64
|
export GOARCH=arm64
|
||||||
export CC=$(pwd)/wrapper/zcc-arm64
|
export CC=$(pwd)/wrapper/zcc-arm64
|
||||||
export CXX=$(pwd)/wrapper/zcxx-arm64
|
export CXX=$(pwd)/wrapper/zcxx-arm64
|
||||||
|
export CGO_ENABLED=1
|
||||||
go build -o "$1" -ldflags="$ldflags" -tags=jsoniter .
|
go build -o "$1" -ldflags="$ldflags" -tags=jsoniter .
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +78,7 @@ BuildDev() {
|
|||||||
export CGO_ENABLED=1
|
export CGO_ENABLED=1
|
||||||
go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
||||||
done
|
done
|
||||||
xgo -targets=windows/amd64,darwin/amd64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
xgo -targets=windows/amd64,darwin/amd64,darwin/arm64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
||||||
mv alist-* dist
|
mv alist-* dist
|
||||||
cd dist
|
cd dist
|
||||||
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
|
cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
|
||||||
@@ -88,7 +91,70 @@ BuildDocker() {
|
|||||||
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
|
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PrepareBuildDockerMusl() {
|
||||||
|
mkdir -p build/musl-libs
|
||||||
|
BASE="https://musl.cc/"
|
||||||
|
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross riscv64-linux-musl-cross powerpc64le-linux-musl-cross)
|
||||||
|
for i in "${FILES[@]}"; do
|
||||||
|
url="${BASE}${i}.tgz"
|
||||||
|
lib_tgz="build/${i}.tgz"
|
||||||
|
curl -L -o "${lib_tgz}" "${url}"
|
||||||
|
tar xf "${lib_tgz}" --strip-components 1 -C build/musl-libs
|
||||||
|
rm -f "${lib_tgz}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildDockerMultiplatform() {
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
# run PrepareBuildDockerMusl before build
|
||||||
|
export PATH=$PATH:$PWD/build/musl-libs/bin
|
||||||
|
|
||||||
|
docker_lflags="--extldflags '-static -fpic' $ldflags"
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
|
||||||
|
OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-s390x linux-riscv64 linux-ppc64le)
|
||||||
|
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc s390x-linux-musl-gcc riscv64-linux-musl-gcc powerpc64le-linux-musl-gcc)
|
||||||
|
for i in "${!OS_ARCHES[@]}"; do
|
||||||
|
os_arch=${OS_ARCHES[$i]}
|
||||||
|
cgo_cc=${CGO_ARGS[$i]}
|
||||||
|
os=${os_arch%%-*}
|
||||||
|
arch=${os_arch##*-}
|
||||||
|
export GOOS=$os
|
||||||
|
export GOARCH=$arch
|
||||||
|
export CC=${cgo_cc}
|
||||||
|
echo "building for $os_arch"
|
||||||
|
go build -o build/$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter .
|
||||||
|
done
|
||||||
|
|
||||||
|
DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)
|
||||||
|
CGO_ARGS=(armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc)
|
||||||
|
GO_ARM=(6 7)
|
||||||
|
export GOOS=linux
|
||||||
|
export GOARCH=arm
|
||||||
|
for i in "${!DOCKER_ARM_ARCHES[@]}"; do
|
||||||
|
docker_arch=${DOCKER_ARM_ARCHES[$i]}
|
||||||
|
cgo_cc=${CGO_ARGS[$i]}
|
||||||
|
export GOARM=${GO_ARM[$i]}
|
||||||
|
export CC=${cgo_cc}
|
||||||
|
echo "building for $docker_arch"
|
||||||
|
go build -o build/${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter .
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
BuildRelease() {
|
BuildRelease() {
|
||||||
|
rm -rf .git/
|
||||||
|
mkdir -p "build"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildReleaseLinuxMusl() {
|
||||||
rm -rf .git/
|
rm -rf .git/
|
||||||
mkdir -p "build"
|
mkdir -p "build"
|
||||||
muslflags="--extldflags '-static -fpic' $ldflags"
|
muslflags="--extldflags '-static -fpic' $ldflags"
|
||||||
@@ -112,13 +178,6 @@ BuildRelease() {
|
|||||||
export CGO_ENABLED=1
|
export CGO_ENABLED=1
|
||||||
go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
||||||
done
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildReleaseLinuxMuslArm() {
|
BuildReleaseLinuxMuslArm() {
|
||||||
@@ -154,6 +213,50 @@ BuildReleaseLinuxMuslArm() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BuildReleaseAndroid() {
|
||||||
|
rm -rf .git/
|
||||||
|
mkdir -p "build"
|
||||||
|
wget https://dl.google.com/android/repository/android-ndk-r26b-linux.zip
|
||||||
|
unzip android-ndk-r26b-linux.zip
|
||||||
|
rm android-ndk-r26b-linux.zip
|
||||||
|
OS_ARCHES=(amd64 arm64 386 arm)
|
||||||
|
CGO_ARGS=(x86_64-linux-android24-clang aarch64-linux-android24-clang i686-linux-android24-clang armv7a-linux-androideabi24-clang)
|
||||||
|
for i in "${!OS_ARCHES[@]}"; do
|
||||||
|
os_arch=${OS_ARCHES[$i]}
|
||||||
|
cgo_cc=$(realpath android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/${CGO_ARGS[$i]})
|
||||||
|
echo building for android-${os_arch}
|
||||||
|
export GOOS=android
|
||||||
|
export GOARCH=${os_arch##*-}
|
||||||
|
export CC=${cgo_cc}
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
go build -o ./build/$appName-android-$os_arch -ldflags="$ldflags" -tags=jsoniter .
|
||||||
|
android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip ./build/$appName-android-$os_arch
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildReleaseFreeBSD() {
|
||||||
|
rm -rf .git/
|
||||||
|
mkdir -p "build/freebsd"
|
||||||
|
OS_ARCHES=(amd64 arm64 i386)
|
||||||
|
GO_ARCHES=(amd64 arm64 386)
|
||||||
|
CGO_ARGS=(x86_64-unknown-freebsd14.1 aarch64-unknown-freebsd14.1 i386-unknown-freebsd14.1)
|
||||||
|
for i in "${!OS_ARCHES[@]}"; do
|
||||||
|
os_arch=${OS_ARCHES[$i]}
|
||||||
|
cgo_cc="clang --target=${CGO_ARGS[$i]} --sysroot=/opt/freebsd/${os_arch}"
|
||||||
|
echo building for freebsd-${os_arch}
|
||||||
|
sudo mkdir -p "/opt/freebsd/${os_arch}"
|
||||||
|
wget -q https://download.freebsd.org/releases/${os_arch}/14.1-RELEASE/base.txz
|
||||||
|
sudo tar -xf ./base.txz -C /opt/freebsd/${os_arch}
|
||||||
|
rm base.txz
|
||||||
|
export GOOS=freebsd
|
||||||
|
export GOARCH=${GO_ARCHES[$i]}
|
||||||
|
export CC=${cgo_cc}
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
export CGO_LDFLAGS="-fuse-ld=lld"
|
||||||
|
go build -o ./build/$appName-freebsd-$os_arch -ldflags="$ldflags" -tags=jsoniter .
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
MakeRelease() {
|
MakeRelease() {
|
||||||
cd build
|
cd build
|
||||||
mkdir compress
|
mkdir compress
|
||||||
@@ -161,12 +264,22 @@ MakeRelease() {
|
|||||||
cp "$i" alist
|
cp "$i" alist
|
||||||
tar -czvf compress/"$i".tar.gz alist
|
tar -czvf compress/"$i".tar.gz alist
|
||||||
rm -f alist
|
rm -f alist
|
||||||
|
done
|
||||||
|
for i in $(find . -type f -name "$appName-android-*"); do
|
||||||
|
cp "$i" alist
|
||||||
|
tar -czvf compress/"$i".tar.gz alist
|
||||||
|
rm -f alist
|
||||||
done
|
done
|
||||||
for i in $(find . -type f -name "$appName-darwin-*"); do
|
for i in $(find . -type f -name "$appName-darwin-*"); do
|
||||||
cp "$i" alist
|
cp "$i" alist
|
||||||
tar -czvf compress/"$i".tar.gz alist
|
tar -czvf compress/"$i".tar.gz alist
|
||||||
rm -f alist
|
rm -f alist
|
||||||
done
|
done
|
||||||
|
for i in $(find . -type f -name "$appName-freebsd-*"); 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
|
for i in $(find . -type f -name "$appName-windows-*"); do
|
||||||
cp "$i" alist.exe
|
cp "$i" alist.exe
|
||||||
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip alist.exe
|
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip alist.exe
|
||||||
@@ -182,20 +295,47 @@ if [ "$1" = "dev" ]; then
|
|||||||
FetchWebDev
|
FetchWebDev
|
||||||
if [ "$2" = "docker" ]; then
|
if [ "$2" = "docker" ]; then
|
||||||
BuildDocker
|
BuildDocker
|
||||||
|
elif [ "$2" = "docker-multiplatform" ]; then
|
||||||
|
BuildDockerMultiplatform
|
||||||
|
elif [ "$2" = "web" ]; then
|
||||||
|
echo "web only"
|
||||||
else
|
else
|
||||||
BuildDev
|
BuildDev
|
||||||
fi
|
fi
|
||||||
elif [ "$1" = "release" ]; then
|
elif [ "$1" = "release" -o "$1" = "beta" ]; then
|
||||||
FetchWebRelease
|
if [ "$1" = "beta" ]; then
|
||||||
|
FetchWebDev
|
||||||
|
else
|
||||||
|
FetchWebRelease
|
||||||
|
fi
|
||||||
if [ "$2" = "docker" ]; then
|
if [ "$2" = "docker" ]; then
|
||||||
BuildDocker
|
BuildDocker
|
||||||
|
elif [ "$2" = "docker-multiplatform" ]; then
|
||||||
|
BuildDockerMultiplatform
|
||||||
elif [ "$2" = "linux_musl_arm" ]; then
|
elif [ "$2" = "linux_musl_arm" ]; then
|
||||||
BuildReleaseLinuxMuslArm
|
BuildReleaseLinuxMuslArm
|
||||||
MakeRelease "md5-linux-musl-arm.txt"
|
MakeRelease "md5-linux-musl-arm.txt"
|
||||||
|
elif [ "$2" = "linux_musl" ]; then
|
||||||
|
BuildReleaseLinuxMusl
|
||||||
|
MakeRelease "md5-linux-musl.txt"
|
||||||
|
elif [ "$2" = "android" ]; then
|
||||||
|
BuildReleaseAndroid
|
||||||
|
MakeRelease "md5-android.txt"
|
||||||
|
elif [ "$2" = "freebsd" ]; then
|
||||||
|
BuildReleaseFreeBSD
|
||||||
|
MakeRelease "md5-freebsd.txt"
|
||||||
|
elif [ "$2" = "web" ]; then
|
||||||
|
echo "web only"
|
||||||
else
|
else
|
||||||
BuildRelease
|
BuildRelease
|
||||||
MakeRelease "md5.txt"
|
MakeRelease "md5.txt"
|
||||||
fi
|
fi
|
||||||
|
elif [ "$1" = "prepare" ]; then
|
||||||
|
if [ "$2" = "docker-multiplatform" ]; then
|
||||||
|
PrepareBuildDockerMusl
|
||||||
|
fi
|
||||||
|
elif [ "$1" = "zip" ]; then
|
||||||
|
MakeRelease "$2".txt
|
||||||
else
|
else
|
||||||
echo -e "Parameter error"
|
echo -e "Parameter error"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ var AdminCmd = &cobra.Command{
|
|||||||
Short: "Show admin user's info and some operations about admin user's password",
|
Short: "Show admin user's info and some operations about admin user's password",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Init()
|
Init()
|
||||||
|
defer Release()
|
||||||
admin, err := op.GetAdmin()
|
admin, err := op.GetAdmin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log.Errorf("failed get admin user: %+v", err)
|
utils.Log.Errorf("failed get admin user: %+v", err)
|
||||||
@@ -57,6 +58,7 @@ var ShowTokenCmd = &cobra.Command{
|
|||||||
Short: "Show admin token",
|
Short: "Show admin token",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Init()
|
Init()
|
||||||
|
defer Release()
|
||||||
token := setting.GetStr(conf.Token)
|
token := setting.GetStr(conf.Token)
|
||||||
utils.Log.Infof("Admin token: %s", token)
|
utils.Log.Infof("Admin token: %s", token)
|
||||||
},
|
},
|
||||||
@@ -64,6 +66,7 @@ var ShowTokenCmd = &cobra.Command{
|
|||||||
|
|
||||||
func setAdminPassword(pwd string) {
|
func setAdminPassword(pwd string) {
|
||||||
Init()
|
Init()
|
||||||
|
defer Release()
|
||||||
admin, err := op.GetAdmin()
|
admin, err := op.GetAdmin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log.Errorf("failed get admin user: %+v", err)
|
utils.Log.Errorf("failed get admin user: %+v", err)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ var Cancel2FACmd = &cobra.Command{
|
|||||||
Short: "Delete 2FA of admin user",
|
Short: "Delete 2FA of admin user",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Init()
|
Init()
|
||||||
|
defer Release()
|
||||||
admin, err := op.GetAdmin()
|
admin, err := op.GetAdmin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log.Errorf("failed to get admin user: %+v", err)
|
utils.Log.Errorf("failed to get admin user: %+v", err)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/bootstrap"
|
"github.com/alist-org/alist/v3/internal/bootstrap"
|
||||||
"github.com/alist-org/alist/v3/internal/bootstrap/data"
|
"github.com/alist-org/alist/v3/internal/bootstrap/data"
|
||||||
|
"github.com/alist-org/alist/v3/internal/db"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -16,7 +17,13 @@ func Init() {
|
|||||||
bootstrap.Log()
|
bootstrap.Log()
|
||||||
bootstrap.InitDB()
|
bootstrap.InitDB()
|
||||||
data.InitData()
|
data.InitData()
|
||||||
|
bootstrap.InitStreamLimit()
|
||||||
bootstrap.InitIndex()
|
bootstrap.InitIndex()
|
||||||
|
bootstrap.InitUpgradePatch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Release() {
|
||||||
|
db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
var pid = -1
|
var pid = -1
|
||||||
|
|||||||
54
cmd/kill.go
Normal file
54
cmd/kill.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KillCmd represents the kill command
|
||||||
|
var KillCmd = &cobra.Command{
|
||||||
|
Use: "kill",
|
||||||
|
Short: "Force kill alist server process by daemon/pid file",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
kill()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func kill() {
|
||||||
|
initDaemon()
|
||||||
|
if pid == -1 {
|
||||||
|
log.Info("Seems not have been started. Try use `alist start` to start server.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to find process by pid: %d, reason: %v", pid, process)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = process.Kill()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to kill process %d: %v", pid, err)
|
||||||
|
} else {
|
||||||
|
log.Info("killed process: ", pid)
|
||||||
|
}
|
||||||
|
err = os.Remove(pidFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to remove pid file")
|
||||||
|
}
|
||||||
|
pid = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(KillCmd)
|
||||||
|
|
||||||
|
// Here you will define your flags and configuration settings.
|
||||||
|
|
||||||
|
// Cobra supports Persistent Flags which will work for this command
|
||||||
|
// and all subcommands, e.g.:
|
||||||
|
// stopCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||||
|
|
||||||
|
// Cobra supports local flags which will only run when this command
|
||||||
|
// is called directly, e.g.:
|
||||||
|
// stopCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers"
|
_ "github.com/alist-org/alist/v3/drivers"
|
||||||
|
"github.com/alist-org/alist/v3/internal/bootstrap"
|
||||||
"github.com/alist-org/alist/v3/internal/bootstrap/data"
|
"github.com/alist-org/alist/v3/internal/bootstrap/data"
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
@@ -137,9 +138,10 @@ var LangCmd = &cobra.Command{
|
|||||||
Use: "lang",
|
Use: "lang",
|
||||||
Short: "Generate language json file",
|
Short: "Generate language json file",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
bootstrap.InitConfig()
|
||||||
err := os.MkdirAll("lang", 0777)
|
err := os.MkdirAll("lang", 0777)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log.Fatal("failed create folder: %s", err.Error())
|
utils.Log.Fatalf("failed create folder: %s", err.Error())
|
||||||
}
|
}
|
||||||
generateDriversJson()
|
generateDriversJson()
|
||||||
generateSettingsJson()
|
generateSettingsJson()
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/cmd/flags"
|
"github.com/alist-org/alist/v3/cmd/flags"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers"
|
||||||
|
_ "github.com/alist-org/alist/v3/internal/archive"
|
||||||
|
_ "github.com/alist-org/alist/v3/internal/offline_download"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||||
|
"github.com/KirCute/sftpd-alist"
|
||||||
|
"github.com/alist-org/alist/v3/internal/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,7 +17,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/cmd/flags"
|
"github.com/alist-org/alist/v3/cmd/flags"
|
||||||
_ "github.com/alist-org/alist/v3/drivers"
|
|
||||||
"github.com/alist-org/alist/v3/internal/bootstrap"
|
"github.com/alist-org/alist/v3/internal/bootstrap"
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
@@ -35,9 +38,9 @@ the address is defined in config file`,
|
|||||||
utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
|
utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
|
||||||
time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
|
time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
|
||||||
}
|
}
|
||||||
bootstrap.InitAria2()
|
bootstrap.InitOfflineDownloadTools()
|
||||||
bootstrap.InitQbittorrent()
|
|
||||||
bootstrap.LoadStorages()
|
bootstrap.LoadStorages()
|
||||||
|
bootstrap.InitTaskManager()
|
||||||
if !flags.Debug && !flags.Dev {
|
if !flags.Debug && !flags.Dev {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
@@ -51,7 +54,7 @@ the address is defined in config file`,
|
|||||||
httpSrv = &http.Server{Addr: httpBase, Handler: r}
|
httpSrv = &http.Server{Addr: httpBase, Handler: r}
|
||||||
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())
|
utils.Log.Fatalf("failed to start http: %s", err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -62,7 +65,7 @@ the address is defined in config file`,
|
|||||||
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())
|
utils.Log.Fatalf("failed to start https: %s", err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -86,11 +89,68 @@ the address is defined in config file`,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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())
|
utils.Log.Fatalf("failed to start unix: %s", err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
|
||||||
|
s3r := gin.New()
|
||||||
|
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
|
||||||
|
server.InitS3(s3r)
|
||||||
|
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
|
||||||
|
utils.Log.Infof("start S3 server @ %s", s3Base)
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
if conf.Conf.S3.SSL {
|
||||||
|
httpsSrv = &http.Server{Addr: s3Base, Handler: s3r}
|
||||||
|
err = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||||
|
}
|
||||||
|
if !conf.Conf.S3.SSL {
|
||||||
|
httpSrv = &http.Server{Addr: s3Base, Handler: s3r}
|
||||||
|
err = httpSrv.ListenAndServe()
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
utils.Log.Fatalf("failed to start s3 server: %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
var ftpDriver *server.FtpMainDriver
|
||||||
|
var ftpServer *ftpserver.FtpServer
|
||||||
|
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable {
|
||||||
|
var err error
|
||||||
|
ftpDriver, err = server.NewMainDriver()
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Fatalf("failed to start ftp driver: %s", err.Error())
|
||||||
|
} else {
|
||||||
|
utils.Log.Infof("start ftp server on %s", conf.Conf.FTP.Listen)
|
||||||
|
go func() {
|
||||||
|
ftpServer = ftpserver.NewFtpServer(ftpDriver)
|
||||||
|
err = ftpServer.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Fatalf("problem ftp server listening: %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var sftpDriver *server.SftpDriver
|
||||||
|
var sftpServer *sftpd.SftpServer
|
||||||
|
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable {
|
||||||
|
var err error
|
||||||
|
sftpDriver, err = server.NewSftpDriver()
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Fatalf("failed to start sftp driver: %s", err.Error())
|
||||||
|
} else {
|
||||||
|
utils.Log.Infof("start sftp server on %s", conf.Conf.SFTP.Listen)
|
||||||
|
go func() {
|
||||||
|
sftpServer = sftpd.NewSftpServer(sftpDriver)
|
||||||
|
err = sftpServer.RunServer()
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Fatalf("problem sftp server listening: %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
// Wait for interrupt signal to gracefully shutdown the server with
|
// Wait for interrupt signal to gracefully shutdown the server with
|
||||||
// a timeout of 1 second.
|
// a timeout of 1 second.
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
@@ -100,7 +160,8 @@ the address is defined in config file`,
|
|||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-quit
|
<-quit
|
||||||
utils.Log.Println("Shutdown server...")
|
utils.Log.Println("Shutdown server...")
|
||||||
|
fs.ArchiveContentUploadTaskManager.RemoveAll()
|
||||||
|
Release()
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -131,6 +192,25 @@ the address is defined in config file`,
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable && ftpServer != nil && ftpDriver != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
ftpDriver.Stop()
|
||||||
|
if err := ftpServer.Stop(); err != nil {
|
||||||
|
utils.Log.Fatal("FTP server shutdown err: ", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable && sftpServer != nil && sftpDriver != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := sftpServer.Close(); err != nil {
|
||||||
|
utils.Log.Fatal("SFTP server shutdown err: ", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
utils.Log.Println("Server exit")
|
utils.Log.Println("Server exit")
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/*
|
//go:build !windows
|
||||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
|
||||||
*/
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -30,11 +30,11 @@ func stop() {
|
|||||||
log.Errorf("failed to find process by pid: %d, reason: %v", pid, process)
|
log.Errorf("failed to find process by pid: %d, reason: %v", pid, process)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = process.Kill()
|
err = process.Signal(syscall.SIGTERM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to kill process %d: %v", pid, err)
|
log.Errorf("failed to terminate process %d: %v", pid, err)
|
||||||
} else {
|
} else {
|
||||||
log.Info("killed process: ", pid)
|
log.Info("terminated process: ", pid)
|
||||||
}
|
}
|
||||||
err = os.Remove(pidFile)
|
err = os.Remove(pidFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
34
cmd/stop_windows.go
Normal file
34
cmd/stop_windows.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StopCmd represents the stop command
|
||||||
|
var StopCmd = &cobra.Command{
|
||||||
|
Use: "stop",
|
||||||
|
Short: "Same as the kill command",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
stop()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(StopCmd)
|
||||||
|
|
||||||
|
// Here you will define your flags and configuration settings.
|
||||||
|
|
||||||
|
// Cobra supports Persistent Flags which will work for this command
|
||||||
|
// and all subcommands, e.g.:
|
||||||
|
// stopCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||||
|
|
||||||
|
// Cobra supports local flags which will only run when this command
|
||||||
|
// is called directly, e.g.:
|
||||||
|
// stopCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
}
|
||||||
153
cmd/storage.go
153
cmd/storage.go
@@ -4,8 +4,14 @@ Copyright © 2023 NAME HERE <EMAIL ADDRESS>
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/db"
|
"github.com/alist-org/alist/v3/internal/db"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,31 +21,136 @@ var storageCmd = &cobra.Command{
|
|||||||
Short: "Manage storage",
|
Short: "Manage storage",
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
var disableStorageCmd = &cobra.Command{
|
||||||
var mountPath string
|
Use: "disable",
|
||||||
var disable = &cobra.Command{
|
Short: "Disable a storage",
|
||||||
Use: "disable",
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Short: "Disable a storage",
|
if len(args) < 1 {
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
utils.Log.Errorf("mount path is required")
|
||||||
Init()
|
return
|
||||||
storage, err := db.GetStorageByMountPath(mountPath)
|
}
|
||||||
|
mountPath := args[0]
|
||||||
|
Init()
|
||||||
|
defer Release()
|
||||||
|
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 {
|
if err != nil {
|
||||||
utils.Log.Errorf("failed to query storage: %+v", err)
|
utils.Log.Errorf("failed to update storage: %+v", err)
|
||||||
} else {
|
} else {
|
||||||
storage.Disabled = true
|
utils.Log.Infof("Storage with mount path [%s] have been disabled", mountPath)
|
||||||
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)
|
|
||||||
|
|
||||||
|
var baseStyle = lipgloss.NewStyle().
|
||||||
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("240"))
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
table table.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd { return nil }
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc":
|
||||||
|
if m.table.Focused() {
|
||||||
|
m.table.Blur()
|
||||||
|
} else {
|
||||||
|
m.table.Focus()
|
||||||
|
}
|
||||||
|
case "q", "ctrl+c":
|
||||||
|
return m, tea.Quit
|
||||||
|
//case "enter":
|
||||||
|
// return m, tea.Batch(
|
||||||
|
// tea.Printf("Let's go to %s!", m.table.SelectedRow()[1]),
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.table, cmd = m.table.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
return baseStyle.Render(m.table.View()) + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
var storageTableHeight int
|
||||||
|
var listStorageCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List all storages",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
Init()
|
||||||
|
defer Release()
|
||||||
|
storages, _, err := db.GetStorages(1, -1)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("failed to query storages: %+v", err)
|
||||||
|
} else {
|
||||||
|
utils.Log.Infof("Found %d storages", len(storages))
|
||||||
|
columns := []table.Column{
|
||||||
|
{Title: "ID", Width: 4},
|
||||||
|
{Title: "Driver", Width: 16},
|
||||||
|
{Title: "Mount Path", Width: 30},
|
||||||
|
{Title: "Enabled", Width: 7},
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []table.Row
|
||||||
|
for i := range storages {
|
||||||
|
storage := storages[i]
|
||||||
|
enabled := "true"
|
||||||
|
if storage.Disabled {
|
||||||
|
enabled = "false"
|
||||||
|
}
|
||||||
|
rows = append(rows, table.Row{
|
||||||
|
strconv.Itoa(int(storage.ID)),
|
||||||
|
storage.Driver,
|
||||||
|
storage.MountPath,
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t := table.New(
|
||||||
|
table.WithColumns(columns),
|
||||||
|
table.WithRows(rows),
|
||||||
|
table.WithFocused(true),
|
||||||
|
table.WithHeight(storageTableHeight),
|
||||||
|
)
|
||||||
|
|
||||||
|
s := table.DefaultStyles()
|
||||||
|
s.Header = s.Header.
|
||||||
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("240")).
|
||||||
|
BorderBottom(true).
|
||||||
|
Bold(false)
|
||||||
|
s.Selected = s.Selected.
|
||||||
|
Foreground(lipgloss.Color("229")).
|
||||||
|
Background(lipgloss.Color("57")).
|
||||||
|
Bold(false)
|
||||||
|
t.SetStyles(s)
|
||||||
|
|
||||||
|
m := model{t}
|
||||||
|
if _, err := tea.NewProgram(m).Run(); err != nil {
|
||||||
|
utils.Log.Errorf("failed to run program: %+v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
RootCmd.AddCommand(storageCmd)
|
||||||
|
storageCmd.AddCommand(disableStorageCmd)
|
||||||
|
storageCmd.AddCommand(listStorageCmd)
|
||||||
|
storageCmd.PersistentFlags().IntVarP(&storageTableHeight, "height", "H", 10, "Table height")
|
||||||
// Here you will define your flags and configuration settings.
|
// Here you will define your flags and configuration settings.
|
||||||
|
|
||||||
// Cobra supports Persistent Flags which will work for this command
|
// Cobra supports Persistent Flags which will work for this command
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -16,14 +17,15 @@ var VersionCmd = &cobra.Command{
|
|||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Show current version of AList",
|
Short: "Show current version of AList",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
goVersion := fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
fmt.Printf(`Built At: %s
|
fmt.Printf(`Built At: %s
|
||||||
Go Version: %s
|
Go Version: %s
|
||||||
Author: %s
|
Author: %s
|
||||||
Commit ID: %s
|
Commit ID: %s
|
||||||
Version: %s
|
Version: %s
|
||||||
WebVersion: %s
|
WebVersion: %s
|
||||||
`,
|
`, conf.BuiltAt, goVersion, conf.GitAuthor, conf.GitCommit, conf.Version, conf.WebVersion)
|
||||||
conf.BuiltAt, conf.GoVersion, conf.GitAuthor, conf.GitCommit, conf.Version, conf.WebVersion)
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
43
drivers/115/appver.go
Normal file
43
drivers/115/appver.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package _115
|
||||||
|
|
||||||
|
import (
|
||||||
|
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||||||
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
md5Salt = "Qclm8MGWUv59TnrR0XPg"
|
||||||
|
appVer = "27.0.5.7"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Pan115) getAppVersion() ([]driver115.AppVersion, error) {
|
||||||
|
result := driver115.VersionResp{}
|
||||||
|
resp, err := base.RestyClient.R().Get(driver115.ApiGetVersion)
|
||||||
|
|
||||||
|
err = driver115.CheckErr(err, &result, resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Data.GetAppVersions(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) getAppVer() string {
|
||||||
|
// todo add some cache?
|
||||||
|
vers, err := d.getAppVersion()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("[115] get app version failed: %v", err)
|
||||||
|
return appVer
|
||||||
|
}
|
||||||
|
for _, ver := range vers {
|
||||||
|
if ver.AppName == "win" {
|
||||||
|
return ver.Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appVer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) initAppVer() {
|
||||||
|
appVer = d.getAppVer()
|
||||||
|
}
|
||||||
@@ -2,19 +2,24 @@ package _115
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Pan115 struct {
|
type Pan115 struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
client *driver115.Pan115Client
|
client *driver115.Pan115Client
|
||||||
|
limiter *rate.Limiter
|
||||||
|
appVerOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) Config() driver.Config {
|
func (d *Pan115) Config() driver.Config {
|
||||||
@@ -26,29 +31,44 @@ func (d *Pan115) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) Init(ctx context.Context) error {
|
func (d *Pan115) Init(ctx context.Context) error {
|
||||||
|
d.appVerOnce.Do(d.initAppVer)
|
||||||
|
if d.LimitRate > 0 {
|
||||||
|
d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
|
||||||
|
}
|
||||||
return d.login()
|
return d.login()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) WaitLimit(ctx context.Context) error {
|
||||||
|
if d.limiter != nil {
|
||||||
|
return d.limiter.Wait(ctx)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Pan115) Drop(ctx context.Context) error {
|
func (d *Pan115) Drop(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
files, err := d.getFiles(dir.GetID())
|
files, err := d.getFiles(dir.GetID())
|
||||||
if err != nil && !errors.Is(err, driver115.ErrNotExist) {
|
if err != nil && !errors.Is(err, driver115.ErrNotExist) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return utils.SliceConvert(files, func(src driver115.File) (model.Obj, error) {
|
return utils.SliceConvert(files, func(src FileObj) (model.Obj, error) {
|
||||||
return src, nil
|
return &src, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
downloadInfo, err := d.client.
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
SetUserAgent(driver115.UA115Browser).
|
return nil, err
|
||||||
Download(file.(driver115.File).PickCode)
|
}
|
||||||
// recover for upload
|
userAgent := args.Header.Get("User-Agent")
|
||||||
d.client.SetUserAgent(driver115.UA115Desktop)
|
downloadInfo, err := d.
|
||||||
|
DownloadWithUA(file.(*FileObj).PickCode, userAgent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -59,39 +79,173 @@ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
|||||||
return link, nil
|
return link, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
if _, err := d.client.Mkdir(parentDir.GetID(), dirName); err != nil {
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
result := driver115.MkdirResp{}
|
||||||
|
form := map[string]string{
|
||||||
|
"pid": parentDir.GetID(),
|
||||||
|
"cname": dirName,
|
||||||
|
}
|
||||||
|
req := d.client.NewRequest().
|
||||||
|
SetFormData(form).
|
||||||
|
SetResult(&result).
|
||||||
|
ForceContentType("application/json;charset=UTF-8")
|
||||||
|
|
||||||
|
resp, err := req.Post(driver115.ApiDirAdd)
|
||||||
|
|
||||||
|
err = driver115.CheckErr(err, &result, resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f, err := d.getNewFile(result.FileID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
return d.client.Move(dstDir.GetID(), srcObj.GetID())
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := d.client.Move(dstDir.GetID(), srcObj.GetID()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f, err := d.getNewFile(srcObj.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
return d.client.Rename(srcObj.GetID(), newName)
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := d.client.Rename(srcObj.GetID(), newName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f, err := d.getNewFile((srcObj.GetID()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return d.client.Copy(dstDir.GetID(), srcObj.GetID())
|
return d.client.Copy(dstDir.GetID(), srcObj.GetID())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {
|
func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return d.client.Delete(obj.GetID())
|
return d.client.Delete(obj.GetID())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
tempFile, err := utils.CreateTempFile(stream.GetReadCloser(), stream.GetSize())
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
_ = tempFile.Close()
|
var (
|
||||||
_ = os.Remove(tempFile.Name())
|
fastInfo *driver115.UploadInitResp
|
||||||
}()
|
dirID = dstDir.GetID()
|
||||||
return d.client.UploadFastOrByMultipart(dstDir.GetID(), stream.GetName(), stream.GetSize(), tempFile)
|
)
|
||||||
|
|
||||||
|
if ok, err := d.client.UploadAvailable(); err != nil || !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if stream.GetSize() > d.client.UploadMetaInfo.SizeLimit {
|
||||||
|
return nil, driver115.ErrUploadTooLarge
|
||||||
|
}
|
||||||
|
//if digest, err = d.client.GetDigestResult(stream); err != nil {
|
||||||
|
// return err
|
||||||
|
//}
|
||||||
|
|
||||||
|
const PreHashSize int64 = 128 * utils.KB
|
||||||
|
hashSize := PreHashSize
|
||||||
|
if stream.GetSize() < PreHashSize {
|
||||||
|
hashSize = stream.GetSize()
|
||||||
|
}
|
||||||
|
reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: hashSize})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
preHash, err := utils.HashReader(utils.SHA1, reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
preHash = strings.ToUpper(preHash)
|
||||||
|
fullHash := stream.GetHash().GetHash(utils.SHA1)
|
||||||
|
if len(fullHash) <= 0 {
|
||||||
|
tmpF, err := stream.CacheFullInTempFile()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fullHash, err = utils.HashFile(utils.SHA1, tmpF)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fullHash = strings.ToUpper(fullHash)
|
||||||
|
|
||||||
|
// rapid-upload
|
||||||
|
// note that 115 add timeout for rapid-upload,
|
||||||
|
// and "sig invalid" err is thrown even when the hash is correct after timeout.
|
||||||
|
if fastInfo, err = d.rapidUpload(stream.GetSize(), stream.GetName(), dirID, preHash, fullHash, stream); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if matched, err := fastInfo.Ok(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if matched {
|
||||||
|
f, err := d.getNewFileByPickCode(fastInfo.PickCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadResult *UploadResult
|
||||||
|
// 闪传失败,上传
|
||||||
|
if stream.GetSize() <= 10*utils.MB { // 文件大小小于10MB,改用普通模式上传
|
||||||
|
if uploadResult, err = d.UploadByOSS(ctx, &fastInfo.UploadOSSParams, stream, dirID, up); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 分片上传
|
||||||
|
if uploadResult, err = d.UploadByMultipart(ctx, &fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID, up); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := d.getNewFile(uploadResult.Data.FileID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) OfflineList(ctx context.Context) ([]*driver115.OfflineTask, error) {
|
||||||
|
resp, err := d.client.ListOfflineTask(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Tasks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {
|
||||||
|
return d.client.AddOfflineTaskURIs(uris, dstDir.GetID(), driver115.WithAppVer(appVer))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, deleteFiles bool) error {
|
||||||
|
return d.client.DeleteOfflineTasks(hashes, deleteFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*Pan115)(nil)
|
var _ driver.Driver = (*Pan115)(nil)
|
||||||
|
|||||||
@@ -6,18 +6,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
|
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"`
|
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"`
|
QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,tv,alipaymini,wechatmini,qandroid" default:"linux" help:"select the QR code device, default linux"`
|
||||||
|
PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
|
||||||
|
LimitRate float64 `json:"limit_rate" type:"float" default:"2" help:"limit all api request rate ([limit]r/1s)"`
|
||||||
driver.RootID
|
driver.RootID
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "115 Cloud",
|
Name: "115 Cloud",
|
||||||
DefaultRoot: "0",
|
DefaultRoot: "0",
|
||||||
OnlyProxy: true,
|
// OnlyProxy: true,
|
||||||
OnlyLocal: true,
|
// OnlyLocal: true,
|
||||||
NoOverwriteUpload: true,
|
// NoOverwriteUpload: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -1,8 +1,38 @@
|
|||||||
package _115
|
package _115
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/SheltonZhu/115driver/pkg/driver"
|
"github.com/SheltonZhu/115driver/pkg/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ model.Obj = (*driver.File)(nil)
|
var _ model.Obj = (*FileObj)(nil)
|
||||||
|
|
||||||
|
type FileObj struct {
|
||||||
|
driver.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileObj) CreateTime() time.Time {
|
||||||
|
return f.File.CreateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileObj) GetHash() utils.HashInfo {
|
||||||
|
return utils.NewHashInfo(utils.SHA1, f.Sha1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadResult struct {
|
||||||
|
driver.BasicResp
|
||||||
|
Data struct {
|
||||||
|
PickCode string `json:"pick_code"`
|
||||||
|
FileSize int `json:"file_size"`
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
ThumbURL string `json:"thumb_url"`
|
||||||
|
Sha1 string `json:"sha1"`
|
||||||
|
Aid int `json:"aid"`
|
||||||
|
FileName string `json:"file_name"`
|
||||||
|
Cid string `json:"cid"`
|
||||||
|
IsVideo int `json:"is_video"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,57 @@
|
|||||||
package _115
|
package _115
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/SheltonZhu/115driver/pkg/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"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/http_range"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||||
|
|
||||||
|
cipher "github.com/SheltonZhu/115driver/pkg/crypto/ec115"
|
||||||
|
crypto "github.com/SheltonZhu/115driver/pkg/crypto/m115"
|
||||||
|
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var UserAgent = driver.UA115Desktop
|
// var UserAgent = driver115.UA115Browser
|
||||||
|
|
||||||
func (d *Pan115) login() error {
|
func (d *Pan115) login() error {
|
||||||
var err error
|
var err error
|
||||||
opts := []driver.Option{
|
opts := []driver115.Option{
|
||||||
driver.UA(UserAgent),
|
driver115.UA(d.getUA()),
|
||||||
func(c *driver.Pan115Client) {
|
func(c *driver115.Pan115Client) {
|
||||||
c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
|
c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
d.client = driver.New(opts...)
|
d.client = driver115.New(opts...)
|
||||||
cr := &driver.Credential{}
|
cr := &driver115.Credential{}
|
||||||
if d.Addition.QRCodeToken != "" {
|
if d.QRCodeToken != "" {
|
||||||
s := &driver.QRCodeSession{
|
s := &driver115.QRCodeSession{
|
||||||
UID: d.Addition.QRCodeToken,
|
UID: d.QRCodeToken,
|
||||||
}
|
}
|
||||||
if cr, err = d.client.QRCodeLogin(s); err != nil {
|
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
|
||||||
return errors.Wrap(err, "failed to login by qrcode")
|
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.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
|
||||||
d.Addition.QRCodeToken = ""
|
d.QRCodeToken = ""
|
||||||
} else if d.Addition.Cookie != "" {
|
} else if d.Cookie != "" {
|
||||||
if err = cr.FromCookie(d.Addition.Cookie); err != nil {
|
if err = cr.FromCookie(d.Cookie); err != nil {
|
||||||
return errors.Wrap(err, "failed to login by cookies")
|
return errors.Wrap(err, "failed to login by cookies")
|
||||||
}
|
}
|
||||||
d.client.ImportCredential(cr)
|
d.client.ImportCredential(cr)
|
||||||
@@ -41,17 +61,489 @@ func (d *Pan115) login() error {
|
|||||||
return d.client.LoginCheck()
|
return d.client.LoginCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan115) getFiles(fileId string) ([]driver.File, error) {
|
func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
|
||||||
res := make([]driver.File, 0)
|
res := make([]FileObj, 0)
|
||||||
if d.PageSize <= 0 {
|
if d.PageSize <= 0 {
|
||||||
d.PageSize = driver.FileListLimit
|
d.PageSize = driver115.FileListLimit
|
||||||
}
|
}
|
||||||
files, err := d.client.ListWithLimit(fileId, d.PageSize)
|
files, err := d.client.ListWithLimit(fileId, d.PageSize, driver115.WithMultiUrls())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, file := range *files {
|
for _, file := range *files {
|
||||||
res = append(res, file)
|
res = append(res, FileObj{file})
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) getNewFile(fileId string) (*FileObj, error) {
|
||||||
|
file, err := d.client.GetFile(fileId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &FileObj{*file}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) getNewFileByPickCode(pickCode string) (*FileObj, error) {
|
||||||
|
result := driver115.GetFileInfoResponse{}
|
||||||
|
req := d.client.NewRequest().
|
||||||
|
SetQueryParam("pick_code", pickCode).
|
||||||
|
ForceContentType("application/json;charset=UTF-8").
|
||||||
|
SetResult(&result)
|
||||||
|
resp, err := req.Get(driver115.ApiFileInfo)
|
||||||
|
if err := driver115.CheckErr(err, &result, resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(result.Files) == 0 {
|
||||||
|
return nil, errors.New("not get file info")
|
||||||
|
}
|
||||||
|
fileInfo := result.Files[0]
|
||||||
|
|
||||||
|
f := &FileObj{}
|
||||||
|
f.From(fileInfo)
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) getUA() string {
|
||||||
|
return fmt.Sprintf("Mozilla/5.0 115Browser/%s", appVer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
|
||||||
|
key := crypto.GenerateKey()
|
||||||
|
result := driver115.DownloadResp{}
|
||||||
|
params, err := utils.Json.Marshal(map[string]string{"pick_code": pickCode})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := crypto.Encode(params, key)
|
||||||
|
|
||||||
|
bodyReader := strings.NewReader(url.Values{"data": []string{data}}.Encode())
|
||||||
|
reqUrl := fmt.Sprintf("%s?t=%s", driver115.AndroidApiDownloadGetUrl, driver115.Now().String())
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader)
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("Cookie", d.Cookie)
|
||||||
|
req.Header.Set("User-Agent", ua)
|
||||||
|
|
||||||
|
resp, err := d.client.Client.GetClient().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := utils.Json.Unmarshal(body, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = result.Err(string(body)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := crypto.Decode(string(result.EncodedData), key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadInfo := struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}{}
|
||||||
|
if err := utils.Json.Unmarshal(b, &downloadInfo); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &driver115.DownloadInfo{}
|
||||||
|
info.PickCode = pickCode
|
||||||
|
info.Header = resp.Request.Header
|
||||||
|
info.Url.Url = downloadInfo.Url
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Pan115) GenerateToken(fileID, preID, timeStamp, fileSize, signKey, signVal string) string {
|
||||||
|
userID := strconv.FormatInt(c.client.UserID, 10)
|
||||||
|
userIDMd5 := md5.Sum([]byte(userID))
|
||||||
|
tokenMd5 := md5.Sum([]byte(md5Salt + fileID + fileSize + signKey + signVal + userID + timeStamp + hex.EncodeToString(userIDMd5[:]) + appVer))
|
||||||
|
return hex.EncodeToString(tokenMd5[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) {
|
||||||
|
var (
|
||||||
|
ecdhCipher *cipher.EcdhCipher
|
||||||
|
encrypted []byte
|
||||||
|
decrypted []byte
|
||||||
|
encodedToken string
|
||||||
|
err error
|
||||||
|
target = "U_1_" + dirID
|
||||||
|
bodyBytes []byte
|
||||||
|
result = driver115.UploadInitResp{}
|
||||||
|
fileSizeStr = strconv.FormatInt(fileSize, 10)
|
||||||
|
)
|
||||||
|
if ecdhCipher, err = cipher.NewEcdhCipher(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := strconv.FormatInt(d.client.UserID, 10)
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("appid", "0")
|
||||||
|
form.Set("appversion", appVer)
|
||||||
|
form.Set("userid", userID)
|
||||||
|
form.Set("filename", fileName)
|
||||||
|
form.Set("filesize", fileSizeStr)
|
||||||
|
form.Set("fileid", fileID)
|
||||||
|
form.Set("target", target)
|
||||||
|
form.Set("sig", d.client.GenerateSignature(fileID, target))
|
||||||
|
|
||||||
|
signKey, signVal := "", ""
|
||||||
|
for retry := true; retry; {
|
||||||
|
t := driver115.NowMilli()
|
||||||
|
|
||||||
|
if encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
params := map[string]string{
|
||||||
|
"k_ec": encodedToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
form.Set("t", t.String())
|
||||||
|
form.Set("token", d.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))
|
||||||
|
if signKey != "" && signVal != "" {
|
||||||
|
form.Set("sign_key", signKey)
|
||||||
|
form.Set("sign_val", signVal)
|
||||||
|
}
|
||||||
|
if encrypted, err = ecdhCipher.Encrypt([]byte(form.Encode())); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := d.client.NewRequest().
|
||||||
|
SetQueryParams(params).
|
||||||
|
SetBody(encrypted).
|
||||||
|
SetHeaderVerbatim("Content-Type", "application/x-www-form-urlencoded").
|
||||||
|
SetDoNotParseResponse(true)
|
||||||
|
resp, err := req.Post(driver115.ApiUploadInit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := resp.RawBody()
|
||||||
|
defer data.Close()
|
||||||
|
if bodyBytes, err = io.ReadAll(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if decrypted, err = ecdhCipher.Decrypt(bodyBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = driver115.CheckErr(json.Unmarshal(decrypted, &result), &result, resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result.Status == 7 {
|
||||||
|
// Update signKey & signVal
|
||||||
|
signKey = result.SignKey
|
||||||
|
signVal, err = UploadDigestRange(stream, result.SignCheck)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
retry = false
|
||||||
|
}
|
||||||
|
result.SHA1 = fileID
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result string, err error) {
|
||||||
|
var start, end int64
|
||||||
|
if _, err = fmt.Sscanf(rangeSpec, "%d-%d", &start, &end); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
length := end - start + 1
|
||||||
|
reader, err := stream.RangeRead(http_range.Range{Start: start, Length: length})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
hashStr, err := utils.HashReader(utils.SHA1, reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
result = strings.ToUpper(hashStr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadByOSS use aliyun sdk to upload
|
||||||
|
func (c *Pan115) UploadByOSS(ctx context.Context, params *driver115.UploadOSSParams, s model.FileStreamer, dirID string, up driver.UpdateProgress) (*UploadResult, error) {
|
||||||
|
ossToken, err := c.client.GetOSSToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ossClient, err := oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bucket, err := ossClient.Bucket(params.Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var bodyBytes []byte
|
||||||
|
r := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
|
||||||
|
Reader: s,
|
||||||
|
UpdateProgress: up,
|
||||||
|
})
|
||||||
|
if err = bucket.PutObject(params.Object, r, append(
|
||||||
|
driver115.OssOption(params, ossToken),
|
||||||
|
oss.CallbackResult(&bodyBytes),
|
||||||
|
)...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadResult UploadResult
|
||||||
|
if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &uploadResult, uploadResult.Err(string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadByMultipart upload by mutipart blocks
|
||||||
|
func (d *Pan115) UploadByMultipart(ctx context.Context, params *driver115.UploadOSSParams, fileSize int64, s model.FileStreamer,
|
||||||
|
dirID string, up driver.UpdateProgress, opts ...driver115.UploadMultipartOption) (*UploadResult, error) {
|
||||||
|
var (
|
||||||
|
chunks []oss.FileChunk
|
||||||
|
parts []oss.UploadPart
|
||||||
|
imur oss.InitiateMultipartUploadResult
|
||||||
|
ossClient *oss.Client
|
||||||
|
bucket *oss.Bucket
|
||||||
|
ossToken *driver115.UploadOSSTokenResp
|
||||||
|
bodyBytes []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
tmpF, err := s.CacheFullInTempFile()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := driver115.DefalutUploadMultipartOptions()
|
||||||
|
if len(opts) > 0 {
|
||||||
|
for _, f := range opts {
|
||||||
|
f(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// oss 启用Sequential必须按顺序上传
|
||||||
|
options.ThreadsNum = 1
|
||||||
|
|
||||||
|
if ossToken, err = d.client.GetOSSToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bucket, err = ossClient.Bucket(params.Bucket); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ossToken一小时后就会失效,所以每50分钟重新获取一次
|
||||||
|
ticker := time.NewTicker(options.TokenRefreshTime)
|
||||||
|
defer ticker.Stop()
|
||||||
|
// 设置超时
|
||||||
|
timeout := time.NewTimer(options.Timeout)
|
||||||
|
|
||||||
|
if chunks, err = SplitFile(fileSize); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if imur, err = bucket.InitiateMultipartUpload(params.Object,
|
||||||
|
oss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken),
|
||||||
|
oss.UserAgentHeader(driver115.OSSUserAgent),
|
||||||
|
oss.EnableSha1(), oss.Sequential(),
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(len(chunks))
|
||||||
|
|
||||||
|
chunksCh := make(chan oss.FileChunk)
|
||||||
|
errCh := make(chan error)
|
||||||
|
UploadedPartsCh := make(chan oss.UploadPart)
|
||||||
|
quit := make(chan struct{})
|
||||||
|
|
||||||
|
// producer
|
||||||
|
go chunksProducer(chunksCh, chunks)
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
quit <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
completedNum := atomic.Int32{}
|
||||||
|
// consumers
|
||||||
|
for i := 0; i < options.ThreadsNum; i++ {
|
||||||
|
go func(threadId int) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
errCh <- fmt.Errorf("recovered in %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for chunk := range chunksCh {
|
||||||
|
var part oss.UploadPart // 出现错误就继续尝试,共尝试3次
|
||||||
|
for retry := 0; retry < 3; retry++ {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
break
|
||||||
|
case <-ticker.C:
|
||||||
|
if ossToken, err = d.client.GetOSSToken(); err != nil { // 到时重新获取ossToken
|
||||||
|
errCh <- errors.Wrap(err, "刷新token时出现错误")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
buf := make([]byte, chunk.Size)
|
||||||
|
if _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if part, err = bucket.UploadPart(imur, driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(buf)),
|
||||||
|
chunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
errCh <- errors.Wrap(err, fmt.Sprintf("上传 %s 的第%d个分片时出现错误:%v", s.GetName(), chunk.Number, err))
|
||||||
|
} else {
|
||||||
|
num := completedNum.Add(1)
|
||||||
|
up(float64(num) * 100.0 / float64(len(chunks)))
|
||||||
|
}
|
||||||
|
UploadedPartsCh <- part
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for part := range UploadedPartsCh {
|
||||||
|
parts = append(parts, part)
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
LOOP:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
// 到时重新获取ossToken
|
||||||
|
if ossToken, err = d.client.GetOSSToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case <-quit:
|
||||||
|
break LOOP
|
||||||
|
case <-errCh:
|
||||||
|
return nil, err
|
||||||
|
case <-timeout.C:
|
||||||
|
return nil, fmt.Errorf("time out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不知道啥原因,oss那边分片上传不计算sha1,导致115服务器校验错误
|
||||||
|
// params.Callback.Callback = strings.ReplaceAll(params.Callback.Callback, "${sha1}", params.SHA1)
|
||||||
|
if _, err := bucket.CompleteMultipartUpload(imur, parts, append(
|
||||||
|
driver115.OssOption(params, ossToken),
|
||||||
|
oss.CallbackResult(&bodyBytes),
|
||||||
|
)...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadResult UploadResult
|
||||||
|
if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &uploadResult, uploadResult.Err(string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
|
||||||
|
for _, chunk := range chunks {
|
||||||
|
ch <- chunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {
|
||||||
|
for i := int64(1); i < 10; i++ {
|
||||||
|
if fileSize < i*utils.GB { // 文件大小小于iGB时分为i*1000片
|
||||||
|
if chunks, err = SplitFileByPartNum(fileSize, int(i*1000)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fileSize > 9*utils.GB { // 文件大小大于9GB时分为10000片
|
||||||
|
if chunks, err = SplitFileByPartNum(fileSize, 10000); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 单个分片大小不能小于100KB
|
||||||
|
if chunks[0].Size < 100*utils.KB {
|
||||||
|
if chunks, err = SplitFileByPartSize(fileSize, 100*utils.KB); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitFileByPartNum splits big file into parts by the num of parts.
|
||||||
|
// Split the file with specified parts count, returns the split result when error is nil.
|
||||||
|
func SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {
|
||||||
|
if chunkNum <= 0 || chunkNum > 10000 {
|
||||||
|
return nil, errors.New("chunkNum invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if int64(chunkNum) > fileSize {
|
||||||
|
return nil, errors.New("oss: chunkNum invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunks []oss.FileChunk
|
||||||
|
chunk := oss.FileChunk{}
|
||||||
|
chunkN := (int64)(chunkNum)
|
||||||
|
for i := int64(0); i < chunkN; i++ {
|
||||||
|
chunk.Number = int(i + 1)
|
||||||
|
chunk.Offset = i * (fileSize / chunkN)
|
||||||
|
if i == chunkN-1 {
|
||||||
|
chunk.Size = fileSize/chunkN + fileSize%chunkN
|
||||||
|
} else {
|
||||||
|
chunk.Size = fileSize / chunkN
|
||||||
|
}
|
||||||
|
chunks = append(chunks, chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitFileByPartSize splits big file into parts by the size of parts.
|
||||||
|
// Splits the file by the part size. Returns the FileChunk when error is nil.
|
||||||
|
func SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, error) {
|
||||||
|
if chunkSize <= 0 {
|
||||||
|
return nil, errors.New("chunkSize invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkN := fileSize / chunkSize
|
||||||
|
if chunkN >= 10000 {
|
||||||
|
return nil, errors.New("Too many parts, please increase part size")
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunks []oss.FileChunk
|
||||||
|
chunk := oss.FileChunk{}
|
||||||
|
for i := int64(0); i < chunkN; i++ {
|
||||||
|
chunk.Number = int(i + 1)
|
||||||
|
chunk.Offset = i * chunkSize
|
||||||
|
chunk.Size = chunkSize
|
||||||
|
chunks = append(chunks, chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileSize%chunkSize > 0 {
|
||||||
|
chunk.Number = len(chunks) + 1
|
||||||
|
chunk.Offset = int64(len(chunks)) * chunkSize
|
||||||
|
chunk.Size = fileSize % chunkSize
|
||||||
|
chunks = append(chunks, chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks, nil
|
||||||
|
}
|
||||||
|
|||||||
299
drivers/115_open/driver.go
Normal file
299
drivers/115_open/driver.go
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
package _115_open
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/cmd/flags"
|
||||||
|
"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/internal/op"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
sdk "github.com/xhofe/115-sdk-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Open115 struct {
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
client *sdk.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) GetAddition() driver.Additional {
|
||||||
|
return &d.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) Init(ctx context.Context) error {
|
||||||
|
d.client = sdk.New(sdk.WithRefreshToken(d.Addition.RefreshToken),
|
||||||
|
sdk.WithAccessToken(d.Addition.AccessToken),
|
||||||
|
sdk.WithOnRefreshToken(func(s1, s2 string) {
|
||||||
|
d.Addition.AccessToken = s1
|
||||||
|
d.Addition.RefreshToken = s2
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
}))
|
||||||
|
if flags.Debug || flags.Dev {
|
||||||
|
d.client.SetDebug(true)
|
||||||
|
}
|
||||||
|
_, err := d.client.UserInfo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
var res []model.Obj
|
||||||
|
pageSize := int64(200)
|
||||||
|
offset := int64(0)
|
||||||
|
for {
|
||||||
|
resp, err := d.client.GetFiles(ctx, &sdk.GetFilesReq{
|
||||||
|
CID: dir.GetID(),
|
||||||
|
Limit: pageSize,
|
||||||
|
Offset: offset,
|
||||||
|
ASC: d.Addition.OrderDirection == "asc",
|
||||||
|
O: d.Addition.OrderBy,
|
||||||
|
// Cur: 1,
|
||||||
|
ShowDir: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res = append(res, utils.MustSliceConvert(resp.Data, func(src sdk.GetFilesResp_File) model.Obj {
|
||||||
|
obj := Obj(src)
|
||||||
|
return &obj
|
||||||
|
})...)
|
||||||
|
if len(res) >= int(resp.Count) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset += pageSize
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
var ua string
|
||||||
|
if args.Header != nil {
|
||||||
|
ua = args.Header.Get("User-Agent")
|
||||||
|
}
|
||||||
|
if ua == "" {
|
||||||
|
ua = base.UserAgent
|
||||||
|
}
|
||||||
|
obj, ok := file.(*Obj)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("can't convert obj")
|
||||||
|
}
|
||||||
|
pc := obj.Pc
|
||||||
|
resp, err := d.client.DownURL(ctx, pc, ua)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u, ok := resp[obj.GetID()]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("can't get link")
|
||||||
|
}
|
||||||
|
return &model.Link{
|
||||||
|
URL: u.URL.URL,
|
||||||
|
Header: http.Header{
|
||||||
|
"User-Agent": []string{ua},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
|
resp, err := d.client.Mkdir(ctx, parentDir.GetID(), dirName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Obj{
|
||||||
|
Fid: resp.FileID,
|
||||||
|
Pid: parentDir.GetID(),
|
||||||
|
Fn: dirName,
|
||||||
|
Fc: "0",
|
||||||
|
Upt: time.Now().Unix(),
|
||||||
|
Uet: time.Now().Unix(),
|
||||||
|
UpPt: time.Now().Unix(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
_, err := d.client.Move(ctx, &sdk.MoveReq{
|
||||||
|
FileIDs: srcObj.GetID(),
|
||||||
|
ToCid: dstDir.GetID(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return srcObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
|
_, err := d.client.UpdateFile(ctx, &sdk.UpdateFileReq{
|
||||||
|
FileID: srcObj.GetID(),
|
||||||
|
FileNma: newName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
obj, ok := srcObj.(*Obj)
|
||||||
|
if ok {
|
||||||
|
obj.Fn = newName
|
||||||
|
}
|
||||||
|
return srcObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
_, err := d.client.Copy(ctx, &sdk.CopyReq{
|
||||||
|
PID: dstDir.GetID(),
|
||||||
|
FileID: srcObj.GetID(),
|
||||||
|
NoDupli: "1",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return srcObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
_obj, ok := obj.(*Obj)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("can't convert obj")
|
||||||
|
}
|
||||||
|
_, err := d.client.DelFile(ctx, &sdk.DelFileReq{
|
||||||
|
FileIDs: _obj.GetID(),
|
||||||
|
ParentID: _obj.Pid,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
|
tempF, err := file.CacheFullInTempFile()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// cal full sha1
|
||||||
|
sha1, err := utils.HashReader(utils.SHA1, tempF)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tempF.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// pre 128k sha1
|
||||||
|
sha1128k, err := utils.HashReader(utils.SHA1, io.LimitReader(tempF, 128*1024))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tempF.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 1. Init
|
||||||
|
resp, err := d.client.UploadInit(ctx, &sdk.UploadInitReq{
|
||||||
|
FileName: file.GetName(),
|
||||||
|
FileSize: file.GetSize(),
|
||||||
|
Target: dstDir.GetID(),
|
||||||
|
FileID: strings.ToUpper(sha1),
|
||||||
|
PreID: strings.ToUpper(sha1128k),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.Status == 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// 2. two way verify
|
||||||
|
if utils.SliceContains([]int{6, 7, 8}, resp.Status) {
|
||||||
|
signCheck := strings.Split(resp.SignCheck, "-") //"sign_check": "2392148-2392298" 取2392148-2392298之间的内容(包含2392148、2392298)的sha1
|
||||||
|
start, err := strconv.ParseInt(signCheck[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
end, err := strconv.ParseInt(signCheck[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tempF.Seek(start, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
signVal, err := utils.HashReader(utils.SHA1, io.LimitReader(tempF, end-start+1))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tempF.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err = d.client.UploadInit(ctx, &sdk.UploadInitReq{
|
||||||
|
FileName: file.GetName(),
|
||||||
|
FileSize: file.GetSize(),
|
||||||
|
Target: dstDir.GetID(),
|
||||||
|
FileID: strings.ToUpper(sha1),
|
||||||
|
PreID: strings.ToUpper(sha1128k),
|
||||||
|
SignKey: resp.SignKey,
|
||||||
|
SignVal: strings.ToUpper(signVal),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.Status == 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. get upload token
|
||||||
|
tokenResp, err := d.client.UploadGetToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 4. upload
|
||||||
|
err = d.multpartUpload(ctx, tempF, file, up, tokenResp, resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||||
|
// // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
|
||||||
|
// return nil, errs.NotImplement
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (d *Open115) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||||
|
// // TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
|
||||||
|
// return nil, errs.NotImplement
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (d *Open115) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||||
|
// // TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
|
||||||
|
// return nil, errs.NotImplement
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (d *Open115) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
|
||||||
|
// // TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional
|
||||||
|
// // a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir
|
||||||
|
// // return errs.NotImplement to use an internal archive tool
|
||||||
|
// return nil, errs.NotImplement
|
||||||
|
// }
|
||||||
|
|
||||||
|
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||||
|
// return nil, errs.NotSupport
|
||||||
|
//}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*Open115)(nil)
|
||||||
36
drivers/115_open/meta.go
Normal file
36
drivers/115_open/meta.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package _115_open
|
||||||
|
|
||||||
|
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.RootID
|
||||||
|
// define other
|
||||||
|
RefreshToken string `json:"refresh_token" required:"true"`
|
||||||
|
OrderBy string `json:"order_by" type:"select" options:"file_name,file_size,user_utime,file_type"`
|
||||||
|
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"`
|
||||||
|
AccessToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "115 Open",
|
||||||
|
LocalSort: false,
|
||||||
|
OnlyLocal: false,
|
||||||
|
OnlyProxy: false,
|
||||||
|
NoCache: false,
|
||||||
|
NoUpload: false,
|
||||||
|
NeedMs: false,
|
||||||
|
DefaultRoot: "0",
|
||||||
|
CheckStatus: false,
|
||||||
|
Alert: "",
|
||||||
|
NoOverwriteUpload: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(func() driver.Driver {
|
||||||
|
return &Open115{}
|
||||||
|
})
|
||||||
|
}
|
||||||
59
drivers/115_open/types.go
Normal file
59
drivers/115_open/types.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package _115_open
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
sdk "github.com/xhofe/115-sdk-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Obj sdk.GetFilesResp_File
|
||||||
|
|
||||||
|
// Thumb implements model.Thumb.
|
||||||
|
func (o *Obj) Thumb() string {
|
||||||
|
return o.Thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTime implements model.Obj.
|
||||||
|
func (o *Obj) CreateTime() time.Time {
|
||||||
|
return time.Unix(o.UpPt, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHash implements model.Obj.
|
||||||
|
func (o *Obj) GetHash() utils.HashInfo {
|
||||||
|
return utils.NewHashInfo(utils.SHA1, o.Sha1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID implements model.Obj.
|
||||||
|
func (o *Obj) GetID() string {
|
||||||
|
return o.Fid
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName implements model.Obj.
|
||||||
|
func (o *Obj) GetName() string {
|
||||||
|
return o.Fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPath implements model.Obj.
|
||||||
|
func (o *Obj) GetPath() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize implements model.Obj.
|
||||||
|
func (o *Obj) GetSize() int64 {
|
||||||
|
return o.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir implements model.Obj.
|
||||||
|
func (o *Obj) IsDir() bool {
|
||||||
|
return o.Fc == "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModTime implements model.Obj.
|
||||||
|
func (o *Obj) ModTime() time.Time {
|
||||||
|
return time.Unix(o.Upt, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ model.Obj = (*Obj)(nil)
|
||||||
|
var _ model.Thumb = (*Obj)(nil)
|
||||||
140
drivers/115_open/upload.go
Normal file
140
drivers/115_open/upload.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package _115_open
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"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/aliyun/aliyun-oss-go-sdk/oss"
|
||||||
|
"github.com/avast/retry-go"
|
||||||
|
sdk "github.com/xhofe/115-sdk-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func calPartSize(fileSize int64) int64 {
|
||||||
|
var partSize int64 = 20 * utils.MB
|
||||||
|
if fileSize > partSize {
|
||||||
|
if fileSize > 1*utils.TB { // file Size over 1TB
|
||||||
|
partSize = 5 * utils.GB // file part size 5GB
|
||||||
|
} else if fileSize > 768*utils.GB { // over 768GB
|
||||||
|
partSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part
|
||||||
|
} else if fileSize > 512*utils.GB { // over 512GB
|
||||||
|
partSize = 82463373 // ≈ 78.6432MB
|
||||||
|
} else if fileSize > 384*utils.GB { // over 384GB
|
||||||
|
partSize = 54975582 // ≈ 52.4288MB
|
||||||
|
} else if fileSize > 256*utils.GB { // over 256GB
|
||||||
|
partSize = 41231687 // ≈ 39.3216MB
|
||||||
|
} else if fileSize > 128*utils.GB { // over 128GB
|
||||||
|
partSize = 27487791 // ≈ 26.2144MB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return partSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open115) singleUpload(ctx context.Context, tempF model.File, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {
|
||||||
|
ossClient, err := oss.New(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bucket, err := ossClient.Bucket(initResp.Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bucket.PutObject(initResp.Object, tempF,
|
||||||
|
oss.Callback(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.Callback))),
|
||||||
|
oss.CallbackVar(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.CallbackVar))),
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// type CallbackResult struct {
|
||||||
|
// State bool `json:"state"`
|
||||||
|
// Code int `json:"code"`
|
||||||
|
// Message string `json:"message"`
|
||||||
|
// Data struct {
|
||||||
|
// PickCode string `json:"pick_code"`
|
||||||
|
// FileName string `json:"file_name"`
|
||||||
|
// FileSize int64 `json:"file_size"`
|
||||||
|
// FileID string `json:"file_id"`
|
||||||
|
// ThumbURL string `json:"thumb_url"`
|
||||||
|
// Sha1 string `json:"sha1"`
|
||||||
|
// Aid int `json:"aid"`
|
||||||
|
// Cid string `json:"cid"`
|
||||||
|
// } `json:"data"`
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (d *Open115) multpartUpload(ctx context.Context, tempF model.File, stream model.FileStreamer, up driver.UpdateProgress, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {
|
||||||
|
fileSize := stream.GetSize()
|
||||||
|
chunkSize := calPartSize(fileSize)
|
||||||
|
|
||||||
|
ossClient, err := oss.New(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bucket, err := ossClient.Bucket(initResp.Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
imur, err := bucket.InitiateMultipartUpload(initResp.Object, oss.Sequential())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
partNum := (stream.GetSize() + chunkSize - 1) / chunkSize
|
||||||
|
parts := make([]oss.UploadPart, partNum)
|
||||||
|
offset := int64(0)
|
||||||
|
for i := int64(1); i <= partNum; i++ {
|
||||||
|
if utils.IsCanceled(ctx) {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
partSize := chunkSize
|
||||||
|
if i == partNum {
|
||||||
|
partSize = fileSize - (i-1)*chunkSize
|
||||||
|
}
|
||||||
|
rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
|
||||||
|
err = retry.Do(func() error {
|
||||||
|
_ = rd.Reset()
|
||||||
|
rateLimitedRd := driver.NewLimitedUploadStream(ctx, rd)
|
||||||
|
part, err := bucket.UploadPart(imur, rateLimitedRd, partSize, int(i))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parts[i-1] = part
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
retry.Attempts(3),
|
||||||
|
retry.DelayType(retry.BackOffDelay),
|
||||||
|
retry.Delay(time.Second))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == partNum {
|
||||||
|
offset = fileSize
|
||||||
|
} else {
|
||||||
|
offset += partSize
|
||||||
|
}
|
||||||
|
up(float64(offset) / float64(fileSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
// callbackRespBytes := make([]byte, 1024)
|
||||||
|
_, err = bucket.CompleteMultipartUpload(
|
||||||
|
imur,
|
||||||
|
parts,
|
||||||
|
oss.Callback(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.Callback))),
|
||||||
|
oss.CallbackVar(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.CallbackVar))),
|
||||||
|
// oss.CallbackResult(&callbackRespBytes),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
3
drivers/115_open/util.go
Normal file
3
drivers/115_open/util.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package _115_open
|
||||||
|
|
||||||
|
// do others that not defined in Driver interface
|
||||||
112
drivers/115_share/driver.go
Normal file
112
drivers/115_share/driver.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package _115_share
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pan115Share struct {
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
client *driver115.Pan115Client
|
||||||
|
limiter *rate.Limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) GetAddition() driver.Additional {
|
||||||
|
return &d.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) Init(ctx context.Context) error {
|
||||||
|
if d.LimitRate > 0 {
|
||||||
|
d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.login()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) WaitLimit(ctx context.Context) error {
|
||||||
|
if d.limiter != nil {
|
||||||
|
return d.limiter.Wait(ctx)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
files := make([]driver115.ShareFile, 0)
|
||||||
|
fileResp, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, dir.GetID(), driver115.QueryLimit(int(d.PageSize)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files = append(files, fileResp.Data.List...)
|
||||||
|
total := fileResp.Data.Count
|
||||||
|
count := len(fileResp.Data.List)
|
||||||
|
for total > count {
|
||||||
|
fileResp, err := d.client.GetShareSnap(
|
||||||
|
d.ShareCode, d.ReceiveCode, dir.GetID(),
|
||||||
|
driver115.QueryLimit(int(d.PageSize)), driver115.QueryOffset(count),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files = append(files, fileResp.Data.List...)
|
||||||
|
count += len(fileResp.Data.List)
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.SliceConvert(files, transFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
downloadInfo, err := d.client.DownloadByShareCode(d.ShareCode, d.ReceiveCode, file.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Link{URL: downloadInfo.URL.URL}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
|
return errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
return errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
|
return errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
return errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
return errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115Share) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
|
return errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*Pan115Share)(nil)
|
||||||
34
drivers/115_share/meta.go
Normal file
34
drivers/115_share/meta.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package _115_share
|
||||||
|
|
||||||
|
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"`
|
||||||
|
QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,tv,alipaymini,wechatmini,qandroid" default:"linux" help:"select the QR code device, default linux"`
|
||||||
|
PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
|
||||||
|
LimitRate float64 `json:"limit_rate" type:"float" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
|
||||||
|
ShareCode string `json:"share_code" type:"text" required:"true" help:"share code of 115 share link"`
|
||||||
|
ReceiveCode string `json:"receive_code" type:"text" required:"true" help:"receive code of 115 share link"`
|
||||||
|
driver.RootID
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "115 Share",
|
||||||
|
DefaultRoot: "0",
|
||||||
|
// OnlyProxy: true,
|
||||||
|
// OnlyLocal: true,
|
||||||
|
CheckStatus: false,
|
||||||
|
Alert: "",
|
||||||
|
NoOverwriteUpload: true,
|
||||||
|
NoUpload: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(func() driver.Driver {
|
||||||
|
return &Pan115Share{}
|
||||||
|
})
|
||||||
|
}
|
||||||
111
drivers/115_share/utils.go
Normal file
111
drivers/115_share/utils.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package _115_share
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ model.Obj = (*FileObj)(nil)
|
||||||
|
|
||||||
|
type FileObj struct {
|
||||||
|
Size int64
|
||||||
|
Sha1 string
|
||||||
|
Utm time.Time
|
||||||
|
FileName string
|
||||||
|
isDir bool
|
||||||
|
FileID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileObj) CreateTime() time.Time {
|
||||||
|
return f.Utm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileObj) GetHash() utils.HashInfo {
|
||||||
|
return utils.NewHashInfo(utils.SHA1, f.Sha1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileObj) GetSize() int64 {
|
||||||
|
return f.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileObj) GetName() string {
|
||||||
|
return f.FileName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileObj) ModTime() time.Time {
|
||||||
|
return f.Utm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileObj) IsDir() bool {
|
||||||
|
return f.isDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileObj) GetID() string {
|
||||||
|
return f.FileID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileObj) GetPath() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func transFunc(sf driver115.ShareFile) (model.Obj, error) {
|
||||||
|
timeInt, err := strconv.ParseInt(sf.UpdateTime, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
utm = time.Unix(timeInt, 0)
|
||||||
|
isDir = (sf.IsFile == 0)
|
||||||
|
fileID = string(sf.FileID)
|
||||||
|
)
|
||||||
|
if isDir {
|
||||||
|
fileID = string(sf.CategoryID)
|
||||||
|
}
|
||||||
|
return &FileObj{
|
||||||
|
Size: int64(sf.Size),
|
||||||
|
Sha1: sf.Sha1,
|
||||||
|
Utm: utm,
|
||||||
|
FileName: string(sf.FileName),
|
||||||
|
isDir: isDir,
|
||||||
|
FileID: fileID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var UserAgent = driver115.UA115Browser
|
||||||
|
|
||||||
|
func (d *Pan115Share) login() error {
|
||||||
|
var err error
|
||||||
|
opts := []driver115.Option{
|
||||||
|
driver115.UA(UserAgent),
|
||||||
|
}
|
||||||
|
d.client = driver115.New(opts...)
|
||||||
|
if _, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, ""); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to get share snap")
|
||||||
|
}
|
||||||
|
cr := &driver115.Credential{}
|
||||||
|
if d.QRCodeToken != "" {
|
||||||
|
s := &driver115.QRCodeSession{
|
||||||
|
UID: d.QRCodeToken,
|
||||||
|
}
|
||||||
|
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to login by qrcode")
|
||||||
|
}
|
||||||
|
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
|
||||||
|
d.QRCodeToken = ""
|
||||||
|
} else if d.Cookie != "" {
|
||||||
|
if err = cr.FromCookie(d.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()
|
||||||
|
}
|
||||||
@@ -9,7 +9,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
@@ -27,6 +30,7 @@ import (
|
|||||||
type Pan123 struct {
|
type Pan123 struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
|
apiRateLimit sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) Config() driver.Config {
|
func (d *Pan123) Config() driver.Config {
|
||||||
@@ -38,19 +42,19 @@ func (d *Pan123) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) Init(ctx context.Context) error {
|
func (d *Pan123) Init(ctx context.Context) error {
|
||||||
_, err := d.request(UserInfo, http.MethodGet, nil, nil)
|
_, err := d.Request(UserInfo, http.MethodGet, nil, nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) Drop(ctx context.Context) error {
|
func (d *Pan123) Drop(ctx context.Context) error {
|
||||||
_, _ = d.request(Logout, http.MethodPost, func(req *resty.Request) {
|
_, _ = d.Request(Logout, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{})
|
req.SetBody(base.Json{})
|
||||||
}, nil)
|
}, nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
files, err := d.getFiles(dir.GetID())
|
files, err := d.getFiles(ctx, dir.GetID(), dir.GetName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -78,7 +82,8 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
|||||||
"size": f.Size,
|
"size": f.Size,
|
||||||
"type": f.Type,
|
"type": f.Type,
|
||||||
}
|
}
|
||||||
resp, err := d.request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
|
resp, err := d.Request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
|
||||||
|
|
||||||
req.SetBody(data).SetHeaders(headers)
|
req.SetBody(data).SetHeaders(headers)
|
||||||
}, nil)
|
}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -131,7 +136,7 @@ func (d *Pan123) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
|
|||||||
"size": 0,
|
"size": 0,
|
||||||
"type": 1,
|
"type": 1,
|
||||||
}
|
}
|
||||||
_, err := d.request(Mkdir, http.MethodPost, func(req *resty.Request) {
|
_, err := d.Request(Mkdir, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data)
|
req.SetBody(data)
|
||||||
}, nil)
|
}, nil)
|
||||||
return err
|
return err
|
||||||
@@ -142,7 +147,7 @@ func (d *Pan123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
"fileIdList": []base.Json{{"FileId": srcObj.GetID()}},
|
"fileIdList": []base.Json{{"FileId": srcObj.GetID()}},
|
||||||
"parentFileId": dstDir.GetID(),
|
"parentFileId": dstDir.GetID(),
|
||||||
}
|
}
|
||||||
_, err := d.request(Move, http.MethodPost, func(req *resty.Request) {
|
_, err := d.Request(Move, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data)
|
req.SetBody(data)
|
||||||
}, nil)
|
}, nil)
|
||||||
return err
|
return err
|
||||||
@@ -154,7 +159,7 @@ func (d *Pan123) Rename(ctx context.Context, srcObj model.Obj, newName string) e
|
|||||||
"fileId": srcObj.GetID(),
|
"fileId": srcObj.GetID(),
|
||||||
"fileName": newName,
|
"fileName": newName,
|
||||||
}
|
}
|
||||||
_, err := d.request(Rename, http.MethodPost, func(req *resty.Request) {
|
_, err := d.Request(Rename, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data)
|
req.SetBody(data)
|
||||||
}, nil)
|
}, nil)
|
||||||
return err
|
return err
|
||||||
@@ -171,7 +176,7 @@ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
"operation": true,
|
"operation": true,
|
||||||
"fileTrashInfoList": []File{f},
|
"fileTrashInfoList": []File{f},
|
||||||
}
|
}
|
||||||
_, err := d.request(Trash, http.MethodPost, func(req *resty.Request) {
|
_, err := d.Request(Trash, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data)
|
req.SetBody(data)
|
||||||
}, nil)
|
}, nil)
|
||||||
return err
|
return err
|
||||||
@@ -180,37 +185,39 @@ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
// const DEFAULT int64 = 10485760
|
etag := file.GetHash().GetHash(utils.MD5)
|
||||||
h := md5.New()
|
if len(etag) < utils.MD5.Width {
|
||||||
// need to calculate md5 of the full content
|
// const DEFAULT int64 = 10485760
|
||||||
tempFile, err := utils.CreateTempFile(stream.GetReadCloser(), stream.GetSize())
|
h := md5.New()
|
||||||
if err != nil {
|
// need to calculate md5 of the full content
|
||||||
return err
|
tempFile, err := file.CacheFullInTempFile()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tempFile.Close()
|
||||||
|
}()
|
||||||
|
if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tempFile.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
etag = hex.EncodeToString(h.Sum(nil))
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
_ = tempFile.Close()
|
|
||||||
_ = os.Remove(tempFile.Name())
|
|
||||||
}()
|
|
||||||
if _, err = io.Copy(h, tempFile); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tempFile.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
etag := hex.EncodeToString(h.Sum(nil))
|
|
||||||
data := base.Json{
|
data := base.Json{
|
||||||
"driveId": 0,
|
"driveId": 0,
|
||||||
"duplicate": 2, // 2->覆盖 1->重命名 0->默认
|
"duplicate": 2, // 2->覆盖 1->重命名 0->默认
|
||||||
"etag": etag,
|
"etag": etag,
|
||||||
"fileName": stream.GetName(),
|
"fileName": file.GetName(),
|
||||||
"parentFileId": dstDir.GetID(),
|
"parentFileId": dstDir.GetID(),
|
||||||
"size": stream.GetSize(),
|
"size": file.GetSize(),
|
||||||
"type": 0,
|
"type": 0,
|
||||||
}
|
}
|
||||||
var resp UploadResp
|
var resp UploadResp
|
||||||
res, err := d.request(UploadRequest, http.MethodPost, func(req *resty.Request) {
|
res, err := d.Request(UploadRequest, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data).SetContext(ctx)
|
req.SetBody(data).SetContext(ctx)
|
||||||
}, &resp)
|
}, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -221,7 +228,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" {
|
if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" {
|
||||||
err = d.newUpload(ctx, &resp, stream, tempFile, up)
|
err = d.newUpload(ctx, &resp, file, up)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
cfg := &aws.Config{
|
cfg := &aws.Config{
|
||||||
@@ -235,17 +242,23 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
uploader := s3manager.NewUploader(s)
|
uploader := s3manager.NewUploader(s)
|
||||||
|
if file.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
||||||
|
uploader.PartSize = file.GetSize() / (s3manager.MaxUploadParts - 1)
|
||||||
|
}
|
||||||
input := &s3manager.UploadInput{
|
input := &s3manager.UploadInput{
|
||||||
Bucket: &resp.Data.Bucket,
|
Bucket: &resp.Data.Bucket,
|
||||||
Key: &resp.Data.Key,
|
Key: &resp.Data.Key,
|
||||||
Body: tempFile,
|
Body: driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
|
||||||
|
Reader: file,
|
||||||
|
UpdateProgress: up,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
_, err = uploader.UploadWithContext(ctx, input)
|
_, err = uploader.UploadWithContext(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
_, err = d.Request(UploadComplete, http.MethodPost, func(req *resty.Request) {
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = d.request(UploadComplete, http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"fileId": resp.Data.FileId,
|
"fileId": resp.Data.FileId,
|
||||||
}).SetContext(ctx)
|
}).SetContext(ctx)
|
||||||
@@ -253,4 +266,12 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Pan123) APIRateLimit(ctx context.Context, api string) error {
|
||||||
|
value, _ := d.apiRateLimit.LoadOrStore(api,
|
||||||
|
rate.NewLimiter(rate.Every(700*time.Millisecond), 1))
|
||||||
|
limiter := value.(*rate.Limiter)
|
||||||
|
|
||||||
|
return limiter.Wait(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*Pan123)(nil)
|
var _ driver.Driver = (*Pan123)(nil)
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ type Addition struct {
|
|||||||
Username string `json:"username" required:"true"`
|
Username string `json:"username" required:"true"`
|
||||||
Password string `json:"password" required:"true"`
|
Password string `json:"password" required:"true"`
|
||||||
driver.RootID
|
driver.RootID
|
||||||
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
|
//OrderBy string `json:"order_by" type:"select" options:"file_id,file_name,size,update_at" default:"file_name"`
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
AccessToken string
|
AccessToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "123Pan",
|
Name: "123Pan",
|
||||||
DefaultRoot: "0",
|
DefaultRoot: "0",
|
||||||
|
LocalSort: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package _123
|
package _123
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -21,6 +22,14 @@ type File struct {
|
|||||||
DownloadUrl string `json:"DownloadUrl"`
|
DownloadUrl string `json:"DownloadUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f File) CreateTime() time.Time {
|
||||||
|
return f.UpdateAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) GetHash() utils.HashInfo {
|
||||||
|
return utils.HashInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
func (f File) GetPath() string {
|
func (f File) GetPath() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -78,8 +87,9 @@ var _ model.Thumb = (*File)(nil)
|
|||||||
type Files struct {
|
type Files struct {
|
||||||
//BaseResp
|
//BaseResp
|
||||||
Data struct {
|
Data struct {
|
||||||
InfoList []File `json:"InfoList"`
|
|
||||||
Next string `json:"Next"`
|
Next string `json:"Next"`
|
||||||
|
Total int `json:"Total"`
|
||||||
|
InfoList []File `json:"InfoList"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (d *Pan123) getS3PreSignedUrls(ctx context.Context, upReq *UploadResp, star
|
|||||||
"StorageNode": upReq.Data.StorageNode,
|
"StorageNode": upReq.Data.StorageNode,
|
||||||
}
|
}
|
||||||
var s3PreSignedUrls S3PreSignedURLs
|
var s3PreSignedUrls S3PreSignedURLs
|
||||||
_, err := d.request(S3PreSignedUrls, http.MethodPost, func(req *resty.Request) {
|
_, err := d.Request(S3PreSignedUrls, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data).SetContext(ctx)
|
req.SetBody(data).SetContext(ctx)
|
||||||
}, &s3PreSignedUrls)
|
}, &s3PreSignedUrls)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -44,7 +44,7 @@ func (d *Pan123) getS3Auth(ctx context.Context, upReq *UploadResp, start, end in
|
|||||||
"uploadId": upReq.Data.UploadId,
|
"uploadId": upReq.Data.UploadId,
|
||||||
}
|
}
|
||||||
var s3PreSignedUrls S3PreSignedURLs
|
var s3PreSignedUrls S3PreSignedURLs
|
||||||
_, err := d.request(S3Auth, http.MethodPost, func(req *resty.Request) {
|
_, err := d.Request(S3Auth, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data).SetContext(ctx)
|
req.SetBody(data).SetContext(ctx)
|
||||||
}, &s3PreSignedUrls)
|
}, &s3PreSignedUrls)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,13 +63,13 @@ func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp, file model.F
|
|||||||
"key": upReq.Data.Key,
|
"key": upReq.Data.Key,
|
||||||
"uploadId": upReq.Data.UploadId,
|
"uploadId": upReq.Data.UploadId,
|
||||||
}
|
}
|
||||||
_, err := d.request(UploadCompleteV2, http.MethodPost, func(req *resty.Request) {
|
_, err := d.Request(UploadCompleteV2, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data).SetContext(ctx)
|
req.SetBody(data).SetContext(ctx)
|
||||||
}, nil)
|
}, nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, reader io.Reader, up driver.UpdateProgress) error {
|
func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
chunkSize := int64(1024 * 1024 * 16)
|
chunkSize := int64(1024 * 1024 * 16)
|
||||||
// fetch s3 pre signed urls
|
// fetch s3 pre signed urls
|
||||||
chunkCount := int(math.Ceil(float64(file.GetSize()) / float64(chunkSize)))
|
chunkCount := int(math.Ceil(float64(file.GetSize()) / float64(chunkSize)))
|
||||||
@@ -81,6 +81,7 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
|
|||||||
batchSize = 10
|
batchSize = 10
|
||||||
getS3UploadUrl = d.getS3PreSignedUrls
|
getS3UploadUrl = d.getS3PreSignedUrls
|
||||||
}
|
}
|
||||||
|
limited := driver.NewLimitedUploadStream(ctx, file)
|
||||||
for i := 1; i <= chunkCount; i += batchSize {
|
for i := 1; i <= chunkCount; i += batchSize {
|
||||||
if utils.IsCanceled(ctx) {
|
if utils.IsCanceled(ctx) {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
@@ -103,11 +104,11 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
|
|||||||
if j == chunkCount {
|
if j == chunkCount {
|
||||||
curSize = file.GetSize() - (int64(chunkCount)-1)*chunkSize
|
curSize = file.GetSize() - (int64(chunkCount)-1)*chunkSize
|
||||||
}
|
}
|
||||||
err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.LimitReader(reader, chunkSize), curSize, false, getS3UploadUrl)
|
err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.LimitReader(limited, chunkSize), curSize, false, getS3UploadUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
up(j * 100 / chunkCount)
|
up(float64(j) * 100 / float64(chunkCount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// complete s3 upload
|
// complete s3 upload
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
package _123
|
package _123
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
// do others that not defined in Driver interface
|
||||||
@@ -18,8 +26,9 @@ const (
|
|||||||
Api = "https://www.123pan.com/api"
|
Api = "https://www.123pan.com/api"
|
||||||
AApi = "https://www.123pan.com/a/api"
|
AApi = "https://www.123pan.com/a/api"
|
||||||
BApi = "https://www.123pan.com/b/api"
|
BApi = "https://www.123pan.com/b/api"
|
||||||
MainApi = Api
|
LoginApi = "https://login.123pan.com/api"
|
||||||
SignIn = MainApi + "/user/sign_in"
|
MainApi = BApi
|
||||||
|
SignIn = LoginApi + "/user/sign_in"
|
||||||
Logout = MainApi + "/user/logout"
|
Logout = MainApi + "/user/logout"
|
||||||
UserInfo = MainApi + "/user/info"
|
UserInfo = MainApi + "/user/info"
|
||||||
FileList = MainApi + "/file/list/new"
|
FileList = MainApi + "/file/list/new"
|
||||||
@@ -37,6 +46,104 @@ const (
|
|||||||
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
|
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func signPath(path string, os string, version string) (k string, v string) {
|
||||||
|
table := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}
|
||||||
|
random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
|
||||||
|
now := time.Now().In(time.FixedZone("CST", 8*3600))
|
||||||
|
timestamp := fmt.Sprint(now.Unix())
|
||||||
|
nowStr := []byte(now.Format("200601021504"))
|
||||||
|
for i := 0; i < len(nowStr); i++ {
|
||||||
|
nowStr[i] = table[nowStr[i]-48]
|
||||||
|
}
|
||||||
|
timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
|
||||||
|
data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
|
||||||
|
dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
|
||||||
|
return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetApi(rawUrl string) string {
|
||||||
|
u, _ := url.Parse(rawUrl)
|
||||||
|
query := u.Query()
|
||||||
|
query.Add(signPath(u.Path, "web", "3"))
|
||||||
|
u.RawQuery = query.Encode()
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
//func GetApi(url string) string {
|
||||||
|
// vm := js.New()
|
||||||
|
// vm.Set("url", url[22:])
|
||||||
|
// r, err := vm.RunString(`
|
||||||
|
// (function(e){
|
||||||
|
// function A(t, e) {
|
||||||
|
// e = 1 < arguments.length && void 0 !== e ? e : 10;
|
||||||
|
// for (var n = function() {
|
||||||
|
// for (var t = [], e = 0; e < 256; e++) {
|
||||||
|
// for (var n = e, r = 0; r < 8; r++)
|
||||||
|
// n = 1 & n ? 3988292384 ^ n >>> 1 : n >>> 1;
|
||||||
|
// t[e] = n
|
||||||
|
// }
|
||||||
|
// return t
|
||||||
|
// }(), r = function(t) {
|
||||||
|
// t = t.replace(/\\r\\n/g, "\\n");
|
||||||
|
// for (var e = "", n = 0; n < t.length; n++) {
|
||||||
|
// var r = t.charCodeAt(n);
|
||||||
|
// r < 128 ? e += String.fromCharCode(r) : e = 127 < r && r < 2048 ? (e += String.fromCharCode(r >> 6 | 192)) + String.fromCharCode(63 & r | 128) : (e = (e += String.fromCharCode(r >> 12 | 224)) + String.fromCharCode(r >> 6 & 63 | 128)) + String.fromCharCode(63 & r | 128)
|
||||||
|
// }
|
||||||
|
// return e
|
||||||
|
// }(t), a = -1, i = 0; i < r.length; i++)
|
||||||
|
// a = a >>> 8 ^ n[255 & (a ^ r.charCodeAt(i))];
|
||||||
|
// return (a = (-1 ^ a) >>> 0).toString(e)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// function v(t) {
|
||||||
|
// return (v = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(t) {
|
||||||
|
// return typeof t
|
||||||
|
// }
|
||||||
|
// : function(t) {
|
||||||
|
// return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t
|
||||||
|
// }
|
||||||
|
// )(t)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for (p in a = Math.round(1e7 * Math.random()),
|
||||||
|
// o = Math.round(((new Date).getTime() + 60 * (new Date).getTimezoneOffset() * 1e3 + 288e5) / 1e3).toString(),
|
||||||
|
// m = ["a", "d", "e", "f", "g", "h", "l", "m", "y", "i", "j", "n", "o", "p", "k", "q", "r", "s", "t", "u", "b", "c", "v", "w", "s", "z"],
|
||||||
|
// u = function(t, e, n) {
|
||||||
|
// var r;
|
||||||
|
// n = 2 < arguments.length && void 0 !== n ? n : 8;
|
||||||
|
// return 0 === arguments.length ? null : (r = "object" === v(t) ? t : (10 === "".concat(t).length && (t = 1e3 * Number.parseInt(t)),
|
||||||
|
// new Date(t)),
|
||||||
|
// t += 6e4 * new Date(t).getTimezoneOffset(),
|
||||||
|
// {
|
||||||
|
// y: (r = new Date(t + 36e5 * n)).getFullYear(),
|
||||||
|
// m: r.getMonth() + 1 < 10 ? "0".concat(r.getMonth() + 1) : r.getMonth() + 1,
|
||||||
|
// d: r.getDate() < 10 ? "0".concat(r.getDate()) : r.getDate(),
|
||||||
|
// h: r.getHours() < 10 ? "0".concat(r.getHours()) : r.getHours(),
|
||||||
|
// f: r.getMinutes() < 10 ? "0".concat(r.getMinutes()) : r.getMinutes()
|
||||||
|
// })
|
||||||
|
// }(o),
|
||||||
|
// h = u.y,
|
||||||
|
// g = u.m,
|
||||||
|
// l = u.d,
|
||||||
|
// c = u.h,
|
||||||
|
// u = u.f,
|
||||||
|
// d = [h, g, l, c, u].join(""),
|
||||||
|
// f = [],
|
||||||
|
// d)
|
||||||
|
// f.push(m[Number(d[p])]);
|
||||||
|
// return h = A(f.join("")),
|
||||||
|
// g = A("".concat(o, "|").concat(a, "|").concat(e, "|").concat("web", "|").concat("3", "|").concat(h)),
|
||||||
|
// "".concat(h, "=").concat(o, "-").concat(a, "-").concat(g);
|
||||||
|
// })(url)
|
||||||
|
// `)
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println(err)
|
||||||
|
// return url
|
||||||
|
// }
|
||||||
|
// v, _ := r.Export().(string)
|
||||||
|
// return url + "?" + v
|
||||||
|
//}
|
||||||
|
|
||||||
func (d *Pan123) login() error {
|
func (d *Pan123) login() error {
|
||||||
var body base.Json
|
var body base.Json
|
||||||
if utils.IsEmailFormat(d.Username) {
|
if utils.IsEmailFormat(d.Username) {
|
||||||
@@ -56,9 +163,9 @@ func (d *Pan123) login() error {
|
|||||||
SetHeaders(map[string]string{
|
SetHeaders(map[string]string{
|
||||||
"origin": "https://www.123pan.com",
|
"origin": "https://www.123pan.com",
|
||||||
"referer": "https://www.123pan.com/",
|
"referer": "https://www.123pan.com/",
|
||||||
"user-agent": "Dart/2.19(dart:io)",
|
"user-agent": "Dart/2.19(dart:io)-alist",
|
||||||
"platform": "android",
|
"platform": "web",
|
||||||
"app-version": "36",
|
"app-version": "3",
|
||||||
//"user-agent": base.UserAgent,
|
//"user-agent": base.UserAgent,
|
||||||
}).
|
}).
|
||||||
SetBody(body).Post(SignIn)
|
SetBody(body).Post(SignIn)
|
||||||
@@ -87,15 +194,17 @@ func (d *Pan123) login() error {
|
|||||||
// return &authKey, nil
|
// return &authKey, nil
|
||||||
//}
|
//}
|
||||||
|
|
||||||
func (d *Pan123) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
func (d *Pan123) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
|
isRetry := false
|
||||||
|
do:
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
req.SetHeaders(map[string]string{
|
req.SetHeaders(map[string]string{
|
||||||
"origin": "https://www.123pan.com",
|
"origin": "https://www.123pan.com",
|
||||||
"referer": "https://www.123pan.com/",
|
"referer": "https://www.123pan.com/",
|
||||||
"authorization": "Bearer " + d.AccessToken,
|
"authorization": "Bearer " + d.AccessToken,
|
||||||
"user-agent": "Dart/2.19(dart:io)",
|
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
|
||||||
"platform": "android",
|
"platform": "web",
|
||||||
"app-version": "36",
|
"app-version": "3",
|
||||||
//"user-agent": base.UserAgent,
|
//"user-agent": base.UserAgent,
|
||||||
})
|
})
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
@@ -109,51 +218,67 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
|
|||||||
// return nil, err
|
// return nil, err
|
||||||
//}
|
//}
|
||||||
//req.SetQueryParam("auth-key", *authKey)
|
//req.SetQueryParam("auth-key", *authKey)
|
||||||
res, err := req.Execute(method, url)
|
res, err := req.Execute(method, GetApi(url))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
body := res.Body()
|
body := res.Body()
|
||||||
code := utils.Json.Get(body, "code").ToInt()
|
code := utils.Json.Get(body, "code").ToInt()
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
if code == 401 {
|
if !isRetry && code == 401 {
|
||||||
err := d.login()
|
err := d.login()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return d.request(url, method, callback, resp)
|
isRetry = true
|
||||||
|
goto do
|
||||||
}
|
}
|
||||||
return nil, errors.New(jsoniter.Get(body, "message").ToString())
|
return nil, errors.New(jsoniter.Get(body, "message").ToString())
|
||||||
}
|
}
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) getFiles(parentId string) ([]File, error) {
|
func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]File, error) {
|
||||||
page := 1
|
page := 1
|
||||||
|
total := 0
|
||||||
res := make([]File, 0)
|
res := make([]File, 0)
|
||||||
|
// 2024-02-06 fix concurrency by 123pan
|
||||||
for {
|
for {
|
||||||
|
if err := d.APIRateLimit(ctx, FileList); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var resp Files
|
var resp Files
|
||||||
query := map[string]string{
|
query := map[string]string{
|
||||||
"driveId": "0",
|
"driveId": "0",
|
||||||
"limit": "100",
|
"limit": "100",
|
||||||
"next": "0",
|
"next": "0",
|
||||||
"orderBy": d.OrderBy,
|
"orderBy": "file_id",
|
||||||
"orderDirection": d.OrderDirection,
|
"orderDirection": "desc",
|
||||||
"parentFileId": parentId,
|
"parentFileId": parentId,
|
||||||
"trashed": "false",
|
"trashed": "false",
|
||||||
"Page": strconv.Itoa(page),
|
"SearchData": "",
|
||||||
|
"Page": strconv.Itoa(page),
|
||||||
|
"OnlyLookAbnormalFile": "0",
|
||||||
|
"event": "homeListFile",
|
||||||
|
"operateType": "4",
|
||||||
|
"inDirectSpace": "false",
|
||||||
}
|
}
|
||||||
_, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
|
_res, err := d.Request(FileList, http.MethodGet, func(req *resty.Request) {
|
||||||
req.SetQueryParams(query)
|
req.SetQueryParams(query)
|
||||||
}, &resp)
|
}, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.Debug(string(_res))
|
||||||
page++
|
page++
|
||||||
res = append(res, resp.Data.InfoList...)
|
res = append(res, resp.Data.InfoList...)
|
||||||
|
total = resp.Data.Total
|
||||||
if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
|
if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(res) != total {
|
||||||
|
log.Warnf("incorrect file count from remote at %s: expected %d, got %d", name, total, len(res))
|
||||||
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|||||||
77
drivers/123_link/driver.go
Normal file
77
drivers/123_link/driver.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package _123Link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
stdpath "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/alist-org/alist/v3/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pan123Link struct {
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
root *Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan123Link) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan123Link) GetAddition() driver.Additional {
|
||||||
|
return &d.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan123Link) Init(ctx context.Context) error {
|
||||||
|
node, err := BuildTree(d.OriginURLs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
node.calSize()
|
||||||
|
d.root = node
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan123Link) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan123Link) Get(ctx context.Context, path string) (model.Obj, error) {
|
||||||
|
node := GetNodeFromRootByPath(d.root, path)
|
||||||
|
return nodeToObj(node, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan123Link) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
node := GetNodeFromRootByPath(d.root, dir.GetPath())
|
||||||
|
if node == nil {
|
||||||
|
return nil, errs.ObjectNotFound
|
||||||
|
}
|
||||||
|
if node.isFile() {
|
||||||
|
return nil, errs.NotFolder
|
||||||
|
}
|
||||||
|
return utils.SliceConvert(node.Children, func(node *Node) (model.Obj, error) {
|
||||||
|
return nodeToObj(node, stdpath.Join(dir.GetPath(), node.Name))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan123Link) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
node := GetNodeFromRootByPath(d.root, file.GetPath())
|
||||||
|
if node == nil {
|
||||||
|
return nil, errs.ObjectNotFound
|
||||||
|
}
|
||||||
|
if node.isFile() {
|
||||||
|
signUrl, err := SignURL(node.Url, d.PrivateKey, d.UID, time.Duration(d.ValidDuration)*time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.Link{
|
||||||
|
URL: signUrl,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, errs.NotFile
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*Pan123Link)(nil)
|
||||||
23
drivers/123_link/meta.go
Normal file
23
drivers/123_link/meta.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package _123Link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Addition struct {
|
||||||
|
OriginURLs string `json:"origin_urls" type:"text" required:"true" default:"https://vip.123pan.com/29/folder/file.mp3" help:"structure:FolderName:\n [FileSize:][Modified:]Url"`
|
||||||
|
PrivateKey string `json:"private_key"`
|
||||||
|
UID uint64 `json:"uid" type:"number"`
|
||||||
|
ValidDuration int64 `json:"valid_duration" type:"number" default:"30" help:"minutes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "123PanLink",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(func() driver.Driver {
|
||||||
|
return &Pan123Link{}
|
||||||
|
})
|
||||||
|
}
|
||||||
152
drivers/123_link/parse.go
Normal file
152
drivers/123_link/parse.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package _123Link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
url2 "net/url"
|
||||||
|
stdpath "path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// build tree from text, text structure definition:
|
||||||
|
/**
|
||||||
|
* FolderName:
|
||||||
|
* [FileSize:][Modified:]Url
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* For example:
|
||||||
|
* folder1:
|
||||||
|
* name1:url1
|
||||||
|
* url2
|
||||||
|
* folder2:
|
||||||
|
* url3
|
||||||
|
* url4
|
||||||
|
* url5
|
||||||
|
* folder3:
|
||||||
|
* url6
|
||||||
|
* url7
|
||||||
|
* url8
|
||||||
|
*/
|
||||||
|
// if there are no name, use the last segment of url as name
|
||||||
|
func BuildTree(text string) (*Node, error) {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
var root = &Node{Level: -1, Name: "root"}
|
||||||
|
stack := []*Node{root}
|
||||||
|
for _, line := range lines {
|
||||||
|
// calculate indent
|
||||||
|
indent := 0
|
||||||
|
for i := 0; i < len(line); i++ {
|
||||||
|
if line[i] != ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
indent++
|
||||||
|
}
|
||||||
|
// if indent is not a multiple of 2, it is an error
|
||||||
|
if indent%2 != 0 {
|
||||||
|
return nil, fmt.Errorf("the line '%s' is not a multiple of 2", line)
|
||||||
|
}
|
||||||
|
// calculate level
|
||||||
|
level := indent / 2
|
||||||
|
line = strings.TrimSpace(line[indent:])
|
||||||
|
// if the line is empty, skip
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// if level isn't greater than the level of the top of the stack
|
||||||
|
// it is not the child of the top of the stack
|
||||||
|
for level <= stack[len(stack)-1].Level {
|
||||||
|
// pop the top of the stack
|
||||||
|
stack = stack[:len(stack)-1]
|
||||||
|
}
|
||||||
|
// if the line is a folder
|
||||||
|
if isFolder(line) {
|
||||||
|
// create a new node
|
||||||
|
node := &Node{
|
||||||
|
Level: level,
|
||||||
|
Name: strings.TrimSuffix(line, ":"),
|
||||||
|
}
|
||||||
|
// add the node to the top of the stack
|
||||||
|
stack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)
|
||||||
|
// push the node to the stack
|
||||||
|
stack = append(stack, node)
|
||||||
|
} else {
|
||||||
|
// if the line is a file
|
||||||
|
// create a new node
|
||||||
|
node, err := parseFileLine(line)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
node.Level = level
|
||||||
|
// add the node to the top of the stack
|
||||||
|
stack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFolder(line string) bool {
|
||||||
|
return strings.HasSuffix(line, ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
// line definition:
|
||||||
|
// [FileSize:][Modified:]Url
|
||||||
|
func parseFileLine(line string) (*Node, error) {
|
||||||
|
// if there is no url, it is an error
|
||||||
|
if !strings.Contains(line, "http://") && !strings.Contains(line, "https://") {
|
||||||
|
return nil, fmt.Errorf("invalid line: %s, because url is required for file", line)
|
||||||
|
}
|
||||||
|
index := strings.Index(line, "http://")
|
||||||
|
if index == -1 {
|
||||||
|
index = strings.Index(line, "https://")
|
||||||
|
}
|
||||||
|
url := line[index:]
|
||||||
|
info := line[:index]
|
||||||
|
node := &Node{
|
||||||
|
Url: url,
|
||||||
|
}
|
||||||
|
name := stdpath.Base(url)
|
||||||
|
unescape, err := url2.PathUnescape(name)
|
||||||
|
if err == nil {
|
||||||
|
name = unescape
|
||||||
|
}
|
||||||
|
node.Name = name
|
||||||
|
if index > 0 {
|
||||||
|
if !strings.HasSuffix(info, ":") {
|
||||||
|
return nil, fmt.Errorf("invalid line: %s, because file info must end with ':'", line)
|
||||||
|
}
|
||||||
|
info = info[:len(info)-1]
|
||||||
|
if info == "" {
|
||||||
|
return nil, fmt.Errorf("invalid line: %s, because file name can't be empty", line)
|
||||||
|
}
|
||||||
|
infoParts := strings.Split(info, ":")
|
||||||
|
size, err := strconv.ParseInt(infoParts[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid line: %s, because file size must be an integer", line)
|
||||||
|
}
|
||||||
|
node.Size = size
|
||||||
|
if len(infoParts) > 1 {
|
||||||
|
modified, err := strconv.ParseInt(infoParts[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid line: %s, because file modified must be an unix timestamp", line)
|
||||||
|
}
|
||||||
|
node.Modified = modified
|
||||||
|
} else {
|
||||||
|
node.Modified = time.Now().Unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitPath(path string) []string {
|
||||||
|
if path == "/" {
|
||||||
|
return []string{"root"}
|
||||||
|
}
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
parts[0] = "root"
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNodeFromRootByPath(root *Node, path string) *Node {
|
||||||
|
return root.getByPath(splitPath(path))
|
||||||
|
}
|
||||||
66
drivers/123_link/types.go
Normal file
66
drivers/123_link/types.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package _123Link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Node is a node in the folder tree
|
||||||
|
type Node struct {
|
||||||
|
Url string
|
||||||
|
Name string
|
||||||
|
Level int
|
||||||
|
Modified int64
|
||||||
|
Size int64
|
||||||
|
Children []*Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *Node) getByPath(paths []string) *Node {
|
||||||
|
if len(paths) == 0 || node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if node.Name != paths[0] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(paths) == 1 {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
for _, child := range node.Children {
|
||||||
|
tmp := child.getByPath(paths[1:])
|
||||||
|
if tmp != nil {
|
||||||
|
return tmp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *Node) isFile() bool {
|
||||||
|
return node.Url != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *Node) calSize() int64 {
|
||||||
|
if node.isFile() {
|
||||||
|
return node.Size
|
||||||
|
}
|
||||||
|
var size int64 = 0
|
||||||
|
for _, child := range node.Children {
|
||||||
|
size += child.calSize()
|
||||||
|
}
|
||||||
|
node.Size = size
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodeToObj(node *Node, path string) (model.Obj, error) {
|
||||||
|
if node == nil {
|
||||||
|
return nil, errs.ObjectNotFound
|
||||||
|
}
|
||||||
|
return &model.Object{
|
||||||
|
Name: node.Name,
|
||||||
|
Size: node.Size,
|
||||||
|
Modified: time.Unix(node.Modified, 0),
|
||||||
|
IsFolder: !node.isFile(),
|
||||||
|
Path: path,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
30
drivers/123_link/util.go
Normal file
30
drivers/123_link/util.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package _123Link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SignURL(originURL, privateKey string, uid uint64, validDuration time.Duration) (newURL string, err error) {
|
||||||
|
if privateKey == "" {
|
||||||
|
return originURL, nil
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
ts = time.Now().Add(validDuration).Unix() // 有效时间戳
|
||||||
|
rInt = rand.Int() // 随机正整数
|
||||||
|
objURL *url.URL
|
||||||
|
)
|
||||||
|
objURL, err = url.Parse(originURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
authKey := fmt.Sprintf("%d-%d-%d-%x", ts, rInt, uid, md5.Sum([]byte(fmt.Sprintf("%s-%d-%d-%d-%s",
|
||||||
|
objURL.Path, ts, rInt, uid, privateKey))))
|
||||||
|
v := objURL.Query()
|
||||||
|
v.Add("auth_key", authKey)
|
||||||
|
objURL.RawQuery = v.Encode()
|
||||||
|
return objURL.String(), nil
|
||||||
|
}
|
||||||
@@ -6,7 +6,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
_123 "github.com/alist-org/alist/v3/drivers/123"
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
@@ -19,6 +24,8 @@ import (
|
|||||||
type Pan123Share struct {
|
type Pan123Share struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
|
apiRateLimit sync.Map
|
||||||
|
ref *_123.Pan123
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123Share) Config() driver.Config {
|
func (d *Pan123Share) Config() driver.Config {
|
||||||
@@ -35,13 +42,23 @@ func (d *Pan123Share) Init(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Pan123Share) InitReference(storage driver.Driver) error {
|
||||||
|
refStorage, ok := storage.(*_123.Pan123)
|
||||||
|
if ok {
|
||||||
|
d.ref = refStorage
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("ref: storage is not 123Pan")
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Pan123Share) Drop(ctx context.Context) error {
|
func (d *Pan123Share) Drop(ctx context.Context) error {
|
||||||
|
d.ref = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (d *Pan123Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
// TODO return the files list, required
|
// TODO return the files list, required
|
||||||
files, err := d.getFiles(dir.GetID())
|
files, err := d.getFiles(ctx, dir.GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -146,4 +163,12 @@ func (d *Pan123Share) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
|
|||||||
// return nil, errs.NotSupport
|
// return nil, errs.NotSupport
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
func (d *Pan123Share) APIRateLimit(ctx context.Context, api string) error {
|
||||||
|
value, _ := d.apiRateLimit.LoadOrStore(api,
|
||||||
|
rate.NewLimiter(rate.Every(700*time.Millisecond), 1))
|
||||||
|
limiter := value.(*rate.Limiter)
|
||||||
|
|
||||||
|
return limiter.Wait(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*Pan123Share)(nil)
|
var _ driver.Driver = (*Pan123Share)(nil)
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import (
|
|||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
ShareKey string `json:"sharekey" required:"true"`
|
ShareKey string `json:"sharekey" required:"true"`
|
||||||
SharePwd string `json:"sharepassword" required:"true"`
|
SharePwd string `json:"sharepassword"`
|
||||||
driver.RootID
|
driver.RootID
|
||||||
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
|
//OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
|
AccessToken string `json:"accesstoken" type:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package _123Share
|
package _123Share
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -21,6 +22,10 @@ type File struct {
|
|||||||
DownloadUrl string `json:"DownloadUrl"`
|
DownloadUrl string `json:"DownloadUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f File) GetHash() utils.HashInfo {
|
||||||
|
return utils.HashInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
func (f File) GetPath() string {
|
func (f File) GetPath() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -36,6 +41,9 @@ func (f File) GetName() string {
|
|||||||
func (f File) ModTime() time.Time {
|
func (f File) ModTime() time.Time {
|
||||||
return f.UpdateAt
|
return f.UpdateAt
|
||||||
}
|
}
|
||||||
|
func (f File) CreateTime() time.Time {
|
||||||
|
return f.UpdateAt
|
||||||
|
}
|
||||||
|
|
||||||
func (f File) IsDir() bool {
|
func (f File) IsDir() bool {
|
||||||
return f.Type == 1
|
return f.Type == 1
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
package _123Share
|
package _123Share
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
@@ -15,20 +23,48 @@ const (
|
|||||||
Api = "https://www.123pan.com/api"
|
Api = "https://www.123pan.com/api"
|
||||||
AApi = "https://www.123pan.com/a/api"
|
AApi = "https://www.123pan.com/a/api"
|
||||||
BApi = "https://www.123pan.com/b/api"
|
BApi = "https://www.123pan.com/b/api"
|
||||||
MainApi = Api
|
MainApi = BApi
|
||||||
FileList = MainApi + "/share/get"
|
FileList = MainApi + "/share/get"
|
||||||
DownloadInfo = MainApi + "/share/download/info"
|
DownloadInfo = MainApi + "/share/download/info"
|
||||||
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
|
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func signPath(path string, os string, version string) (k string, v string) {
|
||||||
|
table := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}
|
||||||
|
random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
|
||||||
|
now := time.Now().In(time.FixedZone("CST", 8*3600))
|
||||||
|
timestamp := fmt.Sprint(now.Unix())
|
||||||
|
nowStr := []byte(now.Format("200601021504"))
|
||||||
|
for i := 0; i < len(nowStr); i++ {
|
||||||
|
nowStr[i] = table[nowStr[i]-48]
|
||||||
|
}
|
||||||
|
timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
|
||||||
|
data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
|
||||||
|
dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
|
||||||
|
return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetApi(rawUrl string) string {
|
||||||
|
u, _ := url.Parse(rawUrl)
|
||||||
|
query := u.Query()
|
||||||
|
query.Add(signPath(u.Path, "web", "3"))
|
||||||
|
u.RawQuery = query.Encode()
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
|
if d.ref != nil {
|
||||||
|
return d.ref.Request(url, method, callback, resp)
|
||||||
|
}
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
req.SetHeaders(map[string]string{
|
req.SetHeaders(map[string]string{
|
||||||
"origin": "https://www.123pan.com",
|
"origin": "https://www.123pan.com",
|
||||||
"referer": "https://www.123pan.com/",
|
"referer": "https://www.123pan.com/",
|
||||||
"user-agent": "Dart/2.19(dart:io)",
|
"authorization": "Bearer " + d.AccessToken,
|
||||||
"platform": "android",
|
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
|
||||||
"app-version": "36",
|
"platform": "web",
|
||||||
|
"app-version": "3",
|
||||||
|
//"user-agent": base.UserAgent,
|
||||||
})
|
})
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(req)
|
callback(req)
|
||||||
@@ -36,7 +72,7 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
|
|||||||
if resp != nil {
|
if resp != nil {
|
||||||
req.SetResult(resp)
|
req.SetResult(resp)
|
||||||
}
|
}
|
||||||
res, err := req.Execute(method, url)
|
res, err := req.Execute(method, GetApi(url))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -48,16 +84,19 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
|
|||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123Share) getFiles(parentId string) ([]File, error) {
|
func (d *Pan123Share) getFiles(ctx context.Context, parentId string) ([]File, error) {
|
||||||
page := 1
|
page := 1
|
||||||
res := make([]File, 0)
|
res := make([]File, 0)
|
||||||
for {
|
for {
|
||||||
|
if err := d.APIRateLimit(ctx, FileList); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var resp Files
|
var resp Files
|
||||||
query := map[string]string{
|
query := map[string]string{
|
||||||
"limit": "100",
|
"limit": "100",
|
||||||
"next": "0",
|
"next": "0",
|
||||||
"orderBy": d.OrderBy,
|
"orderBy": "file_id",
|
||||||
"orderDirection": d.OrderDirection,
|
"orderDirection": "desc",
|
||||||
"parentFileId": parentId,
|
"parentFileId": parentId,
|
||||||
"Page": strconv.Itoa(page),
|
"Page": strconv.Itoa(page),
|
||||||
"shareKey": d.ShareKey,
|
"shareKey": d.ShareKey,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,17 +9,21 @@ type Addition struct {
|
|||||||
//Account string `json:"account" required:"true"`
|
//Account string `json:"account" required:"true"`
|
||||||
Authorization string `json:"authorization" type:"text" required:"true"`
|
Authorization string `json:"authorization" type:"text" required:"true"`
|
||||||
driver.RootID
|
driver.RootID
|
||||||
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
|
Type string `json:"type" type:"select" options:"personal_new,family,group,personal" default:"personal_new"`
|
||||||
CloudID string `json:"cloud_id"`
|
CloudID string `json:"cloud_id"`
|
||||||
|
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "139Yun",
|
Name: "139Yun",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
|
ProxyRangeOption: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
op.RegisterDriver(func() driver.Driver {
|
op.RegisterDriver(func() driver.Driver {
|
||||||
return &Yun139{}
|
d := &Yun139{}
|
||||||
|
d.ProxyRange = true
|
||||||
|
return d
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
package _139
|
package _139
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MetaPersonal string = "personal"
|
||||||
|
MetaFamily string = "family"
|
||||||
|
MetaGroup string = "group"
|
||||||
|
MetaPersonalNew string = "personal_new"
|
||||||
|
)
|
||||||
|
|
||||||
type BaseResp struct {
|
type BaseResp struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
@@ -10,7 +21,7 @@ type Catalog struct {
|
|||||||
CatalogID string `json:"catalogID"`
|
CatalogID string `json:"catalogID"`
|
||||||
CatalogName string `json:"catalogName"`
|
CatalogName string `json:"catalogName"`
|
||||||
//CatalogType int `json:"catalogType"`
|
//CatalogType int `json:"catalogType"`
|
||||||
//CreateTime string `json:"createTime"`
|
CreateTime string `json:"createTime"`
|
||||||
UpdateTime string `json:"updateTime"`
|
UpdateTime string `json:"updateTime"`
|
||||||
//IsShared bool `json:"isShared"`
|
//IsShared bool `json:"isShared"`
|
||||||
//CatalogLevel int `json:"catalogLevel"`
|
//CatalogLevel int `json:"catalogLevel"`
|
||||||
@@ -44,6 +55,7 @@ type Content struct {
|
|||||||
//ContentDesc string `json:"contentDesc"`
|
//ContentDesc string `json:"contentDesc"`
|
||||||
//ContentType int `json:"contentType"`
|
//ContentType int `json:"contentType"`
|
||||||
//ContentOrigin int `json:"contentOrigin"`
|
//ContentOrigin int `json:"contentOrigin"`
|
||||||
|
CreateTime string `json:"createTime"`
|
||||||
UpdateTime string `json:"updateTime"`
|
UpdateTime string `json:"updateTime"`
|
||||||
//CommentCount int `json:"commentCount"`
|
//CommentCount int `json:"commentCount"`
|
||||||
ThumbnailURL string `json:"thumbnailURL"`
|
ThumbnailURL string `json:"thumbnailURL"`
|
||||||
@@ -63,7 +75,7 @@ type Content struct {
|
|||||||
//ParentCatalogID string `json:"parentCatalogId"`
|
//ParentCatalogID string `json:"parentCatalogId"`
|
||||||
//Channel string `json:"channel"`
|
//Channel string `json:"channel"`
|
||||||
//GeoLocFlag string `json:"geoLocFlag"`
|
//GeoLocFlag string `json:"geoLocFlag"`
|
||||||
//Digest string `json:"digest"`
|
Digest string `json:"digest"`
|
||||||
//Version string `json:"version"`
|
//Version string `json:"version"`
|
||||||
//FileEtag string `json:"fileEtag"`
|
//FileEtag string `json:"fileEtag"`
|
||||||
//FileVersion string `json:"fileVersion"`
|
//FileVersion string `json:"fileVersion"`
|
||||||
@@ -141,7 +153,7 @@ type CloudContent struct {
|
|||||||
//ContentSuffix string `json:"contentSuffix"`
|
//ContentSuffix string `json:"contentSuffix"`
|
||||||
ContentSize int64 `json:"contentSize"`
|
ContentSize int64 `json:"contentSize"`
|
||||||
//ContentDesc string `json:"contentDesc"`
|
//ContentDesc string `json:"contentDesc"`
|
||||||
//CreateTime string `json:"createTime"`
|
CreateTime string `json:"createTime"`
|
||||||
//Shottime interface{} `json:"shottime"`
|
//Shottime interface{} `json:"shottime"`
|
||||||
LastUpdateTime string `json:"lastUpdateTime"`
|
LastUpdateTime string `json:"lastUpdateTime"`
|
||||||
ThumbnailURL string `json:"thumbnailURL"`
|
ThumbnailURL string `json:"thumbnailURL"`
|
||||||
@@ -165,7 +177,7 @@ type CloudCatalog struct {
|
|||||||
CatalogID string `json:"catalogID"`
|
CatalogID string `json:"catalogID"`
|
||||||
CatalogName string `json:"catalogName"`
|
CatalogName string `json:"catalogName"`
|
||||||
//CloudID string `json:"cloudID"`
|
//CloudID string `json:"cloudID"`
|
||||||
//CreateTime string `json:"createTime"`
|
CreateTime string `json:"createTime"`
|
||||||
LastUpdateTime string `json:"lastUpdateTime"`
|
LastUpdateTime string `json:"lastUpdateTime"`
|
||||||
//Creator string `json:"creator"`
|
//Creator string `json:"creator"`
|
||||||
//CreatorNickname string `json:"creatorNickname"`
|
//CreatorNickname string `json:"creatorNickname"`
|
||||||
@@ -185,3 +197,92 @@ type QueryContentListResp struct {
|
|||||||
RecallContent interface{} `json:"recallContent"`
|
RecallContent interface{} `json:"recallContent"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QueryGroupContentListResp struct {
|
||||||
|
BaseResp
|
||||||
|
Data struct {
|
||||||
|
Result struct {
|
||||||
|
ResultCode string `json:"resultCode"`
|
||||||
|
ResultDesc string `json:"resultDesc"`
|
||||||
|
} `json:"result"`
|
||||||
|
GetGroupContentResult struct {
|
||||||
|
ParentCatalogID string `json:"parentCatalogID"` // 根目录是"0"
|
||||||
|
CatalogList []struct {
|
||||||
|
Catalog
|
||||||
|
Path string `json:"path"`
|
||||||
|
} `json:"catalogList"`
|
||||||
|
ContentList []Content `json:"contentList"`
|
||||||
|
NodeCount int `json:"nodeCount"` // 文件+文件夹数量
|
||||||
|
CtlgCnt int `json:"ctlgCnt"` // 文件夹数量
|
||||||
|
ContCnt int `json:"contCnt"` // 文件数量
|
||||||
|
} `json:"getGroupContentResult"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParallelHashCtx struct {
|
||||||
|
PartOffset int64 `json:"partOffset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartInfo struct {
|
||||||
|
PartNumber int64 `json:"partNumber"`
|
||||||
|
PartSize int64 `json:"partSize"`
|
||||||
|
ParallelHashCtx ParallelHashCtx `json:"parallelHashCtx"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalThumbnail struct {
|
||||||
|
Style string `json:"style"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalFileItem struct {
|
||||||
|
FileId string `json:"fileId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
|
Thumbnails []PersonalThumbnail `json:"thumbnailUrls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalListResp struct {
|
||||||
|
BaseResp
|
||||||
|
Data struct {
|
||||||
|
Items []PersonalFileItem `json:"items"`
|
||||||
|
NextPageCursor string `json:"nextPageCursor"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalPartInfo struct {
|
||||||
|
PartNumber int `json:"partNumber"`
|
||||||
|
UploadUrl string `json:"uploadUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalUploadResp struct {
|
||||||
|
BaseResp
|
||||||
|
Data struct {
|
||||||
|
FileId string `json:"fileId"`
|
||||||
|
FileName string `json:"fileName"`
|
||||||
|
PartInfos []PersonalPartInfo `json:"partInfos"`
|
||||||
|
Exist bool `json:"exist"`
|
||||||
|
RapidUpload bool `json:"rapidUpload"`
|
||||||
|
UploadId string `json:"uploadId"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalUploadUrlResp struct {
|
||||||
|
BaseResp
|
||||||
|
Data struct {
|
||||||
|
FileId string `json:"fileId"`
|
||||||
|
UploadId string `json:"uploadId"`
|
||||||
|
PartInfos []PersonalPartInfo `json:"partInfos"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshTokenResp struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
Return string `xml:"return"`
|
||||||
|
Token string `xml:"token"`
|
||||||
|
Expiretime int32 `xml:"expiretime"`
|
||||||
|
AccessToken string `xml:"accessToken"`
|
||||||
|
Desc string `xml:"desc"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
@@ -48,10 +50,59 @@ func calSign(body, ts, randStr string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getTime(t string) time.Time {
|
func getTime(t string) time.Time {
|
||||||
stamp, _ := time.ParseInLocation("20060102150405", t, time.Local)
|
stamp, _ := time.ParseInLocation("20060102150405", t, utils.CNLoc)
|
||||||
return stamp
|
return stamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Yun139) refreshToken() error {
|
||||||
|
if d.ref != nil {
|
||||||
|
return d.ref.refreshToken()
|
||||||
|
}
|
||||||
|
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("authorization decode failed: %s", err)
|
||||||
|
}
|
||||||
|
decodeStr := string(decode)
|
||||||
|
splits := strings.Split(decodeStr, ":")
|
||||||
|
if len(splits) < 3 {
|
||||||
|
return fmt.Errorf("authorization is invalid, splits < 3")
|
||||||
|
}
|
||||||
|
strs := strings.Split(splits[2], "|")
|
||||||
|
if len(strs) < 4 {
|
||||||
|
return fmt.Errorf("authorization is invalid, strs < 4")
|
||||||
|
}
|
||||||
|
expiration, err := strconv.ParseInt(strs[3], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("authorization is invalid")
|
||||||
|
}
|
||||||
|
expiration -= time.Now().UnixMilli()
|
||||||
|
if expiration > 1000*60*60*24*15 {
|
||||||
|
// Authorization有效期大于15天无需刷新
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if expiration < 0 {
|
||||||
|
return fmt.Errorf("authorization has expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do"
|
||||||
|
var resp RefreshTokenResp
|
||||||
|
reqBody := "<root><token>" + splits[2] + "</token><account>" + splits[1] + "</account><clienttype>656</clienttype></root>"
|
||||||
|
_, err = base.RestyClient.R().
|
||||||
|
ForceContentType("application/xml").
|
||||||
|
SetBody(reqBody).
|
||||||
|
SetResult(&resp).
|
||||||
|
Post(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.Return != "0" {
|
||||||
|
return fmt.Errorf("failed to refresh token: %s", resp.Desc)
|
||||||
|
}
|
||||||
|
d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token))
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
url := "https://yun.139.com" + pathname
|
url := "https://yun.139.com" + pathname
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
@@ -72,21 +123,22 @@ func (d *Yun139) request(pathname string, method string, callback base.ReqCallba
|
|||||||
req.SetHeaders(map[string]string{
|
req.SetHeaders(map[string]string{
|
||||||
"Accept": "application/json, text/plain, */*",
|
"Accept": "application/json, text/plain, */*",
|
||||||
"CMS-DEVICE": "default",
|
"CMS-DEVICE": "default",
|
||||||
"Authorization": "Basic " + d.Authorization,
|
"Authorization": "Basic " + d.getAuthorization(),
|
||||||
"mcloud-channel": "1000101",
|
"mcloud-channel": "1000101",
|
||||||
"mcloud-client": "10701",
|
"mcloud-client": "10701",
|
||||||
//"mcloud-route": "001",
|
//"mcloud-route": "001",
|
||||||
"mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
|
"mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
|
||||||
//"mcloud-skey":"",
|
//"mcloud-skey":"",
|
||||||
"mcloud-version": "6.6.0",
|
"mcloud-version": "7.14.0",
|
||||||
"Origin": "https://yun.139.com",
|
"Origin": "https://yun.139.com",
|
||||||
"Referer": "https://yun.139.com/w/",
|
"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-DeviceInfo": "||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||",
|
||||||
"x-huawei-channelSrc": "10000034",
|
"x-huawei-channelSrc": "10000034",
|
||||||
"x-inner-ntwk": "2",
|
"x-inner-ntwk": "2",
|
||||||
"x-m4c-caller": "PC",
|
"x-m4c-caller": "PC",
|
||||||
"x-m4c-src": "10002",
|
"x-m4c-src": "10002",
|
||||||
"x-SvcType": svcType,
|
"x-SvcType": svcType,
|
||||||
|
"Inner-Hcy-Router-Https": "1",
|
||||||
})
|
})
|
||||||
|
|
||||||
var e BaseResp
|
var e BaseResp
|
||||||
@@ -124,7 +176,7 @@ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
|
|||||||
"catalogSortType": 0,
|
"catalogSortType": 0,
|
||||||
"contentSortType": 0,
|
"contentSortType": 0,
|
||||||
"commonAccountInfo": base.Json{
|
"commonAccountInfo": base.Json{
|
||||||
"account": d.Account,
|
"account": d.getAccount(),
|
||||||
"accountType": 1,
|
"accountType": 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -139,6 +191,7 @@ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
|
|||||||
Name: catalog.CatalogName,
|
Name: catalog.CatalogName,
|
||||||
Size: 0,
|
Size: 0,
|
||||||
Modified: getTime(catalog.UpdateTime),
|
Modified: getTime(catalog.UpdateTime),
|
||||||
|
Ctime: getTime(catalog.CreateTime),
|
||||||
IsFolder: true,
|
IsFolder: true,
|
||||||
}
|
}
|
||||||
files = append(files, &f)
|
files = append(files, &f)
|
||||||
@@ -150,6 +203,7 @@ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
|
|||||||
Name: content.ContentName,
|
Name: content.ContentName,
|
||||||
Size: content.ContentSize,
|
Size: content.ContentSize,
|
||||||
Modified: getTime(content.UpdateTime),
|
Modified: getTime(content.UpdateTime),
|
||||||
|
HashInfo: utils.NewHashInfo(utils.MD5, content.Digest),
|
||||||
},
|
},
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
||||||
//Thumbnail: content.BigthumbnailURL,
|
//Thumbnail: content.BigthumbnailURL,
|
||||||
@@ -170,7 +224,7 @@ func (d *Yun139) newJson(data map[string]interface{}) base.Json {
|
|||||||
"cloudID": d.CloudID,
|
"cloudID": d.CloudID,
|
||||||
"cloudType": 1,
|
"cloudType": 1,
|
||||||
"commonAccountInfo": base.Json{
|
"commonAccountInfo": base.Json{
|
||||||
"account": d.Account,
|
"account": d.getAccount(),
|
||||||
"accountType": 1,
|
"accountType": 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -191,10 +245,11 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
|||||||
"sortDirection": 1,
|
"sortDirection": 1,
|
||||||
})
|
})
|
||||||
var resp QueryContentListResp
|
var resp QueryContentListResp
|
||||||
_, err := d.post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp)
|
_, err := d.post("/orchestration/familyCloud-rebuild/content/v1.2/queryContentList", data, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
path := resp.Data.Path
|
||||||
for _, catalog := range resp.Data.CloudCatalogList {
|
for _, catalog := range resp.Data.CloudCatalogList {
|
||||||
f := model.Object{
|
f := model.Object{
|
||||||
ID: catalog.CatalogID,
|
ID: catalog.CatalogID,
|
||||||
@@ -202,6 +257,8 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
|||||||
Size: 0,
|
Size: 0,
|
||||||
IsFolder: true,
|
IsFolder: true,
|
||||||
Modified: getTime(catalog.LastUpdateTime),
|
Modified: getTime(catalog.LastUpdateTime),
|
||||||
|
Ctime: getTime(catalog.CreateTime),
|
||||||
|
Path: path, // 文件夹上一级的Path
|
||||||
}
|
}
|
||||||
files = append(files, &f)
|
files = append(files, &f)
|
||||||
}
|
}
|
||||||
@@ -212,13 +269,15 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
|||||||
Name: content.ContentName,
|
Name: content.ContentName,
|
||||||
Size: content.ContentSize,
|
Size: content.ContentSize,
|
||||||
Modified: getTime(content.LastUpdateTime),
|
Modified: getTime(content.LastUpdateTime),
|
||||||
|
Ctime: getTime(content.CreateTime),
|
||||||
|
Path: path, // 文件所在目录的Path
|
||||||
},
|
},
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
||||||
//Thumbnail: content.BigthumbnailURL,
|
//Thumbnail: content.BigthumbnailURL,
|
||||||
}
|
}
|
||||||
files = append(files, &f)
|
files = append(files, &f)
|
||||||
}
|
}
|
||||||
if 100*pageNum > resp.Data.TotalCount {
|
if resp.Data.TotalCount == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
pageNum++
|
pageNum++
|
||||||
@@ -226,12 +285,67 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
|
|||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Yun139) groupGetFiles(catalogID string) ([]model.Obj, error) {
|
||||||
|
pageNum := 1
|
||||||
|
files := make([]model.Obj, 0)
|
||||||
|
for {
|
||||||
|
data := d.newJson(base.Json{
|
||||||
|
"groupID": d.CloudID,
|
||||||
|
"catalogID": path.Base(catalogID),
|
||||||
|
"contentSortType": 0,
|
||||||
|
"sortDirection": 1,
|
||||||
|
"startNumber": pageNum,
|
||||||
|
"endNumber": pageNum + 99,
|
||||||
|
"path": path.Join(d.RootFolderID, catalogID),
|
||||||
|
})
|
||||||
|
|
||||||
|
var resp QueryGroupContentListResp
|
||||||
|
_, err := d.post("/orchestration/group-rebuild/content/v1.0/queryGroupContentList", data, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
path := resp.Data.GetGroupContentResult.ParentCatalogID
|
||||||
|
for _, catalog := range resp.Data.GetGroupContentResult.CatalogList {
|
||||||
|
f := model.Object{
|
||||||
|
ID: catalog.CatalogID,
|
||||||
|
Name: catalog.CatalogName,
|
||||||
|
Size: 0,
|
||||||
|
IsFolder: true,
|
||||||
|
Modified: getTime(catalog.UpdateTime),
|
||||||
|
Ctime: getTime(catalog.CreateTime),
|
||||||
|
Path: catalog.Path, // 文件夹的真实Path, root:/开头
|
||||||
|
}
|
||||||
|
files = append(files, &f)
|
||||||
|
}
|
||||||
|
for _, content := range resp.Data.GetGroupContentResult.ContentList {
|
||||||
|
f := model.ObjThumb{
|
||||||
|
Object: model.Object{
|
||||||
|
ID: content.ContentID,
|
||||||
|
Name: content.ContentName,
|
||||||
|
Size: content.ContentSize,
|
||||||
|
Modified: getTime(content.UpdateTime),
|
||||||
|
Ctime: getTime(content.CreateTime),
|
||||||
|
Path: path, // 文件所在目录的Path
|
||||||
|
},
|
||||||
|
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
|
||||||
|
//Thumbnail: content.BigthumbnailURL,
|
||||||
|
}
|
||||||
|
files = append(files, &f)
|
||||||
|
}
|
||||||
|
if (pageNum + 99) > resp.Data.GetGroupContentResult.NodeCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pageNum = pageNum + 100
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Yun139) getLink(contentId string) (string, error) {
|
func (d *Yun139) getLink(contentId string) (string, error) {
|
||||||
data := base.Json{
|
data := base.Json{
|
||||||
"appName": "",
|
"appName": "",
|
||||||
"contentID": contentId,
|
"contentID": contentId,
|
||||||
"commonAccountInfo": base.Json{
|
"commonAccountInfo": base.Json{
|
||||||
"account": d.Account,
|
"account": d.getAccount(),
|
||||||
"accountType": 1,
|
"accountType": 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -242,9 +356,199 @@ func (d *Yun139) getLink(contentId string) (string, error) {
|
|||||||
}
|
}
|
||||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||||
}
|
}
|
||||||
|
func (d *Yun139) familyGetLink(contentId string, path string) (string, error) {
|
||||||
|
data := d.newJson(base.Json{
|
||||||
|
"contentID": contentId,
|
||||||
|
"path": path,
|
||||||
|
})
|
||||||
|
res, err := d.post("/orchestration/familyCloud-rebuild/content/v1.0/getFileDownLoadURL",
|
||||||
|
data, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Yun139) groupGetLink(contentId string, path string) (string, error) {
|
||||||
|
data := d.newJson(base.Json{
|
||||||
|
"contentID": contentId,
|
||||||
|
"groupID": d.CloudID,
|
||||||
|
"path": path,
|
||||||
|
})
|
||||||
|
res, err := d.post("/orchestration/group-rebuild/groupManage/v1.0/getGroupFileDownLoadURL",
|
||||||
|
data, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func unicode(str string) string {
|
func unicode(str string) string {
|
||||||
textQuoted := strconv.QuoteToASCII(str)
|
textQuoted := strconv.QuoteToASCII(str)
|
||||||
textUnquoted := textQuoted[1 : len(textQuoted)-1]
|
textUnquoted := textQuoted[1 : len(textQuoted)-1]
|
||||||
return textUnquoted
|
return textUnquoted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Yun139) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
|
url := "https://personal-kd-njs.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, */*",
|
||||||
|
"Authorization": "Basic " + d.getAuthorization(),
|
||||||
|
"Caller": "web",
|
||||||
|
"Cms-Device": "default",
|
||||||
|
"Mcloud-Channel": "1000101",
|
||||||
|
"Mcloud-Client": "10701",
|
||||||
|
"Mcloud-Route": "001",
|
||||||
|
"Mcloud-Sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
|
||||||
|
"Mcloud-Version": "7.14.0",
|
||||||
|
"Origin": "https://yun.139.com",
|
||||||
|
"Referer": "https://yun.139.com/w/",
|
||||||
|
"x-DeviceInfo": "||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||",
|
||||||
|
"x-huawei-channelSrc": "10000034",
|
||||||
|
"x-inner-ntwk": "2",
|
||||||
|
"x-m4c-caller": "PC",
|
||||||
|
"x-m4c-src": "10002",
|
||||||
|
"x-SvcType": svcType,
|
||||||
|
"X-Yun-Api-Version": "v1",
|
||||||
|
"X-Yun-App-Channel": "10000034",
|
||||||
|
"X-Yun-Channel-Source": "10000034",
|
||||||
|
"X-Yun-Client-Info": "||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||dW5kZWZpbmVk||",
|
||||||
|
"X-Yun-Module-Type": "100",
|
||||||
|
"X-Yun-Svc-Type": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
var e BaseResp
|
||||||
|
req.SetResult(&e)
|
||||||
|
res, err := req.Execute(method, url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Debugln(res.String())
|
||||||
|
if !e.Success {
|
||||||
|
return nil, errors.New(e.Message)
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
err = utils.Json.Unmarshal(res.Body(), resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.Body(), nil
|
||||||
|
}
|
||||||
|
func (d *Yun139) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) {
|
||||||
|
return d.personalRequest(pathname, http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetBody(data)
|
||||||
|
}, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPersonalTime(t string) time.Time {
|
||||||
|
stamp, err := time.ParseInLocation("2006-01-02T15:04:05.999-07:00", t, utils.CNLoc)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return stamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
|
||||||
|
files := make([]model.Obj, 0)
|
||||||
|
nextPageCursor := ""
|
||||||
|
for {
|
||||||
|
data := base.Json{
|
||||||
|
"imageThumbnailStyleList": []string{"Small", "Large"},
|
||||||
|
"orderBy": "updated_at",
|
||||||
|
"orderDirection": "DESC",
|
||||||
|
"pageInfo": base.Json{
|
||||||
|
"pageCursor": nextPageCursor,
|
||||||
|
"pageSize": 100,
|
||||||
|
},
|
||||||
|
"parentFileId": fileId,
|
||||||
|
}
|
||||||
|
var resp PersonalListResp
|
||||||
|
_, err := d.personalPost("/hcy/file/list", data, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nextPageCursor = resp.Data.NextPageCursor
|
||||||
|
for _, item := range resp.Data.Items {
|
||||||
|
var isFolder = (item.Type == "folder")
|
||||||
|
var f model.Obj
|
||||||
|
if isFolder {
|
||||||
|
f = &model.Object{
|
||||||
|
ID: item.FileId,
|
||||||
|
Name: item.Name,
|
||||||
|
Size: 0,
|
||||||
|
Modified: getPersonalTime(item.UpdatedAt),
|
||||||
|
Ctime: getPersonalTime(item.CreatedAt),
|
||||||
|
IsFolder: isFolder,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var Thumbnails = item.Thumbnails
|
||||||
|
var ThumbnailUrl string
|
||||||
|
if len(Thumbnails) > 0 {
|
||||||
|
ThumbnailUrl = Thumbnails[len(Thumbnails)-1].Url
|
||||||
|
}
|
||||||
|
f = &model.ObjThumb{
|
||||||
|
Object: model.Object{
|
||||||
|
ID: item.FileId,
|
||||||
|
Name: item.Name,
|
||||||
|
Size: item.Size,
|
||||||
|
Modified: getPersonalTime(item.UpdatedAt),
|
||||||
|
Ctime: getPersonalTime(item.CreatedAt),
|
||||||
|
IsFolder: isFolder,
|
||||||
|
},
|
||||||
|
Thumbnail: model.Thumbnail{Thumbnail: ThumbnailUrl},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files = append(files, f)
|
||||||
|
}
|
||||||
|
if len(nextPageCursor) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Yun139) personalGetLink(fileId string) (string, error) {
|
||||||
|
data := base.Json{
|
||||||
|
"fileId": fileId,
|
||||||
|
}
|
||||||
|
res, err := d.personalPost("/hcy/file/getDownloadUrl",
|
||||||
|
data, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var cdnUrl = jsoniter.Get(res, "data", "cdnUrl").ToString()
|
||||||
|
if cdnUrl != "" {
|
||||||
|
return cdnUrl, nil
|
||||||
|
} else {
|
||||||
|
return jsoniter.Get(res, "data", "url").ToString(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Yun139) getAuthorization() string {
|
||||||
|
if d.ref != nil {
|
||||||
|
return d.ref.getAuthorization()
|
||||||
|
}
|
||||||
|
return d.Authorization
|
||||||
|
}
|
||||||
|
func (d *Yun139) getAccount() string {
|
||||||
|
if d.ref != nil {
|
||||||
|
return d.ref.getAccount()
|
||||||
|
}
|
||||||
|
return d.Account
|
||||||
|
}
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
|
|||||||
log.Debugf("uploadData: %+v", uploadData)
|
log.Debugf("uploadData: %+v", uploadData)
|
||||||
requestURL := uploadData.RequestURL
|
requestURL := uploadData.RequestURL
|
||||||
uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&")
|
uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&")
|
||||||
req, err := http.NewRequest(http.MethodPut, requestURL, bytes.NewReader(byteData))
|
req, err := http.NewRequest(http.MethodPut, requestURL, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -375,12 +375,12 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
|
|||||||
req.Header.Set(v[0:i], v[i+1:])
|
req.Header.Set(v[0:i], v[i+1:])
|
||||||
}
|
}
|
||||||
r, err := base.HttpClient.Do(req)
|
r, err := base.HttpClient.Do(req)
|
||||||
log.Debugf("%+v %+v", r, r.Request.Header)
|
|
||||||
r.Body.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
up(int(i * 100 / count))
|
log.Debugf("%+v %+v", r, r.Request.Header)
|
||||||
|
_ = r.Body.Close()
|
||||||
|
up(float64(i) * 100 / float64(count))
|
||||||
}
|
}
|
||||||
fileMd5 := hex.EncodeToString(md5Sum.Sum(nil))
|
fileMd5 := hex.EncodeToString(md5Sum.Sum(nil))
|
||||||
sliceMd5 := fileMd5
|
sliceMd5 := fileMd5
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package _189pc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ import (
|
|||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cloud189PC struct {
|
type Cloud189PC struct {
|
||||||
@@ -24,10 +27,21 @@ type Cloud189PC struct {
|
|||||||
|
|
||||||
loginParam *LoginParam
|
loginParam *LoginParam
|
||||||
tokenInfo *AppSessionResp
|
tokenInfo *AppSessionResp
|
||||||
|
|
||||||
|
uploadThread int
|
||||||
|
|
||||||
|
familyTransferFolder *Cloud189Folder
|
||||||
|
cleanFamilyTransferFile func()
|
||||||
|
|
||||||
|
storageConfig driver.Config
|
||||||
|
ref *Cloud189PC
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Config() driver.Config {
|
func (y *Cloud189PC) Config() driver.Config {
|
||||||
return config
|
if y.storageConfig.Name == "" {
|
||||||
|
y.storageConfig = config
|
||||||
|
}
|
||||||
|
return y.storageConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) GetAddition() driver.Additional {
|
func (y *Cloud189PC) GetAddition() driver.Additional {
|
||||||
@@ -35,47 +49,90 @@ func (y *Cloud189PC) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Init(ctx context.Context) (err error) {
|
func (y *Cloud189PC) Init(ctx context.Context) (err error) {
|
||||||
|
y.storageConfig = config
|
||||||
|
if y.isFamily() {
|
||||||
|
// 兼容旧上传接口
|
||||||
|
if y.Addition.RapidUpload || y.Addition.UploadMethod == "old" {
|
||||||
|
y.storageConfig.NoOverwriteUpload = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 家庭云转存,不支持覆盖上传
|
||||||
|
if y.Addition.FamilyTransfer {
|
||||||
|
y.storageConfig.NoOverwriteUpload = true
|
||||||
|
}
|
||||||
|
}
|
||||||
// 处理个人云和家庭云参数
|
// 处理个人云和家庭云参数
|
||||||
if y.isFamily() && y.RootFolderID == "-11" {
|
if y.isFamily() && y.RootFolderID == "-11" {
|
||||||
y.RootFolderID = ""
|
y.RootFolderID = ""
|
||||||
}
|
}
|
||||||
if !y.isFamily() && y.RootFolderID == "" {
|
if !y.isFamily() && y.RootFolderID == "" {
|
||||||
y.RootFolderID = "-11"
|
y.RootFolderID = "-11"
|
||||||
y.FamilyID = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化请求客户端
|
// 限制上传线程数
|
||||||
if y.client == nil {
|
y.uploadThread, _ = strconv.Atoi(y.UploadThread)
|
||||||
y.client = base.NewRestyClient().SetHeaders(map[string]string{
|
if y.uploadThread < 1 || y.uploadThread > 32 {
|
||||||
"Accept": "application/json;charset=UTF-8",
|
y.uploadThread, y.UploadThread = 3, "3"
|
||||||
"Referer": WEB_URL,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免重复登陆
|
if y.ref == nil {
|
||||||
identity := utils.GetMD5EncodeStr(y.Username + y.Password)
|
// 初始化请求客户端
|
||||||
if !y.isLogin() || y.identity != identity {
|
if y.client == nil {
|
||||||
y.identity = identity
|
y.client = base.NewRestyClient().SetHeaders(map[string]string{
|
||||||
if err = y.login(); err != nil {
|
"Accept": "application/json;charset=UTF-8",
|
||||||
return
|
"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
|
// 处理家庭云ID
|
||||||
if y.isFamily() && y.FamilyID == "" {
|
if y.FamilyID == "" {
|
||||||
if y.FamilyID, err = y.getFamilyID(); err != nil {
|
if y.FamilyID, err = y.getFamilyID(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建中转文件夹
|
||||||
|
if y.FamilyTransfer {
|
||||||
|
if err := y.createFamilyTransferFolder(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理转存文件节流
|
||||||
|
y.cleanFamilyTransferFile = utils.NewThrottle2(time.Minute, func() {
|
||||||
|
if err := y.cleanFamilyTransfer(context.TODO()); err != nil {
|
||||||
|
utils.Log.Errorf("cleanFamilyTransferFolderError:%s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Cloud189PC) InitReference(storage driver.Driver) error {
|
||||||
|
refStorage, ok := storage.(*Cloud189PC)
|
||||||
|
if ok {
|
||||||
|
d.ref = refStorage
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Drop(ctx context.Context) error {
|
func (y *Cloud189PC) Drop(ctx context.Context) error {
|
||||||
|
y.ref = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
return y.getFiles(ctx, dir.GetID())
|
return y.getFiles(ctx, dir.GetID(), y.isFamily())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
@@ -83,8 +140,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
|||||||
URL string `json:"fileDownloadUrl"`
|
URL string `json:"fileDownloadUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFamily := y.isFamily()
|
||||||
fullUrl := API_URL
|
fullUrl := API_URL
|
||||||
if y.isFamily() {
|
if isFamily {
|
||||||
fullUrl += "/family/file"
|
fullUrl += "/family/file"
|
||||||
}
|
}
|
||||||
fullUrl += "/getFileDownloadUrl.action"
|
fullUrl += "/getFileDownloadUrl.action"
|
||||||
@@ -92,7 +150,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
|||||||
_, err := y.get(fullUrl, func(r *resty.Request) {
|
_, err := y.get(fullUrl, func(r *resty.Request) {
|
||||||
r.SetContext(ctx)
|
r.SetContext(ctx)
|
||||||
r.SetQueryParam("fileId", file.GetID())
|
r.SetQueryParam("fileId", file.GetID())
|
||||||
if y.isFamily() {
|
if isFamily {
|
||||||
r.SetQueryParams(map[string]string{
|
r.SetQueryParams(map[string]string{
|
||||||
"familyId": y.FamilyID,
|
"familyId": y.FamilyID,
|
||||||
})
|
})
|
||||||
@@ -102,17 +160,18 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
|||||||
"flag": "1",
|
"flag": "1",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, &downloadUrl)
|
}, &downloadUrl, isFamily)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重定向获取真实链接
|
// 重定向获取真实链接
|
||||||
downloadUrl.URL = strings.Replace(strings.ReplaceAll(downloadUrl.URL, "&", "&"), "http://", "https://", 1)
|
downloadUrl.URL = strings.Replace(strings.ReplaceAll(downloadUrl.URL, "&", "&"), "http://", "https://", 1)
|
||||||
res, err := base.NoRedirectClient.R().SetContext(ctx).Get(downloadUrl.URL)
|
res, err := base.NoRedirectClient.R().SetContext(ctx).SetDoNotParseResponse(true).Get(downloadUrl.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer res.RawBody().Close()
|
||||||
if res.StatusCode() == 302 {
|
if res.StatusCode() == 302 {
|
||||||
downloadUrl.URL = res.Header().Get("location")
|
downloadUrl.URL = res.Header().Get("location")
|
||||||
}
|
}
|
||||||
@@ -138,8 +197,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
|
isFamily := y.isFamily()
|
||||||
fullUrl := API_URL
|
fullUrl := API_URL
|
||||||
if y.isFamily() {
|
if isFamily {
|
||||||
fullUrl += "/family/file"
|
fullUrl += "/family/file"
|
||||||
}
|
}
|
||||||
fullUrl += "/createFolder.action"
|
fullUrl += "/createFolder.action"
|
||||||
@@ -151,7 +211,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
|
|||||||
"folderName": dirName,
|
"folderName": dirName,
|
||||||
"relativePath": "",
|
"relativePath": "",
|
||||||
})
|
})
|
||||||
if y.isFamily() {
|
if isFamily {
|
||||||
req.SetQueryParams(map[string]string{
|
req.SetQueryParams(map[string]string{
|
||||||
"familyId": y.FamilyID,
|
"familyId": y.FamilyID,
|
||||||
"parentId": parentDir.GetID(),
|
"parentId": parentDir.GetID(),
|
||||||
@@ -161,7 +221,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
|
|||||||
"parentFolderId": parentDir.GetID(),
|
"parentFolderId": parentDir.GetID(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, &newFolder)
|
}, &newFolder, isFamily)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -169,27 +229,14 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
var resp CreateBatchTaskResp
|
isFamily := y.isFamily()
|
||||||
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
other := map[string]string{"targetFileName": dstDir.GetName()}
|
||||||
req.SetContext(ctx)
|
|
||||||
req.SetFormData(map[string]string{
|
resp, err := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
|
||||||
"type": "MOVE",
|
FileId: srcObj.GetID(),
|
||||||
"taskInfos": MustString(utils.Json.MarshalToString(
|
FileName: srcObj.GetName(),
|
||||||
[]BatchTaskInfo{
|
IsFolder: BoolToNumber(srcObj.IsDir()),
|
||||||
{
|
})
|
||||||
FileId: srcObj.GetID(),
|
|
||||||
FileName: srcObj.GetName(),
|
|
||||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
"targetFolderId": dstDir.GetID(),
|
|
||||||
})
|
|
||||||
if y.isFamily() {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"familyId": y.FamilyID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -200,10 +247,11 @@ func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
|
isFamily := y.isFamily()
|
||||||
queryParam := make(map[string]string)
|
queryParam := make(map[string]string)
|
||||||
fullUrl := API_URL
|
fullUrl := API_URL
|
||||||
method := http.MethodPost
|
method := http.MethodPost
|
||||||
if y.isFamily() {
|
if isFamily {
|
||||||
fullUrl += "/family/file"
|
fullUrl += "/family/file"
|
||||||
method = http.MethodGet
|
method = http.MethodGet
|
||||||
queryParam["familyId"] = y.FamilyID
|
queryParam["familyId"] = y.FamilyID
|
||||||
@@ -227,7 +275,7 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
|
|||||||
|
|
||||||
_, err := y.request(fullUrl, method, func(req *resty.Request) {
|
_, err := y.request(fullUrl, method, func(req *resty.Request) {
|
||||||
req.SetContext(ctx).SetQueryParams(queryParam)
|
req.SetContext(ctx).SetQueryParams(queryParam)
|
||||||
}, nil, newObj)
|
}, nil, newObj, isFamily)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -235,28 +283,15 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
var resp CreateBatchTaskResp
|
isFamily := y.isFamily()
|
||||||
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
other := map[string]string{"targetFileName": dstDir.GetName()}
|
||||||
req.SetContext(ctx)
|
|
||||||
req.SetFormData(map[string]string{
|
resp, err := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
|
||||||
"type": "COPY",
|
FileId: srcObj.GetID(),
|
||||||
"taskInfos": MustString(utils.Json.MarshalToString(
|
FileName: srcObj.GetName(),
|
||||||
[]BatchTaskInfo{
|
IsFolder: BoolToNumber(srcObj.IsDir()),
|
||||||
{
|
})
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -264,27 +299,13 @@ func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
|
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
var resp CreateBatchTaskResp
|
isFamily := y.isFamily()
|
||||||
_, 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() {
|
resp, err := y.CreateBatchTask("DELETE", IF(isFamily, y.FamilyID, ""), "", nil, BatchTaskInfo{
|
||||||
req.SetFormData(map[string]string{
|
FileId: obj.GetID(),
|
||||||
"familyId": y.FamilyID,
|
FileName: obj.GetName(),
|
||||||
})
|
IsFolder: BoolToNumber(obj.IsDir()),
|
||||||
}
|
})
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -292,18 +313,87 @@ func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
return y.WaitBatchTask("DELETE", resp.TaskID, time.Millisecond*200)
|
return y.WaitBatchTask("DELETE", resp.TaskID, time.Millisecond*200)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (newObj model.Obj, err error) {
|
||||||
switch y.UploadMethod {
|
overwrite := true
|
||||||
case "old":
|
isFamily := y.isFamily()
|
||||||
return y.OldUpload(ctx, dstDir, stream, up)
|
|
||||||
|
// 响应时间长,按需启用
|
||||||
|
if y.Addition.RapidUpload && !stream.IsForceStreamUpload() {
|
||||||
|
if newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {
|
||||||
|
return newObj, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadMethod := y.UploadMethod
|
||||||
|
if stream.IsForceStreamUpload() {
|
||||||
|
uploadMethod = "stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 旧版上传家庭云也有限制
|
||||||
|
if uploadMethod == "old" {
|
||||||
|
return y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启家庭云转存
|
||||||
|
if !isFamily && y.FamilyTransfer {
|
||||||
|
// 修改上传目标为家庭云文件夹
|
||||||
|
transferDstDir := dstDir
|
||||||
|
dstDir = y.familyTransferFolder
|
||||||
|
|
||||||
|
// 使用临时文件名
|
||||||
|
srcName := stream.GetName()
|
||||||
|
stream = &WrapFileStreamer{
|
||||||
|
FileStreamer: stream,
|
||||||
|
Name: fmt.Sprintf("0%s.transfer", uuid.NewString()),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用家庭云上传
|
||||||
|
isFamily = true
|
||||||
|
overwrite = false
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if newObj != nil {
|
||||||
|
// 转存家庭云文件到个人云
|
||||||
|
err = y.SaveFamilyFileToPersonCloud(context.TODO(), y.FamilyID, newObj, transferDstDir, true)
|
||||||
|
// 删除家庭云源文件
|
||||||
|
go y.Delete(context.TODO(), y.FamilyID, newObj)
|
||||||
|
// 批量任务有概率删不掉
|
||||||
|
go y.cleanFamilyTransferFile()
|
||||||
|
// 转存失败返回错误
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找转存文件
|
||||||
|
var file *Cloud189File
|
||||||
|
file, err = y.findFileByName(context.TODO(), newObj.GetName(), transferDstDir.GetID(), false)
|
||||||
|
if err != nil {
|
||||||
|
if err == errs.ObjectNotFound {
|
||||||
|
err = fmt.Errorf("unknown error: No transfer file obtained %s", newObj.GetName())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重命名转存文件
|
||||||
|
newObj, err = y.Rename(context.TODO(), file, srcName)
|
||||||
|
if err != nil {
|
||||||
|
// 重命名失败删除源文件
|
||||||
|
_ = y.Delete(context.TODO(), "", file)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch uploadMethod {
|
||||||
case "rapid":
|
case "rapid":
|
||||||
return y.FastUpload(ctx, dstDir, stream, up)
|
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
||||||
case "stream":
|
case "stream":
|
||||||
if stream.GetSize() == 0 {
|
if stream.GetSize() == 0 {
|
||||||
return y.FastUpload(ctx, dstDir, stream, up)
|
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
return y.StreamUpload(ctx, dstDir, stream, up)
|
return y.StreamUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -160,9 +161,8 @@ func toDesc(o string) string {
|
|||||||
func ParseHttpHeader(str string) map[string]string {
|
func ParseHttpHeader(str string) map[string]string {
|
||||||
header := make(map[string]string)
|
header := make(map[string]string)
|
||||||
for _, value := range strings.Split(str, "&") {
|
for _, value := range strings.Split(str, "&") {
|
||||||
i := strings.Index(value, "=")
|
if k, v, found := strings.Cut(value, "="); found {
|
||||||
if i > 0 {
|
header[k] = v
|
||||||
header[strings.TrimSpace(value[0:i])] = strings.TrimSpace(value[i+1:])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return header
|
return header
|
||||||
@@ -193,3 +193,28 @@ func partSize(size int64) int64 {
|
|||||||
}
|
}
|
||||||
return DEFAULT
|
return DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isBool(bs ...bool) bool {
|
||||||
|
for _, b := range bs {
|
||||||
|
if b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IF[V any](o bool, t V, f V) V {
|
||||||
|
if o {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
type WrapFileStreamer struct {
|
||||||
|
model.FileStreamer
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WrapFileStreamer) GetName() string {
|
||||||
|
return w.Name
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ type Addition struct {
|
|||||||
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
|
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
|
||||||
FamilyID string `json:"family_id"`
|
FamilyID string `json:"family_id"`
|
||||||
UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"`
|
UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"`
|
||||||
|
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||||
|
FamilyTransfer bool `json:"family_transfer"`
|
||||||
|
RapidUpload bool `json:"rapid_upload"`
|
||||||
NoUseOcr bool `json:"no_use_ocr"`
|
NoUseOcr bool `json:"no_use_ocr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 居然有四种返回方式
|
// 居然有四种返回方式
|
||||||
@@ -141,7 +143,7 @@ type FamilyInfoListResp struct {
|
|||||||
type FamilyInfoResp struct {
|
type FamilyInfoResp struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
CreateTime string `json:"createTime"`
|
CreateTime string `json:"createTime"`
|
||||||
FamilyID int `json:"familyId"`
|
FamilyID int64 `json:"familyId"`
|
||||||
RemarkName string `json:"remarkName"`
|
RemarkName string `json:"remarkName"`
|
||||||
Type int `json:"type"`
|
Type int `json:"type"`
|
||||||
UseFlag int `json:"useFlag"`
|
UseFlag int `json:"useFlag"`
|
||||||
@@ -175,6 +177,14 @@ type Cloud189File struct {
|
|||||||
// StarLabel int64 `json:"starLabel"`
|
// StarLabel int64 `json:"starLabel"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cloud189File) CreateTime() time.Time {
|
||||||
|
return time.Time(c.CreateDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cloud189File) GetHash() utils.HashInfo {
|
||||||
|
return utils.NewHashInfo(utils.MD5, c.Md5)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Cloud189File) GetSize() int64 { return c.Size }
|
func (c *Cloud189File) GetSize() int64 { return c.Size }
|
||||||
func (c *Cloud189File) GetName() string { return c.Name }
|
func (c *Cloud189File) GetName() string { return c.Name }
|
||||||
func (c *Cloud189File) ModTime() time.Time { return time.Time(c.LastOpTime) }
|
func (c *Cloud189File) ModTime() time.Time { return time.Time(c.LastOpTime) }
|
||||||
@@ -199,6 +209,14 @@ type Cloud189Folder struct {
|
|||||||
// StarLabel int64 `json:"starLabel"`
|
// StarLabel int64 `json:"starLabel"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cloud189Folder) CreateTime() time.Time {
|
||||||
|
return time.Time(c.CreateDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cloud189Folder) GetHash() utils.HashInfo {
|
||||||
|
return utils.HashInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Cloud189Folder) GetSize() int64 { return 0 }
|
func (c *Cloud189Folder) GetSize() int64 { return 0 }
|
||||||
func (c *Cloud189Folder) GetName() string { return c.Name }
|
func (c *Cloud189Folder) GetName() string { return c.Name }
|
||||||
func (c *Cloud189Folder) ModTime() time.Time { return time.Time(c.LastOpTime) }
|
func (c *Cloud189Folder) ModTime() time.Time { return time.Time(c.LastOpTime) }
|
||||||
@@ -225,7 +243,12 @@ type BatchTaskInfo struct {
|
|||||||
// IsFolder 是否是文件夹,0-否,1-是
|
// IsFolder 是否是文件夹,0-否,1-是
|
||||||
IsFolder int `json:"isFolder"`
|
IsFolder int `json:"isFolder"`
|
||||||
// SrcParentId 文件所在父目录ID
|
// SrcParentId 文件所在父目录ID
|
||||||
//SrcParentId string `json:"srcParentId"`
|
SrcParentId string `json:"srcParentId,omitempty"`
|
||||||
|
|
||||||
|
/* 冲突管理 */
|
||||||
|
// 1 -> 跳过 2 -> 保留 3 -> 覆盖
|
||||||
|
DealWay int `json:"dealWay,omitempty"`
|
||||||
|
IsConflict int `json:"isConflict,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 上传部分 */
|
/* 上传部分 */
|
||||||
@@ -239,14 +262,25 @@ type InitMultiUploadResp struct {
|
|||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
type UploadUrlsResp struct {
|
type UploadUrlsResp struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
UploadUrls map[string]Part `json:"uploadUrls"`
|
Data map[string]UploadUrlsData `json:"uploadUrls"`
|
||||||
}
|
}
|
||||||
type Part struct {
|
type UploadUrlsData struct {
|
||||||
RequestURL string `json:"requestURL"`
|
RequestURL string `json:"requestURL"`
|
||||||
RequestHeader string `json:"requestHeader"`
|
RequestHeader string `json:"requestHeader"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UploadUrlInfo struct {
|
||||||
|
PartNumber int
|
||||||
|
Headers map[string]string
|
||||||
|
UploadUrlsData
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadProgress struct {
|
||||||
|
UploadInfo InitMultiUploadResp
|
||||||
|
UploadParts []string
|
||||||
|
}
|
||||||
|
|
||||||
/* 第二种上传方式 */
|
/* 第二种上传方式 */
|
||||||
type CreateUploadFileResp struct {
|
type CreateUploadFileResp struct {
|
||||||
// 上传文件请求ID
|
// 上传文件请求ID
|
||||||
@@ -327,6 +361,14 @@ type BatchTaskStateResp struct {
|
|||||||
TaskStatus int `json:"taskStatus"` //1 初始化 2 存在冲突 3 执行中,4 完成
|
TaskStatus int `json:"taskStatus"` //1 初始化 2 存在冲突 3 执行中,4 完成
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BatchTaskConflictTaskInfoResp struct {
|
||||||
|
SessionKey string `json:"sessionKey"`
|
||||||
|
TargetFolderID int `json:"targetFolderId"`
|
||||||
|
TaskID string `json:"taskId"`
|
||||||
|
TaskInfos []BatchTaskInfo
|
||||||
|
TaskType int `json:"taskType"`
|
||||||
|
}
|
||||||
|
|
||||||
/* query 加密参数*/
|
/* query 加密参数*/
|
||||||
type Params map[string]string
|
type Params map[string]string
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,12 @@ package alias
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
stdpath "path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
|
"github.com/alist-org/alist/v3/internal/fs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
)
|
)
|
||||||
@@ -45,6 +47,9 @@ func (d *Alias) Init(ctx context.Context) error {
|
|||||||
d.oneKey = k
|
d.oneKey = k
|
||||||
}
|
}
|
||||||
d.autoFlatten = true
|
d.autoFlatten = true
|
||||||
|
} else {
|
||||||
|
d.oneKey = ""
|
||||||
|
d.autoFlatten = false
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -87,8 +92,9 @@ func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
|
|||||||
return nil, errs.ObjectNotFound
|
return nil, errs.ObjectNotFound
|
||||||
}
|
}
|
||||||
var objs []model.Obj
|
var objs []model.Obj
|
||||||
|
fsArgs := &fs.ListArgs{NoLog: true, Refresh: args.Refresh}
|
||||||
for _, dst := range dsts {
|
for _, dst := range dsts {
|
||||||
tmp, err := d.list(ctx, dst, sub)
|
tmp, err := d.list(ctx, dst, sub, fsArgs)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
objs = append(objs, tmp...)
|
objs = append(objs, tmp...)
|
||||||
}
|
}
|
||||||
@@ -105,10 +111,211 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
|||||||
for _, dst := range dsts {
|
for _, dst := range dsts {
|
||||||
link, err := d.link(ctx, dst, sub, args)
|
link, err := d.link(ctx, dst, sub, args)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if !args.Redirect && len(link.URL) > 0 {
|
||||||
|
// 正常情况下 多并发 仅支持返回URL的驱动
|
||||||
|
// alias套娃alias 可以让crypt、mega等驱动(不返回URL的) 支持并发
|
||||||
|
if d.DownloadConcurrency > 0 {
|
||||||
|
link.Concurrency = d.DownloadConcurrency
|
||||||
|
}
|
||||||
|
if d.DownloadPartSize > 0 {
|
||||||
|
link.PartSize = d.DownloadPartSize * utils.KB
|
||||||
|
}
|
||||||
|
}
|
||||||
return link, nil
|
return link, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, errs.ObjectNotFound
|
return nil, errs.ObjectNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Alias) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
|
if !d.Writable {
|
||||||
|
return errs.PermissionDenied
|
||||||
|
}
|
||||||
|
reqPath, err := d.getReqPath(ctx, parentDir, true)
|
||||||
|
if err == nil {
|
||||||
|
return fs.MakeDir(ctx, stdpath.Join(*reqPath, dirName))
|
||||||
|
}
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name dirs cannot make sub-dir")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
if !d.Writable {
|
||||||
|
return errs.PermissionDenied
|
||||||
|
}
|
||||||
|
srcPath, err := d.getReqPath(ctx, srcObj, false)
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name files cannot be moved")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dstPath, err := d.getReqPath(ctx, dstDir, true)
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name dirs cannot be moved to")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fs.Move(ctx, *srcPath, *dstPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
|
if !d.Writable {
|
||||||
|
return errs.PermissionDenied
|
||||||
|
}
|
||||||
|
reqPath, err := d.getReqPath(ctx, srcObj, false)
|
||||||
|
if err == nil {
|
||||||
|
return fs.Rename(ctx, *reqPath, newName)
|
||||||
|
}
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name files cannot be Rename")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
if !d.Writable {
|
||||||
|
return errs.PermissionDenied
|
||||||
|
}
|
||||||
|
srcPath, err := d.getReqPath(ctx, srcObj, false)
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name files cannot be copied")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dstPath, err := d.getReqPath(ctx, dstDir, true)
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name dirs cannot be copied to")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = fs.Copy(ctx, *srcPath, *dstPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
if !d.Writable {
|
||||||
|
return errs.PermissionDenied
|
||||||
|
}
|
||||||
|
reqPath, err := d.getReqPath(ctx, obj, false)
|
||||||
|
if err == nil {
|
||||||
|
return fs.Remove(ctx, *reqPath)
|
||||||
|
}
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name files cannot be Delete")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
|
if !d.Writable {
|
||||||
|
return errs.PermissionDenied
|
||||||
|
}
|
||||||
|
reqPath, err := d.getReqPath(ctx, dstDir, true)
|
||||||
|
if err == nil {
|
||||||
|
return fs.PutDirectly(ctx, *reqPath, s)
|
||||||
|
}
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name dirs cannot be Put")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) PutURL(ctx context.Context, dstDir model.Obj, name, url string) error {
|
||||||
|
if !d.Writable {
|
||||||
|
return errs.PermissionDenied
|
||||||
|
}
|
||||||
|
reqPath, err := d.getReqPath(ctx, dstDir, true)
|
||||||
|
if err == nil {
|
||||||
|
return fs.PutURL(ctx, *reqPath, name, url)
|
||||||
|
}
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name files cannot offline download")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||||
|
root, sub := d.getRootAndPath(obj.GetPath())
|
||||||
|
dsts, ok := d.pathMap[root]
|
||||||
|
if !ok {
|
||||||
|
return nil, errs.ObjectNotFound
|
||||||
|
}
|
||||||
|
for _, dst := range dsts {
|
||||||
|
meta, err := d.getArchiveMeta(ctx, dst, sub, args)
|
||||||
|
if err == nil {
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||||
|
root, sub := d.getRootAndPath(obj.GetPath())
|
||||||
|
dsts, ok := d.pathMap[root]
|
||||||
|
if !ok {
|
||||||
|
return nil, errs.ObjectNotFound
|
||||||
|
}
|
||||||
|
for _, dst := range dsts {
|
||||||
|
l, err := d.listArchive(ctx, dst, sub, args)
|
||||||
|
if err == nil {
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||||
|
// alias的两个驱动,一个支持驱动提取,一个不支持,如何兼容?
|
||||||
|
// 如果访问的是不支持驱动提取的驱动内的压缩文件,GetArchiveMeta就会返回errs.NotImplement,提取URL前缀就会是/ae,Extract就不会被调用
|
||||||
|
// 如果访问的是支持驱动提取的驱动内的压缩文件,GetArchiveMeta就会返回有效值,提取URL前缀就会是/ad,Extract就会被调用
|
||||||
|
root, sub := d.getRootAndPath(obj.GetPath())
|
||||||
|
dsts, ok := d.pathMap[root]
|
||||||
|
if !ok {
|
||||||
|
return nil, errs.ObjectNotFound
|
||||||
|
}
|
||||||
|
for _, dst := range dsts {
|
||||||
|
link, err := d.extract(ctx, dst, sub, args)
|
||||||
|
if err == nil {
|
||||||
|
if !args.Redirect && len(link.URL) > 0 {
|
||||||
|
if d.DownloadConcurrency > 0 {
|
||||||
|
link.Concurrency = d.DownloadConcurrency
|
||||||
|
}
|
||||||
|
if d.DownloadPartSize > 0 {
|
||||||
|
link.PartSize = d.DownloadPartSize * utils.KB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return link, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error {
|
||||||
|
if !d.Writable {
|
||||||
|
return errs.PermissionDenied
|
||||||
|
}
|
||||||
|
srcPath, err := d.getReqPath(ctx, srcObj, false)
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name files cannot be decompressed")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dstPath, err := d.getReqPath(ctx, dstDir, true)
|
||||||
|
if errs.IsNotImplement(err) {
|
||||||
|
return errors.New("same-name dirs cannot be decompressed to")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = fs.ArchiveDecompress(ctx, *srcPath, *dstPath, args)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*Alias)(nil)
|
var _ driver.Driver = (*Alias)(nil)
|
||||||
|
|||||||
@@ -9,19 +9,28 @@ type Addition struct {
|
|||||||
// Usually one of two
|
// Usually one of two
|
||||||
// driver.RootPath
|
// driver.RootPath
|
||||||
// define other
|
// define other
|
||||||
Paths string `json:"paths" required:"true" type:"text"`
|
Paths string `json:"paths" required:"true" type:"text"`
|
||||||
|
ProtectSameName bool `json:"protect_same_name" default:"true" required:"false" help:"Protects same-name files from Delete or Rename"`
|
||||||
|
DownloadConcurrency int `json:"download_concurrency" default:"0" required:"false" type:"number" help:"Need to enable proxy"`
|
||||||
|
DownloadPartSize int `json:"download_part_size" default:"0" type:"number" required:"false" help:"Need to enable proxy. Unit: KB"`
|
||||||
|
Writable bool `json:"writable" type:"bool" default:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "Alias",
|
Name: "Alias",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
NoCache: true,
|
NoCache: true,
|
||||||
NoUpload: true,
|
NoUpload: false,
|
||||||
DefaultRoot: "/",
|
DefaultRoot: "/",
|
||||||
|
ProxyRangeOption: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
op.RegisterDriver(func() driver.Driver {
|
op.RegisterDriver(func() driver.Driver {
|
||||||
return &Alias{}
|
return &Alias{
|
||||||
|
Addition: Addition{
|
||||||
|
ProtectSameName: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ package alias
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
stdpath "path"
|
stdpath "path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/fs"
|
"github.com/alist-org/alist/v3/internal/fs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/alist-org/alist/v3/internal/sign"
|
"github.com/alist-org/alist/v3/internal/sign"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
"github.com/alist-org/alist/v3/server/common"
|
||||||
@@ -15,7 +19,7 @@ import (
|
|||||||
|
|
||||||
func (d *Alias) listRoot() []model.Obj {
|
func (d *Alias) listRoot() []model.Obj {
|
||||||
var objs []model.Obj
|
var objs []model.Obj
|
||||||
for k, _ := range d.pathMap {
|
for k := range d.pathMap {
|
||||||
obj := model.Object{
|
obj := model.Object{
|
||||||
Name: k,
|
Name: k,
|
||||||
IsFolder: true,
|
IsFolder: true,
|
||||||
@@ -61,11 +65,12 @@ func (d *Alias) get(ctx context.Context, path string, dst, sub string) (model.Ob
|
|||||||
Size: obj.GetSize(),
|
Size: obj.GetSize(),
|
||||||
Modified: obj.ModTime(),
|
Modified: obj.ModTime(),
|
||||||
IsFolder: obj.IsDir(),
|
IsFolder: obj.IsDir(),
|
||||||
|
HashInfo: obj.GetHash(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Alias) list(ctx context.Context, dst, sub string) ([]model.Obj, error) {
|
func (d *Alias) list(ctx context.Context, dst, sub string, args *fs.ListArgs) ([]model.Obj, error) {
|
||||||
objs, err := fs.List(ctx, stdpath.Join(dst, sub), &fs.ListArgs{NoLog: true})
|
objs, err := fs.List(ctx, stdpath.Join(dst, sub), args)
|
||||||
// the obj must implement the model.SetPath interface
|
// the obj must implement the model.SetPath interface
|
||||||
// return objs, err
|
// return objs, err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -93,22 +98,128 @@ func (d *Alias) list(ctx context.Context, dst, sub string) ([]model.Obj, error)
|
|||||||
|
|
||||||
func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) {
|
func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) {
|
||||||
reqPath := stdpath.Join(dst, sub)
|
reqPath := stdpath.Join(dst, sub)
|
||||||
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
|
// 参考 crypt 驱动
|
||||||
|
storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if _, ok := storage.(*Alias); !ok && !args.Redirect {
|
||||||
|
link, _, err := op.Link(ctx, storage, reqActualPath, args)
|
||||||
|
return link, err
|
||||||
|
}
|
||||||
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
|
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if common.ShouldProxy(storage, stdpath.Base(sub)) {
|
if common.ShouldProxy(storage, stdpath.Base(sub)) {
|
||||||
return &model.Link{
|
link := &model.Link{
|
||||||
URL: fmt.Sprintf("%s/p%s?sign=%s",
|
URL: fmt.Sprintf("%s/p%s?sign=%s",
|
||||||
common.GetApiUrl(args.HttpReq),
|
common.GetApiUrl(args.HttpReq),
|
||||||
utils.EncodePath(reqPath, true),
|
utils.EncodePath(reqPath, true),
|
||||||
sign.Sign(reqPath)),
|
sign.Sign(reqPath)),
|
||||||
}, nil
|
}
|
||||||
|
if args.HttpReq != nil && d.ProxyRange {
|
||||||
|
link.RangeReadCloser = common.NoProxyRange
|
||||||
|
}
|
||||||
|
return link, nil
|
||||||
}
|
}
|
||||||
link, _, err := fs.Link(ctx, reqPath, args)
|
link, _, err := op.Link(ctx, storage, reqActualPath, args)
|
||||||
return link, err
|
return link, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Alias) getReqPath(ctx context.Context, obj model.Obj, isParent bool) (*string, error) {
|
||||||
|
root, sub := d.getRootAndPath(obj.GetPath())
|
||||||
|
if sub == "" && !isParent {
|
||||||
|
return nil, errs.NotSupport
|
||||||
|
}
|
||||||
|
dsts, ok := d.pathMap[root]
|
||||||
|
if !ok {
|
||||||
|
return nil, errs.ObjectNotFound
|
||||||
|
}
|
||||||
|
var reqPath *string
|
||||||
|
for _, dst := range dsts {
|
||||||
|
path := stdpath.Join(dst, sub)
|
||||||
|
_, err := fs.Get(ctx, path, &fs.GetArgs{NoLog: true})
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !d.ProtectSameName {
|
||||||
|
return &path, nil
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
ok = false
|
||||||
|
} else {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
reqPath = &path
|
||||||
|
}
|
||||||
|
if reqPath == nil {
|
||||||
|
return nil, errs.ObjectNotFound
|
||||||
|
}
|
||||||
|
return reqPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) getArchiveMeta(ctx context.Context, dst, sub string, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||||
|
reqPath := stdpath.Join(dst, sub)
|
||||||
|
storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, ok := storage.(driver.ArchiveReader); ok {
|
||||||
|
return op.GetArchiveMeta(ctx, storage, reqActualPath, model.ArchiveMetaArgs{
|
||||||
|
ArchiveArgs: args,
|
||||||
|
Refresh: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) listArchive(ctx context.Context, dst, sub string, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||||
|
reqPath := stdpath.Join(dst, sub)
|
||||||
|
storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, ok := storage.(driver.ArchiveReader); ok {
|
||||||
|
return op.ListArchive(ctx, storage, reqActualPath, model.ArchiveListArgs{
|
||||||
|
ArchiveInnerArgs: args,
|
||||||
|
Refresh: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) extract(ctx context.Context, dst, sub string, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||||
|
reqPath := stdpath.Join(dst, sub)
|
||||||
|
storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, ok := storage.(driver.ArchiveReader); ok {
|
||||||
|
if _, ok := storage.(*Alias); !ok && !args.Redirect {
|
||||||
|
link, _, err := op.DriverExtract(ctx, storage, reqActualPath, args)
|
||||||
|
return link, err
|
||||||
|
}
|
||||||
|
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if common.ShouldProxy(storage, stdpath.Base(sub)) {
|
||||||
|
link := &model.Link{
|
||||||
|
URL: fmt.Sprintf("%s/ap%s?inner=%s&pass=%s&sign=%s",
|
||||||
|
common.GetApiUrl(args.HttpReq),
|
||||||
|
utils.EncodePath(reqPath, true),
|
||||||
|
utils.EncodePath(args.InnerPath, true),
|
||||||
|
url.QueryEscape(args.Password),
|
||||||
|
sign.SignArchive(reqPath)),
|
||||||
|
}
|
||||||
|
if args.HttpReq != nil && d.ProxyRange {
|
||||||
|
link.RangeReadCloser = common.NoProxyRange
|
||||||
|
}
|
||||||
|
return link, nil
|
||||||
|
}
|
||||||
|
link, _, err := op.DriverExtract(ctx, storage, reqActualPath, args)
|
||||||
|
return link, err
|
||||||
|
}
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,18 +3,21 @@ package alist_v3
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
"github.com/alist-org/alist/v3/server/common"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AListV3 struct {
|
type AListV3 struct {
|
||||||
@@ -33,29 +36,29 @@ func (d *AListV3) GetAddition() driver.Additional {
|
|||||||
func (d *AListV3) Init(ctx context.Context) error {
|
func (d *AListV3) Init(ctx context.Context) error {
|
||||||
d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/")
|
d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/")
|
||||||
var resp common.Resp[MeResp]
|
var resp common.Resp[MeResp]
|
||||||
_, err := d.request("/me", http.MethodGet, func(req *resty.Request) {
|
_, _, err := d.request("/me", http.MethodGet, func(req *resty.Request) {
|
||||||
req.SetResult(&resp)
|
req.SetResult(&resp)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// if the username is not empty and the username is not the same as the current username, then login again
|
// 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 {
|
if d.Username != resp.Data.Username {
|
||||||
err = d.login()
|
err = d.login()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// re-get the user info
|
// re-get the user info
|
||||||
_, err = d.request("/me", http.MethodGet, func(req *resty.Request) {
|
_, _, err = d.request("/me", http.MethodGet, func(req *resty.Request) {
|
||||||
req.SetResult(&resp)
|
req.SetResult(&resp)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if resp.Data.Role == model.GUEST {
|
if resp.Data.Role == model.GUEST {
|
||||||
url := d.Address + "/api/public/settings"
|
u := d.Address + "/api/public/settings"
|
||||||
res, err := base.RestyClient.R().Get(url)
|
res, err := base.RestyClient.R().Get(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -73,7 +76,7 @@ func (d *AListV3) Drop(ctx context.Context) error {
|
|||||||
|
|
||||||
func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
var resp common.Resp[FsListResp]
|
var resp common.Resp[FsListResp]
|
||||||
_, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) {
|
_, _, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetResult(&resp).SetBody(ListReq{
|
req.SetResult(&resp).SetBody(ListReq{
|
||||||
PageReq: model.PageReq{
|
PageReq: model.PageReq{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
@@ -93,8 +96,10 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
|||||||
Object: model.Object{
|
Object: model.Object{
|
||||||
Name: f.Name,
|
Name: f.Name,
|
||||||
Modified: f.Modified,
|
Modified: f.Modified,
|
||||||
|
Ctime: f.Created,
|
||||||
Size: f.Size,
|
Size: f.Size,
|
||||||
IsFolder: f.IsDir,
|
IsFolder: f.IsDir,
|
||||||
|
HashInfo: utils.FromString(f.HashInfo),
|
||||||
},
|
},
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
|
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
|
||||||
}
|
}
|
||||||
@@ -105,11 +110,19 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
|||||||
|
|
||||||
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
var resp common.Resp[FsGetResp]
|
var resp common.Resp[FsGetResp]
|
||||||
_, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
|
// if PassUAToUpsteam is true, then pass the user-agent to the upstream
|
||||||
|
userAgent := base.UserAgent
|
||||||
|
if d.PassUAToUpsteam {
|
||||||
|
userAgent = args.Header.Get("user-agent")
|
||||||
|
if userAgent == "" {
|
||||||
|
userAgent = base.UserAgent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetResult(&resp).SetBody(FsGetReq{
|
req.SetResult(&resp).SetBody(FsGetReq{
|
||||||
Path: file.GetPath(),
|
Path: file.GetPath(),
|
||||||
Password: d.MetaPassword,
|
Password: d.MetaPassword,
|
||||||
})
|
}).SetHeader("user-agent", userAgent)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -120,7 +133,7 @@ func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
_, err := d.request("/fs/mkdir", http.MethodPost, func(req *resty.Request) {
|
_, _, err := d.request("/fs/mkdir", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(MkdirOrLinkReq{
|
req.SetBody(MkdirOrLinkReq{
|
||||||
Path: path.Join(parentDir.GetPath(), dirName),
|
Path: path.Join(parentDir.GetPath(), dirName),
|
||||||
})
|
})
|
||||||
@@ -129,7 +142,7 @@ func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
_, err := d.request("/fs/move", http.MethodPost, func(req *resty.Request) {
|
_, _, err := d.request("/fs/move", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(MoveCopyReq{
|
req.SetBody(MoveCopyReq{
|
||||||
SrcDir: path.Dir(srcObj.GetPath()),
|
SrcDir: path.Dir(srcObj.GetPath()),
|
||||||
DstDir: dstDir.GetPath(),
|
DstDir: dstDir.GetPath(),
|
||||||
@@ -140,7 +153,7 @@ func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
_, err := d.request("/fs/rename", http.MethodPost, func(req *resty.Request) {
|
_, _, err := d.request("/fs/rename", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(RenameReq{
|
req.SetBody(RenameReq{
|
||||||
Path: srcObj.GetPath(),
|
Path: srcObj.GetPath(),
|
||||||
Name: newName,
|
Name: newName,
|
||||||
@@ -150,7 +163,7 @@ func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
_, err := d.request("/fs/copy", http.MethodPost, func(req *resty.Request) {
|
_, _, err := d.request("/fs/copy", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(MoveCopyReq{
|
req.SetBody(MoveCopyReq{
|
||||||
SrcDir: path.Dir(srcObj.GetPath()),
|
SrcDir: path.Dir(srcObj.GetPath()),
|
||||||
DstDir: dstDir.GetPath(),
|
DstDir: dstDir.GetPath(),
|
||||||
@@ -161,7 +174,7 @@ func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
|
func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
_, err := d.request("/fs/remove", http.MethodPost, func(req *resty.Request) {
|
_, _, err := d.request("/fs/remove", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(RemoveReq{
|
req.SetBody(RemoveReq{
|
||||||
Dir: path.Dir(obj.GetPath()),
|
Dir: path.Dir(obj.GetPath()),
|
||||||
Names: []string{obj.GetName()},
|
Names: []string{obj.GetName()},
|
||||||
@@ -170,13 +183,174 @@ func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
_, err := d.request("/fs/put", http.MethodPut, func(req *resty.Request) {
|
reader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
|
||||||
req.SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
|
Reader: s,
|
||||||
SetHeader("Password", d.MetaPassword).
|
UpdateProgress: up,
|
||||||
SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
|
})
|
||||||
SetContentLength(true).
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+"/api/fs/put", reader)
|
||||||
SetBody(stream.GetReadCloser())
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", d.Token)
|
||||||
|
req.Header.Set("File-Path", path.Join(dstDir.GetPath(), s.GetName()))
|
||||||
|
req.Header.Set("Password", d.MetaPassword)
|
||||||
|
if md5 := s.GetHash().GetHash(utils.MD5); len(md5) > 0 {
|
||||||
|
req.Header.Set("X-File-Md5", md5)
|
||||||
|
}
|
||||||
|
if sha1 := s.GetHash().GetHash(utils.SHA1); len(sha1) > 0 {
|
||||||
|
req.Header.Set("X-File-Sha1", sha1)
|
||||||
|
}
|
||||||
|
if sha256 := s.GetHash().GetHash(utils.SHA256); len(sha256) > 0 {
|
||||||
|
req.Header.Set("X-File-Sha256", sha256)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ContentLength = s.GetSize()
|
||||||
|
// client := base.NewHttpClient()
|
||||||
|
// client.Timeout = time.Hour * 6
|
||||||
|
res, err := base.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf("[alist_v3] response body: %s", string(bytes))
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return fmt.Errorf("request failed, status: %s", res.Status)
|
||||||
|
}
|
||||||
|
code := utils.Json.Get(bytes, "code").ToInt()
|
||||||
|
if code != 200 {
|
||||||
|
if code == 401 || code == 403 {
|
||||||
|
err = d.login()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(bytes, "message").ToString())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AListV3) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||||
|
if !d.ForwardArchiveReq {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
var resp common.Resp[ArchiveMetaResp]
|
||||||
|
_, code, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetResult(&resp).SetBody(ArchiveMetaReq{
|
||||||
|
ArchivePass: args.Password,
|
||||||
|
Password: d.MetaPassword,
|
||||||
|
Path: obj.GetPath(),
|
||||||
|
Refresh: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if code == 202 {
|
||||||
|
return nil, errs.WrongArchivePassword
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var tree []model.ObjTree
|
||||||
|
if resp.Data.Content != nil {
|
||||||
|
tree = make([]model.ObjTree, 0, len(resp.Data.Content))
|
||||||
|
for _, content := range resp.Data.Content {
|
||||||
|
tree = append(tree, &content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &model.ArchiveMetaInfo{
|
||||||
|
Comment: resp.Data.Comment,
|
||||||
|
Encrypted: resp.Data.Encrypted,
|
||||||
|
Tree: tree,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AListV3) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||||
|
if !d.ForwardArchiveReq {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
var resp common.Resp[ArchiveListResp]
|
||||||
|
_, code, err := d.request("/fs/archive/list", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetResult(&resp).SetBody(ArchiveListReq{
|
||||||
|
ArchiveMetaReq: ArchiveMetaReq{
|
||||||
|
ArchivePass: args.Password,
|
||||||
|
Password: d.MetaPassword,
|
||||||
|
Path: obj.GetPath(),
|
||||||
|
Refresh: false,
|
||||||
|
},
|
||||||
|
PageReq: model.PageReq{
|
||||||
|
Page: 1,
|
||||||
|
PerPage: 0,
|
||||||
|
},
|
||||||
|
InnerPath: args.InnerPath,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if code == 202 {
|
||||||
|
return nil, errs.WrongArchivePassword
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
Ctime: f.Created,
|
||||||
|
Size: f.Size,
|
||||||
|
IsFolder: f.IsDir,
|
||||||
|
HashInfo: utils.FromString(f.HashInfo),
|
||||||
|
},
|
||||||
|
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
|
||||||
|
}
|
||||||
|
files = append(files, &file)
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AListV3) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||||
|
if !d.ForwardArchiveReq {
|
||||||
|
return nil, errs.NotSupport
|
||||||
|
}
|
||||||
|
var resp common.Resp[ArchiveMetaResp]
|
||||||
|
_, _, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetResult(&resp).SetBody(ArchiveMetaReq{
|
||||||
|
ArchivePass: args.Password,
|
||||||
|
Password: d.MetaPassword,
|
||||||
|
Path: obj.GetPath(),
|
||||||
|
Refresh: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.Link{
|
||||||
|
URL: fmt.Sprintf("%s?inner=%s&pass=%s&sign=%s",
|
||||||
|
resp.Data.RawURL,
|
||||||
|
utils.EncodePath(args.InnerPath, true),
|
||||||
|
url.QueryEscape(args.Password),
|
||||||
|
resp.Data.Sign),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AListV3) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error {
|
||||||
|
if !d.ForwardArchiveReq {
|
||||||
|
return errs.NotImplement
|
||||||
|
}
|
||||||
|
dir, name := path.Split(srcObj.GetPath())
|
||||||
|
_, _, err := d.request("/fs/archive/decompress", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetBody(DecompressReq{
|
||||||
|
ArchivePass: args.Password,
|
||||||
|
CacheFull: args.CacheFull,
|
||||||
|
DstDir: dstDir.GetPath(),
|
||||||
|
InnerPath: args.InnerPath,
|
||||||
|
Name: []string{name},
|
||||||
|
PutIntoNewDir: args.PutIntoNewDir,
|
||||||
|
SrcDir: dir,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,21 @@ import (
|
|||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
driver.RootPath
|
driver.RootPath
|
||||||
Address string `json:"url" required:"true"`
|
Address string `json:"url" required:"true"`
|
||||||
MetaPassword string `json:"meta_password"`
|
MetaPassword string `json:"meta_password"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
PassUAToUpsteam bool `json:"pass_ua_to_upsteam" default:"true"`
|
||||||
|
ForwardArchiveReq bool `json:"forward_archive_requests" default:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "AList V3",
|
Name: "AList V3",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
DefaultRoot: "/",
|
DefaultRoot: "/",
|
||||||
CheckStatus: true,
|
CheckStatus: true,
|
||||||
|
ProxyRangeOption: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListReq struct {
|
type ListReq struct {
|
||||||
@@ -18,9 +19,11 @@ type ObjResp struct {
|
|||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
IsDir bool `json:"is_dir"`
|
IsDir bool `json:"is_dir"`
|
||||||
Modified time.Time `json:"modified"`
|
Modified time.Time `json:"modified"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
Sign string `json:"sign"`
|
Sign string `json:"sign"`
|
||||||
Thumb string `json:"thumb"`
|
Thumb string `json:"thumb"`
|
||||||
Type int `json:"type"`
|
Type int `json:"type"`
|
||||||
|
HashInfo string `json:"hashinfo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FsListResp struct {
|
type FsListResp struct {
|
||||||
@@ -79,3 +82,89 @@ type MeResp struct {
|
|||||||
SsoId string `json:"sso_id"`
|
SsoId string `json:"sso_id"`
|
||||||
Otp bool `json:"otp"`
|
Otp bool `json:"otp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArchiveMetaReq struct {
|
||||||
|
ArchivePass string `json:"archive_pass"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Refresh bool `json:"refresh"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TreeResp struct {
|
||||||
|
ObjResp
|
||||||
|
Children []TreeResp `json:"children"`
|
||||||
|
hashCache *utils.HashInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TreeResp) GetSize() int64 {
|
||||||
|
return t.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TreeResp) GetName() string {
|
||||||
|
return t.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TreeResp) ModTime() time.Time {
|
||||||
|
return t.Modified
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TreeResp) CreateTime() time.Time {
|
||||||
|
return t.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TreeResp) IsDir() bool {
|
||||||
|
return t.ObjResp.IsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TreeResp) GetHash() utils.HashInfo {
|
||||||
|
return utils.FromString(t.HashInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TreeResp) GetID() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TreeResp) GetPath() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TreeResp) GetChildren() []model.ObjTree {
|
||||||
|
ret := make([]model.ObjTree, 0, len(t.Children))
|
||||||
|
for _, child := range t.Children {
|
||||||
|
ret = append(ret, &child)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TreeResp) Thumb() string {
|
||||||
|
return t.ObjResp.Thumb
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArchiveMetaResp struct {
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
Encrypted bool `json:"encrypted"`
|
||||||
|
Content []TreeResp `json:"content"`
|
||||||
|
RawURL string `json:"raw_url"`
|
||||||
|
Sign string `json:"sign"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArchiveListReq struct {
|
||||||
|
model.PageReq
|
||||||
|
ArchiveMetaReq
|
||||||
|
InnerPath string `json:"inner_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArchiveListResp struct {
|
||||||
|
Content []ObjResp `json:"content"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecompressReq struct {
|
||||||
|
ArchivePass string `json:"archive_pass"`
|
||||||
|
CacheFull bool `json:"cache_full"`
|
||||||
|
DstDir string `json:"dst_dir"`
|
||||||
|
InnerPath string `json:"inner_path"`
|
||||||
|
Name []string `json:"name"`
|
||||||
|
PutIntoNewDir bool `json:"put_into_new_dir"`
|
||||||
|
SrcDir string `json:"src_dir"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (d *AListV3) login() error {
|
func (d *AListV3) login() error {
|
||||||
|
if d.Username == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var resp common.Resp[LoginResp]
|
var resp common.Resp[LoginResp]
|
||||||
_, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) {
|
_, _, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetResult(&resp).SetBody(base.Json{
|
req.SetResult(&resp).SetBody(base.Json{
|
||||||
"username": d.Username,
|
"username": d.Username,
|
||||||
"password": d.Password,
|
"password": d.Password,
|
||||||
@@ -28,7 +31,7 @@ func (d *AListV3) login() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AListV3) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
|
func (d *AListV3) request(api, method string, callback base.ReqCallback, retry ...bool) ([]byte, int, error) {
|
||||||
url := d.Address + "/api" + api
|
url := d.Address + "/api" + api
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
req.SetHeader("Authorization", d.Token)
|
req.SetHeader("Authorization", d.Token)
|
||||||
@@ -37,22 +40,26 @@ func (d *AListV3) request(api, method string, callback base.ReqCallback, retry .
|
|||||||
}
|
}
|
||||||
res, err := req.Execute(method, url)
|
res, err := req.Execute(method, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
code := 0
|
||||||
|
if res != nil {
|
||||||
|
code = res.StatusCode()
|
||||||
|
}
|
||||||
|
return nil, code, err
|
||||||
}
|
}
|
||||||
log.Debugf("[alist_v3] response body: %s", res.String())
|
log.Debugf("[alist_v3] response body: %s", res.String())
|
||||||
if res.StatusCode() >= 400 {
|
if res.StatusCode() >= 400 {
|
||||||
return nil, fmt.Errorf("request failed, status: %s", res.Status())
|
return nil, res.StatusCode(), fmt.Errorf("request failed, status: %s", res.Status())
|
||||||
}
|
}
|
||||||
code := utils.Json.Get(res.Body(), "code").ToInt()
|
code := utils.Json.Get(res.Body(), "code").ToInt()
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
if (code == 401 || code == 403) && !utils.IsBool(retry...) {
|
if (code == 401 || code == 403) && !utils.IsBool(retry...) {
|
||||||
err = d.login()
|
err = d.login()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, code, err
|
||||||
}
|
}
|
||||||
return d.request(api, method, callback, true)
|
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 nil, code, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString())
|
||||||
}
|
}
|
||||||
return res.Body(), nil
|
return res.Body(), 200, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/internal/stream"
|
||||||
"github.com/alist-org/alist/v3/pkg/cron"
|
"github.com/alist-org/alist/v3/pkg/cron"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
@@ -50,7 +51,7 @@ func (d *AliDrive) Init(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// get driver id
|
// get driver id
|
||||||
res, err, _ := d.request("https://api.aliyundrive.com/v2/user/get", http.MethodPost, nil, nil)
|
res, err, _ := d.request("https://api.alipan.com/v2/user/get", http.MethodPost, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -67,7 +68,7 @@ func (d *AliDrive) Init(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// init deviceID
|
// init deviceID
|
||||||
deviceID := utils.GetSHA256Encode([]byte(d.UserID))
|
deviceID := utils.HashData(utils.SHA256, []byte(d.UserID))
|
||||||
// init privateKey
|
// init privateKey
|
||||||
privateKey, _ := NewPrivateKeyFromHex(deviceID)
|
privateKey, _ := NewPrivateKeyFromHex(deviceID)
|
||||||
state := State{
|
state := State{
|
||||||
@@ -104,7 +105,7 @@ func (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs
|
|||||||
"file_id": file.GetID(),
|
"file_id": file.GetID(),
|
||||||
"expire_sec": 14400,
|
"expire_sec": 14400,
|
||||||
}
|
}
|
||||||
res, err, _ := d.request("https://api.aliyundrive.com/v2/file/get_download_url", http.MethodPost, func(req *resty.Request) {
|
res, err, _ := d.request("https://api.alipan.com/v2/file/get_download_url", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data)
|
req.SetBody(data)
|
||||||
}, nil)
|
}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -112,14 +113,14 @@ func (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs
|
|||||||
}
|
}
|
||||||
return &model.Link{
|
return &model.Link{
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
"Referer": []string{"https://www.aliyundrive.com/"},
|
"Referer": []string{"https://www.alipan.com/"},
|
||||||
},
|
},
|
||||||
URL: utils.Json.Get(res, "url").ToString(),
|
URL: utils.Json.Get(res, "url").ToString(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
func (d *AliDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
_, err, _ := d.request("https://api.aliyundrive.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
|
_, err, _ := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"check_name_mode": "refuse",
|
"check_name_mode": "refuse",
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
@@ -137,7 +138,7 @@ func (d *AliDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
func (d *AliDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
_, err, _ := d.request("https://api.aliyundrive.com/v3/file/update", http.MethodPost, func(req *resty.Request) {
|
_, err, _ := d.request("https://api.alipan.com/v3/file/update", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"check_name_mode": "refuse",
|
"check_name_mode": "refuse",
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
@@ -154,7 +155,7 @@ func (d *AliDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliDrive) Remove(ctx context.Context, obj model.Obj) error {
|
func (d *AliDrive) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
_, err, _ := d.request("https://api.aliyundrive.com/v2/recyclebin/trash", http.MethodPost, func(req *resty.Request) {
|
_, err, _ := d.request("https://api.alipan.com/v2/recyclebin/trash", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
"file_id": obj.GetID(),
|
"file_id": obj.GetID(),
|
||||||
@@ -163,14 +164,14 @@ func (d *AliDrive) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
file := model.FileStream{
|
file := stream.FileStream{
|
||||||
Obj: stream,
|
Obj: streamer,
|
||||||
ReadCloser: stream,
|
Reader: streamer,
|
||||||
Mimetype: stream.GetMimetype(),
|
Mimetype: streamer.GetMimetype(),
|
||||||
}
|
}
|
||||||
const DEFAULT int64 = 10485760
|
const DEFAULT int64 = 10485760
|
||||||
var count = int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT)))
|
var count = int(math.Ceil(float64(streamer.GetSize()) / float64(DEFAULT)))
|
||||||
|
|
||||||
partInfoList := make([]base.Json, 0, count)
|
partInfoList := make([]base.Json, 0, count)
|
||||||
for i := 1; i <= count; i++ {
|
for i := 1; i <= count; i++ {
|
||||||
@@ -187,25 +188,28 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
|||||||
}
|
}
|
||||||
|
|
||||||
var localFile *os.File
|
var localFile *os.File
|
||||||
if fileStream, ok := file.ReadCloser.(*model.FileStream); ok {
|
if fileStream, ok := file.Reader.(*stream.FileStream); ok {
|
||||||
localFile, _ = fileStream.ReadCloser.(*os.File)
|
localFile, _ = fileStream.Reader.(*os.File)
|
||||||
}
|
}
|
||||||
if d.RapidUpload {
|
if d.RapidUpload {
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||||
io.CopyN(buf, file, 1024)
|
_, err := utils.CopyWithBufferN(buf, file, 1024)
|
||||||
reqBody["pre_hash"] = utils.GetSHA1Encode(buf.Bytes())
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reqBody["pre_hash"] = utils.HashData(utils.SHA1, buf.Bytes())
|
||||||
if localFile != nil {
|
if localFile != nil {
|
||||||
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
|
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 把头部拼接回去
|
// 把头部拼接回去
|
||||||
file.ReadCloser = struct {
|
file.Reader = struct {
|
||||||
io.Reader
|
io.Reader
|
||||||
io.Closer
|
io.Closer
|
||||||
}{
|
}{
|
||||||
Reader: io.MultiReader(buf, file),
|
Reader: io.MultiReader(buf, file),
|
||||||
Closer: file,
|
Closer: &file,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -214,7 +218,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
|||||||
}
|
}
|
||||||
|
|
||||||
var resp UploadResp
|
var resp UploadResp
|
||||||
_, err, e := d.request("https://api.aliyundrive.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
|
_, err, e := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(reqBody)
|
req.SetBody(reqBody)
|
||||||
}, &resp)
|
}, &resp)
|
||||||
|
|
||||||
@@ -268,7 +272,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
|||||||
n, _ := io.NewSectionReader(localFile, o.Int64(), 8).Read(buf[:8])
|
n, _ := io.NewSectionReader(localFile, o.Int64(), 8).Read(buf[:8])
|
||||||
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
|
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
|
||||||
|
|
||||||
_, err, e := d.request("https://api.aliyundrive.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
|
_, err, e := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(reqBody)
|
req.SetBody(reqBody)
|
||||||
}, &resp)
|
}, &resp)
|
||||||
if err != nil && e.Code != "PreHashMatched" {
|
if err != nil && e.Code != "PreHashMatched" {
|
||||||
@@ -281,9 +285,10 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
|||||||
if _, err = localFile.Seek(0, io.SeekStart); err != nil {
|
if _, err = localFile.Seek(0, io.SeekStart); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
file.ReadCloser = localFile
|
file.Reader = localFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rateLimited := driver.NewLimitedUploadStream(ctx, file)
|
||||||
for i, partInfo := range resp.PartInfoList {
|
for i, partInfo := range resp.PartInfoList {
|
||||||
if utils.IsCanceled(ctx) {
|
if utils.IsCanceled(ctx) {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
@@ -292,7 +297,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
|||||||
if d.InternalUpload {
|
if d.InternalUpload {
|
||||||
url = partInfo.InternalUploadUrl
|
url = partInfo.InternalUploadUrl
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest("PUT", url, io.LimitReader(file, DEFAULT))
|
req, err := http.NewRequest("PUT", url, io.LimitReader(rateLimited, DEFAULT))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -301,13 +306,13 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
res.Body.Close()
|
_ = res.Body.Close()
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
up(i * 100 / count)
|
up(float64(i) * 100 / float64(count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var resp2 base.Json
|
var resp2 base.Json
|
||||||
_, err, e = d.request("https://api.aliyundrive.com/v2/file/complete", http.MethodPost, func(req *resty.Request) {
|
_, err, e = d.request("https://api.alipan.com/v2/file/complete", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
"file_id": resp.FileId,
|
"file_id": resp.FileId,
|
||||||
@@ -332,10 +337,10 @@ func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}
|
|||||||
}
|
}
|
||||||
switch args.Method {
|
switch args.Method {
|
||||||
case "doc_preview":
|
case "doc_preview":
|
||||||
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
|
url = "https://api.alipan.com/v2/file/get_office_preview_url"
|
||||||
data["access_token"] = d.AccessToken
|
data["access_token"] = d.AccessToken
|
||||||
case "video_preview":
|
case "video_preview":
|
||||||
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
|
url = "https://api.alipan.com/v2/file/get_video_preview_play_info"
|
||||||
data["category"] = "live_transcoding"
|
data["category"] = "live_transcoding"
|
||||||
data["url_expire_sec"] = 14400
|
data["url_expire_sec"] = 14400
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func (d *AliDrive) createSession() error {
|
|||||||
state.retry = 0
|
state.retry = 0
|
||||||
return fmt.Errorf("createSession failed after three retries")
|
return fmt.Errorf("createSession failed after three retries")
|
||||||
}
|
}
|
||||||
_, err, _ := d.request("https://api.aliyundrive.com/users/v1/users/device/create_session", http.MethodPost, func(req *resty.Request) {
|
_, err, _ := d.request("https://api.alipan.com/users/v1/users/device/create_session", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"deviceName": "samsung",
|
"deviceName": "samsung",
|
||||||
"modelName": "SM-G9810",
|
"modelName": "SM-G9810",
|
||||||
@@ -42,7 +42,7 @@ func (d *AliDrive) createSession() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// func (d *AliDrive) renewSession() error {
|
// func (d *AliDrive) renewSession() error {
|
||||||
// _, err, _ := d.request("https://api.aliyundrive.com/users/v1/users/device/renew_session", http.MethodPost, nil, nil)
|
// _, err, _ := d.request("https://api.alipan.com/users/v1/users/device/renew_session", http.MethodPost, nil, nil)
|
||||||
// return err
|
// return err
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ func (d *AliDrive) sign() {
|
|||||||
// do others that not defined in Driver interface
|
// do others that not defined in Driver interface
|
||||||
|
|
||||||
func (d *AliDrive) refreshToken() error {
|
func (d *AliDrive) refreshToken() error {
|
||||||
url := "https://auth.aliyundrive.com/v2/account/token"
|
url := "https://auth.alipan.com/v2/account/token"
|
||||||
var resp base.TokenResp
|
var resp base.TokenResp
|
||||||
var e RespErr
|
var e RespErr
|
||||||
_, err := base.RestyClient.R().
|
_, err := base.RestyClient.R().
|
||||||
@@ -85,7 +85,7 @@ func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp i
|
|||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
state, ok := global.Load(d.UserID)
|
state, ok := global.Load(d.UserID)
|
||||||
if !ok {
|
if !ok {
|
||||||
if url == "https://api.aliyundrive.com/v2/user/get" {
|
if url == "https://api.alipan.com/v2/user/get" {
|
||||||
state = &State{}
|
state = &State{}
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("can't load user state, user_id: %s", d.UserID), RespErr{}
|
return nil, fmt.Errorf("can't load user state, user_id: %s", d.UserID), RespErr{}
|
||||||
@@ -94,8 +94,8 @@ func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp i
|
|||||||
req.SetHeaders(map[string]string{
|
req.SetHeaders(map[string]string{
|
||||||
"Authorization": "Bearer\t" + d.AccessToken,
|
"Authorization": "Bearer\t" + d.AccessToken,
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"origin": "https://www.aliyundrive.com",
|
"origin": "https://www.alipan.com",
|
||||||
"Referer": "https://aliyundrive.com/",
|
"Referer": "https://alipan.com/",
|
||||||
"X-Signature": state.signature,
|
"X-Signature": state.signature,
|
||||||
"x-request-id": uuid.NewString(),
|
"x-request-id": uuid.NewString(),
|
||||||
"X-Canary": "client=Android,app=adrive,version=v4.1.0",
|
"X-Canary": "client=Android,app=adrive,version=v4.1.0",
|
||||||
@@ -158,7 +158,7 @@ func (d *AliDrive) getFiles(fileId string) ([]File, error) {
|
|||||||
"video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300",
|
"video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300",
|
||||||
"url_expire_sec": 14400,
|
"url_expire_sec": 14400,
|
||||||
}
|
}
|
||||||
_, err, _ := d.request("https://api.aliyundrive.com/v2/file/list", http.MethodPost, func(req *resty.Request) {
|
_, err, _ := d.request("https://api.alipan.com/v2/file/list", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data)
|
req.SetBody(data)
|
||||||
}, &resp)
|
}, &resp)
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ func (d *AliDrive) getFiles(fileId string) ([]File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliDrive) batch(srcId, dstId string, url string) error {
|
func (d *AliDrive) batch(srcId, dstId string, url string) error {
|
||||||
res, err, _ := d.request("https://api.aliyundrive.com/v3/batch", http.MethodPost, func(req *resty.Request) {
|
res, err, _ := d.request("https://api.alipan.com/v3/batch", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"requests": []base.Json{
|
"requests": []base.Json{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ import (
|
|||||||
type AliyundriveOpen struct {
|
type AliyundriveOpen struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
base string
|
|
||||||
|
|
||||||
DriveId string
|
DriveId string
|
||||||
|
|
||||||
limitList func(ctx context.Context, data base.Json) (*Files, error)
|
limitList func(ctx context.Context, data base.Json) (*Files, error)
|
||||||
limitLink func(ctx context.Context, file model.Obj) (*model.Link, error)
|
limitLink func(ctx context.Context, file model.Obj) (*model.Link, error)
|
||||||
|
ref *AliyundriveOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Config() driver.Config {
|
func (d *AliyundriveOpen) Config() driver.Config {
|
||||||
@@ -58,7 +58,17 @@ func (d *AliyundriveOpen) Init(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *AliyundriveOpen) InitReference(storage driver.Driver) error {
|
||||||
|
refStorage, ok := storage.(*AliyundriveOpen)
|
||||||
|
if ok {
|
||||||
|
d.ref = refStorage
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Drop(ctx context.Context) error {
|
func (d *AliyundriveOpen) Drop(ctx context.Context) error {
|
||||||
|
d.ref = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +103,7 @@ func (d *AliyundriveOpen) link(ctx context.Context, file model.Obj) (*model.Link
|
|||||||
}
|
}
|
||||||
url = utils.Json.Get(res, "streamsUrl", d.LIVPDownloadFormat).ToString()
|
url = utils.Json.Get(res, "streamsUrl", d.LIVPDownloadFormat).ToString()
|
||||||
}
|
}
|
||||||
exp := time.Hour
|
exp := time.Minute
|
||||||
return &model.Link{
|
return &model.Link{
|
||||||
URL: url,
|
URL: url,
|
||||||
Expiration: &exp,
|
Expiration: &exp,
|
||||||
@@ -107,7 +117,9 @@ func (d *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.L
|
|||||||
return d.limitLink(ctx, file)
|
return d.limitLink(ctx, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
|
nowTime, _ := getNowTime()
|
||||||
|
newDir := File{CreatedAt: nowTime, UpdatedAt: nowTime}
|
||||||
_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
@@ -115,12 +127,16 @@ func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirN
|
|||||||
"name": dirName,
|
"name": dirName,
|
||||||
"type": "folder",
|
"type": "folder",
|
||||||
"check_name_mode": "refuse",
|
"check_name_mode": "refuse",
|
||||||
})
|
}).SetResult(&newDir)
|
||||||
})
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fileToObj(newDir), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
var resp MoveOrCopyResp
|
||||||
_, err := d.request("/adrive/v1.0/openFile/move", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("/adrive/v1.0/openFile/move", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
@@ -128,20 +144,36 @@ func (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) er
|
|||||||
"to_parent_file_id": dstDir.GetID(),
|
"to_parent_file_id": dstDir.GetID(),
|
||||||
"check_name_mode": "refuse", // optional:ignore,auto_rename,refuse
|
"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
|
//"new_name": "newName", // The new name to use when a file of the same name exists
|
||||||
})
|
}).SetResult(&resp)
|
||||||
})
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.Exist {
|
||||||
|
return nil, errors.New("existence of files with the same name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if srcObj, ok := srcObj.(*model.ObjThumb); ok {
|
||||||
|
srcObj.ID = resp.FileID
|
||||||
|
srcObj.Modified = time.Now()
|
||||||
|
return srcObj, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
func (d *AliyundriveOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
|
var newFile File
|
||||||
_, err := d.request("/adrive/v1.0/openFile/update", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("/adrive/v1.0/openFile/update", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
"file_id": srcObj.GetID(),
|
"file_id": srcObj.GetID(),
|
||||||
"name": newName,
|
"name": newName,
|
||||||
})
|
}).SetResult(&newFile)
|
||||||
})
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fileToObj(newFile), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *AliyundriveOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
@@ -170,7 +202,7 @@ func (d *AliyundriveOpen) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
return d.upload(ctx, dstDir, stream, up)
|
return d.upload(ctx, dstDir, stream, up)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,3 +231,7 @@ func (d *AliyundriveOpen) Other(ctx context.Context, args model.OtherArgs) (inte
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*AliyundriveOpen)(nil)
|
var _ driver.Driver = (*AliyundriveOpen)(nil)
|
||||||
|
var _ driver.MkdirResult = (*AliyundriveOpen)(nil)
|
||||||
|
var _ driver.MoveResult = (*AliyundriveOpen)(nil)
|
||||||
|
var _ driver.RenameResult = (*AliyundriveOpen)(nil)
|
||||||
|
var _ driver.PutResult = (*AliyundriveOpen)(nil)
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
DriveType string `json:"drive_type" type:"select" options:"default,resource,backup" default:"default"`
|
DriveType string `json:"drive_type" type:"select" options:"default,resource,backup" default:"resource"`
|
||||||
driver.RootID
|
driver.RootID
|
||||||
RefreshToken string `json:"refresh_token" required:"true"`
|
RefreshToken string `json:"refresh_token" required:"true"`
|
||||||
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
|
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"`
|
OauthTokenURL string `json:"oauth_token_url" default:"https://api.nn.ci/alist/ali_open/token"`
|
||||||
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
|
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"`
|
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"`
|
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||||
@@ -32,11 +32,10 @@ var config = driver.Config{
|
|||||||
DefaultRoot: "root",
|
DefaultRoot: "root",
|
||||||
NoOverwriteUpload: true,
|
NoOverwriteUpload: true,
|
||||||
}
|
}
|
||||||
|
var API_URL = "https://openapi.alipan.com"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
op.RegisterDriver(func() driver.Driver {
|
op.RegisterDriver(func() driver.Driver {
|
||||||
return &AliyundriveOpen{
|
return &AliyundriveOpen{}
|
||||||
base: "https://openapi.aliyundrive.com",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package aliyundrive_open
|
package aliyundrive_open
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
@@ -17,22 +18,28 @@ type Files struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
DriveId string `json:"drive_id"`
|
DriveId string `json:"drive_id"`
|
||||||
FileId string `json:"file_id"`
|
FileId string `json:"file_id"`
|
||||||
ParentFileId string `json:"parent_file_id"`
|
ParentFileId string `json:"parent_file_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
FileExtension string `json:"file_extension"`
|
FileExtension string `json:"file_extension"`
|
||||||
ContentHash string `json:"content_hash"`
|
ContentHash string `json:"content_hash"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Thumbnail string `json:"thumbnail"`
|
Thumbnail string `json:"thumbnail"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
CreatedAt *time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// create only
|
||||||
|
FileName string `json:"file_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileToObj(f File) *model.ObjThumb {
|
func fileToObj(f File) *model.ObjThumb {
|
||||||
|
if f.Name == "" {
|
||||||
|
f.Name = f.FileName
|
||||||
|
}
|
||||||
return &model.ObjThumb{
|
return &model.ObjThumb{
|
||||||
Object: model.Object{
|
Object: model.Object{
|
||||||
ID: f.FileId,
|
ID: f.FileId,
|
||||||
@@ -40,6 +47,8 @@ func fileToObj(f File) *model.ObjThumb {
|
|||||||
Size: f.Size,
|
Size: f.Size,
|
||||||
Modified: f.UpdatedAt,
|
Modified: f.UpdatedAt,
|
||||||
IsFolder: f.Type == "folder",
|
IsFolder: f.Type == "folder",
|
||||||
|
Ctime: f.CreatedAt,
|
||||||
|
HashInfo: utils.NewHashInfo(utils.SHA1, f.ContentHash),
|
||||||
},
|
},
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
|
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
|
||||||
}
|
}
|
||||||
@@ -67,3 +76,9 @@ type CreateResp struct {
|
|||||||
RapidUpload bool `json:"rapid_upload"`
|
RapidUpload bool `json:"rapid_upload"`
|
||||||
PartInfoList []PartInfo `json:"part_info_list"`
|
PartInfoList []PartInfo `json:"part_info_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MoveOrCopyResp struct {
|
||||||
|
Exist bool `json:"exist"`
|
||||||
|
DriveID string `json:"drive_id"`
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,14 +3,11 @@ package aliyundrive_open
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,7 +15,9 @@ import (
|
|||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"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"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/avast/retry-go"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -32,19 +31,19 @@ func makePartInfos(size int) []base.Json {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func calPartSize(fileSize int64) int64 {
|
func calPartSize(fileSize int64) int64 {
|
||||||
var partSize int64 = 20 * 1024 * 1024
|
var partSize int64 = 20 * utils.MB
|
||||||
if fileSize > partSize {
|
if fileSize > partSize {
|
||||||
if fileSize > 1*1024*1024*1024*1024 { // file Size over 1TB
|
if fileSize > 1*utils.TB { // file Size over 1TB
|
||||||
partSize = 5 * 1024 * 1024 * 1024 // file part size 5GB
|
partSize = 5 * utils.GB // file part size 5GB
|
||||||
} else if fileSize > 768*1024*1024*1024 { // over 768GB
|
} else if fileSize > 768*utils.GB { // over 768GB
|
||||||
partSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part
|
partSize = 109951163 // ≈ 104.8576MB, split 1TB into 10,000 part
|
||||||
} else if fileSize > 512*1024*1024*1024 { // over 512GB
|
} else if fileSize > 512*utils.GB { // over 512GB
|
||||||
partSize = 82463373 // ≈ 78.6432MB
|
partSize = 82463373 // ≈ 78.6432MB
|
||||||
} else if fileSize > 384*1024*1024*1024 { // over 384GB
|
} else if fileSize > 384*utils.GB { // over 384GB
|
||||||
partSize = 54975582 // ≈ 52.4288MB
|
partSize = 54975582 // ≈ 52.4288MB
|
||||||
} else if fileSize > 256*1024*1024*1024 { // over 256GB
|
} else if fileSize > 256*utils.GB { // over 256GB
|
||||||
partSize = 41231687 // ≈ 39.3216MB
|
partSize = 41231687 // ≈ 39.3216MB
|
||||||
} else if fileSize > 128*1024*1024*1024 { // over 128GB
|
} else if fileSize > 128*utils.GB { // over 128GB
|
||||||
partSize = 27487791 // ≈ 26.2144MB
|
partSize = 27487791 // ≈ 26.2144MB
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,73 +64,40 @@ func (d *AliyundriveOpen) getUploadUrl(count int, fileId, uploadId string) ([]Pa
|
|||||||
return resp.PartInfoList, err
|
return resp.PartInfoList, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) uploadPart(ctx context.Context, i, count int, reader *utils.MultiReadable, resp *CreateResp, retry bool) error {
|
func (d *AliyundriveOpen) uploadPart(ctx context.Context, r io.Reader, partInfo PartInfo) error {
|
||||||
partInfo := resp.PartInfoList[i-1]
|
|
||||||
uploadUrl := partInfo.UploadUrl
|
uploadUrl := partInfo.UploadUrl
|
||||||
if d.InternalUpload {
|
if d.InternalUpload {
|
||||||
uploadUrl = strings.ReplaceAll(uploadUrl, "https://cn-beijing-data.aliyundrive.net/", "http://ccp-bj29-bj-1592982087.oss-cn-beijing-internal.aliyuncs.com/")
|
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)
|
req, err := http.NewRequestWithContext(ctx, "PUT", uploadUrl, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req = req.WithContext(ctx)
|
|
||||||
res, err := base.HttpClient.Do(req)
|
res, err := base.HttpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if retry {
|
|
||||||
reader.Reset()
|
|
||||||
return d.uploadPart(ctx, i, count, reader, resp, false)
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
res.Body.Close()
|
_ = 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 {
|
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusConflict {
|
||||||
return fmt.Errorf("upload status: %d", res.StatusCode)
|
return fmt.Errorf("upload status: %d", res.StatusCode)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) normalUpload(ctx context.Context, stream model.FileStreamer, up driver.UpdateProgress, createResp CreateResp, count int, partSize int64) error {
|
func (d *AliyundriveOpen) completeUpload(fileId, uploadId string) (model.Obj, 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
|
// 3. complete
|
||||||
|
var newFile File
|
||||||
_, err := d.request("/adrive/v1.0/openFile/complete", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("/adrive/v1.0/openFile/complete", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
"file_id": createResp.FileId,
|
"file_id": fileId,
|
||||||
"upload_id": createResp.UploadId,
|
"upload_id": uploadId,
|
||||||
})
|
}).SetResult(&newFile)
|
||||||
})
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fileToObj(newFile), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProofRange struct {
|
type ProofRange struct {
|
||||||
@@ -159,110 +125,150 @@ func getProofRange(input string, size int64) (*ProofRange, error) {
|
|||||||
return pr, nil
|
return pr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) calProofCode(file *os.File, fileSize int64) (string, error) {
|
func (d *AliyundriveOpen) calProofCode(stream model.FileStreamer) (string, error) {
|
||||||
proofRange, err := getProofRange(d.AccessToken, fileSize)
|
proofRange, err := getProofRange(d.getAccessToken(), stream.GetSize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
buf := make([]byte, proofRange.End-proofRange.Start)
|
length := proofRange.End - proofRange.Start
|
||||||
_, err = file.ReadAt(buf, proofRange.Start)
|
buf := bytes.NewBuffer(make([]byte, 0, length))
|
||||||
|
reader, err := stream.RangeRead(http_range.Range{Start: proofRange.Start, Length: length})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return base64.StdEncoding.EncodeToString(buf), nil
|
_, err = utils.CopyWithBufferN(buf, reader, length)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
// 1. create
|
// 1. create
|
||||||
// Part Size Unit: Bytes, Default: 20MB,
|
// Part Size Unit: Bytes, Default: 20MB,
|
||||||
// Maximum number of slices 10,000, ≈195.3125GB
|
// Maximum number of slices 10,000, ≈195.3125GB
|
||||||
var partSize = calPartSize(stream.GetSize())
|
var partSize = calPartSize(stream.GetSize())
|
||||||
|
const dateFormat = "2006-01-02T15:04:05.000Z"
|
||||||
|
mtimeStr := stream.ModTime().UTC().Format(dateFormat)
|
||||||
|
ctimeStr := stream.CreateTime().UTC().Format(dateFormat)
|
||||||
|
|
||||||
createData := base.Json{
|
createData := base.Json{
|
||||||
"drive_id": d.DriveId,
|
"drive_id": d.DriveId,
|
||||||
"parent_file_id": dstDir.GetID(),
|
"parent_file_id": dstDir.GetID(),
|
||||||
"name": stream.GetName(),
|
"name": stream.GetName(),
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"check_name_mode": "ignore",
|
"check_name_mode": "ignore",
|
||||||
|
"local_modified_at": mtimeStr,
|
||||||
|
"local_created_at": ctimeStr,
|
||||||
}
|
}
|
||||||
count := int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
|
count := int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
|
||||||
createData["part_info_list"] = makePartInfos(count)
|
createData["part_info_list"] = makePartInfos(count)
|
||||||
// rapid upload
|
// rapid upload
|
||||||
rapidUpload := stream.GetSize() > 100*1024 && d.RapidUpload
|
rapidUpload := !stream.IsForceStreamUpload() && stream.GetSize() > 100*utils.KB && d.RapidUpload
|
||||||
if rapidUpload {
|
if rapidUpload {
|
||||||
log.Debugf("[aliyundrive_open] start cal pre_hash")
|
log.Debugf("[aliyundrive_open] start cal pre_hash")
|
||||||
// read 1024 bytes to calculate pre hash
|
// read 1024 bytes to calculate pre hash
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: 1024})
|
||||||
_, err := io.CopyN(buf, stream, 1024)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
|
}
|
||||||
|
hash, err := utils.HashReader(utils.SHA1, reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
createData["size"] = stream.GetSize()
|
createData["size"] = stream.GetSize()
|
||||||
createData["pre_hash"] = utils.GetSHA1Encode(buf.Bytes())
|
createData["pre_hash"] = hash
|
||||||
// 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
|
var createResp CreateResp
|
||||||
_, err, e := d.requestReturnErrResp("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
_, err, e := d.requestReturnErrResp("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(createData).SetResult(&createResp)
|
req.SetBody(createData).SetResult(&createResp)
|
||||||
})
|
})
|
||||||
|
var tmpF model.File
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if e.Code != "PreHashMatched" || !rapidUpload {
|
if e.Code != "PreHashMatched" || !rapidUpload {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Debugf("[aliyundrive_open] pre_hash matched, start rapid upload")
|
log.Debugf("[aliyundrive_open] pre_hash matched, start rapid upload")
|
||||||
// convert to local file
|
|
||||||
file, err := utils.CreateTempFile(stream, stream.GetSize())
|
hi := stream.GetHash()
|
||||||
if err != nil {
|
hash := hi.GetHash(utils.SHA1)
|
||||||
return err
|
if len(hash) <= 0 {
|
||||||
}
|
tmpF, err = stream.CacheFullInTempFile()
|
||||||
_ = stream.GetReadCloser().Close()
|
if err != nil {
|
||||||
stream.SetReadCloser(file)
|
return nil, err
|
||||||
// calculate full hash
|
}
|
||||||
h := sha1.New()
|
hash, err = utils.HashFile(utils.SHA1, tmpF)
|
||||||
_, err = io.Copy(h, file)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return err
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(createData, "pre_hash")
|
delete(createData, "pre_hash")
|
||||||
createData["proof_version"] = "v1"
|
createData["proof_version"] = "v1"
|
||||||
createData["content_hash_name"] = "sha1"
|
createData["content_hash_name"] = "sha1"
|
||||||
createData["content_hash"] = hex.EncodeToString(h.Sum(nil))
|
createData["content_hash"] = hash
|
||||||
// seek to start
|
createData["proof_code"], err = d.calProofCode(stream)
|
||||||
if _, err = file.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
createData["proof_code"], err = d.calProofCode(file, stream.GetSize())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cal proof code error: %s", err.Error())
|
return nil, fmt.Errorf("cal proof code error: %s", err.Error())
|
||||||
}
|
}
|
||||||
_, err = d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
_, err = d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(createData).SetResult(&createResp)
|
req.SetBody(createData).SetResult(&createResp)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !createResp.RapidUpload {
|
||||||
|
// 2. normal upload
|
||||||
|
log.Debugf("[aliyundive_open] normal upload")
|
||||||
|
|
||||||
|
preTime := time.Now()
|
||||||
|
var offset, length int64 = 0, partSize
|
||||||
|
//var length
|
||||||
|
for i := 0; i < len(createResp.PartInfoList); i++ {
|
||||||
|
if utils.IsCanceled(ctx) {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
// 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 nil, err
|
||||||
|
}
|
||||||
|
preTime = time.Now()
|
||||||
|
}
|
||||||
|
if remain := stream.GetSize() - offset; length > remain {
|
||||||
|
length = remain
|
||||||
|
}
|
||||||
|
rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
|
||||||
|
if rapidUpload {
|
||||||
|
srd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rd = utils.NewMultiReadable(srd)
|
||||||
|
}
|
||||||
|
err = retry.Do(func() error {
|
||||||
|
_ = rd.Reset()
|
||||||
|
rateLimitedRd := driver.NewLimitedUploadStream(ctx, rd)
|
||||||
|
return d.uploadPart(ctx, rateLimitedRd, createResp.PartInfoList[i])
|
||||||
|
},
|
||||||
|
retry.Attempts(3),
|
||||||
|
retry.DelayType(retry.BackOffDelay),
|
||||||
|
retry.Delay(time.Second))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offset += partSize
|
||||||
|
up(float64(i*100) / float64(count))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Debugf("[aliyundrive_open] rapid upload success, file id: %s", createResp.FileId)
|
||||||
|
}
|
||||||
|
|
||||||
log.Debugf("[aliyundrive_open] create file success, resp: %+v", createResp)
|
log.Debugf("[aliyundrive_open] create file success, resp: %+v", createResp)
|
||||||
return d.normalUpload(ctx, stream, up, createResp, count, partSize)
|
// 3. complete
|
||||||
|
return d.completeUpload(createResp.FileId, createResp.UploadId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package aliyundrive_open
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
@@ -15,15 +18,15 @@ import (
|
|||||||
|
|
||||||
// do others that not defined in Driver interface
|
// do others that not defined in Driver interface
|
||||||
|
|
||||||
func (d *AliyundriveOpen) refreshToken() error {
|
func (d *AliyundriveOpen) _refreshToken() (string, string, error) {
|
||||||
url := d.base + "/oauth/access_token"
|
url := API_URL + "/oauth/access_token"
|
||||||
if d.OauthTokenURL != "" && d.ClientID == "" {
|
if d.OauthTokenURL != "" && d.ClientID == "" {
|
||||||
url = d.OauthTokenURL
|
url = d.OauthTokenURL
|
||||||
}
|
}
|
||||||
//var resp base.TokenResp
|
//var resp base.TokenResp
|
||||||
var e ErrResp
|
var e ErrResp
|
||||||
res, err := base.RestyClient.R().
|
res, err := base.RestyClient.R().
|
||||||
ForceContentType("application/json").
|
//ForceContentType("application/json").
|
||||||
SetBody(base.Json{
|
SetBody(base.Json{
|
||||||
"client_id": d.ClientID,
|
"client_id": d.ClientID,
|
||||||
"client_secret": d.ClientSecret,
|
"client_secret": d.ClientSecret,
|
||||||
@@ -34,16 +37,59 @@ func (d *AliyundriveOpen) refreshToken() error {
|
|||||||
SetError(&e).
|
SetError(&e).
|
||||||
Post(url)
|
Post(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", "", err
|
||||||
}
|
}
|
||||||
log.Debugf("[ali_open] refresh token response: %s", res.String())
|
log.Debugf("[ali_open] refresh token response: %s", res.String())
|
||||||
if e.Code != "" {
|
if e.Code != "" {
|
||||||
return fmt.Errorf("failed to refresh token: %s", e.Message)
|
return "", "", fmt.Errorf("failed to refresh token: %s", e.Message)
|
||||||
}
|
}
|
||||||
refresh, access := utils.Json.Get(res.Body(), "refresh_token").ToString(), utils.Json.Get(res.Body(), "access_token").ToString()
|
refresh, access := utils.Json.Get(res.Body(), "refresh_token").ToString(), utils.Json.Get(res.Body(), "access_token").ToString()
|
||||||
if refresh == "" {
|
if refresh == "" {
|
||||||
return errors.New("failed to refresh token: refresh token is empty")
|
return "", "", fmt.Errorf("failed to refresh token: refresh token is empty, resp: %s", res.String())
|
||||||
}
|
}
|
||||||
|
curSub, err := getSub(d.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
newSub, err := getSub(refresh)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if curSub != newSub {
|
||||||
|
return "", "", errors.New("failed to refresh token: sub not match")
|
||||||
|
}
|
||||||
|
return refresh, access, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSub(token string) (string, error) {
|
||||||
|
segments := strings.Split(token, ".")
|
||||||
|
if len(segments) != 3 {
|
||||||
|
return "", errors.New("not a jwt token because of invalid segments")
|
||||||
|
}
|
||||||
|
bs, err := base64.RawStdEncoding.DecodeString(segments[1])
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("failed to decode jwt token")
|
||||||
|
}
|
||||||
|
return utils.Json.Get(bs, "sub").ToString(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AliyundriveOpen) refreshToken() error {
|
||||||
|
if d.ref != nil {
|
||||||
|
return d.ref.refreshToken()
|
||||||
|
}
|
||||||
|
refresh, access, err := d._refreshToken()
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
log.Errorf("[ali_open] failed to refresh token: %s", err)
|
||||||
|
}
|
||||||
|
refresh, access, err = d._refreshToken()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("[ali_open] token exchange: %s -> %s", d.RefreshToken, refresh)
|
||||||
d.RefreshToken, d.AccessToken = refresh, access
|
d.RefreshToken, d.AccessToken = refresh, access
|
||||||
op.MustSaveDriverStorage(d)
|
op.MustSaveDriverStorage(d)
|
||||||
return nil
|
return nil
|
||||||
@@ -57,7 +103,7 @@ func (d *AliyundriveOpen) request(uri, method string, callback base.ReqCallback,
|
|||||||
func (d *AliyundriveOpen) requestReturnErrResp(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error, *ErrResp) {
|
func (d *AliyundriveOpen) requestReturnErrResp(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error, *ErrResp) {
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
// TODO check whether access_token is expired
|
// TODO check whether access_token is expired
|
||||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
req.SetHeader("Authorization", "Bearer "+d.getAccessToken())
|
||||||
if method == http.MethodPost {
|
if method == http.MethodPost {
|
||||||
req.SetHeader("Content-Type", "application/json")
|
req.SetHeader("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
@@ -66,7 +112,7 @@ func (d *AliyundriveOpen) requestReturnErrResp(uri, method string, callback base
|
|||||||
}
|
}
|
||||||
var e ErrResp
|
var e ErrResp
|
||||||
req.SetError(&e)
|
req.SetError(&e)
|
||||||
res, err := req.Execute(method, d.base+uri)
|
res, err := req.Execute(method, API_URL+uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if res != nil {
|
if res != nil {
|
||||||
log.Errorf("[aliyundrive_open] request error: %s", res.String())
|
log.Errorf("[aliyundrive_open] request error: %s", res.String())
|
||||||
@@ -75,7 +121,7 @@ func (d *AliyundriveOpen) requestReturnErrResp(uri, method string, callback base
|
|||||||
}
|
}
|
||||||
isRetry := len(retry) > 0 && retry[0]
|
isRetry := len(retry) > 0 && retry[0]
|
||||||
if e.Code != "" {
|
if e.Code != "" {
|
||||||
if !isRetry && (utils.SliceContains([]string{"AccessTokenInvalid", "AccessTokenExpired", "I400JD"}, e.Code) || d.AccessToken == "") {
|
if !isRetry && (utils.SliceContains([]string{"AccessTokenInvalid", "AccessTokenExpired", "I400JD"}, e.Code) || d.getAccessToken() == "") {
|
||||||
err = d.refreshToken()
|
err = d.refreshToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err, nil
|
return nil, err, nil
|
||||||
@@ -127,3 +173,16 @@ func (d *AliyundriveOpen) getFiles(ctx context.Context, fileId string) ([]File,
|
|||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getNowTime() (time.Time, string) {
|
||||||
|
nowTime := time.Now()
|
||||||
|
nowTimeStr := nowTime.Format("2006-01-02T15:04:05.000Z")
|
||||||
|
return nowTime, nowTimeStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AliyundriveOpen) getAccessToken() string {
|
||||||
|
if d.ref != nil {
|
||||||
|
return d.ref.getAccessToken()
|
||||||
|
}
|
||||||
|
return d.AccessToken
|
||||||
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ func (d *AliyundriveShare) link(ctx context.Context, file model.Obj) (*model.Lin
|
|||||||
"share_id": d.ShareId,
|
"share_id": d.ShareId,
|
||||||
}
|
}
|
||||||
var resp ShareLinkResp
|
var resp ShareLinkResp
|
||||||
_, err := d.request("https://api.aliyundrive.com/v2/file/get_share_link_download_url", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("https://api.alipan.com/v2/file/get_share_link_download_url", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetHeader(CanaryHeaderKey, CanaryHeaderValue).SetBody(data).SetResult(&resp)
|
req.SetHeader(CanaryHeaderKey, CanaryHeaderValue).SetBody(data).SetResult(&resp)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,7 +113,7 @@ func (d *AliyundriveShare) link(ctx context.Context, file model.Obj) (*model.Lin
|
|||||||
}
|
}
|
||||||
return &model.Link{
|
return &model.Link{
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
"Referer": []string{"https://www.aliyundrive.com/"},
|
"Referer": []string{"https://www.alipan.com/"},
|
||||||
},
|
},
|
||||||
URL: resp.DownloadUrl,
|
URL: resp.DownloadUrl,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -128,9 +128,9 @@ func (d *AliyundriveShare) Other(ctx context.Context, args model.OtherArgs) (int
|
|||||||
}
|
}
|
||||||
switch args.Method {
|
switch args.Method {
|
||||||
case "doc_preview":
|
case "doc_preview":
|
||||||
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
|
url = "https://api.alipan.com/v2/file/get_office_preview_url"
|
||||||
case "video_preview":
|
case "video_preview":
|
||||||
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
|
url = "https://api.alipan.com/v2/file/get_video_preview_play_info"
|
||||||
data["category"] = "live_transcoding"
|
data["category"] = "live_transcoding"
|
||||||
default:
|
default:
|
||||||
return nil, errs.NotSupport
|
return nil, errs.NotSupport
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func fileToObj(f File) *model.ObjThumb {
|
|||||||
Name: f.Name,
|
Name: f.Name,
|
||||||
Size: f.Size,
|
Size: f.Size,
|
||||||
Modified: f.UpdatedAt,
|
Modified: f.UpdatedAt,
|
||||||
|
Ctime: f.CreatedAt,
|
||||||
IsFolder: f.Type == "folder",
|
IsFolder: f.Type == "folder",
|
||||||
},
|
},
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
|
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (d *AliyundriveShare) refreshToken() error {
|
func (d *AliyundriveShare) refreshToken() error {
|
||||||
url := "https://auth.aliyundrive.com/v2/account/token"
|
url := "https://auth.alipan.com/v2/account/token"
|
||||||
var resp base.TokenResp
|
var resp base.TokenResp
|
||||||
var e ErrorResp
|
var e ErrorResp
|
||||||
_, err := base.RestyClient.R().
|
_, err := base.RestyClient.R().
|
||||||
@@ -47,7 +47,7 @@ func (d *AliyundriveShare) getShareToken() error {
|
|||||||
var resp ShareTokenResp
|
var resp ShareTokenResp
|
||||||
_, err := base.RestyClient.R().
|
_, err := base.RestyClient.R().
|
||||||
SetResult(&resp).SetError(&e).SetBody(data).
|
SetResult(&resp).SetError(&e).SetBody(data).
|
||||||
Post("https://api.aliyundrive.com/v2/share_link/get_share_token")
|
Post("https://api.alipan.com/v2/share_link/get_share_token")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ func (d *AliyundriveShare) getFiles(fileId string) ([]File, error) {
|
|||||||
SetHeader("x-share-token", d.ShareToken).
|
SetHeader("x-share-token", d.ShareToken).
|
||||||
SetHeader(CanaryHeaderKey, CanaryHeaderValue).
|
SetHeader(CanaryHeaderKey, CanaryHeaderValue).
|
||||||
SetResult(&resp).SetError(&e).SetBody(data).
|
SetResult(&resp).SetError(&e).SetBody(data).
|
||||||
Post("https://api.aliyundrive.com/adrive/v3/file/list")
|
Post("https://api.alipan.com/adrive/v3/file/list")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package drivers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/alist-org/alist/v3/drivers/115"
|
_ "github.com/alist-org/alist/v3/drivers/115"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/115_open"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/115_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/123"
|
_ "github.com/alist-org/alist/v3/drivers/123"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/123_link"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/123_share"
|
_ "github.com/alist-org/alist/v3/drivers/123_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/139"
|
_ "github.com/alist-org/alist/v3/drivers/139"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/189"
|
_ "github.com/alist-org/alist/v3/drivers/189"
|
||||||
@@ -16,23 +19,37 @@ import (
|
|||||||
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
|
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
|
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/baidu_share"
|
_ "github.com/alist-org/alist/v3/drivers/baidu_share"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/chaoxing"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
|
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/crypt"
|
_ "github.com/alist-org/alist/v3/drivers/crypt"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/doubao"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/dropbox"
|
_ "github.com/alist-org/alist/v3/drivers/dropbox"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/febbox"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/github"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/github_releases"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/google_photo"
|
_ "github.com/alist-org/alist/v3/drivers/google_photo"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/halalcloud"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/ilanzou"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/ipfs_api"
|
_ "github.com/alist-org/alist/v3/drivers/ipfs_api"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/kodbox"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/lanzou"
|
_ "github.com/alist-org/alist/v3/drivers/lanzou"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/lenovonas_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/local"
|
_ "github.com/alist-org/alist/v3/drivers/local"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mega"
|
_ "github.com/alist-org/alist/v3/drivers/mega"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/misskey"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mopan"
|
_ "github.com/alist-org/alist/v3/drivers/mopan"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/netease_music"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
|
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/onedrive_sharelink"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/pikpak"
|
_ "github.com/alist-org/alist/v3/drivers/pikpak"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
|
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
|
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/quark_uc_tv"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/quqi"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/s3"
|
_ "github.com/alist-org/alist/v3/drivers/s3"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/seafile"
|
_ "github.com/alist-org/alist/v3/drivers/seafile"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/sftp"
|
_ "github.com/alist-org/alist/v3/drivers/sftp"
|
||||||
@@ -40,10 +57,13 @@ import (
|
|||||||
_ "github.com/alist-org/alist/v3/drivers/teambition"
|
_ "github.com/alist-org/alist/v3/drivers/teambition"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/terabox"
|
_ "github.com/alist-org/alist/v3/drivers/terabox"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/thunder"
|
_ "github.com/alist-org/alist/v3/drivers/thunder"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/thunder_browser"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/thunderx"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/trainbit"
|
_ "github.com/alist-org/alist/v3/drivers/trainbit"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/url_tree"
|
_ "github.com/alist-org/alist/v3/drivers/url_tree"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/uss"
|
_ "github.com/alist-org/alist/v3/drivers/uss"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/virtual"
|
_ "github.com/alist-org/alist/v3/drivers/virtual"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/vtencent"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/webdav"
|
_ "github.com/alist-org/alist/v3/drivers/webdav"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/weiyun"
|
_ "github.com/alist-org/alist/v3/drivers/weiyun"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/wopan"
|
_ "github.com/alist-org/alist/v3/drivers/wopan"
|
||||||
|
|||||||
@@ -4,29 +4,33 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"errors"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
stdpath "path"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/errgroup"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/avast/retry-go"
|
"github.com/avast/retry-go"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
stdpath "path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BaiduNetdisk struct {
|
type BaiduNetdisk struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
}
|
|
||||||
|
|
||||||
const BaiduFileAPI = "https://d.pcs.baidu.com/rest/2.0/pcs/superfile2"
|
uploadThread int
|
||||||
const DefaultSliceSize int64 = 4 * 1024 * 1024
|
vipType int // 会员类型,0普通用户(4G/4M)、1普通会员(10G/16M)、2超级会员(20G/32M)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Config() driver.Config {
|
func (d *BaiduNetdisk) Config() driver.Config {
|
||||||
return config
|
return config
|
||||||
@@ -37,11 +41,24 @@ func (d *BaiduNetdisk) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Init(ctx context.Context) error {
|
func (d *BaiduNetdisk) Init(ctx context.Context) error {
|
||||||
|
d.uploadThread, _ = strconv.Atoi(d.UploadThread)
|
||||||
|
if d.uploadThread < 1 || d.uploadThread > 32 {
|
||||||
|
d.uploadThread, d.UploadThread = 3, "3"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := url.Parse(d.UploadAPI); d.UploadAPI == "" || err != nil {
|
||||||
|
d.UploadAPI = "https://d.pcs.baidu.com"
|
||||||
|
}
|
||||||
|
|
||||||
res, err := d.get("/xpan/nas", map[string]string{
|
res, err := d.get("/xpan/nas", map[string]string{
|
||||||
"method": "uinfo",
|
"method": "uinfo",
|
||||||
}, nil)
|
}, nil)
|
||||||
log.Debugf("[baidu] get uinfo: %s", string(res))
|
log.Debugf("[baidu] get uinfo: %s", string(res))
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.vipType = utils.Json.Get(res, "vip_type").ToInt()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Drop(ctx context.Context) error {
|
func (d *BaiduNetdisk) Drop(ctx context.Context) error {
|
||||||
@@ -65,12 +82,16 @@ func (d *BaiduNetdisk) Link(ctx context.Context, file model.Obj, args model.Link
|
|||||||
return d.linkOfficial(file, args)
|
return d.linkOfficial(file, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
func (d *BaiduNetdisk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
_, err := d.create(stdpath.Join(parentDir.GetPath(), dirName), 0, 1, "", "")
|
var newDir File
|
||||||
return err
|
_, err := d.create(stdpath.Join(parentDir.GetPath(), dirName), 0, 1, "", "", &newDir, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fileToObj(newDir), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *BaiduNetdisk) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
data := []base.Json{
|
data := []base.Json{
|
||||||
{
|
{
|
||||||
"path": srcObj.GetPath(),
|
"path": srcObj.GetPath(),
|
||||||
@@ -79,10 +100,18 @@ func (d *BaiduNetdisk) Move(ctx context.Context, srcObj, dstDir model.Obj) error
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, err := d.manage("move", data)
|
_, err := d.manage("move", data)
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if srcObj, ok := srcObj.(*model.ObjThumb); ok {
|
||||||
|
srcObj.SetPath(stdpath.Join(dstDir.GetPath(), srcObj.GetName()))
|
||||||
|
srcObj.Modified = time.Now()
|
||||||
|
return srcObj, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
func (d *BaiduNetdisk) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
data := []base.Json{
|
data := []base.Json{
|
||||||
{
|
{
|
||||||
"path": srcObj.GetPath(),
|
"path": srcObj.GetPath(),
|
||||||
@@ -90,7 +119,17 @@ func (d *BaiduNetdisk) Rename(ctx context.Context, srcObj model.Obj, newName str
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, err := d.manage("rename", data)
|
_, err := d.manage("rename", data)
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if srcObj, ok := srcObj.(*model.ObjThumb); ok {
|
||||||
|
srcObj.SetPath(stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName))
|
||||||
|
srcObj.Name = newName
|
||||||
|
srcObj.Modified = time.Now()
|
||||||
|
return srcObj, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *BaiduNetdisk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
@@ -111,118 +150,181 @@ func (d *BaiduNetdisk) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) (model.Obj, error) {
|
||||||
|
contentMd5 := stream.GetHash().GetHash(utils.MD5)
|
||||||
|
if len(contentMd5) < utils.MD5.Width {
|
||||||
|
return nil, errors.New("invalid hash")
|
||||||
|
}
|
||||||
|
|
||||||
streamSize := stream.GetSize()
|
streamSize := stream.GetSize()
|
||||||
|
path := stdpath.Join(dstDir.GetPath(), stream.GetName())
|
||||||
|
mtime := stream.ModTime().Unix()
|
||||||
|
ctime := stream.CreateTime().Unix()
|
||||||
|
blockList, _ := utils.Json.MarshalToString([]string{contentMd5})
|
||||||
|
|
||||||
tempFile, err := utils.CreateTempFile(stream.GetReadCloser(), stream.GetSize())
|
var newFile File
|
||||||
|
_, err := d.create(path, streamSize, 0, "", blockList, &newFile, mtime, ctime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func() {
|
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
||||||
_ = tempFile.Close()
|
newFile.Ctime = stream.CreateTime().Unix()
|
||||||
_ = os.Remove(tempFile.Name())
|
newFile.Mtime = stream.ModTime().Unix()
|
||||||
}()
|
return fileToObj(newFile), nil
|
||||||
|
|
||||||
count := int(math.Ceil(float64(streamSize) / float64(DefaultSliceSize)))
|
|
||||||
//cal md5 for first 256k data
|
|
||||||
const SliceSize int64 = 256 * 1024
|
|
||||||
// cal md5
|
|
||||||
h1 := md5.New()
|
|
||||||
h2 := md5.New()
|
|
||||||
blockList := make([]string, 0)
|
|
||||||
contentMd5 := ""
|
|
||||||
sliceMd5 := ""
|
|
||||||
left := streamSize
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
byteSize := DefaultSliceSize
|
|
||||||
if left < DefaultSliceSize {
|
|
||||||
byteSize = left
|
|
||||||
}
|
|
||||||
left -= byteSize
|
|
||||||
_, err = io.Copy(io.MultiWriter(h1, h2), io.LimitReader(tempFile, byteSize))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
blockList = append(blockList, fmt.Sprintf("\"%s\"", hex.EncodeToString(h2.Sum(nil))))
|
|
||||||
h2.Reset()
|
|
||||||
}
|
|
||||||
contentMd5 = hex.EncodeToString(h1.Sum(nil))
|
|
||||||
_, err = tempFile.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if streamSize <= SliceSize {
|
|
||||||
sliceMd5 = contentMd5
|
|
||||||
} else {
|
|
||||||
sliceData := make([]byte, SliceSize)
|
|
||||||
_, err = io.ReadFull(tempFile, sliceData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h2.Write(sliceData)
|
|
||||||
sliceMd5 = hex.EncodeToString(h2.Sum(nil))
|
|
||||||
}
|
|
||||||
rawPath := stdpath.Join(dstDir.GetPath(), stream.GetName())
|
|
||||||
path := encodeURIComponent(rawPath)
|
|
||||||
block_list_str := fmt.Sprintf("[%s]", strings.Join(blockList, ","))
|
|
||||||
data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&block_list=%s&content-md5=%s&slice-md5=%s",
|
|
||||||
path, streamSize,
|
|
||||||
block_list_str,
|
|
||||||
contentMd5, sliceMd5)
|
|
||||||
params := map[string]string{
|
|
||||||
"method": "precreate",
|
|
||||||
}
|
|
||||||
log.Debugf("[baidu_netdisk] precreate data: %s", data)
|
|
||||||
var precreateResp PrecreateResp
|
|
||||||
_, err = d.post("/xpan/file", params, data, &precreateResp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Debugf("%+v", precreateResp)
|
|
||||||
if precreateResp.ReturnType == 2 {
|
|
||||||
//rapid upload, since got md5 match from baidu server
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
params = map[string]string{
|
|
||||||
"method": "upload",
|
|
||||||
"access_token": d.AccessToken,
|
|
||||||
"type": "tmpfile",
|
|
||||||
"path": path,
|
|
||||||
"uploadid": precreateResp.Uploadid,
|
|
||||||
}
|
|
||||||
|
|
||||||
var offset int64 = 0
|
|
||||||
for i, partseq := range precreateResp.BlockList {
|
|
||||||
params["partseq"] = strconv.Itoa(partseq)
|
|
||||||
byteSize := int64(math.Min(float64(streamSize-offset), float64(DefaultSliceSize)))
|
|
||||||
err := retry.Do(func() error {
|
|
||||||
return d.uploadSlice(ctx, ¶ms, stream.GetName(), tempFile, offset, byteSize)
|
|
||||||
},
|
|
||||||
retry.Context(ctx),
|
|
||||||
retry.Attempts(3))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
offset += byteSize
|
|
||||||
|
|
||||||
if len(precreateResp.BlockList) > 0 {
|
|
||||||
up(i * 100 / len(precreateResp.BlockList))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, err = d.create(rawPath, streamSize, 0, precreateResp.Uploadid, block_list_str)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
func (d *BaiduNetdisk) uploadSlice(ctx context.Context, params *map[string]string, fileName string, file *os.File, offset int64, byteSize int64) error {
|
|
||||||
_, err := file.Seek(offset, io.SeekStart)
|
// Put
|
||||||
if err != nil {
|
//
|
||||||
return err
|
// **注意**: 截至 2024/04/20 百度云盘 api 接口返回的时间永远是当前时间,而不是文件时间。
|
||||||
|
// 而实际上云盘存储的时间是文件时间,所以此处需要覆盖时间,保证缓存与云盘的数据一致
|
||||||
|
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
|
// rapid upload
|
||||||
|
if newObj, err := d.PutRapid(ctx, dstDir, stream); err == nil {
|
||||||
|
return newObj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tempFile, err := stream.CacheFullInTempFile()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
streamSize := stream.GetSize()
|
||||||
|
sliceSize := d.getSliceSize(streamSize)
|
||||||
|
count := int(math.Max(math.Ceil(float64(streamSize)/float64(sliceSize)), 1))
|
||||||
|
lastBlockSize := streamSize % sliceSize
|
||||||
|
if streamSize > 0 && lastBlockSize == 0 {
|
||||||
|
lastBlockSize = sliceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
//cal md5 for first 256k data
|
||||||
|
const SliceSize int64 = 256 * utils.KB
|
||||||
|
// cal md5
|
||||||
|
blockList := make([]string, 0, count)
|
||||||
|
byteSize := sliceSize
|
||||||
|
fileMd5H := md5.New()
|
||||||
|
sliceMd5H := md5.New()
|
||||||
|
sliceMd5H2 := md5.New()
|
||||||
|
slicemd5H2Write := utils.LimitWriter(sliceMd5H2, SliceSize)
|
||||||
|
|
||||||
|
for i := 1; i <= count; i++ {
|
||||||
|
if utils.IsCanceled(ctx) {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
if i == count {
|
||||||
|
byteSize = lastBlockSize
|
||||||
|
}
|
||||||
|
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
blockList = append(blockList, hex.EncodeToString(sliceMd5H.Sum(nil)))
|
||||||
|
sliceMd5H.Reset()
|
||||||
|
}
|
||||||
|
contentMd5 := hex.EncodeToString(fileMd5H.Sum(nil))
|
||||||
|
sliceMd5 := hex.EncodeToString(sliceMd5H2.Sum(nil))
|
||||||
|
blockListStr, _ := utils.Json.MarshalToString(blockList)
|
||||||
|
path := stdpath.Join(dstDir.GetPath(), stream.GetName())
|
||||||
|
mtime := stream.ModTime().Unix()
|
||||||
|
ctime := stream.CreateTime().Unix()
|
||||||
|
|
||||||
|
// step.1 预上传
|
||||||
|
// 尝试获取之前的进度
|
||||||
|
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5)
|
||||||
|
if !ok {
|
||||||
|
params := map[string]string{
|
||||||
|
"method": "precreate",
|
||||||
|
}
|
||||||
|
form := map[string]string{
|
||||||
|
"path": path,
|
||||||
|
"size": strconv.FormatInt(streamSize, 10),
|
||||||
|
"isdir": "0",
|
||||||
|
"autoinit": "1",
|
||||||
|
"rtype": "3",
|
||||||
|
"block_list": blockListStr,
|
||||||
|
"content-md5": contentMd5,
|
||||||
|
"slice-md5": sliceMd5,
|
||||||
|
}
|
||||||
|
joinTime(form, ctime, mtime)
|
||||||
|
|
||||||
|
log.Debugf("[baidu_netdisk] precreate data: %s", form)
|
||||||
|
_, err = d.postForm("/xpan/file", params, form, &precreateResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Debugf("%+v", precreateResp)
|
||||||
|
if precreateResp.ReturnType == 2 {
|
||||||
|
//rapid upload, since got md5 match from baidu server
|
||||||
|
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
||||||
|
precreateResp.File.Ctime = ctime
|
||||||
|
precreateResp.File.Mtime = mtime
|
||||||
|
return fileToObj(precreateResp.File), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// step.2 上传分片
|
||||||
|
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
|
||||||
|
retry.Attempts(1),
|
||||||
|
retry.Delay(time.Second),
|
||||||
|
retry.DelayType(retry.BackOffDelay))
|
||||||
|
sem := semaphore.NewWeighted(3)
|
||||||
|
for i, partseq := range precreateResp.BlockList {
|
||||||
|
if utils.IsCanceled(upCtx) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
i, partseq, offset, byteSize := i, partseq, int64(partseq)*sliceSize, sliceSize
|
||||||
|
if partseq+1 == count {
|
||||||
|
byteSize = lastBlockSize
|
||||||
|
}
|
||||||
|
threadG.Go(func(ctx context.Context) error {
|
||||||
|
if err = sem.Acquire(ctx, 1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sem.Release(1)
|
||||||
|
params := map[string]string{
|
||||||
|
"method": "upload",
|
||||||
|
"access_token": d.AccessToken,
|
||||||
|
"type": "tmpfile",
|
||||||
|
"path": path,
|
||||||
|
"uploadid": precreateResp.Uploadid,
|
||||||
|
"partseq": strconv.Itoa(partseq),
|
||||||
|
}
|
||||||
|
err := d.uploadSlice(ctx, params, stream.GetName(),
|
||||||
|
driver.NewLimitedUploadStream(ctx, io.NewSectionReader(tempFile, offset, byteSize)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
up(float64(threadG.Success()) * 100 / float64(len(precreateResp.BlockList)))
|
||||||
|
precreateResp.BlockList[i] = -1
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err = threadG.Wait(); err != nil {
|
||||||
|
// 如果属于用户主动取消,则保存上传进度
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
precreateResp.BlockList = utils.SliceFilter(precreateResp.BlockList, func(s int) bool { return s >= 0 })
|
||||||
|
base.SaveUploadProgress(d, precreateResp, d.AccessToken, contentMd5)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// step.3 创建文件
|
||||||
|
var newFile File
|
||||||
|
_, err = d.create(path, streamSize, 0, precreateResp.Uploadid, blockListStr, &newFile, mtime, ctime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
||||||
|
newFile.Ctime = ctime
|
||||||
|
newFile.Mtime = mtime
|
||||||
|
return fileToObj(newFile), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *BaiduNetdisk) uploadSlice(ctx context.Context, params map[string]string, fileName string, file io.Reader) error {
|
||||||
res, err := base.RestyClient.R().
|
res, err := base.RestyClient.R().
|
||||||
SetContext(ctx).
|
SetContext(ctx).
|
||||||
SetQueryParams(*params).
|
SetQueryParams(params).
|
||||||
SetFileReader("file", fileName, io.LimitReader(file, byteSize)).
|
SetFileReader("file", fileName, file).
|
||||||
Post(BaiduFileAPI)
|
Post(d.UploadAPI + "/rest/2.0/pcs/superfile2")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,17 @@ import (
|
|||||||
type Addition struct {
|
type Addition struct {
|
||||||
RefreshToken string `json:"refresh_token" required:"true"`
|
RefreshToken string `json:"refresh_token" required:"true"`
|
||||||
driver.RootPath
|
driver.RootPath
|
||||||
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
|
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"`
|
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
|
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
|
||||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||||
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
||||||
AccessToken string
|
AccessToken string
|
||||||
|
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||||
|
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
|
||||||
|
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
|
||||||
|
LowBandwithUploadMode bool `json:"low_bandwith_upload_mode" default:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package baidu_netdisk
|
package baidu_netdisk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TokenErrResp struct {
|
type TokenErrResp struct {
|
||||||
@@ -17,10 +19,8 @@ type File struct {
|
|||||||
//OwnerType int `json:"owner_type"`
|
//OwnerType int `json:"owner_type"`
|
||||||
//Category int `json:"category"`
|
//Category int `json:"category"`
|
||||||
//RealCategory string `json:"real_category"`
|
//RealCategory string `json:"real_category"`
|
||||||
FsId int64 `json:"fs_id"`
|
FsId int64 `json:"fs_id"`
|
||||||
ServerMtime int64 `json:"server_mtime"`
|
|
||||||
//OperId int `json:"oper_id"`
|
//OperId int `json:"oper_id"`
|
||||||
//ServerCtime int `json:"server_ctime"`
|
|
||||||
Thumbs struct {
|
Thumbs struct {
|
||||||
//Icon string `json:"icon"`
|
//Icon string `json:"icon"`
|
||||||
Url3 string `json:"url3"`
|
Url3 string `json:"url3"`
|
||||||
@@ -28,29 +28,52 @@ type File struct {
|
|||||||
//Url1 string `json:"url1"`
|
//Url1 string `json:"url1"`
|
||||||
} `json:"thumbs"`
|
} `json:"thumbs"`
|
||||||
//Wpfile int `json:"wpfile"`
|
//Wpfile int `json:"wpfile"`
|
||||||
//LocalMtime int `json:"local_mtime"`
|
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
//ExtentTinyint7 int `json:"extent_tinyint7"`
|
//ExtentTinyint7 int `json:"extent_tinyint7"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
//Share int `json:"share"`
|
//Share int `json:"share"`
|
||||||
//ServerAtime int `json:"server_atime"`
|
|
||||||
//Pl int `json:"pl"`
|
//Pl int `json:"pl"`
|
||||||
//LocalCtime int `json:"local_ctime"`
|
|
||||||
ServerFilename string `json:"server_filename"`
|
ServerFilename string `json:"server_filename"`
|
||||||
//Md5 string `json:"md5"`
|
Md5 string `json:"md5"`
|
||||||
//OwnerId int `json:"owner_id"`
|
//OwnerId int `json:"owner_id"`
|
||||||
//Unlist int `json:"unlist"`
|
//Unlist int `json:"unlist"`
|
||||||
Isdir int `json:"isdir"`
|
Isdir int `json:"isdir"`
|
||||||
|
|
||||||
|
// list resp
|
||||||
|
ServerCtime int64 `json:"server_ctime"`
|
||||||
|
ServerMtime int64 `json:"server_mtime"`
|
||||||
|
LocalMtime int64 `json:"local_mtime"`
|
||||||
|
LocalCtime int64 `json:"local_ctime"`
|
||||||
|
//ServerAtime int64 `json:"server_atime"` `
|
||||||
|
|
||||||
|
// only create and precreate resp
|
||||||
|
Ctime int64 `json:"ctime"`
|
||||||
|
Mtime int64 `json:"mtime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileToObj(f File) *model.ObjThumb {
|
func fileToObj(f File) *model.ObjThumb {
|
||||||
|
if f.ServerFilename == "" {
|
||||||
|
f.ServerFilename = path.Base(f.Path)
|
||||||
|
}
|
||||||
|
if f.ServerCtime == 0 {
|
||||||
|
f.ServerCtime = f.Ctime
|
||||||
|
}
|
||||||
|
if f.ServerMtime == 0 {
|
||||||
|
f.ServerMtime = f.Mtime
|
||||||
|
}
|
||||||
return &model.ObjThumb{
|
return &model.ObjThumb{
|
||||||
Object: model.Object{
|
Object: model.Object{
|
||||||
ID: strconv.FormatInt(f.FsId, 10),
|
ID: strconv.FormatInt(f.FsId, 10),
|
||||||
|
Path: f.Path,
|
||||||
Name: f.ServerFilename,
|
Name: f.ServerFilename,
|
||||||
Size: f.Size,
|
Size: f.Size,
|
||||||
Modified: time.Unix(f.ServerMtime, 0),
|
Modified: time.Unix(f.ServerMtime, 0),
|
||||||
|
Ctime: time.Unix(f.ServerCtime, 0),
|
||||||
IsFolder: f.Isdir == 1,
|
IsFolder: f.Isdir == 1,
|
||||||
|
|
||||||
|
// 直接获取的MD5是错误的
|
||||||
|
HashInfo: utils.NewHashInfo(utils.MD5, DecryptMd5(f.Md5)),
|
||||||
},
|
},
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbs.Url3},
|
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbs.Url3},
|
||||||
}
|
}
|
||||||
@@ -154,10 +177,15 @@ type DownloadResp2 struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PrecreateResp struct {
|
type PrecreateResp struct {
|
||||||
Path string `json:"path"`
|
Errno int `json:"errno"`
|
||||||
Uploadid string `json:"uploadid"`
|
RequestId int64 `json:"request_id"`
|
||||||
ReturnType int `json:"return_type"`
|
ReturnType int `json:"return_type"`
|
||||||
BlockList []int `json:"block_list"`
|
|
||||||
Errno int `json:"errno"`
|
// return_type=1
|
||||||
RequestId int64 `json:"request_id"`
|
Path string `json:"path"`
|
||||||
|
Uploadid string `json:"uploadid"`
|
||||||
|
BlockList []int `json:"block_list"`
|
||||||
|
|
||||||
|
// return_type=2
|
||||||
|
File File `json:"info"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
package baidu_netdisk
|
package baidu_netdisk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/avast/retry-go"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/avast/retry-go"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -21,7 +24,7 @@ import (
|
|||||||
|
|
||||||
func (d *BaiduNetdisk) refreshToken() error {
|
func (d *BaiduNetdisk) refreshToken() error {
|
||||||
err := d._refreshToken()
|
err := d._refreshToken()
|
||||||
if err != nil && err == errs.EmptyToken {
|
if err != nil && errors.Is(err, errs.EmptyToken) {
|
||||||
err = d._refreshToken()
|
err = d._refreshToken()
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -73,7 +76,7 @@ func (d *BaiduNetdisk) request(furl string, method string, callback base.ReqCall
|
|||||||
log.Info("refreshing baidu_netdisk token.")
|
log.Info("refreshing baidu_netdisk token.")
|
||||||
err2 := d.refreshToken()
|
err2 := d.refreshToken()
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
return err2
|
return retry.Unrecoverable(err2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("req: [%s] ,errno: %d, refer to https://pan.baidu.com/union/doc/", furl, errno)
|
return fmt.Errorf("req: [%s] ,errno: %d, refer to https://pan.baidu.com/union/doc/", furl, errno)
|
||||||
@@ -81,7 +84,10 @@ func (d *BaiduNetdisk) request(furl string, method string, callback base.ReqCall
|
|||||||
result = res.Body()
|
result = res.Body()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
retry.Attempts(3))
|
retry.LastErrorOnly(true),
|
||||||
|
retry.Attempts(3),
|
||||||
|
retry.Delay(time.Second),
|
||||||
|
retry.DelayType(retry.BackOffDelay))
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +97,10 @@ func (d *BaiduNetdisk) get(pathname string, params map[string]string, resp inter
|
|||||||
}, resp)
|
}, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) post(pathname string, params map[string]string, data interface{}, resp interface{}) ([]byte, error) {
|
func (d *BaiduNetdisk) postForm(pathname string, params map[string]string, form map[string]string, resp interface{}) ([]byte, error) {
|
||||||
return d.request("https://pan.baidu.com/rest/2.0"+pathname, http.MethodPost, func(req *resty.Request) {
|
return d.request("https://pan.baidu.com/rest/2.0"+pathname, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetQueryParams(params)
|
req.SetQueryParams(params)
|
||||||
req.SetBody(data)
|
req.SetFormData(form)
|
||||||
}, resp)
|
}, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +136,7 @@ func (d *BaiduNetdisk) getFiles(dir string) ([]File, error) {
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) linkOfficial(file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *BaiduNetdisk) linkOfficial(file model.Obj, _ model.LinkArgs) (*model.Link, error) {
|
||||||
var resp DownloadResp
|
var resp DownloadResp
|
||||||
params := map[string]string{
|
params := map[string]string{
|
||||||
"method": "filemetas",
|
"method": "filemetas",
|
||||||
@@ -149,6 +155,7 @@ func (d *BaiduNetdisk) linkOfficial(file model.Obj, args model.LinkArgs) (*model
|
|||||||
//if res.StatusCode() == 302 {
|
//if res.StatusCode() == 302 {
|
||||||
u = res.Header().Get("location")
|
u = res.Header().Get("location")
|
||||||
//}
|
//}
|
||||||
|
|
||||||
return &model.Link{
|
return &model.Link{
|
||||||
URL: u,
|
URL: u,
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
@@ -157,7 +164,7 @@ func (d *BaiduNetdisk) linkOfficial(file model.Obj, args model.LinkArgs) (*model
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *BaiduNetdisk) linkCrack(file model.Obj, _ model.LinkArgs) (*model.Link, error) {
|
||||||
var resp DownloadResp2
|
var resp DownloadResp2
|
||||||
param := map[string]string{
|
param := map[string]string{
|
||||||
"target": fmt.Sprintf("[\"%s\"]", file.GetPath()),
|
"target": fmt.Sprintf("[\"%s\"]", file.GetPath()),
|
||||||
@@ -171,6 +178,7 @@ func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Li
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model.Link{
|
return &model.Link{
|
||||||
URL: resp.Info[0].Dlink,
|
URL: resp.Info[0].Dlink,
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
@@ -179,32 +187,156 @@ func (d *BaiduNetdisk) linkCrack(file model.Obj, args model.LinkArgs) (*model.Li
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) manage(opera string, filelist interface{}) ([]byte, error) {
|
func (d *BaiduNetdisk) manage(opera string, filelist any) ([]byte, error) {
|
||||||
params := map[string]string{
|
params := map[string]string{
|
||||||
"method": "filemanager",
|
"method": "filemanager",
|
||||||
"opera": opera,
|
"opera": opera,
|
||||||
}
|
}
|
||||||
marshal, err := utils.Json.Marshal(filelist)
|
marshal, _ := utils.Json.MarshalToString(filelist)
|
||||||
if err != nil {
|
return d.postForm("/xpan/file", params, map[string]string{
|
||||||
return nil, err
|
"async": "0",
|
||||||
}
|
"filelist": marshal,
|
||||||
data := fmt.Sprintf("async=0&filelist=%s&ondup=newcopy", string(marshal))
|
"ondup": "fail",
|
||||||
return d.post("/xpan/file", params, data, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) create(path string, size int64, isdir int, uploadid, block_list string) ([]byte, error) {
|
func (d *BaiduNetdisk) create(path string, size int64, isdir int, uploadid, block_list string, resp any, mtime, ctime int64) ([]byte, error) {
|
||||||
params := map[string]string{
|
params := map[string]string{
|
||||||
"method": "create",
|
"method": "create",
|
||||||
}
|
}
|
||||||
data := fmt.Sprintf("path=%s&size=%d&isdir=%d&rtype=3", encodeURIComponent(path), size, isdir)
|
form := map[string]string{
|
||||||
if uploadid != "" {
|
"path": path,
|
||||||
data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list)
|
"size": strconv.FormatInt(size, 10),
|
||||||
|
"isdir": strconv.Itoa(isdir),
|
||||||
|
"rtype": "3",
|
||||||
}
|
}
|
||||||
return d.post("/xpan/file", params, data, nil)
|
if mtime != 0 && ctime != 0 {
|
||||||
|
joinTime(form, ctime, mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uploadid != "" {
|
||||||
|
form["uploadid"] = uploadid
|
||||||
|
}
|
||||||
|
if block_list != "" {
|
||||||
|
form["block_list"] = block_list
|
||||||
|
}
|
||||||
|
return d.postForm("/xpan/file", params, form, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeURIComponent(str string) string {
|
func joinTime(form map[string]string, ctime, mtime int64) {
|
||||||
r := url.QueryEscape(str)
|
form["local_mtime"] = strconv.FormatInt(mtime, 10)
|
||||||
r = strings.ReplaceAll(r, "+", "%20")
|
form["local_ctime"] = strconv.FormatInt(ctime, 10)
|
||||||
return r
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultSliceSize int64 = 4 * utils.MB
|
||||||
|
VipSliceSize int64 = 16 * utils.MB
|
||||||
|
SVipSliceSize int64 = 32 * utils.MB
|
||||||
|
|
||||||
|
MaxSliceNum = 2048 // 文档写的是 1024/没写 ,但实际测试是 2048
|
||||||
|
SliceStep int64 = 1 * utils.MB
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *BaiduNetdisk) getSliceSize(filesize int64) int64 {
|
||||||
|
// 非会员固定为 4MB
|
||||||
|
if d.vipType == 0 {
|
||||||
|
if d.CustomUploadPartSize != 0 {
|
||||||
|
log.Warnf("CustomUploadPartSize is not supported for non-vip user, use DefaultSliceSize")
|
||||||
|
}
|
||||||
|
if filesize > MaxSliceNum*DefaultSliceSize {
|
||||||
|
log.Warnf("File size(%d) is too large, may cause upload failure", filesize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultSliceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.CustomUploadPartSize != 0 {
|
||||||
|
if d.CustomUploadPartSize < DefaultSliceSize {
|
||||||
|
log.Warnf("CustomUploadPartSize(%d) is less than DefaultSliceSize(%d), use DefaultSliceSize", d.CustomUploadPartSize, DefaultSliceSize)
|
||||||
|
return DefaultSliceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.vipType == 1 && d.CustomUploadPartSize > VipSliceSize {
|
||||||
|
log.Warnf("CustomUploadPartSize(%d) is greater than VipSliceSize(%d), use VipSliceSize", d.CustomUploadPartSize, VipSliceSize)
|
||||||
|
return VipSliceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.vipType == 2 && d.CustomUploadPartSize > SVipSliceSize {
|
||||||
|
log.Warnf("CustomUploadPartSize(%d) is greater than SVipSliceSize(%d), use SVipSliceSize", d.CustomUploadPartSize, SVipSliceSize)
|
||||||
|
return SVipSliceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.CustomUploadPartSize
|
||||||
|
}
|
||||||
|
|
||||||
|
maxSliceSize := DefaultSliceSize
|
||||||
|
|
||||||
|
switch d.vipType {
|
||||||
|
case 1:
|
||||||
|
maxSliceSize = VipSliceSize
|
||||||
|
case 2:
|
||||||
|
maxSliceSize = SVipSliceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload on low bandwidth
|
||||||
|
if d.LowBandwithUploadMode {
|
||||||
|
size := DefaultSliceSize
|
||||||
|
|
||||||
|
for size <= maxSliceSize {
|
||||||
|
if filesize <= MaxSliceNum*size {
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
size += SliceStep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filesize > MaxSliceNum*maxSliceSize {
|
||||||
|
log.Warnf("File size(%d) is too large, may cause upload failure", filesize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxSliceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// func encodeURIComponent(str string) string {
|
||||||
|
// r := url.QueryEscape(str)
|
||||||
|
// r = strings.ReplaceAll(r, "+", "%20")
|
||||||
|
// return r
|
||||||
|
// }
|
||||||
|
|
||||||
|
func DecryptMd5(encryptMd5 string) string {
|
||||||
|
if _, err := hex.DecodeString(encryptMd5); err == nil {
|
||||||
|
return encryptMd5
|
||||||
|
}
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
out.Grow(len(encryptMd5))
|
||||||
|
for i, n := 0, int64(0); i < len(encryptMd5); i++ {
|
||||||
|
if i == 9 {
|
||||||
|
n = int64(unicode.ToLower(rune(encryptMd5[i])) - 'g')
|
||||||
|
} else {
|
||||||
|
n, _ = strconv.ParseInt(encryptMd5[i:i+1], 16, 64)
|
||||||
|
}
|
||||||
|
out.WriteString(strconv.FormatInt(n^int64(15&i), 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptMd5 = out.String()
|
||||||
|
return encryptMd5[8:16] + encryptMd5[:8] + encryptMd5[24:32] + encryptMd5[16:24]
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncryptMd5(originalMd5 string) string {
|
||||||
|
reversed := originalMd5[8:16] + originalMd5[:8] + originalMd5[24:32] + originalMd5[16:24]
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
out.Grow(len(reversed))
|
||||||
|
for i, n := 0, int64(0); i < len(reversed); i++ {
|
||||||
|
n, _ = strconv.ParseInt(reversed[i:i+1], 16, 64)
|
||||||
|
n ^= int64(15 & i)
|
||||||
|
if i == 9 {
|
||||||
|
out.WriteRune(rune(n) + 'g')
|
||||||
|
} else {
|
||||||
|
out.WriteString(strconv.FormatInt(n, 16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,24 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/errgroup"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/avast/retry-go"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,9 +29,12 @@ type BaiduPhoto struct {
|
|||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
|
|
||||||
AccessToken string
|
// AccessToken string
|
||||||
Uk int64
|
Uk int64
|
||||||
root model.Obj
|
bdstoken string
|
||||||
|
root model.Obj
|
||||||
|
|
||||||
|
uploadThread int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduPhoto) Config() driver.Config {
|
func (d *BaiduPhoto) Config() driver.Config {
|
||||||
@@ -37,10 +46,15 @@ func (d *BaiduPhoto) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduPhoto) Init(ctx context.Context) error {
|
func (d *BaiduPhoto) Init(ctx context.Context) error {
|
||||||
if err := d.refreshToken(); err != nil {
|
d.uploadThread, _ = strconv.Atoi(d.UploadThread)
|
||||||
return err
|
if d.uploadThread < 1 || d.uploadThread > 32 {
|
||||||
|
d.uploadThread, d.UploadThread = 3, "3"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if err := d.refreshToken(); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
// root
|
// root
|
||||||
if d.AlbumID != "" {
|
if d.AlbumID != "" {
|
||||||
albumID := strings.Split(d.AlbumID, "|")[0]
|
albumID := strings.Split(d.AlbumID, "|")[0]
|
||||||
@@ -62,6 +76,10 @@ func (d *BaiduPhoto) Init(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
d.bdstoken, err = d.getBDStoken()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
d.Uk, err = strconv.ParseInt(info.YouaID, 10, 64)
|
d.Uk, err = strconv.ParseInt(info.YouaID, 10, 64)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -71,7 +89,7 @@ func (d *BaiduPhoto) GetRoot(ctx context.Context) (model.Obj, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduPhoto) Drop(ctx context.Context) error {
|
func (d *BaiduPhoto) Drop(ctx context.Context) error {
|
||||||
d.AccessToken = ""
|
// d.AccessToken = ""
|
||||||
d.Uk = 0
|
d.Uk = 0
|
||||||
d.root = nil
|
d.root = nil
|
||||||
return nil
|
return nil
|
||||||
@@ -126,13 +144,18 @@ func (d *BaiduPhoto) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
|||||||
case *File:
|
case *File:
|
||||||
return d.linkFile(ctx, file, args)
|
return d.linkFile(ctx, file, args)
|
||||||
case *AlbumFile:
|
case *AlbumFile:
|
||||||
f, err := d.CopyAlbumFile(ctx, file)
|
// 处理共享相册
|
||||||
if err != nil {
|
if d.Uk != file.Uk {
|
||||||
return nil, err
|
// 有概率无法获取到链接
|
||||||
|
// return d.linkAlbum(ctx, file, args)
|
||||||
|
|
||||||
|
f, err := d.CopyAlbumFile(ctx, file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d.linkFile(ctx, f, args)
|
||||||
}
|
}
|
||||||
return d.linkFile(ctx, f, args)
|
return d.linkFile(ctx, &file.File, args)
|
||||||
// 有概率无法获取到链接
|
|
||||||
//return d.linkAlbum(ctx, file, args)
|
|
||||||
}
|
}
|
||||||
return nil, errs.NotFile
|
return nil, errs.NotFile
|
||||||
}
|
}
|
||||||
@@ -211,45 +234,57 @@ func (d *BaiduPhoto) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
|
// 不支持大小为0的文件
|
||||||
|
if stream.GetSize() == 0 {
|
||||||
|
return nil, fmt.Errorf("file size cannot be zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// 暂时没有找到妙传方式
|
||||||
|
|
||||||
// 需要获取完整文件md5,必须支持 io.Seek
|
// 需要获取完整文件md5,必须支持 io.Seek
|
||||||
tempFile, err := utils.CreateTempFile(stream.GetReadCloser(), stream.GetSize())
|
tempFile, err := stream.CacheFullInTempFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
_ = tempFile.Close()
|
const DEFAULT int64 = 1 << 22
|
||||||
_ = os.Remove(tempFile.Name())
|
const SliceSize int64 = 1 << 18
|
||||||
}()
|
|
||||||
|
|
||||||
// 计算需要的数据
|
// 计算需要的数据
|
||||||
const DEFAULT = 1 << 22
|
streamSize := stream.GetSize()
|
||||||
const SliceSize = 1 << 18
|
count := int(math.Ceil(float64(streamSize) / float64(DEFAULT)))
|
||||||
count := int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT)))
|
lastBlockSize := streamSize % DEFAULT
|
||||||
|
if lastBlockSize == 0 {
|
||||||
|
lastBlockSize = DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
// step.1 计算MD5
|
||||||
sliceMD5List := make([]string, 0, count)
|
sliceMD5List := make([]string, 0, count)
|
||||||
fileMd5 := md5.New()
|
byteSize := int64(DEFAULT)
|
||||||
sliceMd5 := md5.New()
|
fileMd5H := md5.New()
|
||||||
sliceMd52 := md5.New()
|
sliceMd5H := md5.New()
|
||||||
slicemd52Write := utils.LimitWriter(sliceMd52, SliceSize)
|
sliceMd5H2 := md5.New()
|
||||||
|
slicemd5H2Write := utils.LimitWriter(sliceMd5H2, SliceSize)
|
||||||
for i := 1; i <= count; i++ {
|
for i := 1; i <= count; i++ {
|
||||||
if utils.IsCanceled(ctx) {
|
if utils.IsCanceled(ctx) {
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
|
if i == count {
|
||||||
_, err := io.CopyN(io.MultiWriter(fileMd5, sliceMd5, slicemd52Write), tempFile, DEFAULT)
|
byteSize = lastBlockSize
|
||||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
}
|
||||||
|
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sliceMD5List = append(sliceMD5List, hex.EncodeToString(sliceMd5.Sum(nil)))
|
sliceMD5List = append(sliceMD5List, hex.EncodeToString(sliceMd5H.Sum(nil)))
|
||||||
sliceMd5.Reset()
|
sliceMd5H.Reset()
|
||||||
}
|
}
|
||||||
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
|
contentMd5 := hex.EncodeToString(fileMd5H.Sum(nil))
|
||||||
return nil, err
|
sliceMd5 := hex.EncodeToString(sliceMd5H2.Sum(nil))
|
||||||
}
|
blockListStr, _ := utils.Json.MarshalToString(sliceMD5List)
|
||||||
content_md5 := hex.EncodeToString(fileMd5.Sum(nil))
|
|
||||||
slice_md5 := hex.EncodeToString(sliceMd52.Sum(nil))
|
|
||||||
|
|
||||||
// 开始执行上传
|
// step.2 预上传
|
||||||
params := map[string]string{
|
params := map[string]string{
|
||||||
"autoinit": "1",
|
"autoinit": "1",
|
||||||
"isdir": "0",
|
"isdir": "0",
|
||||||
@@ -257,56 +292,87 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
|||||||
"ctype": "11",
|
"ctype": "11",
|
||||||
"path": fmt.Sprintf("/%s", stream.GetName()),
|
"path": fmt.Sprintf("/%s", stream.GetName()),
|
||||||
"size": fmt.Sprint(stream.GetSize()),
|
"size": fmt.Sprint(stream.GetSize()),
|
||||||
"slice-md5": slice_md5,
|
"slice-md5": sliceMd5,
|
||||||
"content-md5": content_md5,
|
"content-md5": contentMd5,
|
||||||
"block_list": MustString(utils.Json.MarshalToString(sliceMD5List)),
|
"block_list": blockListStr,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预上传
|
// 尝试获取之前的进度
|
||||||
var precreateResp PrecreateResp
|
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, strconv.FormatInt(d.Uk, 10), contentMd5)
|
||||||
_, err = d.Post(FILE_API_URL_V1+"/precreate", func(r *resty.Request) {
|
if !ok {
|
||||||
r.SetContext(ctx)
|
_, err = d.Post(FILE_API_URL_V1+"/precreate", func(r *resty.Request) {
|
||||||
r.SetFormData(params)
|
r.SetContext(ctx)
|
||||||
}, &precreateResp)
|
r.SetFormData(params)
|
||||||
if err != nil {
|
r.SetQueryParam("bdstoken", d.bdstoken)
|
||||||
return nil, err
|
}, &precreateResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch precreateResp.ReturnType {
|
switch precreateResp.ReturnType {
|
||||||
case 1: // 上传文件
|
case 1: //step.3 上传文件切片
|
||||||
uploadParams := map[string]string{
|
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
|
||||||
"method": "upload",
|
retry.Attempts(3),
|
||||||
"path": params["path"],
|
retry.Delay(time.Second),
|
||||||
"uploadid": precreateResp.UploadID,
|
retry.DelayType(retry.BackOffDelay))
|
||||||
}
|
sem := semaphore.NewWeighted(3)
|
||||||
|
for i, partseq := range precreateResp.BlockList {
|
||||||
|
if utils.IsCanceled(upCtx) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
i, partseq, offset, byteSize := i, partseq, int64(partseq)*DEFAULT, DEFAULT
|
||||||
if utils.IsCanceled(ctx) {
|
if partseq+1 == count {
|
||||||
return nil, ctx.Err()
|
byteSize = lastBlockSize
|
||||||
}
|
}
|
||||||
uploadParams["partseq"] = fmt.Sprint(i)
|
|
||||||
_, err = d.Post("https://c3.pcs.baidu.com/rest/2.0/pcs/superfile2", func(r *resty.Request) {
|
threadG.Go(func(ctx context.Context) error {
|
||||||
r.SetContext(ctx)
|
if err = sem.Acquire(ctx, 1); err != nil {
|
||||||
r.SetQueryParams(uploadParams)
|
return err
|
||||||
r.SetFileReader("file", stream.GetName(), io.LimitReader(tempFile, DEFAULT))
|
}
|
||||||
}, nil)
|
defer sem.Release(1)
|
||||||
if err != nil {
|
uploadParams := map[string]string{
|
||||||
return nil, err
|
"method": "upload",
|
||||||
|
"path": params["path"],
|
||||||
|
"partseq": fmt.Sprint(partseq),
|
||||||
|
"uploadid": precreateResp.UploadID,
|
||||||
|
"app_id": "16051585",
|
||||||
|
}
|
||||||
|
_, err = d.Post("https://c3.pcs.baidu.com/rest/2.0/pcs/superfile2", func(r *resty.Request) {
|
||||||
|
r.SetContext(ctx)
|
||||||
|
r.SetQueryParams(uploadParams)
|
||||||
|
r.SetFileReader("file", stream.GetName(),
|
||||||
|
driver.NewLimitedUploadStream(ctx, io.NewSectionReader(tempFile, offset, byteSize)))
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
up(float64(threadG.Success()) * 100 / float64(len(precreateResp.BlockList)))
|
||||||
|
precreateResp.BlockList[i] = -1
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err = threadG.Wait(); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
precreateResp.BlockList = utils.SliceFilter(precreateResp.BlockList, func(s int) bool { return s >= 0 })
|
||||||
|
base.SaveUploadProgress(d, strconv.FormatInt(d.Uk, 10), contentMd5)
|
||||||
}
|
}
|
||||||
up(i * 100 / count)
|
return nil, err
|
||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case 2: // 创建文件
|
case 2: //step.4 创建文件
|
||||||
params["uploadid"] = precreateResp.UploadID
|
params["uploadid"] = precreateResp.UploadID
|
||||||
_, err = d.Post(FILE_API_URL_V1+"/create", func(r *resty.Request) {
|
_, err = d.Post(FILE_API_URL_V1+"/create", func(r *resty.Request) {
|
||||||
r.SetContext(ctx)
|
r.SetContext(ctx)
|
||||||
r.SetFormData(params)
|
r.SetFormData(params)
|
||||||
|
r.SetQueryParam("bdstoken", d.bdstoken)
|
||||||
}, &precreateResp)
|
}, &precreateResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case 3: // 增加到相册
|
case 3: //step.5 增加到相册
|
||||||
rootfile := precreateResp.Data.toFile()
|
rootfile := precreateResp.Data.toFile()
|
||||||
if album, ok := dstDir.(*Album); ok {
|
if album, ok := dstDir.(*Album); ok {
|
||||||
return d.AddAlbumFile(ctx, album, rootfile)
|
return d.AddAlbumFile(ctx, album, rootfile)
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ func moveFileToAlbumFile(file *File, album *Album, uk int64) *AlbumFile {
|
|||||||
|
|
||||||
func renameAlbum(album *Album, newName string) *Album {
|
func renameAlbum(album *Album, newName string) *Album {
|
||||||
return &Album{
|
return &Album{
|
||||||
AlbumID: album.AlbumID,
|
AlbumID: album.AlbumID,
|
||||||
Tid: album.Tid,
|
Tid: album.Tid,
|
||||||
JoinTime: album.JoinTime,
|
JoinTime: album.JoinTime,
|
||||||
CreateTime: album.CreateTime,
|
CreationTime: album.CreationTime,
|
||||||
Title: newName,
|
Title: newName,
|
||||||
Mtime: time.Now().Unix(),
|
Mtime: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
RefreshToken string `json:"refresh_token" required:"true"`
|
// RefreshToken string `json:"refresh_token" required:"true"`
|
||||||
ShowType string `json:"show_type" type:"select" options:"root,root_only_album,root_only_file" default:"root"`
|
Cookie string `json:"cookie" required:"true"`
|
||||||
AlbumID string `json:"album_id"`
|
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"`
|
//AlbumPassword string `json:"album_password"`
|
||||||
DeleteOrigin bool `json:"delete_origin"`
|
DeleteOrigin bool `json:"delete_origin"`
|
||||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
// ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
// ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||||
|
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,22 +53,17 @@ type (
|
|||||||
Ctime int64 `json:"ctime"` // 创建时间 s
|
Ctime int64 `json:"ctime"` // 创建时间 s
|
||||||
Mtime int64 `json:"mtime"` // 修改时间 s
|
Mtime int64 `json:"mtime"` // 修改时间 s
|
||||||
Thumburl []string `json:"thumburl"`
|
Thumburl []string `json:"thumburl"`
|
||||||
|
Md5 string `json:"md5"`
|
||||||
parseTime *time.Time
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *File) GetSize() int64 { return c.Size }
|
func (c *File) GetSize() int64 { return c.Size }
|
||||||
func (c *File) GetName() string { return getFileName(c.Path) }
|
func (c *File) GetName() string { return getFileName(c.Path) }
|
||||||
func (c *File) ModTime() time.Time {
|
func (c *File) CreateTime() time.Time { return time.Unix(c.Ctime, 0) }
|
||||||
if c.parseTime == nil {
|
func (c *File) ModTime() time.Time { return time.Unix(c.Mtime, 0) }
|
||||||
c.parseTime = toTime(c.Mtime)
|
func (c *File) IsDir() bool { return false }
|
||||||
}
|
func (c *File) GetID() string { return "" }
|
||||||
return *c.parseTime
|
func (c *File) GetPath() string { return "" }
|
||||||
}
|
|
||||||
func (c *File) IsDir() bool { return false }
|
|
||||||
func (c *File) GetID() string { return "" }
|
|
||||||
func (c *File) GetPath() string { return "" }
|
|
||||||
func (c *File) Thumb() string {
|
func (c *File) Thumb() string {
|
||||||
if len(c.Thumburl) > 0 {
|
if len(c.Thumburl) > 0 {
|
||||||
return c.Thumburl[0]
|
return c.Thumburl[0]
|
||||||
@@ -74,6 +71,10 @@ func (c *File) Thumb() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *File) GetHash() utils.HashInfo {
|
||||||
|
return utils.NewHashInfo(utils.MD5, DecryptMd5(c.Md5))
|
||||||
|
}
|
||||||
|
|
||||||
/*相册部分*/
|
/*相册部分*/
|
||||||
type (
|
type (
|
||||||
AlbumListResp struct {
|
AlbumListResp struct {
|
||||||
@@ -84,12 +85,12 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
Album struct {
|
Album struct {
|
||||||
AlbumID string `json:"album_id"`
|
AlbumID string `json:"album_id"`
|
||||||
Tid int64 `json:"tid"`
|
Tid int64 `json:"tid"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
JoinTime int64 `json:"join_time"`
|
JoinTime int64 `json:"join_time"`
|
||||||
CreateTime int64 `json:"create_time"`
|
CreationTime int64 `json:"create_time"`
|
||||||
Mtime int64 `json:"mtime"`
|
Mtime int64 `json:"mtime"`
|
||||||
|
|
||||||
parseTime *time.Time
|
parseTime *time.Time
|
||||||
}
|
}
|
||||||
@@ -109,17 +110,17 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *Album) GetSize() int64 { return 0 }
|
func (a *Album) GetHash() utils.HashInfo {
|
||||||
func (a *Album) GetName() string { return a.Title }
|
return utils.HashInfo{}
|
||||||
func (a *Album) ModTime() time.Time {
|
|
||||||
if a.parseTime == nil {
|
|
||||||
a.parseTime = toTime(a.Mtime)
|
|
||||||
}
|
|
||||||
return *a.parseTime
|
|
||||||
}
|
}
|
||||||
func (a *Album) IsDir() bool { return true }
|
|
||||||
func (a *Album) GetID() string { return "" }
|
func (a *Album) GetSize() int64 { return 0 }
|
||||||
func (a *Album) GetPath() string { return "" }
|
func (a *Album) GetName() string { return a.Title }
|
||||||
|
func (a *Album) CreateTime() time.Time { return time.Unix(a.CreationTime, 0) }
|
||||||
|
func (a *Album) ModTime() time.Time { return time.Unix(a.Mtime, 0) }
|
||||||
|
func (a *Album) IsDir() bool { return true }
|
||||||
|
func (a *Album) GetID() string { return "" }
|
||||||
|
func (a *Album) GetPath() string { return "" }
|
||||||
|
|
||||||
type (
|
type (
|
||||||
CopyFileResp struct {
|
CopyFileResp struct {
|
||||||
@@ -160,9 +161,9 @@ type (
|
|||||||
CreateFileResp
|
CreateFileResp
|
||||||
|
|
||||||
//不存在返回
|
//不存在返回
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
UploadID string `json:"uploadid"`
|
UploadID string `json:"uploadid"`
|
||||||
Blocklist []int64 `json:"block_list"`
|
BlockList []int `json:"block_list"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ package baiduphoto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
)
|
)
|
||||||
@@ -21,9 +23,10 @@ const (
|
|||||||
FILE_API_URL_V2 = API_URL + "/file/v2"
|
FILE_API_URL_V2 = API_URL + "/file/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
func (d *BaiduPhoto) Request(client *resty.Client, furl string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
||||||
req := base.RestyClient.R().
|
req := client.R().
|
||||||
SetQueryParam("access_token", d.AccessToken)
|
// SetQueryParam("access_token", d.AccessToken)
|
||||||
|
SetHeader("Cookie", d.Cookie)
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(req)
|
callback(req)
|
||||||
}
|
}
|
||||||
@@ -45,10 +48,10 @@ func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallba
|
|||||||
return nil, fmt.Errorf("no shared albums found")
|
return nil, fmt.Errorf("no shared albums found")
|
||||||
case 50100:
|
case 50100:
|
||||||
return nil, fmt.Errorf("illegal title, only supports 50 characters")
|
return nil, fmt.Errorf("illegal title, only supports 50 characters")
|
||||||
case -6:
|
// case -6:
|
||||||
if err = d.refreshToken(); err != nil {
|
// if err = d.refreshToken(); err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron)
|
return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron)
|
||||||
}
|
}
|
||||||
@@ -63,36 +66,36 @@ func (d *BaiduPhoto) Request(furl string, method string, callback base.ReqCallba
|
|||||||
// return res.Body(), nil
|
// return res.Body(), nil
|
||||||
//}
|
//}
|
||||||
|
|
||||||
func (d *BaiduPhoto) refreshToken() error {
|
// func (d *BaiduPhoto) refreshToken() error {
|
||||||
u := "https://openapi.baidu.com/oauth/2.0/token"
|
// u := "https://openapi.baidu.com/oauth/2.0/token"
|
||||||
var resp base.TokenResp
|
// var resp base.TokenResp
|
||||||
var e TokenErrResp
|
// var e TokenErrResp
|
||||||
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
|
// _, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
|
||||||
"grant_type": "refresh_token",
|
// "grant_type": "refresh_token",
|
||||||
"refresh_token": d.RefreshToken,
|
// "refresh_token": d.RefreshToken,
|
||||||
"client_id": d.ClientID,
|
// "client_id": d.ClientID,
|
||||||
"client_secret": d.ClientSecret,
|
// "client_secret": d.ClientSecret,
|
||||||
}).Get(u)
|
// }).Get(u)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
if e.ErrorMsg != "" {
|
// if e.ErrorMsg != "" {
|
||||||
return &e
|
// return &e
|
||||||
}
|
// }
|
||||||
if resp.RefreshToken == "" {
|
// if resp.RefreshToken == "" {
|
||||||
return errs.EmptyToken
|
// return errs.EmptyToken
|
||||||
}
|
// }
|
||||||
d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
|
// d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken
|
||||||
op.MustSaveDriverStorage(d)
|
// op.MustSaveDriverStorage(d)
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (d *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
func (d *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
||||||
return d.Request(furl, http.MethodGet, callback, resp)
|
return d.Request(base.RestyClient, furl, http.MethodGet, callback, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
func (d *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
||||||
return d.Request(furl, http.MethodPost, callback, resp)
|
return d.Request(base.RestyClient, furl, http.MethodPost, callback, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有文件
|
// 获取所有文件
|
||||||
@@ -338,24 +341,29 @@ func (d *BaiduPhoto) linkAlbum(ctx context.Context, file *AlbumFile, args model.
|
|||||||
headers["X-Forwarded-For"] = args.IP
|
headers["X-Forwarded-For"] = args.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := base.NoRedirectClient.R().
|
resp, err := d.Request(base.NoRedirectClient, ALBUM_API_URL+"/download", http.MethodHead, func(r *resty.Request) {
|
||||||
SetContext(ctx).
|
r.SetContext(ctx)
|
||||||
SetHeaders(headers).
|
r.SetHeaders(headers)
|
||||||
SetQueryParams(map[string]string{
|
r.SetQueryParams(map[string]string{
|
||||||
"access_token": d.AccessToken,
|
"fsid": fmt.Sprint(file.Fsid),
|
||||||
"fsid": fmt.Sprint(file.Fsid),
|
"album_id": file.AlbumID,
|
||||||
"album_id": file.AlbumID,
|
"tid": fmt.Sprint(file.Tid),
|
||||||
"tid": fmt.Sprint(file.Tid),
|
"uk": fmt.Sprint(file.Uk),
|
||||||
"uk": fmt.Sprint(file.Uk),
|
})
|
||||||
}).
|
}, nil)
|
||||||
Head(ALBUM_API_URL + "/download")
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode() != 302 {
|
||||||
|
return nil, fmt.Errorf("not found 302 redirect")
|
||||||
|
}
|
||||||
|
|
||||||
|
location := resp.Header().Get("Location")
|
||||||
|
|
||||||
link := &model.Link{
|
link := &model.Link{
|
||||||
URL: res.Header().Get("location"),
|
URL: location,
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
"User-Agent": []string{headers["User-Agent"]},
|
"User-Agent": []string{headers["User-Agent"]},
|
||||||
"Referer": []string{"https://photo.baidu.com/"},
|
"Referer": []string{"https://photo.baidu.com/"},
|
||||||
@@ -385,10 +393,24 @@ func (d *BaiduPhoto) linkFile(ctx context.Context, file *File, args model.LinkAr
|
|||||||
"fsid": fmt.Sprint(file.Fsid),
|
"fsid": fmt.Sprint(file.Fsid),
|
||||||
})
|
})
|
||||||
}, &downloadUrl)
|
}, &downloadUrl)
|
||||||
|
|
||||||
|
// resp, err := d.Request(base.NoRedirectClient, FILE_API_URL_V1+"/download", http.MethodHead, func(r *resty.Request) {
|
||||||
|
// r.SetContext(ctx)
|
||||||
|
// r.SetHeaders(headers)
|
||||||
|
// r.SetQueryParams(map[string]string{
|
||||||
|
// "fsid": fmt.Sprint(file.Fsid),
|
||||||
|
// })
|
||||||
|
// }, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if resp.StatusCode() != 302 {
|
||||||
|
// return nil, fmt.Errorf("not found 302 redirect")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// location := resp.Header().Get("Location")
|
||||||
link := &model.Link{
|
link := &model.Link{
|
||||||
URL: downloadUrl.Dlink,
|
URL: downloadUrl.Dlink,
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
@@ -453,3 +475,55 @@ func (d *BaiduPhoto) uInfo() (*UInfo, error) {
|
|||||||
}
|
}
|
||||||
return &info, nil
|
return &info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *BaiduPhoto) getBDStoken() (string, error) {
|
||||||
|
var info struct {
|
||||||
|
Result struct {
|
||||||
|
Bdstoken string `json:"bdstoken"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Uk int64 `json:"uk"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
_, err := d.Get("https://pan.baidu.com/api/gettemplatevariable?fields=[%22bdstoken%22,%22token%22,%22uk%22]", nil, &info)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return info.Result.Bdstoken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptMd5(encryptMd5 string) string {
|
||||||
|
if _, err := hex.DecodeString(encryptMd5); err == nil {
|
||||||
|
return encryptMd5
|
||||||
|
}
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
out.Grow(len(encryptMd5))
|
||||||
|
for i, n := 0, int64(0); i < len(encryptMd5); i++ {
|
||||||
|
if i == 9 {
|
||||||
|
n = int64(unicode.ToLower(rune(encryptMd5[i])) - 'g')
|
||||||
|
} else {
|
||||||
|
n, _ = strconv.ParseInt(encryptMd5[i:i+1], 16, 64)
|
||||||
|
}
|
||||||
|
out.WriteString(strconv.FormatInt(n^int64(15&i), 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptMd5 = out.String()
|
||||||
|
return encryptMd5[8:16] + encryptMd5[:8] + encryptMd5[24:32] + encryptMd5[16:24]
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncryptMd5(originalMd5 string) string {
|
||||||
|
reversed := originalMd5[8:16] + originalMd5[:8] + originalMd5[24:32] + originalMd5[16:24]
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
out.Grow(len(reversed))
|
||||||
|
for i, n := 0, int64(0); i < len(reversed); i++ {
|
||||||
|
n, _ = strconv.ParseInt(reversed[i:i+1], 16, 64)
|
||||||
|
n ^= int64(15 & i)
|
||||||
|
if i == 9 {
|
||||||
|
out.WriteRune(rune(n) + 'g')
|
||||||
|
} else {
|
||||||
|
out.WriteString(strconv.FormatInt(n, 16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user