Compare commits

...

123 Commits

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

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

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

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

* refactor: keep the official api

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

* alidrive增加快传开关
2022-04-03 17:56:21 +08:00
18c82e79b5 feat: sftp support 2022-04-02 19:28:43 +08:00
d69d24a5b2 fix(alidrive): judge status of delete folder (close #886) 2022-04-02 14:34:34 +08:00
342729179d chore: Merge pull request #884 from Xhofe/dev
fix: some issues of webdav due to virtual path
2022-04-01 22:02:39 +08:00
0537449335 fix(webdav): virtual path no account 2022-04-01 21:57:55 +08:00
df90311453 fix(webdav): alist path not found 2022-04-01 20:40:57 +08:00
876579ea3b chore: Merge pull request #874 from Xhofe/dev
support mount to root path
2022-04-01 09:42:37 +08:00
e83081380e workflow: cancel build docker for pr 2022-04-01 09:40:53 +08:00
9daeaf7562 fix: virtual path, support mount to root path 2022-04-01 09:40:08 +08:00
104 changed files with 5574 additions and 980 deletions

View File

@ -51,6 +51,33 @@
"contributions": [
"doc"
]
},
{
"login": "Windman1320",
"name": "Windman",
"avatar_url": "https://avatars.githubusercontent.com/u/9999486?v=4",
"profile": "https://github.com/Windman1320",
"contributions": [
"code"
]
},
{
"login": "ericarena",
"name": "ericarena",
"avatar_url": "https://avatars.githubusercontent.com/u/4518927?v=4",
"profile": "https://github.com/ericarena",
"contributions": [
"code"
]
},
{
"login": "WntFlm",
"name": "WntFlm",
"avatar_url": "https://avatars.githubusercontent.com/u/34620278?v=4",
"profile": "https://github.com/WntFlm",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@ -7,6 +7,14 @@ body:
value: |
Thanks for taking the time to fill out this bug report, please **confirm that your issue is not a duplicate issue and not because of your operation or version issues**
感谢您花时间填写此错误报告,请**务必确认您的issue不是重复的且不是因为您的操作或版本问题**
- type: checkboxes
attributes:
label: Please make sure of the following things
description: You may select more than one, even select all.
options:
- label: I have read the [documentation](https://alist-doc.nn.ci).
- label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure it's due to `alist` and not something else(such as `Dependencies` or `Operational`).
- type: input
id: version
attributes:

View File

@ -2,6 +2,15 @@ name: "Feature request"
description: Feature request
labels: ["enhancement: pending triage"]
body:
- type: checkboxes
attributes:
label: Please make sure of the following things
description: You may select more than one, even select all.
options:
- label: I have read the [documentation](https://alist-doc.nn.ci).
- label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure this feature is not implemented.
- label: I'm sure it's a reasonable and popular requirement.
- type: textarea
id: feature-description
attributes:

View File

@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [1.17]
go-version: [1.18]
name: Build
runs-on: ${{ matrix.platform }}
steps:
@ -30,7 +30,7 @@ jobs:
with:
path: alist
- name: Install upx
- name: Install dependencies
run: |
docker pull techknowlogick/xgo:latest
go install src.techknowlogick.com/xgo@latest

View File

@ -3,8 +3,6 @@ name: build_docker
on:
push:
branches: [ v2 ]
pull_request:
branches: [ v2 ]
jobs:
build_docker:

View File

@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [1.17]
go-version: [1.18]
name: Release
runs-on: ${{ matrix.platform }}
steps:

1
.gitignore vendored
View File

@ -22,6 +22,7 @@ dist/
# vendor/
bin/*
/alist
/alist.exe
*.json
public/*.html
public/assets/

View File

@ -7,7 +7,7 @@
Prerequisites:
- [git](https://nodejs.org/zh-cn/)
- [Go 1.17+](https://golang.org/doc/install)
- [Go 1.18+](https://golang.org/doc/install)
- [gcc](https://gcc.gnu.org/)
- [nodejs](https://nodejs.org/)

View File

@ -1,5 +1,5 @@
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
## Contributors ✨
@ -16,6 +16,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://www.iflu.cf/"><img src="https://avatars.githubusercontent.com/u/63903027?v=4?s=100" width="100px;" alt=""/><br /><sub><b>道辰</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=DaoChen6" title="Documentation">📖</a></td>
<td align="center"><a href="https://vg-land.github.io/"><img src="https://avatars.githubusercontent.com/u/16739728?v=4?s=100" width="100px;" alt=""/><br /><sub><b>vg-land</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=vg-land" title="Code">💻</a></td>
<td align="center"><a href="https://c5y.moe"><img src="https://avatars.githubusercontent.com/u/18461360?v=4?s=100" width="100px;" alt=""/><br /><sub><b>凌莞~(=^▽^=)</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=Clansty" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Windman1320"><img src="https://avatars.githubusercontent.com/u/9999486?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Windman</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=Windman1320" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ericarena"><img src="https://avatars.githubusercontent.com/u/4518927?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ericarena</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=ericarena" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/WntFlm"><img src="https://avatars.githubusercontent.com/u/34620278?v=4?s=100" width="100px;" alt=""/><br /><sub><b>WntFlm</b></sub></a><br /><a href="https://github.com/Xhofe/alist/commits?author=WntFlm" title="Code">💻</a></td>
</tr>
</table>

View File

@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
<https://www.gnu.org/licenses/>.

View File

@ -30,7 +30,7 @@ English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contri
- [x] [PikPak](https://www.mypikpak.com/)
- [x] [ShandianPan](https://shandianpan.com/)
- [x] [S3](https://aws.amazon.com/s3/)
- [x] WebDav
- [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
- [x] [Mediatrack](https://www.mediatrack.cn/)
- [x] [139yun](https://yun.139.com/) (Personal, Family)
@ -38,6 +38,8 @@ English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contri
- [x] [Baidu Disk](http://pan.baidu.com/)
- [x] [Quark](https://pan.quark.cn)
- [x] [XunleiCloud](https://pan.xunlei.com/)
- [x] SFTP
- [x] [Baidu.Photo](https://photo.baidu.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
@ -86,4 +88,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) · [@QQGroup](https://jq.qq.com/?_wv=1027&k=OVPJcv2b)
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@QQGroup](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)

View File

@ -30,7 +30,7 @@
- [x] [PikPak](https://www.mypikpak.com/)
- [x] [闪电盘](https://shandianpan.com/)
- [x] [S3](https://aws.amazon.com/cn/s3/)
- [x] WebDav
- [x] WebDav(支持无API的OneDrive/SharePoint)
- [x] Teambition[中国](https://www.teambition.com/ )[国际](https://us.teambition.com/ )
- [x] [分秒帧](https://www.mediatrack.cn/)
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
@ -38,6 +38,8 @@
- [x] [百度网盘](http://pan.baidu.com/)
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷云盘](https://pan.xunlei.com/)
- [x] SFTP
- [x] [一刻相册](https://photo.baidu.com/)
- [x] 部署方便,开箱即用
- [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览
@ -86,4 +88,4 @@
---
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@QQGroup](https://jq.qq.com/?_wv=1027&k=OVPJcv2b)
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram](https://t.me/alist_chat) · [@QQ](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)

View File

@ -12,7 +12,6 @@ import (
)
func Init() bool {
//bootstrap.InitLog()
bootstrap.InitConf()
bootstrap.InitCron()
bootstrap.InitModel()

View File

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

View File

@ -7,6 +7,7 @@ import (
log "github.com/sirupsen/logrus"
"io/ioutil"
"os"
"path/filepath"
)
// InitConf init config
@ -46,7 +47,11 @@ func InitConf() {
if !conf.Conf.Force {
confFromEnv()
}
err := os.MkdirAll(conf.Conf.TempDir, 0700)
err := os.RemoveAll(filepath.Join(conf.Conf.TempDir))
if err != nil {
log.Errorln("failed delete temp file:", err)
}
err = os.MkdirAll(conf.Conf.TempDir, 0700)
if err != nil {
log.Fatalf("create temp dir error: %s", err.Error())
}

View File

@ -74,9 +74,9 @@ func InitModel() {
log.Infof("auto migrate model...")
if databaseConfig.Type == "mysql" {
err = conf.DB.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4").
AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{})
AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{}, &model.SearchFile{})
} else {
err = conf.DB.AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{})
err = conf.DB.AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{}, &model.SearchFile{})
}
if err != nil {
log.Fatalf("failed to auto migrate: %s", err.Error())

View File

@ -118,12 +118,19 @@ func InitSettings() {
Group: model.FRONT,
},
{
Key: "home readme url",
Description: "when have multiple, the readme file to show",
Key: "global readme url",
Description: "Default display when directory has no readme",
Type: "string",
Access: model.PUBLIC,
Group: model.FRONT,
},
{
Key: "pdf viewer url",
Type: "string",
Value: "https://alist-org.github.io/pdf.js/web/viewer.html?file=$url",
Access: model.PUBLIC,
Group: model.FRONT,
},
{
Key: "autoplay video",
Value: "false",
@ -252,12 +259,36 @@ func InitSettings() {
},
{
Key: "ocr api",
Value: "https://api.xhofe.top/ocr/file/json",
Value: "https://api.nn.ci/ocr/file/json",
Description: "Used to identify verification codes",
Type: "string",
Access: model.PRIVATE,
Group: model.BACK,
},
{
Key: "enable search",
Value: "false",
Type: "bool",
Access: model.PUBLIC,
Group: model.BACK,
Description: "Experimental function, not recommended as it's still under development",
},
{
Key: "Aria2 RPC url",
Value: "http://localhost:6800/jsonrpc",
Description: "Aria2 RPC url, e.g. 'http://aria2.example.com:6800/jsonrpc'",
Type: "string",
Access: model.PRIVATE,
Group: model.BACK,
},
{
Key: "Aria2 RPC secret",
Value: "",
Description: "Aria2 RPC secret, e.g. '123456'",
Type: "string",
Access: model.PRIVATE,
Group: model.BACK,
},
}
for i, _ := range settings {
v := settings[i]

View File

@ -51,6 +51,7 @@ BUILD() {
gitTag=$(git describe --long --tags --dirty --always)
webTag=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
echo "build version: $gitTag"
ldflags="\
-w -s \
-X 'github.com/Xhofe/alist/conf.BuiltAt=$builtAt' \
@ -60,7 +61,7 @@ BUILD() {
-X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \
-X 'github.com/Xhofe/alist/conf.WebTag=$webTag' \
"
rm -rf .git/
if [ "$1" == "release" ]; then
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
else
@ -96,7 +97,7 @@ BUILD_MUSL() {
gitTag=$(git describe --long --tags --dirty --always)
webTag=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
ldflags="\
-w -s \
-w -s --extldflags '-static -fpic' \
-X 'github.com/Xhofe/alist/conf.BuiltAt=$builtAt' \
-X 'github.com/Xhofe/alist/conf.GoVersion=$goVersion' \
-X 'github.com/Xhofe/alist/conf.GitAuthor=$gitAuthor' \
@ -148,8 +149,8 @@ elif [ "$1" = "docker" ]; then
elif [ "$1" = "build" ]; then
BUILD build
elif [ "$1" = "release" ]; then
BUILD release
BUILD_MUSL
BUILD release
RELEASE
else
echo -e "${RED_COLOR} Parameter error ${RES}"

View File

@ -40,7 +40,7 @@ var (
"go", "sh", "c", "cpp", "h", "hpp", "tsx", "vtt", "srt", "ass"}
DProxyTypes = []string{"m3u8"}
OfficeTypes = []string{"doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf"}
VideoTypes = []string{"mp4", "mkv", "avi", "mov", "rmvb", "webm", "flv"}
VideoTypes = []string{"mp4", "mkv", "avi", "mov", "rmvb", "webm", "flv", "m4v"}
AudioTypes = []string{"mp3", "flac", "ogg", "m4a", "wav", "opus"}
ImageTypes = []string{"jpg", "tiff", "jpeg", "png", "gif", "bmp", "svg", "ico", "swf", "webp"}
)
@ -85,6 +85,7 @@ var (
"Visitor WebDAV username", "Visitor WebDAV password",
"default page size", "load type",
"ocr api", "favicon",
"enable search",
}
)

View File

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

View File

@ -1,9 +1,18 @@
package _23
import (
"bytes"
"crypto/md5"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strconv"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
@ -13,12 +22,6 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strconv"
)
type Pan123 struct{}
@ -67,6 +70,12 @@ func (driver Pan123) Items() []base.Item {
Required: true,
Default: "asc",
},
{
Name: "bool_1",
Label: "stream upload",
Type: base.TypeBool,
Description: "io stream upload (test)",
},
}
}
@ -108,10 +117,10 @@ func (driver Pan123) File(path string, account *model.Account) (*model.File, err
func (driver Pan123) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
var rawFiles []Pan123File
var rawFiles []File
cache, err := base.GetCache(path, account)
if err == nil {
rawFiles, _ = cache.([]Pan123File)
rawFiles, _ = cache.([]File)
} else {
file, err := driver.File(path, account)
if err != nil {
@ -125,7 +134,7 @@ func (driver Pan123) Files(path string, account *model.Account) ([]model.File, e
_ = base.SetCache(path, rawFiles, account)
}
}
files := make([]model.File, 0)
files := make([]model.File, 0, len(rawFiles))
for _, file := range rawFiles {
files = append(files, *driver.FormatFile(&file))
}
@ -278,12 +287,13 @@ func (driver Pan123) Delete(path string, account *model.Account) error {
if err != nil {
return err
}
log.Debugln("delete 123 file: ", file)
data := base.Json{
"driveId": 0,
"operation": true,
"fileTrashInfoList": file,
"fileTrashInfoList": []File{*file},
}
_, err = driver.Request("https://www.123pan.com/api/file/trash",
_, err = driver.Request("https://www.123pan.com/b/api/file/trash",
base.Post, nil, nil, &data, nil, false, account)
return err
}
@ -299,46 +309,57 @@ func (driver Pan123) Upload(file *model.FileStream, account *model.Account) erro
if !parentFile.IsDir() {
return base.ErrNotFolder
}
parentFileId, _ := strconv.Atoi(parentFile.Id)
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
_, err = io.Copy(tempFile, file)
if err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
const DEFAULT int64 = 10485760
var uploadFile io.Reader
h := md5.New()
_, err = io.Copy(h, tempFile)
if err != nil {
return err
if account.Bool1 && file.GetSize() > uint64(DEFAULT) {
// 只计算前10MIB
buf := bytes.NewBuffer(make([]byte, 0, DEFAULT))
if n, err := io.CopyN(io.MultiWriter(buf, h), file, DEFAULT); err != io.EOF && n == 0 {
return err
}
// 增加额外参数防止MD5碰撞
h.Write([]byte(file.Name))
num := make([]byte, 8)
binary.BigEndian.PutUint64(num, file.Size)
h.Write(num)
// 拼装
uploadFile = io.MultiReader(buf, file)
} else {
// 计算完整文件MD5
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
if _, err = io.Copy(io.MultiWriter(tempFile, h), file); err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
uploadFile = tempFile
}
etag := hex.EncodeToString(h.Sum(nil))
log.Debugln("md5:", etag)
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return err
}
data := base.Json{
"driveId": 0,
"duplicate": true,
"duplicate": 2, // 2->覆盖 1->重命名 0->默认
"etag": etag,
"fileName": file.GetFileName(),
"parentFileId": parentFileId,
"parentFileId": parentFile.Id,
"size": file.GetSize(),
"type": 0,
}
var resp UploadResp
_, err = driver.Request("https://www.123pan.com/api/file/upload_request",
base.Post, nil, nil, &data, &resp, false, account)
base.Post, map[string]string{"app-version": "1.1"}, nil, &data, &resp, false, account)
//res, err := driver.Post("https://www.123pan.com/api/file/upload_request", data, account)
if err != nil {
return err
@ -360,7 +381,7 @@ func (driver Pan123) Upload(file *model.FileStream, account *model.Account) erro
input := &s3manager.UploadInput{
Bucket: &resp.Data.Bucket,
Key: &resp.Data.Key,
Body: tempFile,
Body: uploadFile,
}
_, err = uploader.Upload(input)
if err != nil {

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

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

View File

@ -448,6 +448,7 @@ func (driver Cloud139) Upload(file *model.FileStream, account *model.Account) er
return err
}
log.Debugf("%+v", res)
res.Body.Close()
start += byteSize
}
return nil

View File

@ -17,8 +17,6 @@ import (
"io"
"math"
"net/http"
"net/http/cookiejar"
"path/filepath"
"regexp"
"strconv"
"strings"
@ -60,11 +58,9 @@ func (driver Cloud189) FormatFile(file *Cloud189File) *model.File {
f.UpdatedAt = &lastOpTime
}
if file.Size == -1 {
f.Type = conf.FOLDER
f.Size = 0
} else {
f.Type = utils.GetFileType(filepath.Ext(file.Name))
}
f.Type = file.GetType()
return f
}
@ -97,24 +93,19 @@ type LoginResp struct {
// Login refer to PanIndex
func (driver Cloud189) Login(account *model.Account) error {
client, ok := client189Map[account.Name]
if !ok {
//cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
client = resty.New()
//client.SetCookieJar(cookieJar)
client.SetTimeout(base.DefaultTimeout)
client.SetRetryCount(3)
client.SetHeader("Referer", "https://cloud.189.cn/")
}
// clear cookie
jar, _ := cookiejar.New(nil)
client.SetCookieJar(jar)
client := resty.New()
//client.SetCookieJar(cookieJar)
client.SetTimeout(base.DefaultTimeout)
client.SetRetryCount(3)
client.SetHeader("Referer", "https://cloud.189.cn/")
url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
b := ""
lt := ""
ltText := regexp.MustCompile(`lt = "(.+?)"`)
var res *resty.Response
var err error
for i := 0; i < 3; i++ {
res, err := client.R().Get(url)
res, err = client.R().Get(url)
if err != nil {
return err
}
@ -132,7 +123,8 @@ func (driver Cloud189) Login(account *model.Account) error {
}
}
if lt == "" {
return fmt.Errorf("get empty login page")
return fmt.Errorf("get page: %s \nstatus: %d \nrequest url: %s\nredirect url: %s",
b, res.StatusCode(), res.RawResponse.Request.URL.String(), res.Header().Get("location"))
}
captchaToken := regexp.MustCompile(`captchaToken' value='(.+?)'`).FindStringSubmatch(b)[1]
returnUrl := regexp.MustCompile(`returnUrl = '(.+?)'`).FindStringSubmatch(b)[1]
@ -172,7 +164,7 @@ func (driver Cloud189) Login(account *model.Account) error {
passwordRsa := RsaEncode([]byte(account.Password), jRsakey, true)
url = "https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do"
var loginResp LoginResp
res, err := client.R().
res, err = client.R().
SetHeaders(map[string]string{
"lt": lt,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
@ -578,6 +570,7 @@ func (driver Cloud189) NewUpload(file *model.FileStream, account *model.Account)
r, err := base.HttpClient.Do(req)
log.Debugf("%+v %+v", r, r.Request.Header)
r.Body.Close()
if err != nil {
return err
}

View File

@ -187,7 +187,7 @@ func (driver Cloud189) Link(args base.Args, account *model.Account) (*base.Link,
link := base.Link{
Headers: []base.Header{
{Name: "User-Agent", Value: base.UserAgent},
{Name: "Authorization", Value: ""},
//{Name: "Authorization", Value: ""},
},
}
if res.StatusCode() == 302 {

View File

@ -1,5 +1,11 @@
package _89
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/utils"
"path"
)
type Cloud189Error struct {
ErrorCode string `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
@ -17,6 +23,24 @@ type Cloud189File struct {
Url string `json:"url"`
}
func (f Cloud189File) GetSize() uint64 {
if f.Size == -1 {
return 0
}
return uint64(f.Size)
}
func (f Cloud189File) GetName() string {
return f.Name
}
func (f Cloud189File) GetType() int {
if f.Size == -1 {
return conf.FOLDER
}
return utils.GetFileType(path.Ext(f.Name))
}
type Cloud189Folder struct {
Id int64 `json:"id"`
LastOpTime string `json:"lastOpTime"`

View File

@ -34,7 +34,7 @@ func GetState(account *model.Account) *State {
SetHeaders(map[string]string{
"Accept": "application/json;charset=UTF-8",
"User-Agent": base.UserAgent,
}),
}).SetTimeout(base.DefaultTimeout),
}
userStateCache.States[account.Username] = state
return state
@ -198,7 +198,7 @@ func (s *State) refreshSession(account *model.Account) error {
"accessToken": s.AccessToken,
}).
SetHeader("X-Request-ID", uuid.NewString()).
Get("https://api.cloud.189.cn/getSessionForPC.action")
Get(API_URL + "/getSessionForPC.action")
if err != nil {
return err
}
@ -223,10 +223,8 @@ func (s *State) refreshSession(account *model.Account) error {
return nil
}
func (s *State) IsLogin() bool {
_, err := s.Request("GET", API_URL+"/getUserInfo.action", nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
}, nil)
func (s *State) IsLogin(account *model.Account) bool {
_, err := s.Request(http.MethodGet, API_URL+"/getUserInfo.action", nil, func(r *resty.Request) { r.SetQueryParams(clientSuffix()) }, account)
return err == nil
}
@ -242,12 +240,12 @@ func (s *State) RefreshSession(account *model.Account) error {
return s.refreshSession(account)
}
func (s *State) Request(method string, fullUrl string, params url.Values, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
func (s *State) Request(method string, fullUrl string, params Params, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
s.Lock()
dateOfGmt := getHttpDateStr()
sessionKey := s.SessionKey
sessionSecret := s.SessionSecret
if account != nil && isFamily(account) {
if isFamily(account) {
sessionKey = s.FamilySessionKey
sessionSecret = s.FamilySessionSecret
}
@ -267,25 +265,12 @@ func (s *State) Request(method string, fullUrl string, params url.Values, callba
}
req.SetHeader("Signature", signatureOfHmac(sessionSecret, sessionKey, method, fullUrl, dateOfGmt, paramsData))
callback(req)
if callback != nil {
callback(req)
}
s.Unlock()
var err error
var res *resty.Response
switch method {
case "GET":
res, err = req.Get(fullUrl)
case "POST":
res, err = req.Post(fullUrl)
case "DELETE":
res, err = req.Delete(fullUrl)
case "PATCH":
res, err = req.Patch(fullUrl)
case "PUT":
res, err = req.Put(fullUrl)
default:
return nil, base.ErrNotSupport
}
res, err := req.Execute(method, fullUrl)
if err != nil {
return nil, err
}
@ -298,33 +283,36 @@ func (s *State) Request(method string, fullUrl string, params url.Values, callba
}
if erron.Code != "" && erron.Code != "SUCCESS" {
if erron.Msg == "" {
if erron.Message == "" {
return nil, fmt.Errorf(res.String())
}
return nil, fmt.Errorf(erron.Message)
}
return nil, fmt.Errorf(erron.Msg)
}
if erron.ErrorCode != "" {
return nil, fmt.Errorf(erron.ErrorMsg)
}
if account != nil {
switch utils.Json.Get(res.Body(), "res_code").ToInt64() {
case 11, 18:
switch erron.ErrorCode {
case "InvalidSessionKey":
if err := s.RefreshSession(account); err != nil {
return nil, err
}
return s.Request(method, fullUrl, params, callback, account)
case 0:
if res.StatusCode() == http.StatusOK {
return res, nil
}
fallthrough
default:
return nil, fmt.Errorf(res.String())
}
return nil, fmt.Errorf(erron.ErrorMsg)
}
if utils.Json.Get(res.Body(), "res_code").ToInt64() != 0 {
return res, fmt.Errorf(utils.Json.Get(res.Body(), "res_message").ToString())
switch utils.Json.Get(res.Body(), "res_code").ToInt64() {
case 11, 18:
if err := s.RefreshSession(account); err != nil {
return nil, err
}
return s.Request(method, fullUrl, params, callback, account)
case 0:
if res.StatusCode() == http.StatusOK {
return res, nil
}
return nil, fmt.Errorf(res.String())
default:
return nil, fmt.Errorf(utils.Json.Get(res.Body(), "res_message").ToString())
}
return res, nil
}

View File

@ -1,16 +1,19 @@
package _189
import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
@ -50,10 +53,9 @@ func (driver Cloud189) Items() []base.Item {
Description: "account password",
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
Required: true,
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
},
{
Name: "internal_type",
@ -63,10 +65,9 @@ func (driver Cloud189) Items() []base.Item {
Values: "Personal,Family",
},
{
Name: "site_id",
Label: "family id",
Type: base.TypeString,
Required: true,
Name: "site_id",
Label: "family id",
Type: base.TypeString,
},
{
Name: "order_by",
@ -82,6 +83,11 @@ func (driver Cloud189) Items() []base.Item {
Values: "true,false",
Required: true,
},
{
Name: "bool_1",
Label: "fast upload",
Type: base.TypeBool,
},
}
}
@ -92,10 +98,14 @@ func (driver Cloud189) Save(account *model.Account, old *model.Account) error {
if !isFamily(account) && account.RootFolder == "" {
account.RootFolder = "-11"
account.SiteId = ""
}
if isFamily(account) && account.RootFolder == "-11" {
account.RootFolder = ""
}
state := GetState(account)
if !state.IsLogin() {
if !state.IsLogin(account) {
if err := state.Login(account); err != nil {
return err
}
@ -121,7 +131,7 @@ func (driver Cloud189) Save(account *model.Account, old *model.Account) error {
func (driver Cloud189) getFamilyInfoList(account *model.Account) ([]FamilyInfoResp, error) {
var resp FamilyInfoListResp
_, err := GetState(account).Request("GET", API_URL+"/family/manage/getFamilyList.action", nil, func(r *resty.Request) {
_, err := GetState(account).Request(http.MethodGet, API_URL+"/family/manage/getFamilyList.action", nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetResult(&resp)
}, account)
@ -179,16 +189,16 @@ func (driver Cloud189) Files(path string, account *model.Account) ([]model.File,
client := GetState(account)
for pageNum := 1; ; pageNum++ {
var resp Cloud189FilesResp
queryparam := map[string]string{
"folderId": file.Id,
"fileType": "0",
"mediaAttr": "0",
"iconOption": "5",
"pageNum": fmt.Sprint(pageNum),
"pageSize": "130",
}
_, err = client.Request("GET", fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix()).SetQueryParams(queryparam)
_, err = client.Request(http.MethodGet, fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix()).
SetQueryParams(map[string]string{
"folderId": file.Id,
"fileType": "0",
"mediaAttr": "0",
"iconOption": "5",
"pageNum": fmt.Sprint(pageNum),
"pageSize": "130",
})
if isFamily(account) {
r.SetQueryParams(map[string]string{
"familyId": account.SiteId,
@ -212,10 +222,6 @@ func (driver Cloud189) Files(path string, account *model.Account) ([]model.File,
break
}
mustTime := func(str string) *time.Time {
time, _ := http.ParseTime(str)
return &time
}
for _, folder := range resp.FileListAO.FolderList {
files = append(files, model.File{
Id: fmt.Sprint(folder.ID),
@ -223,7 +229,7 @@ func (driver Cloud189) Files(path string, account *model.Account) ([]model.File,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: mustTime(folder.CreateDate),
UpdatedAt: MustParseTime(folder.LastOpTime),
})
}
for _, file := range resp.FileListAO.FileList {
@ -233,7 +239,7 @@ func (driver Cloud189) Files(path string, account *model.Account) ([]model.File,
Size: file.Size,
Type: utils.GetFileType(filepath.Ext(file.Name)),
Driver: driver.Config().Name,
UpdatedAt: mustTime(file.CreateDate),
UpdatedAt: MustParseTime(file.LastOpTime),
Thumbnail: file.Icon.SmallUrl,
})
}
@ -279,7 +285,7 @@ func (driver Cloud189) Link(args base.Args, account *model.Account) (*base.Link,
var downloadUrl struct {
URL string `json:"fileDownloadUrl"`
}
_, err = GetState(account).Request("GET", fullUrl, nil, func(r *resty.Request) {
_, err = GetState(account).Request(http.MethodGet, fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix()).SetQueryParam("fileId", file.Id)
if isFamily(account) {
r.SetQueryParams(map[string]string{
@ -324,7 +330,7 @@ func (driver Cloud189) MakeDir(path string, account *model.Account) error {
}
fullUrl += "/createFolder.action"
_, err = GetState(account).Request("POST", fullUrl, nil, func(r *resty.Request) {
_, err = GetState(account).Request(http.MethodPost, fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix()).SetQueryParams(map[string]string{
"folderName": name,
"relativePath": "",
@ -354,7 +360,7 @@ func (driver Cloud189) Move(src string, dst string, account *model.Account) erro
return err
}
_, err = GetState(account).Request("POST", API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
_, err = GetState(account).Request(http.MethodPost, API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
"type": "MOVE",
"taskInfos": string(MustToBytes(utils.Json.Marshal(
@ -390,10 +396,10 @@ func (driver Cloud189) Move(src string, dst string, account *model.Account) erro
var queryParam map[string]string
fullUrl := API_URL
method := "POST"
method := http.MethodPost
if isFamily(account) {
fullUrl += "/family/file"
method = "GET"
method = http.MethodGet
}
if srcFile.IsDir() {
fullUrl += "/moveFolder.action"
@ -431,10 +437,10 @@ func (driver Cloud189) Rename(src string, dst string, account *model.Account) er
var queryParam map[string]string
fullUrl := API_URL
method := "POST"
method := http.MethodPost
if isFamily(account) {
fullUrl += "/family/file"
method = "GET"
method = http.MethodGet
}
if srcFile.IsDir() {
fullUrl += "/renameFolder.action"
@ -470,7 +476,7 @@ func (driver Cloud189) Copy(src string, dst string, account *model.Account) erro
return err
}
_, err = GetState(account).Request("POST", API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
_, err = GetState(account).Request(http.MethodPost, API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
"type": "COPY",
"taskInfos": string(MustToBytes(utils.Json.Marshal(
@ -500,7 +506,7 @@ func (driver Cloud189) Delete(path string, account *model.Account) error {
return err
}
_, err = GetState(account).Request("POST", API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
_, err = GetState(account).Request(http.MethodPost, API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
"type": "DELETE",
"taskInfos": string(MustToBytes(utils.Json.Marshal(
@ -535,20 +541,226 @@ func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) er
return base.ErrNotFolder
}
if isFamily(account) {
return driver.uploadFamily(file, parentFile, account)
if account.Bool1 {
return driver.FastUpload(file, parentFile, account)
}
return driver.uploadPerson(file, parentFile, account)
return driver.CommonUpload(file, parentFile, account)
/*
if isFamily(account) {
return driver.uploadFamily(file, parentFile, account)
}
return driver.uploadPerson(file, parentFile, account)
*/
}
func (driver Cloud189) CommonUpload(file *model.FileStream, parentFile *model.File, account *model.Account) error {
// 初始化上传
state := GetState(account)
const DEFAULT int64 = 10485760
count := int(math.Ceil(float64(file.Size) / float64(DEFAULT)))
params := Params{
"parentFolderId": parentFile.Id,
"fileName": url.PathEscape(file.Name),
"fileSize": fmt.Sprint(file.Size),
"sliceSize": fmt.Sprint(DEFAULT),
"lazyCheck": "1",
}
fullUrl := UPLOAD_URL
if isFamily(account) {
params.Set("familyId", account.SiteId)
fullUrl += "/family"
} else {
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
fullUrl += "/person"
}
var initMultiUpload InitMultiUploadResp
_, err := state.Request(http.MethodGet, fullUrl+"/initMultiUpload", params, func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&initMultiUpload) }, account)
if err != nil {
return err
}
fileMd5 := md5.New()
silceMd5 := md5.New()
silceMd5Hexs := make([]string, 0, count)
byteData := bytes.NewBuffer(make([]byte, DEFAULT))
for i := 1; i <= count; i++ {
byteData.Reset()
silceMd5.Reset()
if n, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5, byteData), file, DEFAULT); err != io.EOF && n == 0 {
return err
}
md5Bytes := silceMd5.Sum(nil)
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Bytes)))
silceMd5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)
var uploadUrl UploadUrlsResp
_, err = state.Request(http.MethodGet, fullUrl+"/getMultiUploadUrls",
Params{"partInfo": fmt.Sprintf("%d-%s", i, silceMd5Base64), "uploadFileId": initMultiUpload.Data.UploadFileID},
func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&uploadUrl) },
account)
if err != nil {
return err
}
uploadData := uploadUrl.UploadUrls[fmt.Sprint("partNumber_", i)]
req, _ := http.NewRequest(http.MethodPut, uploadData.RequestURL, byteData)
req.Header.Set("User-Agent", "")
for k, v := range ParseHttpHeader(uploadData.RequestHeader) {
req.Header.Set(k, v)
}
for k, v := range clientSuffix() {
req.URL.RawQuery += fmt.Sprintf("&%s=%s", k, v)
}
r, err := base.HttpClient.Do(req)
if err != nil {
return err
}
if r.StatusCode != http.StatusOK {
data, _ := io.ReadAll(r.Body)
r.Body.Close()
return fmt.Errorf(string(data))
}
r.Body.Close()
}
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
sliceMd5Hex := fileMd5Hex
if int64(file.Size) > DEFAULT {
sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n")))
}
_, err = state.Request(http.MethodGet, fullUrl+"/commitMultiUploadFile",
Params{
"uploadFileId": initMultiUpload.Data.UploadFileID,
"fileMd5": fileMd5Hex,
"sliceMd5": sliceMd5Hex,
"lazyCheck": "1",
"isLog": "0",
"opertype": "3",
},
func(r *resty.Request) { r.SetQueryParams(clientSuffix()) }, account)
return err
}
func (driver Cloud189) FastUpload(file *model.FileStream, parentFile *model.File, account *model.Account) error {
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
// 初始化上传
state := GetState(account)
const DEFAULT int64 = 10485760
count := int(math.Ceil(float64(file.Size) / float64(DEFAULT)))
// 优先计算所需信息
fileMd5 := md5.New()
silceMd5 := md5.New()
silceMd5Hexs := make([]string, 0, count)
silceMd5Base64s := make([]string, 0, count)
for i := 1; i <= count; i++ {
silceMd5.Reset()
if n, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5, tempFile), file, DEFAULT); err != nil && n == 0 {
return err
}
md5Byte := silceMd5.Sum(nil)
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte)))
silceMd5Base64s = append(silceMd5Base64s, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte)))
}
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
sliceMd5Hex := fileMd5Hex
if int64(file.Size) > DEFAULT {
sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n")))
}
params := Params{
"parentFolderId": parentFile.Id,
"fileName": url.PathEscape(file.Name),
"fileSize": fmt.Sprint(file.Size),
"fileMd5": fileMd5Hex,
"sliceSize": fmt.Sprint(DEFAULT),
"sliceMd5": sliceMd5Hex,
}
fullUrl := UPLOAD_URL
if isFamily(account) {
params.Set("familyId", account.SiteId)
fullUrl += "/family"
} else {
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
fullUrl += "/person"
}
var uploadInfo InitMultiUploadResp
_, err = state.Request(http.MethodGet, fullUrl+"/initMultiUpload", params, func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&uploadInfo) }, account)
if err != nil {
return err
}
if uploadInfo.Data.FileDataExists != 1 {
var uploadUrls UploadUrlsResp
_, err := state.Request(http.MethodGet, fullUrl+"/getMultiUploadUrls",
Params{
"uploadFileId": uploadInfo.Data.UploadFileID,
"partInfo": strings.Join(silceMd5Base64s, ","),
},
func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&uploadUrls) },
account)
if err != nil {
return err
}
for i := 1; i <= count; i++ {
uploadData := uploadUrls.UploadUrls[fmt.Sprint("partNumber_", i)]
req, _ := http.NewRequest(http.MethodPut, uploadData.RequestURL, io.NewSectionReader(tempFile, int64(i-1)*DEFAULT, DEFAULT))
req.Header.Set("User-Agent", "")
for k, v := range ParseHttpHeader(uploadData.RequestHeader) {
req.Header.Set(k, v)
}
for k, v := range clientSuffix() {
req.URL.RawQuery += fmt.Sprintf("&%s=%s", k, v)
}
r, err := base.HttpClient.Do(req)
if err != nil {
return err
}
if r.StatusCode != http.StatusOK {
data, _ := io.ReadAll(r.Body)
r.Body.Close()
return fmt.Errorf(string(data))
}
r.Body.Close()
}
}
_, err = state.Request(http.MethodGet, fullUrl+"/commitMultiUploadFile",
Params{
"uploadFileId": uploadInfo.Data.UploadFileID,
"isLog": "0",
"opertype": "3",
},
func(r *resty.Request) { r.SetQueryParams(clientSuffix()) },
account)
return err
}
/*
func (driver Cloud189) uploadFamily(file *model.FileStream, parentFile *model.File, account *model.Account) error {
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
fileMd5 := md5.New()
if _, err = io.Copy(io.MultiWriter(fileMd5, tempFile), file); err != nil {
@ -557,7 +769,7 @@ func (driver Cloud189) uploadFamily(file *model.FileStream, parentFile *model.Fi
client := GetState(account)
var createUpload CreateUploadFileResult
_, err = client.Request("GET", API_URL+"/family/file/createFamilyFile.action", nil, func(r *resty.Request) {
_, err = client.Request(http.MethodGet, API_URL+"/family/file/createFamilyFile.action", nil, func(r *resty.Request) {
r.SetQueryParams(map[string]string{
"fileMd5": hex.EncodeToString(fileMd5.Sum(nil)),
"fileName": file.Name,
@ -579,7 +791,7 @@ func (driver Cloud189) uploadFamily(file *model.FileStream, parentFile *model.Fi
}
}
_, err = client.Request("GET", createUpload.FileCommitUrl, nil, func(r *resty.Request) {
_, err = client.Request(http.MethodGet, createUpload.FileCommitUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetHeaders(map[string]string{
"FamilyId": account.SiteId,
@ -596,8 +808,10 @@ func (driver Cloud189) uploadPerson(file *model.FileStream, parentFile *model.Fi
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
fileMd5 := md5.New()
if _, err = io.Copy(io.MultiWriter(fileMd5, tempFile), file); err != nil {
@ -606,7 +820,7 @@ func (driver Cloud189) uploadPerson(file *model.FileStream, parentFile *model.Fi
client := GetState(account)
var createUpload CreateUploadFileResult
_, err = client.Request("POST", API_URL+"/createUploadFile.action", nil, func(r *resty.Request) {
_, err = client.Request(http.MethodPost, API_URL+"/createUploadFile.action", nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
"parentFolderId": parentFile.Id,
@ -634,7 +848,7 @@ func (driver Cloud189) uploadPerson(file *model.FileStream, parentFile *model.Fi
}
}
_, err = client.Request("POST", createUpload.FileCommitUrl, nil, func(r *resty.Request) {
_, err = client.Request(http.MethodPost, createUpload.FileCommitUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetFormData(map[string]string{
"uploadFileId": fmt.Sprint(createUpload.UploadFileId),
@ -689,7 +903,7 @@ func (driver Cloud189) getUploadFileState(uploadFileId int64, account *model.Acc
fullUrl += "/getUploadFileStatus.action"
}
var uploadFileState UploadFileStatusResult
_, err := GetState(account).Request("GET", fullUrl, nil, func(r *resty.Request) {
_, err := GetState(account).Request(http.MethodGet, fullUrl, nil, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetQueryParams(map[string]string{
"uploadFileId": fmt.Sprint(uploadFileId),
@ -704,128 +918,6 @@ func (driver Cloud189) getUploadFileState(uploadFileId int64, account *model.Acc
return nil, err
}
return &uploadFileState, nil
}
}*/
/*
暂时未解决
func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) error {
if file == nil {
return base.ErrEmptyFile
}
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
}
if !parentFile.IsDir() {
return base.ErrNotFolder
}
fullUrl := UPLOAD_URL
if isFamily(account) {
fullUrl += "/family"
} else {
fullUrl += "/person"
}
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
// 初始化上传
const DEFAULT int64 = 10485760
count := int64(math.Ceil(float64(file.Size) / float64(DEFAULT)))
fileMd5 := md5.New()
silceMd5 := md5.New()
silceMd5Hexs := make([]string, 0, count)
silceMd5Base64s := make([]string, 0, count)
for i := int64(1); i <= count; i++ {
if _, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5, tempFile), file, DEFAULT); err != io.EOF {
return err
}
md5Byte := silceMd5.Sum(nil)
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte)))
silceMd5Base64s = append(silceMd5Base64s, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte)))
}
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
sliceMd5Hex := fileMd5Hex
if int64(file.Size) > DEFAULT {
sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n")))
}
qID := uuid.NewString()
client := GetState(account)
param := MapToUrlValues(map[string]interface{}{
"parentFolderId": parentFile.Id,
"fileName": url.QueryEscape(file.Name),
"fileMd5": fileMd5Hex,
"fileSize": fmt.Sprint(file.Size),
"sliceMd5": sliceMd5Hex,
"sliceSize": fmt.Sprint(DEFAULT),
})
if isFamily(account) {
param.Set("familyId", account.SiteId)
}
var uploadInfo InitMultiUploadResp
_, err = client.Request("GET", fullUrl+"/initMultiUpload", param, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetHeader("X-Request-ID", qID)
r.SetResult(&uploadInfo)
}, account)
if err != nil {
return err
}
if uploadInfo.Data.FileDataExists != 1 {
param = MapToUrlValues(map[string]interface{}{
"uploadFileId": uploadInfo.Data.UploadFileID,
"partInfo": strings.Join(silceMd5Base64s, ","),
})
if isFamily(account) {
param.Set("familyId", account.SiteId)
}
var uploadUrls UploadUrlsResp
_, err := client.Request("GET", fullUrl+"/getMultiUploadUrls", param, func(r *resty.Request) {
r.SetQueryParams(clientSuffix())
r.SetHeader("X-Request-ID", qID).SetHeader("content-type", "application/x-www-form-urlencoded")
r.SetResult(&uploadUrls)
}, account)
if err != nil {
return err
}
var i int64
for _, uploadurl := range uploadUrls.UploadUrls {
req := resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).SetProxy("http://192.168.0.30:8888").R()
for _, header := range strings.Split(decodeURIComponent(uploadurl.RequestHeader), "&") {
i := strings.Index(header, "=")
req.SetHeader(header[0:i], header[i+1:])
}
_, err := req.SetBody(io.NewSectionReader(tempFile, i*DEFAULT, DEFAULT)).Put(uploadurl.RequestURL)
if err != nil {
return err
}
}
}
param = MapToUrlValues(map[string]interface{}{
"uploadFileId": uploadInfo.Data.UploadFileID,
"isLog": "0",
"opertype": "1",
})
if isFamily(account) {
param.Set("familyId", account.SiteId)
}
_, err = client.Request("GET", fullUrl+"/commitMultiUploadFile", param, func(r *resty.Request) {
r.SetHeader("X-Request-ID", qID)
r.SetQueryParams(clientSuffix())
}, account)
return err
}
*/
var _ base.Driver = (*Cloud189)(nil)

