Compare commits

...

171 Commits

Author SHA1 Message Date
d6d2f52922 💄 change default color 2021-12-21 13:13:01 +08:00
9162e782a0 🐛 fix default customize head 2021-12-21 13:09:59 +08:00
dc41ceb99b 🐛 fix initialization sequence 2021-12-21 00:32:09 +08:00
c5e274f52a 💚 fix mv web file 2021-12-21 00:15:43 +08:00
3781043c78 🐛 fix build web file 2021-12-20 23:59:55 +08:00
1485ab2677 🎇 add local config 2021-12-20 23:56:17 +08:00
337bf08cd3 🐛 fix pikpak refresh_token endless loop 2021-12-20 23:21:52 +08:00
22665aa19a 🎨 move default text types 2021-12-20 17:24:49 +08:00
1ab6b4e201 💄 change default customize head 2021-12-20 17:21:11 +08:00
d97afb691b support pikpak and google api proxy 2021-12-20 15:44:17 +08:00
b63e65880f 🚧 google drive api proxy 2021-12-20 01:00:53 +08:00
44cbe0522c support subtitle 2021-12-19 21:24:04 +08:00
fedab86c30 🚧 change some proxy 2021-12-19 20:55:29 +08:00
731dbf6c3a 🐛 fix 123pan 403 2021-12-19 20:32:47 +08:00
d00f75c814 🎨 change link interface 2021-12-19 20:00:53 +08:00
f5b8815a84 🎨 abstract 123pan request 2021-12-19 17:54:28 +08:00
99d06c7449 support api proxy 2021-12-19 17:12:19 +08:00
8e7b2c5837 api proxy 2021-12-19 17:10:20 +08:00
3d3a97288a add webdav tips for browser 2021-12-18 15:23:32 +08:00
c2142cc03a 🎨 Optimize code structure 2021-12-18 15:22:56 +08:00
3ce94de823 🐛 fix FTP down 2021-12-17 12:14:27 +08:00
c64c003257 🎇 feat: pikpak support 2021-12-16 22:50:23 +08:00
1c65588b4a 🎇 webdav update delete cache 2021-12-16 22:02:45 +08:00
d49f92b542 ftp support 2021-12-16 18:03:58 +08:00
92a0453e00 🎨 change internal type 2021-12-16 15:44:18 +08:00
bd7d27efc7 🐛 onedrive retry refresh token 2021-12-16 15:25:49 +08:00
b2055777e0 🐛 fix empty refresh_token 2021-12-16 14:07:15 +08:00
fe79f9518b 🚧 123pan write 2021-12-11 17:44:33 +08:00
a7e9bb9e9a 🐛 fix alist driver files 2021-12-11 14:01:03 +08:00
73d85d96f1 🎇 189cloud write 2021-12-10 22:25:09 +08:00
ff91d7a37d 🎇 delete cache 2021-12-10 22:24:55 +08:00
78f81ddc3b 🎨 add some debug log 2021-12-10 22:24:43 +08:00
2f8258053f support native proxy url 2021-12-10 15:55:21 +08:00
511efce624 📝 update readme 2021-12-10 00:06:50 +08:00
14ff3450ab 📝 update readme 2021-12-09 23:05:16 +08:00
bbba161d55 🐛 fix proxy sign mismatch 2021-12-09 23:01:52 +08:00
6b61f8e9cc 🐛 fix alist root foler invalid 2021-12-09 21:56:28 +08:00
a295e7024a 🎨 change link interface 2021-12-09 19:24:34 +08:00
b36eaf08f0 🎨 abstract cache method 2021-12-08 22:58:44 +08:00
bb6e520ab5 🎨 delete useless code 2021-12-08 22:44:45 +08:00
9b64e2e045 🐛 fix cache bug 2021-12-08 22:41:30 +08:00
ee7c12c30f cf workers proxy 2021-12-08 21:40:20 +08:00
96d6d58910 🐛 fix alist driver cache 2021-12-08 21:39:35 +08:00
2bf235a5ac 🐛 fix link req nil 2021-12-08 20:27:39 +08:00
236f9969c0 🚧 support proxy 2021-12-08 20:00:52 +08:00
09e63027d9 🎇 allow delete deprecated setting 2021-12-08 19:36:07 +08:00
a15dae291e support webm video preview 2021-12-08 14:43:38 +08:00
efaaeedfb8 🐛 fix can't get index.html 2021-12-08 10:35:18 +08:00
190c8001a5 🚧 support proxy url 2021-12-08 10:33:26 +08:00
b8698700ef support add another alist 2021-12-08 09:10:00 +08:00
985b81826f 🎇 support add another alist 2021-12-07 20:16:34 +08:00
74d8fa3919 🔥 remove useless remark 2021-12-07 16:34:20 +08:00
43e4928bb9 🐛 fix aliyundrive webdav delete and upload 2021-12-07 16:32:59 +08:00
03580fd76c 🎨 Improve the code structure 2021-12-07 15:56:43 +08:00
6e8d551420 🚧 aliyundrive webdav write 2021-12-06 22:14:32 +08:00
28998d6f8c 🚧 aliyundrive webdav write 2021-12-06 17:49:20 +08:00
1779617cb9 🎨 Improve the code structure 2021-12-06 15:55:05 +08:00
7dfe48339c 🚧 native webdav write 2021-12-05 16:09:39 +08:00
9c5627a382 🚧 webdav write interface 2021-12-05 15:22:19 +08:00
809850321a 🔧 change customize settings 2021-12-05 13:43:08 +08:00
bdc1f68746 📝 update readme 2021-12-04 18:37:26 +08:00
9aaef6c3a3 📝 update readme image 2021-12-03 11:24:43 +08:00
bb50c52d0e 📝 update readme 2021-12-02 23:01:54 +08:00
6041e5a0fa 🎇 Add ISSUE_TEMPLATE 2021-12-02 22:55:01 +08:00
308a86c36e 📝 update readme 2021-12-02 22:38:18 +08:00
ba7c4fc230 🎇 Create LICENSE 2021-12-02 22:37:56 +08:00
d81ec0637d 🎇 md5 and filename 2021-12-02 18:57:29 +08:00
1d7d37e642 🐛 fix onedrive sort 2021-12-01 11:25:33 +08:00
b62a716267 remove onedrive empty cache 2021-12-01 11:25:04 +08:00
944941db10 🎇 support https 2021-12-01 00:19:06 +08:00
bd91acc5d0 🐛 fix lanzou webdav time error 2021-12-01 00:15:34 +08:00
cd50227835 🎇 support lanzou 2021-11-30 21:30:50 +08:00
50226f66e3 google default root folder 2021-11-30 19:10:27 +08:00
9dcaa9b07a 🎨 add types 2021-11-30 18:53:37 +08:00
fa6c0f78bc 🐛 fix windows sharepoint path error 2021-11-30 16:45:08 +08:00
7f35dc6ade add onedrive thumbnail 2021-11-30 16:27:23 +08:00
5d6463b75a 🎨 move code 2021-11-30 10:00:06 +08:00
733b38b435 🎨 improve code structure 2021-11-30 09:37:51 +08:00
50a02a7af7 🔥 remove useless code 2021-11-29 21:30:52 +08:00
71b1517de7 🐛 fix windows check parent password 2021-11-29 21:09:16 +08:00
ffdd88ec66 webdav proxy 2021-11-29 16:42:46 +08:00
4ff2756572 🐛 fix 123pan webdav can't get 2021-11-29 16:06:49 +08:00
d955038ebc delete useless path unescape 2021-11-29 16:05:58 +08:00
72d5e4e691 delete useless log 2021-11-29 16:05:14 +08:00
b1e662cd34 🐛 fix onedrive slow response 2021-11-28 17:01:27 +08:00
0f0e1104a4 WebDAV auth 2021-11-28 16:18:09 +08:00
3041da35ab 🐛 fix webdav proxy link auth 2021-11-28 16:05:41 +08:00
9eab54a7c8 webdav(view only) 2021-11-28 15:10:48 +08:00
0b8d3a0a2c 🐛 fix can't get file 2021-11-28 15:10:06 +08:00
f9945a14a8 🎇 webdav unfinished 2021-11-28 00:12:04 +08:00
c39752ceb4 🎇 optimization googledrive 2021-11-27 20:40:03 +08:00
53b383d2cf 🔨 refactor driver: onedrive 2021-11-27 20:20:14 +08:00
e76fc3e616 🔨 refactor driver: native 2021-11-27 20:07:32 +08:00
eb21b87020 🔨 refactor driver: google 2021-11-27 19:52:38 +08:00
f577d82242 🔨 refactor driver: ali 2021-11-27 19:40:36 +08:00
98691b2aa8 🔨 refactor driver(123 and 189) 2021-11-27 18:55:09 +08:00
4fe6ed6c3e 🐛 fix log 2021-11-26 20:23:33 +08:00
fe73ece57d change log 2021-11-26 20:20:32 +08:00
59b8f1084a 🐛 fixed checking the parent folder when checked the password of the current folder 2021-11-25 16:24:39 +08:00
2f669ac45c 📝 update doc 2021-11-24 18:58:17 +08:00
d03d91d518 🐛 fix google drive root folder label 2021-11-24 13:34:15 +08:00
fe981f67ec 🐛 fix windows check parent 2021-11-23 16:22:23 +08:00
8cfabfd0f5 resolved #170 native driver sort 2021-11-23 16:09:42 +08:00
163ee1159e change path interface 2021-11-23 15:58:03 +08:00
e31402e94f resolved #169 2021-11-23 15:46:10 +08:00
5500980d63 🐛 fix onedrive get files 2021-11-23 15:39:09 +08:00
b1695445e0 🐛 index add write header now 2021-11-22 18:10:09 +08:00
5db1ad4adf 🎇 resolved #160 2021-11-17 22:19:11 +08:00
725f5b0c55 support 123pan 2021-11-17 22:17:04 +08:00
87a74394b3 google client retry 2021-11-17 20:40:42 +08:00
a41c820525 🐛 fix debug output 2021-11-17 20:29:01 +08:00
cd53dc6d24 📝 update readme 2021-11-17 17:37:41 +08:00
cfe16b5ed2 add GoogleDrive 2021-11-17 17:21:11 +08:00
3d9746485d change proxy setting 2021-11-17 17:20:57 +08:00
0b7f2fee7d proxy add account 2021-11-17 16:37:12 +08:00
36d52e0b75 delete cache file 2021-11-17 16:25:32 +08:00
e4d254e4b0 simplify delete account when change name 2021-11-17 15:25:31 +08:00
e8d27a30b4 🐛 fix delete account when change name 2021-11-17 15:21:51 +08:00
69514668cc 🎇 replace title 2021-11-16 22:32:05 +08:00
e5f8f59c87 🐛 fix native account status 2021-11-16 18:23:59 +08:00
f87ee1ed9e 🐛 fix 189 account status 2021-11-16 15:08:55 +08:00
07155cfd01 add show current password 2021-11-16 15:00:56 +08:00
5e982980dc 🐛 fix determine gbk 2021-11-16 00:03:49 +08:00
8987958e26 🐛 fix 189 size and time 2021-11-15 20:31:44 +08:00
4466cb19a5 🎇 get 189 redirect url 2021-11-15 20:15:25 +08:00
da74e29b26 🐛 fix gbk garbled 2021-11-15 19:20:39 +08:00
82272fcbf5 🐛 fix windows onedrive path 2021-11-15 19:07:04 +08:00
74d86f8cc4 🎇 delete useless cookieJar 2021-11-15 19:06:44 +08:00
c03646dedf 🐛 change text proxy 2021-11-15 19:06:10 +08:00
c0d1888e25 support 189cloud(login refer to PanIndex) 2021-11-15 18:00:43 +08:00
f4942e89bd 🐛 fix account status 2021-11-15 15:01:14 +08:00
27e61c9eb8 🐛 fix change name error 2021-11-15 14:54:22 +08:00
aeb72320ca 🐛 fix get error account 2021-11-15 14:53:32 +08:00
caddba05e9 💚 remove ppc64le docker 2021-11-14 14:53:23 +08:00
0dffb9aaa1 add customize style / script 2021-11-14 14:37:26 +08:00
0f93d2bfed 🐛 fix version type 2021-11-14 00:30:15 +08:00
b785945210 🐛 fix add account 2021-11-13 19:31:35 +08:00
48a65784c7 🐛 fix can't delete and get meta 2021-11-13 17:10:44 +08:00
5f34b8ab80 account and meta add id 2021-11-13 16:49:03 +08:00
959ef620fb check parent folder password 2021-11-13 16:12:12 +08:00
a73501463f fix proxy 2021-11-13 16:02:19 +08:00
cf07b3921c 🔨 switch fiber to gin 2021-11-13 15:53:26 +08:00
65ec4e3611 dockerfile volume 2021-11-13 14:23:41 +08:00
7799c1f3bc 🎇 change default setting 2021-11-12 23:03:39 +08:00
e902c2ded7 add settings 2021-11-12 22:56:30 +08:00
2c675ae909 change settings 2021-11-12 19:39:01 +08:00
98c017730f resolved #155 2021-11-11 19:51:25 +08:00
f4affb2c69 add go proxy 2021-11-09 21:36:27 +08:00
17af21079f 💚 fix web path 2021-11-09 19:35:46 +08:00
f7d35ec925 💚 fix dockerfile 2021-11-09 18:12:36 +08:00
a8730e82b5 💚 fix dockerfile 2021-11-09 18:02:15 +08:00
6275e27d1b 💚 fix dockerfile 2021-11-09 17:54:38 +08:00
e70353704f 💚 fix dockerfile 2021-11-09 17:22:58 +08:00
be5b1e42d4 💚 fix dockerfile 2021-11-09 16:51:30 +08:00
7d08cbc4a9 🚧 build docker 2021-11-09 16:46:03 +08:00
1542878d66 🎨 format code 2021-11-09 16:03:04 +08:00
ac8f5d5737 🐛 file name contains + 2021-11-07 23:05:50 +08:00
9ed5b6e581 🐛 file name contains + 2021-11-07 22:12:52 +08:00
db7bff2d61 🐛 fix: #151 2021-11-07 14:04:54 +08:00
7970e737f0 add markdown theme setting 2021-11-06 17:33:00 +08:00
d4523d52ee 🐛 delete timed task 2021-11-06 17:25:07 +08:00
ac8476702c 🐛 refer to https://github.com/ad-m/github-push-action/issues/44#issuecomment-581706892 2021-11-05 20:52:50 +08:00
12f68eaed9 💚 fix upload asserts 2021-11-05 19:47:02 +08:00
6a51f02845 💚 mv build.sh 2021-11-05 19:11:39 +08:00
e7071e1093 release asserts files cdn 2021-11-05 19:03:54 +08:00
11b141b190 update onedrive account status 2021-11-05 16:32:20 +08:00
7e099b39cf cache len(files)=0: request 2021-11-05 16:30:50 +08:00
b46bf0dfc9 🐛 delete beta1 meta 2021-11-04 23:25:53 +08:00
91f64161b2 💚 change back to ubuntu 2021-11-04 22:38:08 +08:00
8255ef4346 💚 print md5 2021-11-04 21:34:52 +08:00
254b6c6f79 print build version 2021-11-04 18:42:27 +08:00
95 changed files with 14190 additions and 1588 deletions

39
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: "Bug report"
description: Bug report
labels: [pending triage]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: version
attributes:
label: Alist Version / Alist 版本
description: What version of our software are you running?
placeholder: v2.0.0
validations:
required: true
- type: textarea
id: bug-description
attributes:
label: Describe the bug / 问题描述
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction / 复现链接
description: |
Please provide a link to a repo that can reproduce the problem you ran into.
请提供能复现此问题的链接
validations:
required: false
- type: textarea
id: logs
attributes:
label: 日志 / Logs
description: |
Please copy and paste any relevant log output.
请复制粘贴错误日志,或者截图
render: shell

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

View File

@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
platform: [macos-latest]
platform: [ubuntu-latest]
go-version: [1.17]
name: Build
runs-on: ${{ matrix.platform }}
@ -25,8 +25,8 @@ jobs:
with:
node-version: '16'
- name: Setup docker
uses: docker-practice/actions-setup-docker@master
# - name: Setup docker
# uses: docker-practice/actions-setup-docker@master
- name: Checkout
uses: actions/checkout@v2
@ -45,20 +45,12 @@ jobs:
run: |
docker pull techknowlogick/xgo:latest
go install src.techknowlogick.com/xgo@latest
brew install upx
- name: Build web
run: |
cd alist-web
yarn
yarn build
cd ..
sudo apt install upx
- name: Build
run: |
cd alist
mv alist/build.sh .
bash build.sh
cd ..
- name: Upload artifact
uses: actions/upload-artifact@v2

48
.github/workflows/docker.yml vendored Normal file
View File

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

View File

@ -9,7 +9,7 @@ jobs:
release:
strategy:
matrix:
platform: [macos-latest]
platform: [ubuntu-latest]
go-version: [1.17]
name: Release
runs-on: ${{ matrix.platform }}
@ -19,8 +19,8 @@ jobs:
with:
go-version: ${{ matrix.go-version }}
- name: Setup docker
uses: docker-practice/actions-setup-docker@master
# - name: Setup docker
# uses: docker-practice/actions-setup-docker@master
- name: Setup Node
uses: actions/setup-node@v2
@ -32,6 +32,8 @@ jobs:
with:
ref: v2
path: alist
persist-credentials: false
fetch-depth: 0
- name: Checkout web repo
uses: actions/checkout@v2
@ -39,25 +41,27 @@ jobs:
repository: Xhofe/alist-web
ref: v2
path: alist-web
persist-credentials: false
fetch-depth: 0
- name: Set up xgo
run: |
docker pull techknowlogick/xgo:latest
go install src.techknowlogick.com/xgo@latest
brew install upx
- name: Build web
run: |
cd alist-web
yarn
yarn build
cd ..
sudo apt install upx
- name: Build
run: |
cd alist
mv alist/build.sh .
bash build.sh release
cd ..
- name: Upload asserts files
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.MY_TOKEN }}
branch: cdn
directory: alist-web
repository: Xhofe/alist-web
- name: Release
uses: softprops/action-gh-release@v1

5
.gitignore vendored
View File

@ -21,7 +21,8 @@ dist/
# Dependency directories (remove the comment below to include it)
# vendor/
bin/*
alist
/alist
*.json
public/index.html
public/assets/
public/assets/
data/

14
Dockerfile Normal file
View File

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

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Xhofe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

68
README.md Normal file → Executable file
View File

@ -1,43 +1,65 @@
<h2 align="center">Alist</h2>
<p align="center">
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/release/Xhofe/alist?style=flat-square" alt="Release version"></a>
<div align="center">
<h3><a href="https://alist.nn.ci">Alist</a></h3>
<p><em>🗂Another file list program that supports multiple storage, powered by Gin and React.</em></p>
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/release/Xhofe/alist?style=flat-square" alt="latest version"></a>
<a href="https://github.com/Xhofe/alist/discussions"><img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936&style=flat-square" alt="discussions"></a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build?style=flat-square" alt="Build status"></a>
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/downloads/Xhofe/alist/total?style=flat-square" alt="Downloads"></a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Xhofe/alist?style=flat-square" alt="License"></a>
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/downloads/Xhofe/alist/total?style=flat-square&color=%239F7AEA" alt="Downloads"></a>
<a href="https://github.com/Xhofe/alist/blob/v2/LICENSE"><img src="https://img.shields.io/github/license/Xhofe/alist?style=flat-square" alt="License"></a>
<a href="https://pay.xhofe.top">
<img src="https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat-square" alt="donate">
</a>
</p>
</div>
---
### 这是什么?
English | [中文](./README_cn.md)
一款支持多种存储的目录文件列表程序,后端基于`go-fiber`,前端使用`react`
## Features
### 前端项目地址
- [x] multiple storage
- [x] Local storage
- [x] [aliyundrive](https://www.aliyundrive.com/)
- [x] OneDrive / Sharepoint ([global](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us
- [x] [189cloud](https://cloud.189.cn)
- [x] [GoogleDrive](https://drive.google.com/)
- [x] [123pan](https://www.123pan.com/)
- [x] [lanzou](https://pc.woozooo.com/)
- [x] [Alist](https://github.com/Xhofe/alist)
- [x] FTP
- [x] [PikPak](https://www.mypikpak.com/)
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
- [x] Video and audio preview (mp4, mp3, ...)
- [x] Office documents preview (docx, pptx, xlsx, ...)
- [x] `README.md` preview rendering
- [x] File permalink copy and direct file download
- [x] Dark mode
- [x] I18n
- [x] Protected routes (password protection and authentication)
- [x] WebDav (readonly)
- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist)
- [x] Cloudflare workers proxy
- https://github.com/Xhofe/alist-web
## Discussion
### 演示地址
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports only.**
- https://alist.nn.ci
## Demo
### 预览
Available at: <https://alist.nn.ci>.
<a href="https://alist.nn.ci/"><img src="https://store.heytapimage.com/cdo-portal/feedback/202111/03/695ef77854a144e928518efde38db97a.png"></a>
![demo](https://inews.gtimg.com/newsapp_ls/0/14256614096/0)
### 支持的存储
## Document
- 本地存储
- 阿里云盘
- Onedrive
- ...
<https://alist-doc.nn.ci/en/>
### 如何使用
## License
- https://www.nn.ci/archives/alist.html
The `AList` is open-source software licensed under the MIT license.
### License
---
The `AList` is open-source software licensed under the MIT license.
> [@Blog](https://www.nn.ci/) · [@GitHub](https://github.com/Xhofe)

64
README_cn.md Normal file
View File

@ -0,0 +1,64 @@
<div align="center">
<h3><a href="https://alist.nn.ci">Alist</a></h3>
<p><em>🗂️一个支持多存储的文件列表程序,使用 Gin 和 React 。</em></p>
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/release/Xhofe/alist?style=flat-square" alt="latest version"></a>
<a href="https://github.com/Xhofe/alist/discussions"><img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936&style=flat-square" alt="discussions"></a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build?style=flat-square" alt="Build status"></a>
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/downloads/Xhofe/alist/total?style=flat-square&color=%239F7AEA" alt="Downloads"></a>
<a href="https://github.com/Xhofe/alist/blob/v2/LICENSE"><img src="https://img.shields.io/github/license/Xhofe/alist?style=flat-square" alt="License"></a>
<a href="https://pay.xhofe.top">
<img src="https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat-square" alt="donate">
</a>
</div>
---
[English](./README.md) | 中文
## 支持
- [x] 多种存储
- [x] 本地存储
- [x] [阿里云盘](https://www.aliyundrive.com/)
- [x] OneDrive / Sharepoint[国际版](https://www.office.com/), [世纪互联](https://portal.partner.microsoftonline.cn),de,us
- [x] [天翼云盘](https://cloud.189.cn)
- [x] [GoogleDrive](https://drive.google.com/)
- [x] [123云盘](https://www.123pan.com/)
- [x] [蓝奏云](https://pc.woozooo.com/)
- [x] [Alist](https://github.com/Xhofe/alist)
- [x] FTP
- [x] [PikPak](https://www.mypikpak.com/)
- [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览
- [x] 视频和音频预览mp4、mp3 等)
- [x] Office 文档预览docx、pptx、xlsx、...
- [x] `README.md` 预览渲染
- [x] 文件永久链接复制和直接文件下载
- [x] 黑暗模式
- [x] 国际化
- [x] 受保护的路由(密码保护和身份验证)
- [x] WebDav只读
- [x] [Docker 部署](https://hub.docker.com/r/xhofe/alist)
- [x] Cloudflare workers 中转
## 讨论
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) **issue仅针对错误报告。**
## 演示
<https://alist.nn.ci>
![演示](https://inews.gtimg.com/newsapp_ls/0/14256614096/0)
## 文档
<https://alist-doc.nn.ci/>
## 许可
`AList` 是在 MIT 许可下许可的开源软件。
---
> [@Blog](https://www.nn.ci/) · [@GitHub](https://github.com/Xhofe)

430
alist-proxy.js Normal file
View File

@ -0,0 +1,430 @@
const HOST = "YOUR_HOST";
const TOKEN = "YOUR_TOKEN";
addEventListener("fetch", (event) => {
const request = event.request;
const url = new URL(request.url);
const sign = url.searchParams.get("sign");
if (request.method === "OPTIONS") {
// Handle CORS preflight requests
event.respondWith(handleOptions(request));
} else if (sign && sign.length === 16) {
// Handle requests to the Down server
event.respondWith(handleDownload(request));
} else {
// Handle requests to the API server
event.respondWith(handleRequest(event));
}
});
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
"Access-Control-Max-Age": "86400",
};
async function handleDownload(request) {
const origin = request.headers.get("origin");
const url = new URL(request.url);
const path = decodeURI(url.pathname);
const sign = url.searchParams.get("sign");
const name = path.split("/").pop();
const right = md5(`alist-${TOKEN}-${name}`).slice(8, 24);
if (sign !== right) {
const resp = new Response(
JSON.stringify({
code: 401,
message: `sign mismatch`,
}),
{
headers: {
"content-type": "application/json;charset=UTF-8",
},
}
);
resp.headers.set("Access-Control-Allow-Origin", origin);
return resp;
}
let resp = await fetch(`${HOST}/api/admin/link`, {
method: "POST",
headers: {
"content-type": "application/json;charset=UTF-8",
Authorization: TOKEN,
},
body: JSON.stringify({
path: path,
}),
});
let res = await resp.json();
if (res.code !== 200) {
return new Response(JSON.stringify(res));
}
request = new Request(res.data.url, request);
if (res.data.headers) {
for (const header of res.data.headers) {
request.headers.set(header.name, header.value);
}
}
let response = await fetch(request);
// Recreate the response so we can modify the headers
response = new Response(response.body, response);
// Set CORS headers
response.headers.set("Access-Control-Allow-Origin", origin);
// Append to/Add Vary header so browser will cache response correctly
response.headers.append("Vary", "Origin");
return response;
}
/**
* Respond to the request
* @param {Request} request
*/
async function handleRequest(event) {
const { request } = event;
//请求头部、返回对象
let reqHeaders = new Headers(request.headers),
outBody,
outStatus = 200,
outStatusText = "OK",
outCt = null,
outHeaders = new Headers({
"Access-Control-Allow-Origin": reqHeaders.get("Origin"),
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers":
reqHeaders.get("Access-Control-Allow-Headers") ||
"Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token, Notion-Version",
});
try {
//取域名第一个斜杠后的所有信息为代理链接
let url = request.url.substr(8);
url = decodeURIComponent(url.substr(url.indexOf("/") + 1));
//需要忽略的代理
if (
request.method == "OPTIONS" &&
reqHeaders.has("access-control-request-headers")
) {
//输出提示
return new Response(null, PREFLIGHT_INIT);
} else if (
url.length < 3 ||
url.indexOf(".") == -1 ||
url == "favicon.ico" ||
url == "robots.txt"
) {
return Response.redirect("https://baidu.com", 301);
}
//阻断
else if (blocker.check(url)) {
return Response.redirect("https://baidu.com", 301);
} else {
//补上前缀 http://
url = url
.replace(/https:(\/)*/, "https://")
.replace(/http:(\/)*/, "http://");
if (url.indexOf("://") == -1) {
url = "http://" + url;
}
//构建 fetch 参数
let fp = {
method: request.method,
headers: {},
};
//保留头部其它信息
let he = reqHeaders.entries();
for (let h of he) {
if (!["content-length"].includes(h[0])) {
fp.headers[h[0]] = h[1];
}
}
// 是否带 body
if (["POST", "PUT", "PATCH", "DELETE"].indexOf(request.method) >= 0) {
const ct = (reqHeaders.get("content-type") || "").toLowerCase();
if (ct.includes("application/json")) {
let requestJSON = await request.json();
console.log(typeof requestJSON);
fp.body = JSON.stringify(requestJSON);
} else if (
ct.includes("application/text") ||
ct.includes("text/html")
) {
fp.body = await request.text();
} else if (ct.includes("form")) {
// fp.body = await request.formData();
fp.body = await request.text();
} else {
fp.body = await request.blob();
}
}
// 发起 fetch
let fr = await fetch(url, fp);
outCt = fr.headers.get("content-type");
if (outCt.includes("application/text") || outCt.includes("text/html")) {
try {
// 添加base
let newFr = new HTMLRewriter()
.on("head", {
element(element) {
element.prepend(`<base href="${url}" />`, {
html: true,
});
},
})
.transform(fr);
fr = newFr;
} catch (e) {}
}
outStatus = fr.status;
outStatusText = fr.statusText;
outBody = fr.body;
}
} catch (err) {
outCt = "application/json";
outBody = JSON.stringify({
code: -1,
msg: JSON.stringify(err.stack) || err,
});
}
//设置类型
if (outCt && outCt != "") {
outHeaders.set("content-type", outCt);
}
let response = new Response(outBody, {
status: outStatus,
statusText: outStatusText,
headers: outHeaders,
});
return response;
}
const blocker = {
keys: [],
check: function (url) {
url = url.toLowerCase();
let len = blocker.keys.filter((x) => url.includes(x)).length;
return len != 0;
},
};
function handleOptions(request) {
// Make sure the necessary headers are present
// for this to be a valid pre-flight request
let headers = request.headers;
if (
headers.get("Origin") !== null &&
headers.get("Access-Control-Request-Method") !== null
// && headers.get("Access-Control-Request-Headers") !== null
) {
// Handle CORS pre-flight request.
// If you want to check or reject the requested method + headers
// you can do that here.
let respHeaders = {
...corsHeaders,
// Allow all future content Request headers to go back to browser
// such as Authorization (Bearer) or X-Client-Name-Version
"Access-Control-Allow-Headers": request.headers.get(
"Access-Control-Request-Headers"
),
};
return new Response(null, {
headers: respHeaders,
});
} else {
// Handle standard OPTIONS request.
// If you want to allow other HTTP Methods, you can do that here.
return new Response(null, {
headers: {
Allow: "GET, HEAD, POST, OPTIONS",
},
});
}
}
!(function (a) {
"use strict";
function b(a, b) {
var c = (65535 & a) + (65535 & b),
d = (a >> 16) + (b >> 16) + (c >> 16);
return (d << 16) | (65535 & c);
}
function c(a, b) {
return (a << b) | (a >>> (32 - b));
}
function d(a, d, e, f, g, h) {
return b(c(b(b(d, a), b(f, h)), g), e);
}
function e(a, b, c, e, f, g, h) {
return d((b & c) | (~b & e), a, b, f, g, h);
}
function f(a, b, c, e, f, g, h) {
return d((b & e) | (c & ~e), a, b, f, g, h);
}
function g(a, b, c, e, f, g, h) {
return d(b ^ c ^ e, a, b, f, g, h);
}
function h(a, b, c, e, f, g, h) {
return d(c ^ (b | ~e), a, b, f, g, h);
}
function i(a, c) {
(a[c >> 5] |= 128 << c % 32), (a[(((c + 64) >>> 9) << 4) + 14] = c);
var d,
i,
j,
k,
l,
m = 1732584193,
n = -271733879,
o = -1732584194,
p = 271733878;
for (d = 0; d < a.length; d += 16)
(i = m),
(j = n),
(k = o),
(l = p),
(m = e(m, n, o, p, a[d], 7, -680876936)),
(p = e(p, m, n, o, a[d + 1], 12, -389564586)),
(o = e(o, p, m, n, a[d + 2], 17, 606105819)),
(n = e(n, o, p, m, a[d + 3], 22, -1044525330)),
(m = e(m, n, o, p, a[d + 4], 7, -176418897)),
(p = e(p, m, n, o, a[d + 5], 12, 1200080426)),
(o = e(o, p, m, n, a[d + 6], 17, -1473231341)),
(n = e(n, o, p, m, a[d + 7], 22, -45705983)),
(m = e(m, n, o, p, a[d + 8], 7, 1770035416)),
(p = e(p, m, n, o, a[d + 9], 12, -1958414417)),
(o = e(o, p, m, n, a[d + 10], 17, -42063)),
(n = e(n, o, p, m, a[d + 11], 22, -1990404162)),
(m = e(m, n, o, p, a[d + 12], 7, 1804603682)),
(p = e(p, m, n, o, a[d + 13], 12, -40341101)),
(o = e(o, p, m, n, a[d + 14], 17, -1502002290)),
(n = e(n, o, p, m, a[d + 15], 22, 1236535329)),
(m = f(m, n, o, p, a[d + 1], 5, -165796510)),
(p = f(p, m, n, o, a[d + 6], 9, -1069501632)),
(o = f(o, p, m, n, a[d + 11], 14, 643717713)),
(n = f(n, o, p, m, a[d], 20, -373897302)),
(m = f(m, n, o, p, a[d + 5], 5, -701558691)),
(p = f(p, m, n, o, a[d + 10], 9, 38016083)),
(o = f(o, p, m, n, a[d + 15], 14, -660478335)),
(n = f(n, o, p, m, a[d + 4], 20, -405537848)),
(m = f(m, n, o, p, a[d + 9], 5, 568446438)),
(p = f(p, m, n, o, a[d + 14], 9, -1019803690)),
(o = f(o, p, m, n, a[d + 3], 14, -187363961)),
(n = f(n, o, p, m, a[d + 8], 20, 1163531501)),
(m = f(m, n, o, p, a[d + 13], 5, -1444681467)),
(p = f(p, m, n, o, a[d + 2], 9, -51403784)),
(o = f(o, p, m, n, a[d + 7], 14, 1735328473)),
(n = f(n, o, p, m, a[d + 12], 20, -1926607734)),
(m = g(m, n, o, p, a[d + 5], 4, -378558)),
(p = g(p, m, n, o, a[d + 8], 11, -2022574463)),
(o = g(o, p, m, n, a[d + 11], 16, 1839030562)),
(n = g(n, o, p, m, a[d + 14], 23, -35309556)),
(m = g(m, n, o, p, a[d + 1], 4, -1530992060)),
(p = g(p, m, n, o, a[d + 4], 11, 1272893353)),
(o = g(o, p, m, n, a[d + 7], 16, -155497632)),
(n = g(n, o, p, m, a[d + 10], 23, -1094730640)),
(m = g(m, n, o, p, a[d + 13], 4, 681279174)),
(p = g(p, m, n, o, a[d], 11, -358537222)),
(o = g(o, p, m, n, a[d + 3], 16, -722521979)),
(n = g(n, o, p, m, a[d + 6], 23, 76029189)),
(m = g(m, n, o, p, a[d + 9], 4, -640364487)),
(p = g(p, m, n, o, a[d + 12], 11, -421815835)),
(o = g(o, p, m, n, a[d + 15], 16, 530742520)),
(n = g(n, o, p, m, a[d + 2], 23, -995338651)),
(m = h(m, n, o, p, a[d], 6, -198630844)),
(p = h(p, m, n, o, a[d + 7], 10, 1126891415)),
(o = h(o, p, m, n, a[d + 14], 15, -1416354905)),
(n = h(n, o, p, m, a[d + 5], 21, -57434055)),
(m = h(m, n, o, p, a[d + 12], 6, 1700485571)),
(p = h(p, m, n, o, a[d + 3], 10, -1894986606)),
(o = h(o, p, m, n, a[d + 10], 15, -1051523)),
(n = h(n, o, p, m, a[d + 1], 21, -2054922799)),
(m = h(m, n, o, p, a[d + 8], 6, 1873313359)),
(p = h(p, m, n, o, a[d + 15], 10, -30611744)),
(o = h(o, p, m, n, a[d + 6], 15, -1560198380)),
(n = h(n, o, p, m, a[d + 13], 21, 1309151649)),
(m = h(m, n, o, p, a[d + 4], 6, -145523070)),
(p = h(p, m, n, o, a[d + 11], 10, -1120210379)),
(o = h(o, p, m, n, a[d + 2], 15, 718787259)),
(n = h(n, o, p, m, a[d + 9], 21, -343485551)),
(m = b(m, i)),
(n = b(n, j)),
(o = b(o, k)),
(p = b(p, l));
return [m, n, o, p];
}
function j(a) {
var b,
c = "";
for (b = 0; b < 32 * a.length; b += 8)
c += String.fromCharCode((a[b >> 5] >>> b % 32) & 255);
return c;
}
function k(a) {
var b,
c = [];
for (c[(a.length >> 2) - 1] = void 0, b = 0; b < c.length; b += 1) c[b] = 0;
for (b = 0; b < 8 * a.length; b += 8)
c[b >> 5] |= (255 & a.charCodeAt(b / 8)) << b % 32;
return c;
}
function l(a) {
return j(i(k(a), 8 * a.length));
}
function m(a, b) {
var c,
d,
e = k(a),
f = [],
g = [];
for (
f[15] = g[15] = void 0, e.length > 16 && (e = i(e, 8 * a.length)), c = 0;
16 > c;
c += 1
)
(f[c] = 909522486 ^ e[c]), (g[c] = 1549556828 ^ e[c]);
return (d = i(f.concat(k(b)), 512 + 8 * b.length)), j(i(g.concat(d), 640));
}
function n(a) {
var b,
c,
d = "0123456789abcdef",
e = "";
for (c = 0; c < a.length; c += 1)
(b = a.charCodeAt(c)), (e += d.charAt((b >>> 4) & 15) + d.charAt(15 & b));
return e;
}
function o(a) {
return unescape(encodeURIComponent(a));
}
function p(a) {
return l(o(a));
}
function q(a) {
return n(p(a));
}
function r(a, b) {
return m(o(a), o(b));
}
function s(a, b) {
return n(r(a, b));
}
function t(a, b, c) {
return b ? (c ? r(b, a) : s(b, a)) : c ? p(a) : q(a);
}
"function" == typeof define && define.amd
? define(function () {
return t;
})
: (a.md5 = t);
})(this);

View File

@ -1,47 +1,58 @@
package main
import (
"flag"
"fmt"
"github.com/Xhofe/alist/bootstrap"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/public"
_ "github.com/Xhofe/alist/drivers"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/server"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"net/http"
)
func init() {
flag.StringVar(&conf.ConfigFile, "conf", "config.json", "config file")
flag.BoolVar(&conf.Debug, "debug", false, "start with debug mode")
flag.BoolVar(&conf.Version, "version", false, "print version info")
flag.Parse()
}
func Init() {
bootstrap.InitLog()
func Init() bool {
//bootstrap.InitLog()
bootstrap.InitConf()
bootstrap.InitCron()
bootstrap.InitModel()
if conf.Password {
pass, err := model.GetSettingByKey("password")
if err != nil {
log.Errorf(err.Error())
return false
}
log.Infof("current password: %s", pass.Value)
return false
}
server.InitIndex()
bootstrap.InitSettings()
bootstrap.InitAccounts()
bootstrap.InitCache()
return true
}
func main() {
if conf.Version {
fmt.Printf("Built At: %s\nGo Version: %s\nAuthor: %s\nCommit ID: %s\nVersion:%s\n", conf.BuiltAt, conf.GoVersion, conf.GitAuthor, conf.GitCommit, conf.GitTag)
fmt.Printf("Built At: %s\nGo Version: %s\nAuthor: %s\nCommit ID: %s\nVersion: %s\n", conf.BuiltAt, conf.GoVersion, conf.GitAuthor, conf.GitCommit, conf.GitTag)
return
}
Init()
app := fiber.New()
server.InitApiRouter(app)
app.Use("/", filesystem.New(filesystem.Config{
Root: http.FS(public.Public),
NotFoundFile: "index.html",
}))
log.Info("starting server")
err := app.Listen(fmt.Sprintf(":%d", conf.Conf.Port))
if !Init() {
return
}
if !conf.Debug {
gin.SetMode(gin.ReleaseMode)
}
r := gin.Default()
server.InitApiRouter(r)
base := fmt.Sprintf("%s:%d", conf.Conf.Address, conf.Conf.Port)
log.Infof("start server @ %s", base)
var err error
if conf.Conf.Https {
err = r.RunTLS(base, conf.Conf.CertFile, conf.Conf.KeyFile)
} else {
err = r.Run(base)
}
if err != nil {
log.Errorf("failed to start: %s", err.Error())
}

30
bootstrap/account.go Normal file
View File

@ -0,0 +1,30 @@
package bootstrap
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
log "github.com/sirupsen/logrus"
)
func InitAccounts() {
log.Infof("init accounts...")
var accounts []model.Account
if err := conf.DB.Find(&accounts).Error; err != nil {
log.Fatalf("failed sync init accounts")
}
for i, account := range accounts {
model.RegisterAccount(account)
driver, ok := base.GetDriver(account.Type)
if !ok {
log.Errorf("no [%s] driver", account.Type)
} else {
err := driver.Save(&accounts[i], nil)
if err != nil {
log.Errorf("init account [%s] error:[%s]", account.Name, err.Error())
} else {
log.Infof("success init account: %s, type: %s", account.Name, account.Type)
}
}
}
}

View File

@ -13,6 +13,10 @@ func InitConf() {
log.Infof("reading config file: %s", conf.ConfigFile)
if !utils.Exists(conf.ConfigFile) {
log.Infof("config file not exists, creating default config file")
_, err := utils.CreatNestedFile(conf.ConfigFile)
if err != nil {
log.Fatalf("failed to create config file")
}
conf.Conf = conf.DefaultConfig()
if !utils.WriteToJson(conf.ConfigFile, conf.Conf) {
log.Fatalf("failed to create default config file")

View File

@ -1,11 +1,12 @@
package bootstrap
import (
"flag"
"github.com/Xhofe/alist/conf"
log "github.com/sirupsen/logrus"
)
// initLog init log
// InitLog init log
func InitLog() {
if conf.Debug {
log.SetLevel(log.DebugLevel)
@ -18,4 +19,14 @@ func InitLog() {
TimestampFormat: "2006-01-02 15:04:05",
FullTimestamp: true,
})
log.Infof("init log...")
}
func init() {
flag.StringVar(&conf.ConfigFile, "conf", "data/config.json", "config file")
flag.BoolVar(&conf.Debug, "debug", false, "start with debug mode")
flag.BoolVar(&conf.Version, "version", false, "print version info")
flag.BoolVar(&conf.Password, "password", false, "print current password")
flag.Parse()
InitLog()
}

View File

@ -3,7 +3,6 @@ package bootstrap
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers"
"github.com/Xhofe/alist/model"
log "github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
@ -72,117 +71,10 @@ func InitModel() {
default:
log.Fatalf("not supported database type: %s", databaseConfig.Type)
}
log.Infof("auto migrate model")
log.Infof("auto migrate model...")
err := conf.DB.AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{})
if err != nil {
log.Fatalf("failed to auto migrate")
}
// TODO init filetype
initAccounts()
initSettings()
}
func initAccounts() {
log.Infof("init accounts...")
var accounts []model.Account
if err := conf.DB.Find(&accounts).Error; err != nil {
log.Fatalf("failed sync init accounts")
}
for _, account := range accounts {
model.RegisterAccount(account)
driver, ok := drivers.GetDriver(account.Type)
if !ok {
log.Errorf("no [%s] driver", driver)
} else {
err := driver.Save(&account, nil)
if err != nil {
log.Errorf("init account [%s] error:[%s]", account.Name, err.Error())
}
}
}
}
func initSettings() {
log.Infof("init settings...")
version := model.SettingItem{
Key: "version",
Value: conf.GitTag,
Description: "version",
Group: model.CONST,
}
_ = model.SaveSetting(version)
settings := []model.SettingItem{
{
Key: "title",
Value: "Alist",
Description: "title",
Group: model.PUBLIC,
},
{
Key: "password",
Value: "alist",
Description: "password",
Group: model.PRIVATE,
},
{
Key: "logo",
Value: "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png",
Description: "logo",
Group: model.PUBLIC,
},
{
Key: "favicon",
Value: "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png",
Description: "favicon",
Group: model.PUBLIC,
},
{
Key: "icon color",
Value: "teal.300",
Description: "icon's color",
Group: model.PUBLIC,
},
{
Key: "text types",
Value: "txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,go,sh,c,cpp,h,hpp",
Description: "text type extensions",
},
{
Key: "readme file",
Value: "hide",
Description: "hide readme file? (show/hide)",
},
{
Key: "music cover",
Value: "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png",
Description: "music cover image",
Group: model.PUBLIC,
},
{
Key: "site beian",
Description: "chinese beian info",
Group: model.PUBLIC,
},
{
Key: "home readme url",
Description: "when have multiple, the readme file to show",
Group: model.PUBLIC,
},
}
for _, v := range settings {
_, err := model.GetSettingByKey(v.Key)
if err == gorm.ErrRecordNotFound {
err = model.SaveSetting(v)
if err != nil {
log.Fatalf("failed write setting: %s", err.Error())
}
}
}
textTypes, err := model.GetSettingByKey("text types")
if err == nil {
conf.TextTypes = strings.Split(textTypes.Value, ",")
}
}

204
bootstrap/setting.go Normal file
View File

@ -0,0 +1,204 @@
package bootstrap
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"strings"
)
func InitSettings() {
log.Infof("init settings...")
version := model.SettingItem{
Key: "version",
Value: conf.GitTag,
Description: "version",
Type: "string",
Group: model.CONST,
Version: conf.GitTag,
}
err := model.SaveSetting(version)
if err != nil {
log.Fatalf("failed write setting: %s", err.Error())
}
settings := []model.SettingItem{
{
Key: "title",
Value: "Alist",
Description: "title",
Type: "string",
Group: model.PUBLIC,
},
{
Key: "password",
Value: "alist",
Description: "password",
Type: "string",
Group: model.PRIVATE,
},
{
Key: "logo",
Value: "https://store.heytapimage.com/cdo-portal/feedback/202112/05/1542f45f86b8609495b69c5380753135.png",
Description: "logo",
Type: "string",
Group: model.PUBLIC,
},
{
Key: "favicon",
Value: "https://store.heytapimage.com/cdo-portal/feedback/202112/05/1542f45f86b8609495b69c5380753135.png",
Description: "favicon",
Type: "string",
Group: model.PUBLIC,
},
{
Key: "icon color",
Value: "#1890ff",
Description: "icon's color",
Type: "string",
Group: model.PUBLIC,
},
{
Key: "text types",
Value: strings.Join(conf.TextTypes, ","),
Type: "string",
Description: "text type extensions",
},
{
Key: "hide readme file",
Value: "true",
Type: "bool",
Description: "hide readme file? ",
},
{
Key: "music cover",
Value: "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png",
Description: "music cover image",
Type: "string",
Group: model.PUBLIC,
},
{
Key: "site beian",
Description: "chinese beian info",
Type: "string",
Group: model.PUBLIC,
},
{
Key: "home readme url",
Description: "when have multiple, the readme file to show",
Type: "string",
Group: model.PUBLIC,
},
{
Key: "markdown theme",
Value: "vuepress",
Description: "default | github | vuepress",
Group: model.PUBLIC,
Type: "select",
Values: "default,github,vuepress",
},
{
Key: "autoplay video",
Value: "false",
Type: "bool",
Group: model.PUBLIC,
},
{
Key: "autoplay audio",
Value: "false",
Type: "bool",
Group: model.PUBLIC,
},
{
Key: "check parent folder",
Value: "false",
Type: "bool",
Description: "check parent folder password",
Group: model.PRIVATE,
},
{
Key: "customize head",
Value: `<style>
.chakra-ui-light{
background-color: #FAF5FF;
}
.main-box {
border-radius: 15px !important;
box-shadow: unset !important;
}
.chakra-ui-light .main-box {
background-color: rgba(255,255,255,0.9) !important;
}
.chakra-ui-light .readme-box {
background-color: rgba(255,255,255,0.9) !important;
}
.readme-box {
border-radius: 15px !important;
box-shadow: unset !important;
}
</style>`,
Type: "text",
Description: "Customize head, placed at the beginning of the head",
Group: model.PRIVATE,
},
{
Key: "customize body",
Value: "",
Type: "text",
Description: "Customize script, placed at the end of the body",
Group: model.PRIVATE,
},
{
Key: "animation",
Value: "true",
Type: "bool",
Description: "when there are a lot of files, the animation will freeze when opening",
Group: model.PUBLIC,
},
{
Key: "check down link",
Value: "false",
Type: "bool",
Description: "check down link password, your link will be 'https://alist.com/d/filename?pw=xxx'",
Group: model.PUBLIC,
},
{
Key: "WebDAV username",
Value: "alist",
Description: "WebDAV username",
Type: "string",
Group: model.PRIVATE,
},
{
Key: "WebDAV password",
Value: "alist",
Description: "WebDAV password",
Type: "string",
Group: model.PRIVATE,
},
}
for i, _ := range settings {
v := settings[i]
v.Version = conf.GitTag
o, err := model.GetSettingByKey(v.Key)
if err != nil {
if err == gorm.ErrRecordNotFound {
err = model.SaveSetting(v)
if err != nil {
log.Fatalf("failed write setting: %s", err.Error())
}
} else {
log.Fatal("can't get setting: %s", err.Error())
}
} else {
o.Version = conf.GitTag
err = model.SaveSetting(*o)
if err != nil {
log.Fatalf("failed write setting: %s", err.Error())
}
}
}
model.LoadSettings()
}

View File

@ -1,16 +1,62 @@
#!/bin/bash
if [ "$1" == "web" ]; then
git clone https://github.com/Xhofe/alist-web.git
cd alist-web || exit
yarn
yarn build
mv dist/* ../public
cd ..
exit 0
fi
go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,https://goproxy.io,direct
if [ "$1" == "docker" ]; then
appName="alist"
builtAt="$(date +'%F %T %z')"
goVersion=$(go version | sed 's/go version //')
gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
gitCommit=$(git log --pretty=format:"%h" -1)
gitTag=$(git describe --long --tags --dirty --always)
ldflags="\
-w -s \
-X 'github.com/Xhofe/alist/conf.BuiltAt=$builtAt' \
-X 'github.com/Xhofe/alist/conf.GoVersion=$goVersion' \
-X 'github.com/Xhofe/alist/conf.GitAuthor=$gitAuthor' \
-X 'github.com/Xhofe/alist/conf.GitCommit=$gitCommit' \
-X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \
"
go build -o ./bin/alist -ldflags="$ldflags" alist.go
exit 0
fi
cd alist-web || exit
webCommit=$(git log --pretty=format:"%h" -1)
echo "web commit id: $webCommit"
yarn
if [ "$1" == "release" ]; then
yarn build --base="https://cdn.jsdelivr.net/gh/Xhofe/alist-web@cdn/v2/$webCommit"
mv dist/assets ..
mv dist/index.html ../alist/public
# 构建local
yarn build
mv dist/index.html dist/local.html
mv dist/* ../alist/public
else
yarn build
mv dist/* ../alist/public
fi
cd ..
cd alist
appName="alist"
builtAt="$(date +'%F %T %z')"
goVersion=$(go version | sed 's/go version //')
gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
gitCommit=$(git log --pretty=format:"%h" -1)
gitTag=$(git describe --long --tags --dirty --always)
if [ "$1" == "release" ]; then
gitTag=$(git describe --abbrev=0 --tags)
else
gitTag=$(git describe --abbrev=0 --tags)-next
fi
echo "build version: $gitTag"
ldflags="\
-w -s \
@ -21,19 +67,17 @@ ldflags="\
-X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \
"
cp -R ../alist-web/dist/* public
if [ "$1" == "release" ]; then
xgo -out alist -ldflags="$ldflags" .
else
xgo -targets=linux/amd64,windows/amd64,darwin/amd64 -out alist -ldflags="$ldflags" .
xgo -targets=linux/amd64,windows/amd64 -out alist -ldflags="$ldflags" .
fi
mkdir "build"
mv alist-* build
cd build || exit
upx -9 ./*
find . -type f -print0 | xargs -0 md5sum > md5.txt
cat md5.txt
# compress file (release)
if [ "$1" == "release" ]; then
mkdir compress
@ -50,4 +94,17 @@ if [ "$1" == "release" ]; then
do
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip "$i"
done
fi
cd ../..
if [ "$1" == "release" ]; then
cd alist-web
git checkout cdn
mkdir "v2/$webCommit"
mv ../assets/ v2/$webCommit
git add .
git config --local user.email "i@nn.ci"
git config --local user.name "Xhofe"
git commit --allow-empty -m "upload $webCommit assets files" -a
cd ..
fi

View File

@ -14,6 +14,10 @@ type Config struct {
Address string `json:"address"`
Port int `json:"port"`
Database Database `json:"database"`
Https bool `json:"https"`
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
Local bool `json:"local"`
}
func DefaultConfig() *Config {
@ -22,13 +26,9 @@ func DefaultConfig() *Config {
Port: 5244,
Database: Database{
Type: "sqlite3",
User: "",
Password: "",
Host: "",
Port: 0,
Name: "",
TablePrefix: "x_",
DBFile: "data.db",
DBFile: "data/data.db",
},
}
}

View File

@ -12,7 +12,7 @@ var (
GoVersion string
GitAuthor string
GitCommit string
GitTag string
GitTag string = "dev"
)
var (
@ -20,6 +20,7 @@ var (
Conf *Config
Debug bool
Version bool
Password bool
DB *gorm.DB
Cache *cache.Cache
@ -28,9 +29,23 @@ var (
)
var (
TextTypes = []string{"txt", "go", "md"}
TextTypes = []string{"txt", "htm", "html", "xml", "java", "properties", "sql",
"js", "md", "json", "conf", "ini", "vue", "php", "py", "bat", "gitignore", "yml",
"go", "sh", "c", "cpp", "h", "hpp", "tsx", "vtt", "srt", "ass"}
OfficeTypes = []string{"doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf"}
VideoTypes = []string{"mp4", "mkv", "avi", "mov", "rmvb"}
AudioTypes = []string{"mp3", "flac", "ogg"}
VideoTypes = []string{"mp4", "mkv", "avi", "mov", "rmvb", "webm"}
AudioTypes = []string{"mp3", "flac", "ogg", "m4a"}
ImageTypes = []string{"jpg", "tiff", "jpeg", "png", "gif", "bmp", "svg"}
)
// settings
var (
RawIndexHtml string
IndexHtml string
CheckParent bool
CheckDown bool
Token string
DavUsername string
DavPassword string
)

232
drivers/123/123.go Normal file
View File

@ -0,0 +1,232 @@
package _23
import (
"crypto/hmac"
"crypto/sha256"
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"math/rand"
"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"`
}
func (driver Pan123) Login(account *model.Account) error {
url := "https://www.123pan.com/api/user/sign_in"
if account.APIProxyUrl != "" {
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
}
var resp Pan123TokenResp
_, err := base.RestyClient.R().
SetResult(&resp).
SetBody(base.Json{
"passport": account.Username,
"password": account.Password,
}).Post(url)
if err != nil {
return err
}
if resp.Code != 200 {
err = fmt.Errorf(resp.Message)
account.Status = resp.Message
} else {
account.Status = "work"
account.AccessToken = resp.Data.Token
}
_ = model.SaveAccount(account)
return err
}
func (driver Pan123) FormatFile(file *Pan123File) *model.File {
f := &model.File{
Id: strconv.FormatInt(file.FileId, 10),
Name: file.FileName,
Size: file.Size,
Driver: driver.Config().Name,
UpdatedAt: file.UpdateAt,
}
if file.Type == 1 {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(filepath.Ext(file.FileName))
}
return f
}
func (driver Pan123) GetFiles(parentId string, account *model.Account) ([]Pan123File, error) {
next := "0"
res := make([]Pan123File, 0)
for next != "-1" {
var resp Pan123Files
query := map[string]string{
"driveId": "0",
"limit": "100",
"next": next,
"orderBy": account.OrderBy,
"orderDirection": account.OrderDirection,
"parentFileId": parentId,
"trashed": "false",
}
_, err := driver.Request("https://www.123pan.com/api/file/list",
base.Get, nil, query, nil, &resp, false, account)
if err != nil {
return nil, err
}
next = resp.Data.Next
res = append(res, resp.Data.InfoList...)
}
return res, nil
}
func (driver Pan123) Request(url string, method int, headers, query map[string]string, data *base.Json, resp interface{}, proxy bool, account *model.Account) ([]byte, error) {
rawUrl := url
if account.APIProxyUrl != "" && proxy {
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
}
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+account.AccessToken)
if headers != nil {
req.SetHeaders(headers)
}
if query != nil {
req.SetQueryParams(query)
}
if data != nil {
req.SetBody(data)
}
if resp != nil {
req.SetResult(resp)
}
var res *resty.Response
var err error
switch method {
case base.Get:
res, err = req.Get(url)
case base.Post:
res, err = req.Post(url)
default:
return nil, base.ErrNotSupport
}
if err != nil {
return nil, err
}
log.Debug(res.String())
body := res.Body()
code := jsoniter.Get(body, "code").ToInt()
if code != 0 {
if code == 401 {
err := driver.Login(account)
if err != nil {
return nil, err
}
return driver.Request(rawUrl, method, headers, query, data, resp, proxy, account)
}
return nil, errors.New(jsoniter.Get(body, "message").ToString())
}
return body, nil
}
//func (driver Pan123) Post(url string, data base.Json, account *model.Account) ([]byte, error) {
// res, err := pan123Client.R().
// SetHeader("authorization", "Bearer "+account.AccessToken).
// SetBody(data).Post(url)
// if err != nil {
// return nil, err
// }
// body := res.Body()
// if jsoniter.Get(body, "code").ToInt() != 0 {
// return nil, errors.New(jsoniter.Get(body, "message").ToString())
// }
// return body, nil
//}
func (driver Pan123) GetFile(path string, account *model.Account) (*Pan123File, error) {
dir, name := filepath.Split(path)
dir = utils.ParsePath(dir)
_, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
parentFiles_, _ := base.GetCache(dir, account)
parentFiles, _ := parentFiles_.([]Pan123File)
for _, file := range parentFiles {
if file.FileName == name {
if file.Type != conf.FOLDER {
return &file, err
} else {
return nil, base.ErrNotFile
}
}
}
return nil, base.ErrPathNotFound
}
func RandStr(length int) string {
str := "123456789abcdefghijklmnopqrstuvwxyz"
bytes := []byte(str)
var result []byte
rand.Seed(time.Now().UnixNano() + int64(rand.Intn(100)))
for i := 0; i < length; i++ {
result = append(result, bytes[rand.Intn(len(bytes))])
}
return string(result)
}
func HMAC(message string, secret string) string {
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte(message))
// fmt.Println(h.Sum(nil))
//sha := hex.EncodeToString(h.Sum(nil))
// fmt.Println(sha)
//return sha
return string(h.Sum(nil))
}
func init() {
base.RegisterDriver(&Pan123{})
}

355
drivers/123/driver.go Normal file
View File

@ -0,0 +1,355 @@
package _23
import (
"encoding/hex"
"encoding/xml"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"net/url"
"path/filepath"
"strconv"
"strings"
"time"
)
type Pan123 struct{}
func (driver Pan123) Config() base.DriverConfig {
return base.DriverConfig{
Name: "123Pan",
NeedSetLink: true,
}
}
func (driver Pan123) Items() []base.Item {
return []base.Item{
{
Name: "username",
Label: "username",
Type: base.TypeString,
Required: true,
Description: "account username/phone number",
},
{
Name: "password",
Label: "password",
Type: base.TypeString,
Required: true,
Description: "account password",
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
Required: false,
},
{
Name: "order_by",
Label: "order_by",
Type: base.TypeSelect,
Values: "name,fileId,updateAt,createAt",
Required: true,
},
{
Name: "order_direction",
Label: "order_direction",
Type: base.TypeSelect,
Values: "asc,desc",
Required: true,
},
}
}
func (driver Pan123) Save(account *model.Account, old *model.Account) error {
if account.RootFolder == "" {
account.RootFolder = "0"
}
err := driver.Login(account)
return err
}
func (driver Pan123) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver Pan123) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
var rawFiles []Pan123File
cache, err := base.GetCache(path, account)
if err == nil {
rawFiles, _ = cache.([]Pan123File)
} else {
file, err := driver.File(path, account)
if err != nil {
return nil, err
}
rawFiles, err = driver.GetFiles(file.Id, account)
if err != nil {
return nil, err
}
if len(rawFiles) > 0 {
_ = base.SetCache(path, rawFiles, account)
}
}
files := make([]model.File, 0)
for _, file := range rawFiles {
files = append(files, *driver.FormatFile(&file))
}
return files, nil
}
func (driver Pan123) Link(args base.Args, account *model.Account) (*base.Link, error) {
log.Debugf("%+v", args)
file, err := driver.GetFile(utils.ParsePath(args.Path), account)
if err != nil {
return nil, err
}
var resp Pan123DownResp
var headers map[string]string
if args.IP != "" && args.IP != "::1" {
headers = map[string]string{
//"X-Real-IP": "1.1.1.1",
"X-Forwarded-For": args.IP,
}
}
data := base.Json{
"driveId": 0,
"etag": file.Etag,
"fileId": file.FileId,
"fileName": file.FileName,
"s3keyFlag": file.S3KeyFlag,
"size": file.Size,
"type": file.Type,
}
_, err = driver.Request("https://www.123pan.com/api/file/download_info",
base.Post, headers, nil, &data, &resp, false, account)
//_, err = pan123Client.R().SetResult(&resp).SetHeader("authorization", "Bearer "+account.AccessToken).
// SetBody().Post("https://www.123pan.com/api/file/download_info")
if err != nil {
return nil, err
}
u, err := url.Parse(resp.Data.DownloadUrl)
if err != nil {
return nil, err
}
u_ := fmt.Sprintf("https://%s%s", u.Host, u.Path)
res, err := base.NoRedirectClient.R().SetQueryParamsFromValues(u.Query()).Get(u_)
if err != nil {
return nil, err
}
log.Debug(res.String())
link := base.Link{
Url: resp.Data.DownloadUrl,
}
if res.StatusCode() == 302 {
link.Url = res.Header().Get("location")
}
return &link, nil
}
func (driver Pan123) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
log.Debugf("pan123 path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver Pan123) Proxy(c *gin.Context, account *model.Account) {
c.Request.Header.Del("origin")
}
func (driver Pan123) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver Pan123) MakeDir(path string, account *model.Account) error {
dir, name := filepath.Split(path)
parentFile, err := driver.File(dir, account)
if err != nil {
return err
}
if !parentFile.IsDir() {
return base.ErrNotFolder
}
parentFileId, _ := strconv.Atoi(parentFile.Id)
data := base.Json{
"driveId": 0,
"etag": "",
"fileName": name,
"parentFileId": parentFileId,
"size": 0,
"type": 1,
}
_, err = driver.Request("https://www.123pan.com/api/file/upload_request",
base.Post, nil, nil, &data, nil, false, account)
//_, err = driver.Post("https://www.123pan.com/api/file/upload_request", data, account)
if err == nil {
_ = base.DeleteCache(dir, account)
}
return err
}
func (driver Pan123) Move(src string, dst string, account *model.Account) error {
srcDir, _ := filepath.Split(src)
dstDir, dstName := filepath.Split(dst)
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
fileId, _ := strconv.Atoi(srcFile.Id)
// rename
if srcDir == dstDir {
data := base.Json{
"driveId": 0,
"fileId": fileId,
"fileName": dstName,
}
_, err = driver.Request("https://www.123pan.com/api/file/rename",
base.Post, nil, nil, &data, nil, false, account)
//_, err = driver.Post("https://www.123pan.com/api/file/rename", data, account)
} else {
// move
dstDirFile, err := driver.File(dstDir, account)
if err != nil {
return err
}
parentFileId, _ := strconv.Atoi(dstDirFile.Id)
data := base.Json{
"fileId": fileId,
"parentFileId": parentFileId,
}
_, err = driver.Request("https://www.123pan.com/api/file/mod_pid",
base.Post, nil, nil, &data, nil, false, account)
//_, err = driver.Post("https://www.123pan.com/api/file/mod_pid", data, account)
}
if err != nil {
_ = base.DeleteCache(srcDir, account)
_ = base.DeleteCache(dstDir, account)
}
return err
}
func (driver Pan123) Copy(src string, dst string, account *model.Account) error {
return base.ErrNotSupport
}
func (driver Pan123) Delete(path string, account *model.Account) error {
file, err := driver.GetFile(path, account)
if err != nil {
return err
}
data := base.Json{
"driveId": 0,
"operation": true,
"fileTrashInfoList": file,
}
_, err = driver.Request("https://www.123pan.com/api/file/trash",
base.Post, nil, nil, &data, nil, false, account)
//_, err = driver.Post("https://www.123pan.com/api/file/trash", data, account)
if err == nil {
_ = base.DeleteCache(utils.Dir(path), account)
}
return err
}
type UploadResp struct {
XMLName xml.Name `xml:"InitiateMultipartUploadResult"`
Bucket string `xml:"Bucket"`
Key string `xml:"Key"`
UploadId string `xml:"UploadId"`
}
// TODO unfinished
func (driver Pan123) Upload(file *model.FileStream, account *model.Account) error {
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
}
if !parentFile.IsDir() {
return base.ErrNotFolder
}
parentFileId, _ := strconv.Atoi(parentFile.Id)
data := base.Json{
"driveId": 0,
"duplicate": true,
"etag": RandStr(32), //maybe file's md5
"fileName": file.GetFileName(),
"parentFileId": parentFileId,
"size": file.GetSize(),
"type": 0,
}
res, err := driver.Request("https://www.123pan.com/api/file/upload_request",
base.Post, nil, nil, &data, nil, false, account)
//res, err := driver.Post("https://www.123pan.com/api/file/upload_request", data, account)
if err != nil {
return err
}
baseUrl := fmt.Sprintf("https://file.123pan.com/%s/%s", jsoniter.Get(res, "data.Bucket").ToString(), jsoniter.Get(res, "data.Key").ToString())
var resp UploadResp
kSecret := jsoniter.Get(res, "data.SecretAccessKey").ToString()
nowTimeStr := time.Now().String()
Date := strings.ReplaceAll(strings.Split(nowTimeStr, "T")[0], "-", "")
StringToSign := fmt.Sprintf("%s\n%s\n%s\n%s",
"AWS4-HMAC-SHA256",
nowTimeStr,
fmt.Sprintf("%s/us-east-1/s3/aws4_request", Date),
)
kDate := HMAC("AWS4"+kSecret, Date)
kRegion := HMAC(kDate, "us-east-1")
kService := HMAC(kRegion, "s3")
kSigning := HMAC(kService, "aws4_request")
_, err = base.RestyClient.R().SetResult(&resp).SetHeaders(map[string]string{
"Authorization": fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=%s",
jsoniter.Get(res, "data.AccessKeyId"),
Date,
hex.EncodeToString([]byte(HMAC(StringToSign, kSigning)))),
"X-Amz-Content-Sha256": "UNSIGNED-PAYLOAD",
"X-Amz-Date": nowTimeStr,
"x-amz-security-token": jsoniter.Get(res, "data.SessionToken").ToString(),
}).Post(fmt.Sprintf("%s?uploads", baseUrl))
if err != nil {
return err
}
return base.ErrNotImplement
}
var _ base.Driver = (*Pan123)(nil)

491
drivers/189/189.go Normal file
View File

@ -0,0 +1,491 @@
package _89
import (
"crypto/aes"
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
mathRand "math/rand"
"net/url"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
var client189Map map[string]*resty.Client
func (driver Cloud189) FormatFile(file *Cloud189File) *model.File {
f := &model.File{
Id: strconv.FormatInt(file.Id, 10),
Name: file.Name,
Size: file.Size,
Driver: driver.Config().Name,
UpdatedAt: nil,
Thumbnail: file.Icon.SmallUrl,
Url: file.Url,
}
loc, _ := time.LoadLocation("Local")
lastOpTime, err := time.ParseInLocation("2006-01-02 15:04:05", file.LastOpTime, loc)
if err == nil {
f.UpdatedAt = &lastOpTime
}
if file.Size == -1 {
f.Type = conf.FOLDER
f.Size = 0
} else {
f.Type = utils.GetFileType(filepath.Ext(file.Name))
}
return f
}
//func (c Cloud189) GetFile(path string, account *model.Account) (*Cloud189File, error) {
// dir, name := filepath.Split(path)
// dir = utils.ParsePath(dir)
// _, _, err := c.ParentPath(dir, account)
// if err != nil {
// return nil, err
// }
// parentFiles_, _ := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, dir))
// parentFiles, _ := parentFiles_.([]Cloud189File)
// for _, file := range parentFiles {
// if file.Name == name {
// if file.Size != -1 {
// return &file, err
// } else {
// return nil, ErrNotFile
// }
// }
// }
// return nil, ErrPathNotFound
//}
type Cloud189Down struct {
ResCode int `json:"res_code"`
ResMessage string `json:"res_message"`
FileDownloadUrl string `json:"fileDownloadUrl"`
}
type LoginResp struct {
Msg string `json:"msg"`
Result int `json:"result"`
ToUrl string `json:"toUrl"`
}
// Login refer to PanIndex
func (driver Cloud189) Login(account *model.Account) error {
client, ok := client189Map[account.Name]
if !ok {
//cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
client = resty.New()
//client.SetCookieJar(cookieJar)
client.SetRetryCount(3)
client.SetHeader("Referer", "https://cloud.189.cn/")
}
url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
b := ""
lt := ""
ltText := regexp.MustCompile(`lt = "(.+?)"`)
for i := 0; i < 3; i++ {
res, err := client.R().Get(url)
if err != nil {
return err
}
b = res.String()
ltTextArr := ltText.FindStringSubmatch(b)
if len(ltTextArr) > 0 {
lt = ltTextArr[1]
break
} else {
<-time.After(time.Second)
}
}
if lt == "" {
return fmt.Errorf("get empty login page")
}
captchaToken := regexp.MustCompile(`captchaToken' value='(.+?)'`).FindStringSubmatch(b)[1]
returnUrl := regexp.MustCompile(`returnUrl = '(.+?)'`).FindStringSubmatch(b)[1]
paramId := regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(b)[1]
//reqId := regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(b)[1]
jRsakey := regexp.MustCompile(`j_rsaKey" value="(\S+)"`).FindStringSubmatch(b)[1]
vCodeID := regexp.MustCompile(`picCaptcha\.do\?token\=([A-Za-z0-9\&\=]+)`).FindStringSubmatch(b)[1]
vCodeRS := ""
if vCodeID != "" {
// need ValidateCode
}
userRsa := RsaEncode([]byte(account.Username), jRsakey)
passwordRsa := RsaEncode([]byte(account.Password), jRsakey)
url = "https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do"
var loginResp LoginResp
res, err := client.R().
SetHeaders(map[string]string{
"lt": lt,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
"Referer": "https://open.e.189.cn/",
"accept": "application/json;charset=UTF-8",
}).SetFormData(map[string]string{
"appKey": "cloud",
"accountType": "01",
"userName": "{RSA}" + userRsa,
"password": "{RSA}" + passwordRsa,
"validateCode": vCodeRS,
"captchaToken": captchaToken,
"returnUrl": returnUrl,
"mailSuffix": "@pan.cn",
"paramId": paramId,
"clientType": "10010",
"dynamicCheck": "FALSE",
"cb_SaveName": "1",
"isOauth2": "false",
}).Post(url)
if err != nil {
return err
}
err = json.Unmarshal(res.Body(), &loginResp)
if err != nil {
log.Error(err.Error())
return err
}
if loginResp.Result != 0 {
return fmt.Errorf(loginResp.Msg)
}
_, err = client.R().Get(loginResp.ToUrl)
if err != nil {
log.Errorf(err.Error())
return err
}
client189Map[account.Name] = client
return nil
}
type Cloud189Error struct {
ErrorCode string `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
}
type Cloud189File struct {
Id int64 `json:"id"`
LastOpTime string `json:"lastOpTime"`
Name string `json:"name"`
Size int64 `json:"size"`
Icon struct {
SmallUrl string `json:"smallUrl"`
//LargeUrl string `json:"largeUrl"`
} `json:"icon"`
Url string `json:"url"`
}
type Cloud189Folder struct {
Id int64 `json:"id"`
LastOpTime string `json:"lastOpTime"`
Name string `json:"name"`
}
type Cloud189Files struct {
ResCode int `json:"res_code"`
ResMessage string `json:"res_message"`
FileListAO struct {
Count int `json:"count"`
FileList []Cloud189File `json:"fileList"`
FolderList []Cloud189Folder `json:"folderList"`
} `json:"fileListAO"`
}
func (driver Cloud189) GetFiles(fileId string, account *model.Account) ([]Cloud189File, error) {
client, ok := client189Map[account.Name]
if !ok {
return nil, fmt.Errorf("can't find [%s] client", account.Name)
}
res := make([]Cloud189File, 0)
pageNum := 1
for {
var e Cloud189Error
var resp Cloud189Files
_, err := client.R().SetResult(&resp).SetError(&e).
SetHeader("Accept", "application/json;charset=UTF-8").
SetQueryParams(map[string]string{
"noCache": random(),
"pageSize": "60",
"pageNum": strconv.Itoa(pageNum),
"mediaType": "0",
"folderId": fileId,
"iconOption": "5",
"orderBy": account.OrderBy,
"descending": account.OrderDirection,
}).Get("https://cloud.189.cn/api/open/file/listFiles.action")
if err != nil {
return nil, err
}
if e.ErrorCode != "" {
if e.ErrorCode == "InvalidSessionKey" {
err = driver.Login(account)
if err != nil {
return nil, err
}
return driver.GetFiles(fileId, account)
}
}
if resp.ResCode != 0 {
return nil, fmt.Errorf(resp.ResMessage)
}
if resp.FileListAO.Count == 0 {
break
}
res = append(res, resp.FileListAO.FileList...)
for _, folder := range resp.FileListAO.FolderList {
res = append(res, Cloud189File{
Id: folder.Id,
LastOpTime: folder.LastOpTime,
Name: folder.Name,
Size: -1,
})
}
pageNum++
}
return res, nil
}
func (driver Cloud189) Request(url string, method string, form map[string]string, headers map[string]string, account *model.Account) ([]byte, error) {
client, ok := client189Map[account.Name]
if !ok {
return nil, fmt.Errorf("can't find [%s] client", account.Name)
}
//var resp base.Json
var e Cloud189Error
req := client.R().SetError(&e).
SetHeader("Accept", "application/json;charset=UTF-8").
SetQueryParams(map[string]string{
"noCache": random(),
})
if form != nil {
req = req.SetFormData(form)
}
if headers != nil {
req = req.SetHeaders(headers)
}
var err error
var res *resty.Response
if strings.ToUpper(method) == "GET" {
res, err = req.Get(url)
} else {
res, err = req.Post(url)
}
if err != nil {
return nil, err
}
if e.ErrorCode != "" {
if e.ErrorCode == "InvalidSessionKey" {
err = driver.Login(account)
if err != nil {
return nil, err
}
return driver.Request(url, method, form, nil, account)
}
}
//log.Debug(res, jsoniter.Get(res.Body(),"res_code").ToInt())
if jsoniter.Get(res.Body(),"res_code").ToInt() != 0 {
err = errors.New(jsoniter.Get(res.Body(),"res_message").ToString())
}
return res.Body(), err
}
func (driver Cloud189) GetSessionKey(account *model.Account) (string, error) {
resp, err := driver.Request("https://cloud.189.cn/v2/getUserBriefInfo.action", "GET", nil, nil, account)
if err != nil {
return "", err
}
return jsoniter.Get(resp, "sessionKey").ToString(), nil
}
func (driver Cloud189) GetResKey(account *model.Account) (string, string, error) {
resp, err := driver.Request("https://cloud.189.cn/api/security/generateRsaKey.action", "GET", nil, nil, account)
if err != nil {
return "", "", err
}
return jsoniter.Get(resp, "pubKey").ToString(), jsoniter.Get(resp, "pkId").ToString(), nil
}
func (driver Cloud189) UploadRequest(url string, form map[string]string, account *model.Account) ([]byte, error) {
sessionKey, err := driver.GetSessionKey(account)
if err != nil {
return nil, err
}
pubKey, pkId, err := driver.GetResKey(account)
if err != nil {
return nil, err
}
xRId := "e007e99a-370c-4a14-a143-1b1541972fcf"
pkey := strings.ReplaceAll(xRId, "-", "")
params := aesEncrypt(qs(form), pkey[:16])
date := strconv.FormatInt(time.Now().Unix(), 10)
signature := hmacSha1(fmt.Sprintf("SessionKey=%s&Operate=GET&RequestURI=%s&Date=%s&params=%s", sessionKey, url, date, params), pkey)
encryptionText := RsaEncode([]byte(pkey), pubKey)
res, err := base.RestyClient.R().SetHeaders(map[string]string{
"signature": signature,
"sessionKey": sessionKey,
"encryptionText": encryptionText,
"pkId": pkId,
"x-request-id": xRId,
"x-request-date": date,
"origin": "https://cloud.189.cn",
"referer": "https://cloud.189.cn/",
}).SetQueryParam("params", params).Get("https://upload.cloud.189.cn" + url)
if err != nil {
return nil, err
}
log.Debug(res.String())
data := res.Body()
if jsoniter.Get(data, "code").ToString() != "SUCCESS" {
return nil, errors.New(jsoniter.Get(data, "msg").ToString())
}
return data, nil
}
func random() string {
return fmt.Sprintf("0.%17v", mathRand.New(mathRand.NewSource(time.Now().UnixNano())).Int63n(100000000000000000))
}
func RsaEncode(origData []byte, j_rsakey string) string {
publicKey := []byte("-----BEGIN PUBLIC KEY-----\n" + j_rsakey + "\n-----END PUBLIC KEY-----")
block, _ := pem.Decode(publicKey)
pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
pub := pubInterface.(*rsa.PublicKey)
b, err := rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
if err != nil {
log.Errorf("err: %s", err.Error())
}
return b64tohex(base64.StdEncoding.EncodeToString(b))
}
var b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"
func int2char(a int) string {
return strings.Split(BI_RM, "")[a]
}
func b64tohex(a string) string {
d := ""
e := 0
c := 0
for i := 0; i < len(a); i++ {
m := strings.Split(a, "")[i]
if m != "=" {
v := strings.Index(b64map, m)
if 0 == e {
e = 1
d += int2char(v >> 2)
c = 3 & v
} else if 1 == e {
e = 2
d += int2char(c<<2 | v>>4)
c = 15 & v
} else if 2 == e {
e = 3
d += int2char(c)
d += int2char(v >> 2)
c = 3 & v
} else {
e = 0
d += int2char(c<<2 | v>>4)
d += int2char(15 & v)
}
}
}
if e == 1 {
d += int2char(c << 2)
}
return d
}
func qs(form map[string]string) string {
strList := make([]string, 0)
for k, v := range form {
strList = append(strList, fmt.Sprintf("%s=%s", k, url.QueryEscape(v)))
}
return strings.Join(strList, "&")
}
func aesEncrypt(data, key string) string {
encrypted := AesEncryptECB([]byte(data), []byte(key))
//return string(encrypted)
return hex.EncodeToString(encrypted)
}
func hmacSha1(data string, secret string) string {
h := hmac.New(sha1.New, []byte(secret))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
func AesEncryptECB(origData []byte, key []byte) (encrypted []byte) {
cipher, _ := aes.NewCipher(generateKey(key))
length := (len(origData) + aes.BlockSize) / aes.BlockSize
plain := make([]byte, length*aes.BlockSize)
copy(plain, origData)
pad := byte(len(plain) - len(origData))
for i := len(origData); i < len(plain); i++ {
plain[i] = pad
}
encrypted = make([]byte, len(plain))
// 分组分块加密
for bs, be := 0, cipher.BlockSize(); bs <= len(origData); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
cipher.Encrypt(encrypted[bs:be], plain[bs:be])
}
return encrypted
}
func AesDecryptECB(encrypted []byte, key []byte) (decrypted []byte) {
cipher, _ := aes.NewCipher(generateKey(key))
decrypted = make([]byte, len(encrypted))
//
for bs, be := 0, cipher.BlockSize(); bs < len(encrypted); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
cipher.Decrypt(decrypted[bs:be], encrypted[bs:be])
}
trim := 0
if len(decrypted) > 0 {
trim = len(decrypted) - int(decrypted[len(decrypted)-1])
}
return decrypted[:trim]
}
func generateKey(key []byte) (genKey []byte) {
genKey = make([]byte, 16)
copy(genKey, key)
for i := 16; i < len(key); {
for j := 0; j < 16 && i < len(key); j, i = j+1, i+1 {
genKey[j] ^= key[i]
}
}
return genKey
}
func getMd5(data []byte) []byte {
h := md5.New()
h.Write(data)
return h.Sum(nil)
}
func init() {
base.RegisterDriver(&Cloud189{})
client189Map = make(map[string]*resty.Client, 0)
}

442
drivers/189/driver.go Normal file
View File

@ -0,0 +1,442 @@
package _89
import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"io"
"math"
"net/http"
"path/filepath"
"strconv"
"strings"
)
type Cloud189 struct{}
func (driver Cloud189) Config() base.DriverConfig {
return base.DriverConfig{
Name: "189Cloud",
NeedSetLink: true,
}
}
func (driver Cloud189) Items() []base.Item {
return []base.Item{
{
Name: "username",
Label: "username",
Type: base.TypeString,
Required: true,
Description: "account username/phone number",
},
{
Name: "password",
Label: "password",
Type: base.TypeString,
Required: true,
Description: "account password",
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
Required: true,
},
{
Name: "order_by",
Label: "order_by",
Type: base.TypeSelect,
Values: "name,size,lastOpTime,createdDate",
Required: true,
},
{
Name: "order_direction",
Label: "desc",
Type: base.TypeSelect,
Values: "true,false",
Required: true,
},
}
}
func (driver Cloud189) Save(account *model.Account, old *model.Account) error {
if old != nil && old.Name != account.Name {
delete(client189Map, old.Name)
}
if err := driver.Login(account); err != nil {
account.Status = err.Error()
_ = model.SaveAccount(account)
return err
}
account.Status = "work"
err := model.SaveAccount(account)
if err != nil {
return err
}
return nil
}
func (driver Cloud189) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver Cloud189) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
var rawFiles []Cloud189File
cache, err := base.GetCache(path, account)
if err == nil {
rawFiles, _ = cache.([]Cloud189File)
} else {
file, err := driver.File(path, account)
if err != nil {
return nil, err
}
rawFiles, err = driver.GetFiles(file.Id, account)
if err != nil {
return nil, err
}
if len(rawFiles) > 0 {
_ = base.SetCache(path, rawFiles, account)
}
}
files := make([]model.File, 0)
for _, file := range rawFiles {
files = append(files, *driver.FormatFile(&file))
}
return files, nil
}
func (driver Cloud189) Link(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(utils.ParsePath(args.Path), account)
if err != nil {
return nil, err
}
if file.Type == conf.FOLDER {
return nil, base.ErrNotFile
}
client, ok := client189Map[account.Name]
if !ok {
return nil, fmt.Errorf("can't find [%s] client", account.Name)
}
var e Cloud189Error
var resp Cloud189Down
_, err = client.R().SetResult(&resp).SetError(&e).
SetHeader("Accept", "application/json;charset=UTF-8").
SetQueryParams(map[string]string{
"noCache": random(),
"fileId": file.Id,
}).Get("https://cloud.189.cn/api/open/file/getFileDownloadUrl.action")
if err != nil {
return nil, err
}
if e.ErrorCode != "" {
if e.ErrorCode == "InvalidSessionKey" {
err = driver.Login(account)
if err != nil {
return nil, err
}
return driver.Link(args, account)
}
}
if resp.ResCode != 0 {
return nil, fmt.Errorf(resp.ResMessage)
}
res, err := base.NoRedirectClient.R().Get(resp.FileDownloadUrl)
if err != nil {
return nil, err
}
link := base.Link{}
if res.StatusCode() == 302 {
link.Url = res.Header().Get("location")
} else {
link.Url = resp.FileDownloadUrl
}
return &link, nil
}
func (driver Cloud189) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
log.Debugf("189 path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver Cloud189) Proxy(ctx *gin.Context, account *model.Account) {
ctx.Request.Header.Del("Origin")
}
func (driver Cloud189) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver Cloud189) MakeDir(path string, account *model.Account) error {
dir, name := filepath.Split(path)
parent, err := driver.File(dir, account)
if err != nil {
return err
}
if !parent.IsDir() {
return base.ErrNotFolder
}
form := map[string]string{
"parentFolderId": parent.Id,
"folderName": name,
}
_, err = driver.Request("https://cloud.189.cn/api/open/file/createFolder.action", "POST", form, nil, account)
if err == nil {
_ = base.DeleteCache(dir, account)
}
return err
}
func (driver Cloud189) Move(src string, dst string, account *model.Account) error {
srcDir, _ := filepath.Split(src)
dstDir, dstName := filepath.Split(dst)
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
// rename
if srcDir == dstDir {
url := "https://cloud.189.cn/api/open/file/renameFile.action"
idKey := "fileId"
nameKey := "destFileName"
if srcFile.IsDir() {
url = "https://cloud.189.cn/api/open/file/renameFolder.action"
idKey = "folderId"
nameKey = "destFolderName"
}
form := map[string]string{
idKey: srcFile.Id,
nameKey: dstName,
}
_, err = driver.Request(url, "POST", form, nil, account)
} else {
// move
dstDirFile, err := driver.File(dstDir, account)
if err != nil {
return err
}
isFolder := 0
if srcFile.IsDir() {
isFolder = 1
}
taskInfos := []base.Json{
{
"fileId": srcFile.Id,
"fileName": dstName,
"isFolder": isFolder,
},
}
taskInfosBytes, err := json.Marshal(taskInfos)
if err != nil {
return err
}
form := map[string]string{
"type": "MOVE",
"targetFolderId": dstDirFile.Id,
"taskInfos": string(taskInfosBytes),
}
_, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", "POST", form, nil, account)
}
if err == nil {
_ = base.DeleteCache(srcDir, account)
_ = base.DeleteCache(dstDir, account)
}
return err
}
func (driver Cloud189) Copy(src string, dst string, account *model.Account) error {
dstDir, dstName := filepath.Split(dst)
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
dstDirFile, err := driver.File(dstDir, account)
if err != nil {
return err
}
isFolder := 0
if srcFile.IsDir() {
isFolder = 1
}
taskInfos := []base.Json{
{
"fileId": srcFile.Id,
"fileName": dstName,
"isFolder": isFolder,
},
}
taskInfosBytes, err := json.Marshal(taskInfos)
if err != nil {
return err
}
form := map[string]string{
"type": "COPY",
"targetFolderId": dstDirFile.Id,
"taskInfos": string(taskInfosBytes),
}
_, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", "POST", form, nil, account)
if err == nil {
_ = base.DeleteCache(dstDir, account)
}
return err
}
func (driver Cloud189) Delete(path string, account *model.Account) error {
path = utils.ParsePath(path)
file, err := driver.File(path, account)
if err != nil {
return err
}
isFolder := 0
if file.IsDir() {
isFolder = 1
}
taskInfos := []base.Json{
{
"fileId": file.Id,
"fileName": file.Name,
"isFolder": isFolder,
},
}
taskInfosBytes, err := json.Marshal(taskInfos)
if err != nil {
return err
}
form := map[string]string{
"type": "DELETE",
"targetFolderId": "",
"taskInfos": string(taskInfosBytes),
}
_, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", "POST", form, nil, account)
if err == nil {
_ = base.DeleteCache(utils.Dir(path), account)
}
return err
}
// Upload Error: decrypt encryptionText failed
func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) error {
const DEFAULT uint64 = 10485760
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
var finish uint64 = 0
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
}
if !parentFile.IsDir() {
return base.ErrNotFolder
}
res, err := driver.UploadRequest("/person/initMultiUpload", map[string]string{
"parentFolderId": parentFile.Id,
"fileName": file.Name,
"fileSize": strconv.FormatInt(int64(file.Size), 10),
"sliceSize": strconv.FormatInt(int64(DEFAULT), 10),
"lazyCheck": "1",
}, account)
if err != nil {
return err
}
uploadFileId := jsoniter.Get(res, "data.uploadFileId").ToString()
var i int64
var byteSize uint64
md5s := make([]string, 0)
md5Sum := md5.New()
for i = 1; i <= count; i++ {
byteSize = file.GetSize() - finish
if DEFAULT < byteSize {
byteSize = DEFAULT
}
log.Debugf("%d,%d", byteSize, finish)
byteData := make([]byte, byteSize)
n, err := io.ReadFull(file, byteData)
log.Debug(err, n)
if err != nil {
return err
}
finish += uint64(n)
md5Bytes := getMd5(byteData)
md5Str := hex.EncodeToString(md5Bytes)
md5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)
md5s = append(md5s, md5Str)
md5Sum.Write(byteData)
res, err = driver.UploadRequest("/person/getMultiUploadUrls", map[string]string{
"partInfo": fmt.Sprintf("%s-%s", strconv.FormatInt(i, 10), md5Base64),
"uploadFileId": uploadFileId,
}, account)
if err != nil {
return err
}
uploadData := jsoniter.Get(res, "uploadUrls.partNumber_"+strconv.FormatInt(i, 10))
headers := strings.Split(uploadData.Get("requestHeader").ToString(), "&")
req, err := http.NewRequest("PUT", uploadData.Get("requestURL").ToString(), bytes.NewBuffer(byteData))
if err != nil {
return err
}
for _, header := range headers {
kv := strings.Split(header, "=")
req.Header.Set(kv[0], strings.Join(kv[1:], "="))
}
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
log.Debugf("%+v", res)
}
id := md5Sum.Sum(nil)
res, err = driver.UploadRequest("/person/commitMultiUploadFile", map[string]string{
"uploadFileId": uploadFileId,
"fileMd5": hex.EncodeToString(id),
"sliceMd5": utils.GetMD5Encode(strings.Join(md5s, "\n")),
"lazyCheck": "1",
}, account)
if err == nil {
_ = base.DeleteCache(file.ParentPath, account)
}
return err
}
var _ base.Driver = (*Cloud189)(nil)

View File

@ -1,373 +0,0 @@
package drivers
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
"github.com/gofiber/fiber/v2"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
"path/filepath"
"time"
)
var aliClient = resty.New()
func init() {
RegisterDriver("AliDrive", &AliDrive{})
aliClient.
SetRetryCount(3).
SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36").
SetHeader("content-type", "application/json").
SetHeader("origin", "https://aliyundrive.com")
}
type AliDrive struct{}
func (a AliDrive) Preview(path string, account *model.Account) (interface{}, error) {
file, err := a.GetFile(path, account)
if err != nil {
return nil, err
}
// office
var resp Json
var e AliRespError
var url string
req := Json{
"drive_id": account.DriveId,
"file_id": file.FileId,
}
switch file.Category {
case "doc":
{
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
req["access_token"] = account.AccessToken
}
case "video":
{
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
req["category"] = "live_transcoding"
}
default:
return nil, fmt.Errorf("don't support")
}
_, err = aliClient.R().SetResult(&resp).SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(req).Post(url)
if err != nil {
return nil, err
}
if e.Code != "" {
return nil, fmt.Errorf("%s", e.Message)
}
return resp, nil
}
func (a AliDrive) Items() []Item {
return []Item{
{
Name: "refresh_token",
Label: "refresh token",
Type: "string",
Required: true,
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: "string",
Required: false,
},
{
Name: "limit",
Label: "limit",
Type: "number",
Required: false,
Description: ">0 and <=200",
},
}
}
func (a AliDrive) Proxy(ctx *fiber.Ctx) {
ctx.Request().Header.Del("Origin")
ctx.Request().Header.Set("Referer", "https://www.aliyundrive.com/")
}
type AliRespError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type AliFiles struct {
Items []AliFile `json:"items"`
NextMarker string `json:"next_marker"`
}
type AliFile struct {
DriveId string `json:"drive_id"`
CreatedAt *time.Time `json:"created_at"`
FileExtension string `json:"file_extension"`
FileId string `json:"file_id"`
Type string `json:"type"`
Name string `json:"name"`
Category string `json:"category"`
ParentFileId string `json:"parent_file_id"`
UpdatedAt *time.Time `json:"updated_at"`
Size int64 `json:"size"`
Thumbnail string `json:"thumbnail"`
Url string `json:"url"`
}
func (a AliDrive) FormatFile(file *AliFile) *model.File {
f := &model.File{
Name: file.Name,
Size: file.Size,
UpdatedAt: file.UpdatedAt,
Thumbnail: file.Thumbnail,
Driver: "AliDrive",
Url: file.Url,
}
if file.Type == "folder" {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(file.FileExtension)
}
if file.Category == "video" {
f.Type = conf.VIDEO
}
if file.Category == "image" {
f.Type = conf.IMAGE
}
return f
}
func (a AliDrive) GetFiles(fileId string, account *model.Account) ([]AliFile, error) {
marker := "first"
res := make([]AliFile, 0)
for marker != "" {
if marker == "first" {
marker = ""
}
var resp AliFiles
var e AliRespError
_, err := aliClient.R().
SetResult(&resp).
SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(Json{
"drive_id": account.DriveId,
"fields": "*",
"image_thumbnail_process": "image/resize,w_400/format,jpeg",
"image_url_process": "image/resize,w_1920/format,jpeg",
"limit": account.Limit,
"marker": marker,
"order_by": account.OrderBy,
"order_direction": account.OrderDirection,
"parent_file_id": fileId,
"video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300",
"url_expire_sec": 14400,
}).Post("https://api.aliyundrive.com/v2/file/list")
if err != nil {
return nil, err
}
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = a.RefreshToken(account)
if err != nil {
return nil, err
} else {
_ = model.SaveAccount(*account)
return a.GetFiles(fileId, account)
}
}
return nil, fmt.Errorf("%s", e.Message)
}
marker = resp.NextMarker
res = append(res, resp.Items...)
}
return res, nil
}
func (a AliDrive) GetFile(path string, account *model.Account) (*AliFile, error) {
dir, name := filepath.Split(path)
dir = utils.ParsePath(dir)
_, _, err := a.Path(dir, account)
if err != nil {
return nil, err
}
parentFiles_, _ := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, dir))
parentFiles, _ := parentFiles_.([]AliFile)
for _, file := range parentFiles {
if file.Name == name {
if file.Type == "file" {
return &file, err
} else {
return nil, fmt.Errorf("not file")
}
}
}
return nil, fmt.Errorf("path not found")
}
// path: /aaa/bbb
func (a AliDrive) Path(path string, account *model.Account) (*model.File, []*model.File, error) {
path = utils.ParsePath(path)
log.Debugf("ali path: %s", path)
cache, err := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path))
if err == nil {
file, ok := cache.(AliFile)
if ok {
return a.FormatFile(&file), nil, nil
} else {
files, _ := cache.([]AliFile)
res := make([]*model.File, 0)
for _, file = range files {
res = append(res, a.FormatFile(&file))
}
return nil, res, nil
}
} else {
fileId := account.RootFolder
if path != "/" {
dir, name := filepath.Split(path)
dir = utils.ParsePath(dir)
_, _, err = a.Path(dir, account)
if err != nil {
return nil, nil, err
}
parentFiles_, _ := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, dir))
parentFiles, _ := parentFiles_.([]AliFile)
found := false
for _, file := range parentFiles {
if file.Name == name {
found = true
if file.Type == "file" {
url, err := a.Link(path, account)
if err != nil {
return nil, nil, err
}
file.Url = url
return a.FormatFile(&file), nil, nil
} else {
fileId = file.FileId
break
}
}
}
if !found {
return nil, nil, fmt.Errorf("path not found")
}
}
files, err := a.GetFiles(fileId, account)
if err != nil {
return nil, nil, err
}
_ = conf.Cache.Set(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path), files, nil)
res := make([]*model.File, 0)
for _, file := range files {
res = append(res, a.FormatFile(&file))
}
return nil, res, nil
}
}
func (a AliDrive) Link(path string, account *model.Account) (string, error) {
file, err := a.GetFile(utils.ParsePath(path), account)
if err != nil {
return "", err
}
var resp Json
var e AliRespError
_, err = aliClient.R().SetResult(&resp).
SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(Json{
"drive_id": account.DriveId,
"file_id": file.FileId,
"expire_sec": 14400,
}).Post("https://api.aliyundrive.com/v2/file/get_download_url")
if err != nil {
return "", err
}
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = a.RefreshToken(account)
if err != nil {
return "", err
} else {
_ = model.SaveAccount(*account)
return a.Link(path, account)
}
}
return "", fmt.Errorf("%s", e.Message)
}
return resp["url"].(string), nil
}
func (a AliDrive) RefreshToken(account *model.Account) error {
url := "https://auth.aliyundrive.com/v2/account/token"
var resp TokenResp
var e AliRespError
_, err := aliClient.R().
//ForceContentType("application/json").
SetBody(Json{"refresh_token": account.RefreshToken, "grant_type": "refresh_token"}).
SetResult(&resp).
SetError(&e).
Post(url)
if err != nil {
return err
}
log.Debugf("%+v,%+v", resp, e)
if e.Code != "" {
return fmt.Errorf("failed to refresh token: %s", e.Message)
}
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
return nil
}
func (a AliDrive) Save(account *model.Account, old *model.Account) error {
if old != nil {
conf.Cron.Remove(cron.EntryID(old.CronId))
}
if account.RootFolder == "" {
account.RootFolder = "root"
}
if account.Limit == 0 {
account.Limit = 200
}
err := a.RefreshToken(account)
if err != nil {
return err
}
var resp Json
_, _ = aliClient.R().SetResult(&resp).
SetBody("{}").
SetHeader("authorization", "Bearer\t"+account.AccessToken).
Post("https://api.aliyundrive.com/v2/user/get")
log.Debugf("user info: %+v", resp)
account.DriveId = resp["default_drive_id"].(string)
cronId, err := conf.Cron.AddFunc("@every 2h", func() {
name := account.Name
newAccount, ok := model.GetAccount(name)
if !ok {
return
}
err = a.RefreshToken(&newAccount)
if err != nil {
newAccount.Status = err.Error()
}
_ = model.SaveAccount(newAccount)
})
if err != nil {
return err
}
account.CronId = int(cronId)
err = model.SaveAccount(*account)
if err != nil {
return err
}
return nil
}
var _ Driver = (*AliDrive)(nil)

View File

@ -0,0 +1,243 @@
package alidrive
import (
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"path/filepath"
"strings"
"time"
)
var aliClient = resty.New()
type AliRespError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type AliFiles struct {
Items []AliFile `json:"items"`
NextMarker string `json:"next_marker"`
}
type AliFile struct {
DriveId string `json:"drive_id"`
CreatedAt *time.Time `json:"created_at"`
FileExtension string `json:"file_extension"`
FileId string `json:"file_id"`
Type string `json:"type"`
Name string `json:"name"`
Category string `json:"category"`
ParentFileId string `json:"parent_file_id"`
UpdatedAt *time.Time `json:"updated_at"`
Size int64 `json:"size"`
Thumbnail string `json:"thumbnail"`
Url string `json:"url"`
}
func (driver AliDrive) FormatFile(file *AliFile) *model.File {
f := &model.File{
Id: file.FileId,
Name: file.Name,
Size: file.Size,
UpdatedAt: file.UpdatedAt,
Thumbnail: file.Thumbnail,
Driver: driver.Config().Name,
Url: file.Url,
}
if file.Type == "folder" {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(file.FileExtension)
}
if file.Category == "video" {
f.Type = conf.VIDEO
}
if file.Category == "image" {
f.Type = conf.IMAGE
}
return f
}
func (driver AliDrive) GetFiles(fileId string, account *model.Account) ([]AliFile, error) {
marker := "first"
res := make([]AliFile, 0)
for marker != "" {
if marker == "first" {
marker = ""
}
var resp AliFiles
var e AliRespError
_, err := aliClient.R().
SetResult(&resp).
SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(base.Json{
"drive_id": account.DriveId,
"fields": "*",
"image_thumbnail_process": "image/resize,w_400/format,jpeg",
"image_url_process": "image/resize,w_1920/format,jpeg",
"limit": account.Limit,
"marker": marker,
"order_by": account.OrderBy,
"order_direction": account.OrderDirection,
"parent_file_id": fileId,
"video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300",
"url_expire_sec": 14400,
}).Post("https://api.aliyundrive.com/v2/file/list")
if err != nil {
return nil, err
}
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = driver.RefreshToken(account)
if err != nil {
return nil, err
} else {
_ = model.SaveAccount(account)
return driver.GetFiles(fileId, account)
}
}
return nil, fmt.Errorf("%s", e.Message)
}
marker = resp.NextMarker
res = append(res, resp.Items...)
}
return res, nil
}
func (driver AliDrive) GetFile(path string, account *model.Account) (*AliFile, error) {
dir, name := filepath.Split(path)
dir = utils.ParsePath(dir)
_, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
parentFiles_, _ := base.GetCache(dir, account)
parentFiles, _ := parentFiles_.([]AliFile)
for _, file := range parentFiles {
if file.Name == name {
if file.Type == "file" {
return &file, err
} else {
return nil, fmt.Errorf("not file")
}
}
}
return nil, base.ErrPathNotFound
}
func (driver AliDrive) RefreshToken(account *model.Account) error {
url := "https://auth.aliyundrive.com/v2/account/token"
var resp base.TokenResp
var e AliRespError
_, err := aliClient.R().
//ForceContentType("application/json").
SetBody(base.Json{"refresh_token": account.RefreshToken, "grant_type": "refresh_token"}).
SetResult(&resp).
SetError(&e).
Post(url)
if err != nil {
account.Status = err.Error()
return err
}
log.Debugf("%+v,%+v", resp, e)
if e.Code != "" {
account.Status = e.Message
return fmt.Errorf("failed to refresh token: %s", e.Message)
} else {
account.Status = "work"
}
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
return nil
}
func (driver AliDrive) Rename(fileId, name string, account *model.Account) error {
var resp base.Json
var e AliRespError
_, err := aliClient.R().SetResult(&resp).SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(base.Json{
"check_name_mode": "refuse",
"drive_id": account.DriveId,
"file_id": fileId,
"name": name,
}).Post("https://api.aliyundrive.com/v3/file/update")
if err != nil {
return err
}
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = driver.RefreshToken(account)
if err != nil {
return err
} else {
_ = model.SaveAccount(account)
return driver.Rename(fileId, name, account)
}
}
return fmt.Errorf("%s", e.Message)
}
if resp["name"] == name {
return nil
}
return fmt.Errorf("%+v", resp)
}
func (driver AliDrive) Batch(srcId,dstId string, account *model.Account) error {
var e AliRespError
res, err := aliClient.R().SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(base.Json{
"requests": []base.Json{
{
"headers": base.Json{
"Content-Type": "application/json",
},
"method":"POST",
"id":srcId,
"body":base.Json{
"drive_id": account.DriveId,
"file_id":srcId,
"to_drive_id":account.DriveId,
"to_parent_file_id":dstId,
},
},
},
"resource": "file",
}).Post("https://api.aliyundrive.com/v3/batch")
if err != nil {
return err
}
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = driver.RefreshToken(account)
if err != nil {
return err
} else {
_ = model.SaveAccount(account)
return driver.Batch(srcId, dstId, account)
}
}
return fmt.Errorf("%s", e.Message)
}
if strings.Contains(res.String(), `"status":200`) {
return nil
}
return errors.New(res.String())
}
func init() {
base.RegisterDriver(&AliDrive{})
aliClient.
SetRetryCount(3).
SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36").
SetHeader("content-type", "application/json").
SetHeader("origin", "https://www.aliyundrive.com")
}

478
drivers/alidrive/driver.go Normal file
View File

@ -0,0 +1,478 @@
package alidrive
import (
"bytes"
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
"io"
"math"
"net/http"
"path/filepath"
)
type AliDrive struct{}
func (driver AliDrive) Config() base.DriverConfig {
return base.DriverConfig{
Name: "AliDrive",
NeedSetLink: true,
}
}
func (driver AliDrive) Items() []base.Item {
return []base.Item{
{
Name: "order_by",
Label: "order_by",
Type: base.TypeSelect,
Values: "name,size,updated_at,created_at",
Required: false,
},
{
Name: "order_direction",
Label: "order_direction",
Type: base.TypeSelect,
Values: "ASC,DESC",
Required: false,
},
{
Name: "refresh_token",
Label: "refresh token",
Type: base.TypeString,
Required: true,
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
Required: false,
},
{
Name: "limit",
Label: "limit",
Type: base.TypeNumber,
Required: false,
Description: ">0 and <=200",
},
}
}
func (driver AliDrive) Save(account *model.Account, old *model.Account) error {
if old != nil {
conf.Cron.Remove(cron.EntryID(old.CronId))
}
if account.RootFolder == "" {
account.RootFolder = "root"
}
if account.Limit == 0 {
account.Limit = 200
}
err := driver.RefreshToken(account)
if err != nil {
return err
}
var resp base.Json
_, _ = aliClient.R().SetResult(&resp).
SetBody("{}").
SetHeader("authorization", "Bearer\t"+account.AccessToken).
Post("https://api.aliyundrive.com/v2/user/get")
log.Debugf("user info: %+v", resp)
account.DriveId = resp["default_drive_id"].(string)
cronId, err := conf.Cron.AddFunc("@every 2h", func() {
name := account.Name
log.Debugf("ali account name: %s", name)
newAccount, ok := model.GetAccount(name)
log.Debugf("ali account: %+v", newAccount)
if !ok {
return
}
err = driver.RefreshToken(&newAccount)
_ = model.SaveAccount(&newAccount)
})
if err != nil {
return err
}
account.CronId = int(cronId)
err = model.SaveAccount(account)
if err != nil {
return err
}
return nil
}
func (driver AliDrive) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver AliDrive) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
var rawFiles []AliFile
cache, err := base.GetCache(path, account)
if err == nil {
rawFiles, _ = cache.([]AliFile)
} else {
file, err := driver.File(path, account)
if err != nil {
return nil, err
}
rawFiles, err = driver.GetFiles(file.Id, account)
if err != nil {
return nil, err
}
if len(rawFiles) > 0 {
_ = base.SetCache(path, rawFiles, account)
}
}
files := make([]model.File, 0)
for _, file := range rawFiles {
files = append(files, *driver.FormatFile(&file))
}
return files, nil
}
func (driver AliDrive) Link(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(args.Path, account)
if err != nil {
return nil, err
}
var resp base.Json
var e AliRespError
_, err = aliClient.R().SetResult(&resp).
SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(base.Json{
"drive_id": account.DriveId,
"file_id": file.Id,
"expire_sec": 14400,
}).Post("https://api.aliyundrive.com/v2/file/get_download_url")
if err != nil {
return nil, err
}
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = driver.RefreshToken(account)
if err != nil {
return nil, err
} else {
_ = model.SaveAccount(account)
return driver.Link(args, account)
}
}
return nil, fmt.Errorf("%s", e.Message)
}
return &base.Link{
Url: resp["url"].(string),
}, nil
}
func (driver AliDrive) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
log.Debugf("ali path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver AliDrive) Proxy(c *gin.Context, account *model.Account) {
c.Request.Header.Del("Origin")
c.Request.Header.Set("Referer", "https://www.aliyundrive.com/")
}
func (driver AliDrive) Preview(path string, account *model.Account) (interface{}, error) {
file, err := driver.GetFile(path, account)
if err != nil {
return nil, err
}
// office
var resp base.Json
var e AliRespError
var url string
req := base.Json{
"drive_id": account.DriveId,
"file_id": file.FileId,
}
switch file.Category {
case "doc":
{
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
req["access_token"] = account.AccessToken
}
case "video":
{
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
req["category"] = "live_transcoding"
}
default:
return nil, base.ErrNotSupport
}
_, err = aliClient.R().SetResult(&resp).SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(req).Post(url)
if err != nil {
return nil, err
}
if e.Code != "" {
return nil, fmt.Errorf("%s", e.Message)
}
return resp, nil
}
func (driver AliDrive) MakeDir(path string, account *model.Account) error {
dir, name := filepath.Split(path)
parentFile, err := driver.File(dir, account)
if err != nil {
return err
}
if !parentFile.IsDir() {
return base.ErrNotFolder
}
var resp base.Json
var e AliRespError
_, err = aliClient.R().SetResult(&resp).SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(base.Json{
"check_name_mode": "refuse",
"drive_id": account.DriveId,
"name": name,
"parent_file_id": parentFile.Id,
"type": "folder",
}).Post("https://api.aliyundrive.com/adrive/v2/file/createWithFolders")
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = driver.RefreshToken(account)
if err != nil {
return err
} else {
_ = model.SaveAccount(account)
return driver.MakeDir(path, account)
}
}
return fmt.Errorf("%s", e.Message)
}
if resp["file_name"] == name {
_ = base.DeleteCache(dir, account)
return nil
}
return fmt.Errorf("%+v", resp)
}
func (driver AliDrive) Move(src string, dst string, account *model.Account) error {
srcDir, _ := filepath.Split(src)
dstDir, dstName := filepath.Split(dst)
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
// rename
if srcDir == dstDir {
err = driver.Rename(srcFile.Id, dstName, account)
} else {
// move
dstDirFile, err := driver.File(dstDir, account)
if err != nil {
return err
}
err = driver.Batch(srcFile.Id, dstDirFile.Id, account)
}
if err != nil {
_ = base.DeleteCache(srcDir, account)
_ = base.DeleteCache(dstDir, account)
}
return err
}
func (driver AliDrive) Copy(src string, dst string, account *model.Account) error {
return base.ErrNotSupport
}
func (driver AliDrive) Delete(path string, account *model.Account) error {
file, err := driver.File(path, account)
if err != nil {
return err
}
var e AliRespError
res, err := aliClient.R().SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(base.Json{
"drive_id": account.DriveId,
"file_id": file.Id,
}).Post("https://api.aliyundrive.com/v2/recyclebin/trash")
if err != nil {
return err
}
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = driver.RefreshToken(account)
if err != nil {
return err
} else {
_ = model.SaveAccount(account)
return driver.Delete(path, account)
}
}
return fmt.Errorf("%s", e.Message)
}
if res.StatusCode() == 204 {
_ = base.DeleteCache(utils.Dir(path), account)
return nil
}
return errors.New(res.String())
}
type UploadResp struct {
FileId string `json:"file_id"`
UploadId string `json:"upload_id"`
PartInfoList []struct {
UploadUrl string `json:"upload_url"`
} `json:"part_info_list"`
}
func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) error {
const DEFAULT uint64 = 10485760
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
var finish uint64 = 0
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
}
if !parentFile.IsDir() {
return base.ErrNotFolder
}
var resp UploadResp
var e AliRespError
partInfoList := make([]base.Json, 0)
var i int64
for i = 0; i < count; i++ {
partInfoList = append(partInfoList, base.Json{
"part_number": i + 1,
})
}
_, err = aliClient.R().SetResult(&resp).SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(base.Json{
"check_name_mode": "auto_rename",
// content_hash
"content_hash_name": "none",
"drive_id": account.DriveId,
"name": file.GetFileName(),
"parent_file_id": parentFile.Id,
"part_info_list": partInfoList,
//proof_code
"proof_version": "v1",
"size": file.GetSize(),
"type": "file",
}).Post("https://api.aliyundrive.com/adrive/v2/file/createWithFolders") // /v2/file/create_with_proof
//log.Debugf("%+v\n%+v", resp, e)
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = driver.RefreshToken(account)
if err != nil {
return err
} else {
_ = model.SaveAccount(account)
return driver.Upload(file, account)
}
}
return fmt.Errorf("%s", e.Message)
}
var byteSize uint64
for i = 0; i < count; i++ {
byteSize = file.GetSize() - finish
if DEFAULT < byteSize {
byteSize = DEFAULT
}
log.Debugf("%d,%d", byteSize, finish)
byteData := make([]byte, byteSize)
n, err := io.ReadFull(file, byteData)
//n, err := file.Read(byteData)
//byteData, err := io.ReadAll(file)
//n := len(byteData)
log.Debug(err, n)
if err != nil {
return err
}
finish += uint64(n)
req, err := http.NewRequest("PUT", resp.PartInfoList[i].UploadUrl, bytes.NewBuffer(byteData))
if err != nil {
return err
}
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
log.Debugf("%+v", res)
//res, err := base.BaseClient.R().
// SetHeader("Content-Type","").
// SetBody(byteData).Put(resp.PartInfoList[i].UploadUrl)
//if err != nil {
// return err
//}
//log.Debugf("put to %s : %d,%s", resp.PartInfoList[i].UploadUrl, res.StatusCode(),res.String())
}
var resp2 base.Json
_, err = aliClient.R().SetResult(&resp2).SetError(&e).
SetHeader("authorization", "Bearer\t"+account.AccessToken).
SetBody(base.Json{
"drive_id": account.DriveId,
"file_id": resp.FileId,
"upload_id": resp.UploadId,
}).Post("https://api.aliyundrive.com/v2/file/complete")
if e.Code != "" {
//if e.Code == "AccessTokenInvalid" {
// err = driver.RefreshToken(account)
// if err != nil {
// return err
// } else {
// _ = model.SaveAccount(account)
// return driver.Upload(file, account)
// }
//}
return fmt.Errorf("%s", e.Message)
}
if resp2["file_id"] == resp.FileId {
_ = base.DeleteCache(file.ParentPath, account)
return nil
}
return fmt.Errorf("%+v", resp2)
}
var _ base.Driver = (*AliDrive)(nil)

40
drivers/alist/alist.go Normal file
View File

@ -0,0 +1,40 @@
package alist
import (
"errors"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
)
type BaseResp struct {
Code int `json:"code"`
Message string `json:"message"`
}
type PathResp struct {
BaseResp
Data []model.File `json:"data"`
}
type PreviewResp struct {
BaseResp
Data interface{} `json:"data"`
}
func (driver *Alist) Login(account *model.Account) error {
var resp BaseResp
_, err := base.RestyClient.R().SetResult(&resp).
SetHeader("Authorization", account.AccessToken).
Get(account.SiteUrl+"/api/admin/login")
if err != nil {
return err
}
if resp.Code != 200 {
return errors.New(resp.Message)
}
return nil
}
func init() {
base.RegisterDriver(&Alist{})
}

185
drivers/alist/driver.go Normal file
View File

@ -0,0 +1,185 @@
package alist
import (
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
"path/filepath"
"strings"
"time"
)
type Alist struct{}
func (driver Alist) Config() base.DriverConfig {
return base.DriverConfig{
Name: "Alist",
OnlyProxy: false,
}
}
func (driver Alist) Items() []base.Item {
return []base.Item{
{
Name: "site_url",
Label: "alist site url",
Type: base.TypeString,
Required: true,
},
{
Name: "access_token",
Label: "token",
Type: base.TypeString,
Description: "admin token",
Required: true,
},
{
Name: "root_folder",
Label: "root folder path",
Type: base.TypeString,
Required: false,
},
}
}
func (driver Alist) Save(account *model.Account, old *model.Account) error {
account.SiteUrl = strings.TrimRight(account.SiteUrl, "/")
if account.RootFolder == "" {
account.RootFolder = "/"
}
err := driver.Login(account)
if err == nil {
account.Status = "work"
} else {
account.Status = err.Error()
}
_ = model.SaveAccount(account)
return err
}
func (driver Alist) File(path string, account *model.Account) (*model.File, error) {
now := time.Now()
if path == "/" {
return &model.File{
Id: "root",
Name: "root",
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: &now,
}, nil
}
_, files, err := driver.Path(utils.Dir(path), account)
if err != nil {
return nil, err
}
if files == nil {
return nil, base.ErrPathNotFound
}
name := utils.Base(path)
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver Alist) Files(path string, account *model.Account) ([]model.File, error) {
//return nil, base.ErrNotImplement
_, files, err := driver.Path(path, account)
if err != nil {
return nil, err
}
if files == nil {
return nil, base.ErrNotFolder
}
return files, nil
}
func (driver Alist) Link(args base.Args, account *model.Account) (*base.Link, error) {
path := args.Path
path = utils.ParsePath(path)
name := utils.Base(path)
flag := "d"
if utils.GetFileType(filepath.Ext(path)) == conf.TEXT {
flag = "p"
}
link := base.Link{}
link.Url = fmt.Sprintf("%s/%s%s?sign=%s", account.SiteUrl, flag, path, utils.SignWithToken(name, conf.Token))
return &link, nil
}
func (driver Alist) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
path = filepath.Join(account.RootFolder, path)
path = strings.ReplaceAll(path, "\\", "/")
cache, err := base.GetCache(path, account)
if err == nil {
files := cache.([]model.File)
return nil, files, nil
}
var resp PathResp
_, err = base.RestyClient.R().SetResult(&resp).
SetHeader("Authorization", account.AccessToken).
SetBody(base.Json{
"path": path,
}).Post(account.SiteUrl + "/api/public/path")
if err != nil {
return nil, nil, err
}
if resp.Code != 200 {
return nil, nil, errors.New(resp.Message)
}
if resp.Message == "file" {
return &resp.Data[0], nil, nil
}
if len(resp.Data) > 0 {
_ = base.SetCache(path, resp.Data, account)
}
return nil, resp.Data, nil
}
func (driver Alist) Proxy(c *gin.Context, account *model.Account) {}
func (driver Alist) Preview(path string, account *model.Account) (interface{}, error) {
var resp PathResp
_, err := base.RestyClient.R().SetResult(&resp).
SetHeader("Authorization", account.AccessToken).
SetBody(base.Json{
"path": path,
}).Post(account.SiteUrl + "/api/public/preview")
if err != nil {
return nil, err
}
if resp.Code != 200 {
return nil, errors.New(resp.Message)
}
return resp.Data, nil
}
func (driver Alist) MakeDir(path string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver Alist) Move(src string, dst string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver Alist) Copy(src string, dst string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver Alist) Delete(path string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver Alist) Upload(file *model.FileStream, account *model.Account) error {
return base.ErrNotImplement
}
var _ base.Driver = (*Alist)(nil)

14
drivers/all.go Normal file
View File

@ -0,0 +1,14 @@
package drivers
import (
_ "github.com/Xhofe/alist/drivers/123"
_ "github.com/Xhofe/alist/drivers/189"
_ "github.com/Xhofe/alist/drivers/alidrive"
_ "github.com/Xhofe/alist/drivers/alist"
_ "github.com/Xhofe/alist/drivers/ftp"
_ "github.com/Xhofe/alist/drivers/google"
_ "github.com/Xhofe/alist/drivers/lanzou"
_ "github.com/Xhofe/alist/drivers/native"
_ "github.com/Xhofe/alist/drivers/onedrive"
_ "github.com/Xhofe/alist/drivers/pikpak"
)

28
drivers/base/cache.go Normal file
View File

@ -0,0 +1,28 @@
package base
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
log "github.com/sirupsen/logrus"
)
func KeyCache(path string, account *model.Account) string {
path = utils.ParsePath(path)
return fmt.Sprintf("%s%s", account.Name, path)
}
func SetCache(path string, obj interface{}, account *model.Account) error {
return conf.Cache.Set(conf.Ctx, KeyCache(path, account), obj, nil)
}
func GetCache(path string, account *model.Account) (interface{}, error) {
return conf.Cache.Get(conf.Ctx, KeyCache(path, account))
}
func DeleteCache(path string, account *model.Account) error {
err := conf.Cache.Delete(conf.Ctx, KeyCache(path, account))
log.Debugf("delete cache %s: %+v", path, err)
return err
}

142
drivers/base/driver.go Normal file
View File

@ -0,0 +1,142 @@
package base
import (
"github.com/Xhofe/alist/model"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"net/http"
)
type DriverConfig struct {
Name string
OnlyProxy bool
NoLink bool // 必须本机返回的
ApiProxy bool // 使用API中转的
NeedSetLink bool // 需要设置链接的
}
type Args struct {
Path string
IP string
}
type Driver interface {
// Config 配置
Config() DriverConfig
// Items 账号所需参数
Items() []Item
// Save 保存时处理
Save(account *model.Account, old *model.Account) error
// File 取文件
File(path string, account *model.Account) (*model.File, error)
// Files 取文件夹
Files(path string, account *model.Account) ([]model.File, error)
// Link 取链接
Link(args Args, account *model.Account) (*Link, error)
// Path 取路径(文件或文件夹)
Path(path string, account *model.Account) (*model.File, []model.File, error)
// Proxy 代理处理
Proxy(c *gin.Context, account *model.Account)
// Preview 预览
Preview(path string, account *model.Account) (interface{}, error)
// MakeDir 创建文件夹
MakeDir(path string, account *model.Account) error
// Move 移动/改名
Move(src string, dst string, account *model.Account) error
// Copy 拷贝
Copy(src string, dst string, account *model.Account) error
// Delete 删除
Delete(path string, account *model.Account) error
// Upload 上传
Upload(file *model.FileStream, account *model.Account) error
// TODO
//Search(path string, keyword string, account *model.Account) ([]*model.File, error)
}
type Item struct {
Name string `json:"name"`
Label string `json:"label"`
Type string `json:"type"`
Values string `json:"values"`
Required bool `json:"required"`
Description string `json:"description"`
}
var driversMap = map[string]Driver{}
func RegisterDriver(driver Driver) {
log.Infof("register driver: [%s]", driver.Config().Name)
driversMap[driver.Config().Name] = driver
}
func GetDriver(name string) (driver Driver, ok bool) {
driver, ok = driversMap[name]
return
}
func GetDrivers() map[string][]Item {
res := make(map[string][]Item, 0)
for k, v := range driversMap {
if v.Config().OnlyProxy {
res[k] = v.Items()
} else {
res[k] = append([]Item{
//{
// Name: "allow_proxy",
// Label: "allow_proxy",
// Type: TypeBool,
// Required: true,
// Description: "allow proxy",
//},
{
Name: "proxy",
Label: "proxy",
Type: TypeBool,
Required: true,
Description: "web proxy",
},
{
Name: "webdav_proxy",
Label: "webdav proxy",
Type: TypeBool,
Required: true,
Description: "Transfer the WebDAV of this account through the server",
},
}, v.Items()...)
}
res[k] = append([]Item{
{
Name: "down_proxy_url",
Label: "down_proxy_url",
Type: TypeString,
},
}, res[k]...)
if v.Config().ApiProxy {
res[k] = append([]Item{
{
Name: "api_proxy_url",
Label: "api_proxy_url",
Type: TypeString,
},
}, res[k]...)
}
}
return res
}
var NoRedirectClient *resty.Client
var RestyClient = resty.New()
var HttpClient = &http.Client{}
func init() {
NoRedirectClient = resty.New().SetRedirectPolicy(
resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}),
)
userAgent := "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
NoRedirectClient.SetHeader("user-agent", userAgent)
RestyClient.SetHeader("user-agent", userAgent)
RestyClient.SetRetryCount(3)
}

46
drivers/base/types.go Normal file
View File

@ -0,0 +1,46 @@
package base
import (
"errors"
)
var (
ErrPathNotFound = errors.New("path not found")
ErrNotFile = errors.New("not file")
ErrNotImplement = errors.New("not implement")
ErrNotSupport = errors.New("not support")
ErrNotFolder = errors.New("not a folder")
)
const (
TypeString = "string"
TypeSelect = "select"
TypeBool = "bool"
TypeNumber = "number"
)
const (
Get = iota
Post
Put
Delete
Patch
)
type Json map[string]interface{}
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type Header struct{
Name string `json:"name"`
Value string `json:"value"`
}
type Link struct {
Url string `json:"url"`
Headers []Header `json:"headers"`
Data []byte
}

View File

@ -1,55 +0,0 @@
package drivers
import (
"github.com/Xhofe/alist/model"
"github.com/gofiber/fiber/v2"
)
type Driver interface {
Items() []Item
Path(path string, account *model.Account) (*model.File, []*model.File, error)
Link(path string, account *model.Account) (string, error)
Save(account *model.Account, old *model.Account) error
Proxy(ctx *fiber.Ctx)
Preview(path string, account *model.Account) (interface{}, error)
// TODO
//MakeDir(path string, account *model.Account) error
//Move(src string, des string, account *model.Account) error
//Delete(path string) error
//Upload(file *fs.File, path string, account *model.Account) error
}
type Item struct {
Name string `json:"name"`
Label string `json:"label"`
Type string `json:"type"`
Values string `json:"values"`
Required bool `json:"required"`
Description string `json:"description"`
}
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
var driversMap = map[string]Driver{}
func RegisterDriver(name string, driver Driver) {
driversMap[name] = driver
}
func GetDriver(name string) (driver Driver, ok bool) {
driver, ok = driversMap[name]
return
}
func GetDrivers() map[string][]Item {
res := make(map[string][]Item, 0)
for k, v := range driversMap {
res[k] = v.Items()
}
return res
}
type Json map[string]interface{}

265
drivers/ftp/driver.go Normal file
View File

@ -0,0 +1,265 @@
package ftp
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
"github.com/jlaffaye/ftp"
log "github.com/sirupsen/logrus"
"io/ioutil"
"path/filepath"
)
type FTP struct{}
func (driver FTP) Config() base.DriverConfig {
return base.DriverConfig{
Name: "FTP",
OnlyProxy: true,
NoLink: true,
}
}
func (driver FTP) Items() []base.Item {
return []base.Item{
{
Name: "site_url",
Label: "ftp host url",
Type: base.TypeString,
Required: true,
},
{
Name: "username",
Label: "username",
Type: base.TypeString,
Required: true,
},
{
Name: "password",
Label: "password",
Type: base.TypeString,
Required: true,
},
{
Name: "root_folder",
Label: "root folder path",
Type: base.TypeString,
Required: false,
},
{
Name: "order_by",
Label: "order_by",
Type: base.TypeSelect,
Values: "name,size,updated_at",
Required: false,
},
{
Name: "order_direction",
Label: "order_direction",
Type: base.TypeSelect,
Values: "ASC,DESC",
Required: false,
},
}
}
func (driver FTP) Save(account *model.Account, old *model.Account) error {
if account.RootFolder == "" {
account.RootFolder = "/"
}
conn, err := driver.Login(account)
if err != nil {
account.Status = err.Error()
} else {
account.Status = "work"
_ = conn.Quit()
}
_ = model.SaveAccount(account)
return err
}
func (driver FTP) File(path string, account *model.Account) (*model.File, error) {
log.Debugf("file: %s", path)
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver FTP) Files(path string, account *model.Account) ([]model.File, error) {
log.Debugf("files: %s", path)
path = utils.ParsePath(path)
cache, err := base.GetCache(path, account)
if err == nil {
files, _ := cache.([]model.File)
return files, nil
}
realPath := utils.Join(account.RootFolder, path)
conn, err := driver.Login(account)
if err != nil {
return nil, err
}
defer func() { _ = conn.Quit() }()
entries, err := conn.List(realPath)
if err != nil {
return nil, err
}
res := make([]model.File, 0)
for i, _ := range entries {
entry := entries[i]
f := model.File{
Name: entry.Name,
Size: int64(entry.Size),
UpdatedAt: &entry.Time,
Driver: driver.Config().Name,
}
if entry.Type == ftp.EntryTypeFolder {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(filepath.Ext(entry.Name))
}
res = append(res, f)
}
if len(res) > 0 {
_ = base.SetCache(path, res, account)
}
return res, nil
}
func (driver FTP) Link(args base.Args, account *model.Account) (*base.Link, error) {
path := args.Path
path = utils.ParsePath(path)
realPath := utils.Join(account.RootFolder, path)
conn, err := driver.Login(account)
if err != nil {
return nil, err
}
defer func() { _ = conn.Quit() }()
resp, err := conn.Retr(realPath)
if err != nil {
return nil, err
}
defer func() { _ = resp.Close() }()
data, err := ioutil.ReadAll(resp)
if err != nil {
return nil, err
}
return &base.Link{
Data: data,
}, nil
}
func (driver FTP) Path(path string, account *model.Account) (*model.File, []model.File, error) {
log.Debugf("ftp path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
//file.Url, _ = driver.Link(path, account)
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
model.SortFiles(files, account)
return nil, files, nil
}
func (driver FTP) Proxy(c *gin.Context, account *model.Account) {
}
func (driver FTP) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver FTP) MakeDir(path string, account *model.Account) error {
path = utils.ParsePath(path)
realPath := utils.Join(account.RootFolder, path)
conn, err := driver.Login(account)
if err != nil {
return err
}
defer func() { _ = conn.Quit() }()
err = conn.MakeDir(realPath)
if err == nil {
_ = base.DeleteCache(utils.Dir(path), account)
}
return err
}
func (driver FTP) Move(src string, dst string, account *model.Account) error {
//if utils.Dir(src) != utils.Dir(dst) {
// return base.ErrNotSupport
//}
realSrc := utils.Join(account.RootFolder, src)
realDst := utils.Join(account.RootFolder, dst)
conn, err := driver.Login(account)
if err != nil {
return err
}
defer func() { _ = conn.Quit() }()
err = conn.Rename(realSrc, realDst)
if err != nil {
_ = base.DeleteCache(utils.Dir(src), account)
_ = base.DeleteCache(utils.Dir(dst), account)
}
return err
}
func (driver FTP) Copy(src string, dst string, account *model.Account) error {
return base.ErrNotSupport
}
func (driver FTP) Delete(path string, account *model.Account) error {
path = utils.ParsePath(path)
realPath := utils.Join(account.RootFolder, path)
conn, err := driver.Login(account)
if err != nil {
return err
}
defer func() { _ = conn.Quit() }()
err = conn.Delete(realPath)
if err == nil {
_ = base.DeleteCache(utils.Dir(path), account)
}
return err
}
func (driver FTP) Upload(file *model.FileStream, account *model.Account) error {
realPath := utils.Join(account.RootFolder, file.ParentPath, file.Name)
conn, err := driver.Login(account)
if err != nil {
return err
}
defer func() { _ = conn.Quit() }()
err = conn.Stor(realPath, file)
if err == nil {
_ = base.DeleteCache(utils.Dir(file.ParentPath), account)
}
return err
}
var _ base.Driver = (*FTP)(nil)

23
drivers/ftp/ftp.go Normal file
View File

@ -0,0 +1,23 @@
package ftp
import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/jlaffaye/ftp"
)
func (driver FTP) Login(account *model.Account) (*ftp.ServerConn, error) {
conn, err := ftp.Connect(account.SiteUrl)
if err != nil {
return nil, err
}
err = conn.Login(account.Username, account.Password)
if err != nil {
return nil, err
}
return conn, nil
}
func init() {
base.RegisterDriver(&FTP{})
}

190
drivers/google/driver.go Normal file
View File

@ -0,0 +1,190 @@
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/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"path/filepath"
)
type GoogleDrive struct{}
func (driver GoogleDrive) Config() base.DriverConfig {
return base.DriverConfig{
Name: "GoogleDrive",
OnlyProxy: true,
ApiProxy: true,
}
}
func (driver GoogleDrive) Items() []base.Item {
return []base.Item{
{
Name: "client_id",
Label: "client id",
Type: base.TypeString,
Required: true,
},
{
Name: "client_secret",
Label: "client secret",
Type: base.TypeString,
Required: true,
},
{
Name: "refresh_token",
Label: "refresh token",
Type: base.TypeString,
Required: true,
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
Required: false,
},
}
}
func (driver GoogleDrive) Save(account *model.Account, old *model.Account) error {
account.Proxy = true
err := driver.RefreshToken(account)
if err != nil {
account.Status = err.Error()
_ = model.SaveAccount(account)
return err
}
if account.RootFolder == "" {
account.RootFolder = "root"
}
account.Status = "work"
_ = model.SaveAccount(account)
return nil
}
func (driver GoogleDrive) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver GoogleDrive) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
var rawFiles []File
cache, err := base.GetCache(path, account)
if err == nil {
rawFiles, _ = cache.([]File)
} else {
file, err := driver.File(path, account)
if err != nil {
return nil, err
}
rawFiles, err = driver.GetFiles(file.Id, account)
if err != nil {
return nil, err
}
if len(rawFiles) > 0 {
_ = base.SetCache(path, rawFiles, account)
}
}
files := make([]model.File, 0)
for _, file := range rawFiles {
files = append(files, *driver.FormatFile(&file))
}
return files, nil
}
func (driver GoogleDrive) Link(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(args.Path, account)
if err != nil {
return nil, err
}
if file.Type == conf.FOLDER {
return nil, base.ErrNotFile
}
url := fmt.Sprintf("https://www.googleapis.com/drive/v3/files/%s?includeItemsFromAllDrives=true&supportsAllDrives=true", file.Id)
_, err = driver.Request(url, base.Get, nil, nil, nil, nil, nil, account)
if err != nil {
return nil, err
}
link := base.Link{
Url: url + "&alt=media",
Headers: []base.Header{
{
Name: "Authorization",
Value: "Bearer " + account.AccessToken,
},
},
}
return &link, nil
}
func (driver GoogleDrive) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
log.Debugf("google path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver GoogleDrive) Proxy(c *gin.Context, account *model.Account) {
c.Request.Header.Add("Authorization", "Bearer "+account.AccessToken)
}
func (driver GoogleDrive) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver GoogleDrive) MakeDir(path string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver GoogleDrive) Move(src string, dst string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver GoogleDrive) Copy(src string, dst string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver GoogleDrive) Delete(path string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver GoogleDrive) Upload(file *model.FileStream, account *model.Account) error {
return base.ErrNotImplement
}
var _ base.Driver = (*GoogleDrive)(nil)

View File

@ -0,0 +1,179 @@
package google
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"path/filepath"
"strconv"
"time"
)
type TokenError struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}
func (driver GoogleDrive) RefreshToken(account *model.Account) error {
url := "https://www.googleapis.com/oauth2/v4/token"
if account.APIProxyUrl != "" {
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
}
var resp base.TokenResp
var e TokenError
res, err := base.RestyClient.R().SetResult(&resp).SetError(&e).
SetFormData(map[string]string{
"client_id": account.ClientId,
"client_secret": account.ClientSecret,
"refresh_token": account.RefreshToken,
"grant_type": "refresh_token",
}).Post(url)
if err != nil {
return err
}
log.Debug(res.String())
if e.Error != "" {
return fmt.Errorf(e.Error)
}
account.AccessToken = resp.AccessToken
account.Status = "work"
return nil
}
type File struct {
Id string `json:"id"`
Name string `json:"name"`
MimeType string `json:"mimeType"`
ModifiedTime *time.Time `json:"modifiedTime"`
Size string `json:"size"`
}
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) *model.File {
f := &model.File{
Id: file.Id,
Name: file.Name,
Driver: driver.Config().Name,
UpdatedAt: file.ModifiedTime,
Thumbnail: "",
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))
}
return f
}
type Files struct {
NextPageToken string `json:"nextPageToken"`
Files []File `json:"files"`
}
type Error struct {
Error struct {
Errors []struct {
Domain string `json:"domain"`
Reason string `json:"reason"`
Message string `json:"message"`
LocationType string `json:"location_type"`
Location string `json:"location"`
}
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func (driver GoogleDrive) GetFiles(id string, account *model.Account) ([]File, error) {
pageToken := "first"
res := make([]File, 0)
for pageToken != "" {
if pageToken == "first" {
pageToken = ""
}
var resp Files
query := map[string]string{
"orderBy": "folder,name,modifiedTime desc",
"fields": "files(id,name,mimeType,size,modifiedTime),nextPageToken",
"pageSize": "1000",
"q": fmt.Sprintf("'%s' in parents and trashed = false", id),
"includeItemsFromAllDrives": "true",
"supportsAllDrives": "true",
"pageToken": pageToken,
}
_, err := driver.Request("https://www.googleapis.com/drive/v3/files",
base.Get, nil, query, nil, nil, &resp, account)
if err != nil {
return nil, err
}
pageToken = resp.NextPageToken
res = append(res, resp.Files...)
}
return res, nil
}
func (driver GoogleDrive) Request(url string, method int, headers, query, form map[string]string, data *base.Json, resp interface{}, account *model.Account) ([]byte, error) {
rawUrl := url
if account.APIProxyUrl != "" {
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
}
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+account.AccessToken)
if headers != nil {
req.SetHeaders(headers)
}
if query != nil {
req.SetQueryParams(query)
}
if form != nil {
req.SetFormData(form)
}
if data != nil {
req.SetBody(data)
}
if resp != nil {
req.SetResult(resp)
}
var res *resty.Response
var err error
var e Error
req.SetError(&e)
switch method {
case base.Get:
res, err = req.Get(url)
case base.Post:
res, err = req.Post(url)
default:
return nil, base.ErrNotSupport
}
if err != nil {
return nil, err
}
log.Debug(res.String())
if e.Error.Code != 0 {
if e.Error.Code == 401 {
err = driver.RefreshToken(account)
if err != nil {
_ = model.SaveAccount(account)
return nil, err
}
return driver.Request(rawUrl, method, headers, query, form, data, resp, account)
}
return nil, fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
}
return res.Body(), nil
}
func init() {
base.RegisterDriver(&GoogleDrive{})
}

185
drivers/lanzou/driver.go Normal file
View File

@ -0,0 +1,185 @@
package lanzou
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"path/filepath"
)
type Lanzou struct{}
func (driver Lanzou) Config() base.DriverConfig {
return base.DriverConfig{
Name: "Lanzou",
NeedSetLink: true,
}
}
func (driver Lanzou) Items() []base.Item {
return []base.Item{
{
Name: "internal_type",
Label: "lanzou type",
Type: base.TypeSelect,
Required: true,
Values: "cookie,url",
},
{
Name: "access_token",
Label: "cookie",
Type: base.TypeString,
Description: "about 15 days valid",
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
},
{
Name: "site_url",
Label: "share url",
Type: base.TypeString,
},
{
Name: "password",
Label: "share password",
Type: base.TypeString,
},
}
}
func (driver Lanzou) Save(account *model.Account, old *model.Account) error {
if account.InternalType == "cookie" {
if account.RootFolder == "" {
account.RootFolder = "-1"
}
}
account.Status = "work"
_ = model.SaveAccount(account)
return nil
}
func (driver Lanzou) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver Lanzou) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
var rawFiles []LanZouFile
cache, err := base.GetCache(path, account)
if err == nil {
rawFiles, _ = cache.([]LanZouFile)
} else {
file, err := driver.File(path, account)
if err != nil {
return nil, err
}
rawFiles, err = driver.GetFiles(file.Id, account)
if err != nil {
return nil, err
}
if len(rawFiles) > 0 {
_ = base.SetCache(path, rawFiles, account)
}
}
files := make([]model.File, 0)
for _, file := range rawFiles {
files = append(files, *driver.FormatFile(&file))
}
return files, nil
}
func (driver Lanzou) Link(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(args.Path, account)
if err != nil {
return nil, err
}
log.Debugf("down file: %+v", file)
downId := file.Id
if account.InternalType == "cookie" {
downId, err = driver.GetDownPageId(file.Id, account)
if err != nil {
return nil, err
}
}
url, err := driver.GetLink(downId)
if err != nil {
return nil, err
}
link := base.Link{
Url: url,
}
return &link, nil
}
func (driver Lanzou) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
log.Debugf("lanzou path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver Lanzou) Proxy(c *gin.Context, account *model.Account) {
c.Request.Header.Del("Origin")
}
func (driver Lanzou) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver *Lanzou) MakeDir(path string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver *Lanzou) Move(src string, dst string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver *Lanzou) Copy(src string, dst string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver *Lanzou) Delete(path string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver *Lanzou) Upload(file *model.FileStream, account *model.Account) error {
return base.ErrNotImplement
}
var _ base.Driver = (*Lanzou)(nil)

239
drivers/lanzou/lanzou.go Normal file
View File

@ -0,0 +1,239 @@
package lanzou
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"path/filepath"
"regexp"
"strconv"
"time"
)
var lanzouClient = resty.New()
type LanZouFile struct {
Name string `json:"name"`
NameAll string `json:"name_all"`
Id string `json:"id"`
FolId string `json:"fol_id"`
Size string `json:"size"`
Time string `json:"time"`
Folder bool
}
func (driver *Lanzou) FormatFile(file *LanZouFile) *model.File {
now := time.Now()
f := &model.File{
Id: file.Id,
Name: file.Name,
Driver: driver.Config().Name,
SizeStr: file.Size,
TimeStr: file.Time,
UpdatedAt: &now,
}
if file.Folder {
f.Type = conf.FOLDER
f.Id = file.FolId
} else {
f.Name = file.NameAll
f.Type = utils.GetFileType(filepath.Ext(file.NameAll))
}
return f
}
type LanZouFilesResp struct {
Zt int `json:"zt"`
Info interface{} `json:"info"`
Text []LanZouFile `json:"text"`
}
func (driver *Lanzou) GetFiles(folderId string, account *model.Account) ([]LanZouFile, error) {
if account.InternalType == "cookie" {
files := make([]LanZouFile, 0)
var resp LanZouFilesResp
// folders
res, err := lanzouClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
SetFormData(map[string]string{
"task": "47",
"folder_id": folderId,
}).Post("https://pc.woozooo.com/doupload.php")
if err != nil {
return nil, err
}
log.Debug(res.String())
if resp.Zt != 1 && resp.Zt != 2 {
return nil, fmt.Errorf("%v", resp.Info)
}
for _, file := range resp.Text {
file.Folder = true
files = append(files, file)
}
// files
pg := 1
for {
_, err = lanzouClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
SetFormData(map[string]string{
"task": "5",
"folder_id": folderId,
"pg": strconv.Itoa(pg),
}).Post("https://pc.woozooo.com/doupload.php")
if err != nil {
return nil, err
}
if resp.Zt != 1 {
return nil, fmt.Errorf("%v", resp.Info)
}
if len(resp.Text) == 0 {
break
}
files = append(files, resp.Text...)
pg++
}
return files, nil
} else {
return driver.GetFilesByUrl(account)
}
}
func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error) {
files := make([]LanZouFile, 0)
shareUrl := account.SiteUrl
res, err := lanzouClient.R().Get(shareUrl)
if err != nil {
return nil, err
}
lxArr := regexp.MustCompile(`'lx':(.+?),`).FindStringSubmatch(res.String())
if len(lxArr) == 0 {
return nil, fmt.Errorf("get empty page")
}
lx := lxArr[1]
fid := regexp.MustCompile(`'fid':(.+?),`).FindStringSubmatch(res.String())[1]
uid := regexp.MustCompile(`'uid':'(.+?)',`).FindStringSubmatch(res.String())[1]
rep := regexp.MustCompile(`'rep':'(.+?)',`).FindStringSubmatch(res.String())[1]
up := regexp.MustCompile(`'up':(.+?),`).FindStringSubmatch(res.String())[1]
ls := regexp.MustCompile(`'ls':(.+?),`).FindStringSubmatch(res.String())[1]
tName := regexp.MustCompile(`'t':(.+?),`).FindStringSubmatch(res.String())[1]
kName := regexp.MustCompile(`'k':(.+?),`).FindStringSubmatch(res.String())[1]
t := regexp.MustCompile(`var ` + tName + ` = '(.+?)';`).FindStringSubmatch(res.String())[1]
k := regexp.MustCompile(`var ` + kName + ` = '(.+?)';`).FindStringSubmatch(res.String())[1]
pg := 1
for {
var resp LanZouFilesResp
res, err = lanzouClient.R().SetResult(&resp).SetFormData(map[string]string{
"lx": lx,
"fid": fid,
"uid": uid,
"pg": strconv.Itoa(pg),
"rep": rep,
"t": t,
"k": k,
"up": up,
"ls": ls,
"pwd": account.Password,
}).Post("https://wwa.lanzouo.com/filemoreajax.php")
if err != nil {
log.Debug(err)
break
}
log.Debug(res.String())
if resp.Zt != 1 {
return nil, fmt.Errorf("%v", resp.Info)
}
if len(resp.Text) == 0 {
break
}
pg++
files = append(files, resp.Text...)
}
return files, nil
}
//type LanzouDownInfo struct {
// FId string `json:"f_id"`
// IsNewd string `json:"is_newd"`
//}
// 获取下载页面的ID
func (driver *Lanzou) GetDownPageId(fileId string, account *model.Account) (string, error) {
var resp LanZouFilesResp
res, err := lanzouClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
SetFormData(map[string]string{
"task": "22",
"file_id": fileId,
}).Post("https://pc.woozooo.com/doupload.php")
if err != nil {
return "", err
}
log.Debug(res.String())
if resp.Zt != 1 {
return "", fmt.Errorf("%v", resp.Info)
}
info, ok := resp.Info.(map[string]interface{})
if !ok {
return "", fmt.Errorf("%v", resp.Info)
}
fid, ok := info["f_id"].(string)
if !ok {
return "", fmt.Errorf("%v", info["f_id"])
}
return fid, nil
}
type LanzouLinkResp struct {
Dom string `json:"dom"`
Url string `json:"url"`
Zt int `json:"zt"`
}
func (driver *Lanzou) GetLink(downId string) (string, error) {
res, err := lanzouClient.R().Get("https://wwa.lanzouo.com/" + downId)
if err != nil {
return "", err
}
iframe := regexp.MustCompile(`<iframe class="ifr2" name=".{2,20}" src="(.+?)"`).FindStringSubmatch(res.String())
if len(iframe) == 0 {
return "", fmt.Errorf("get down empty page")
}
iframeUrl := "https://wwa.lanzouo.com" + iframe[1]
res, err = lanzouClient.R().Get(iframeUrl)
if err != nil {
return "", err
}
ajaxdata := regexp.MustCompile(`var ajaxdata = '(.+?)'`).FindStringSubmatch(res.String())
if len(ajaxdata) == 0 {
return "", fmt.Errorf("get iframe empty page")
}
signs := ajaxdata[1]
sign := regexp.MustCompile(`var ispostdowns = '(.+?)';`).FindStringSubmatch(res.String())[1]
websignkey := regexp.MustCompile(`'websignkey':'(.+?)'`).FindStringSubmatch(res.String())[1]
var resp LanzouLinkResp
form := map[string]string{
"action": "downprocess",
"signs": signs,
"sign": sign,
"ves": "1",
"websign": "",
"websignkey": websignkey,
}
log.Debugf("form: %+v", form)
_, err = lanzouClient.R().SetResult(&resp).
SetHeader("origin", "https://wwa.lanzouo.com").
SetHeader("referer", iframeUrl).
SetFormData(form).Post("https://wwa.lanzouo.com/ajaxm.php")
if resp.Zt == 1 {
return resp.Dom + "/file/" + resp.Url, nil
}
return "", fmt.Errorf("can't get link")
}
func init() {
base.RegisterDriver(&Lanzou{})
lanzouClient.
SetRetryCount(3).
SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36")
}

View File

@ -1,111 +0,0 @@
package drivers
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gofiber/fiber/v2"
log "github.com/sirupsen/logrus"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
type Native struct {
}
func (n Native) Preview(path string, account *model.Account) (interface{}, error) {
return nil,fmt.Errorf("no need")
}
func (n Native) Items() []Item {
return []Item{
{
Name: "root_folder",
Label: "root folder path",
Type: "string",
Required: true,
},
}
}
func (n Native) Proxy(ctx *fiber.Ctx) {
// unnecessary
}
func (n Native) Save(account *model.Account, old *model.Account) error {
log.Debugf("save a account: [%s]", account.Name)
if !utils.Exists(account.RootFolder) {
return fmt.Errorf("[%s] not exist", account.RootFolder)
}
return nil
}
// TODO sort files
func (n Native) Path(path string, account *model.Account) (*model.File, []*model.File, error) {
fullPath := filepath.Join(account.RootFolder, path)
log.Debugf("%s-%s-%s", account.RootFolder, path, fullPath)
if !utils.Exists(fullPath) {
return nil, nil, fmt.Errorf("path not found")
}
if utils.IsDir(fullPath) {
result := make([]*model.File, 0)
files, err := ioutil.ReadDir(fullPath)
if err != nil {
return nil, nil, err
}
for _, f := range files {
if strings.HasPrefix(f.Name(), ".") {
continue
}
time := f.ModTime()
file := &model.File{
Name: f.Name(),
Size: f.Size(),
Type: 0,
UpdatedAt: &time,
Driver: "Native",
}
if f.IsDir() {
file.Type = conf.FOLDER
} else {
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
}
result = append(result, file)
}
return nil, result, nil
}
f, err := os.Stat(fullPath)
if err != nil {
return nil, nil, err
}
time := f.ModTime()
file := &model.File{
Name: f.Name(),
Size: f.Size(),
Type: utils.GetFileType(filepath.Ext(f.Name())),
UpdatedAt: &time,
Driver: "Native",
}
return file, nil, nil
}
func (n Native) Link(path string, account *model.Account) (string, error) {
fullPath := filepath.Join(account.RootFolder, path)
s, err := os.Stat(fullPath)
if err != nil {
return "", err
}
if s.IsDir() {
return "", fmt.Errorf("can't down folder")
}
return fullPath, nil
}
var _ Driver = (*Native)(nil)
func init() {
RegisterDriver("Native", &Native{})
}

234
drivers/native/driver.go Normal file
View File

@ -0,0 +1,234 @@
package native
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
type Native struct{}
func (driver Native) Config() base.DriverConfig {
return base.DriverConfig{
Name: "Native",
OnlyProxy: true,
NoLink: true,
}
}
func (driver Native) Items() []base.Item {
return []base.Item{
{
Name: "root_folder",
Label: "root folder path",
Type: base.TypeString,
Required: true,
},
{
Name: "order_by",
Label: "order_by",
Type: base.TypeSelect,
Values: "name,size,updated_at",
Required: false,
},
{
Name: "order_direction",
Label: "order_direction",
Type: base.TypeSelect,
Values: "ASC,DESC",
Required: false,
},
}
}
func (driver Native) Save(account *model.Account, old *model.Account) error {
log.Debugf("save a account: [%s]", account.Name)
if !utils.Exists(account.RootFolder) {
account.Status = fmt.Sprintf("[%s] not exist", account.RootFolder)
_ = model.SaveAccount(account)
return fmt.Errorf("[%s] not exist", account.RootFolder)
}
account.Status = "work"
account.Proxy = true
err := model.SaveAccount(account)
if err != nil {
return err
}
return nil
}
func (driver Native) File(path string, account *model.Account) (*model.File, error) {
fullPath := filepath.Join(account.RootFolder, path)
if !utils.Exists(fullPath) {
return nil, base.ErrPathNotFound
}
f, err := os.Stat(fullPath)
if err != nil {
return nil, err
}
time := f.ModTime()
file := &model.File{
Name: f.Name(),
Size: f.Size(),
UpdatedAt: &time,
Driver: driver.Config().Name,
}
if f.IsDir() {
file.Type = conf.FOLDER
} else {
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
}
return file, nil
}
func (driver Native) Files(path string, account *model.Account) ([]model.File, error) {
fullPath := filepath.Join(account.RootFolder, path)
if !utils.Exists(fullPath) {
return nil, base.ErrPathNotFound
}
files := make([]model.File, 0)
rawFiles, err := ioutil.ReadDir(fullPath)
if err != nil {
return nil, err
}
for _, f := range rawFiles {
if strings.HasPrefix(f.Name(), ".") {
continue
}
time := f.ModTime()
file := model.File{
Name: f.Name(),
Size: f.Size(),
Type: 0,
UpdatedAt: &time,
Driver: driver.Config().Name,
}
if f.IsDir() {
file.Type = conf.FOLDER
} else {
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
}
files = append(files, file)
}
model.SortFiles(files, account)
return files, nil
}
func (driver Native) Link(args base.Args, account *model.Account) (*base.Link, error) {
fullPath := filepath.Join(account.RootFolder, args.Path)
s, err := os.Stat(fullPath)
if err != nil {
return nil, err
}
if s.IsDir() {
return nil, base.ErrNotFile
}
link := base.Link{
Url: fullPath,
}
return &link, nil
}
func (driver Native) Path(path string, account *model.Account) (*model.File, []model.File, error) {
log.Debugf("native path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
//file.Url, _ = driver.Link(path, account)
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
model.SortFiles(files, account)
return nil, files, nil
}
func (driver Native) Proxy(c *gin.Context, account *model.Account) {
// unnecessary
}
func (driver Native) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver Native) MakeDir(path string, account *model.Account) error {
fullPath := filepath.Join(account.RootFolder, path)
err := os.MkdirAll(fullPath, 0700)
return err
}
func (driver Native) Move(src string, dst string, account *model.Account) error {
fullSrc := filepath.Join(account.RootFolder, src)
fullDst := filepath.Join(account.RootFolder, dst)
return os.Rename(fullSrc, fullDst)
}
func (driver Native) Copy(src string, dst string, account *model.Account) error {
fullSrc := filepath.Join(account.RootFolder, src)
fullDst := filepath.Join(account.RootFolder, dst)
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
dstFile, err := driver.File(dst, account)
if err == nil {
if !dstFile.IsDir() {
return base.ErrNotSupport
}
}
if srcFile.IsDir() {
return driver.CopyDir(fullSrc, fullDst)
}
return driver.CopyFile(fullSrc, fullDst)
}
func (driver Native) Delete(path string, account *model.Account) error {
fullPath := filepath.Join(account.RootFolder, path)
file, err := driver.File(path, account)
if err != nil {
return err
}
if file.IsDir() {
return os.RemoveAll(fullPath)
}
return os.Remove(fullPath)
}
func (driver Native) Upload(file *model.FileStream, account *model.Account) error {
fullPath := filepath.Join(account.RootFolder, file.ParentPath, file.Name)
_, err := driver.File(filepath.Join(file.ParentPath, file.Name), account)
if err == nil {
// TODO overwrite?
}
basePath := filepath.Dir(fullPath)
if !utils.Exists(basePath) {
err := os.MkdirAll(basePath, 0744)
if err != nil {
return err
}
}
out, err := os.Create(fullPath)
if err != nil {
return err
}
defer func() {
_ = out.Close()
}()
_, err = io.Copy(out, file)
return err
}
var _ base.Driver = (*Native)(nil)

74
drivers/native/native.go Normal file
View File

@ -0,0 +1,74 @@
package native
import (
"fmt"
"github.com/Xhofe/alist/drivers/base"
"io"
"io/ioutil"
"os"
"path"
)
// File copies a single file from src to dst
func (driver *Native) CopyFile(src, dst string) error {
var err error
var srcfd *os.File
var dstfd *os.File
var srcinfo os.FileInfo
if srcfd, err = os.Open(src); err != nil {
return err
}
defer srcfd.Close()
if dstfd, err = os.Create(dst); err != nil {
return err
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = os.Stat(src); err != nil {
return err
}
return os.Chmod(dst, srcinfo.Mode())
}
// Dir copies a whole directory recursively
func (driver *Native) CopyDir(src string, dst string) error {
var err error
var fds []os.FileInfo
var srcinfo os.FileInfo
if srcinfo, err = os.Stat(src); err != nil {
return err
}
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
return err
}
if fds, err = ioutil.ReadDir(src); err != nil {
return err
}
for _, fd := range fds {
srcfp := path.Join(src, fd.Name())
dstfp := path.Join(dst, fd.Name())
if fd.IsDir() {
if err = driver.CopyDir(srcfp, dstfp); err != nil {
fmt.Println(err)
}
} else {
if err = driver.CopyFile(srcfp, dstfp); err != nil {
fmt.Println(err)
}
}
}
return nil
}
func init() {
base.RegisterDriver(&Native{})
}

View File

@ -1,307 +0,0 @@
package drivers
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
"github.com/gofiber/fiber/v2"
"github.com/robfig/cron/v3"
"path/filepath"
"time"
)
type Onedrive struct{}
var oneClient = resty.New()
type OnedriveHost struct {
Oauth string
Api string
}
var onedriveHostMap = map[string]OnedriveHost{
"global": {
Oauth: "https://login.microsoftonline.com",
Api: "https://graph.microsoft.com",
},
"cn": {
Oauth: "https://login.chinacloudapi.cn",
Api: "https://microsoftgraph.chinacloudapi.cn",
},
"us": {
Oauth: "https://login.microsoftonline.us",
Api: "https://graph.microsoft.us",
},
"de": {
Oauth: "https://login.microsoftonline.de",
Api: "https://graph.microsoft.de",
},
}
func init() {
RegisterDriver("Onedrive", &Onedrive{})
oneClient.SetRetryCount(3)
}
func (o Onedrive) GetMetaUrl(account *model.Account, auth bool, path string) string {
path = filepath.Join(account.RootFolder, path)
host, _ := onedriveHostMap[account.Zone]
if auth {
return host.Oauth
}
switch account.OnedriveType {
case "onedrive":
{
if path == "/" {
return fmt.Sprintf("%s/v1.0/me/drive/root", host.Api)
} else {
return fmt.Sprintf("%s/v1.0/me/drive/root:%s:", host.Api, path)
}
}
case "sharepoint":
{
if path == "/" {
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root", host.Api, account.SiteId)
} else {
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root:%s:", host.Api, account.SiteId, path)
}
}
default:
return ""
}
}
func (o Onedrive) Items() []Item {
return []Item{
{
Name: "zone",
Label: "zone",
Type: "select",
Required: true,
Values: "global,cn,us,de",
Description: "",
},
{
Name: "onedrive_type",
Label: "onedrive type",
Type: "select",
Required: true,
Values: "onedrive,sharepoint",
},
{
Name: "client_id",
Label: "client id",
Type: "string",
Required: true,
},
{
Name: "client_secret",
Label: "client secret",
Type: "string",
Required: true,
},
{
Name: "redirect_uri",
Label: "redirect uri",
Type: "string",
Required: true,
},
{
Name: "refresh_token",
Label: "refresh token",
Type: "string",
Required: true,
},
{
Name: "site_id",
Label: "site id",
Type: "string",
Required: false,
},
{
Name: "root_folder",
Label: "root folder path",
Type: "string",
Required: false,
},
}
}
type OneTokenErr struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}
func (o Onedrive) RefreshToken(account *model.Account) error {
url := o.GetMetaUrl(account, true, "") + "/common/oauth2/v2.0/token"
var resp TokenResp
var e OneTokenErr
_, err := oneClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
"grant_type": "refresh_token",
"client_id": account.ClientId,
"client_secret": account.ClientSecret,
"redirect_uri": account.RedirectUri,
"refresh_token": account.RefreshToken,
}).Post(url)
if err != nil {
return err
}
if e.Error != "" {
return fmt.Errorf("%s", e.ErrorDescription)
}
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
return nil
}
type OneFile struct {
Name string `json:"name"`
Size int64 `json:"size"`
LastModifiedDateTime *time.Time `json:"lastModifiedDateTime"`
Url string `json:"@microsoft.graph.downloadUrl"`
File struct {
MimeType string `json:"mimeType"`
} `json:"file"`
}
type OneFiles struct {
Value []OneFile `json:"value"`
}
type OneRespErr struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func (o Onedrive) FormatFile(file *OneFile) *model.File {
f := &model.File{
Name: file.Name,
Size: file.Size,
UpdatedAt: file.LastModifiedDateTime,
Driver: "OneDrive",
Url: file.Url,
}
if file.File.MimeType == "" {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(filepath.Ext(file.Name))
}
return f
}
func (o Onedrive) GetFiles(account *model.Account, path string) ([]OneFile, error) {
var files OneFiles
var e OneRespErr
_, err := oneClient.R().SetResult(&files).SetError(&e).
SetHeader("Authorization", "Bearer "+account.AccessToken).
Get(o.GetMetaUrl(account, false, path) + "/children")
if err != nil {
return nil, err
}
if e.Error.Code != "" {
return nil, fmt.Errorf("%s", e.Error.Message)
}
return files.Value, nil
}
func (o Onedrive) GetFile(account *model.Account, path string) (*OneFile, error) {
var file OneFile
var e OneRespErr
_, err := oneClient.R().SetResult(&file).SetError(&e).
SetHeader("Authorization", "Bearer "+account.AccessToken).
Get(o.GetMetaUrl(account, false, path))
if err != nil {
return nil, err
}
if e.Error.Code != "" {
return nil, fmt.Errorf("%s", e.Error.Message)
}
return &file, nil
}
func (o Onedrive) Path(path string, account *model.Account) (*model.File, []*model.File, error) {
path = utils.ParsePath(path)
cache, err := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path))
if err == nil {
files, _ := cache.([]*model.File)
return nil, files, nil
}
file, err := o.GetFile(account, path)
if err != nil {
return nil, nil, err
}
if file.File.MimeType != "" {
return o.FormatFile(file), nil, nil
} else {
files, err := o.GetFiles(account, path)
if err != nil {
return nil, nil, err
}
res := make([]*model.File, 0)
for _, file := range files {
res = append(res, o.FormatFile(&file))
}
_ = conf.Cache.Set(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path), res, nil)
return nil, res, nil
}
}
func (o Onedrive) Link(path string, account *model.Account) (string, error) {
file, err := o.GetFile(account, path)
if err != nil {
return "", err
}
if file.File.MimeType == "" {
return "", fmt.Errorf("can't down folder")
}
return file.Url, nil
}
func (o Onedrive) Save(account *model.Account, old *model.Account) error {
_, ok := onedriveHostMap[account.Zone]
if !ok {
return fmt.Errorf("no [%s] zone", account.Zone)
}
if old != nil {
conf.Cron.Remove(cron.EntryID(old.CronId))
}
account.RootFolder = utils.ParsePath(account.RootFolder)
err := o.RefreshToken(account)
if err != nil {
return err
}
cronId, err := conf.Cron.AddFunc("@every 1h", func() {
name := account.Name
newAccount, ok := model.GetAccount(name)
if !ok {
return
}
err = o.RefreshToken(&newAccount)
if err != nil {
newAccount.Status = err.Error()
}
_ = model.SaveAccount(newAccount)
})
if err != nil {
return err
}
account.CronId = int(cronId)
err = model.SaveAccount(*account)
if err != nil {
return err
}
return nil
}
func (o Onedrive) Proxy(ctx *fiber.Ctx) {
}
func (o Onedrive) Preview(path string, account *model.Account) (interface{}, error) {
return nil, nil
}
var _ Driver = (*Onedrive)(nil)

232
drivers/onedrive/driver.go Normal file
View File

@ -0,0 +1,232 @@
package onedrive
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
"path/filepath"
)
type Onedrive struct{}
func (driver Onedrive) Config() base.DriverConfig {
return base.DriverConfig{
Name: "Onedrive",
}
}
func (driver Onedrive) Items() []base.Item {
return []base.Item{
{
Name: "zone",
Label: "zone",
Type: base.TypeSelect,
Required: true,
Values: "global,cn,us,de",
Description: "",
},
{
Name: "internal_type",
Label: "onedrive type",
Type: base.TypeSelect,
Required: true,
Values: "onedrive,sharepoint",
},
{
Name: "client_id",
Label: "client id",
Type: base.TypeString,
Required: true,
},
{
Name: "client_secret",
Label: "client secret",
Type: base.TypeString,
Required: true,
},
{
Name: "redirect_uri",
Label: "redirect uri",
Type: base.TypeString,
Required: true,
},
{
Name: "refresh_token",
Label: "refresh token",
Type: base.TypeString,
Required: true,
},
{
Name: "site_id",
Label: "site id",
Type: base.TypeString,
Required: false,
},
{
Name: "root_folder",
Label: "root folder path",
Type: base.TypeString,
Required: false,
},
{
Name: "order_by",
Label: "order_by",
Type: base.TypeSelect,
Values: "name,size,lastModifiedDateTime",
Required: false,
},
{
Name: "order_direction",
Label: "order_direction",
Type: base.TypeSelect,
Values: "asc,desc",
Required: false,
},
}
}
func (driver Onedrive) Save(account *model.Account, old *model.Account) error {
_, ok := onedriveHostMap[account.Zone]
if !ok {
return fmt.Errorf("no [%s] zone", account.Zone)
}
if old != nil {
conf.Cron.Remove(cron.EntryID(old.CronId))
}
account.RootFolder = utils.ParsePath(account.RootFolder)
err := driver.RefreshToken(account)
if err != nil {
return err
}
cronId, err := conf.Cron.AddFunc("@every 1h", func() {
name := account.Name
log.Debugf("onedrive account name: %s", name)
newAccount, ok := model.GetAccount(name)
log.Debugf("onedrive account: %+v", newAccount)
if !ok {
return
}
err = driver.RefreshToken(&newAccount)
_ = model.SaveAccount(&newAccount)
})
if err != nil {
return err
}
account.CronId = int(cronId)
err = model.SaveAccount(account)
if err != nil {
return err
}
return nil
}
func (driver Onedrive) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver Onedrive) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
cache, err := base.GetCache(path, account)
if err == nil {
files, _ := cache.([]model.File)
return files, nil
}
rawFiles, err := driver.GetFiles(account, path)
if err != nil {
return nil, err
}
files := make([]model.File, 0)
for _, file := range rawFiles {
files = append(files, *driver.FormatFile(&file))
}
if len(files) > 0 {
_ = base.SetCache(path, files, account)
}
return files, nil
}
func (driver Onedrive) Link(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.GetFile(account, args.Path)
if err != nil {
return nil, err
}
if file.File.MimeType == "" {
return nil, base.ErrNotFile
}
link := base.Link{
Url: file.Url,
}
return &link, nil
}
func (driver Onedrive) Path(path string, account *model.Account) (*model.File, []model.File, error) {
log.Debugf("onedrive path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver Onedrive) Proxy(c *gin.Context, account *model.Account) {
c.Request.Header.Del("Origin")
}
func (driver Onedrive) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver Onedrive) MakeDir(path string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver Onedrive) Move(src string, dst string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver Onedrive) Copy(src string, dst string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver Onedrive) Delete(path string, account *model.Account) error {
return base.ErrNotImplement
}
func (driver Onedrive) Upload(file *model.FileStream, account *model.Account) error {
return base.ErrNotImplement
}
var _ base.Driver = (*Onedrive)(nil)

View File

@ -0,0 +1,204 @@
package onedrive
import (
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"path/filepath"
"time"
)
var oneClient = resty.New()
type Host struct {
Oauth string
Api string
}
var onedriveHostMap = map[string]Host{
"global": {
Oauth: "https://login.microsoftonline.com",
Api: "https://graph.microsoft.com",
},
"cn": {
Oauth: "https://login.chinacloudapi.cn",
Api: "https://microsoftgraph.chinacloudapi.cn",
},
"us": {
Oauth: "https://login.microsoftonline.us",
Api: "https://graph.microsoft.us",
},
"de": {
Oauth: "https://login.microsoftonline.de",
Api: "https://graph.microsoft.de",
},
}
func (driver Onedrive) GetMetaUrl(account *model.Account, auth bool, path string) string {
path = filepath.Join(account.RootFolder, path)
log.Debugf(path)
host, _ := onedriveHostMap[account.Zone]
if auth {
return host.Oauth
}
switch account.InternalType {
case "onedrive":
{
if path == "/" || path == "\\" {
return fmt.Sprintf("%s/v1.0/me/drive/root", host.Api)
} else {
return fmt.Sprintf("%s/v1.0/me/drive/root:%s:", host.Api, path)
}
}
case "sharepoint":
{
if path == "/" || path == "\\" {
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root", host.Api, account.SiteId)
} else {
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root:%s:", host.Api, account.SiteId, path)
}
}
default:
return ""
}
}
type OneTokenErr struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}
func (driver Onedrive) RefreshToken(account *model.Account) error {
err := driver.refreshToken(account)
if err != nil && err.Error() == "empty refresh_token" {
return driver.refreshToken(account)
}
return err
}
func (driver Onedrive) refreshToken(account *model.Account) error {
url := driver.GetMetaUrl(account, true, "") + "/common/oauth2/v2.0/token"
var resp base.TokenResp
var e OneTokenErr
_, err := oneClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
"grant_type": "refresh_token",
"client_id": account.ClientId,
"client_secret": account.ClientSecret,
"redirect_uri": account.RedirectUri,
"refresh_token": account.RefreshToken,
}).Post(url)
if err != nil {
account.Status = err.Error()
return err
}
if e.Error != "" {
account.Status = e.ErrorDescription
return fmt.Errorf("%s", e.ErrorDescription)
} else {
account.Status = "work"
}
if resp.RefreshToken == "" {
account.Status = "empty refresh_token"
return errors.New("empty refresh_token")
}
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
return nil
}
type OneFile struct {
Name string `json:"name"`
Size int64 `json:"size"`
LastModifiedDateTime *time.Time `json:"lastModifiedDateTime"`
Url string `json:"@microsoft.graph.downloadUrl"`
File struct {
MimeType string `json:"mimeType"`
} `json:"file"`
Thumbnails []struct{
Medium struct{
Url string `json:"url"`
} `json:"medium"`
} `json:"thumbnails"`
}
type OneFiles struct {
Value []OneFile `json:"value"`
NextLink string `json:"@odata.nextLink"`
}
type OneRespErr struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func (driver Onedrive) FormatFile(file *OneFile) *model.File {
f := &model.File{
Name: file.Name,
Size: file.Size,
UpdatedAt: file.LastModifiedDateTime,
Driver: driver.Config().Name,
Url: file.Url,
}
if len(file.Thumbnails) > 0 {
f.Thumbnail = file.Thumbnails[0].Medium.Url
}
if file.File.MimeType == "" {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(filepath.Ext(file.Name))
}
return f
}
func (driver Onedrive) GetFiles(account *model.Account, path string) ([]OneFile, error) {
var res []OneFile
nextLink := driver.GetMetaUrl(account, false, path) + "/children?$expand=thumbnails"
if account.OrderBy != "" {
nextLink += fmt.Sprintf("&orderby=%s", account.OrderBy)
if account.OrderDirection != "" {
nextLink += fmt.Sprintf("%%20%s", account.OrderDirection)
}
}
for nextLink != "" {
var files OneFiles
var e OneRespErr
_, err := oneClient.R().SetResult(&files).SetError(&e).
SetHeader("Authorization", "Bearer "+account.AccessToken).
Get(nextLink)
if err != nil {
return nil, err
}
if e.Error.Code != "" {
return nil, fmt.Errorf("%s", e.Error.Message)
}
res = append(res, files.Value...)
nextLink = files.NextLink
}
return res, nil
}
func (driver Onedrive) GetFile(account *model.Account, path string) (*OneFile, error) {
var file OneFile
var e OneRespErr
_, err := oneClient.R().SetResult(&file).SetError(&e).
SetHeader("Authorization", "Bearer "+account.AccessToken).
Get(driver.GetMetaUrl(account, false, path))
if err != nil {
return nil, err
}
if e.Error.Code != "" {
return nil, fmt.Errorf("%s", e.Error.Message)
}
return &file, nil
}
func init() {
base.RegisterDriver(&Onedrive{})
oneClient.SetRetryCount(3)
}

236
drivers/pikpak/driver.go Normal file
View File

@ -0,0 +1,236 @@
package pikpak
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"path/filepath"
)
type PikPak struct{}
func (driver PikPak) Config() base.DriverConfig {
return base.DriverConfig{
Name: "PikPak",
NeedSetLink: true,
ApiProxy: true,
}
}
func (driver PikPak) Items() []base.Item {
return []base.Item{
{
Name: "username",
Label: "username",
Type: base.TypeString,
Required: true,
},
{
Name: "password",
Label: "password",
Type: base.TypeString,
Required: true,
},
{
Name: "root_folder",
Label: "root folder id",
Type: base.TypeString,
Required: false,
},
}
}
func (driver PikPak) Save(account *model.Account, old *model.Account) error {
err := driver.Login(account)
return err
}
func (driver PikPak) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver PikPak) Files(path string, account *model.Account) ([]model.File, error) {
path = utils.ParsePath(path)
var files []model.File
cache, err := base.GetCache(path, account)
if err == nil {
files, _ = cache.([]model.File)
} else {
file, err := driver.File(path, account)
if err != nil {
return nil, err
}
rawFiles, err := driver.GetFiles(file.Id, account)
if err != nil {
return nil, err
}
files = make([]model.File, 0)
for _, file := range rawFiles {
files = append(files, *driver.FormatFile(&file))
}
if len(files) > 0 {
_ = base.SetCache(path, files, account)
}
}
return files, nil
}
func (driver PikPak) Link(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(args.Path, account)
if err != nil {
return nil, err
}
var resp File
_, err = driver.Request(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.Id),
base.Get, nil, nil, &resp, account)
if err != nil {
return nil, err
}
return &base.Link{
Url: resp.WebContentLink,
}, nil
}
func (driver PikPak) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
log.Debugf("pikpak path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver PikPak) Proxy(c *gin.Context, account *model.Account) {
}
func (driver PikPak) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver PikPak) MakeDir(path string, account *model.Account) error {
path = utils.ParsePath(path)
dir, name := filepath.Split(path)
parentFile, err := driver.File(dir, account)
if err != nil {
return err
}
if !parentFile.IsDir() {
return base.ErrNotFolder
}
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Post, nil, &base.Json{
"kind": "drive#folder",
"parent_id": parentFile.Id,
"name": name,
}, nil, account)
if err == nil {
_ = base.DeleteCache(dir, account)
}
return err
}
func (driver PikPak) Move(src string, dst string, account *model.Account) error {
srcDir, _ := filepath.Split(src)
dstDir, dstName := filepath.Split(dst)
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
// rename
if srcDir == dstDir {
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files/"+srcFile.Id, base.Patch, nil, &base.Json{
"name": dstName,
}, nil, account)
} else {
// move
dstDirFile, err := driver.File(dstDir, account)
if err != nil {
return err
}
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchMove", base.Post, nil, &base.Json{
"ids": []string{srcFile.Id},
"to": base.Json{
"parent_id": dstDirFile.Id,
},
}, nil, account)
}
if err == nil {
_ = base.DeleteCache(srcDir, account)
_ = base.DeleteCache(dstDir, account)
}
return err
}
func (driver PikPak) Copy(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
dstDirFile, err := driver.File(utils.Dir(dst), account)
if err != nil {
return err
}
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchCopy", base.Post, nil, &base.Json{
"ids": []string{srcFile.Id},
"to": base.Json{
"parent_id": dstDirFile.Id,
},
}, nil, account)
if err == nil {
_ = base.DeleteCache(utils.Dir(dst), account)
}
return err
}
func (driver PikPak) Delete(path string, account *model.Account) error {
file, err := driver.File(path, account)
if err != nil {
return err
}
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchTrash", base.Post, nil, &base.Json{
"ids": []string{file.Id},
}, nil, account)
if err == nil {
_ = base.DeleteCache(utils.Dir(path), account)
}
return err
}
func (driver PikPak) Upload(file *model.FileStream, account *model.Account) error {
return base.ErrNotImplement
}
var _ base.Driver = (*PikPak)(nil)

197
drivers/pikpak/pikpak.go Normal file
View File

@ -0,0 +1,197 @@
package pikpak
import (
"errors"
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"path/filepath"
"strconv"
"time"
)
type RespErr struct {
ErrorCode int `json:"error_code"`
Error string `json:"error"`
}
func (driver PikPak) Login(account *model.Account) error {
url := "https://user.mypikpak.com/v1/auth/signin"
if account.APIProxyUrl != "" {
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
}
var e RespErr
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
"captcha_token": "",
"client_id": "YNxT9w7GMdWvEOKa",
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
"username": account.Username,
"password": account.Password,
}).Post(url)
if err != nil {
account.Status = err.Error()
return err
}
log.Debug(res.String())
if e.ErrorCode != 0 {
account.Status = e.Error
return errors.New(e.Error)
}
data := res.Body()
account.Status = "work"
account.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
account.AccessToken = jsoniter.Get(data, "access_token").ToString()
_ = model.SaveAccount(account)
return nil
}
func (driver PikPak) RefreshToken(account *model.Account) error {
url := "https://user.mypikpak.com/v1/auth/token"
if account.APIProxyUrl != "" {
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
}
var e RespErr
res, err := base.RestyClient.R().SetError(&e).
SetHeader("user-agent", "").SetBody(base.Json{
"client_id": "YNxT9w7GMdWvEOKa",
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
"grant_type": "refresh_token",
"refresh_token": account.RefreshToken,
}).Post(url)
if err != nil {
account.Status = err.Error()
return err
}
if e.ErrorCode != 0 {
if e.ErrorCode == 4126 {
// refresh_token 失效,重新登陆
return driver.Login(account)
}
return errors.New(e.Error)
}
data := res.Body()
account.Status = "work"
account.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
account.AccessToken = jsoniter.Get(data, "access_token").ToString()
log.Debugf("%s\n %+v", res.String(), account)
_ = model.SaveAccount(account)
return nil
}
func (driver PikPak) Request(url string, method int, query map[string]string, data *base.Json, resp interface{}, account *model.Account) ([]byte, error) {
rawUrl := url
if account.APIProxyUrl != "" {
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
}
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+account.AccessToken)
if query != nil {
req.SetQueryParams(query)
}
if data != nil {
req.SetBody(data)
}
if resp != nil {
req.SetResult(resp)
}
var e RespErr
req.SetError(&e)
var res *resty.Response
var err error
switch method {
case base.Get:
res, err = req.Get(url)
case base.Post:
res, err = req.Post(url)
case base.Patch:
res, err = req.Patch(url)
default:
return nil, base.ErrNotSupport
}
if err != nil {
return nil, err
}
log.Debug(res.String())
if e.ErrorCode != 0 {
if e.ErrorCode == 16 {
// login / refresh token
err = driver.RefreshToken(account)
if err != nil {
return nil, err
}
return driver.Request(rawUrl, method, query, data, resp, account)
} else {
return nil, errors.New(e.Error)
}
}
return res.Body(), nil
}
type File struct {
Id string `json:"id"`
Kind string `json:"kind"`
Name string `json:"name"`
ModifiedTime *time.Time `json:"modified_time"`
Size string `json:"size"`
ThumbnailLink string `json:"thumbnail_link"`
WebContentLink string `json:"web_content_link"`
}
func (driver PikPak) FormatFile(file *File) *model.File {
size, _ := strconv.ParseInt(file.Size, 10, 64)
f := &model.File{
Id: file.Id,
Name: file.Name,
Size: size,
Driver: driver.Config().Name,
UpdatedAt: file.ModifiedTime,
Thumbnail: file.ThumbnailLink,
}
if file.Kind == "drive#folder" {
f.Type = conf.FOLDER
} else {
f.Type = utils.GetFileType(filepath.Ext(file.Name))
}
return f
}
type Files struct {
Files []File `json:"files"`
NextPageToken string `json:"next_page_token"`
}
func (driver PikPak) GetFiles(id string, account *model.Account) ([]File, error) {
res := make([]File, 0)
pageToken := "first"
for pageToken != "" {
if pageToken == "first" {
pageToken = ""
}
query := map[string]string{
"parent_id": id,
"thumbnail_size": "SIZE_LARGE",
"with_audit": "true",
"limit": "100",
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
"page_token": pageToken,
}
var resp Files
_, err := driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Get, query, nil, &resp, account)
if err != nil {
return nil, err
}
log.Debugf("%+v", resp)
pageToken = resp.NextPageToken
res = append(res, resp.Files...)
}
return res, nil
}
func init() {
base.RegisterDriver(&PikPak{})
}

20
go.mod
View File

@ -4,9 +4,9 @@ go 1.17
require (
github.com/eko/gocache/v2 v2.1.0
github.com/gin-gonic/gin v1.7.4
github.com/go-playground/validator/v10 v10.9.0
github.com/go-resty/resty/v2 v2.6.0
github.com/gofiber/fiber/v2 v2.20.2
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/robfig/cron/v3 v3.0.0
github.com/sirupsen/logrus v1.8.1
@ -18,17 +18,18 @@ require (
require (
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
github.com/andybalholm/brotli v1.0.3 // indirect
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
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gin-contrib/cors v1.3.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-redis/redis/v8 v8.9.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/golang/protobuf v1.4.3 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.10.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
@ -39,19 +40,21 @@ require (
github.com/jackc/pgx/v4 v4.13.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.2 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/jlaffaye/ftp v0.0.0-20211117213618-11820403398b // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pegasus-kv/thrift v0.13.0 // indirect
github.com/prometheus/client_golang v1.10.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.18.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.31.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
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
@ -59,8 +62,9 @@ require (
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.23.0 // 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
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/apimachinery v0.0.0-20191123233150-4c4803ed55e3 // indirect
)

70
go.sum
View File

@ -22,9 +22,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
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/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@ -101,6 +98,13 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA=
github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
@ -115,10 +119,15 @@ github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nA
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-redis/redis/v8 v8.9.0 h1:FTTbB7WqlXfVNdVv0SsxA+oVi0bAwit6bMe3IUucq2o=
@ -129,8 +138,6 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofiber/fiber/v2 v2.20.2 h1:dqizbjO1pCmH6K+b+kBk7TCJK4rmgjJXvX8/MZDbK60=
github.com/gofiber/fiber/v2 v2.20.2/go.mod h1:/LdZHMUXZvTTo7gU4+b1hclqCAdoQphNQ9bi9gutPyI=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
@ -148,16 +155,18 @@ github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -257,6 +266,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jlaffaye/ftp v0.0.0-20211117213618-11820403398b h1:Ur6QAxsHCK99Quj9PaWafoV4unb0DO/HWiKExD+TN5g=
github.com/jlaffaye/ftp v0.0.0-20211117213618-11820403398b/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@ -264,7 +275,10 @@ github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBv
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@ -272,20 +286,21 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
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/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=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -304,7 +319,10 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
@ -320,10 +338,13 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@ -416,6 +437,7 @@ github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
@ -463,15 +485,14 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=
github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.29.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE=
github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -510,7 +531,6 @@ 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-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
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 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
@ -598,8 +618,8 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
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-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
@ -664,17 +684,23 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
@ -690,8 +716,10 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -2,25 +2,26 @@ package model
import (
"github.com/Xhofe/alist/conf"
"github.com/robfig/cron/v3"
"time"
)
type Account struct {
Name string `json:"name" gorm:"primaryKey" validate:"required"`
Index int `json:"index"`
Type string `json:"type"`
ID uint `json:"id" gorm:"primaryKey"` // 唯一ID
Name string `json:"name" gorm:"unique" binding:"required"` // 唯一名称
Index int `json:"index"` // 序号 用于排序
Type string `json:"type"` // 类型即driver
Username string `json:"username"`
Password string `json:"password"`
RefreshToken string `json:"refresh_token"`
AccessToken string `json:"access_token"`
RootFolder string `json:"root_folder"`
Status string `json:"status"`
Status string `json:"status"` // 状态
CronId int
DriveId string
Limit int `json:"limit"`
OrderBy string `json:"order_by"`
OrderDirection string `json:"order_direction"`
Proxy bool `json:"proxy"`
UpdatedAt *time.Time `json:"updated_at"`
Search bool `json:"search"`
ClientId string `json:"client_id"`
@ -29,24 +30,41 @@ type Account struct {
RedirectUri string `json:"redirect_uri"`
SiteUrl string `json:"site_url"`
SiteId string `json:"site_id"`
OnedriveType string `json:"onedrive_type"`
InternalType string `json:"internal_type"`
WebdavProxy bool `json:"webdav_proxy"` // 开启之后只会webdav走中转
Proxy bool `json:"proxy"` // 是否中转,开启之后web和webdav都会走中转
//AllowProxy bool `json:"allow_proxy"` // 是否允许中转下载
DownProxyUrl string `json:"down_proxy_url"` // 用于中转下载服务的URL 两处 1. path请求中返回的链接 2. down下载时进行302
APIProxyUrl string `json:"api_proxy_url"` // 用于中转api的地址
}
var accountsMap = map[string]Account{}
// SaveAccount save account to database
func SaveAccount(account Account) error {
func SaveAccount(account *Account) error {
if err := conf.DB.Save(account).Error; err != nil {
return err
}
RegisterAccount(account)
RegisterAccount(*account)
return nil
}
func DeleteAccount(name string) error {
account := Account{
Name: name,
func CreateAccount(account *Account) error {
if err := conf.DB.Create(account).Error; err != nil {
return err
}
RegisterAccount(*account)
return nil
}
func DeleteAccount(id uint) error {
var account Account
account.ID = id
if err := conf.DB.First(&account).Error; err != nil {
return err
}
name := account.Name
conf.Cron.Remove(cron.EntryID(account.CronId))
if err := conf.DB.Delete(&account).Error; err != nil {
return err
}
@ -54,6 +72,10 @@ func DeleteAccount(name string) error {
return nil
}
func DeleteAccountFromMap(name string) {
delete(accountsMap, name)
}
func AccountsCount() int {
return len(accountsMap)
}
@ -72,14 +94,23 @@ func GetAccount(name string) (Account, bool) {
return account, ok
}
func GetAccountFiles() ([]*File, error) {
files := make([]*File, 0)
func GetAccountById(id uint) (*Account, error) {
var account Account
account.ID = id
if err := conf.DB.First(&account).Error; err != nil {
return nil, err
}
return &account, nil
}
func GetAccountFiles() ([]File, error) {
files := make([]File, 0)
var accounts []Account
if err := conf.DB.Order("`index`").Find(&accounts).Error; err != nil {
return nil, err
}
for _, v := range accounts {
files = append(files, &File{
files = append(files, File{
Name: v.Name,
Size: 0,
Type: conf.FOLDER,

View File

@ -1,8 +1,14 @@
package model
import "time"
import (
"github.com/Xhofe/alist/conf"
"sort"
"strings"
"time"
)
type File struct {
Id string `json:"-"`
Name string `json:"name"`
Size int64 `json:"size"`
Type int `json:"type"`
@ -10,4 +16,53 @@ type File struct {
UpdatedAt *time.Time `json:"updated_at"`
Thumbnail string `json:"thumbnail"`
Url string `json:"url"`
}
SizeStr string `json:"size_str"`
TimeStr string `json:"time_str"`
}
func SortFiles(files []File, account *Account) {
if account.OrderBy == "" {
return
}
sort.Slice(files, func(i, j int) bool {
switch account.OrderBy {
case "name":
{
c := strings.Compare(files[i].Name, files[j].Name)
if account.OrderDirection == "DESC" {
return c >= 0
}
return c <= 0
}
case "size":
{
if account.OrderDirection == "DESC" {
return files[i].Size >= files[j].Size
}
return files[i].Size <= files[j].Size
}
case "updated_at":
if account.OrderDirection == "DESC" {
return files[i].UpdatedAt.After(*files[j].UpdatedAt)
}
return files[i].UpdatedAt.Before(*files[j].UpdatedAt)
}
return false
})
}
func (f File) GetSize() uint64 {
return uint64(f.Size)
}
func (f File) GetName() string {
return f.Name
}
func (f File) ModTime() time.Time {
return *f.UpdatedAt
}
func (f File) IsDir() bool {
return f.Type == conf.FOLDER
}

35
model/file_stream.go Normal file
View File

@ -0,0 +1,35 @@
package model
import "io"
type FileStream struct {
File io.ReadCloser
Size uint64
ParentPath string
Name string
MIMEType string
}
func (file FileStream) Read(p []byte) (n int, err error) {
return file.File.Read(p)
}
func (file FileStream) GetMIMEType() string {
return file.MIMEType
}
func (file FileStream) GetSize() uint64 {
return file.Size
}
func (file FileStream) Close() error {
return file.File.Close()
}
func (file FileStream) GetFileName() string {
return file.Name
}
func (file FileStream) GetParentPath() string {
return file.ParentPath
}

View File

@ -1,17 +1,20 @@
package model
import "github.com/Xhofe/alist/conf"
import (
"github.com/Xhofe/alist/conf"
log "github.com/sirupsen/logrus"
)
type Meta struct {
Path string `json:"path" gorm:"primaryKey" validate:"required"`
ID uint `json:"id" gorm:"primaryKey"`
Path string `json:"path" gorm:"unique" binding:"required"`
Password string `json:"password"`
Hide string `json:"hide"`
}
func GetMetaByPath(path string) (*Meta, error) {
var meta Meta
meta.Path = path
err := conf.DB.First(&meta).Error
err := conf.DB.Where("path = ?", path).First(&meta).Error
if err != nil {
return nil, err
}
@ -19,11 +22,16 @@ func GetMetaByPath(path string) (*Meta, error) {
}
func SaveMeta(meta Meta) error {
return conf.DB.Save(meta).Error
return conf.DB.Save(&meta).Error
}
func DeleteMeta(path string) error {
meta := Meta{Path: path}
func CreateMeta(meta Meta) error {
return conf.DB.Create(&meta).Error
}
func DeleteMeta(id uint) error {
meta := Meta{ID: id}
log.Debugf("delete meta: %+v", meta)
return conf.DB.Delete(&meta).Error
}

View File

@ -1,7 +1,10 @@
package model
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/utils"
"strings"
)
const (
@ -11,11 +14,13 @@ const (
)
type SettingItem struct {
Key string `json:"key" gorm:"primaryKey" validate:"required"`
Key string `json:"key" gorm:"primaryKey" binding:"required"`
Value string `json:"value"`
Description string `json:"description"`
//Type string `json:"type"`
Type string `json:"type"`
Group int `json:"group"`
Values string `json:"values"`
Version string `json:"version"`
}
func SaveSettings(items []SettingItem) error {
@ -42,10 +47,63 @@ func GetSettings() (*[]SettingItem, error) {
return &items, nil
}
func DeleteSetting(key string) error {
setting := SettingItem{
Key: key,
}
return conf.DB.Delete(&setting).Error
}
func GetSettingByKey(key string) (*SettingItem, error) {
var items SettingItem
if err := conf.DB.Where("key = ?", key).First(&items).Error; err != nil {
if err := conf.DB.Where("`key` = ?", key).First(&items).Error; err != nil {
return nil, err
}
return &items, nil
}
func LoadSettings() {
textTypes, err := GetSettingByKey("text types")
if err == nil {
conf.TextTypes = strings.Split(textTypes.Value, ",")
}
checkParent, err := GetSettingByKey("check parent folder")
if err == nil {
conf.CheckParent = checkParent.Value == "true"
}
checkDown, err := GetSettingByKey("check down link")
if err == nil {
conf.CheckDown = checkDown.Value == "true"
}
favicon, err := GetSettingByKey("favicon")
if err == nil {
//conf.Favicon = favicon.Value
conf.IndexHtml = strings.Replace(conf.RawIndexHtml, "https://store.heytapimage.com/cdo-portal/feedback/202110/30/d43c41c5d257c9bc36366e310374fb19.png", favicon.Value, 1)
}
title, err := GetSettingByKey("title")
if err == nil {
conf.IndexHtml = strings.Replace(conf.IndexHtml, "Loading...", title.Value, 1)
}
customizeHead, err := GetSettingByKey("customize head")
if err == nil {
conf.IndexHtml = strings.Replace(conf.IndexHtml, "<!-- customize head -->", customizeHead.Value, 1)
}
customizeBody, err := GetSettingByKey("customize body")
if err == nil {
conf.IndexHtml = strings.Replace(conf.IndexHtml, "<!-- customize body -->", customizeBody.Value, 1)
}
adminPassword, err := GetSettingByKey("password")
if err == nil {
conf.Token = utils.GetMD5Encode(fmt.Sprintf("https://github.com/Xhofe/alist-%s", adminPassword.Value))
}
davUsername, err := GetSettingByKey("WebDAV username")
if err == nil {
conf.DavUsername = davUsername.Value
}
davPassword, err := GetSettingByKey("WebDAV password")
if err == nil {
conf.DavPassword = davPassword.Value
}
}

View File

@ -4,3 +4,9 @@ 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

@ -1,55 +0,0 @@
package server
import (
"fmt"
"github.com/Xhofe/alist/drivers"
"github.com/Xhofe/alist/model"
"github.com/gofiber/fiber/v2"
"time"
)
func GetAccounts(ctx *fiber.Ctx) error {
accounts, err := model.GetAccounts()
if err != nil {
return ErrorResp(ctx, err, 500)
}
return SuccessResp(ctx, accounts)
}
func SaveAccount(ctx *fiber.Ctx) error {
var req model.Account
if err := ctx.BodyParser(&req); err != nil {
return ErrorResp(ctx, err, 400)
}
if err := validate.Struct(req); err != nil {
return ErrorResp(ctx, err, 400)
}
driver, ok := drivers.GetDriver(req.Type)
if !ok {
return ErrorResp(ctx, fmt.Errorf("no [%s] driver", req.Type), 400)
}
old, ok := model.GetAccount(req.Name)
now := time.Now()
req.UpdatedAt = &now
if err := model.SaveAccount(req); err != nil {
return ErrorResp(ctx, err, 500)
} else {
if ok {
err = driver.Save(&req, &old)
} else {
err = driver.Save(&req, nil)
}
if err != nil {
return ErrorResp(ctx, err, 500)
}
return SuccessResp(ctx)
}
}
func DeleteAccount(ctx *fiber.Ctx) error {
name := ctx.Query("name")
if err := model.DeleteAccount(name); err != nil {
return ErrorResp(ctx, err, 500)
}
return SuccessResp(ctx)
}

View File

@ -1,15 +0,0 @@
package server
import (
"github.com/Xhofe/alist/conf"
"github.com/gofiber/fiber/v2"
)
func ClearCache(ctx *fiber.Ctx) error {
err := conf.Cache.Clear(conf.Ctx)
if err != nil {
return ErrorResp(ctx,err,500)
}else {
return SuccessResp(ctx)
}
}

View File

@ -1,40 +0,0 @@
package server
import (
"fmt"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
func Auth(ctx *fiber.Ctx) error {
token := ctx.Get("Authorization")
password, err := model.GetSettingByKey("password")
if err != nil {
if err == gorm.ErrRecordNotFound {
return ErrorResp(ctx, fmt.Errorf("password not set"), 400)
}
return ErrorResp(ctx, err, 500)
}
if token != utils.GetMD5Encode(password.Value) {
return ErrorResp(ctx, fmt.Errorf("wrong password"), 401)
}
return ctx.Next()
}
func Login(ctx *fiber.Ctx) error {
return SuccessResp(ctx)
}
func SetSuccess(ctx *fiber.Ctx) error {
ctx.Status(200)
return ctx.Next()
}
func CheckAccount(ctx *fiber.Ctx) error {
if model.AccountsCount() == 0 {
return ErrorResp(ctx,fmt.Errorf("no accounts,please add one first"),1001)
}
return ctx.Next()
}

View File

@ -1,65 +0,0 @@
package server
import (
"fmt"
"github.com/Xhofe/alist/drivers"
"github.com/Xhofe/alist/model"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"strings"
)
var validate = validator.New()
type Resp struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
func ParsePath(rawPath string) (*model.Account,string,drivers.Driver,error) {
var path,name string
switch model.AccountsCount() {
case 0:
return nil,"",nil,fmt.Errorf("no accounts,please add one first")
case 1:
path = rawPath
break
default:
paths := strings.Split(rawPath,"/")
path = "/" + strings.Join(paths[2:],"/")
name = paths[1]
}
account,ok := model.GetAccount(name)
if !ok {
return nil,"",nil,fmt.Errorf("no [%s] account", name)
}
driver,ok := drivers.GetDriver(account.Type)
if !ok {
return nil,"",nil,fmt.Errorf("no [%s] driver",account.Type)
}
return &account,path,driver,nil
}
func ErrorResp(ctx *fiber.Ctx,err error,code int) error {
return ctx.JSON(Resp{
Code: code,
Message: err.Error(),
Data: nil,
})
}
func SuccessResp(ctx *fiber.Ctx, data ...interface{}) error {
if len(data) == 0 {
return ctx.JSON(Resp{
Code: 200,
Message: "success",
Data: nil,
})
}
return ctx.JSON(Resp{
Code: 200,
Message: "success",
Data: data[0],
})
}

51
server/common/check.go Normal file
View File

@ -0,0 +1,51 @@
package common
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func Login(c *gin.Context) {
SuccessResp(c)
}
func CheckParent(path string, password string) bool {
meta, err := model.GetMetaByPath(path)
if err == nil {
if meta.Password != "" && meta.Password != password {
return false
}
return true
} else {
if path == "/" {
return true
}
return CheckParent(utils.Dir(path), password)
}
}
func CheckDownLink(path string, passwordMd5 string, name string) bool {
if !conf.CheckDown {
return true
}
meta, err := model.GetMetaByPath(path)
log.Debugf("check down path: %s", path)
if err == nil {
log.Debugf("check down link: %s,%s", meta.Password, passwordMd5)
if meta.Password != "" && utils.SignWithPassword(name, meta.Password) != passwordMd5 {
return false
}
return true
} else {
if !conf.CheckParent {
return true
}
if path == "/" {
return true
}
return CheckDownLink(utils.Dir(path), passwordMd5, name)
}
}

71
server/common/common.go Normal file
View File

@ -0,0 +1,71 @@
package common
import (
"fmt"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"strings"
)
type Resp struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
type PathReq struct {
Path string `json:"path"`
Password string `json:"password"`
}
func ParsePath(rawPath string) (*model.Account, string, base.Driver, error) {
var path, name string
switch model.AccountsCount() {
case 0:
return nil, "", nil, fmt.Errorf("no accounts,please add one first")
case 1:
path = rawPath
break
default:
paths := strings.Split(rawPath, "/")
path = "/" + strings.Join(paths[2:], "/")
name = paths[1]
}
account, ok := model.GetAccount(name)
if !ok {
return nil, "", nil, fmt.Errorf("no [%s] account", name)
}
driver, ok := base.GetDriver(account.Type)
if !ok {
return nil, "", nil, fmt.Errorf("no [%s] driver", account.Type)
}
return &account, path, driver, nil
}
func ErrorResp(c *gin.Context, err error, code int) {
log.Error(err.Error())
c.JSON(200, Resp{
Code: code,
Message: err.Error(),
Data: nil,
})
c.Abort()
}
func SuccessResp(c *gin.Context, data ...interface{}) {
if len(data) == 0 {
c.JSON(200, Resp{
Code: 200,
Message: "success",
Data: nil,
})
return
}
c.JSON(200, Resp{
Code: 200,
Message: "success",
Data: data[0],
})
}

View File

@ -0,0 +1,95 @@
package controllers
import (
"fmt"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/server/common"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"strconv"
"time"
)
func GetAccounts(c *gin.Context) {
accounts, err := model.GetAccounts()
if err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c, accounts)
}
func CreateAccount(c *gin.Context) {
var req model.Account
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
driver, ok := base.GetDriver(req.Type)
if !ok {
common.ErrorResp(c, fmt.Errorf("no [%s] driver", req.Type), 400)
return
}
now := time.Now()
req.UpdatedAt = &now
if err := model.CreateAccount(&req); err != nil {
common.ErrorResp(c, err, 500)
} else {
log.Debugf("new account: %+v", req)
err = driver.Save(&req, nil)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}
}
func SaveAccount(c *gin.Context) {
var req model.Account
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
driver, ok := base.GetDriver(req.Type)
if !ok {
common.ErrorResp(c, fmt.Errorf("no [%s] driver", req.Type), 400)
return
}
old, err := model.GetAccountById(req.ID)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
now := time.Now()
req.UpdatedAt = &now
if old.Name != req.Name {
model.DeleteAccountFromMap(old.Name)
}
if err := model.SaveAccount(&req); err != nil {
common.ErrorResp(c, err, 500)
} else {
log.Debugf("save account: %+v", req)
err = driver.Save(&req, old)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}
}
func DeleteAccount(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := model.DeleteAccount(uint(id)); err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}

View File

@ -0,0 +1,16 @@
package controllers
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/server/common"
"github.com/gin-gonic/gin"
)
func ClearCache(c *gin.Context) {
err := conf.Cache.Clear(conf.Ctx)
if err != nil {
common.ErrorResp(c, err, 500)
} else {
common.SuccessResp(c)
}
}

157
server/controllers/down.go Normal file
View File

@ -0,0 +1,157 @@
package controllers
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/server/common"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"strings"
)
func Down(c *gin.Context) {
rawPath := c.Param("path")
rawPath = utils.ParsePath(rawPath)
log.Debugf("down: %s", rawPath)
account, path, driver, err := common.ParsePath(rawPath)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
if driver.Config().OnlyProxy || account.Proxy {
Proxy(c)
return
}
link, err := driver.Link(base.Args{Path: path, IP: c.ClientIP()}, account)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
c.Redirect(302, link.Url)
return
}
func Proxy(c *gin.Context) {
rawPath := c.Param("path")
rawPath = utils.ParsePath(rawPath)
log.Debugf("proxy: %s", rawPath)
account, path, driver, err := common.ParsePath(rawPath)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
// 只有以下几种情况允许中转:
// 1. 账号开启中转
// 2. driver只能中转
// 3. 是文本类型文件
// 4. 开启webdav中转需要验证sign
if !account.Proxy && !driver.Config().OnlyProxy && utils.GetFileType(filepath.Ext(rawPath)) != conf.TEXT {
// 只开启了webdav中转验证sign
ok := false
if account.WebdavProxy {
_, ok = c.Get("sign")
}
if !ok {
common.ErrorResp(c, fmt.Errorf("[%s] not allowed proxy", account.Name), 403)
return
}
}
// 中转时有中转机器使用中转机器,若携带标志位则表明不能再走中转机器了
if account.DownProxyUrl != "" && c.Param("d") != "1" {
name := utils.Base(rawPath)
link := fmt.Sprintf("%s%s?sign=%s", account.DownProxyUrl, rawPath, utils.SignWithToken(name, conf.Token))
c.Redirect(302, link)
return
}
// 对于中转不需要重设IP
link, err := driver.Link(base.Args{Path: path}, account)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
// 本机读取数据
if account.Type == "FTP" {
c.Data(http.StatusOK, "application/octet-stream", link.Data)
return
}
// 本机文件直接返回文件
if account.Type == "Native" {
// 对于名称为index.html的文件需要特殊处理
if utils.Base(rawPath) == "index.html" {
file, err := os.Open(link.Url)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
defer func() {
_ = file.Close()
}()
fileStat, err := os.Stat(link.Url)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
http.ServeContent(c.Writer, c.Request, utils.Base(rawPath), fileStat.ModTime(), file)
return
}
c.File(link.Url)
return
} else {
if utils.GetFileType(filepath.Ext(rawPath)) == conf.TEXT {
Text(c, link)
return
}
driver.Proxy(c, account)
r := c.Request
w := c.Writer
target, err := url.Parse(link.Url)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
protocol := "http://"
if strings.HasPrefix(link.Url, "https://") {
protocol = "https://"
}
targetHost, err := url.Parse(fmt.Sprintf("%s%s", protocol, target.Host))
proxy := httputil.NewSingleHostReverseProxy(targetHost)
r.URL = target
r.Host = target.Host
proxy.ServeHTTP(w, r)
}
}
var client *resty.Client
func init() {
client = resty.New()
client.SetRetryCount(3)
}
func Text(c *gin.Context, link *base.Link) {
res, err := client.R().Get(link.Url)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
text := res.String()
t := utils.GetStrCoding(res.Body())
log.Debugf("text type: %s", t)
if t != utils.UTF8 {
body, err := utils.GbkToUtf8(res.Body())
if err != nil {
common.ErrorResp(c, err, 500)
return
}
text = string(body)
}
c.String(200, text)
}

View File

@ -0,0 +1,11 @@
package controllers
import (
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/server/common"
"github.com/gin-gonic/gin"
)
func GetDrivers(c *gin.Context) {
common.SuccessResp(c, base.GetDrivers())
}

View File

@ -0,0 +1,61 @@
package controllers
import (
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/server/common"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
"strconv"
)
func GetMetas(c *gin.Context) {
metas,err := model.GetMetas()
if err != nil {
common.ErrorResp(c,err,500)
return
}
common.SuccessResp(c, metas)
}
func CreateMeta(c *gin.Context) {
var req model.Meta
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
req.Path = utils.ParsePath(req.Path)
if err := model.CreateMeta(req); err != nil {
common.ErrorResp(c, err, 500)
} else {
common.SuccessResp(c)
}
}
func SaveMeta(c *gin.Context) {
var req model.Meta
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
req.Path = utils.ParsePath(req.Path)
if err := model.SaveMeta(req); err != nil {
common.ErrorResp(c, err, 500)
} else {
common.SuccessResp(c)
}
}
func DeleteMeta(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
//path = utils.ParsePath(path)
if err := model.DeleteMeta(uint(id)); err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}

130
server/controllers/path.go Normal file
View File

@ -0,0 +1,130 @@
package controllers
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/server/common"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"strings"
)
func Path(c *gin.Context) {
reqV, _ := c.Get("req")
req := reqV.(common.PathReq)
if model.AccountsCount() > 1 && req.Path == "/" {
files, err := model.GetAccountFiles()
if err != nil {
common.ErrorResp(c, err, 500)
return
}
c.JSON(200, common.Resp{
Code: 200,
Message: "folder",
Data: files,
})
return
}
account, path, driver, err := common.ParsePath(req.Path)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
file, files, err := driver.Path(path, account)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
if file != nil {
// 对于中转文件或只能中转,将链接修改为中转链接
if driver.Config().OnlyProxy || account.Proxy {
if account.DownProxyUrl != "" {
file.Url = fmt.Sprintf("%s%s?sign=%s", account.DownProxyUrl, req.Path, utils.SignWithToken(file.Name, conf.Token))
} else {
file.Url = fmt.Sprintf("//%s/d%s", c.Request.Host, req.Path)
}
} else if driver.Config().NeedSetLink {
link, err := driver.Link(base.Args{Path: path, IP: c.ClientIP()}, account)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
file.Url = link.Url
}
c.JSON(200, common.Resp{
Code: 200,
Message: "file",
Data: []*model.File{file},
})
} else {
meta, _ := model.GetMetaByPath(req.Path)
if meta != nil && meta.Hide != "" {
tmpFiles := make([]model.File, 0)
hideFiles := strings.Split(meta.Hide, ",")
for _, item := range files {
if !utils.IsContain(hideFiles, item.Name) {
tmpFiles = append(tmpFiles, item)
}
}
files = tmpFiles
}
c.JSON(200, common.Resp{
Code: 200,
Message: "folder",
Data: files,
})
}
}
// Link 返回真实的链接,且携带头,只提供给中转程序使用
func Link(c *gin.Context) {
var req common.PathReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
req.Path = utils.ParsePath(req.Path)
rawPath := req.Path
rawPath = utils.ParsePath(rawPath)
log.Debugf("link: %s", rawPath)
account, path, driver, err := common.ParsePath(rawPath)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
if driver.Config().NoLink {
common.SuccessResp(c, base.Link{
Url: fmt.Sprintf("//%s/d%s?d=1&sign=%s", c.Request.Host, req.Path, utils.SignWithToken(utils.Base(rawPath), conf.Token)),
})
return
}
link, err := driver.Link(base.Args{Path: path, IP: c.ClientIP()}, account)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c, link)
return
}
func Preview(c *gin.Context) {
reqV, _ := c.Get("req")
req := reqV.(common.PathReq)
rawPath := req.Path
rawPath = utils.ParsePath(rawPath)
log.Debugf("preview: %s", rawPath)
account, path, driver, err := common.ParsePath(rawPath)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
data, err := driver.Preview(path, account)
if err != nil {
common.ErrorResp(c, err, 500)
} else {
common.SuccessResp(c, data)
}
}

View File

@ -0,0 +1,48 @@
package controllers
import (
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/server/common"
"github.com/gin-gonic/gin"
)
func SaveSettings(c *gin.Context) {
var req []model.SettingItem
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := model.SaveSettings(req); err != nil {
common.ErrorResp(c, err, 500)
} else {
model.LoadSettings()
common.SuccessResp(c)
}
}
func GetSettings(c *gin.Context) {
settings, err := model.GetSettings()
if err != nil {
common.ErrorResp(c, err, 400)
return
}
common.SuccessResp(c, settings)
}
func GetSettingsPublic(c *gin.Context) {
settings, err := model.GetSettingsPublic()
if err != nil {
common.ErrorResp(c, err, 400)
return
}
common.SuccessResp(c, settings)
}
func DeleteSetting(c *gin.Context) {
key := c.Query("key")
if err := model.DeleteSetting(key); err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}

View File

@ -1,68 +0,0 @@
package server
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/utils"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/proxy"
log "github.com/sirupsen/logrus"
"net/url"
"path/filepath"
)
func Down(ctx *fiber.Ctx) error {
rawPath, err:= url.QueryUnescape(ctx.Params("*"))
if err != nil {
return ErrorResp(ctx,err,500)
}
rawPath = utils.ParsePath(rawPath)
log.Debugf("down: %s",rawPath)
account, path, driver, err := ParsePath(rawPath)
if err != nil {
return ErrorResp(ctx, err, 500)
}
link, err := driver.Link(path, account)
if err != nil {
return ErrorResp(ctx, err, 500)
}
if account.Type == "Native" {
return ctx.SendFile(link)
} else {
return ctx.Redirect(link, 302)
}
}
func Proxy(ctx *fiber.Ctx) error {
rawPath, err:= url.QueryUnescape(ctx.Params("*"))
if err != nil {
return ErrorResp(ctx,err,500)
}
rawPath = utils.ParsePath(rawPath)
log.Debugf("proxy: %s",rawPath)
account, path, driver, err := ParsePath(rawPath)
if err != nil {
return ErrorResp(ctx, err, 500)
}
if !account.Proxy && utils.GetFileType(filepath.Ext(rawPath))!=conf.TEXT {
return ErrorResp(ctx,fmt.Errorf("[%s] not allowed proxy",account.Name),403)
}
link, err := driver.Link(path, account)
if err != nil {
return ErrorResp(ctx, err, 500)
}
if account.Type == "Native" {
return ctx.SendFile(link)
} else {
driver.Proxy(ctx)
if err := proxy.Do(ctx, link); err != nil {
log.Errorf("proxy error: %s", err)
return ErrorResp(ctx,err,500)
}
// Remove Server header from response
ctx.Response().Header.Del(fiber.HeaderServer)
ctx.Set("Access-Control-Allow-Origin","*")
log.Debugf("proxy hedaer: %+v", ctx.Response().Header.String())
return nil
}
}

View File

@ -1,10 +0,0 @@
package server
import (
"github.com/Xhofe/alist/drivers"
"github.com/gofiber/fiber/v2"
)
func GetDrivers(ctx *fiber.Ctx) error {
return SuccessResp(ctx, drivers.GetDrivers())
}

View File

@ -1,40 +0,0 @@
package server
import (
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gofiber/fiber/v2"
)
func GetMetas(ctx *fiber.Ctx) error {
metas,err := model.GetMetas()
if err != nil {
return ErrorResp(ctx,err,500)
}
return SuccessResp(ctx, metas)
}
func SaveMeta(ctx *fiber.Ctx) error {
var req model.Meta
if err := ctx.BodyParser(&req); err != nil {
return ErrorResp(ctx, err, 400)
}
if err := validate.Struct(req); err != nil {
return ErrorResp(ctx, err, 400)
}
req.Path = utils.ParsePath(req.Path)
if err := model.SaveMeta(req); err != nil {
return ErrorResp(ctx, err, 500)
} else {
return SuccessResp(ctx)
}
}
func DeleteMeta(ctx *fiber.Ctx) error {
path := ctx.Query("path")
path = utils.ParsePath(path)
if err := model.DeleteMeta(path); err != nil {
return ErrorResp(ctx, err, 500)
}
return SuccessResp(ctx)
}

View File

@ -0,0 +1,16 @@
package middlewares
import (
"fmt"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/server/common"
"github.com/gin-gonic/gin"
)
func CheckAccount(c *gin.Context) {
if model.AccountsCount() == 0 {
common.ErrorResp(c, fmt.Errorf("no accounts,please add one first"), 1001)
return
}
c.Next()
}

View File

@ -0,0 +1,27 @@
package middlewares
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/server/common"
"github.com/gin-gonic/gin"
)
func Auth(c *gin.Context) {
token := c.GetHeader("Authorization")
//password, err := model.GetSettingByKey("password")
//if err != nil {
// if err == gorm.ErrRecordNotFound {
// common.ErrorResp(c, fmt.Errorf("password not set"), 400)
// return
// }
// common.ErrorResp(c, err, 500)
// return
//}
//if token != utils.GetMD5Encode(password.Value) {
if token != conf.Token {
common.ErrorResp(c, fmt.Errorf("wrong password"), 401)
return
}
c.Next()
}

View File

@ -0,0 +1,28 @@
package middlewares
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/server/common"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
)
func DownCheck(c *gin.Context) {
sign := c.Query("sign")
rawPath := c.Param("path")
rawPath = utils.ParsePath(rawPath)
name := utils.Base(rawPath)
if sign == utils.SignWithToken(name, conf.Token) {
c.Set("sign", true)
c.Next()
return
}
pw := c.Query("pw")
if !common.CheckDownLink(utils.Dir(rawPath), pw, utils.Base(rawPath)) {
common.ErrorResp(c, fmt.Errorf("wrong password"), 401)
c.Abort()
return
}
c.Next()
}

View File

@ -0,0 +1,40 @@
package middlewares
import (
"fmt"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/server/common"
"github.com/Xhofe/alist/utils"
"github.com/gin-gonic/gin"
)
func PathCheck(c *gin.Context) {
var req common.PathReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
req.Path = utils.ParsePath(req.Path)
c.Set("req",req)
token := c.GetHeader("Authorization")
if token == conf.Token {
c.Next()
return
}
meta, err := model.GetMetaByPath(req.Path)
if err == nil {
if meta.Password != "" && meta.Password != req.Password {
common.ErrorResp(c, fmt.Errorf("wrong password"), 401)
c.Abort()
return
}
} else if conf.CheckParent {
if !common.CheckParent(utils.Dir(req.Path), req.Password) {
common.ErrorResp(c, fmt.Errorf("wrong password"), 401)
c.Abort()
return
}
}
c.Next()
}

View File

@ -1,123 +0,0 @@
package server
import (
"fmt"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/gofiber/fiber/v2"
log "github.com/sirupsen/logrus"
"strings"
)
type PathReq struct {
Path string `json:"Path"`
Password string `json:"Password"`
}
func Path(ctx *fiber.Ctx) error {
var req PathReq
if err := ctx.BodyParser(&req); err != nil {
return ErrorResp(ctx, err, 400)
}
req.Path = utils.ParsePath(req.Path)
log.Debugf("path: %s", req.Path)
meta, err := model.GetMetaByPath(req.Path)
if err == nil {
if meta.Password != "" && meta.Password != req.Password {
return ErrorResp(ctx, fmt.Errorf("wrong password"), 401)
}
// TODO hide or ignore?
}
if model.AccountsCount() > 1 && req.Path == "/" {
files, err := model.GetAccountFiles()
if err != nil {
return ErrorResp(ctx, err, 500)
}
return ctx.JSON(Resp{
Code: 200,
Message: "folder",
Data: files,
})
}
account, path, driver, err := ParsePath(req.Path)
if err != nil {
return ErrorResp(ctx, err, 500)
}
file, files, err := driver.Path(path, account)
if err != nil {
return ErrorResp(ctx, err, 500)
}
if file != nil {
if account.Type == "Native" {
file.Url = fmt.Sprintf("%s://%s/p%s", ctx.Protocol(), ctx.Hostname(), req.Path)
}
return ctx.JSON(Resp{
Code: 200,
Message: "file",
Data: []*model.File{file},
})
} else {
if meta != nil && meta.Hide != "" {
tmpFiles := make([]*model.File, 0)
hideFiles := strings.Split(meta.Hide, ",")
for _, item := range files {
if !utils.IsContain(hideFiles, item.Name) {
tmpFiles = append(tmpFiles, item)
}
}
files = tmpFiles
}
return ctx.JSON(Resp{
Code: 200,
Message: "folder",
Data: files,
})
}
}
func Link(ctx *fiber.Ctx) error {
var req PathReq
if err := ctx.BodyParser(&req); err != nil {
return ErrorResp(ctx, err, 400)
}
rawPath := req.Path
rawPath = utils.ParsePath(rawPath)
log.Debugf("link: %s", rawPath)
account, path, driver, err := ParsePath(rawPath)
if err != nil {
return ErrorResp(ctx, err, 500)
}
link, err := driver.Link(path, account)
if err != nil {
return ErrorResp(ctx, err, 500)
}
if account.Type == "Native" {
return SuccessResp(ctx, fiber.Map{
"url": fmt.Sprintf("%s://%s/p%s", ctx.Protocol(), ctx.Hostname(), rawPath),
})
} else {
return SuccessResp(ctx, fiber.Map{
"url": link,
})
}
}
func Preview(ctx *fiber.Ctx) error {
var req PathReq
if err := ctx.BodyParser(&req); err != nil {
return ErrorResp(ctx, err, 400)
}
rawPath := req.Path
rawPath = utils.ParsePath(rawPath)
log.Debugf("preview: %s", rawPath)
account, path, driver, err := ParsePath(rawPath)
if err != nil {
return ErrorResp(ctx, err, 500)
}
data, err := driver.Preview(path, account)
if err != nil {
return ErrorResp(ctx, err, 500)
} else {
return SuccessResp(ctx, data)
}
}

View File

@ -1,42 +1,61 @@
package server
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/Xhofe/alist/server/common"
"github.com/Xhofe/alist/server/controllers"
"github.com/Xhofe/alist/server/middlewares"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func InitApiRouter(app *fiber.App) {
func InitApiRouter(r *gin.Engine) {
// TODO from settings
app.Use(cors.New())
app.Get("/d/*", Down)
// TODO check allow proxy?
app.Get("/p/*", Proxy)
Cors(r)
r.GET("/d/*path", middlewares.DownCheck, controllers.Down)
r.GET("/p/*path", middlewares.DownCheck, controllers.Proxy)
api := app.Group("/api")
api.Use(SetSuccess)
api := r.Group("/api")
public := api.Group("/public")
{
public.Post("/path", CheckAccount, Path)
public.Post("/preview", CheckAccount, Preview)
public.Get("/settings", GetSettingsPublic)
public.Post("/link", CheckAccount, Link)
path := public.Group("", middlewares.PathCheck, middlewares.CheckAccount)
path.POST("/path", controllers.Path)
path.POST("/preview", controllers.Preview)
//path.POST("/link",middlewares.Auth, controllers.Link)
public.GET("/settings", controllers.GetSettingsPublic)
}
admin := api.Group("/admin")
{
admin.Use(Auth)
admin.Get("/login", Login)
admin.Get("/settings", GetSettings)
admin.Post("/settings", SaveSettings)
admin.Post("/account", SaveAccount)
admin.Get("/accounts", GetAccounts)
admin.Delete("/account", DeleteAccount)
admin.Get("/drivers", GetDrivers)
admin.Get("/clear_cache",ClearCache)
admin.Use(middlewares.Auth)
admin.GET("/login", common.Login)
admin.GET("/settings", controllers.GetSettings)
admin.POST("/settings", controllers.SaveSettings)
admin.DELETE("/setting", controllers.DeleteSetting)
admin.Get("/metas", GetMetas)
admin.Post("/meta", SaveMeta)
admin.Delete("/meta", DeleteMeta)
admin.POST("/account/create", controllers.CreateAccount)
admin.POST("/account/save", controllers.SaveAccount)
admin.GET("/accounts", controllers.GetAccounts)
admin.DELETE("/account", controllers.DeleteAccount)
admin.GET("/drivers", controllers.GetDrivers)
admin.GET("/clear_cache", controllers.ClearCache)
admin.GET("/metas", controllers.GetMetas)
admin.POST("/meta/create", controllers.CreateMeta)
admin.POST("/meta/save", controllers.SaveMeta)
admin.DELETE("/meta", controllers.DeleteMeta)
admin.POST("/link", controllers.Link)
}
WebDav(r)
Static(r)
}
func Cors(r *gin.Engine) {
config := cors.DefaultConfig()
config.AllowAllOrigins = true
config.AllowHeaders = append(config.AllowHeaders, "Authorization")
r.Use(cors.New(config))
}

View File

@ -1,43 +0,0 @@
package server
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/model"
"github.com/gofiber/fiber/v2"
"strings"
)
func SaveSettings(ctx *fiber.Ctx) error {
var req []model.SettingItem
if err := ctx.BodyParser(&req); err != nil {
return ErrorResp(ctx, err, 400)
}
//if err := validate.Struct(req); err != nil {
// return ErrorResp(ctx, err, 400)
//}
if err := model.SaveSettings(req); err != nil {
return ErrorResp(ctx, err, 500)
} else {
textTypes, err := model.GetSettingByKey("text types")
if err==nil{
conf.TextTypes = strings.Split(textTypes.Value,",")
}
return SuccessResp(ctx)
}
}
func GetSettings(ctx *fiber.Ctx) error {
settings, err := model.GetSettings()
if err != nil {
return ErrorResp(ctx, err, 400)
}
return SuccessResp(ctx, settings)
}
func GetSettingsPublic(ctx *fiber.Ctx) error {
settings, err := model.GetSettingsPublic()
if err != nil {
return ErrorResp(ctx, err, 400)
}
return SuccessResp(ctx, settings)
}

43
server/static.go Normal file
View File

@ -0,0 +1,43 @@
package server
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/public"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"io/fs"
"io/ioutil"
"net/http"
)
func InitIndex() {
var index fs.File
var err error
if conf.Conf.Local {
index, err = public.Public.Open("local.html")
} else {
index, err = public.Public.Open("index.html")
}
if err != nil {
log.Fatalf(err.Error())
return
}
data, _ := ioutil.ReadAll(index)
conf.RawIndexHtml = string(data)
}
func Static(r *gin.Engine) {
//InitIndex()
assets, err := fs.Sub(public.Public, "assets")
if err != nil {
log.Fatalf("can't find assets folder")
}
r.StaticFS("/assets/", http.FS(assets))
r.NoRoute(func(c *gin.Context) {
c.Status(200)
c.Header("Content-Type", "text/html")
_, _ = c.Writer.WriteString(conf.IndexHtml)
c.Writer.Flush()
c.Writer.WriteHeaderNow()
})
}

60
server/webdav.go Normal file
View File

@ -0,0 +1,60 @@
package server
import (
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/server/webdav"
"github.com/gin-gonic/gin"
"net/http"
)
var handler *webdav.Handler
func init() {
handler = &webdav.Handler{
Prefix: "/dav",
LockSystem: webdav.NewMemLS(),
}
}
func WebDav(r *gin.Engine) {
dav := r.Group("/dav")
dav.Use(WebDAVAuth)
dav.Any("/*path", ServeWebDAV)
dav.Any("", ServeWebDAV)
dav.Handle("PROPFIND", "/*path", ServeWebDAV)
dav.Handle("PROPFIND", "", ServeWebDAV)
dav.Handle("MKCOL", "/*path", ServeWebDAV)
dav.Handle("LOCK", "/*path", ServeWebDAV)
dav.Handle("UNLOCK", "/*path", ServeWebDAV)
dav.Handle("PROPPATCH", "/*path", ServeWebDAV)
dav.Handle("COPY", "/*path", ServeWebDAV)
dav.Handle("MOVE", "/*path", ServeWebDAV)
}
func ServeWebDAV(c *gin.Context) {
fs := webdav.FileSystem{}
handler.ServeHTTP(c.Writer,c.Request,&fs)
}
func WebDAVAuth(c *gin.Context) {
if c.Request.Method == "OPTIONS" {
c.Next()
return
}
username, password, ok := c.Request.BasicAuth()
if !ok {
c.Writer.Header()["WWW-Authenticate"] = []string{`Basic realm="alist"`}
c.Status(http.StatusUnauthorized)
c.Abort()
return
}
if conf.DavUsername != "" && conf.DavUsername != username {
c.Status(http.StatusUnauthorized)
c.Abort()
}
if conf.DavPassword != "" && conf.DavPassword != password {
c.Status(http.StatusUnauthorized)
c.Abort()
}
c.Next()
}

319
server/webdav/file.go Normal file
View File

@ -0,0 +1,319 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package webdav
import (
"context"
"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"
"net/http"
"path"
"path/filepath"
"strings"
"time"
)
type FileSystem struct{}
func ParsePath(rawPath string) (*model.Account, string, base.Driver, error) {
var internalPath, name string
switch model.AccountsCount() {
case 0:
return nil, "", nil, fmt.Errorf("no accounts,please add one first")
case 1:
internalPath = rawPath
break
default:
paths := strings.Split(rawPath, "/")
internalPath = "/" + strings.Join(paths[2:], "/")
name = paths[1]
}
account, ok := model.GetAccount(name)
if !ok {
return nil, "", nil, fmt.Errorf("no [%s] account", name)
}
driver, ok := base.GetDriver(account.Type)
if !ok {
return nil, "", nil, fmt.Errorf("no [%s] driver", account.Type)
}
return &account, internalPath, driver, nil
}
func (fs *FileSystem) File(rawPath string) (*model.File, error) {
rawPath = utils.ParsePath(rawPath)
if model.AccountsCount() > 1 && rawPath == "/" {
now := time.Now()
return &model.File{
Name: "root",
Size: 0,
Type: conf.FOLDER,
Driver: "root",
UpdatedAt: &now,
}, nil
}
account, path_, driver, err := ParsePath(rawPath)
if err != nil {
return nil, err
}
return driver.File(path_, account)
}
func (fs *FileSystem) Files(rawPath string) ([]model.File, error) {
rawPath = utils.ParsePath(rawPath)
if model.AccountsCount() > 1 && rawPath == "/" {
files, err := model.GetAccountFiles()
if err != nil {
return nil, err
}
return files, nil
}
account, path_, driver, err := ParsePath(rawPath)
if err != nil {
return nil, err
}
return driver.Files(path_, account)
}
//func GetPW(path string, name string) string {
// if !conf.CheckDown {
// return ""
// }
// meta, err := model.GetMetaByPath(path)
// if err == nil {
// if meta.Password != "" {
// return utils.SignWithPassword(name, meta.Password)
// }
// return ""
// } else {
// if !conf.CheckParent {
// return ""
// }
// if path == "/" {
// return ""
// }
// return GetPW(utils.Dir(path), name)
// }
//}
func ClientIP(r *http.Request) string {
xForwardedFor := r.Header.Get("X-Forwarded-For")
ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
if ip != "" {
return ip
}
ip = strings.TrimSpace(r.Header.Get("X-Real-Ip"))
if ip != "" {
return ip
}
if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
return ip
}
return ""
}
func (fs *FileSystem) Link(r *http.Request, rawPath string) (string, error) {
rawPath = utils.ParsePath(rawPath)
log.Debugf("get link path: %s", rawPath)
if model.AccountsCount() > 1 && rawPath == "/" {
// error
}
account, path_, driver, err := ParsePath(rawPath)
if err != nil {
return "", err
}
link := ""
protocol := "http"
if r.TLS != nil {
protocol = "https"
}
if driver.Config().OnlyProxy || account.WebdavProxy {
link = fmt.Sprintf("%s://%s/p%s", protocol, r.Host, rawPath)
if conf.CheckDown {
sign := utils.SignWithToken(utils.Base(rawPath), conf.Token)
link += "?sign" + sign
}
} else {
link_, err := driver.Link(base.Args{Path: path_, IP: ClientIP(r)}, account)
if err != nil {
return "", err
}
link = link_.Url
}
log.Debugf("webdav get link: %s", link)
return link, err
}
func (fs *FileSystem) CreateDirectory(ctx context.Context, rawPath string) error {
rawPath = utils.ParsePath(rawPath)
if rawPath == "/" {
return ErrNotImplemented
}
if model.AccountsCount() > 1 && len(strings.Split(rawPath, "/")) < 2 {
return ErrNotImplemented
}
account, path_, driver, err := ParsePath(rawPath)
if err != nil {
return err
}
return driver.MakeDir(path_, account)
}
func (fs *FileSystem) Upload(ctx context.Context, r *http.Request, rawPath string) error {
rawPath = utils.ParsePath(rawPath)
if model.AccountsCount() > 1 && rawPath == "/" {
return ErrNotImplemented
}
account, path_, driver, err := ParsePath(rawPath)
if err != nil {
return err
}
//fileSize, err := strconv.ParseUint(r.Header.Get("Content-Length"), 10, 64)
fileSize := uint64(r.ContentLength)
//if err != nil {
// return err
//}
filePath, fileName := filepath.Split(path_)
fileData := model.FileStream{
MIMEType: r.Header.Get("Content-Type"),
File: r.Body,
Size: fileSize,
Name: fileName,
ParentPath: filePath,
}
return driver.Upload(&fileData, account)
}
func (fs *FileSystem) Delete(rawPath string) error {
rawPath = utils.ParsePath(rawPath)
if rawPath == "/" {
return ErrNotImplemented
}
if model.AccountsCount() > 1 && len(strings.Split(rawPath, "/")) < 2 {
return ErrNotImplemented
}
account, path_, driver, err := ParsePath(rawPath)
if err != nil {
return err
}
return driver.Delete(path_, account)
}
// slashClean is equivalent to but slightly more efficient than
// path.Clean("/" + name).
func slashClean(name string) string {
if name == "" || name[0] != '/' {
name = "/" + name
}
return path.Clean(name)
}
// moveFiles moves files and/or directories from src to dst.
//
// See section 9.9.4 for when various HTTP status codes apply.
func moveFiles(ctx context.Context, fs *FileSystem, src string, dst string, overwrite bool) (status int, err error) {
src = utils.ParsePath(src)
dst = utils.ParsePath(dst)
log.Debugf("move %s -> %s", src, dst)
if src == dst {
return http.StatusMethodNotAllowed, errDestinationEqualsSource
}
srcAccount, srcPath, driver, err := ParsePath(src)
if err != nil {
return http.StatusMethodNotAllowed, err
}
dstAccount, dstPath, _, err := ParsePath(dst)
if err != nil {
return http.StatusMethodNotAllowed, err
}
if srcAccount.Name != dstAccount.Name {
return http.StatusMethodNotAllowed, errInvalidDestination
}
err = driver.Move(srcPath, dstPath, srcAccount)
if err != nil {
log.Debug(err)
return http.StatusInternalServerError, err
}
return http.StatusNoContent, nil
}
// copyFiles copies files and/or directories from src to dst.
//
// See section 9.8.5 for when various HTTP status codes apply.
func copyFiles(ctx context.Context, fs *FileSystem, src string, dst string, overwrite bool, depth int, recursion int) (status int, err error) {
src = utils.ParsePath(src)
dst = utils.ParsePath(dst)
log.Debugf("move %s -> %s", src, dst)
if src == dst {
return http.StatusMethodNotAllowed, errDestinationEqualsSource
}
srcAccount, srcPath, driver, err := ParsePath(src)
if err != nil {
return http.StatusMethodNotAllowed, err
}
dstAccount, dstPath, _, err := ParsePath(dst)
if err != nil {
return http.StatusMethodNotAllowed, err
}
if srcAccount.Name != dstAccount.Name {
// TODO 跨账号复制
return http.StatusMethodNotAllowed, errInvalidDestination
}
err = driver.Copy(srcPath, dstPath, srcAccount)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusNoContent, nil
}
// walkFS traverses filesystem fs starting at name up to depth levels.
//
// Allowed values for depth are 0, 1 or infiniteDepth. For each visited node,
// walkFS calls walkFn. If a visited file system node is a directory and
// walkFn returns filepath.SkipDir, walkFS will skip traversal of this node.
func walkFS(
ctx context.Context,
fs *FileSystem,
depth int,
name string,
info FileInfo,
walkFn func(reqPath string, info FileInfo, err error) error) error {
// This implementation is based on Walk's code in the standard path/filepath package.
err := walkFn(name, info, nil)
if err != nil {
if info.IsDir() && err == filepath.SkipDir {
return nil
}
return err
}
if !info.IsDir() || depth == 0 {
return nil
}
if depth == 1 {
depth = 0
}
files, err := fs.Files(name)
if err != nil {
return err
}
for _, fileInfo := range files {
filename := path.Join(name, fileInfo.Name)
err = walkFS(ctx, fs, depth, filename, &fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
return nil
}

173
server/webdav/if.go Normal file
View File

@ -0,0 +1,173 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package webdav
// The If header is covered by Section 10.4.
// http://www.webdav.org/specs/rfc4918.html#HEADER_If
import (
"strings"
)
// ifHeader is a disjunction (OR) of ifLists.
type ifHeader struct {
lists []ifList
}
// ifList is a conjunction (AND) of Conditions, and an optional resource tag.
type ifList struct {
resourceTag string
conditions []Condition
}
// parseIfHeader parses the "If: foo bar" HTTP header. The httpHeader string
// should omit the "If:" prefix and have any "\r\n"s collapsed to a " ", as is
// returned by req.Header.Get("If") for a http.Request req.
func parseIfHeader(httpHeader string) (h ifHeader, ok bool) {
s := strings.TrimSpace(httpHeader)
switch tokenType, _, _ := lex(s); tokenType {
case '(':
return parseNoTagLists(s)
case angleTokenType:
return parseTaggedLists(s)
default:
return ifHeader{}, false
}
}
func parseNoTagLists(s string) (h ifHeader, ok bool) {
for {
l, remaining, ok := parseList(s)
if !ok {
return ifHeader{}, false
}
h.lists = append(h.lists, l)
if remaining == "" {
return h, true
}
s = remaining
}
}
func parseTaggedLists(s string) (h ifHeader, ok bool) {
resourceTag, n := "", 0
for first := true; ; first = false {
tokenType, tokenStr, remaining := lex(s)
switch tokenType {
case angleTokenType:
if !first && n == 0 {
return ifHeader{}, false
}
resourceTag, n = tokenStr, 0
s = remaining
case '(':
n++
l, remaining, ok := parseList(s)
if !ok {
return ifHeader{}, false
}
l.resourceTag = resourceTag
h.lists = append(h.lists, l)
if remaining == "" {
return h, true
}
s = remaining
default:
return ifHeader{}, false
}
}
}
func parseList(s string) (l ifList, remaining string, ok bool) {
tokenType, _, s := lex(s)
if tokenType != '(' {
return ifList{}, "", false
}
for {
tokenType, _, remaining = lex(s)
if tokenType == ')' {
if len(l.conditions) == 0 {
return ifList{}, "", false
}
return l, remaining, true
}
c, remaining, ok := parseCondition(s)
if !ok {
return ifList{}, "", false
}
l.conditions = append(l.conditions, c)
s = remaining
}
}
func parseCondition(s string) (c Condition, remaining string, ok bool) {
tokenType, tokenStr, s := lex(s)
if tokenType == notTokenType {
c.Not = true
tokenType, tokenStr, s = lex(s)
}
switch tokenType {
case strTokenType, angleTokenType:
c.Token = tokenStr
case squareTokenType:
c.ETag = tokenStr
default:
return Condition{}, "", false
}
return c, s, true
}
// Single-rune tokens like '(' or ')' have a token type equal to their rune.
// All other tokens have a negative token type.
const (
errTokenType = rune(-1)
eofTokenType = rune(-2)
strTokenType = rune(-3)
notTokenType = rune(-4)
angleTokenType = rune(-5)
squareTokenType = rune(-6)
)
func lex(s string) (tokenType rune, tokenStr string, remaining string) {
// The net/textproto Data that parses the HTTP header will collapse
// Linear White Space that spans multiple "\r\n" lines to a single " ",
// so we don't need to look for '\r' or '\n'.
for len(s) > 0 && (s[0] == '\t' || s[0] == ' ') {
s = s[1:]
}
if len(s) == 0 {
return eofTokenType, "", ""
}
i := 0
loop:
for ; i < len(s); i++ {
switch s[i] {
case '\t', ' ', '(', ')', '<', '>', '[', ']':
break loop
}
}
if i != 0 {
tokenStr, remaining = s[:i], s[i:]
if tokenStr == "Not" {
return notTokenType, "", remaining
}
return strTokenType, tokenStr, remaining
}
j := 0
switch s[0] {
case '<':
j, tokenType = strings.IndexByte(s, '>'), angleTokenType
case '[':
j, tokenType = strings.IndexByte(s, ']'), squareTokenType
default:
return rune(s[0]), "", s[1:]
}
if j < 0 {
return errTokenType, "", ""
}
return tokenType, s[1:j], s[j+1:]
}

View File

@ -0,0 +1,11 @@
This is a fork of the encoding/xml package at ca1d6c4, the last commit before
https://go.googlesource.com/go/+/c0d6d33 "encoding/xml: restore Go 1.4 name
space behavior" made late in the lead-up to the Go 1.5 release.
The list of encoding/xml changes is at
https://go.googlesource.com/go/+log/master/src/encoding/xml
This fork is temporary, and I (nigeltao) expect to revert it after Go 1.6 is
released.
See http://golang.org/issue/11841

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,692 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xml
import (
"bytes"
"encoding"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
)
// BUG(rsc): Mapping between XML elements and data structures is inherently flawed:
// an XML element is an order-dependent collection of anonymous
// values, while a data structure is an order-independent collection
// of named values.
// See package json for a textual representation more suitable
// to data structures.
// Unmarshal parses the XML-encoded data and stores the result in
// the value pointed to by v, which must be an arbitrary struct,
// slice, or string. Well-formed data that does not fit into v is
// discarded.
//
// Because Unmarshal uses the reflect package, it can only assign
// to exported (upper case) fields. Unmarshal uses a case-sensitive
// comparison to match XML element names to tag values and struct
// field names.
//
// Unmarshal maps an XML element to a struct using the following rules.
// In the rules, the tag of a field refers to the value associated with the
// key 'xml' in the struct field's tag (see the example above).
//
// * If the struct has a field of type []byte or string with tag
// ",innerxml", Unmarshal accumulates the raw XML nested inside the
// element in that field. The rest of the rules still apply.
//
// * If the struct has a field named XMLName of type xml.Name,
// Unmarshal records the element name in that field.
//
// * If the XMLName field has an associated tag of the form
// "name" or "namespace-URL name", the XML element must have
// the given name (and, optionally, name space) or else Unmarshal
// returns an error.
//
// * If the XML element has an attribute whose name matches a
// struct field name with an associated tag containing ",attr" or
// the explicit name in a struct field tag of the form "name,attr",
// Unmarshal records the attribute value in that field.
//
// * If the XML element contains character data, that data is
// accumulated in the first struct field that has tag ",chardata".
// The struct field may have type []byte or string.
// If there is no such field, the character data is discarded.
//
// * If the XML element contains comments, they are accumulated in
// the first struct field that has tag ",comment". The struct
// field may have type []byte or string. If there is no such
// field, the comments are discarded.
//
// * If the XML element contains a sub-element whose name matches
// the prefix of a tag formatted as "a" or "a>b>c", unmarshal
// will descend into the XML structure looking for elements with the
// given names, and will map the innermost elements to that struct
// field. A tag starting with ">" is equivalent to one starting
// with the field name followed by ">".
//
// * If the XML element contains a sub-element whose name matches
// a struct field's XMLName tag and the struct field has no
// explicit name tag as per the previous rule, unmarshal maps
// the sub-element to that struct field.
//
// * If the XML element contains a sub-element whose name matches a
// field without any mode flags (",attr", ",chardata", etc), Unmarshal
// maps the sub-element to that struct field.
//
// * If the XML element contains a sub-element that hasn't matched any
// of the above rules and the struct has a field with tag ",any",
// unmarshal maps the sub-element to that struct field.
//
// * An anonymous struct field is handled as if the fields of its
// value were part of the outer struct.
//
// * A struct field with tag "-" is never unmarshalled into.
//
// Unmarshal maps an XML element to a string or []byte by saving the
// concatenation of that element's character data in the string or
// []byte. The saved []byte is never nil.
//
// Unmarshal maps an attribute value to a string or []byte by saving
// the value in the string or slice.
//
// Unmarshal maps an XML element to a slice by extending the length of
// the slice and mapping the element to the newly created value.
//
// Unmarshal maps an XML element or attribute value to a bool by
// setting it to the boolean value represented by the string.
//
// Unmarshal maps an XML element or attribute value to an integer or
// floating-point field by setting the field to the result of
// interpreting the string value in decimal. There is no check for
// overflow.
//
// Unmarshal maps an XML element to an xml.Name by recording the
// element name.
//
// Unmarshal maps an XML element to a pointer by setting the pointer
// to a freshly allocated value and then mapping the element to that value.
//
func Unmarshal(data []byte, v interface{}) error {
return NewDecoder(bytes.NewReader(data)).Decode(v)
}
// Decode works like xml.Unmarshal, except it reads the decoder
// stream to find the start element.
func (d *Decoder) Decode(v interface{}) error {
return d.DecodeElement(v, nil)
}
// DecodeElement works like xml.Unmarshal except that it takes
// a pointer to the start XML element to decode into v.
// It is useful when a client reads some raw XML tokens itself
// but also wants to defer to Unmarshal for some elements.
func (d *Decoder) DecodeElement(v interface{}, start *StartElement) error {
val := reflect.ValueOf(v)
if val.Kind() != reflect.Ptr {
return errors.New("non-pointer passed to Unmarshal")
}
return d.unmarshal(val.Elem(), start)
}
// An UnmarshalError represents an error in the unmarshalling process.
type UnmarshalError string
func (e UnmarshalError) Error() string { return string(e) }
// Unmarshaler is the interface implemented by objects that can unmarshal
// an XML element description of themselves.
//
// UnmarshalXML decodes a single XML element
// beginning with the given start element.
// If it returns an error, the outer call to Unmarshal stops and
// returns that error.
// UnmarshalXML must consume exactly one XML element.
// One common implementation strategy is to unmarshal into
// a separate value with a layout matching the expected XML
// using d.DecodeElement, and then to copy the data from
// that value into the receiver.
// Another common strategy is to use d.Token to process the
// XML object one token at a time.
// UnmarshalXML may not use d.RawToken.
type Unmarshaler interface {
UnmarshalXML(d *Decoder, start StartElement) error
}
// UnmarshalerAttr is the interface implemented by objects that can unmarshal
// an XML attribute description of themselves.
//
// UnmarshalXMLAttr decodes a single XML attribute.
// If it returns an error, the outer call to Unmarshal stops and
// returns that error.
// UnmarshalXMLAttr is used only for struct fields with the
// "attr" option in the field tag.
type UnmarshalerAttr interface {
UnmarshalXMLAttr(attr Attr) error
}
// receiverType returns the receiver type to use in an expression like "%s.MethodName".
func receiverType(val interface{}) string {
t := reflect.TypeOf(val)
if t.Name() != "" {
return t.String()
}
return "(" + t.String() + ")"
}
// unmarshalInterface unmarshals a single XML element into val.
// start is the opening tag of the element.
func (p *Decoder) unmarshalInterface(val Unmarshaler, start *StartElement) error {
// Record that decoder must stop at end tag corresponding to start.
p.pushEOF()
p.unmarshalDepth++
err := val.UnmarshalXML(p, *start)
p.unmarshalDepth--
if err != nil {
p.popEOF()
return err
}
if !p.popEOF() {
return fmt.Errorf("xml: %s.UnmarshalXML did not consume entire <%s> element", receiverType(val), start.Name.Local)
}
return nil
}
// unmarshalTextInterface unmarshals a single XML element into val.
// The chardata contained in the element (but not its children)
// is passed to the text unmarshaler.
func (p *Decoder) unmarshalTextInterface(val encoding.TextUnmarshaler, start *StartElement) error {
var buf []byte
depth := 1
for depth > 0 {
t, err := p.Token()
if err != nil {
return err
}
switch t := t.(type) {
case CharData:
if depth == 1 {
buf = append(buf, t...)
}
case StartElement:
depth++
case EndElement:
depth--
}
}
return val.UnmarshalText(buf)
}
// unmarshalAttr unmarshals a single XML attribute into val.
func (p *Decoder) unmarshalAttr(val reflect.Value, attr Attr) error {
if val.Kind() == reflect.Ptr {
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
val = val.Elem()
}
if val.CanInterface() && val.Type().Implements(unmarshalerAttrType) {
// This is an unmarshaler with a non-pointer receiver,
// so it's likely to be incorrect, but we do what we're told.
return val.Interface().(UnmarshalerAttr).UnmarshalXMLAttr(attr)
}
if val.CanAddr() {
pv := val.Addr()
if pv.CanInterface() && pv.Type().Implements(unmarshalerAttrType) {
return pv.Interface().(UnmarshalerAttr).UnmarshalXMLAttr(attr)
}
}
// Not an UnmarshalerAttr; try encoding.TextUnmarshaler.
if val.CanInterface() && val.Type().Implements(textUnmarshalerType) {
// This is an unmarshaler with a non-pointer receiver,
// so it's likely to be incorrect, but we do what we're told.
return val.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(attr.Value))
}
if val.CanAddr() {
pv := val.Addr()
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {
return pv.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(attr.Value))
}
}
copyValue(val, []byte(attr.Value))
return nil
}
var (
unmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
unmarshalerAttrType = reflect.TypeOf((*UnmarshalerAttr)(nil)).Elem()
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
)
// Unmarshal a single XML element into val.
func (p *Decoder) unmarshal(val reflect.Value, start *StartElement) error {
// Find start element if we need it.
if start == nil {
for {
tok, err := p.Token()
if err != nil {
return err
}
if t, ok := tok.(StartElement); ok {
start = &t
break
}
}
}
// Load value from interface, but only if the result will be
// usefully addressable.
if val.Kind() == reflect.Interface && !val.IsNil() {
e := val.Elem()
if e.Kind() == reflect.Ptr && !e.IsNil() {
val = e
}
}
if val.Kind() == reflect.Ptr {
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
val = val.Elem()
}
if val.CanInterface() && val.Type().Implements(unmarshalerType) {
// This is an unmarshaler with a non-pointer receiver,
// so it's likely to be incorrect, but we do what we're told.
return p.unmarshalInterface(val.Interface().(Unmarshaler), start)
}
if val.CanAddr() {
pv := val.Addr()
if pv.CanInterface() && pv.Type().Implements(unmarshalerType) {
return p.unmarshalInterface(pv.Interface().(Unmarshaler), start)
}
}
if val.CanInterface() && val.Type().Implements(textUnmarshalerType) {
return p.unmarshalTextInterface(val.Interface().(encoding.TextUnmarshaler), start)
}
if val.CanAddr() {
pv := val.Addr()
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {
return p.unmarshalTextInterface(pv.Interface().(encoding.TextUnmarshaler), start)
}
}
var (
data []byte
saveData reflect.Value
comment []byte
saveComment reflect.Value
saveXML reflect.Value
saveXMLIndex int
saveXMLData []byte
saveAny reflect.Value
sv reflect.Value
tinfo *typeInfo
err error
)
switch v := val; v.Kind() {
default:
return errors.New("unknown type " + v.Type().String())
case reflect.Interface:
// TODO: For now, simply ignore the field. In the near
// future we may choose to unmarshal the start
// element on it, if not nil.
return p.Skip()
case reflect.Slice:
typ := v.Type()
if typ.Elem().Kind() == reflect.Uint8 {
// []byte
saveData = v
break
}
// Slice of element values.
// Grow slice.
n := v.Len()
if n >= v.Cap() {
ncap := 2 * n
if ncap < 4 {
ncap = 4
}
new := reflect.MakeSlice(typ, n, ncap)
reflect.Copy(new, v)
v.Set(new)
}
v.SetLen(n + 1)
// Recur to read element into slice.
if err := p.unmarshal(v.Index(n), start); err != nil {
v.SetLen(n)
return err
}
return nil
case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.String:
saveData = v
case reflect.Struct:
typ := v.Type()
if typ == nameType {
v.Set(reflect.ValueOf(start.Name))
break
}
sv = v
tinfo, err = getTypeInfo(typ)
if err != nil {
return err
}
// Validate and assign element name.
if tinfo.xmlname != nil {
finfo := tinfo.xmlname
if finfo.name != "" && finfo.name != start.Name.Local {
return UnmarshalError("expected element type <" + finfo.name + "> but have <" + start.Name.Local + ">")
}
if finfo.xmlns != "" && finfo.xmlns != start.Name.Space {
e := "expected element <" + finfo.name + "> in name space " + finfo.xmlns + " but have "
if start.Name.Space == "" {
e += "no name space"
} else {
e += start.Name.Space
}
return UnmarshalError(e)
}
fv := finfo.value(sv)
if _, ok := fv.Interface().(Name); ok {
fv.Set(reflect.ValueOf(start.Name))
}
}
// Assign attributes.
// Also, determine whether we need to save character data or comments.
for i := range tinfo.fields {
finfo := &tinfo.fields[i]
switch finfo.flags & fMode {
case fAttr:
strv := finfo.value(sv)
// Look for attribute.
for _, a := range start.Attr {
if a.Name.Local == finfo.name && (finfo.xmlns == "" || finfo.xmlns == a.Name.Space) {
if err := p.unmarshalAttr(strv, a); err != nil {
return err
}
break
}
}
case fCharData:
if !saveData.IsValid() {
saveData = finfo.value(sv)
}
case fComment:
if !saveComment.IsValid() {
saveComment = finfo.value(sv)
}
case fAny, fAny | fElement:
if !saveAny.IsValid() {
saveAny = finfo.value(sv)
}
case fInnerXml:
if !saveXML.IsValid() {
saveXML = finfo.value(sv)
if p.saved == nil {
saveXMLIndex = 0
p.saved = new(bytes.Buffer)
} else {
saveXMLIndex = p.savedOffset()
}
}
}
}
}
// Find end element.
// Process sub-elements along the way.
Loop:
for {
var savedOffset int
if saveXML.IsValid() {
savedOffset = p.savedOffset()
}
tok, err := p.Token()
if err != nil {
return err
}
switch t := tok.(type) {
case StartElement:
consumed := false
if sv.IsValid() {
consumed, err = p.unmarshalPath(tinfo, sv, nil, &t)
if err != nil {
return err
}
if !consumed && saveAny.IsValid() {
consumed = true
if err := p.unmarshal(saveAny, &t); err != nil {
return err
}
}
}
if !consumed {
if err := p.Skip(); err != nil {
return err
}
}
case EndElement:
if saveXML.IsValid() {
saveXMLData = p.saved.Bytes()[saveXMLIndex:savedOffset]
if saveXMLIndex == 0 {
p.saved = nil
}
}
break Loop
case CharData:
if saveData.IsValid() {
data = append(data, t...)
}
case Comment:
if saveComment.IsValid() {
comment = append(comment, t...)
}
}
}
if saveData.IsValid() && saveData.CanInterface() && saveData.Type().Implements(textUnmarshalerType) {
if err := saveData.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil {
return err
}
saveData = reflect.Value{}
}
if saveData.IsValid() && saveData.CanAddr() {
pv := saveData.Addr()
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {
if err := pv.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil {
return err
}
saveData = reflect.Value{}
}
}
if err := copyValue(saveData, data); err != nil {
return err
}
switch t := saveComment; t.Kind() {
case reflect.String:
t.SetString(string(comment))
case reflect.Slice:
t.Set(reflect.ValueOf(comment))
}
switch t := saveXML; t.Kind() {
case reflect.String:
t.SetString(string(saveXMLData))
case reflect.Slice:
t.Set(reflect.ValueOf(saveXMLData))
}
return nil
}
func copyValue(dst reflect.Value, src []byte) (err error) {
dst0 := dst
if dst.Kind() == reflect.Ptr {
if dst.IsNil() {
dst.Set(reflect.New(dst.Type().Elem()))
}
dst = dst.Elem()
}
// Save accumulated data.
switch dst.Kind() {
case reflect.Invalid:
// Probably a comment.
default:
return errors.New("cannot unmarshal into " + dst0.Type().String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
itmp, err := strconv.ParseInt(string(src), 10, dst.Type().Bits())
if err != nil {
return err
}
dst.SetInt(itmp)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
utmp, err := strconv.ParseUint(string(src), 10, dst.Type().Bits())
if err != nil {
return err
}
dst.SetUint(utmp)
case reflect.Float32, reflect.Float64:
ftmp, err := strconv.ParseFloat(string(src), dst.Type().Bits())
if err != nil {
return err
}
dst.SetFloat(ftmp)
case reflect.Bool:
value, err := strconv.ParseBool(strings.TrimSpace(string(src)))
if err != nil {
return err
}
dst.SetBool(value)
case reflect.String:
dst.SetString(string(src))
case reflect.Slice:
if len(src) == 0 {
// non-nil to flag presence
src = []byte{}
}
dst.SetBytes(src)
}
return nil
}
// unmarshalPath walks down an XML structure looking for wanted
// paths, and calls unmarshal on them.
// The consumed result tells whether XML elements have been consumed
// from the Decoder until start's matching end element, or if it's
// still untouched because start is uninteresting for sv's fields.
func (p *Decoder) unmarshalPath(tinfo *typeInfo, sv reflect.Value, parents []string, start *StartElement) (consumed bool, err error) {
recurse := false
Loop:
for i := range tinfo.fields {
finfo := &tinfo.fields[i]
if finfo.flags&fElement == 0 || len(finfo.parents) < len(parents) || finfo.xmlns != "" && finfo.xmlns != start.Name.Space {
continue
}
for j := range parents {
if parents[j] != finfo.parents[j] {
continue Loop
}
}
if len(finfo.parents) == len(parents) && finfo.name == start.Name.Local {
// It's a perfect match, unmarshal the field.
return true, p.unmarshal(finfo.value(sv), start)
}
if len(finfo.parents) > len(parents) && finfo.parents[len(parents)] == start.Name.Local {
// It's a prefix for the field. Break and recurse
// since it's not ok for one field path to be itself
// the prefix for another field path.
recurse = true
// We can reuse the same slice as long as we
// don't try to append to it.
parents = finfo.parents[:len(parents)+1]
break
}
}
if !recurse {
// We have no business with this element.
return false, nil
}
// The element is not a perfect match for any field, but one
// or more fields have the path to this element as a parent
// prefix. Recurse and attempt to match these.
for {
var tok Token
tok, err = p.Token()
if err != nil {
return true, err
}
switch t := tok.(type) {
case StartElement:
consumed2, err := p.unmarshalPath(tinfo, sv, parents, &t)
if err != nil {
return true, err
}
if !consumed2 {
if err := p.Skip(); err != nil {
return true, err
}
}
case EndElement:
return true, nil
}
}
}
// Skip reads tokens until it has consumed the end element
// matching the most recent start element already consumed.
// It recurs if it encounters a start element, so it can be used to
// skip nested structures.
// It returns nil if it finds an end element matching the start
// element; otherwise it returns an error describing the problem.
func (d *Decoder) Skip() error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch tok.(type) {
case StartElement:
if err := d.Skip(); err != nil {
return err
}
case EndElement:
return nil
}
}
}

View File

@ -0,0 +1,371 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xml
import (
"fmt"
"reflect"
"strings"
"sync"
)
// typeInfo holds details for the xml representation of a type.
type typeInfo struct {
xmlname *fieldInfo
fields []fieldInfo
}
// fieldInfo holds details for the xml representation of a single field.
type fieldInfo struct {
idx []int
name string
xmlns string
flags fieldFlags
parents []string
}
type fieldFlags int
const (
fElement fieldFlags = 1 << iota
fAttr
fCharData
fInnerXml
fComment
fAny
fOmitEmpty
fMode = fElement | fAttr | fCharData | fInnerXml | fComment | fAny
)
var tinfoMap = make(map[reflect.Type]*typeInfo)
var tinfoLock sync.RWMutex
var nameType = reflect.TypeOf(Name{})
// getTypeInfo returns the typeInfo structure with details necessary
// for marshalling and unmarshalling typ.
func getTypeInfo(typ reflect.Type) (*typeInfo, error) {
tinfoLock.RLock()
tinfo, ok := tinfoMap[typ]
tinfoLock.RUnlock()
if ok {
return tinfo, nil
}
tinfo = &typeInfo{}
if typ.Kind() == reflect.Struct && typ != nameType {
n := typ.NumField()
for i := 0; i < n; i++ {
f := typ.Field(i)
if f.PkgPath != "" || f.Tag.Get("xml") == "-" {
continue // Private field
}
// For embedded structs, embed its fields.
if f.Anonymous {
t := f.Type
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() == reflect.Struct {
inner, err := getTypeInfo(t)
if err != nil {
return nil, err
}
if tinfo.xmlname == nil {
tinfo.xmlname = inner.xmlname
}
for _, finfo := range inner.fields {
finfo.idx = append([]int{i}, finfo.idx...)
if err := addFieldInfo(typ, tinfo, &finfo); err != nil {
return nil, err
}
}
continue
}
}
finfo, err := structFieldInfo(typ, &f)
if err != nil {
return nil, err
}
if f.Name == "XMLName" {
tinfo.xmlname = finfo
continue
}
// Add the field if it doesn't conflict with other fields.
if err := addFieldInfo(typ, tinfo, finfo); err != nil {
return nil, err
}
}
}
tinfoLock.Lock()
tinfoMap[typ] = tinfo
tinfoLock.Unlock()
return tinfo, nil
}
// structFieldInfo builds and returns a fieldInfo for f.
func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, error) {
finfo := &fieldInfo{idx: f.Index}
// Split the tag from the xml namespace if necessary.
tag := f.Tag.Get("xml")
if i := strings.Index(tag, " "); i >= 0 {
finfo.xmlns, tag = tag[:i], tag[i+1:]
}
// Parse flags.
tokens := strings.Split(tag, ",")
if len(tokens) == 1 {
finfo.flags = fElement
} else {
tag = tokens[0]
for _, flag := range tokens[1:] {
switch flag {
case "attr":
finfo.flags |= fAttr
case "chardata":
finfo.flags |= fCharData
case "innerxml":
finfo.flags |= fInnerXml
case "comment":
finfo.flags |= fComment
case "any":
finfo.flags |= fAny
case "omitempty":
finfo.flags |= fOmitEmpty
}
}
// Validate the flags used.
valid := true
switch mode := finfo.flags & fMode; mode {
case 0:
finfo.flags |= fElement
case fAttr, fCharData, fInnerXml, fComment, fAny:
if f.Name == "XMLName" || tag != "" && mode != fAttr {
valid = false
}
default:
// This will also catch multiple modes in a single field.
valid = false
}
if finfo.flags&fMode == fAny {
finfo.flags |= fElement
}
if finfo.flags&fOmitEmpty != 0 && finfo.flags&(fElement|fAttr) == 0 {
valid = false
}
if !valid {
return nil, fmt.Errorf("xml: invalid tag in field %s of type %s: %q",
f.Name, typ, f.Tag.Get("xml"))
}
}
// Use of xmlns without a name is not allowed.
if finfo.xmlns != "" && tag == "" {
return nil, fmt.Errorf("xml: namespace without name in field %s of type %s: %q",
f.Name, typ, f.Tag.Get("xml"))
}
if f.Name == "XMLName" {
// The XMLName field records the XML element name. Don't
// process it as usual because its name should default to
// empty rather than to the field name.
finfo.name = tag
return finfo, nil
}
if tag == "" {
// If the name part of the tag is completely empty, get
// default from XMLName of underlying struct if feasible,
// or field name otherwise.
if xmlname := lookupXMLName(f.Type); xmlname != nil {
finfo.xmlns, finfo.name = xmlname.xmlns, xmlname.name
} else {
finfo.name = f.Name
}
return finfo, nil
}
if finfo.xmlns == "" && finfo.flags&fAttr == 0 {
// If it's an element no namespace specified, get the default
// from the XMLName of enclosing struct if possible.
if xmlname := lookupXMLName(typ); xmlname != nil {
finfo.xmlns = xmlname.xmlns
}
}
// Prepare field name and parents.
parents := strings.Split(tag, ">")
if parents[0] == "" {
parents[0] = f.Name
}
if parents[len(parents)-1] == "" {
return nil, fmt.Errorf("xml: trailing '>' in field %s of type %s", f.Name, typ)
}
finfo.name = parents[len(parents)-1]
if len(parents) > 1 {
if (finfo.flags & fElement) == 0 {
return nil, fmt.Errorf("xml: %s chain not valid with %s flag", tag, strings.Join(tokens[1:], ","))
}
finfo.parents = parents[:len(parents)-1]
}
// If the field type has an XMLName field, the names must match
// so that the behavior of both marshalling and unmarshalling
// is straightforward and unambiguous.
if finfo.flags&fElement != 0 {
ftyp := f.Type
xmlname := lookupXMLName(ftyp)
if xmlname != nil && xmlname.name != finfo.name {
return nil, fmt.Errorf("xml: name %q in tag of %s.%s conflicts with name %q in %s.XMLName",
finfo.name, typ, f.Name, xmlname.name, ftyp)
}
}
return finfo, nil
}
// lookupXMLName returns the fieldInfo for typ's XMLName field
// in case it exists and has a valid xml field tag, otherwise
// it returns nil.
func lookupXMLName(typ reflect.Type) (xmlname *fieldInfo) {
for typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
if typ.Kind() != reflect.Struct {
return nil
}
for i, n := 0, typ.NumField(); i < n; i++ {
f := typ.Field(i)
if f.Name != "XMLName" {
continue
}
finfo, err := structFieldInfo(typ, &f)
if finfo.name != "" && err == nil {
return finfo
}
// Also consider errors as a non-existent field tag
// and let getTypeInfo itself report the error.
break
}
return nil
}
func min(a, b int) int {
if a <= b {
return a
}
return b
}
// addFieldInfo adds finfo to tinfo.fields if there are no
// conflicts, or if conflicts arise from previous fields that were
// obtained from deeper embedded structures than finfo. In the latter
// case, the conflicting entries are dropped.
// A conflict occurs when the path (parent + name) to a field is
// itself a prefix of another path, or when two paths match exactly.
// It is okay for field paths to share a common, shorter prefix.
func addFieldInfo(typ reflect.Type, tinfo *typeInfo, newf *fieldInfo) error {
var conflicts []int
Loop:
// First, figure all conflicts. Most working code will have none.
for i := range tinfo.fields {
oldf := &tinfo.fields[i]
if oldf.flags&fMode != newf.flags&fMode {
continue
}
if oldf.xmlns != "" && newf.xmlns != "" && oldf.xmlns != newf.xmlns {
continue
}
minl := min(len(newf.parents), len(oldf.parents))
for p := 0; p < minl; p++ {
if oldf.parents[p] != newf.parents[p] {
continue Loop
}
}
if len(oldf.parents) > len(newf.parents) {
if oldf.parents[len(newf.parents)] == newf.name {
conflicts = append(conflicts, i)
}
} else if len(oldf.parents) < len(newf.parents) {
if newf.parents[len(oldf.parents)] == oldf.name {
conflicts = append(conflicts, i)
}
} else {
if newf.name == oldf.name {
conflicts = append(conflicts, i)
}
}
}
// Without conflicts, add the new field and return.
if conflicts == nil {
tinfo.fields = append(tinfo.fields, *newf)
return nil
}
// If any conflict is shallower, ignore the new field.
// This matches the Go field resolution on embedding.
for _, i := range conflicts {
if len(tinfo.fields[i].idx) < len(newf.idx) {
return nil
}
}
// Otherwise, if any of them is at the same depth level, it's an error.
for _, i := range conflicts {
oldf := &tinfo.fields[i]
if len(oldf.idx) == len(newf.idx) {
f1 := typ.FieldByIndex(oldf.idx)
f2 := typ.FieldByIndex(newf.idx)
return &TagPathError{typ, f1.Name, f1.Tag.Get("xml"), f2.Name, f2.Tag.Get("xml")}
}
}
// Otherwise, the new field is shallower, and thus takes precedence,
// so drop the conflicting fields from tinfo and append the new one.
for c := len(conflicts) - 1; c >= 0; c-- {
i := conflicts[c]
copy(tinfo.fields[i:], tinfo.fields[i+1:])
tinfo.fields = tinfo.fields[:len(tinfo.fields)-1]
}
tinfo.fields = append(tinfo.fields, *newf)
return nil
}
// A TagPathError represents an error in the unmarshalling process
// caused by the use of field tags with conflicting paths.
type TagPathError struct {
Struct reflect.Type
Field1, Tag1 string
Field2, Tag2 string
}
func (e *TagPathError) Error() string {
return fmt.Sprintf("%s field %q with tag %q conflicts with field %q with tag %q", e.Struct, e.Field1, e.Tag1, e.Field2, e.Tag2)
}
// value returns v's field value corresponding to finfo.
// It's equivalent to v.FieldByIndex(finfo.idx), but initializes
// and dereferences pointers as necessary.
func (finfo *fieldInfo) value(v reflect.Value) reflect.Value {
for i, x := range finfo.idx {
if i > 0 {
t := v.Type()
if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
v = v.Elem()
}
}
v = v.Field(x)
}
return v
}

File diff suppressed because it is too large Load Diff

445
server/webdav/lock.go Normal file
View File

@ -0,0 +1,445 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package webdav
import (
"container/heap"
"errors"
"strconv"
"strings"
"sync"
"time"
)
var (
// ErrConfirmationFailed is returned by a LockSystem's Confirm method.
ErrConfirmationFailed = errors.New("webdav: confirmation failed")
// ErrForbidden is returned by a LockSystem's Unlock method.
ErrForbidden = errors.New("webdav: forbidden")
// ErrLocked is returned by a LockSystem's Create, Refresh and Unlock methods.
ErrLocked = errors.New("webdav: locked")
// ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods.
ErrNoSuchLock = errors.New("webdav: no such lock")
)
// Condition can match a WebDAV resource, based on a token or ETag.
// Exactly one of Token and ETag should be non-empty.
type Condition struct {
Not bool
Token string
ETag string
}
// LockSystem manages access to a collection of named resources. The elements
// in a lock name are separated by slash ('/', U+002F) characters, regardless
// of host operating system convention.
type LockSystem interface {
// Confirm confirms that the caller can claim all of the locks specified by
// the given conditions, and that holding the union of all of those locks
// gives exclusive access to all of the named resources. Up to two resources
// can be named. Empty names are ignored.
//
// Exactly one of release and err will be non-nil. If release is non-nil,
// all of the requested locks are held until release is called. Calling
// release does not unlock the lock, in the WebDAV UNLOCK sense, but once
// Confirm has confirmed that a lock claim is valid, that lock cannot be
// Confirmed again until it has been released.
//
// If Confirm returns ErrConfirmationFailed then the Handler will continue
// to try any other set of locks presented (a WebDAV HTTP request can
// present more than one set of locks). If it returns any other non-nil
// error, the Handler will write a "500 Internal Server Error" HTTP status.
Confirm(now time.Time, name0, name1 string, conditions ...Condition) (release func(), err error)
// Create creates a lock with the given depth, duration, owner and root
// (name). The depth will either be negative (meaning infinite) or zero.
//
// If Create returns ErrLocked then the Handler will write a "423 Locked"
// HTTP status. If it returns any other non-nil error, the Handler will
// write a "500 Internal Server Error" HTTP status.
//
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for
// when to use each error.
//
// The token returned identifies the created lock. It should be an absolute
// URI as defined by RFC 3986, Section 4.3. In particular, it should not
// contain whitespace.
Create(now time.Time, details LockDetails) (token string, err error)
// Refresh refreshes the lock with the given token.
//
// If Refresh returns ErrLocked then the Handler will write a "423 Locked"
// HTTP Status. If Refresh returns ErrNoSuchLock then the Handler will write
// a "412 Precondition Failed" HTTP Status. If it returns any other non-nil
// error, the Handler will write a "500 Internal Server Error" HTTP status.
//
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for
// when to use each error.
Refresh(now time.Time, token string, duration time.Duration) (LockDetails, error)
// Unlock unlocks the lock with the given token.
//
// If Unlock returns ErrForbidden then the Handler will write a "403
// Forbidden" HTTP Status. If Unlock returns ErrLocked then the Handler
// will write a "423 Locked" HTTP status. If Unlock returns ErrNoSuchLock
// then the Handler will write a "409 Conflict" HTTP Status. If it returns
// any other non-nil error, the Handler will write a "500 Internal Server
// Error" HTTP status.
//
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.11.1 for
// when to use each error.
Unlock(now time.Time, token string) error
}
// LockDetails are a lock's metadata.
type LockDetails struct {
// Root is the root resource name being locked. For a zero-depth lock, the
// root is the only resource being locked.
Root string
// Duration is the lock timeout. A negative duration means infinite.
Duration time.Duration
// OwnerXML is the verbatim <owner> XML given in a LOCK HTTP request.
//
// TODO: does the "verbatim" nature play well with XML namespaces?
// Does the OwnerXML field need to have more structure? See
// https://codereview.appspot.com/175140043/#msg2
OwnerXML string
// ZeroDepth is whether the lock has zero depth. If it does not have zero
// depth, it has infinite depth.
ZeroDepth bool
}
// NewMemLS returns a new in-memory LockSystem.
func NewMemLS() LockSystem {
return &memLS{
byName: make(map[string]*memLSNode),
byToken: make(map[string]*memLSNode),
gen: uint64(time.Now().Unix()),
}
}
type memLS struct {
mu sync.Mutex
byName map[string]*memLSNode
byToken map[string]*memLSNode
gen uint64
// byExpiry only contains those nodes whose LockDetails have a finite
// Duration and are yet to expire.
byExpiry byExpiry
}
func (m *memLS) nextToken() string {
m.gen++
return strconv.FormatUint(m.gen, 10)
}
func (m *memLS) collectExpiredNodes(now time.Time) {
for len(m.byExpiry) > 0 {
if now.Before(m.byExpiry[0].expiry) {
break
}
m.remove(m.byExpiry[0])
}
}
func (m *memLS) Confirm(now time.Time, name0, name1 string, conditions ...Condition) (func(), error) {
m.mu.Lock()
defer m.mu.Unlock()
m.collectExpiredNodes(now)
var n0, n1 *memLSNode
if name0 != "" {
if n0 = m.lookup(slashClean(name0), conditions...); n0 == nil {
return nil, ErrConfirmationFailed
}
}
if name1 != "" {
if n1 = m.lookup(slashClean(name1), conditions...); n1 == nil {
return nil, ErrConfirmationFailed
}
}
// Don't hold the same node twice.
if n1 == n0 {
n1 = nil
}
if n0 != nil {
m.hold(n0)
}
if n1 != nil {
m.hold(n1)
}
return func() {
m.mu.Lock()
defer m.mu.Unlock()
if n1 != nil {
m.unhold(n1)
}
if n0 != nil {
m.unhold(n0)
}
}, nil
}
// lookup returns the node n that locks the named resource, provided that n
// matches at least one of the given conditions and that lock isn't held by
// another party. Otherwise, it returns nil.
//
// n may be a parent of the named resource, if n is an infinite depth lock.
func (m *memLS) lookup(name string, conditions ...Condition) (n *memLSNode) {
// TODO: support Condition.Not and Condition.ETag.
for _, c := range conditions {
n = m.byToken[c.Token]
if n == nil || n.held {
continue
}
if name == n.details.Root {
return n
}
if n.details.ZeroDepth {
continue
}
if n.details.Root == "/" || strings.HasPrefix(name, n.details.Root+"/") {
return n
}
}
return nil
}
func (m *memLS) hold(n *memLSNode) {
if n.held {
panic("webdav: memLS inconsistent held state")
}
n.held = true
if n.details.Duration >= 0 && n.byExpiryIndex >= 0 {
heap.Remove(&m.byExpiry, n.byExpiryIndex)
}
}
func (m *memLS) unhold(n *memLSNode) {
if !n.held {
panic("webdav: memLS inconsistent held state")
}
n.held = false
if n.details.Duration >= 0 {
heap.Push(&m.byExpiry, n)
}
}
func (m *memLS) Create(now time.Time, details LockDetails) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.collectExpiredNodes(now)
details.Root = slashClean(details.Root)
if !m.canCreate(details.Root, details.ZeroDepth) {
return "", ErrLocked
}
n := m.create(details.Root)
n.token = m.nextToken()
m.byToken[n.token] = n
n.details = details
if n.details.Duration >= 0 {
n.expiry = now.Add(n.details.Duration)
heap.Push(&m.byExpiry, n)
}
return n.token, nil
}
func (m *memLS) Refresh(now time.Time, token string, duration time.Duration) (LockDetails, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.collectExpiredNodes(now)
n := m.byToken[token]
if n == nil {
return LockDetails{}, ErrNoSuchLock
}
if n.held {
return LockDetails{}, ErrLocked
}
if n.byExpiryIndex >= 0 {
heap.Remove(&m.byExpiry, n.byExpiryIndex)
}
n.details.Duration = duration
if n.details.Duration >= 0 {
n.expiry = now.Add(n.details.Duration)
heap.Push(&m.byExpiry, n)
}
return n.details, nil
}
func (m *memLS) Unlock(now time.Time, token string) error {
m.mu.Lock()
defer m.mu.Unlock()
m.collectExpiredNodes(now)
n := m.byToken[token]
if n == nil {
return ErrNoSuchLock
}
if n.held {
return ErrLocked
}
m.remove(n)
return nil
}
func (m *memLS) canCreate(name string, zeroDepth bool) bool {
return walkToRoot(name, func(name0 string, first bool) bool {
n := m.byName[name0]
if n == nil {
return true
}
if first {
if n.token != "" {
// The target node is already locked.
return false
}
if !zeroDepth {
// The requested lock depth is infinite, and the fact that n exists
// (n != nil) means that a descendent of the target node is locked.
return false
}
} else if n.token != "" && !n.details.ZeroDepth {
// An ancestor of the target node is locked with infinite depth.
return false
}
return true
})
}
func (m *memLS) create(name string) (ret *memLSNode) {
walkToRoot(name, func(name0 string, first bool) bool {
n := m.byName[name0]
if n == nil {
n = &memLSNode{
details: LockDetails{
Root: name0,
},
byExpiryIndex: -1,
}
m.byName[name0] = n
}
n.refCount++
if first {
ret = n
}
return true
})
return ret
}
func (m *memLS) remove(n *memLSNode) {
delete(m.byToken, n.token)
n.token = ""
walkToRoot(n.details.Root, func(name0 string, first bool) bool {
x := m.byName[name0]
x.refCount--
if x.refCount == 0 {
delete(m.byName, name0)
}
return true
})
if n.byExpiryIndex >= 0 {
heap.Remove(&m.byExpiry, n.byExpiryIndex)
}
}
func walkToRoot(name string, f func(name0 string, first bool) bool) bool {
for first := true; ; first = false {
if !f(name, first) {
return false
}
if name == "/" {
break
}
name = name[:strings.LastIndex(name, "/")]
if name == "" {
name = "/"
}
}
return true
}
type memLSNode struct {
// details are the lock metadata. Even if this node's name is not explicitly locked,
// details.Root will still equal the node's name.
details LockDetails
// token is the unique identifier for this node's lock. An empty token means that
// this node is not explicitly locked.
token string
// refCount is the number of self-or-descendent nodes that are explicitly locked.
refCount int
// expiry is when this node's lock expires.
expiry time.Time
// byExpiryIndex is the index of this node in memLS.byExpiry. It is -1
// if this node does not expire, or has expired.
byExpiryIndex int
// held is whether this node's lock is actively held by a Confirm call.
held bool
}
type byExpiry []*memLSNode
func (b *byExpiry) Len() int {
return len(*b)
}
func (b *byExpiry) Less(i, j int) bool {
return (*b)[i].expiry.Before((*b)[j].expiry)
}
func (b *byExpiry) Swap(i, j int) {
(*b)[i], (*b)[j] = (*b)[j], (*b)[i]
(*b)[i].byExpiryIndex = i
(*b)[j].byExpiryIndex = j
}
func (b *byExpiry) Push(x interface{}) {
n := x.(*memLSNode)
n.byExpiryIndex = len(*b)
*b = append(*b, n)
}
func (b *byExpiry) Pop() interface{} {
i := len(*b) - 1
n := (*b)[i]
(*b)[i] = nil
n.byExpiryIndex = -1
*b = (*b)[:i]
return n
}
const infiniteTimeout = -1
// parseTimeout parses the Timeout HTTP header, as per section 10.7. If s is
// empty, an infiniteTimeout is returned.
func parseTimeout(s string) (time.Duration, error) {
if s == "" {
return infiniteTimeout, nil
}
if i := strings.IndexByte(s, ','); i >= 0 {
s = s[:i]
}
s = strings.TrimSpace(s)
if s == "Infinite" {
return infiniteTimeout, nil
}
const pre = "Second-"
if !strings.HasPrefix(s, pre) {
return 0, errInvalidTimeout
}
s = s[len(pre):]
if s == "" || s[0] < '0' || '9' < s[0] {
return 0, errInvalidTimeout
}
n, err := strconv.ParseInt(s, 10, 64)
if err != nil || 1<<32-1 < n {
return 0, errInvalidTimeout
}
return time.Duration(n) * time.Second, nil
}

413
server/webdav/prop.go Normal file
View File

@ -0,0 +1,413 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package webdav
import (
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
"net/http"
"strconv"
"time"
)
type FileInfo interface {
GetSize() uint64
GetName() string
ModTime() time.Time
IsDir() bool
//GetPosition() string
}
// Proppatch describes a property update instruction as defined in RFC 4918.
// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
type Proppatch struct {
// Remove specifies whether this patch removes properties. If it does not
// remove them, it sets them.
Remove bool
// Props contains the properties to be set or removed.
Props []Property
}
// Propstat describes a XML propstat element as defined in RFC 4918.
// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
type Propstat struct {
// Props contains the properties for which Status applies.
Props []Property
// Status defines the HTTP status code of the properties in Prop.
// Allowed values include, but are not limited to the WebDAV status
// code extensions for HTTP/1.1.
// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
Status int
// XMLError contains the XML representation of the optional error element.
// XML content within this field must not rely on any predefined
// namespace declarations or prefixes. If empty, the XML error element
// is omitted.
XMLError string
// ResponseDescription contains the contents of the optional
// responsedescription field. If empty, the XML element is omitted.
ResponseDescription string
}
// makePropstats returns a slice containing those of x and y whose Props slice
// is non-empty. If both are empty, it returns a slice containing an otherwise
// zero Propstat whose HTTP status code is 200 OK.
func makePropstats(x, y Propstat) []Propstat {
pstats := make([]Propstat, 0, 2)
if len(x.Props) != 0 {
pstats = append(pstats, x)
}
if len(y.Props) != 0 {
pstats = append(pstats, y)
}
if len(pstats) == 0 {
pstats = append(pstats, Propstat{
Status: http.StatusOK,
})
}
return pstats
}
// DeadPropsHolder holds the dead properties of a resource.
//
// Dead properties are those properties that are explicitly defined. In
// comparison, live properties, such as DAV:getcontentlength, are implicitly
// defined by the underlying resource, and cannot be explicitly overridden or
// removed. See the Terminology section of
// http://www.webdav.org/specs/rfc4918.html#rfc.section.3
//
// There is a whitelist of the names of live properties. This package handles
// all live properties, and will only pass non-whitelisted names to the Patch
// method of DeadPropsHolder implementations.
type DeadPropsHolder interface {
// DeadProps returns a copy of the dead properties held.
DeadProps() (map[xml.Name]Property, error)
// Patch patches the dead properties held.
//
// Patching is atomic; either all or no patches succeed. It returns (nil,
// non-nil) if an internal server error occurred, otherwise the Propstats
// collectively contain one Property for each proposed patch Property. If
// all patches succeed, Patch returns a slice of length one and a Propstat
// element with a 200 OK HTTP status code. If none succeed, for reasons
// other than an internal server error, no Propstat has status 200 OK.
//
// For more details on when various HTTP status codes apply, see
// http://www.webdav.org/specs/rfc4918.html#PROPPATCH-status
Patch([]Proppatch) ([]Propstat, error)
}
// liveProps contains all supported, protected DAV: properties.
var liveProps = map[xml.Name]struct {
// findFn implements the propfind function of this property. If nil,
// it indicates a hidden property.
findFn func(context.Context, *FileSystem, LockSystem, string, FileInfo) (string, error)
// dir is true if the property applies to directories.
dir bool
}{
{Space: "DAV:", Local: "resourcetype"}: {
findFn: findResourceType,
dir: true,
},
{Space: "DAV:", Local: "displayname"}: {
findFn: findDisplayName,
dir: true,
},
{Space: "DAV:", Local: "getcontentlength"}: {
findFn: findContentLength,
dir: false,
},
{Space: "DAV:", Local: "getlastmodified"}: {
findFn: findLastModified,
// http://webdav.org/specs/rfc4918.html#PROPERTY_getlastmodified
// suggests that getlastmodified should only apply to GETable
// resources, and this package does not support GET on directories.
//
// Nonetheless, some WebDAV clients expect child directories to be
// sortable by getlastmodified date, so this value is true, not false.
// See golang.org/issue/15334.
dir: true,
},
{Space: "DAV:", Local: "creationdate"}: {
findFn: nil,
dir: false,
},
{Space: "DAV:", Local: "getcontentlanguage"}: {
findFn: nil,
dir: false,
},
{Space: "DAV:", Local: "getcontenttype"}: {
findFn: findContentType,
dir: false,
},
{Space: "DAV:", Local: "getetag"}: {
findFn: findETag,
// findETag implements ETag as the concatenated hex values of a file's
// modification time and size. This is not a reliable synchronization
// mechanism for directories, so we do not advertise getetag for DAV
// collections.
dir: false,
},
// TODO: The lockdiscovery property requires LockSystem to list the
// active locks on a resource.
{Space: "DAV:", Local: "lockdiscovery"}: {},
{Space: "DAV:", Local: "supportedlock"}: {
findFn: findSupportedLock,
dir: true,
},
}
// TODO(nigeltao) merge props and allprop?
// Props returns the status of the properties named pnames for resource name.
//
// Each Propstat has a unique status and each property name will only be part
// of one Propstat element.
func props(ctx context.Context, fs *FileSystem, ls LockSystem, fi FileInfo, pnames []xml.Name) ([]Propstat, error) {
isDir := fi.IsDir()
var deadProps map[xml.Name]Property
pstatOK := Propstat{Status: http.StatusOK}
pstatNotFound := Propstat{Status: http.StatusNotFound}
for _, pn := range pnames {
// If this file has dead properties, check if they contain pn.
if dp, ok := deadProps[pn]; ok {
pstatOK.Props = append(pstatOK.Props, dp)
continue
}
// Otherwise, it must either be a live property or we don't know it.
if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !isDir) {
innerXML, err := prop.findFn(ctx, fs, ls, fi.GetName(), fi)
if err != nil {
return nil, err
}
pstatOK.Props = append(pstatOK.Props, Property{
XMLName: pn,
InnerXML: []byte(innerXML),
})
} else {
pstatNotFound.Props = append(pstatNotFound.Props, Property{
XMLName: pn,
})
}
}
return makePropstats(pstatOK, pstatNotFound), nil
}
// Propnames returns the property names defined for resource name.
func propnames(ctx context.Context, fs *FileSystem, ls LockSystem, fi FileInfo) ([]xml.Name, error) {
isDir := fi.IsDir()
var deadProps map[xml.Name]Property
pnames := make([]xml.Name, 0, len(liveProps)+len(deadProps))
for pn, prop := range liveProps {
if prop.findFn != nil && (prop.dir || !isDir) {
pnames = append(pnames, pn)
}
}
return pnames, nil
}
// Allprop returns the properties defined for resource name and the properties
// named in include.
//
// Note that RFC 4918 defines 'allprop' to return the DAV: properties defined
// within the RFC plus dead properties. Other live properties should only be
// returned if they are named in 'include'.
//
// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
func allprop(ctx context.Context, fs *FileSystem, ls LockSystem, info FileInfo, include []xml.Name) ([]Propstat, error) {
pnames, err := propnames(ctx, fs, ls, info)
if err != nil {
return nil, err
}
// Add names from include if they are not already covered in pnames.
nameset := make(map[xml.Name]bool)
for _, pn := range pnames {
nameset[pn] = true
}
for _, pn := range include {
if !nameset[pn] {
pnames = append(pnames, pn)
}
}
return props(ctx, fs, ls, info, pnames)
}
// Patch patches the properties of resource name. The return values are
// constrained in the same manner as DeadPropsHolder.Patch.
func patch(ctx context.Context, fs *FileSystem, ls LockSystem, name string, patches []Proppatch) ([]Propstat, error) {
conflict := false
loop:
for _, patch := range patches {
for _, p := range patch.Props {
if _, ok := liveProps[p.XMLName]; ok {
conflict = true
break loop
}
}
}
if conflict {
pstatForbidden := Propstat{
Status: http.StatusForbidden,
XMLError: `<D:cannot-modify-protected-property xmlns:D="DAV:"/>`,
}
pstatFailedDep := Propstat{
Status: StatusFailedDependency,
}
for _, patch := range patches {
for _, p := range patch.Props {
if _, ok := liveProps[p.XMLName]; ok {
pstatForbidden.Props = append(pstatForbidden.Props, Property{XMLName: p.XMLName})
} else {
pstatFailedDep.Props = append(pstatFailedDep.Props, Property{XMLName: p.XMLName})
}
}
}
return makePropstats(pstatForbidden, pstatFailedDep), nil
}
// The file doesn't implement the optional DeadPropsHolder interface, so
// all patches are forbidden.
pstat := Propstat{Status: http.StatusOK}
for _, patch := range patches {
for _, p := range patch.Props {
pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})
}
}
return []Propstat{pstat}, nil
}
func escapeXML(s string) string {
for i := 0; i < len(s); i++ {
// As an optimization, if s contains only ASCII letters, digits or a
// few special characters, the escaped value is s itself and we don't
// need to allocate a buffer and convert between string and []byte.
switch c := s[i]; {
case c == ' ' || c == '_' ||
('+' <= c && c <= '9') || // Digits as well as + , - . and /
('A' <= c && c <= 'Z') ||
('a' <= c && c <= 'z'):
continue
}
// Otherwise, go through the full escaping process.
var buf bytes.Buffer
xml.EscapeText(&buf, []byte(s))
return buf.String()
}
return s
}
func findResourceType(ctx context.Context, fs *FileSystem, ls LockSystem, name string, fi FileInfo) (string, error) {
if fi.IsDir() {
return `<D:collection xmlns:D="DAV:"/>`, nil
}
return "", nil
}
func findDisplayName(ctx context.Context, fs *FileSystem, ls LockSystem, name string, fi FileInfo) (string, error) {
if slashClean(name) == "/" {
// Hide the real name of a possibly prefixed root directory.
return "", nil
}
return escapeXML(fi.GetName()), nil
}
func findContentLength(ctx context.Context, fs *FileSystem, ls LockSystem, name string, fi FileInfo) (string, error) {
return strconv.FormatUint(fi.GetSize(), 10), nil
}
func findLastModified(ctx context.Context, fs *FileSystem, ls LockSystem, name string, fi FileInfo) (string, error) {
return fi.ModTime().UTC().Format(http.TimeFormat), nil
}
// ErrNotImplemented should be returned by optional interfaces if they
// want the original implementation to be used.
var ErrNotImplemented = errors.New("not implemented")
// ContentTyper is an optional interface for the os.FileInfo
// objects returned by the FileSystem.
//
// If this interface is defined then it will be used to read the
// content type from the object.
//
// If this interface is not defined the file will be opened and the
// content type will be guessed from the initial contents of the file.
type ContentTyper interface {
// ContentType returns the content type for the file.
//
// If this returns error ErrNotImplemented then the error will
// be ignored and the base implementation will be used
// instead.
ContentType(ctx context.Context) (string, error)
}
func findContentType(ctx context.Context, fs *FileSystem, ls LockSystem, name string, fi FileInfo) (string, error) {
//if do, ok := fi.(ContentTyper); ok {
// ctype, err := do.ContentType(ctx)
// if err != ErrNotImplemented {
// return ctype, err
// }
//}
//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
//if err != nil {
// return "", err
//}
//defer f.Close()
//// This implementation is based on serveContent's code in the standard net/http package.
//ctype := mime.TypeByExtension(filepath.Ext(name))
//if ctype != "" {
// return ctype, nil
//}
//// Read a chunk to decide between utf-8 text and binary.
//var buf [512]byte
//n, err := io.ReadFull(f, buf[:])
//if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
// return "", err
//}
//ctype = http.DetectContentType(buf[:n])
//// Rewind file.
//_, err = f.Seek(0, os.SEEK_SET)
//return ctype, err
return "", nil
}
// ETager is an optional interface for the os.FileInfo objects
// returned by the FileSystem.
//
// If this interface is defined then it will be used to read the ETag
// for the object.
//
// If this interface is not defined an ETag will be computed using the
// ModTime() and the Size() methods of the os.FileInfo object.
type ETager interface {
// ETag returns an ETag for the file. This should be of the
// form "value" or W/"value"
//
// If this returns error ErrNotImplemented then the error will
// be ignored and the base implementation will be used
// instead.
ETag(ctx context.Context) (string, error)
}
func findETag(ctx context.Context, fs *FileSystem, ls LockSystem, reqPath string, fi FileInfo) (string, error) {
return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.GetSize()), nil
}
func findSupportedLock(ctx context.Context, fs *FileSystem, ls LockSystem, name string, fi FileInfo) (string, error) {
return `` +
`<D:lockentry xmlns:D="DAV:">` +
`<D:lockscope><D:exclusive/></D:lockscope>` +
`<D:locktype><D:write/></D:locktype>` +
`</D:lockentry>`, nil
}

725
server/webdav/webdav.go Normal file
View File

@ -0,0 +1,725 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package webdav provides a WebDAV server implementation.
package webdav // import "golang.org/x/net/webdav"
import (
"context"
"errors"
"fmt"
"github.com/Xhofe/alist/utils"
log "github.com/sirupsen/logrus"
"net/http"
"net/url"
"path"
"strings"
"time"
)
type Handler struct {
// Prefix is the URL path prefix to strip from WebDAV resource paths.
Prefix string
// LockSystem is the lock management system.
LockSystem LockSystem
// Logger is an optional error logger. If non-nil, it will be called
// for all HTTP requests.
Logger func(*http.Request, error)
}
func (h *Handler) stripPrefix(p string) (string, int, error) {
if h.Prefix == "" {
return p, http.StatusOK, nil
}
prefix := h.Prefix
if r := strings.TrimPrefix(p, prefix); len(r) < len(p) {
if len(r) == 0 {
r = "/"
}
return utils.RemoveLastSlash(r), http.StatusOK, nil
}
return p, http.StatusNotFound, errPrefixMismatch
}
// isPathExist 路径是否存在
func isPathExist(ctx context.Context, fs *FileSystem, path string) (bool, FileInfo) {
file, err := fs.File(path)
if err != nil {
log.Debug(err)
return false, nil
}
return true, file
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, fs *FileSystem) {
status, err := http.StatusBadRequest, errUnsupportedMethod
if h.LockSystem == nil {
status, err = http.StatusInternalServerError, errNoLockSystem
} else {
switch r.Method {
case "OPTIONS":
status, err = h.handleOptions(w, r, fs)
case "GET", "HEAD", "POST":
status, err = h.handleGetHeadPost(w, r, fs)
case "DELETE":
status, err = h.handleDelete(w, r, fs)
case "PUT":
status, err = h.handlePut(w, r, fs)
case "MKCOL":
status, err = h.handleMkcol(w, r, fs)
case "COPY", "MOVE":
status, err = h.handleCopyMove(w, r, fs)
case "LOCK":
status, err = h.handleLock(w, r, fs)
case "UNLOCK":
status, err = h.handleUnlock(w, r, fs)
case "PROPFIND":
status, err = h.handlePropfind(w, r, fs)
case "PROPPATCH":
status, err = h.handleProppatch(w, r, fs)
}
}
if err != nil {
log.Error(err)
}
if status != 0 {
w.WriteHeader(status)
if status != http.StatusNoContent {
_, _ = w.Write([]byte(StatusText(status)))
}
}
if h.Logger != nil {
h.Logger(r, err)
}
}
// OK
func (h *Handler) lock(now time.Time, root string, fs *FileSystem) (token string, status int, err error) {
token, err = h.LockSystem.Create(now, LockDetails{
Root: root,
Duration: infiniteTimeout,
ZeroDepth: true,
})
if err != nil {
if err == ErrLocked {
return "", StatusLocked, err
}
return "", http.StatusInternalServerError, err
}
return token, 0, nil
}
// ok
func (h *Handler) confirmLocks(r *http.Request, src, dst string, fs *FileSystem) (release func(), status int, err error) {
hdr := r.Header.Get("If")
if hdr == "" {
// An empty If header means that the client hasn't previously created locks.
// Even if this client doesn't care about locks, we still need to check that
// the resources aren't locked by another client, so we create temporary
// locks that would conflict with another client's locks. These temporary
// locks are unlocked at the end of the HTTP request.
now, srcToken, dstToken := time.Now(), "", ""
if src != "" {
srcToken, status, err = h.lock(now, src, fs)
if err != nil {
return nil, status, err
}
}
if dst != "" {
dstToken, status, err = h.lock(now, dst, fs)
if err != nil {
if srcToken != "" {
h.LockSystem.Unlock(now, srcToken)
}
return nil, status, err
}
}
return func() {
if dstToken != "" {
h.LockSystem.Unlock(now, dstToken)
}
if srcToken != "" {
h.LockSystem.Unlock(now, srcToken)
}
}, 0, nil
}
ih, ok := parseIfHeader(hdr)
if !ok {
return nil, http.StatusBadRequest, errInvalidIfHeader
}
// ih is a disjunction (OR) of ifLists, so any ifList will do.
for _, l := range ih.lists {
lsrc := l.resourceTag
if lsrc == "" {
lsrc = src
} else {
u, err := url.Parse(lsrc)
if err != nil {
continue
}
//if u.Host != r.Host {
// continue
//}
lsrc, status, err = h.stripPrefix(u.Path)
if err != nil {
return nil, status, err
}
}
release, err = h.LockSystem.Confirm(
time.Now(),
lsrc,
dst,
l.conditions...,
)
if err == ErrConfirmationFailed {
continue
}
if err != nil {
return nil, http.StatusInternalServerError, err
}
return release, 0, nil
}
// Section 10.4.1 says that "If this header is evaluated and all state lists
// fail, then the request must fail with a 412 (Precondition Failed) status."
// We follow the spec even though the cond_put_corrupt_token test case from
// the litmus test warns on seeing a 412 instead of a 423 (Locked).
return nil, http.StatusPreconditionFailed, ErrLocked
}
//OK
func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request, fs *FileSystem) (status int, err error) {
reqPath, status, err := h.stripPrefix(r.URL.Path)
if err != nil {
return status, err
}
ctx := r.Context()
allow := "OPTIONS, LOCK, PUT, MKCOL"
if exist, fi := isPathExist(ctx, fs, reqPath); exist {
log.Debugf("fi: %+v", fi)
if fi.IsDir() {
allow = "OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND"
} else {
allow = "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT"
}
}
w.Header().Set("Allow", allow)
// http://www.webdav.org/specs/rfc4918.html#dav.compliance.classes
w.Header().Set("DAV", "1, 2")
// http://msdn.microsoft.com/en-au/library/cc250217.aspx
w.Header().Set("MS-Author-Via", "DAV")
return 0, nil
}
// OK
func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request, fs *FileSystem) (status int, err error) {
reqPath, status, err := h.stripPrefix(r.URL.Path)
if err != nil {
return status, err
}
ctx := r.Context()
if reqPath == "/" {
_, err = w.Write([]byte("Please connect using software that supports WebDAV instead of a browser.\n"))
return http.StatusMethodNotAllowed, err
}
exist, file := isPathExist(ctx, fs, reqPath)
if !exist {
return http.StatusNotFound, nil
}
etag, err := findETag(ctx, fs, h.LockSystem, reqPath, file)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("ETag", etag)
log.Debugf("url: %+v", r.URL)
link, err := fs.Link(r, reqPath)
if err != nil {
return http.StatusInternalServerError, err
}
http.Redirect(w, r, link, 302)
return 0, nil
}
// OK
func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request, fs *FileSystem) (status int, err error) {
reqPath, status, err := h.stripPrefix(r.URL.Path)
if err != nil {
return status, err
}
release, status, err := h.confirmLocks(r, reqPath, "", fs)
if err != nil {
return status, err
}
defer release()
err = fs.Delete(reqPath)
if err != nil {
return http.StatusMethodNotAllowed, err
}
return http.StatusNoContent, nil
}
// OK
func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request, fs *FileSystem) (status int, err error) {
reqPath, status, err := h.stripPrefix(r.URL.Path)
if err != nil {
return status, err
}
release, status, err := h.confirmLocks(r, reqPath, "", fs)
if err != nil {
return status, err
}
defer release()
// TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz'
// comments in http.checkEtag.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = fs.Upload(ctx, r, reqPath)
if err != nil {
return http.StatusMethodNotAllowed, err
}
_, fi := isPathExist(ctx, fs, reqPath)
etag, err := findETag(ctx, fs, h.LockSystem, reqPath, fi)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("ETag", etag)
return http.StatusCreated, nil
}
// OK
func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request, fs *FileSystem) (status int, err error) {
reqPath, status, err := h.stripPrefix(r.URL.Path)
if err != nil {
return status, err
}
release, status, err := h.confirmLocks(r, reqPath, "", fs)
if err != nil {
return status, err
}
defer release()
ctx := r.Context()
if r.ContentLength > 0 {
return http.StatusUnsupportedMediaType, nil
}
if strings.Contains(r.UserAgent(), "rclone") {
//if _, ok := ctx.Value(fsctx.IgnoreDirectoryConflictCtx).(bool); !ok {
// ctx = context.WithValue(ctx, fsctx.IgnoreDirectoryConflictCtx, true)
//}
}
if err := fs.CreateDirectory(ctx, reqPath); err != nil {
return http.StatusConflict, err
}
return http.StatusCreated, nil
}
// OK
func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request, fs *FileSystem) (status int, err error) {
hdr := r.Header.Get("Destination")
if hdr == "" {
return http.StatusBadRequest, errInvalidDestination
}
u, err := url.Parse(hdr)
if err != nil {
return http.StatusBadRequest, errInvalidDestination
}
//if u.Host != "" && u.Host != r.Host {
// return http.StatusBadGateway, errInvalidDestination
//}
src, status, err := h.stripPrefix(r.URL.Path)
if err != nil {
return status, err
}
dst, status, err := h.stripPrefix(u.Path)
if err != nil {
return status, err
}
if dst == "" {
return http.StatusBadGateway, errInvalidDestination
}
if dst == src {
return http.StatusForbidden, errDestinationEqualsSource
}
ctx := r.Context()
isExist, _ := isPathExist(ctx, fs, src)
if !isExist {
return http.StatusNotFound, nil
}
if r.Method == "COPY" {
// Section 7.5.1 says that a COPY only needs to lock the destination,
// not both destination and source. Strictly speaking, this is racy,
// even though a COPY doesn't modify the source, if a concurrent
// operation modifies the source. However, the litmus test explicitly
// checks that COPYing a locked-by-another source is OK.
release, status, err := h.confirmLocks(r, "", dst, fs)
if err != nil {
return status, err
}
defer release()
// Section 9.8.3 says that "The COPY method on a collection without a Depth
// header must act as if a Depth header with value "infinity" was included".
depth := infiniteDepth
if hdr := r.Header.Get("Depth"); hdr != "" {
depth = parseDepth(hdr)
if depth != 0 && depth != infiniteDepth {
// Section 9.8.3 says that "A client may submit a Depth header on a
// COPY on a collection with a value of "0" or "infinity"."
return http.StatusBadRequest, errInvalidDepth
}
}
return copyFiles(ctx, fs, src, dst, r.Header.Get("Overwrite") != "F", depth, 0)
}
// windows下某些情况下网盘根目录下Office保存文件时附带的锁token只包含源文件
// 此处暂时去除了对dst锁的检查
release, status, err := h.confirmLocks(r, src, "", fs)
if err != nil {
return status, err
}
defer release()
// Section 9.9.2 says that "The MOVE method on a collection must act as if
// a "Depth: infinity" header was used on it. A client must not submit a
// Depth header on a MOVE on a collection with any value but "infinity"."
if hdr := r.Header.Get("Depth"); hdr != "" {
if parseDepth(hdr) != infiniteDepth {
return http.StatusBadRequest, errInvalidDepth
}
}
return moveFiles(ctx, fs, src, dst, r.Header.Get("Overwrite") == "T")
}
// OK
func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request, fs *FileSystem) (retStatus int, retErr error) {
duration, err := parseTimeout(r.Header.Get("Timeout"))
if err != nil {
return http.StatusBadRequest, err
}
li, status, err := readLockInfo(r.Body)
if err != nil {
return status, err
}
//ctx := r.Context()
token, ld, now, created := "", LockDetails{}, time.Now(), false
if li == (lockInfo{}) {
// An empty lockInfo means to refresh the lock.
ih, ok := parseIfHeader(r.Header.Get("If"))
if !ok {
return http.StatusBadRequest, errInvalidIfHeader
}
if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
token = ih.lists[0].conditions[0].Token
}
if token == "" {
return http.StatusBadRequest, errInvalidLockToken
}
ld, err = h.LockSystem.Refresh(now, token, duration)
if err != nil {
if err == ErrNoSuchLock {
return http.StatusPreconditionFailed, err
}
return http.StatusInternalServerError, err
}
} else {
// Section 9.10.3 says that "If no Depth header is submitted on a LOCK request,
// then the request MUST act as if a "Depth:infinity" had been submitted."
depth := infiniteDepth
if hdr := r.Header.Get("Depth"); hdr != "" {
depth = parseDepth(hdr)
if depth != 0 && depth != infiniteDepth {
// Section 9.10.3 says that "Values other than 0 or infinity must not be
// used with the Depth header on a LOCK method".
return http.StatusBadRequest, errInvalidDepth
}
}
reqPath, status, err := h.stripPrefix(r.URL.Path)
if err != nil {
return status, err
}
ld = LockDetails{
Root: reqPath,
Duration: duration,
OwnerXML: li.Owner.InnerXML,
ZeroDepth: depth == 0,
}
token, err = h.LockSystem.Create(now, ld)
if err != nil {
if err == ErrLocked {
return StatusLocked, err
}
return http.StatusInternalServerError, err
}
defer func() {
if retErr != nil {
h.LockSystem.Unlock(now, token)
}
}()
// Create the resource if it didn't previously exist.
//if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil {
// f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
// if err != nil {
// // TODO: detect missing intermediate dirs and return http.StatusConflict?
// return http.StatusInternalServerError, err
// }
// f.Close()
// created = true
//}
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
// Lock-Token value is a Coded-URL. We add angle brackets.
w.Header().Set("Lock-Token", "<"+token+">")
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
if created {
// This is "w.WriteHeader(http.StatusCreated)" and not "return
// http.StatusCreated, nil" because we write our own (XML) response to w
// and Handler.ServeHTTP would otherwise write "Created".
w.WriteHeader(http.StatusCreated)
}
writeLockInfo(w, token, ld)
return 0, nil
}
// OK
func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request, fs *FileSystem) (status int, err error) {
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
// Lock-Token value is a Coded-URL. We strip its angle brackets.
t := r.Header.Get("Lock-Token")
if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' {
return http.StatusBadRequest, errInvalidLockToken
}
t = t[1 : len(t)-1]
switch err = h.LockSystem.Unlock(time.Now(), t); err {
case nil:
return http.StatusNoContent, err
case ErrForbidden:
return http.StatusForbidden, err
case ErrLocked:
return StatusLocked, err
case ErrNoSuchLock:
return http.StatusConflict, err
default:
return http.StatusInternalServerError, err
}
}
// OK
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request, fs *FileSystem) (status int, err error) {
reqPath, status, err := h.stripPrefix(r.URL.Path)
if err != nil {
return status, err
}
ctx := r.Context()
ok, fi := isPathExist(ctx, fs, reqPath)
if !ok {
return http.StatusNotFound, err
}
depth := infiniteDepth
if hdr := r.Header.Get("Depth"); hdr != "" {
depth = parseDepth(hdr)
if depth == invalidDepth {
return http.StatusBadRequest, errInvalidDepth
}
}
pf, status, err := readPropfind(r.Body)
if err != nil {
return status, err
}
mw := multistatusWriter{w: w}
walkFn := func(reqPath string, info FileInfo, err error) error {
if err != nil {
return err
}
var pstats []Propstat
if pf.Propname != nil {
pnames, err := propnames(ctx, fs, h.LockSystem, info)
if err != nil {
return err
}
pstat := Propstat{Status: http.StatusOK}
for _, xmlname := range pnames {
pstat.Props = append(pstat.Props, Property{XMLName: xmlname})
}
pstats = append(pstats, pstat)
} else if pf.Allprop != nil {
pstats, err = allprop(ctx, fs, h.LockSystem, info, pf.Prop)
} else {
pstats, err = props(ctx, fs, h.LockSystem, info, pf.Prop)
}
if err != nil {
return err
}
href := path.Join(h.Prefix, reqPath)
if href != "/" && info.IsDir() {
href += "/"
}
return mw.write(makePropstatResponse(href, pstats))
}
walkErr := walkFS(ctx, fs, depth, reqPath, fi, walkFn)
closeErr := mw.close()
if walkErr != nil {
return http.StatusInternalServerError, walkErr
}
if closeErr != nil {
return http.StatusInternalServerError, closeErr
}
return 0, nil
}
func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request, fs *FileSystem) (status int, err error) {
reqPath, status, err := h.stripPrefix(r.URL.Path)
if err != nil {
return status, err
}
release, status, err := h.confirmLocks(r, reqPath, "", fs)
if err != nil {
return status, err
}
defer release()
ctx := r.Context()
if exist, _ := isPathExist(ctx, fs, reqPath); !exist {
return http.StatusNotFound, nil
}
patches, status, err := readProppatch(r.Body)
if err != nil {
return status, err
}
pstats, err := patch(ctx, fs, h.LockSystem, reqPath, patches)
if err != nil {
return http.StatusInternalServerError, err
}
mw := multistatusWriter{w: w}
writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats))
closeErr := mw.close()
if writeErr != nil {
return http.StatusInternalServerError, writeErr
}
if closeErr != nil {
return http.StatusInternalServerError, closeErr
}
return 0, nil
}
func makePropstatResponse(href string, pstats []Propstat) *response {
resp := response{
Href: []string{(&url.URL{Path: href}).EscapedPath()},
Propstat: make([]propstat, 0, len(pstats)),
}
for _, p := range pstats {
var xmlErr *xmlError
if p.XMLError != "" {
xmlErr = &xmlError{InnerXML: []byte(p.XMLError)}
}
resp.Propstat = append(resp.Propstat, propstat{
Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)),
Prop: p.Props,
ResponseDescription: p.ResponseDescription,
Error: xmlErr,
})
}
return &resp
}
const (
infiniteDepth = -1
invalidDepth = -2
)
// parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and
// infiniteDepth. Parsing any other string returns invalidDepth.
//
// Different WebDAV methods have further constraints on valid depths:
// - PROPFIND has no further restrictions, as per section 9.1.
// - COPY accepts only "0" or "infinity", as per section 9.8.3.
// - MOVE accepts only "infinity", as per section 9.9.2.
// - LOCK accepts only "0" or "infinity", as per section 9.10.3.
// These constraints are enforced by the handleXxx methods.
func parseDepth(s string) int {
switch s {
case "0":
return 0
case "1":
return 1
case "infinity":
return infiniteDepth
}
return invalidDepth
}
// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
const (
StatusMulti = 207
StatusUnprocessableEntity = 422
StatusLocked = 423
StatusFailedDependency = 424
StatusInsufficientStorage = 507
)
func StatusText(code int) string {
switch code {
case StatusMulti:
return "Multi-Status"
case StatusUnprocessableEntity:
return "Unprocessable Entity"
case StatusLocked:
return "Locked"
case StatusFailedDependency:
return "Failed Dependency"
case StatusInsufficientStorage:
return "Insufficient Storage"
}
return http.StatusText(code)
}
var (
errDestinationEqualsSource = errors.New("webdav: destination equals source")
errDirectoryNotEmpty = errors.New("webdav: directory not empty")
errInvalidDepth = errors.New("webdav: invalid depth")
errInvalidDestination = errors.New("webdav: invalid destination")
errInvalidIfHeader = errors.New("webdav: invalid If header")
errInvalidLockInfo = errors.New("webdav: invalid lock info")
errInvalidLockToken = errors.New("webdav: invalid lock token")
errInvalidPropfind = errors.New("webdav: invalid propfind")
errInvalidProppatch = errors.New("webdav: invalid proppatch")
errInvalidResponse = errors.New("webdav: invalid response")
errInvalidTimeout = errors.New("webdav: invalid timeout")
errNoFileSystem = errors.New("webdav: no file system")
errNoLockSystem = errors.New("webdav: no lock system")
errNotADirectory = errors.New("webdav: not a directory")
errPrefixMismatch = errors.New("webdav: prefix mismatch")
errRecursionTooDeep = errors.New("webdav: recursion too deep")
errUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
errUnsupportedMethod = errors.New("webdav: unsupported method")
)

519
server/webdav/xml.go Normal file
View File

@ -0,0 +1,519 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package webdav
// The XML encoding is covered by Section 14.
// http://www.webdav.org/specs/rfc4918.html#xml.element.definitions
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"net/http"
"time"
// As of https://go-review.googlesource.com/#/c/12772/ which was submitted
// in July 2015, this package uses an internal fork of the standard
// library's encoding/xml package, due to changes in the way namespaces
// were encoded. Such changes were introduced in the Go 1.5 cycle, but were
// rolled back in response to https://github.com/golang/go/issues/11841
//
// However, this package's exported API, specifically the Property and
// DeadPropsHolder types, need to refer to the standard library's version
// of the xml.Name type, as code that imports this package cannot refer to
// the internal version.
//
// This file therefore imports both the internal and external versions, as
// ixml and xml, and converts between them.
//
// In the long term, this package should use the standard library's version
// only, and the internal fork deleted, once
// https://github.com/golang/go/issues/13400 is resolved.
ixml "github.com/Xhofe/alist/server/webdav/internal/xml"
)
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo
type lockInfo struct {
XMLName ixml.Name `xml:"lockinfo"`
Exclusive *struct{} `xml:"lockscope>exclusive"`
Shared *struct{} `xml:"lockscope>shared"`
Write *struct{} `xml:"locktype>write"`
Owner owner `xml:"owner"`
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner
type owner struct {
InnerXML string `xml:",innerxml"`
}
func readLockInfo(r io.Reader) (li lockInfo, status int, err error) {
c := &countingReader{r: r}
if err = ixml.NewDecoder(c).Decode(&li); err != nil {
if err == io.EOF {
if c.n == 0 {
// An empty body means to refresh the lock.
// http://www.webdav.org/specs/rfc4918.html#refreshing-locks
return lockInfo{}, 0, nil
}
err = errInvalidLockInfo
}
return lockInfo{}, http.StatusBadRequest, err
}
// We only support exclusive (non-shared) write locks. In practice, these are
// the only types of locks that seem to matter.
if li.Exclusive == nil || li.Shared != nil || li.Write == nil {
return lockInfo{}, http.StatusNotImplemented, errUnsupportedLockInfo
}
return li, 0, nil
}
type countingReader struct {
n int
r io.Reader
}
func (c *countingReader) Read(p []byte) (int, error) {
n, err := c.r.Read(p)
c.n += n
return n, err
}
func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) {
depth := "infinity"
if ld.ZeroDepth {
depth = "0"
}
timeout := ld.Duration / time.Second
return fmt.Fprintf(w, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<D:prop xmlns:D=\"DAV:\"><D:lockdiscovery><D:activelock>\n"+
" <D:locktype><D:write/></D:locktype>\n"+
" <D:lockscope><D:exclusive/></D:lockscope>\n"+
" <D:depth>%s</D:depth>\n"+
" <D:owner>%s</D:owner>\n"+
" <D:timeout>Second-%d</D:timeout>\n"+
" <D:locktoken><D:href>%s</D:href></D:locktoken>\n"+
" <D:lockroot><D:href>%s</D:href></D:lockroot>\n"+
"</D:activelock></D:lockdiscovery></D:prop>",
depth, ld.OwnerXML, timeout, escape(token), escape(ld.Root),
)
}
func escape(s string) string {
for i := 0; i < len(s); i++ {
switch s[i] {
case '"', '&', '\'', '<', '>':
b := bytes.NewBuffer(nil)
ixml.EscapeText(b, []byte(s))
return b.String()
}
}
return s
}
// Next returns the next token, if any, in the XML stream of d.
// RFC 4918 requires to ignore comments, processing instructions
// and directives.
// http://www.webdav.org/specs/rfc4918.html#property_values
// http://www.webdav.org/specs/rfc4918.html#xml-extensibility
func next(d *ixml.Decoder) (ixml.Token, error) {
for {
t, err := d.Token()
if err != nil {
return t, err
}
switch t.(type) {
case ixml.Comment, ixml.Directive, ixml.ProcInst:
continue
default:
return t, nil
}
}
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind)
type propfindProps []xml.Name
// UnmarshalXML appends the property names enclosed within start to pn.
//
// It returns an error if start does not contain any properties or if
// properties contain values. Character data between properties is ignored.
func (pn *propfindProps) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error {
for {
t, err := next(d)
if err != nil {
return err
}
switch t.(type) {
case ixml.EndElement:
if len(*pn) == 0 {
return fmt.Errorf("%s must not be empty", start.Name.Local)
}
return nil
case ixml.StartElement:
name := t.(ixml.StartElement).Name
t, err = next(d)
if err != nil {
return err
}
if _, ok := t.(ixml.EndElement); !ok {
return fmt.Errorf("unexpected token %T", t)
}
*pn = append(*pn, xml.Name(name))
}
}
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind
type propfind struct {
XMLName ixml.Name `xml:"DAV: propfind"`
Allprop *struct{} `xml:"DAV: allprop"`
Propname *struct{} `xml:"DAV: propname"`
Prop propfindProps `xml:"DAV: prop"`
Include propfindProps `xml:"DAV: include"`
}
func readPropfind(r io.Reader) (pf propfind, status int, err error) {
c := countingReader{r: r}
if err = ixml.NewDecoder(&c).Decode(&pf); err != nil {
if err == io.EOF {
if c.n == 0 {
// An empty body means to propfind allprop.
// http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
return propfind{Allprop: new(struct{})}, 0, nil
}
err = errInvalidPropfind
}
return propfind{}, http.StatusBadRequest, err
}
if pf.Allprop == nil && pf.Include != nil {
return propfind{}, http.StatusBadRequest, errInvalidPropfind
}
if pf.Allprop != nil && (pf.Prop != nil || pf.Propname != nil) {
return propfind{}, http.StatusBadRequest, errInvalidPropfind
}
if pf.Prop != nil && pf.Propname != nil {
return propfind{}, http.StatusBadRequest, errInvalidPropfind
}
if pf.Propname == nil && pf.Allprop == nil && pf.Prop == nil {
return propfind{}, http.StatusBadRequest, errInvalidPropfind
}
return pf, 0, nil
}
// Property represents a single DAV resource property as defined in RFC 4918.
// See http://www.webdav.org/specs/rfc4918.html#data.model.for.resource.properties
type Property struct {
// XMLName is the fully qualified name that identifies this property.
XMLName xml.Name
// Lang is an optional xml:lang attribute.
Lang string `xml:"xml:lang,attr,omitempty"`
// InnerXML contains the XML representation of the property value.
// See http://www.webdav.org/specs/rfc4918.html#property_values
//
// Property values of complex type or mixed-content must have fully
// expanded XML namespaces or be self-contained with according
// XML namespace declarations. They must not rely on any XML
// namespace declarations within the scope of the XML document,
// even including the DAV: namespace.
InnerXML []byte `xml:",innerxml"`
}
// ixmlProperty is the same as the Property type except it holds an ixml.Name
// instead of an xml.Name.
type ixmlProperty struct {
XMLName ixml.Name
Lang string `xml:"xml:lang,attr,omitempty"`
InnerXML []byte `xml:",innerxml"`
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
// See multistatusWriter for the "D:" namespace prefix.
type xmlError struct {
XMLName ixml.Name `xml:"D:error"`
InnerXML []byte `xml:",innerxml"`
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
// See multistatusWriter for the "D:" namespace prefix.
type propstat struct {
Prop []Property `xml:"D:prop>_ignored_"`
Status string `xml:"D:status"`
Error *xmlError `xml:"D:error"`
ResponseDescription string `xml:"D:responsedescription,omitempty"`
}
// ixmlPropstat is the same as the propstat type except it holds an ixml.Name
// instead of an xml.Name.
type ixmlPropstat struct {
Prop []ixmlProperty `xml:"D:prop>_ignored_"`
Status string `xml:"D:status"`
Error *xmlError `xml:"D:error"`
ResponseDescription string `xml:"D:responsedescription,omitempty"`
}
// MarshalXML prepends the "D:" namespace prefix on properties in the DAV: namespace
// before encoding. See multistatusWriter.
func (ps propstat) MarshalXML(e *ixml.Encoder, start ixml.StartElement) error {
// Convert from a propstat to an ixmlPropstat.
ixmlPs := ixmlPropstat{
Prop: make([]ixmlProperty, len(ps.Prop)),
Status: ps.Status,
Error: ps.Error,
ResponseDescription: ps.ResponseDescription,
}
for k, prop := range ps.Prop {
ixmlPs.Prop[k] = ixmlProperty{
XMLName: ixml.Name(prop.XMLName),
Lang: prop.Lang,
InnerXML: prop.InnerXML,
}
}
for k, prop := range ixmlPs.Prop {
if prop.XMLName.Space == "DAV:" {
prop.XMLName = ixml.Name{Space: "", Local: "D:" + prop.XMLName.Local}
ixmlPs.Prop[k] = prop
}
}
// Distinct type to avoid infinite recursion of MarshalXML.
type newpropstat ixmlPropstat
return e.EncodeElement(newpropstat(ixmlPs), start)
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_response
// See multistatusWriter for the "D:" namespace prefix.
type response struct {
XMLName ixml.Name `xml:"D:response"`
Href []string `xml:"D:href"`
Propstat []propstat `xml:"D:propstat"`
Status string `xml:"D:status,omitempty"`
Error *xmlError `xml:"D:error"`
ResponseDescription string `xml:"D:responsedescription,omitempty"`
}
// MultistatusWriter marshals one or more Responses into a XML
// multistatus response.
// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_multistatus
// TODO(rsto, mpl): As a workaround, the "D:" namespace prefix, defined as
// "DAV:" on this element, is prepended on the nested response, as well as on all
// its nested elements. All property names in the DAV: namespace are prefixed as
// well. This is because some versions of Mini-Redirector (on windows 7) ignore
// elements with a default namespace (no prefixed namespace). A less intrusive fix
// should be possible after golang.org/cl/11074. See https://golang.org/issue/11177
type multistatusWriter struct {
// ResponseDescription contains the optional responsedescription
// of the multistatus XML element. Only the latest content before
// close will be emitted. Empty response descriptions are not
// written.
responseDescription string
w http.ResponseWriter
enc *ixml.Encoder
}
// Write validates and emits a DAV response as part of a multistatus response
// element.
//
// It sets the HTTP status code of its underlying http.ResponseWriter to 207
// (Multi-Status) and populates the Content-Type header. If r is the
// first, valid response to be written, Write prepends the XML representation
// of r with a multistatus tag. Callers must call close after the last response
// has been written.
func (w *multistatusWriter) write(r *response) error {
switch len(r.Href) {
case 0:
return errInvalidResponse
case 1:
if len(r.Propstat) > 0 != (r.Status == "") {
return errInvalidResponse
}
default:
if len(r.Propstat) > 0 || r.Status == "" {
return errInvalidResponse
}
}
err := w.writeHeader()
if err != nil {
return err
}
return w.enc.Encode(r)
}
// writeHeader writes a XML multistatus start element on w's underlying
// http.ResponseWriter and returns the result of the write operation.
// After the first write attempt, writeHeader becomes a no-op.
func (w *multistatusWriter) writeHeader() error {
if w.enc != nil {
return nil
}
w.w.Header().Add("Content-Type", "text/xml; charset=utf-8")
w.w.WriteHeader(StatusMulti)
_, err := fmt.Fprintf(w.w, `<?xml version="1.0" encoding="UTF-8"?>`)
if err != nil {
return err
}
w.enc = ixml.NewEncoder(w.w)
return w.enc.EncodeToken(ixml.StartElement{
Name: ixml.Name{
Space: "DAV:",
Local: "multistatus",
},
Attr: []ixml.Attr{{
Name: ixml.Name{Space: "xmlns", Local: "D"},
Value: "DAV:",
}},
})
}
// Close completes the marshalling of the multistatus response. It returns
// an error if the multistatus response could not be completed. If both the
// return value and field enc of w are nil, then no multistatus response has
// been written.
func (w *multistatusWriter) close() error {
if w.enc == nil {
return nil
}
var end []ixml.Token
if w.responseDescription != "" {
name := ixml.Name{Space: "DAV:", Local: "responsedescription"}
end = append(end,
ixml.StartElement{Name: name},
ixml.CharData(w.responseDescription),
ixml.EndElement{Name: name},
)
}
end = append(end, ixml.EndElement{
Name: ixml.Name{Space: "DAV:", Local: "multistatus"},
})
for _, t := range end {
err := w.enc.EncodeToken(t)
if err != nil {
return err
}
}
return w.enc.Flush()
}
var xmlLangName = ixml.Name{Space: "http://www.w3.org/XML/1998/namespace", Local: "lang"}
func xmlLang(s ixml.StartElement, d string) string {
for _, attr := range s.Attr {
if attr.Name == xmlLangName {
return attr.Value
}
}
return d
}
type xmlValue []byte
func (v *xmlValue) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error {
// The XML value of a property can be arbitrary, mixed-content XML.
// To make sure that the unmarshalled value contains all required
// namespaces, we encode all the property value XML tokens into a
// buffer. This forces the encoder to redeclare any used namespaces.
var b bytes.Buffer
e := ixml.NewEncoder(&b)
for {
t, err := next(d)
if err != nil {
return err
}
if e, ok := t.(ixml.EndElement); ok && e.Name == start.Name {
break
}
if err = e.EncodeToken(t); err != nil {
return err
}
}
err := e.Flush()
if err != nil {
return err
}
*v = b.Bytes()
return nil
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for proppatch)
type proppatchProps []Property
// UnmarshalXML appends the property names and values enclosed within start
// to ps.
//
// An xml:lang attribute that is defined either on the DAV:prop or property
// name XML element is propagated to the property's Lang field.
//
// UnmarshalXML returns an error if start does not contain any properties or if
// property values contain syntactically incorrect XML.
func (ps *proppatchProps) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error {
lang := xmlLang(start, "")
for {
t, err := next(d)
if err != nil {
return err
}
switch elem := t.(type) {
case ixml.EndElement:
if len(*ps) == 0 {
return fmt.Errorf("%s must not be empty", start.Name.Local)
}
return nil
case ixml.StartElement:
p := Property{
XMLName: xml.Name(t.(ixml.StartElement).Name),
Lang: xmlLang(t.(ixml.StartElement), lang),
}
err = d.DecodeElement(((*xmlValue)(&p.InnerXML)), &elem)
if err != nil {
return err
}
*ps = append(*ps, p)
}
}
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_set
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_remove
type setRemove struct {
XMLName ixml.Name
Lang string `xml:"xml:lang,attr,omitempty"`
Prop proppatchProps `xml:"DAV: prop"`
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propertyupdate
type propertyupdate struct {
XMLName ixml.Name `xml:"DAV: propertyupdate"`
Lang string `xml:"xml:lang,attr,omitempty"`
SetRemove []setRemove `xml:",any"`
}
func readProppatch(r io.Reader) (patches []Proppatch, status int, err error) {
var pu propertyupdate
if err = ixml.NewDecoder(r).Decode(&pu); err != nil {
return nil, http.StatusBadRequest, err
}
for _, op := range pu.SetRemove {
remove := false
switch op.XMLName {
case ixml.Name{Space: "DAV:", Local: "set"}:
// No-op.
case ixml.Name{Space: "DAV:", Local: "remove"}:
for _, p := range op.Prop {
if len(p.InnerXML) > 0 {
return nil, http.StatusBadRequest, errInvalidProppatch
}
}
remove = true
default:
return nil, http.StatusBadRequest, errInvalidProppatch
}
patches = append(patches, Proppatch{Remove: remove, Props: op.Prop})
}
return patches, 0, nil
}

59
utils/code.go Normal file
View File

@ -0,0 +1,59 @@
package utils
import (
"bytes"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"io/ioutil"
"unicode/utf8"
)
func IsGBK(data []byte) bool {
length := len(data)
var i = 0
for i < length {
if data[i] <= 0x7f {
//编码0~127,只有一个字节的编码兼容ASCII码
i++
continue
} else {
//大于127的使用双字节编码落在gbk编码范围内的字符
if data[i] >= 0x81 &&
data[i] <= 0xfe &&
data[i+1] >= 0x40 &&
data[i+1] <= 0xfe &&
data[i+1] != 0xf7 {
i += 2
continue
} else {
return false
}
}
}
return true
}
const (
GBK string = "GBK"
UTF8 string = "UTF8"
UNKNOWN string = "UNKNOWN"
)
func GetStrCoding(data []byte) string {
if utf8.Valid(data) {
return UTF8
} else if IsGBK(data) {
return GBK
} else {
return UNKNOWN
}
}
func GbkToUtf8(s []byte) ([]byte, error) {
reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewDecoder())
d, e := ioutil.ReadAll(reader)
if e != nil {
return nil, e
}
return d, nil
}

View File

@ -88,3 +88,37 @@ func ParsePath(path string) string {
}
return path
}
func RemoveLastSlash(path string) string {
if len(path) > 1 {
return strings.TrimSuffix(path, "/")
}
return path
}
func Dir(path string) string {
idx := strings.LastIndex(path, "/")
if idx == 0 {
return "/"
}
if idx == -1 {
return path
}
return path[:idx]
}
func Base(path string) string {
idx := strings.LastIndex(path, "/")
if idx == -1 {
return path
}
return path[idx+1:]
}
func Join(elem ...string) string {
res := filepath.Join(elem...)
if res == "\\" {
res = "/"
}
return res
}

View File

@ -3,6 +3,7 @@ package utils
import (
"crypto/md5"
"encoding/hex"
"fmt"
)
// GetMD5Encode
@ -16,3 +17,11 @@ func GetMD5Encode(data string) string {
func Get16MD5Encode(data string) string {
return GetMD5Encode(data)[8:24]
}
func SignWithPassword(name, password string) string {
return Get16MD5Encode(fmt.Sprintf("alist-%s-%s", password, name))
}
func SignWithToken(name, token string) string {
return Get16MD5Encode(fmt.Sprintf("alist-%s-%s", token, name))
}