View File

@ -137,6 +137,7 @@ type BatchTaskInfo struct {
//SrcParentId string `json:"srcParentId"`
}
/*
type CreateUploadFileResult struct {
// UploadFileId 上传文件请求ID
UploadFileId int64 `json:"uploadFileId"`
@ -157,8 +158,8 @@ type UploadFileStatusResult struct {
FileCommitUrl string `json:"fileCommitUrl"`
FileDataExists int `json:"fileDataExists"`
}
*/
/*
type InitMultiUploadResp struct {
//Code string `json:"code"`
Data struct {
@ -177,4 +178,3 @@ type Part struct {
RequestURL string `json:"requestURL"`
RequestHeader string `json:"requestHeader"`
}
*/

View File

@ -15,6 +15,7 @@ import (
rand2 "math/rand"
"net/http"
"net/url"
"sort"
"strings"
"time"
@ -118,19 +119,17 @@ func toFamilyOrderBy(o string) string {
}
}
func MapToUrlValues(m map[string]interface{}) url.Values {
url := make(url.Values, len(m))
for k, v := range m {
url.Add(k, fmt.Sprint(v))
func ParseHttpHeader(str string) map[string]string {
header := make(map[string]string)
for _, value := range strings.Split(str, "&") {
i := strings.Index(value, "=")
header[strings.TrimSpace(value[0:i])] = strings.TrimSpace(value[i+1:])
}
return url
return header
}
func decodeURIComponent(str string) string {
r, _ := url.QueryUnescape(str)
//r, _ := url.PathUnescape(str)
//r = strings.ReplaceAll(r, " ", "+")
return r
func MustString(str string, err error) string {
return str
}
func MustToBytes(b []byte, err error) []byte {
@ -143,3 +142,36 @@ func BoolToNumber(b bool) int {
}
return 0
}
func MustParseTime(str string) *time.Time {
loc, _ := time.LoadLocation("Local")
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", str, loc)
return &lastOpTime
}
type Params map[string]string
func (p Params) Set(k, v string) {
p[k] = v
}
func (p Params) Encode() string {
if p == nil {
return ""
}
var buf strings.Builder
keys := make([]string, 0, len(p))
for k := range p {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
buf.WriteString(p[k])
}
return buf.String()
}

View File

@ -3,7 +3,6 @@ package alidrive
import (
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
@ -11,36 +10,10 @@ import (
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"path/filepath"
"time"
)
var aliClient = resty.New()
type AliRespError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type AliFiles struct {
Items []AliFile `json:"items"`
NextMarker string `json:"next_marker"`
}
type AliFile struct {
DriveId string `json:"drive_id"`
CreatedAt *time.Time `json:"created_at"`
FileExtension string `json:"file_extension"`
FileId string `json:"file_id"`
Type string `json:"type"`
Name string `json:"name"`
Category string `json:"category"`
ParentFileId string `json:"parent_file_id"`
UpdatedAt *time.Time `json:"updated_at"`
Size int64 `json:"size"`
Thumbnail string `json:"thumbnail"`
Url string `json:"url"`
}
func (driver AliDrive) FormatFile(file *AliFile) *model.File {
f := &model.File{
Id: file.FileId,
@ -51,17 +24,7 @@ func (driver AliDrive) FormatFile(file *AliFile) *model.File {
Driver: driver.Config().Name,
Url: file.Url,
}
if file.Type == "folder" {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(file.FileExtension)
}
if file.Category == "video" {
f.Type = conf.VIDEO
}
if file.Category == "image" {
f.Type = conf.IMAGE
}
f.Type = file.GetType()
return f
}

View File

@ -66,6 +66,11 @@ func (driver AliDrive) Items() []base.Item {
Required: false,
Description: ">0 and <=200",
},
{
Name: "bool_1",
Label: "fast upload",
Type: base.TypeBool,
},
}
}
@ -371,7 +376,7 @@ func (driver AliDrive) Delete(path string, account *model.Account) error {
}
return fmt.Errorf("%s", e.Message)
}
if res.StatusCode() == 204 {
if res.StatusCode() < 400 {
return nil
}
return errors.New(res.String())
@ -391,8 +396,7 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
if file == nil {
return base.ErrEmptyFile
}
const DEFAULT int64 = 10485760
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
@ -401,16 +405,14 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
return base.ErrNotFolder
}
const DEFAULT int64 = 10485760
var count = int(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
partInfoList := make([]base.Json, 0, count)
var i int64
for i = 0; i < count; i++ {
partInfoList = append(partInfoList, base.Json{
"part_number": i + 1,
})
for i := 1; i <= count; i++ {
partInfoList = append(partInfoList, base.Json{"part_number": i})
}
buf := make([]byte, 1024)
n, _ := file.Read(buf[:])
reqBody := base.Json{
"check_name_mode": "overwrite",
"drive_id": account.DriveId,
@ -419,9 +421,24 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
"part_info_list": partInfoList,
"size": file.GetSize(),
"type": "file",
"pre_hash": utils.GetSHA1Encode(string(buf[:n])),
}
fileReader := io.MultiReader(bytes.NewReader(buf[:n]), file.File)
if account.Bool1 {
buf := bytes.NewBuffer(make([]byte, 0, 1024))
io.CopyN(buf, file, 1024)
reqBody["pre_hash"] = utils.GetSHA1Encode(buf.String())
// 把头部拼接回去
file.File = struct {
io.Reader
io.Closer
}{
Reader: io.MultiReader(buf, file.File),
Closer: file.File,
}
} else {
reqBody["content_hash_name"] = "none"
reqBody["proof_version"] = "v1"
}
var resp UploadResp
var e AliRespError
@ -444,18 +461,20 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
return fmt.Errorf("%s", e.Message)
}
if e.Code == "PreHashMatched" {
if account.Bool1 && e.Code == "PreHashMatched" {
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
delete(reqBody, "pre_hash")
h := sha1.New()
if _, err = io.Copy(tempFile, io.TeeReader(fileReader, h)); err != nil {
if _, err = io.Copy(io.MultiWriter(tempFile, h), file.File); err != nil {
return err
}
reqBody["content_hash"] = hex.EncodeToString(h.Sum(nil))
@ -470,10 +489,11 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
o = i ? r.mod(i) : new gt.BigNumber(0);
(t.file.slice(o.toNumber(), Math.min(o.plus(8).toNumber(), t.file.size)))
*/
buf := make([]byte, 8)
r, _ := new(big.Int).SetString(utils.GetMD5Encode(account.AccessToken)[:16], 16)
i := new(big.Int).SetUint64(file.Size)
o := r.Mod(r, i)
n, _ = io.NewSectionReader(tempFile, o.Int64(), 8).Read(buf[:8])
n, _ := io.NewSectionReader(tempFile, o.Int64(), 8).Read(buf[:8])
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
_, err = client.Post("https://api.aliyundrive.com/adrive/v2/file/createWithFolders")
@ -488,14 +508,15 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
return nil
}
// 秒传失败
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return err
}
fileReader = tempFile
file.File = tempFile
}
for i = 0; i < count; i++ {
req, err := http.NewRequest("PUT", resp.PartInfoList[i].UploadUrl, io.LimitReader(fileReader, DEFAULT))
for _, partInfo := range resp.PartInfoList {
req, err := http.NewRequest("PUT", partInfo.UploadUrl, io.LimitReader(file.File, DEFAULT))
if err != nil {
return err
}
@ -504,6 +525,7 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
return err
}
log.Debugf("%+v", res)
res.Body.Close()
//res, err := base.BaseClient.R().
// SetHeader("Content-Type","").
// SetBody(byteData).Put(resp.PartInfoList[i].UploadUrl)
@ -523,7 +545,7 @@ func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) er
if err != nil {
return err
}
if e.Code != "" {
if e.Code != "" && e.Code != "PreHashMatched" {
//if e.Code == "AccessTokenInvalid" {
// err = driver.RefreshToken(account)
// if err != nil {

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

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

View File

@ -18,12 +18,14 @@ import (
_ "github.com/Xhofe/alist/drivers/pikpak"
_ "github.com/Xhofe/alist/drivers/quark"
_ "github.com/Xhofe/alist/drivers/s3"
_ "github.com/Xhofe/alist/drivers/sftp"
_ "github.com/Xhofe/alist/drivers/shandian"
_ "github.com/Xhofe/alist/drivers/teambition"
_ "github.com/Xhofe/alist/drivers/uss"
_ "github.com/Xhofe/alist/drivers/webdav"
_ "github.com/Xhofe/alist/drivers/xunlei"
_ "github.com/Xhofe/alist/drivers/yandex"
_ "github.com/Xhofe/alist/drivers/baiduphoto"
log "github.com/sirupsen/logrus"
"strings"
)

View File

@ -48,8 +48,7 @@ func (driver Baidu) refreshToken(account *model.Account) error {
return nil
}
func (driver Baidu) Request(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
u := "https://pan.baidu.com/rest/2.0" + pathname
func (driver Baidu) Request(fullurl string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
req := base.RestyClient.R()
req.SetQueryParam("access_token", account.AccessToken)
if headers != nil {
@ -71,15 +70,15 @@ func (driver Baidu) Request(pathname string, method int, headers, query, form ma
var err error
switch method {
case base.Get:
res, err = req.Get(u)
res, err = req.Get(fullurl)
case base.Post:
res, err = req.Post(u)
res, err = req.Post(fullurl)
case base.Patch:
res, err = req.Patch(u)
res, err = req.Patch(fullurl)
case base.Delete:
res, err = req.Delete(u)
res, err = req.Delete(fullurl)
case base.Put:
res, err = req.Put(u)
res, err = req.Put(fullurl)
default:
return nil, base.ErrNotSupport
}
@ -94,7 +93,7 @@ func (driver Baidu) Request(pathname string, method int, headers, query, form ma
if err != nil {
return nil, err
}
return driver.Request(pathname, method, headers, query, form, data, resp, account)
return driver.Request(fullurl, method, headers, query, form, data, resp, account)
}
return nil, fmt.Errorf("errno: %d, refer to https://pan.baidu.com/union/doc/", errno)
}
@ -102,11 +101,11 @@ func (driver Baidu) Request(pathname string, method int, headers, query, form ma
}
func (driver Baidu) Get(pathname string, params map[string]string, resp interface{}, account *model.Account) ([]byte, error) {
return driver.Request(pathname, base.Get, nil, params, nil, nil, resp, account)
return driver.Request("https://pan.baidu.com/rest/2.0"+pathname, base.Get, nil, params, nil, nil, resp, account)
}
func (driver Baidu) Post(pathname string, params map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
return driver.Request(pathname, base.Post, nil, params, nil, data, resp, account)
return driver.Request("https://pan.baidu.com/rest/2.0"+pathname, base.Post, nil, params, nil, data, resp, account)
}
func (driver Baidu) manage(opera string, filelist interface{}, account *model.Account) ([]byte, error) {

View File

@ -58,6 +58,14 @@ func (driver Baidu) Items() []base.Item {
Default: "asc",
Required: false,
},
{
Name: "internal_type",
Label: "download api",
Type: base.TypeSelect,
Required: true,
Values: "official,crack",
Default: "official",
},
{
Name: "client_id",
Label: "client id",
@ -125,6 +133,13 @@ func (driver Baidu) Files(path string, account *model.Account) ([]model.File, er
}
func (driver Baidu) Link(args base.Args, account *model.Account) (*base.Link, error) {
if account.InternalType == "crack" {
return driver.LinkCrack(args, account)
}
return driver.LinkOfficial(args, account)
}
func (driver Baidu) LinkOfficial(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(args.Path, account)
if err != nil {
return nil, err
@ -157,6 +172,32 @@ func (driver Baidu) Link(args base.Args, account *model.Account) (*base.Link, er
}}, nil
}
func (driver Baidu) LinkCrack(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(args.Path, account)
if err != nil {
return nil, err
}
if file.IsDir() {
return nil, base.ErrNotFile
}
var resp DownloadResp2
param := map[string]string{
"target": fmt.Sprintf("[\"%s\"]", utils.Join(account.RootFolder, args.Path)),
"dlink": "1",
"web": "5",
"origin": "dlna",
}
_, err = driver.Request("https://pan.baidu.com/api/filemetas", base.Get, nil, param, nil, nil, &resp, account)
if err != nil {
return nil, err
}
return &base.Link{
Url: resp.Info[0].Dlink,
Headers: []base.Header{
{Name: "User-Agent", Value: "pan.baidu.com"},
}}, nil
}
func (driver Baidu) Path(path string, account *model.Account) (*model.File, []model.File, error) {
file, err := driver.File(path, account)
if err != nil {

View File

@ -74,6 +74,65 @@ type DownloadResp struct {
RequestId string `json:"request_id"`
}
type DownloadResp2 struct {
Errno int `json:"errno"`
Info []struct {
//ExtentTinyint4 int `json:"extent_tinyint4"`
//ExtentTinyint1 int `json:"extent_tinyint1"`
//Bitmap string `json:"bitmap"`
//Category int `json:"category"`
//Isdir int `json:"isdir"`
//Videotag int `json:"videotag"`
Dlink string `json:"dlink"`
//OperID int64 `json:"oper_id"`
//PathMd5 int `json:"path_md5"`
//Wpfile int `json:"wpfile"`
//LocalMtime int `json:"local_mtime"`
/*Thumbs struct {
Icon string `json:"icon"`
URL3 string `json:"url3"`
URL2 string `json:"url2"`
URL1 string `json:"url1"`
} `json:"thumbs"`*/
//PlaySource int `json:"play_source"`
//Share int `json:"share"`
//FileKey string `json:"file_key"`
//Errno int `json:"errno"`
//LocalCtime int `json:"local_ctime"`
//Rotate int `json:"rotate"`
//Metadata time.Time `json:"metadata"`
//Height int `json:"height"`
//SampleRate int `json:"sample_rate"`
//Width int `json:"width"`
//OwnerType int `json:"owner_type"`
//Privacy int `json:"privacy"`
//ExtentInt3 int64 `json:"extent_int3"`
//RealCategory string `json:"real_category"`
//SrcLocation string `json:"src_location"`
//MetaInfo string `json:"meta_info"`
//ID string `json:"id"`
//Duration int `json:"duration"`
//FileSize string `json:"file_size"`
//Channels int `json:"channels"`
//UseSegment int `json:"use_segment"`
//ServerCtime int `json:"server_ctime"`
//Resolution string `json:"resolution"`
//OwnerID int `json:"owner_id"`
//ExtraInfo string `json:"extra_info"`
//Size int `json:"size"`
//FsID int64 `json:"fs_id"`
//ExtentTinyint3 int `json:"extent_tinyint3"`
//Md5 string `json:"md5"`
//Path string `json:"path"`
//FrameRate int `json:"frame_rate"`
//ExtentTinyint2 int `json:"extent_tinyint2"`
//ServerFilename string `json:"server_filename"`
//ServerMtime int `json:"server_mtime"`
//TkbindID int `json:"tkbind_id"`
} `json:"info"`
RequestID int64 `json:"request_id"`
}
type PrecreateResp struct {
Path string `json:"path"`
Uploadid string `json:"uploadid"`

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

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

View File

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

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

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

View File

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

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

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

View File

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

View File

@ -19,8 +19,9 @@ type DriverConfig struct {
}
type Args struct {
Path string
IP string
Path string
IP string
Header http.Header
}
type Driver interface {

View File

@ -3,6 +3,7 @@ package base
import (
"errors"
"io"
"net/http"
)
var (
@ -49,4 +50,6 @@ type Link struct {
Headers []Header `json:"headers"`
Data io.ReadCloser
FilePath string `json:"path"` // for native
Status int
Header http.Header
}

View File

@ -227,13 +227,21 @@ func (driver FTP) Copy(src string, dst string, account *model.Account) error {
func (driver FTP) Delete(path string, account *model.Account) error {
path = utils.ParsePath(path)
file, err := driver.File(path, account)
if err != nil {
return err
}
realPath := utils.Join(account.RootFolder, path)
conn, err := driver.Login(account)
if err != nil {
return err
}
//defer func() { _ = conn.Quit() }()
err = conn.Delete(realPath)
if file.IsDir() {
err = conn.RemoveDirRecur(realPath)
} else {
err = conn.Delete(realPath)
}
return err
}

View File

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

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

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

View File

@ -2,28 +2,15 @@ package lanzou
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
log "github.com/sirupsen/logrus"
"net/url"
"path/filepath"
"regexp"
"strconv"
"time"
)
type LanZouFile struct {
Name string `json:"name"`
NameAll string `json:"name_all"`
Id string `json:"id"`
FolId string `json:"fol_id"`
Size string `json:"size"`
Time string `json:"time"`
Folder bool
}
func (driver *Lanzou) FormatFile(file *LanZouFile) *model.File {
now := time.Now()
f := &model.File{
@ -35,12 +22,11 @@ func (driver *Lanzou) FormatFile(file *LanZouFile) *model.File {
UpdatedAt: &now,
}
if file.Folder {
f.Type = conf.FOLDER
f.Id = file.FolId
} else {
f.Name = file.NameAll
f.Type = utils.GetFileType(filepath.Ext(file.NameAll))
}
f.Type = file.GetType()
return f
}

View File

@ -1,5 +1,39 @@
package lanzou
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/utils"
"path"
)
type LanZouFile struct {
Name string `json:"name"`
NameAll string `json:"name_all"`
Id string `json:"id"`
FolId string `json:"fol_id"`
Size string `json:"size"`
Time string `json:"time"`
Folder bool
}
func (f LanZouFile) GetSize() uint64 {
return 0
}
func (f LanZouFile) GetName() string {
if f.Folder {
return f.Name
}
return f.NameAll
}
func (f LanZouFile) GetType() int {
if f.Folder {
return conf.FOLDER
}
return utils.GetFileType(path.Ext(f.NameAll))
}
type DownPageResp struct {
Zt int `json:"zt"`
Info struct {

View File

@ -71,7 +71,6 @@ func (driver Native) File(path string, account *model.Account) (*model.File, err
time := f.ModTime()
file := &model.File{
Name: f.Name(),
Size: f.Size(),
UpdatedAt: &time,
Driver: driver.Config().Name,
}
@ -79,6 +78,7 @@ func (driver Native) File(path string, account *model.Account) (*model.File, err
file.Type = conf.FOLDER
} else {
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
file.Size = f.Size()
}
return file, nil
}
@ -103,7 +103,6 @@ func (driver Native) Files(path string, account *model.Account) ([]model.File, e
time := f.ModTime()
file := model.File{
Name: f.Name(),
Size: f.Size(),
Type: 0,
UpdatedAt: &time,
Driver: driver.Config().Name,
@ -112,9 +111,14 @@ func (driver Native) Files(path string, account *model.Account) ([]model.File, e
file.Type = conf.FOLDER
} else {
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
file.Size = f.Size()
}
files = append(files, file)
}
_, err = base.GetCache(path, account)
if len(files) != 0 && err != nil {
_ = base.SetCache(path, files, account)
}
return files, nil
}

View File

@ -53,6 +53,7 @@ func (driver Onedrive) Items() []base.Item {
Label: "redirect uri",
Type: base.TypeString,
Required: true,
Default: "https://tool.nn.ci/onedrive/callback",
},
{
Name: "refresh_token",

View File

@ -307,8 +307,10 @@ func (driver Onedrive) UploadBig(file *model.FileStream, account *model.Account)
res, err := base.HttpClient.Do(req)
if res.StatusCode != 201 && res.StatusCode != 202 {
data, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return errors.New(string(data))
}
res.Body.Close()
}
return nil
}

View File

@ -138,6 +138,7 @@ func (driver Quark) Link(args base.Args, account *model.Account) (*base.Link, er
Url: resp.Data[0].DownloadUrl,
Headers: []base.Header{
{Name: "Cookie", Value: account.AccessToken},
{Name: "Referer", Value: "https://pan.quark.cn"},
},
}, nil
}

View File

@ -103,7 +103,7 @@ func (driver Quark) GetFiles(parent string, account *model.Account) ([]model.Fil
for _, f := range resp.Data.List {
files = append(files, *driver.formatFile(&f))
}
if page*size >= resp.Metadata.Count {
if page*size >= resp.Metadata.Total {
break
}
page++

View File

@ -51,37 +51,37 @@ type SortResp struct {
type DownResp struct {
Resp
Data []struct {
Fid string `json:"fid"`
FileName string `json:"file_name"`
PdirFid string `json:"pdir_fid"`
Category int `json:"category"`
FileType int `json:"file_type"`
Size int `json:"size"`
FormatType string `json:"format_type"`
Status int `json:"status"`
Tags string `json:"tags"`
LCreatedAt int64 `json:"l_created_at"`
LUpdatedAt int64 `json:"l_updated_at"`
NameSpace int `json:"name_space"`
Thumbnail string `json:"thumbnail"`
DownloadUrl string `json:"download_url"`
Md5 string `json:"md5"`
RiskType int `json:"risk_type"`
RangeSize int `json:"range_size"`
BackupSign int `json:"backup_sign"`
ObjCategory string `json:"obj_category"`
Duration int `json:"duration"`
FileSource string `json:"file_source"`
File bool `json:"file"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
PrivateExtra struct {
} `json:"_private_extra"`
//Fid string `json:"fid"`
//FileName string `json:"file_name"`
//PdirFid string `json:"pdir_fid"`
//Category int `json:"category"`
//FileType int `json:"file_type"`
//Size int `json:"size"`
//FormatType string `json:"format_type"`
//Status int `json:"status"`
//Tags string `json:"tags"`
//LCreatedAt int64 `json:"l_created_at"`
//LUpdatedAt int64 `json:"l_updated_at"`
//NameSpace int `json:"name_space"`
//Thumbnail string `json:"thumbnail"`
DownloadUrl string `json:"download_url"`
//Md5 string `json:"md5"`
//RiskType int `json:"risk_type"`
//RangeSize int `json:"range_size"`
//BackupSign int `json:"backup_sign"`
//ObjCategory string `json:"obj_category"`
//Duration int `json:"duration"`
//FileSource string `json:"file_source"`
//File bool `json:"file"`
//CreatedAt int64 `json:"created_at"`
//UpdatedAt int64 `json:"updated_at"`
//PrivateExtra struct {
//} `json:"_private_extra"`
} `json:"data"`
Metadata struct {
Acc2 string `json:"acc2"`
Acc1 string `json:"acc1"`
} `json:"metadata"`
//Metadata struct {
// Acc2 string `json:"acc2"`
// Acc1 string `json:"acc1"`
//} `json:"metadata"`
}
type UpPreResp struct {

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

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

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

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

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

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

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

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

View File

@ -9,6 +9,7 @@ import (
)
type Template struct {
base.Base
}
func (driver Template) Config() base.DriverConfig {
@ -111,39 +112,40 @@ func (driver Template) Path(path string, account *model.Account) (*model.File, [
return nil, files, nil
}
func (driver Template) Preview(path string, account *model.Account) (interface{}, error) {
//TODO preview interface if driver support
return nil, base.ErrNotImplement
}
func (driver Template) MakeDir(path string, account *model.Account) error {
//TODO make dir
return base.ErrNotImplement
}
func (driver Template) Move(src string, dst string, account *model.Account) error {
//TODO move file/dir
return base.ErrNotImplement
}
func (driver Template) Rename(src string, dst string, account *model.Account) error {
//TODO rename file/dir
return base.ErrNotImplement
}
func (driver Template) Copy(src string, dst string, account *model.Account) error {
//TODO copy file/dir
return base.ErrNotImplement
}
func (driver Template) Delete(path string, account *model.Account) error {
//TODO delete file/dir
return base.ErrNotImplement
}
func (driver Template) Upload(file *model.FileStream, account *model.Account) error {
//TODO upload file
return base.ErrNotImplement
}
// Optional function
//func (driver Template) Preview(path string, account *model.Account) (interface{}, error) {
// //TODO preview interface if driver support
// return nil, base.ErrNotImplement
//}
//
//func (driver Template) MakeDir(path string, account *model.Account) error {
// //TODO make dir
// return base.ErrNotImplement
//}
//
//func (driver Template) Move(src string, dst string, account *model.Account) error {
// //TODO move file/dir
// return base.ErrNotImplement
//}
//
//func (driver Template) Rename(src string, dst string, account *model.Account) error {
// //TODO rename file/dir
// return base.ErrNotImplement
//}
//
//func (driver Template) Copy(src string, dst string, account *model.Account) error {
// //TODO copy file/dir
// return base.ErrNotImplement
//}
//
//func (driver Template) Delete(path string, account *model.Account) error {
// //TODO delete file/dir
// return base.ErrNotImplement
//}
//
//func (driver Template) Upload(file *model.FileStream, account *model.Account) error {
// //TODO upload file
// return base.ErrNotImplement
//}
var _ base.Driver = (*Template)(nil)

View File

@ -3,8 +3,10 @@ package webdav
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/drivers/webdav/odrvcookie"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"net/http"
"path/filepath"
)
@ -40,6 +42,15 @@ func (driver WebDav) Items() []base.Item {
Type: base.TypeString,
Required: true,
},
{
Name: "internal_type",
Label: "vendor",
Type: base.TypeSelect,
Required: true,
Default: "other",
Values: "sharepoint,other",
Description: "webdav vendor",
},
}
}
@ -47,9 +58,17 @@ func (driver WebDav) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
account.Status = "work"
var err error
if isSharePoint(account) {
_, err = odrvcookie.GetCookie(account.Username, account.Password, account.SiteUrl)
}
if err != nil {
account.Status = err.Error()
} else {
account.Status = "work"
}
_ = model.SaveAccount(account)
return nil
return err
}
func (driver WebDav) File(path string, account *model.Account) (*model.File, error) {
@ -114,11 +133,27 @@ func (driver WebDav) Files(path string, account *model.Account) ([]model.File, e
func (driver WebDav) Link(args base.Args, account *model.Account) (*base.Link, error) {
path := args.Path
c := driver.NewClient(account)
reader, err := c.ReadStream(driver.WebDavPath(path))
callback := func(r *http.Request) {
if args.Header.Get("Range") != "" {
r.Header.Set("Range", args.Header.Get("Range"))
}
if args.Header.Get("If-Range") != "" {
r.Header.Set("If-Range", args.Header.Get("If-Range"))
}
}
reader, header, err := c.ReadStream(driver.WebDavPath(path), callback)
if err != nil {
return nil, err
}
return &base.Link{Data: reader}, nil
link := &base.Link{Data: reader}
if header.Get("Content-Range") != "" {
link.Status = 206
link.Header = http.Header{
"Content-Range": header.Values("Content-Range"),
"Content-Length": header.Values("Content-Length"),
}
}
return link, nil
}
func (driver WebDav) Path(path string, account *model.Account) (*model.File, []model.File, error) {
@ -178,7 +213,11 @@ func (driver WebDav) Upload(file *model.FileStream, account *model.Account) erro
}
c := driver.NewClient(account)
path := utils.Join(file.ParentPath, file.Name)
err := c.WriteStream(driver.WebDavPath(path), file, 0644)
callback := func(r *http.Request) {
r.Header.Set("Content-Type", file.GetMIMEType())
r.ContentLength = int64(file.GetSize())
}
err := c.WriteStream(driver.WebDavPath(path), file, 0644, callback)
return err
}

View File

@ -0,0 +1,47 @@
package odrvcookie
import (
"github.com/Xhofe/alist/utils/cookie"
log "github.com/sirupsen/logrus"
"net/http"
"sync"
"time"
)
type SpCookie struct {
Cookie string
expire time.Time
}
func (sp SpCookie) IsExpire() bool {
return time.Now().After(sp.expire)
}
var cookiesMap = struct {
sync.Mutex
m map[string]*SpCookie
}{m: make(map[string]*SpCookie)}
func GetCookie(username, password, siteUrl string) (string, error) {
cookiesMap.Lock()
defer cookiesMap.Unlock()
spCookie, ok := cookiesMap.m[username]
if ok {
if !spCookie.IsExpire() {
log.Debugln("sp use old cookie.")
return spCookie.Cookie, nil
}
}
log.Debugln("fetch new cookie")
ca := New(username, password, siteUrl)
tokenConf, err := ca.Cookies()
if err != nil {
return "", err
}
spCookie = &SpCookie{
Cookie: cookie.ToString([]*http.Cookie{&tokenConf.RtFa, &tokenConf.FedAuth}),
expire: time.Now().Add(time.Hour * 12),
}
cookiesMap.m[username] = spCookie
return spCookie.Cookie, nil
}

View File

@ -0,0 +1,206 @@
// Package odrvcookie can fetch authentication cookies for a sharepoint webdav endpoint
package odrvcookie
import (
"bytes"
"encoding/xml"
"fmt"
"html/template"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
"golang.org/x/net/publicsuffix"
)
// CookieAuth hold the authentication information
// These are username and password as well as the authentication endpoint
type CookieAuth struct {
user string
pass string
endpoint string
}
// CookieResponse contains the requested cookies
type CookieResponse struct {
RtFa http.Cookie
FedAuth http.Cookie
}
// SuccessResponse hold a response from the sharepoint webdav
type SuccessResponse struct {
XMLName xml.Name `xml:"Envelope"`
Succ SuccessResponseBody `xml:"Body"`
}
// SuccessResponseBody is the body of a success response, it holds the token
type SuccessResponseBody struct {
XMLName xml.Name
Type string `xml:"RequestSecurityTokenResponse>TokenType"`
Created time.Time `xml:"RequestSecurityTokenResponse>Lifetime>Created"`
Expires time.Time `xml:"RequestSecurityTokenResponse>Lifetime>Expires"`
Token string `xml:"RequestSecurityTokenResponse>RequestedSecurityToken>BinarySecurityToken"`
}
// reqString is a template that gets populated with the user data in order to retrieve a "BinarySecurityToken"
const reqString = `<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">{{ .LoginUrl }}</a:To>
<o:Security s:mustUnderstand="1"
xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<o:UsernameToken>
<o:Username>{{ .Username }}</o:Username>
<o:Password>{{ .Password }}</o:Password>
</o:UsernameToken>
</o:Security>
</s:Header>
<s:Body>
<t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
<a:EndpointReference>
<a:Address>{{ .Address }}</a:Address>
</a:EndpointReference>
</wsp:AppliesTo>
<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
</t:RequestSecurityToken>
</s:Body>
</s:Envelope>`
// New creates a new CookieAuth struct
func New(pUser, pPass, pEndpoint string) CookieAuth {
retStruct := CookieAuth{
user: pUser,
pass: pPass,
endpoint: pEndpoint,
}
return retStruct
}
// Cookies creates a CookieResponse. It fetches the auth token and then
// retrieves the Cookies
func (ca *CookieAuth) Cookies() (CookieResponse, error) {
spToken, err := ca.getSPToken()
if err != nil {
return CookieResponse{}, err
}
return ca.getSPCookie(spToken)
}
func (ca *CookieAuth) getSPCookie(conf *SuccessResponse) (CookieResponse, error) {
spRoot, err := url.Parse(ca.endpoint)
if err != nil {
return CookieResponse{}, err
}
u, err := url.Parse("https://" + spRoot.Host + "/_forms/default.aspx?wa=wsignin1.0")
if err != nil {
return CookieResponse{}, err
}
// To authenticate with davfs or anything else we need two cookies (rtFa and FedAuth)
// In order to get them we use the token we got earlier and a cookieJar
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
return CookieResponse{}, err
}
client := &http.Client{
Jar: jar,
}
// Send the previously aquired Token as a Post parameter
if _, err = client.Post(u.String(), "text/xml", strings.NewReader(conf.Succ.Token)); err != nil {
return CookieResponse{}, err
}
cookieResponse := CookieResponse{}
for _, cookie := range jar.Cookies(u) {
if (cookie.Name == "rtFa") || (cookie.Name == "FedAuth") {
switch cookie.Name {
case "rtFa":
cookieResponse.RtFa = *cookie
case "FedAuth":
cookieResponse.FedAuth = *cookie
}
}
}
return cookieResponse, err
}
var loginUrlsMap = map[string]string{
"com": "https://login.microsoftonline.com",
"cn": "https://login.chinacloudapi.cn",
"us": "https://login.microsoftonline.us",
"de": "https://login.microsoftonline.de",
}
func getLoginUrl(endpoint string) (string, error) {
spRoot, err := url.Parse(endpoint)
if err != nil {
return "", err
}
domains := strings.Split(spRoot.Host, ".")
tld := domains[len(domains)-1]
loginUrl, ok := loginUrlsMap[tld]
if !ok {
return "", fmt.Errorf("tld %s is not supported", tld)
}
return loginUrl + "/extSTS.srf", nil
}
func (ca *CookieAuth) getSPToken() (*SuccessResponse, error) {
loginUrl, err := getLoginUrl(ca.endpoint)
if err != nil {
return nil, err
}
reqData := map[string]string{
"Username": ca.user,
"Password": ca.pass,
"Address": ca.endpoint,
"LoginUrl": loginUrl,
}
t := template.Must(template.New("authXML").Parse(reqString))
buf := &bytes.Buffer{}
if err := t.Execute(buf, reqData); err != nil {
return nil, err
}
// Execute the first request which gives us an auth token for the sharepoint service
// With this token we can authenticate on the login page and save the returned cookies
req, err := http.NewRequest("POST", loginUrl, buf)
if err != nil {
return nil, err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBuf := bytes.Buffer{}
respBuf.ReadFrom(resp.Body)
s := respBuf.Bytes()
var conf SuccessResponse
err = xml.Unmarshal(s, &conf)
if err != nil {
return nil, err
}
return &conf, err
}

7
drivers/webdav/util.go Normal file
View File

@ -0,0 +1,7 @@
package webdav
import "github.com/Xhofe/alist/model"
func isSharePoint(account *model.Account) bool {
return account.InternalType == "sharepoint"
}

View File

@ -2,14 +2,26 @@ package webdav
import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/drivers/webdav/odrvcookie"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/pkg/gowebdav"
"github.com/Xhofe/alist/utils"
"github.com/studio-b12/gowebdav"
"net/http"
"strings"
)
func (driver WebDav) NewClient(account *model.Account) *gowebdav.Client {
return gowebdav.NewClient(account.SiteUrl, account.Username, account.Password)
c := gowebdav.NewClient(account.SiteUrl, account.Username, account.Password)
if isSharePoint(account) {
cookie, err := odrvcookie.GetCookie(account.Username, account.Password, account.SiteUrl)
if err == nil {
c.SetInterceptor(func(method string, rq *http.Request) {
rq.Header.Del("Authorization")
rq.Header.Set("Cookie", cookie)
})
}
}
return c
}
func (driver WebDav) WebDavPath(path string) string {

View File

@ -1,21 +1,21 @@
package xunlei
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/go-resty/resty/v2"
"strings"
"time"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
log "github.com/sirupsen/logrus"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
)
type XunLeiCloud struct{}
@ -48,8 +48,61 @@ func (driver XunLeiCloud) Items() []base.Item {
Description: "account password",
},
{
Name: "root_folder",
Label: "root folder file_id",
Name: "captcha_token",
Label: "verified captcha token",
Type: base.TypeString,
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
},
{
Name: "client_version",
Label: "client version",
Default: "7.43.0.7998",
Type: base.TypeString,
Required: true,
},
{
Name: "client_id",
Label: "client id",
Default: "Xp6vsxz_7IYVw2BB",
Type: base.TypeString,
Required: true,
},
{
Name: "client_secret",
Label: "client secret",
Default: "Xp6vsy4tN9toTVdMSpomVdXpRmES",
Type: base.TypeString,
Required: true,
},
{
Name: "algorithms",
Label: "algorithms",
Default: "hrVPGbeqYPs+CIscj05VpAtjalzY5yjpvlMS8bEo,DrI0uTP,HHK0VXyMgY0xk2K0o,BBaXsExvL3GadmIacjWv7ISUJp3ifAwqbJumu,5toJ7ejB+bh1,5LsZTFAFjgvFvIl1URBgOAJ,QcJ5Ry+,hYgZVz8r7REROaCYfd9,zw6gXgkk/8TtGrmx6EGfekPESLnbZfDFwqR,gtSwLnMBa8h12nF3DU6+LwEQPHxd,fMG8TvtAYbCkxuEbIm0Xi/Lb7Z",
Type: base.TypeString,
Required: true,
},
{
Name: "package_name",
Label: "package name",
Default: "com.xunlei.downloadprovider",
Type: base.TypeString,
Required: true,
},
{
Name: "user_agent",
Label: "user agent",
Default: "ANDROID-com.xunlei.downloadprovider/7.43.0.7998 netWorkType/WIFI appid/40 deviceName/Samsung_Sm-g9810 deviceModel/SM-G9810 OSVersion/7.1.2 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_0_9+) (JAVA 0)",
Type: base.TypeString,
Required: false,
},
{
Name: "device_id",
Label: "device id",
Default: utils.GetMD5Encode(uuid.NewString()),
Type: base.TypeString,
Required: true,
},
@ -60,10 +113,18 @@ func (driver XunLeiCloud) Save(account *model.Account, old *model.Account) error
if account == nil {
return nil
}
state := GetState(account)
if state.isTokensExpires() {
return state.Login(account)
client := GetClient(account)
// 指定验证通过的captchaToken
if account.CaptchaToken != "" {
client.UpdateCaptchaToken(strings.TrimSpace(account.CaptchaToken))
account.CaptchaToken = ""
}
if client.token == "" {
return client.Login(account)
}
account.Status = "work"
model.SaveAccount(account)
return nil
@ -101,19 +162,23 @@ func (driver XunLeiCloud) Files(path string, account *model.Account) ([]model.Fi
files, _ := cache.([]model.File)
return files, nil
}
file, err := driver.File(path, account)
parentFile, err := driver.File(path, account)
if err != nil {
return nil, err
}
time.Sleep(time.Millisecond * 300)
files := make([]model.File, 0)
var pageToken string
for {
var fileList FileList
_, err = GetState(account).Request("GET", FILE_API_URL, func(r *resty.Request) {
_, err = GetClient(account).Request("GET", FILE_API_URL, func(r *resty.Request) {
r.SetQueryParams(map[string]string{
"parent_id": file.Id,
"page_token": fileList.NextPageToken,
"parent_id": parentFile.Id,
"page_token": pageToken,
"with_audit": "true",
"limit": "100",
"filters": `{"phase": {"eq": "PHASE_TYPE_COMPLETE"}, "trashed":{"eq":false}}`,
})
r.SetResult(&fileList)
@ -129,6 +194,7 @@ func (driver XunLeiCloud) Files(path string, account *model.Account) ([]model.Fi
if fileList.NextPageToken == "" {
break
}
pageToken = fileList.NextPageToken
}
if len(files) > 0 {
_ = base.SetCache(path, files, account)
@ -162,8 +228,9 @@ func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Li
return nil, base.ErrNotFile
}
var lFile Files
_, err = GetState(account).Request("GET", FILE_API_URL+"/{id}", func(r *resty.Request) {
r.SetPathParam("id", file.Id)
clinet := GetClient(account)
_, err = clinet.Request("GET", FILE_API_URL+"/{fileID}", func(r *resty.Request) {
r.SetPathParam("fileID", file.Id)
r.SetQueryParam("with_audit", "true")
r.SetResult(&lFile)
}, account)
@ -172,7 +239,7 @@ func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Li
}
return &base.Link{
Headers: []base.Header{
{Name: "User-Agent", Value: base.UserAgent},
{Name: "User-Agent", Value: clinet.userAgent},
},
Url: lFile.WebContentLink,
}, nil
@ -180,7 +247,6 @@ func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Li
func (driver XunLeiCloud) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
log.Debugf("xunlei path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
@ -199,6 +265,18 @@ func (driver XunLeiCloud) Preview(path string, account *model.Account) (interfac
return nil, base.ErrNotSupport
}
func (driver XunLeiCloud) Rename(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
_, err = GetClient(account).Request("PATCH", FILE_API_URL+"/{fileID}", func(r *resty.Request) {
r.SetPathParam("fileID", srcFile.Id)
r.SetBody(&base.Json{"name": filepath.Base(dst)})
}, account)
return err
}
func (driver XunLeiCloud) MakeDir(path string, account *model.Account) error {
dir, name := filepath.Split(path)
parentFile, err := driver.File(dir, account)
@ -208,7 +286,7 @@ func (driver XunLeiCloud) MakeDir(path string, account *model.Account) error {
if !parentFile.IsDir() {
return base.ErrNotFolder
}
_, err = GetState(account).Request("POST", FILE_API_URL, func(r *resty.Request) {
_, err = GetClient(account).Request("POST", FILE_API_URL, func(r *resty.Request) {
r.SetBody(&base.Json{
"kind": FOLDER,
"name": name,
@ -229,7 +307,7 @@ func (driver XunLeiCloud) Move(src string, dst string, account *model.Account) e
return err
}
_, err = GetState(account).Request("POST", FILE_API_URL+":batchMove", func(r *resty.Request) {
_, err = GetClient(account).Request("POST", FILE_API_URL+":batchMove", func(r *resty.Request) {
r.SetBody(&base.Json{
"to": base.Json{"parent_id": dstDirFile.Id},
"ids": []string{srcFile.Id},
@ -248,7 +326,7 @@ func (driver XunLeiCloud) Copy(src string, dst string, account *model.Account) e
if err != nil {
return err
}
_, err = GetState(account).Request("POST", FILE_API_URL+":batchCopy", func(r *resty.Request) {
_, err = GetClient(account).Request("POST", FILE_API_URL+":batchCopy", func(r *resty.Request) {
r.SetBody(&base.Json{
"to": base.Json{"parent_id": dstDirFile.Id},
"ids": []string{srcFile.Id},
@ -262,8 +340,8 @@ func (driver XunLeiCloud) Delete(path string, account *model.Account) error {
if err != nil {
return err
}
_, err = GetState(account).Request("PATCH", FILE_API_URL+"/{id}/trash", func(r *resty.Request) {
r.SetPathParam("id", srcFile.Id)
_, err = GetClient(account).Request("PATCH", FILE_API_URL+"/{fileID}/trash", func(r *resty.Request) {
r.SetPathParam("fileID", srcFile.Id)
r.SetBody(&base.Json{})
}, account)
return err
@ -279,28 +357,35 @@ func (driver XunLeiCloud) Upload(file *model.FileStream, account *model.Account)
return err
}
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
/*
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer os.Remove(tempFile.Name())
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
gcid, err := getGcid(io.TeeReader(file, tempFile), int64(file.Size))
if err != nil {
return err
}
gcid, err := getGcid(io.TeeReader(file, tempFile), int64(file.Size))
if err != nil {
return err
}
tempFile.Close()
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return err
}
*/
var resp UploadTaskResponse
_, err = GetState(account).Request("POST", FILE_API_URL, func(r *resty.Request) {
_, err = GetClient(account).Request("POST", FILE_API_URL, func(r *resty.Request) {
r.SetBody(&base.Json{
"kind": FILE,
"parent_id": parentFile.Id,
"name": file.Name,
"size": fmt.Sprint(file.Size),
"hash": gcid,
"size": file.Size,
"hash": "1CF254FBC456E1B012CD45C546636AA62CF8350E",
"upload_type": UPLOAD_TYPE_RESUMABLE,
})
r.SetResult(&resp)
@ -311,30 +396,24 @@ func (driver XunLeiCloud) Upload(file *model.FileStream, account *model.Account)
param := resp.Resumable.Params
if resp.UploadType == UPLOAD_TYPE_RESUMABLE {
client, err := oss.New(param.Endpoint, param.AccessKeyID, param.AccessKeySecret, oss.SecurityToken(param.SecurityToken), oss.EnableMD5(true))
param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".")
s, err := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),
Region: aws.String("xunlei"),
Endpoint: aws.String(param.Endpoint),
})
if err != nil {
return err
}
bucket, err := client.Bucket(param.Bucket)
if err != nil {
return err
}
return bucket.UploadFile(param.Key, tempFile.Name(), 1<<22, oss.Routines(3), oss.Checkpoint(true, ""), oss.Expires(param.Expiration))
_, err = s3manager.NewUploader(s).Upload(&s3manager.UploadInput{
Bucket: aws.String(param.Bucket),
Key: aws.String(param.Key),
Expires: aws.Time(param.Expiration),
Body: file,
})
return err
}
return nil
}
func (driver XunLeiCloud) Rename(src string, dst string, account *model.Account) error {
_, dstName := filepath.Split(dst)
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
_, err = GetState(account).Request("PATCH", FILE_API_URL+"/{id}", func(r *resty.Request) {
r.SetPathParam("id", srcFile.Id)
r.SetBody(&base.Json{"name": dstName})
}, account)
return err
}
var _ base.Driver = (*XunLeiCloud)(nil)

View File

@ -1,23 +1,35 @@
package xunlei
import (
"fmt"
"time"
)
type Erron struct {
Error string `json:"error"`
ErrorCode int64 `json:"error_code"`
ErrorMsg string `json:"error"`
ErrorDescription string `json:"error_description"`
// ErrorDetails interface{} `json:"error_details"`
}
func (e *Erron) HasError() bool {
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
}
func (e *Erron) Error() string {
return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)
}
/*
* 验证码Token
**/
type CaptchaTokenRequest struct {
Action string `json:"action"`
CaptchaToken string `json:"captcha_token"`
ClientID string `json:"client_id"`
DeviceID string `json:"device_id"`
Meta map[string]string `json:"meta"`
//RedirectUri string `json:"redirect_uri"`
RedirectUri string `json:"redirect_uri"`
}
type CaptchaTokenResponse struct {
@ -26,6 +38,9 @@ type CaptchaTokenResponse struct {
Url string `json:"url"`
}
/*
* 登录
**/
type TokenResponse struct {
TokenType string `json:"token_type"`
AccessToken string `json:"access_token"`
@ -36,6 +51,10 @@ type TokenResponse struct {
UserID string `json:"user_id"`
}
func (t *TokenResponse) Token() string {
return fmt.Sprint(t.TokenType, " ", t.AccessToken)
}
type SignInRequest struct {
CaptchaToken string `json:"captcha_token"`
@ -46,6 +65,9 @@ type SignInRequest struct {
Password string `json:"password"`
}
/*
* 文件
**/
type FileList struct {
Kind string `json:"kind"`
NextPageToken string `json:"next_page_token"`
@ -116,6 +138,9 @@ type Files struct {
//Collection interface{} `json:"collection"`
}
/*
* 上传
**/
type UploadTaskResponse struct {
UploadType string `json:"upload_type"`

View File

@ -3,40 +3,10 @@ package xunlei
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"net/url"
"github.com/Xhofe/alist/utils"
)
const (
// 小米浏览器
CLIENT_ID = "X7MtiU0Gb5YqWv-6"
CLIENT_SECRET = "84MYEih3Eeu2HF4RrGce3Q"
CLIENT_VERSION = "5.1.0.51045"
ALG_VERSION = "1"
PACKAGE_NAME = "com.xunlei.xcloud.lib"
)
var Algorithms = []string{
"",
"BXza40wm+P4zw8rEFpHA",
"UfZLfKfYRmKTA0",
"OMBGVt/9Wcaln1XaBz",
"Jn217F4rk5FPPWyhoeV",
"w5OwkGo0pGpb0Xe/XZ5T3",
"5guM3DNiY4F78x49zQ97q75",
"QXwn4D2j884wJgrYXjGClM/IVrJX",
"NXBRosYvbHIm6w8vEB",
"2kZ8Ie1yW2ib4O2iAkNpJobP",
"11CoVJJQEc",
"xf3QWysVwnVsNv5DCxU+cgNT1rK",
"9eEfKkrqkfw",
"T78dnANexYRbiZy",
}
const (
API_URL = "https://api-pan.xunlei.com/drive/v1"
FILE_API_URL = API_URL + "/files"
@ -44,9 +14,8 @@ const (
)
const (
FOLDER = "drive#folder"
FILE = "drive#file"
FOLDER = "drive#folder"
FILE = "drive#file"
RESUMABLE = "drive#resumable"
)
@ -57,47 +26,32 @@ const (
UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL"
)
func captchaSign(driverID string, time int64) string {
str := fmt.Sprint(CLIENT_ID, CLIENT_VERSION, PACKAGE_NAME, driverID, time)
for _, algorithm := range Algorithms {
str = utils.GetMD5Encode(fmt.Sprint(str, algorithm))
}
return fmt.Sprint(ALG_VERSION, ".", str)
}
func getAction(method string, u string) string {
c, _ := url.Parse(u)
return fmt.Sprint(method, ":", c.Path)
return method + ":" + c.Path
}
// 计算文件Gcid
func getGcid(r io.Reader, size int64) (string, error) {
calcBlockSize := func(j int64) int64 {
if j >= 0 && j <= 134217728 {
return 262144
if j >= 0 && j <= 0x8000000 {
return 0x40000
}
if j <= 134217728 || j > 268435456 {
if j <= 268435456 || j > 536870912 {
return 2097152
if j <= 0x8000000 || j > 0x10000000 {
if j <= 0x10000000 || j > 0x20000000 {
return 0x200000
}
return 1048576
return 0x100000
}
return 524288
return 0x80000
}
/*
calcBlockSize := func(j int64) int64 {
psize := int64(0x40000)
for j/psize > 0x200 {
psize <<= 1
}
return psize
}
*/
hash1 := sha1.New()
hash2 := sha1.New()
readSize := calcBlockSize(size)
for {
hash2.Reset()
if n, err := io.CopyN(hash2, r, calcBlockSize(size)); err != nil && n == 0 {
if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
if err != io.EOF {
return "", err
}

View File

@ -3,6 +3,7 @@ package xunlei
import (
"fmt"
"net/http"
"strings"
"sync"
"time"
@ -13,275 +14,269 @@ import (
log "github.com/sirupsen/logrus"
)
var xunleiClient = resty.New().SetHeaders(map[string]string{"Accept": "application/json;charset=UTF-8"}).SetTimeout(base.DefaultTimeout)
// 缓存登录状态
var userClients sync.Map
// 一个账户只允许登陆一次
var userStateCache = struct {
func GetClient(account *model.Account) *Client {
if v, ok := userClients.Load(account.Username); ok {
return v.(*Client)
}
client := &Client{
Client: base.RestyClient,
clientID: account.ClientId,
clientSecret: account.ClientSecret,
clientVersion: account.ClientVersion,
packageName: account.PackageName,
algorithms: strings.Split(account.Algorithms, ","),
userAgent: account.UserAgent,
deviceID: account.DeviceId,
}
userClients.Store(account.Username, client)
return client
}
type Client struct {
*resty.Client
sync.Mutex
States map[string]*State
}{States: make(map[string]*State)}
func GetState(account *model.Account) *State {
userStateCache.Lock()
defer userStateCache.Unlock()
if v, ok := userStateCache.States[account.Username]; ok && v != nil {
return v
}
state := new(State).Init()
userStateCache.States[account.Username] = state
return state
clientID string
clientSecret string
clientVersion string
packageName string
algorithms []string
userAgent string
deviceID string
captchaToken string
token string
refreshToken string
userID string
}
type State struct {
sync.Mutex
captchaToken string
captchaTokenExpiresTime int64
tokenType string
accessToken string
refreshToken string
tokenExpiresTime int64 //Milli
userID string
}
func (s *State) init() *State {
s.captchaToken = ""
s.captchaTokenExpiresTime = 0
s.tokenType = ""
s.accessToken = ""
s.refreshToken = ""
s.tokenExpiresTime = 0
s.userID = "0"
return s
}
func (s *State) getToken(account *model.Account) (string, error) {
if s.isTokensExpires() {
if err := s.refreshToken_(account); err != nil {
return "", err
}
}
return fmt.Sprint(s.tokenType, " ", s.accessToken), nil
}
func (s *State) getCaptchaToken(action string, account *model.Account) (string, error) {
if s.isCaptchaTokenExpires() {
return s.newCaptchaToken(action, nil, account)
}
return s.captchaToken, nil
}
func (s *State) isCaptchaTokenExpires() bool {
return time.Now().UnixMilli() >= s.captchaTokenExpiresTime || s.captchaToken == "" || s.tokenType == ""
}
func (s *State) isTokensExpires() bool {
return time.Now().UnixMilli() >= s.tokenExpiresTime || s.accessToken == ""
}
func (s *State) newCaptchaToken(action string, meta map[string]string, account *model.Account) (string, error) {
ctime := time.Now().UnixMilli()
driverID := utils.GetMD5Encode(account.Username)
creq := CaptchaTokenRequest{
// 请求验证码token
func (c *Client) requestCaptchaToken(action string, meta map[string]string) error {
param := CaptchaTokenRequest{
Action: action,
CaptchaToken: s.captchaToken,
ClientID: CLIENT_ID,
DeviceID: driverID,
Meta: map[string]string{
"captcha_sign": captchaSign(driverID, ctime),
"client_version": CLIENT_VERSION,
"package_name": PACKAGE_NAME,
"timestamp": fmt.Sprint(ctime),
"user_id": s.userID,
},
}
for k, v := range meta {
creq.Meta[k] = v
CaptchaToken: c.captchaToken,
ClientID: c.clientID,
DeviceID: c.deviceID,
Meta: meta,
RedirectUri: "xlaccsdk01://xunlei.com/callback?state=harbor",
}
var e Erron
var resp CaptchaTokenResponse
_, err := xunleiClient.R().
SetBody(&creq).
_, err := c.Client.R().
SetBody(&param).
SetError(&e).
SetResult(&resp).
SetHeader("X-Device-Id", driverID).
SetQueryParam("client_id", CLIENT_ID).
SetHeader("X-Device-Id", c.deviceID).
SetQueryParam("client_id", c.clientID).
Post(XLUSER_API_URL + "/shield/captcha/init")
if err != nil {
return "", err
return err
}
if e.ErrorCode != 0 {
return "", fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
if e.HasError() {
return &e
}
if resp.Url != "" {
return "", fmt.Errorf("需要验证验证码")
return fmt.Errorf("need verify:%s", resp.Url)
}
s.captchaTokenExpiresTime = (ctime + resp.ExpiresIn*1000) - 30000
s.captchaToken = resp.CaptchaToken
return s.captchaToken, nil
if resp.CaptchaToken == "" {
return fmt.Errorf("empty captchaToken")
}
c.captchaToken = resp.CaptchaToken
return nil
}
func (s *State) refreshToken_(account *model.Account) error {
var e Erron
var resp TokenResponse
_, err := xunleiClient.R().
SetResult(&resp).SetError(&e).
SetBody(&base.Json{
"grant_type": "refresh_token",
"refresh_token": s.refreshToken,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
}).
SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)).SetQueryParam("client_id", CLIENT_ID).
Post(XLUSER_API_URL + "/auth/token")
if err != nil {
return err
}
switch e.ErrorCode {
case 4122, 4121:
return s.login(account)
case 0:
s.tokenExpiresTime = (time.Now().UnixMilli() + resp.ExpiresIn*1000) - 30000
s.tokenType = resp.TokenType
s.accessToken = resp.AccessToken
s.refreshToken = resp.RefreshToken
s.userID = resp.UserID
return nil
default:
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
// 验证码签名
func (c *Client) captchaSign(time string) string {
str := fmt.Sprint(c.clientID, c.clientVersion, c.packageName, c.deviceID, time)
for _, algorithm := range c.algorithms {
str = utils.GetMD5Encode(str + algorithm)
}
return "1." + str
}
func (s *State) login(account *model.Account) error {
s.init()
ctime := time.Now().UnixMilli()
// 登录
func (c *Client) Login(account *model.Account) (err error) {
c.Lock()
defer c.Unlock()
defer func() {
if err != nil {
account.Status = err.Error()
} else {
account.Status = "work"
}
model.SaveAccount(account)
}()
meta := make(map[string]string)
if strings.Contains(account.Username, "@") {
meta["email"] = account.Username
} else if len(account.Username) >= 11 {
if !strings.Contains(account.Username, "+") {
account.Username = "+86 " + account.Username
}
meta["phone_number"] = account.Username
} else {
meta["username"] = account.Username
}
url := XLUSER_API_URL + "/auth/signin"
captchaToken, err := s.newCaptchaToken(getAction("POST", url), map[string]string{"username": account.Username}, account)
err = c.requestCaptchaToken(getAction(http.MethodPost, url), meta)
if err != nil {
return err
}
signReq := SignInRequest{
CaptchaToken: captchaToken,
ClientID: CLIENT_ID,
ClientSecret: CLIENT_SECRET,
Username: account.Username,
Password: account.Password,
}
var e Erron
var resp TokenResponse
_, err = xunleiClient.R().
_, err = c.Client.R().
SetResult(&resp).
SetError(&e).
SetBody(&signReq).
SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)).
SetQueryParam("client_id", CLIENT_ID).
SetBody(&SignInRequest{
CaptchaToken: c.captchaToken,
ClientID: c.clientID,
ClientSecret: c.clientSecret,
Username: account.Username,
Password: account.Password,
}).
SetHeader("X-Device-Id", c.deviceID).
SetQueryParam("client_id", c.clientID).
Post(url)
if err != nil {
return err
}
defer model.SaveAccount(account)
if e.ErrorCode != 0 {
account.Status = e.Error
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
if e.HasError() {
return &e
}
account.Status = "work"
s.tokenExpiresTime = (ctime + resp.ExpiresIn*1000) - 30000
s.tokenType = resp.TokenType
s.accessToken = resp.AccessToken
s.refreshToken = resp.RefreshToken
s.userID = resp.UserID
if resp.RefreshToken == "" {
return base.ErrEmptyToken
}
c.token = resp.Token()
c.refreshToken = resp.RefreshToken
c.userID = resp.UserID
return nil
}
func (s *State) Request(method string, url string, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
s.Lock()
token, err := s.getToken(account)
if err != nil {
return nil, err
}
// 刷新验证码token
func (c *Client) RefreshCaptchaToken(action string) error {
c.Lock()
defer c.Unlock()
captchaToken, err := s.getCaptchaToken(getAction(method, url), account)
if err != nil {
return nil, err
timestamp := fmt.Sprint(time.Now().UnixMilli())
param := map[string]string{
"client_version": c.clientVersion,
"package_name": c.packageName,
"user_id": c.userID,
"captcha_sign": c.captchaSign(timestamp),
"timestamp": timestamp,
}
return c.requestCaptchaToken(action, param)
}
req := xunleiClient.R().
SetHeaders(map[string]string{
"X-Device-Id": utils.GetMD5Encode(account.Username),
"Authorization": token,
"X-Captcha-Token": captchaToken,
// 刷新token
func (c *Client) RefreshToken() error {
c.Lock()
defer c.Unlock()
var e Erron
var resp TokenResponse
_, err := c.Client.R().
SetError(&e).
SetResult(&resp).
SetBody(&base.Json{
"grant_type": "refresh_token",
"refresh_token": c.refreshToken,
"client_id": c.clientID,
"client_secret": c.clientSecret,
}).
SetQueryParam("client_id", CLIENT_ID)
callback(req)
s.Unlock()
var res *resty.Response
switch method {
case "GET":
res, err = req.Get(url)
case "POST":
res, err = req.Post(url)
case "DELETE":
res, err = req.Delete(url)
case "PATCH":
res, err = req.Patch(url)
case "PUT":
res, err = req.Put(url)
default:
return nil, base.ErrNotSupport
SetHeader("X-Device-Id", c.deviceID).
SetQueryParam("client_id", c.clientID).
Post(XLUSER_API_URL + "/auth/token")
if err != nil {
return err
}
if e.HasError() {
return &e
}
if resp.RefreshToken == "" {
return base.ErrEmptyToken
}
c.token = resp.TokenType + " " + resp.AccessToken
c.refreshToken = resp.RefreshToken
c.userID = resp.UserID
return nil
}
func (c *Client) Request(method string, url string, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
c.Lock()
req := c.Client.R().
SetHeaders(map[string]string{
"X-Device-Id": c.deviceID,
"Authorization": c.token,
"X-Captcha-Token": c.captchaToken,
"User-Agent": c.userAgent,
"client_id": c.clientID,
}).
SetQueryParam("client_id", c.clientID)
if callback != nil {
callback(req)
}
c.Unlock()
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
log.Debug(res.String())
var e Erron
utils.Json.Unmarshal(res.Body(), &e)
switch e.ErrorCode {
case 9:
s.newCaptchaToken(getAction(method, url), nil, account)
fallthrough
case 4122, 4121:
return s.Request(method, url, callback, account)
case 0:
if res.StatusCode() == http.StatusOK {
return res, nil
}
return nil, fmt.Errorf(res.String())
default:
return nil, fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
if err = utils.Json.Unmarshal(res.Body(), &e); err != nil {
return nil, err
}
// 处理错误
switch e.ErrorCode {
case 0:
return res, nil
case 4122, 4121, 10: // token过期
if err = c.RefreshToken(); err == nil {
break
}
fallthrough
case 16: // 登录失效
if err = c.Login(account); err != nil {
return nil, err
}
case 9: // 验证码token过期
if err = c.RefreshCaptchaToken(getAction(method, url)); err != nil {
return nil, err
}
default:
return nil, &e
}
return c.Request(method, url, callback, account)
}
func (s *State) Init() *State {
s.Lock()
defer s.Unlock()
return s.init()
}
func (c *Client) UpdateCaptchaToken(captchaToken string) bool {
c.Lock()
defer c.Unlock()
func (s *State) GetCaptchaToken(action string, account *model.Account) (string, error) {
s.Lock()
defer s.Unlock()
return s.getCaptchaToken(action, account)
}
func (s *State) GetToken(account *model.Account) (string, error) {
s.Lock()
defer s.Unlock()
return s.getToken(account)
}
func (s *State) Login(account *model.Account) error {
s.Lock()
defer s.Unlock()
return s.login(account)
if captchaToken != "" {
c.captchaToken = captchaToken
return true
}
return false
}

View File

@ -213,7 +213,8 @@ func (driver Yandex) Upload(file *model.FileStream, account *model.Account) erro
}
req.Header.Set("Content-Length", strconv.FormatUint(file.Size, 10))
req.Header.Set("Content-Type", "application/octet-stream")
_, err = base.HttpClient.Do(req)
res, err := base.HttpClient.Do(req)
res.Body.Close()
//res, err := base.RestyClient.R().
// SetHeader("Content-Length", strconv.FormatUint(file.Size, 10)).
// SetBody(file).Put(resp.Href)

15
go.mod
View File

@ -1,6 +1,6 @@
module github.com/Xhofe/alist
go 1.17
go 1.18
require (
github.com/aws/aws-sdk-go v1.27.0
@ -13,9 +13,9 @@ require (
github.com/jlaffaye/ftp v0.0.0-20211117213618-11820403398b
github.com/json-iterator/go v1.1.12
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/sftp v1.13.4
github.com/robfig/cron/v3 v3.0.0
github.com/sirupsen/logrus v1.8.1
github.com/studio-b12/gowebdav v0.0.0-20211109083228-3f8721cd4b6f
github.com/upyun/go-sdk/v3 v3.0.2
golang.org/x/text v0.3.7
gorm.io/driver/mysql v1.3.2
@ -24,16 +24,15 @@ require (
gorm.io/gorm v1.23.1
)
require github.com/kr/fs v0.1.0 // indirect
require (
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/fatih/color v1.13.0
github.com/mattn/go-colorable v0.1.9 // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
)
require (
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
github.com/aliyun/aliyun-oss-go-sdk v2.2.1+incompatible
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
@ -73,9 +72,9 @@ require (
go.opentelemetry.io/otel v0.20.0 // indirect
go.opentelemetry.io/otel/metric v0.20.0 // indirect
go.opentelemetry.io/otel/trace v0.20.0 // indirect
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70 // indirect
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
golang.org/x/net v0.0.0-20211209124913-491a49abca63
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect

32
go.sum
View File

@ -20,8 +20,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/aliyun/aliyun-oss-go-sdk v2.2.1+incompatible h1:uuJIwCFhbZy+zdvLy5zrcIToPEQP0s5CFOZ0Zj03O/w=
github.com/aliyun/aliyun-oss-go-sdk v2.2.1+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/allegro/bigcache/v2 v2.2.5 h1:mRc8r6GQjuJsmSKQNPsR5jQVXc8IJ1xsW5YXUYMLfqI=
github.com/allegro/bigcache/v2 v2.2.5/go.mod h1:FppZsIO+IZk7gCuj5FiIDHGygD9xvWQcqg1uIPMb6tY=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@ -34,8 +32,6 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ
github.com/aws/aws-sdk-go v1.27.0 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -46,7 +42,6 @@ github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQ
github.com/caarlos0/env/v6 v6.9.1 h1:zOkkjM0F6ltnQ5eBX6IPI41UP/KDGEK7rRPwGCNos8k=
github.com/caarlos0/env/v6 v6.9.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc=
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@ -226,7 +221,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@ -248,7 +242,6 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
@ -307,6 +300,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -420,6 +415,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg=
github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -465,8 +462,6 @@ github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThC
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
@ -505,11 +500,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/studio-b12/gowebdav v0.0.0-20211109083228-3f8721cd4b6f h1:L2NE7BXnSlSLoNYZ0lCwZDjdnYjCNYC71k9ClZUTFTs=
github.com/studio-b12/gowebdav v0.0.0-20211109083228-3f8721cd4b6f/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
@ -556,11 +548,12 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -595,7 +588,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -644,13 +636,14 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70 h1:SeSEfdIxyvwGJliREIJhRPPXvW6sDlLT+UQ3B0hD0NA=
golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY=
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -660,9 +653,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -50,6 +50,13 @@ type Account struct {
CustomHost string `json:"custom_host"`
ExtractFolder string `json:"extract_folder"`
Bool1 bool `json:"bool_1"`
// for xunlei
Algorithms string `json:"algorithms"`
ClientVersion string `json:"client_version"`
PackageName string `json:"package_name"`
UserAgent string `json:"user_agent"`
CaptchaToken string `json:"captcha_token"`
DeviceId string `json:"device_id"`
}
var accountsMap = make(map[string]Account)
@ -207,6 +214,9 @@ func GetAccountsByPath(path string) []Account {
if bIndex != -1 {
name = name[:bIndex]
}
if name == "/" {
name = ""
}
// 不是这个账号
if path != name && !strings.HasPrefix(path, name+"/") {
continue
@ -253,6 +263,9 @@ func GetAccountFilesByPath(prefix string) []File {
continue
}
full := utils.ParsePath(v.Name)
if len(full) <= len(prefix) {
continue
}
// 不是以prefix为前缀
if !strings.HasPrefix(full, prefix+"/") && prefix != "/" {
continue

View File

@ -56,7 +56,7 @@ func ExtractFolder(files []File, account *Account) {
return
}
front := account.ExtractFolder == "front"
sort.Slice(files, func(i, j int) bool {
sort.SliceStable(files, func(i, j int) bool {
if files[i].IsDir() || files[j].IsDir() {
if !files[i].IsDir() {
return !front
@ -84,3 +84,7 @@ func (f File) ModTime() time.Time {
func (f File) IsDir() bool {
return f.Type == conf.FOLDER
}
func (f File) GetType() int {
return f.Type
}

View File

@ -12,6 +12,7 @@ type Meta struct {
Hide string `json:"hide"`
Upload bool `json:"upload"`
OnlyShows string `json:"only_shows"`
Readme string `json:"readme"`
}
func GetMetaByPath(path string) (*Meta, error) {

35
model/search_file.go Normal file
View File

@ -0,0 +1,35 @@
package model
import (
"fmt"
"github.com/Xhofe/alist/conf"
)
type ISearchFile interface {
GetName() string
GetSize() uint64
GetType() int
}
type SearchFile struct {
Path string `json:"path" gorm:"index"`
Name string `json:"name"`
Size uint64 `json:"size"`
Type int `json:"type"`
}
func CreateSearchFiles(files []SearchFile) error {
return conf.DB.Create(files).Error
}
func DeleteSearchFilesByPath(path string) error {
return conf.DB.Where(fmt.Sprintf("%s = ?", columnName("path")), path).Delete(&SearchFile{}).Error
}
func SearchByNameAndPath(path, keyword string) ([]SearchFile, error) {
var files []SearchFile
if err := conf.DB.Where(fmt.Sprintf("%s LIKE ? AND %s LIKE ?", columnName("path"), columnName("name")), fmt.Sprintf("%s%%", path), fmt.Sprintf("%%%s%%", keyword)).Find(&files).Error; err != nil {
return nil, err
}
return files, nil
}

21
pkg/gowebdav/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Folders to ignore
/src
/bin
/pkg
/gowebdav
/.idea
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
.vscode/

10
pkg/gowebdav/.travis.yml Normal file
View File

@ -0,0 +1,10 @@
language: go
go:
- "1.x"
install:
- go get ./...
script:
- go test -v --short ./...

27
pkg/gowebdav/LICENSE Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2014, Studio B12 GmbH
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

33
pkg/gowebdav/Makefile Normal file
View File

@ -0,0 +1,33 @@
BIN := gowebdav
SRC := $(wildcard *.go) cmd/gowebdav/main.go
all: test cmd
cmd: ${BIN}
${BIN}: ${SRC}
go build -o $@ ./cmd/gowebdav
test:
go test -v --short ./...
api:
@sed '/^## API$$/,$$d' -i README.md
@echo '## API' >> README.md
@godoc2md github.com/studio-b12/gowebdav | sed '/^$$/N;/^\n$$/D' |\
sed '2d' |\
sed 's/\/src\/github.com\/studio-b12\/gowebdav\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\
sed 's/\/src\/target\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\
sed 's/^#/##/g' >> README.md
check:
gofmt -w -s $(SRC)
@echo
gocyclo -over 15 .
@echo
golint ./...
clean:
@rm -f ${BIN}
.PHONY: all cmd clean test api check

564
pkg/gowebdav/README.md Normal file
View File

@ -0,0 +1,564 @@
# GoWebDAV
[![Build Status](https://travis-ci.org/studio-b12/gowebdav.svg?branch=master)](https://travis-ci.org/studio-b12/gowebdav)
[![GoDoc](https://godoc.org/github.com/studio-b12/gowebdav?status.svg)](https://godoc.org/github.com/studio-b12/gowebdav)
[![Go Report Card](https://goreportcard.com/badge/github.com/studio-b12/gowebdav)](https://goreportcard.com/report/github.com/studio-b12/gowebdav)
A golang WebDAV client library.
## Main features
`gowebdav` library allows to perform following actions on the remote WebDAV server:
* [create path](#create-path-on-a-webdav-server)
* [get files list](#get-files-list)
* [download file](#download-file-to-byte-array)
* [upload file](#upload-file-from-byte-array)
* [get information about specified file/folder](#get-information-about-specified-filefolder)
* [move file to another location](#move-file-to-another-location)
* [copy file to another location](#copy-file-to-another-location)
* [delete file](#delete-file)
## Usage
First of all you should create `Client` instance using `NewClient()` function:
```go
root := "https://webdav.mydomain.me"
user := "user"
password := "password"
c := gowebdav.NewClient(root, user, password)
```
After you can use this `Client` to perform actions, described below.
**NOTICE:** we will not check errors in examples, to focus you on the `gowebdav` library's code, but you should do it in your code!
### Create path on a WebDAV server
```go
err := c.Mkdir("folder", 0644)
```
In case you want to create several folders you can use `c.MkdirAll()`:
```go
err := c.MkdirAll("folder/subfolder/subfolder2", 0644)
```
### Get files list
```go
files, _ := c.ReadDir("folder/subfolder")
for _, file := range files {
//notice that [file] has os.FileInfo type
fmt.Println(file.Name())
}
```
### Download file to byte array
```go
webdavFilePath := "folder/subfolder/file.txt"
localFilePath := "/tmp/webdav/file.txt"
bytes, _ := c.Read(webdavFilePath)
ioutil.WriteFile(localFilePath, bytes, 0644)
```
### Download file via reader
Also you can use `c.ReadStream()` method:
```go
webdavFilePath := "folder/subfolder/file.txt"
localFilePath := "/tmp/webdav/file.txt"
reader, _ := c.ReadStream(webdavFilePath)
file, _ := os.Create(localFilePath)
defer file.Close()
io.Copy(file, reader)
```
### Upload file from byte array
```go
webdavFilePath := "folder/subfolder/file.txt"
localFilePath := "/tmp/webdav/file.txt"
bytes, _ := ioutil.ReadFile(localFilePath)
c.Write(webdavFilePath, bytes, 0644)
```
### Upload file via writer
```go
webdavFilePath := "folder/subfolder/file.txt"
localFilePath := "/tmp/webdav/file.txt"
file, _ := os.Open(localFilePath)
defer file.Close()
c.WriteStream(webdavFilePath, file, 0644)
```
### Get information about specified file/folder
```go
webdavFilePath := "folder/subfolder/file.txt"
info := c.Stat(webdavFilePath)
//notice that [info] has os.FileInfo type
fmt.Println(info)
```
### Move file to another location
```go
oldPath := "folder/subfolder/file.txt"
newPath := "folder/subfolder/moved.txt"
isOverwrite := true
c.Rename(oldPath, newPath, isOverwrite)
```
### Copy file to another location
```go
oldPath := "folder/subfolder/file.txt"
newPath := "folder/subfolder/file-copy.txt"
isOverwrite := true
c.Copy(oldPath, newPath, isOverwrite)
```
### Delete file
```go
webdavFilePath := "folder/subfolder/file.txt"
c.Remove(webdavFilePath)
```
## Links
More details about WebDAV server you can read from following resources:
* [RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)](https://tools.ietf.org/html/rfc4918)
* [RFC 5689 - Extended MKCOL for Web Distributed Authoring and Versioning (WebDAV)](https://tools.ietf.org/html/rfc5689)
* [RFC 2616 - HTTP/1.1 Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html "HTTP/1.1 Status Code Definitions")
* [WebDav: Next Generation Collaborative Web Authoring By Lisa Dusseaul](https://books.google.de/books?isbn=0130652083 "WebDav: Next Generation Collaborative Web Authoring By Lisa Dusseault")
**NOTICE**: RFC 2518 is obsoleted by RFC 4918 in June 2007
## Contributing
All contributing are welcome. If you have any suggestions or find some bug - please create an Issue to let us make this project better. We appreciate your help!
## License
This library is distributed under the BSD 3-Clause license found in the [LICENSE](https://github.com/studio-b12/gowebdav/blob/master/LICENSE) file.
## API
`import "github.com/studio-b12/gowebdav"`
* [Overview](#pkg-overview)
* [Index](#pkg-index)
* [Examples](#pkg-examples)
* [Subdirectories](#pkg-subdirectories)
### <a name="pkg-overview">Overview</a>
Package gowebdav is a WebDAV client library with a command line tool
included.
### <a name="pkg-index">Index</a>
* [func FixSlash(s string) string](#FixSlash)
* [func FixSlashes(s string) string](#FixSlashes)
* [func Join(path0 string, path1 string) string](#Join)
* [func PathEscape(path string) string](#PathEscape)
* [func ReadConfig(uri, netrc string) (string, string)](#ReadConfig)
* [func String(r io.Reader) string](#String)
* [type Authenticator](#Authenticator)
* [type BasicAuth](#BasicAuth)
* [func (b *BasicAuth) Authorize(req *http.Request, method string, path string)](#BasicAuth.Authorize)
* [func (b *BasicAuth) Pass() string](#BasicAuth.Pass)
* [func (b *BasicAuth) Type() string](#BasicAuth.Type)
* [func (b *BasicAuth) User() string](#BasicAuth.User)
* [type Client](#Client)
* [func NewClient(uri, user, pw string) *Client](#NewClient)
* [func (c *Client) Connect() error](#Client.Connect)
* [func (c *Client) Copy(oldpath, newpath string, overwrite bool) error](#Client.Copy)
* [func (c *Client) Mkdir(path string, _ os.FileMode) error](#Client.Mkdir)
* [func (c *Client) MkdirAll(path string, _ os.FileMode) error](#Client.MkdirAll)
* [func (c *Client) Read(path string) ([]byte, error)](#Client.Read)
* [func (c *Client) ReadDir(path string) ([]os.FileInfo, error)](#Client.ReadDir)
* [func (c *Client) ReadStream(path string) (io.ReadCloser, error)](#Client.ReadStream)
* [func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error)](#Client.ReadStreamRange)
* [func (c *Client) Remove(path string) error](#Client.Remove)
* [func (c *Client) RemoveAll(path string) error](#Client.RemoveAll)
* [func (c *Client) Rename(oldpath, newpath string, overwrite bool) error](#Client.Rename)
* [func (c *Client) SetHeader(key, value string)](#Client.SetHeader)
* [func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))](#Client.SetInterceptor)
* [func (c *Client) SetTimeout(timeout time.Duration)](#Client.SetTimeout)
* [func (c *Client) SetTransport(transport http.RoundTripper)](#Client.SetTransport)
* [func (c *Client) Stat(path string) (os.FileInfo, error)](#Client.Stat)
* [func (c *Client) Write(path string, data []byte, _ os.FileMode) error](#Client.Write)
* [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error](#Client.WriteStream)
* [type DigestAuth](#DigestAuth)
* [func (d *DigestAuth) Authorize(req *http.Request, method string, path string)](#DigestAuth.Authorize)
* [func (d *DigestAuth) Pass() string](#DigestAuth.Pass)
* [func (d *DigestAuth) Type() string](#DigestAuth.Type)
* [func (d *DigestAuth) User() string](#DigestAuth.User)
* [type File](#File)
* [func (f File) ContentType() string](#File.ContentType)
* [func (f File) ETag() string](#File.ETag)
* [func (f File) IsDir() bool](#File.IsDir)
* [func (f File) ModTime() time.Time](#File.ModTime)
* [func (f File) Mode() os.FileMode](#File.Mode)
* [func (f File) Name() string](#File.Name)
* [func (f File) Path() string](#File.Path)
* [func (f File) Size() int64](#File.Size)
* [func (f File) String() string](#File.String)
* [func (f File) Sys() interface{}](#File.Sys)
* [type NoAuth](#NoAuth)
* [func (n *NoAuth) Authorize(req *http.Request, method string, path string)](#NoAuth.Authorize)
* [func (n *NoAuth) Pass() string](#NoAuth.Pass)
* [func (n *NoAuth) Type() string](#NoAuth.Type)
* [func (n *NoAuth) User() string](#NoAuth.User)
##### <a name="pkg-examples">Examples</a>
* [PathEscape](#example_PathEscape)
##### <a name="pkg-files">Package files</a>
[basicAuth.go](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go) [client.go](https://github.com/studio-b12/gowebdav/blob/master/client.go) [digestAuth.go](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go) [doc.go](https://github.com/studio-b12/gowebdav/blob/master/doc.go) [file.go](https://github.com/studio-b12/gowebdav/blob/master/file.go) [netrc.go](https://github.com/studio-b12/gowebdav/blob/master/netrc.go) [requests.go](https://github.com/studio-b12/gowebdav/blob/master/requests.go) [utils.go](https://github.com/studio-b12/gowebdav/blob/master/utils.go)
### <a name="FixSlash">func</a> [FixSlash](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=707:737#L45)
``` go
func FixSlash(s string) string
```
FixSlash appends a trailing / to our string
### <a name="FixSlashes">func</a> [FixSlashes](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=859:891#L53)
``` go
func FixSlashes(s string) string
```
FixSlashes appends and prepends a / if they are missing
### <a name="Join">func</a> [Join](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=992:1036#L62)
``` go
func Join(path0 string, path1 string) string
```
Join joins two paths
### <a name="PathEscape">func</a> [PathEscape](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=506:541#L36)
``` go
func PathEscape(path string) string
```
PathEscape escapes all segments of a given path
### <a name="ReadConfig">func</a> [ReadConfig](https://github.com/studio-b12/gowebdav/blob/master/netrc.go?s=428:479#L27)
``` go
func ReadConfig(uri, netrc string) (string, string)
```
ReadConfig reads login and password configuration from ~/.netrc
machine foo.com login username password 123456
### <a name="String">func</a> [String](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=1166:1197#L67)
``` go
func String(r io.Reader) string
```
String pulls a string out of our io.Reader
### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=388:507#L29)
``` go
type Authenticator interface {
Type() string
User() string
Pass() string
Authorize(*http.Request, string, string)
}
```
Authenticator stub
### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=106:157#L9)
``` go
type BasicAuth struct {
// contains filtered or unexported fields
}
```
BasicAuth structure holds our credentials
#### <a name="BasicAuth.Authorize">func</a> (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=473:549#L30)
``` go
func (b *BasicAuth) Authorize(req *http.Request, method string, path string)
```
Authorize the current request
#### <a name="BasicAuth.Pass">func</a> (\*BasicAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=388:421#L25)
``` go
func (b *BasicAuth) Pass() string
```
Pass holds the BasicAuth password
#### <a name="BasicAuth.Type">func</a> (\*BasicAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=201:234#L15)
``` go
func (b *BasicAuth) Type() string
```
Type identifies the BasicAuthenticator
#### <a name="BasicAuth.User">func</a> (\*BasicAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=297:330#L20)
``` go
func (b *BasicAuth) User() string
```
User holds the BasicAuth username
### <a name="Client">type</a> [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=172:364#L18)
``` go
type Client struct {
// contains filtered or unexported fields
}
```
Client defines our structure
#### <a name="NewClient">func</a> [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1019:1063#L62)
``` go
func NewClient(uri, user, pw string) *Client
```
NewClient creates a new instance of client
#### <a name="Client.Connect">func</a> (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1843:1875#L87)
``` go
func (c *Client) Connect() error
```
Connect connects to our dav server
#### <a name="Client.Copy">func</a> (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6702:6770#L313)
``` go
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error
```
Copy copies a file from A to B
#### <a name="Client.Mkdir">func</a> (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5793:5849#L272)
``` go
func (c *Client) Mkdir(path string, _ os.FileMode) error
```
Mkdir makes a directory
#### <a name="Client.MkdirAll">func</a> (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6028:6087#L283)
``` go
func (c *Client) MkdirAll(path string, _ os.FileMode) error
```
MkdirAll like mkdir -p, but for webdav
#### <a name="Client.Read">func</a> (\*Client) [Read](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6876:6926#L318)
``` go
func (c *Client) Read(path string) ([]byte, error)
```
Read reads the contents of a remote file
#### <a name="Client.ReadDir">func</a> (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=2869:2929#L130)
``` go
func (c *Client) ReadDir(path string) ([]os.FileInfo, error)
```
ReadDir reads the contents of a remote directory
#### <a name="Client.ReadStream">func</a> (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7237:7300#L336)
``` go
func (c *Client) ReadStream(path string) (io.ReadCloser, error)
```
ReadStream reads the stream for a given path
#### <a name="Client.ReadStreamRange">func</a> (\*Client) [ReadStreamRange](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8049:8139#L358)
``` go
func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error)
```
ReadStreamRange reads the stream representing a subset of bytes for a given path,
utilizing HTTP Range Requests if the server supports it.
The range is expressed as offset from the start of the file and length, for example
offset=10, length=10 will return bytes 10 through 19.
If the server does not support partial content requests and returns full content instead,
this function will emulate the behavior by skipping `offset` bytes and limiting the result
to `length`.
#### <a name="Client.Remove">func</a> (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5299:5341#L249)
``` go
func (c *Client) Remove(path string) error
```
Remove removes a remote file
#### <a name="Client.RemoveAll">func</a> (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5407:5452#L254)
``` go
func (c *Client) RemoveAll(path string) error
```
RemoveAll removes remote files
#### <a name="Client.Rename">func</a> (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6536:6606#L308)
``` go
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error
```
Rename moves a file from A to B
#### <a name="Client.SetHeader">func</a> (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1235:1280#L67)
``` go
func (c *Client) SetHeader(key, value string)
```
SetHeader lets us set arbitrary headers for a given client
#### <a name="Client.SetInterceptor">func</a> (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1387:1469#L72)
``` go
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))
```
SetInterceptor lets us set an arbitrary interceptor for a given client
#### <a name="Client.SetTimeout">func</a> (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1571:1621#L77)
``` go
func (c *Client) SetTimeout(timeout time.Duration)
```
SetTimeout exposes the ability to set a time limit for requests
#### <a name="Client.SetTransport">func</a> (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1714:1772#L82)
``` go
func (c *Client) SetTransport(transport http.RoundTripper)
```
SetTransport exposes the ability to define custom transports
#### <a name="Client.Stat">func</a> (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4255:4310#L197)
``` go
func (c *Client) Stat(path string) (os.FileInfo, error)
```
Stat returns the file stats for a specified path
#### <a name="Client.Write">func</a> (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9051:9120#L388)
``` go
func (c *Client) Write(path string, data []byte, _ os.FileMode) error
```
Write writes data to a given path
#### <a name="Client.WriteStream">func</a> (\*Client) [WriteStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9476:9556#L411)
``` go
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error
```
WriteStream writes a stream
### <a name="DigestAuth">type</a> [DigestAuth](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=157:254#L14)
``` go
type DigestAuth struct {
// contains filtered or unexported fields
}
```
DigestAuth structure holds our credentials
#### <a name="DigestAuth.Authorize">func</a> (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=577:654#L36)
``` go
func (d *DigestAuth) Authorize(req *http.Request, method string, path string)
```
Authorize the current request
#### <a name="DigestAuth.Pass">func</a> (\*DigestAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=491:525#L31)
``` go
func (d *DigestAuth) Pass() string
```
Pass holds the DigestAuth password
#### <a name="DigestAuth.Type">func</a> (\*DigestAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=299:333#L21)
``` go
func (d *DigestAuth) Type() string
```
Type identifies the DigestAuthenticator
#### <a name="DigestAuth.User">func</a> (\*DigestAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=398:432#L26)
``` go
func (d *DigestAuth) User() string
```
User holds the DigestAuth username
### <a name="File">type</a> [File](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=93:253#L10)
``` go
type File struct {
// contains filtered or unexported fields
}
```
File is our structure for a given file
#### <a name="File.ContentType">func</a> (File) [ContentType](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=476:510#L31)
``` go
func (f File) ContentType() string
```
ContentType returns the content type of a file
#### <a name="File.ETag">func</a> (File) [ETag](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=929:956#L56)
``` go
func (f File) ETag() string
```
ETag returns the ETag of a file
#### <a name="File.IsDir">func</a> (File) [IsDir](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1035:1061#L61)
``` go
func (f File) IsDir() bool
```
IsDir let us see if a given file is a directory or not
#### <a name="File.ModTime">func</a> (File) [ModTime](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=836:869#L51)
``` go
func (f File) ModTime() time.Time
```
ModTime returns the modified time of a file
#### <a name="File.Mode">func</a> (File) [Mode](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=665:697#L41)
``` go
func (f File) Mode() os.FileMode
```
Mode will return the mode of a given file
#### <a name="File.Name">func</a> (File) [Name](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=378:405#L26)
``` go
func (f File) Name() string
```
Name returns the name of a file
#### <a name="File.Path">func</a> (File) [Path](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=295:322#L21)
``` go
func (f File) Path() string
```
Path returns the full path of a file
#### <a name="File.Size">func</a> (File) [Size](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=573:599#L36)
``` go
func (f File) Size() int64
```
Size returns the size of a file
#### <a name="File.String">func</a> (File) [String](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1183:1212#L71)
``` go
func (f File) String() string
```
String lets us see file information
#### <a name="File.Sys">func</a> (File) [Sys](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1095:1126#L66)
``` go
func (f File) Sys() interface{}
```
Sys ????
### <a name="NoAuth">type</a> [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=551:599#L37)
``` go
type NoAuth struct {
// contains filtered or unexported fields
}
```
NoAuth structure holds our credentials
#### <a name="NoAuth.Authorize">func</a> (\*NoAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=894:967#L58)
``` go
func (n *NoAuth) Authorize(req *http.Request, method string, path string)
```
Authorize the current request
#### <a name="NoAuth.Pass">func</a> (\*NoAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=812:842#L53)
``` go
func (n *NoAuth) Pass() string
```
Pass returns the current password
#### <a name="NoAuth.Type">func</a> (\*NoAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=638:668#L43)
``` go
func (n *NoAuth) Type() string
```
Type identifies the authenticator
#### <a name="NoAuth.User">func</a> (\*NoAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=724:754#L48)
``` go
func (n *NoAuth) User() string
```
User returns the current user
- - -
Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)

34
pkg/gowebdav/basicAuth.go Normal file
View File

@ -0,0 +1,34 @@
package gowebdav
import (
"encoding/base64"
"net/http"
)
// BasicAuth structure holds our credentials
type BasicAuth struct {
user string
pw string
}
// Type identifies the BasicAuthenticator
func (b *BasicAuth) Type() string {
return "BasicAuth"
}
// User holds the BasicAuth username
func (b *BasicAuth) User() string {
return b.user
}
// Pass holds the BasicAuth password
func (b *BasicAuth) Pass() string {
return b.pw
}
// Authorize the current request
func (b *BasicAuth) Authorize(req *http.Request, method string, path string) {
a := b.user + ":" + b.pw
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a))
req.Header.Set("Authorization", auth)
}

447
pkg/gowebdav/client.go Normal file
View File

@ -0,0 +1,447 @@
package gowebdav
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"os"
pathpkg "path"
"strings"
"sync"
"time"
)
// Client defines our structure
type Client struct {
root string
headers http.Header
interceptor func(method string, rq *http.Request)
c *http.Client
authMutex sync.Mutex
auth Authenticator
}
// Authenticator stub
type Authenticator interface {
Type() string
User() string
Pass() string
Authorize(*http.Request, string, string)
}
// NoAuth structure holds our credentials
type NoAuth struct {
user string
pw string
}
// Type identifies the authenticator
func (n *NoAuth) Type() string {
return "NoAuth"
}
// User returns the current user
func (n *NoAuth) User() string {
return n.user
}
// Pass returns the current password
func (n *NoAuth) Pass() string {
return n.pw
}
// Authorize the current request
func (n *NoAuth) Authorize(req *http.Request, method string, path string) {
}
// NewClient creates a new instance of client
func NewClient(uri, user, pw string) *Client {
return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{user, pw}}
}
// SetHeader lets us set arbitrary headers for a given client
func (c *Client) SetHeader(key, value string) {
c.headers.Add(key, value)
}
// SetInterceptor lets us set an arbitrary interceptor for a given client
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) {
c.interceptor = interceptor
}
// SetTimeout exposes the ability to set a time limit for requests
func (c *Client) SetTimeout(timeout time.Duration) {
c.c.Timeout = timeout
}
// SetTransport exposes the ability to define custom transports
func (c *Client) SetTransport(transport http.RoundTripper) {
c.c.Transport = transport
}
// Connect connects to our dav server
func (c *Client) Connect() error {
rs, err := c.options("/")
if err != nil {
return err
}
err = rs.Body.Close()
if err != nil {
return err
}
if rs.StatusCode != 200 {
return newPathError("Connect", c.root, rs.StatusCode)
}
return nil
}
type props struct {
Status string `xml:"DAV: status"`
Name string `xml:"DAV: prop>displayname,omitempty"`
Type xml.Name `xml:"DAV: prop>resourcetype>collection,omitempty"`
Size string `xml:"DAV: prop>getcontentlength,omitempty"`
ContentType string `xml:"DAV: prop>getcontenttype,omitempty"`
ETag string `xml:"DAV: prop>getetag,omitempty"`
Modified string `xml:"DAV: prop>getlastmodified,omitempty"`
}
type response struct {
Href string `xml:"DAV: href"`
Props []props `xml:"DAV: propstat"`
}
func getProps(r *response, status string) *props {
for _, prop := range r.Props {
if strings.Contains(prop.Status, status) {
return &prop
}
}
return nil
}
// ReadDir reads the contents of a remote directory
func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
path = FixSlashes(path)
files := make([]os.FileInfo, 0)
skipSelf := true
parse := func(resp interface{}) error {
r := resp.(*response)
if skipSelf {
skipSelf = false
if p := getProps(r, "200"); p != nil && p.Type.Local == "collection" {
r.Props = nil
return nil
}
return newPathError("ReadDir", path, 405)
}
if p := getProps(r, "200"); p != nil {
f := new(File)
if ps, err := url.PathUnescape(r.Href); err == nil {
f.name = pathpkg.Base(ps)
} else {
f.name = p.Name
}
f.path = path + f.name
f.modified = parseModified(&p.Modified)
f.etag = p.ETag
f.contentType = p.ContentType
if p.Type.Local == "collection" {
f.path += "/"
f.size = 0
f.isdir = true
} else {
f.size = parseInt64(&p.Size)
f.isdir = false
}
files = append(files, *f)
}
r.Props = nil
return nil
}
err := c.propfind(path, false,
`<d:propfind xmlns:d='DAV:'>
<d:prop>
<d:displayname/>
<d:resourcetype/>
<d:getcontentlength/>
<d:getcontenttype/>
<d:getetag/>
<d:getlastmodified/>
</d:prop>
</d:propfind>`,
&response{},
parse)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
err = newPathErrorErr("ReadDir", path, err)
}
}
return files, err
}
// Stat returns the file stats for a specified path
func (c *Client) Stat(path string) (os.FileInfo, error) {
var f *File
parse := func(resp interface{}) error {
r := resp.(*response)
if p := getProps(r, "200"); p != nil && f == nil {
f = new(File)
f.name = p.Name
f.path = path
f.etag = p.ETag
f.contentType = p.ContentType
if p.Type.Local == "collection" {
if !strings.HasSuffix(f.path, "/") {
f.path += "/"
}
f.size = 0
f.modified = time.Unix(0, 0)
f.isdir = true
} else {
f.size = parseInt64(&p.Size)
f.modified = parseModified(&p.Modified)
f.isdir = false
}
}
r.Props = nil
return nil
}
err := c.propfind(path, true,
`<d:propfind xmlns:d='DAV:'>
<d:prop>
<d:displayname/>
<d:resourcetype/>
<d:getcontentlength/>
<d:getcontenttype/>
<d:getetag/>
<d:getlastmodified/>
</d:prop>
</d:propfind>`,
&response{},
parse)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
err = newPathErrorErr("ReadDir", path, err)
}
}
return f, err
}
// Remove removes a remote file
func (c *Client) Remove(path string) error {
return c.RemoveAll(path)
}
// RemoveAll removes remote files
func (c *Client) RemoveAll(path string) error {
rs, err := c.req("DELETE", path, nil, nil)
if err != nil {
return newPathError("Remove", path, 400)
}
err = rs.Body.Close()
if err != nil {
return err
}
if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 {
return nil
}
return newPathError("Remove", path, rs.StatusCode)
}
// Mkdir makes a directory
func (c *Client) Mkdir(path string, _ os.FileMode) (err error) {
path = FixSlashes(path)
status, err := c.mkcol(path)
if err != nil {
return
}
if status == 201 {
return nil
}
return newPathError("Mkdir", path, status)
}
// MkdirAll like mkdir -p, but for webdav
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) {
path = FixSlashes(path)
status, err := c.mkcol(path)
if err != nil {
return
}
if status == 201 {
return nil
}
if status == 409 {
paths := strings.Split(path, "/")
sub := "/"
for _, e := range paths {
if e == "" {
continue
}
sub += e + "/"
status, err = c.mkcol(sub)
if err != nil {
return
}
if status != 201 {
return newPathError("MkdirAll", sub, status)
}
}
return nil
}
return newPathError("MkdirAll", path, status)
}
// Rename moves a file from A to B
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error {
return c.copymove("MOVE", oldpath, newpath, overwrite)
}
// Copy copies a file from A to B
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error {
return c.copymove("COPY", oldpath, newpath, overwrite)
}
// Read reads the contents of a remote file
func (c *Client) Read(path string) ([]byte, error) {
var stream io.ReadCloser
var err error
if stream, _, err = c.ReadStream(path, nil); err != nil {
return nil, err
}
defer stream.Close()
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(stream)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// ReadStream reads the stream for a given path
func (c *Client) ReadStream(path string, callback func(rq *http.Request)) (io.ReadCloser, http.Header, error) {
rs, err := c.req("GET", path, nil, callback)
if err != nil {
return nil, nil, newPathErrorErr("ReadStream", path, err)
}
if rs.StatusCode < 400 {
return rs.Body, rs.Header, nil
}
rs.Body.Close()
return nil, nil, newPathError("ReadStream", path, rs.StatusCode)
}
// ReadStreamRange reads the stream representing a subset of bytes for a given path,
// utilizing HTTP Range Requests if the server supports it.
// The range is expressed as offset from the start of the file and length, for example
// offset=10, length=10 will return bytes 10 through 19.
//
// If the server does not support partial content requests and returns full content instead,
// this function will emulate the behavior by skipping `offset` bytes and limiting the result
// to `length`.
func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) {
rs, err := c.req("GET", path, nil, func(r *http.Request) {
r.Header.Add("Range", fmt.Sprintf("bytes=%v-%v", offset, offset+length-1))
})
if err != nil {
return nil, newPathErrorErr("ReadStreamRange", path, err)
}
if rs.StatusCode == http.StatusPartialContent {
// server supported partial content, return as-is.
return rs.Body, nil
}
// server returned success, but did not support partial content, so we have the whole
// stream in rs.Body
if rs.StatusCode == 200 {
// discard first 'offset' bytes.
if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil {
return nil, newPathErrorErr("ReadStreamRange", path, err)
}
// return a io.ReadCloser that is limited to `length` bytes.
return &limitedReadCloser{rs.Body, int(length)}, nil
}
rs.Body.Close()
return nil, newPathError("ReadStream", path, rs.StatusCode)
}
// Write writes data to a given path
func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) {
s, err := c.put(path, bytes.NewReader(data), nil)
if err != nil {
return
}
switch s {
case 200, 201, 204:
return nil
case 409:
err = c.createParentCollection(path)
if err != nil {
return
}
s, err = c.put(path, bytes.NewReader(data), nil)
if err != nil {
return
}
if s == 200 || s == 201 || s == 204 {
return
}
}
return newPathError("Write", path, s)
}
// WriteStream writes a stream
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode, callback func(r *http.Request)) (err error) {
err = c.createParentCollection(path)
if err != nil {
return err
}
s, err := c.put(path, stream, callback)
if err != nil {
return err
}
switch s {
case 200, 201, 204:
return nil
default:
return newPathError("WriteStream", path, s)
}
}

View File

@ -0,0 +1,103 @@
# Description
Command line tool for [gowebdav](https://github.com/studio-b12/gowebdav) library.
# Prerequisites
## Software
* **OS**: all, which are supported by `Golang`
* **Golang**: version 1.x
* **Git**: version 2.14.2 at higher (required to install via `go get`)
# Install
```sh
go get -u github.com/studio-b12/gowebdav/cmd/gowebdav
```
# Usage
It is recommended to set following environment variables to improve your experience with this tool:
* `ROOT` is an URL of target WebDAV server (e.g. `https://webdav.mydomain.me/user_root_folder`)
* `USER` is a login to connect to specified server (e.g. `user`)
* `PASSWORD` is a password to connect to specified server (e.g. `p@s$w0rD`)
In following examples we suppose that:
* environment variable `ROOT` is set to `https://webdav.mydomain.me/ufolder`
* environment variable `USER` is set to `user`
* environment variable `PASSWORD` is set `p@s$w0rD`
* folder `/ufolder/temp` exists on the server
* file `/ufolder/temp/file.txt` exists on the server
* file `/ufolder/temp/document.rtf` exists on the server
* file `/tmp/webdav/to_upload.txt` exists on the local machine
* folder `/tmp/webdav/` is used to download files from the server
## Examples
#### Get content of specified folder
```sh
gowebdav -X LS temp
```
#### Get info about file/folder
```sh
gowebdav -X STAT temp
gowebdav -X STAT temp/file.txt
```
#### Create folder on the remote server
```sh
gowebdav -X MKDIR temp2
gowebdav -X MKDIRALL all/folders/which-you-want/to_create
```
#### Download file
```sh
gowebdav -X GET temp/document.rtf /tmp/webdav/document.rtf
```
You may do not specify target local path, in this case file will be downloaded to the current folder with the
#### Upload file
```sh
gowebdav -X PUT temp/uploaded.txt /tmp/webdav/to_upload.txt
```
#### Move file on the remote server
```sh
gowebdav -X MV temp/file.txt temp/moved_file.txt
```
#### Copy file to another location
```sh
gowebdav -X MV temp/file.txt temp/file-copy.txt
```
#### Delete file from the remote server
```sh
gowebdav -X DEL temp/file.txt
```
# Wrapper script
You can create wrapper script for your server (via `$EDITOR ./dav && chmod a+x ./dav`) and add following content to it:
```sh
#!/bin/sh
ROOT="https://my.dav.server/" \
USER="foo" \
PASSWORD="$(pass dav/foo@my.dav.server)" \
gowebdav $@
```
It allows you to use [pass](https://www.passwordstore.org/ "the standard unix password manager") or similar tools to retrieve the password.
## Examples
Using the `dav` wrapper:
```sh
$ ./dav -X LS /
$ echo hi dav! > hello && ./dav -X PUT /hello
$ ./dav -X STAT /hello
$ ./dav -X PUT /hello_dav hello
$ ./dav -X GET /hello_dav
$ ./dav -X GET /hello_dav hello.txt
```

View File

@ -0,0 +1,263 @@
package main
import (
"errors"
"flag"
"fmt"
"io"
"io/fs"
"os"
"os/user"
"path"
"path/filepath"
"runtime"
"strings"
d "github.com/Xhofe/alist/pkg/gowebdav"
)
func main() {
root := flag.String("root", os.Getenv("ROOT"), "WebDAV Endpoint [ENV.ROOT]")
user := flag.String("user", os.Getenv("USER"), "User [ENV.USER]")
password := flag.String("pw", os.Getenv("PASSWORD"), "Password [ENV.PASSWORD]")
netrc := flag.String("netrc-file", filepath.Join(getHome(), ".netrc"), "read login from netrc file")
method := flag.String("X", "", `Method:
LS <PATH>
STAT <PATH>
MKDIR <PATH>
MKDIRALL <PATH>
GET <PATH> [<FILE>]
PUT <PATH> [<FILE>]
MV <OLD> <NEW>
CP <OLD> <NEW>
DEL <PATH>
`)
flag.Parse()
if *root == "" {
fail("Set WebDAV ROOT")
}
if argsLength := len(flag.Args()); argsLength == 0 || argsLength > 2 {
fail("Unsupported arguments")
}
if *password == "" {
if u, p := d.ReadConfig(*root, *netrc); u != "" && p != "" {
user = &u
password = &p
}
}
c := d.NewClient(*root, *user, *password)
cmd := getCmd(*method)
if e := cmd(c, flag.Arg(0), flag.Arg(1)); e != nil {
fail(e)
}
}
func fail(err interface{}) {
if err != nil {
fmt.Println(err)
}
os.Exit(-1)
}
func getHome() string {
u, e := user.Current()
if e != nil {
return os.Getenv("HOME")
}
if u != nil {
return u.HomeDir
}
switch runtime.GOOS {
case "windows":
return ""
default:
return "~/"
}
}
func getCmd(method string) func(c *d.Client, p0, p1 string) error {
switch strings.ToUpper(method) {
case "LS", "LIST", "PROPFIND":
return cmdLs
case "STAT":
return cmdStat
case "GET", "PULL", "READ":
return cmdGet
case "DELETE", "RM", "DEL":
return cmdRm
case "MKCOL", "MKDIR":
return cmdMkdir
case "MKCOLALL", "MKDIRALL", "MKDIRP":
return cmdMkdirAll
case "RENAME", "MV", "MOVE":
return cmdMv
case "COPY", "CP":
return cmdCp
case "PUT", "PUSH", "WRITE":
return cmdPut
default:
return func(c *d.Client, p0, p1 string) (err error) {
return errors.New("Unsupported method: " + method)
}
}
}
func cmdLs(c *d.Client, p0, _ string) (err error) {
files, err := c.ReadDir(p0)
if err == nil {
fmt.Println(fmt.Sprintf("ReadDir: '%s' entries: %d ", p0, len(files)))
for _, f := range files {
fmt.Println(f)
}
}
return
}
func cmdStat(c *d.Client, p0, _ string) (err error) {
file, err := c.Stat(p0)
if err == nil {
fmt.Println(file)
}
return
}
func cmdGet(c *d.Client, p0, p1 string) (err error) {
bytes, err := c.Read(p0)
if err == nil {
if p1 == "" {
p1 = filepath.Join(".", p0)
}
err = writeFile(p1, bytes, 0644)
if err == nil {
fmt.Println(fmt.Sprintf("Written %d bytes to: %s", len(bytes), p1))
}
}
return
}
func cmdRm(c *d.Client, p0, _ string) (err error) {
if err = c.Remove(p0); err == nil {
fmt.Println("Remove: " + p0)
}
return
}
func cmdMkdir(c *d.Client, p0, _ string) (err error) {
if err = c.Mkdir(p0, 0755); err == nil {
fmt.Println("Mkdir: " + p0)
}
return
}
func cmdMkdirAll(c *d.Client, p0, _ string) (err error) {
if err = c.MkdirAll(p0, 0755); err == nil {
fmt.Println("MkdirAll: " + p0)
}
return
}
func cmdMv(c *d.Client, p0, p1 string) (err error) {
if err = c.Rename(p0, p1, true); err == nil {
fmt.Println("Rename: " + p0 + " -> " + p1)
}
return
}
func cmdCp(c *d.Client, p0, p1 string) (err error) {
if err = c.Copy(p0, p1, true); err == nil {
fmt.Println("Copy: " + p0 + " -> " + p1)
}
return
}
func cmdPut(c *d.Client, p0, p1 string) (err error) {
if p1 == "" {
p1 = path.Join(".", p0)
} else {
var fi fs.FileInfo
fi, err = c.Stat(p0)
if err != nil && !d.IsErrNotFound(err) {
return
}
if !d.IsErrNotFound(err) && fi.IsDir() {
p0 = path.Join(p0, p1)
}
}
stream, err := getStream(p1)
if err != nil {
return
}
defer stream.Close()
if err = c.WriteStream(p0, stream, 0644, nil); err == nil {
fmt.Println("Put: " + p1 + " -> " + p0)
}
return
}
func writeFile(path string, bytes []byte, mode os.FileMode) error {
parent := filepath.Dir(path)
if _, e := os.Stat(parent); os.IsNotExist(e) {
if e := os.MkdirAll(parent, os.ModePerm); e != nil {
return e
}
}
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(bytes)
return err
}
func getStream(pathOrString string) (io.ReadCloser, error) {
fi, err := os.Stat(pathOrString)
if err != nil {
return nil, err
}
if fi.IsDir() {
return nil, &os.PathError{
Op: "Open",
Path: pathOrString,
Err: errors.New("Path: '" + pathOrString + "' is a directory"),
}
}
f, err := os.Open(pathOrString)
if err == nil {
return f, nil
}
return nil, &os.PathError{
Op: "Open",
Path: pathOrString,
Err: err,
}
}

146
pkg/gowebdav/digestAuth.go Normal file
View File

@ -0,0 +1,146 @@
package gowebdav
import (
"crypto/md5"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net/http"
"strings"
)
// DigestAuth structure holds our credentials
type DigestAuth struct {
user string
pw string
digestParts map[string]string
}
// Type identifies the DigestAuthenticator
func (d *DigestAuth) Type() string {
return "DigestAuth"
}
// User holds the DigestAuth username
func (d *DigestAuth) User() string {
return d.user
}
// Pass holds the DigestAuth password
func (d *DigestAuth) Pass() string {
return d.pw
}
// Authorize the current request
func (d *DigestAuth) Authorize(req *http.Request, method string, path string) {
d.digestParts["uri"] = path
d.digestParts["method"] = method
d.digestParts["username"] = d.user
d.digestParts["password"] = d.pw
req.Header.Set("Authorization", getDigestAuthorization(d.digestParts))
}
func digestParts(resp *http.Response) map[string]string {
result := map[string]string{}
if len(resp.Header["Www-Authenticate"]) > 0 {
wantedHeaders := []string{"nonce", "realm", "qop", "opaque", "algorithm", "entityBody"}
responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",")
for _, r := range responseHeaders {
for _, w := range wantedHeaders {
if strings.Contains(r, w) {
result[w] = strings.Trim(
strings.SplitN(r, `=`, 2)[1],
`"`,
)
}
}
}
}
return result
}
func getMD5(text string) string {
hasher := md5.New()
hasher.Write([]byte(text))
return hex.EncodeToString(hasher.Sum(nil))
}
func getCnonce() string {
b := make([]byte, 8)
io.ReadFull(rand.Reader, b)
return fmt.Sprintf("%x", b)[:16]
}
func getDigestAuthorization(digestParts map[string]string) string {
d := digestParts
// These are the correct ha1 and ha2 for qop=auth. We should probably check for other types of qop.
var (
ha1 string
ha2 string
nonceCount = 00000001
cnonce = getCnonce()
response string
)
// 'ha1' value depends on value of "algorithm" field
switch d["algorithm"] {
case "MD5", "":
ha1 = getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"])
case "MD5-sess":
ha1 = getMD5(
fmt.Sprintf("%s:%v:%s",
getMD5(d["username"]+":"+d["realm"]+":"+d["password"]),
nonceCount,
cnonce,
),
)
}
// 'ha2' value depends on value of "qop" field
switch d["qop"] {
case "auth", "":
ha2 = getMD5(d["method"] + ":" + d["uri"])
case "auth-int":
if d["entityBody"] != "" {
ha2 = getMD5(d["method"] + ":" + d["uri"] + ":" + getMD5(d["entityBody"]))
}
}
// 'response' value depends on value of "qop" field
switch d["qop"] {
case "":
response = getMD5(
fmt.Sprintf("%s:%s:%s",
ha1,
d["nonce"],
ha2,
),
)
case "auth", "auth-int":
response = getMD5(
fmt.Sprintf("%s:%s:%v:%s:%s:%s",
ha1,
d["nonce"],
nonceCount,
cnonce,
d["qop"],
ha2,
),
)
}
authorization := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", nc=%v, cnonce="%s", response="%s"`,
d["username"], d["realm"], d["nonce"], d["uri"], nonceCount, cnonce, response)
if d["qop"] != "" {
authorization += fmt.Sprintf(`, qop=%s`, d["qop"])
}
if d["opaque"] != "" {
authorization += fmt.Sprintf(`, opaque="%s"`, d["opaque"])
}
return authorization
}

3
pkg/gowebdav/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package gowebdav is a WebDAV client library with a command line tool
// included.
package gowebdav

49
pkg/gowebdav/errors.go Normal file
View File

@ -0,0 +1,49 @@
package gowebdav
import (
"fmt"
"os"
)
// StatusError implements error and wraps
// an erroneous status code.
type StatusError struct {
Status int
}
func (se StatusError) Error() string {
return fmt.Sprintf("%d", se.Status)
}
// IsErrCode returns true if the given error
// is an os.PathError wrapping a StatusError
// with the given status code.
func IsErrCode(err error, code int) bool {
if pe, ok := err.(*os.PathError); ok {
se, ok := pe.Err.(StatusError)
return ok && se.Status == code
}
return false
}
// IsErrNotFound is shorthand for IsErrCode
// for status 404.
func IsErrNotFound(err error) bool {
return IsErrCode(err, 404)
}
func newPathError(op string, path string, statusCode int) error {
return &os.PathError{
Op: op,
Path: path,
Err: StatusError{statusCode},
}
}
func newPathErrorErr(op string, path string, err error) error {
return &os.PathError{
Op: op,
Path: path,
Err: err,
}
}

77
pkg/gowebdav/file.go Normal file
View File

@ -0,0 +1,77 @@
package gowebdav
import (
"fmt"
"os"
"time"
)
// File is our structure for a given file
type File struct {
path string
name string
contentType string
size int64
modified time.Time
etag string
isdir bool
}
// Path returns the full path of a file
func (f File) Path() string {
return f.path
}
// Name returns the name of a file
func (f File) Name() string {
return f.name
}
// ContentType returns the content type of a file
func (f File) ContentType() string {
return f.contentType
}
// Size returns the size of a file
func (f File) Size() int64 {
return f.size
}
// Mode will return the mode of a given file
func (f File) Mode() os.FileMode {
// TODO check webdav perms
if f.isdir {
return 0775 | os.ModeDir
}
return 0664
}
// ModTime returns the modified time of a file
func (f File) ModTime() time.Time {
return f.modified
}
// ETag returns the ETag of a file
func (f File) ETag() string {
return f.etag
}
// IsDir let us see if a given file is a directory or not
func (f File) IsDir() bool {
return f.isdir
}
// Sys ????
func (f File) Sys() interface{} {
return nil
}
// String lets us see file information
func (f File) String() string {
if f.isdir {
return fmt.Sprintf("Dir : '%s' - '%s'", f.path, f.name)
}
return fmt.Sprintf("File: '%s' SIZE: %d MODIFIED: %s ETAG: %s CTYPE: %s", f.path, f.size, f.modified.String(), f.etag, f.contentType)
}

54
pkg/gowebdav/netrc.go Normal file
View File

@ -0,0 +1,54 @@
package gowebdav
import (
"bufio"
"fmt"
"net/url"
"os"
"regexp"
"strings"
)
func parseLine(s string) (login, pass string) {
fields := strings.Fields(s)
for i, f := range fields {
if f == "login" {
login = fields[i+1]
}
if f == "password" {
pass = fields[i+1]
}
}
return login, pass
}
// ReadConfig reads login and password configuration from ~/.netrc
// machine foo.com login username password 123456
func ReadConfig(uri, netrc string) (string, string) {
u, err := url.Parse(uri)
if err != nil {
return "", ""
}
file, err := os.Open(netrc)
if err != nil {
return "", ""
}
defer file.Close()
re := fmt.Sprintf(`^.*machine %s.*$`, u.Host)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
s := scanner.Text()
matched, err := regexp.MatchString(re, s)
if err != nil {
return "", ""
}
if matched {
return parseLine(s)
}
}
return "", ""
}

214
pkg/gowebdav/requests.go Normal file
View File

@ -0,0 +1,214 @@
package gowebdav
import (
"bytes"
"fmt"
"io"
"net/http"
"path"
"strings"
)
func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (req *http.Response, err error) {
var r *http.Request
var retryBuf io.Reader
if body != nil {
// If the authorization fails, we will need to restart reading
// from the passed body stream.
// When body is seekable, use seek to reset the streams
// cursor to the start.
// Otherwise, copy the stream into a buffer while uploading
// and use the buffers content on retry.
if sk, ok := body.(io.Seeker); ok {
if _, err = sk.Seek(0, io.SeekStart); err != nil {
return
}
retryBuf = body
} else {
buff := &bytes.Buffer{}
retryBuf = buff
body = io.TeeReader(body, buff)
}
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), body)
} else {
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), nil)
}
if err != nil {
return nil, err
}
for k, vals := range c.headers {
for _, v := range vals {
r.Header.Add(k, v)
}
}
// make sure we read 'c.auth' only once since it will be substituted below
// and that is unsafe to do when multiple goroutines are running at the same time.
c.authMutex.Lock()
auth := c.auth
c.authMutex.Unlock()
auth.Authorize(r, method, path)
if intercept != nil {
intercept(r)
}
if c.interceptor != nil {
c.interceptor(method, r)
}
rs, err := c.c.Do(r)
if err != nil {
return nil, err
}
if rs.StatusCode == 401 && auth.Type() == "NoAuth" {
wwwAuthenticateHeader := strings.ToLower(rs.Header.Get("Www-Authenticate"))
if strings.Index(wwwAuthenticateHeader, "digest") > -1 {
c.authMutex.Lock()
c.auth = &DigestAuth{auth.User(), auth.Pass(), digestParts(rs)}
c.authMutex.Unlock()
} else if strings.Index(wwwAuthenticateHeader, "basic") > -1 {
c.authMutex.Lock()
c.auth = &BasicAuth{auth.User(), auth.Pass()}
c.authMutex.Unlock()
} else {
return rs, newPathError("Authorize", c.root, rs.StatusCode)
}
// retryBuf will be nil if body was nil initially so no check
// for body == nil is required here.
return c.req(method, path, retryBuf, intercept)
} else if rs.StatusCode == 401 {
return rs, newPathError("Authorize", c.root, rs.StatusCode)
}
return rs, err
}
func (c *Client) mkcol(path string) (status int, err error) {
rs, err := c.req("MKCOL", path, nil, nil)
if err != nil {
return
}
defer rs.Body.Close()
status = rs.StatusCode
if status == 405 {
status = 201
}
return
}
func (c *Client) options(path string) (*http.Response, error) {
return c.req("OPTIONS", path, nil, func(rq *http.Request) {
rq.Header.Add("Depth", "0")
})
}
func (c *Client) propfind(path string, self bool, body string, resp interface{}, parse func(resp interface{}) error) error {
rs, err := c.req("PROPFIND", path, strings.NewReader(body), func(rq *http.Request) {
if self {
rq.Header.Add("Depth", "0")
} else {
rq.Header.Add("Depth", "1")
}
rq.Header.Add("Content-Type", "application/xml;charset=UTF-8")
rq.Header.Add("Accept", "application/xml,text/xml")
rq.Header.Add("Accept-Charset", "utf-8")
// TODO add support for 'gzip,deflate;q=0.8,q=0.7'
rq.Header.Add("Accept-Encoding", "")
})
if err != nil {
return err
}
defer rs.Body.Close()
if rs.StatusCode != 207 {
return newPathError("PROPFIND", path, rs.StatusCode)
}
return parseXML(rs.Body, resp, parse)
}
func (c *Client) doCopyMove(
method string,
oldpath string,
newpath string,
overwrite bool,
) (
status int,
r io.ReadCloser,
err error,
) {
rs, err := c.req(method, oldpath, nil, func(rq *http.Request) {
rq.Header.Add("Destination", PathEscape(Join(c.root, newpath)))
if overwrite {
rq.Header.Add("Overwrite", "T")
} else {
rq.Header.Add("Overwrite", "F")
}
})
if err != nil {
return
}
status = rs.StatusCode
r = rs.Body
return
}
func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) (err error) {
s, data, err := c.doCopyMove(method, oldpath, newpath, overwrite)
if err != nil {
return
}
if data != nil {
defer data.Close()
}
switch s {
case 201, 204:
return nil
case 207:
// TODO handle multistat errors, worst case ...
log(fmt.Sprintf(" TODO handle %s - %s multistatus result %s", method, oldpath, String(data)))
case 409:
err := c.createParentCollection(newpath)
if err != nil {
return err
}
return c.copymove(method, oldpath, newpath, overwrite)
}
return newPathError(method, oldpath, s)
}
func (c *Client) put(path string, stream io.Reader, callback func(r *http.Request)) (status int, err error) {
rs, err := c.req("PUT", path, stream, callback)
if err != nil {
return
}
defer rs.Body.Close()
//all, _ := io.ReadAll(rs.Body)
//logrus.Debugln("put res: ", string(all))
status = rs.StatusCode
return
}
func (c *Client) createParentCollection(itemPath string) (err error) {
parentPath := path.Dir(itemPath)
if parentPath == "." || parentPath == "/" {
return nil
}
return c.MkdirAll(parentPath, 0755)
}

118
pkg/gowebdav/utils.go Normal file
View File

@ -0,0 +1,118 @@
package gowebdav
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"net/url"
"strconv"
"strings"
"time"
)
func log(msg interface{}) {
fmt.Println(msg)
}
// PathEscape escapes all segments of a given path
func PathEscape(path string) string {
s := strings.Split(path, "/")
for i, e := range s {
s[i] = url.PathEscape(e)
}
return strings.Join(s, "/")
}
// FixSlash appends a trailing / to our string
func FixSlash(s string) string {
if !strings.HasSuffix(s, "/") {
s += "/"
}
return s
}
// FixSlashes appends and prepends a / if they are missing
func FixSlashes(s string) string {
if !strings.HasPrefix(s, "/") {
s = "/" + s
}
return FixSlash(s)
}
// Join joins two paths
func Join(path0 string, path1 string) string {
return strings.TrimSuffix(path0, "/") + "/" + strings.TrimPrefix(path1, "/")
}
// String pulls a string out of our io.Reader
func String(r io.Reader) string {
buf := new(bytes.Buffer)
// TODO - make String return an error as well
_, _ = buf.ReadFrom(r)
return buf.String()
}
func parseUint(s *string) uint {
if n, e := strconv.ParseUint(*s, 10, 32); e == nil {
return uint(n)
}
return 0
}
func parseInt64(s *string) int64 {
if n, e := strconv.ParseInt(*s, 10, 64); e == nil {
return n
}
return 0
}
func parseModified(s *string) time.Time {
if t, e := time.Parse(time.RFC1123, *s); e == nil {
return t
}
return time.Unix(0, 0)
}
func parseXML(data io.Reader, resp interface{}, parse func(resp interface{}) error) error {
decoder := xml.NewDecoder(data)
for t, _ := decoder.Token(); t != nil; t, _ = decoder.Token() {
switch se := t.(type) {
case xml.StartElement:
if se.Name.Local == "response" {
if e := decoder.DecodeElement(resp, &se); e == nil {
if err := parse(resp); err != nil {
return err
}
}
}
}
}
return nil
}
// limitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it.
type limitedReadCloser struct {
rc io.ReadCloser
remaining int
}
func (l *limitedReadCloser) Read(buf []byte) (int, error) {
if l.remaining <= 0 {
return 0, io.EOF
}
if len(buf) > l.remaining {
buf = buf[0:l.remaining]
}
n, err := l.rc.Read(buf)
l.remaining -= n
return n, err
}
func (l *limitedReadCloser) Close() error {
return l.rc.Close()
}

View File

@ -0,0 +1,67 @@
package gowebdav
import (
"fmt"
"net/url"
"testing"
)
func TestJoin(t *testing.T) {
eq(t, "/", "", "")
eq(t, "/", "/", "/")
eq(t, "/foo", "", "/foo")
eq(t, "foo/foo", "foo/", "/foo")
eq(t, "foo/foo", "foo/", "foo")
}
func eq(t *testing.T, expected string, s0 string, s1 string) {
s := Join(s0, s1)
if s != expected {
t.Error("For", "'"+s0+"','"+s1+"'", "expeted", "'"+expected+"'", "got", "'"+s+"'")
}
}
func ExamplePathEscape() {
fmt.Println(PathEscape(""))
fmt.Println(PathEscape("/"))
fmt.Println(PathEscape("/web"))
fmt.Println(PathEscape("/web/"))
fmt.Println(PathEscape("/w e b/d a v/s%u&c#k:s/"))
// Output:
//
// /
// /web
// /web/
// /w%20e%20b/d%20a%20v/s%25u&c%23k:s/
}
func TestEscapeURL(t *testing.T) {
ex := "https://foo.com/w%20e%20b/d%20a%20v/s%25u&c%23k:s/"
u, _ := url.Parse("https://foo.com" + PathEscape("/w e b/d a v/s%u&c#k:s/"))
if ex != u.String() {
t.Error("expected: " + ex + " got: " + u.String())
}
}
func TestFixSlashes(t *testing.T) {
expected := "/"
if got := FixSlashes(""); got != expected {
t.Errorf("expected: %q, got: %q", expected, got)
}
expected = "/path/"
if got := FixSlashes("path"); got != expected {
t.Errorf("expected: %q, got: %q", expected, got)
}
if got := FixSlashes("/path"); got != expected {
t.Errorf("expected: %q, got: %q", expected, got)
}
if got := FixSlashes("path/"); got != expected {
t.Errorf("expected: %q, got: %q", expected, got)
}
}

View File

@ -4,9 +4,3 @@ import "embed"
//go:embed *
var Public embed.FS
////go:embed index.html
//var Index embed.FS
//
////go:embed assets/**
//var Assets embed.FS

View File

@ -38,7 +38,10 @@ func ParsePath(rawPath string) (*model.Account, string, base.Driver, error) {
if bIndex != -1 {
name = name[:bIndex]
}
return &account, strings.TrimPrefix(rawPath, name), driver, nil
//if name == "/" {
// name = ""
//}
return &account, utils.ParsePath(strings.TrimPrefix(rawPath, name)), driver, nil
}
func ErrorResp(c *gin.Context, err error, code int) {

View File

@ -9,9 +9,9 @@ import (
func Path(rawPath string) (*model.File, []model.File, *model.Account, base.Driver, string, error) {
account, path, driver, err := ParsePath(rawPath)
accountFiles := model.GetAccountFilesByPath(rawPath)
if err != nil {
if err.Error() == "path not found" {
accountFiles := model.GetAccountFilesByPath(rawPath)
if len(accountFiles) != 0 {
return nil, accountFiles, nil, nil, path, nil
}
@ -21,13 +21,31 @@ func Path(rawPath string) (*model.File, []model.File, *model.Account, base.Drive
log.Debugln("use account: ", account.Name)
file, files, err := operate.Path(driver, account, path)
if err != nil {
if err.Error() == "path not found" {
if len(accountFiles) != 0 {
return nil, accountFiles, nil, nil, path, nil
}
}
return nil, nil, nil, nil, "", err
}
if file != nil {
return file, nil, account, driver, path, nil
} else {
accountFiles := model.GetAccountFilesByPath(rawPath)
files = append(files, accountFiles...)
for _, accountFile := range accountFiles {
if !containsByName(files, accountFile) {
files = append(files, accountFile)
}
}
return nil, files, account, driver, path, nil
}
}
func containsByName(files []model.File, file model.File) bool {
for _, f := range files {
if f.Name == file.Name {
return true
}
}
return false
}

View File

@ -12,6 +12,7 @@ import (
"net/url"
"os"
"strconv"
"strings"
)
var HttpClient = &http.Client{}
@ -25,9 +26,18 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *base.Link, file *model.
_ = link.Data.Close()
}()
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%s`, url.QueryEscape(file.Name)))
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%s`, file.Name))
w.Header().Set("Content-Length", strconv.FormatInt(file.Size, 10))
w.WriteHeader(http.StatusOK)
if link.Header != nil {
for h, val := range link.Header {
w.Header()[h] = val
}
}
if link.Status == 0 {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(link.Status)
}
_, err = io.Copy(w, link.Data)
if err != nil {
return err
@ -56,8 +66,12 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *base.Link, file *model.
return err
}
for h, val := range r.Header {
if strings.ToLower(h) == "authorization" {
continue
}
req.Header[h] = val
}
log.Debugf("req headers: %+v", r.Header)
for _, header := range link.Headers {
req.Header.Set(header.Name, header.Value)
}

View File

@ -60,6 +60,7 @@ type Meta struct {
Driver string `json:"driver"`
Upload bool `json:"upload"`
Total int `json:"total"`
Readme string `json:"readme"`
//Pages int `json:"pages"`
}
@ -75,8 +76,10 @@ func Path(c *gin.Context) {
_, ok := c.Get("admin")
meta, _ := model.GetMetaByPath(req.Path)
upload := false
if meta != nil && meta.Upload {
upload = true
readme := ""
if meta != nil {
upload = meta.Upload
readme = meta.Readme
}
err := CheckPagination(&req)
if err != nil {
@ -137,6 +140,7 @@ func Path(c *gin.Context) {
Driver: driverName,
Upload: upload,
Total: total,
Readme: readme,
},
Files: files,
},

View File

@ -55,12 +55,13 @@ func Proxy(c *gin.Context) {
return
}
// 对于中转不需要重设IP
link, err := driver.Link(base.Args{Path: path}, account)
link, err := driver.Link(base.Args{Path: path, Header: c.Request.Header}, account)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
err = common.Proxy(c.Writer, c.Request, link, file)
log.Debugln("web proxy error:", err)
if err != nil {
common.ErrorResp(c, err, 500)
}

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