mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-11-25 19:37:41 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
532a326ad6 | ||
|
|
917ad07ab7 | ||
|
|
50fd7de045 | ||
|
|
f6527f1c4c | ||
|
|
a3ef3d1416 | ||
|
|
89b05021f8 | ||
|
|
7e40acad3f |
98
.all-contributorsrc
Normal file
98
.all-contributorsrc
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"files": [
|
||||
"CONTRIBUTORS.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "Xhofe",
|
||||
"name": "Xhofe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/36558727?v=4",
|
||||
"profile": "http://nn.ci",
|
||||
"contributions": [
|
||||
"code",
|
||||
"ideas",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "foxxorcat",
|
||||
"name": "foxxorcat",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/95907542?v=4",
|
||||
"profile": "https://github.com/foxxorcat",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "DaoChen6",
|
||||
"name": "道辰",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/63903027?v=4",
|
||||
"profile": "https://www.iflu.cf/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "vg-land",
|
||||
"name": "vg-land",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16739728?v=4",
|
||||
"profile": "https://vg-land.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Clansty",
|
||||
"name": "凌莞~(=^▽^=)",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18461360?v=4",
|
||||
"profile": "https://c5y.moe",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Windman1320",
|
||||
"name": "Windman",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9999486?v=4",
|
||||
"profile": "https://github.com/Windman1320",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ericarena",
|
||||
"name": "ericarena",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4518927?v=4",
|
||||
"profile": "https://github.com/ericarena",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "WntFlm",
|
||||
"name": "WntFlm",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/34620278?v=4",
|
||||
"profile": "https://github.com/WntFlm",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "XZB-1248",
|
||||
"name": "XZB-1248",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/28593573?v=4",
|
||||
"profile": "https://github.com/XZB-1248",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "alist",
|
||||
"projectOwner": "alist-org",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true
|
||||
}
|
||||
48
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
48
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: "Bug report"
|
||||
description: Bug report
|
||||
labels: [pending triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report, please **confirm that your issue is not a duplicate issue and not because of your operation or version issues**
|
||||
感谢您花时间填写此错误报告,请**务必确认您的issue不是重复的且不是因为您的操作或版本问题**
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please make sure of the following things
|
||||
description: You may select more than one, even select all.
|
||||
options:
|
||||
- label: I have read the [documentation](https://alist-doc.nn.ci).
|
||||
- label: I'm sure there are no duplicate issues or discussions.
|
||||
- label: I'm sure it's due to `alist` and not something else(such as `Dependencies` or `Operational`).
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
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: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs / 日志
|
||||
description: |
|
||||
Please copy and paste any relevant log output.
|
||||
请复制粘贴错误日志,或者截图
|
||||
render: shell
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/Xhofe/alist/discussions
|
||||
about: Use GitHub discussions for message-board style questions and discussions.
|
||||
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "Feature request"
|
||||
description: Feature request
|
||||
labels: ["enhancement: pending triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please make sure of the following things
|
||||
description: You may select more than one, even select all.
|
||||
options:
|
||||
- label: I have read the [documentation](https://alist-doc.nn.ci).
|
||||
- label: I'm sure there are no duplicate issues or discussions.
|
||||
- label: I'm sure this feature is not implemented.
|
||||
- label: I'm sure it's a reasonable and popular requirement.
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Description of the feature / 需求描述
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: suggested-solution
|
||||
attributes:
|
||||
label: Suggested solution / 实现思路
|
||||
description: |
|
||||
Solutions to achieve this requirement.
|
||||
实现此需求的解决思路。
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context / 附件
|
||||
description: |
|
||||
Any other context or screenshots about the feature request here, or information you find helpful.
|
||||
相关的任何其他上下文或截图,或者你觉得有帮助的信息
|
||||
21
.github/config.yml
vendored
Normal file
21
.github/config.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Configuration for welcome - https://github.com/behaviorbot/welcome
|
||||
|
||||
# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
|
||||
|
||||
# Comment to be posted to on first time issues
|
||||
newIssueWelcomeComment: >
|
||||
Thanks for opening your first issue here! Be sure to follow the issue template!
|
||||
|
||||
# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
|
||||
|
||||
# Comment to be posted to on PRs from first time contributors in your repository
|
||||
newPRWelcomeComment: >
|
||||
Thanks for opening this pull request! Please check out our contributing guidelines.
|
||||
|
||||
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
|
||||
|
||||
# Comment to be posted to on pull requests merged by a first time user
|
||||
firstPRMergeComment: >
|
||||
Congrats on merging your first pull request! We here at behavior bot are proud of you!
|
||||
|
||||
# It is recommend to include as many gifs and emojis as possible
|
||||
50
.github/workflows/build.yml
vendored
Normal file
50
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ '**' ]
|
||||
pull_request:
|
||||
branches: [ '**' ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ubuntu-latest]
|
||||
go-version: [1.18]
|
||||
name: Build
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: alist
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
docker pull techknowlogick/xgo:latest
|
||||
go install src.techknowlogick.com/xgo@latest
|
||||
sudo apt install upx
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
mv alist/build.sh .
|
||||
bash build.sh web
|
||||
mv dist/* alist/public
|
||||
bash build.sh build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: artifact
|
||||
path: alist/build
|
||||
44
.github/workflows/build_docker.yml
vendored
Normal file
44
.github/workflows/build_docker.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: build_docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ v2 ]
|
||||
|
||||
jobs:
|
||||
build_docker:
|
||||
name: Docker
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: xhofe/alist
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Build web
|
||||
run: |
|
||||
bash build.sh web
|
||||
mv dist/* public
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: xhofe
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
||||
20
.github/workflows/issue_check_question.yml
vendored
Normal file
20
.github/workflows/issue_check_question.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Check need info
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 */7 * *"
|
||||
|
||||
jobs:
|
||||
check-need-info:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: close-issues
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'close-issues'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
labels: 'question'
|
||||
inactive-day: 7
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}, this issue was closed due to no activities in 7 days.
|
||||
你好 @${{ github.event.issue.user.login }},此issue因超过7天未回复被关闭。
|
||||
25
.github/workflows/issue_duplicate.yml
vendored
Normal file
25
.github/workflows/issue_duplicate.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Issue Duplicate
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
create-comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'duplicate'
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}, your issue is a duplicate and will be closed.
|
||||
你好 @${{ github.event.issue.user.login }},你的issue是重复的,将被关闭。
|
||||
- name: Close issue
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'close-issue'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
25
.github/workflows/issue_invalid.yml
vendored
Normal file
25
.github/workflows/issue_invalid.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Issue Invalid
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
create-comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'invalid'
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}, your issue is invalid and will be closed.
|
||||
你好 @${{ github.event.issue.user.login }},你的issue无效,将被关闭。
|
||||
- name: Close issue
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'close-issue'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
16
.github/workflows/issue_month_statistics.yml
vendored
Normal file
16
.github/workflows/issue_month_statistics.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Issue Month Statistics
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 1 * *"
|
||||
|
||||
jobs:
|
||||
month-statistics:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: month-statistics
|
||||
uses: actions-cool/issues-month-statistics@v1
|
||||
with:
|
||||
count-lables: true
|
||||
count-comments: true
|
||||
emoji: 'eyes'
|
||||
20
.github/workflows/issue_question.yml
vendored
Normal file
20
.github/workflows/issue_question.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Issue Question
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
create-comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'question'
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: actions-cool/issues-helper@v2.0.0
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}, please input issue by template and add detail. Issues labeled by `question` will be closed if no activities in 7 days.
|
||||
你好 @${{ github.event.issue.user.login }},请按照issue模板填写, 并详细说明问题/复现步骤/复现链接/实现思路或提供更多信息等, 7天内未回复issue自动关闭。
|
||||
25
.github/workflows/issue_wontfix.yml
vendored
Normal file
25
.github/workflows/issue_wontfix.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Issue Wontfix
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
lock-issue:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'wontfix'
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}, this issue will not be worked on and will be closed.
|
||||
你好 @${{ github.event.issue.user.login }},这不会被处理,将被关闭。
|
||||
- name: Close issue
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'close-issue'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
58
.github/workflows/release.yml
vendored
Normal file
58
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
release:
|
||||
needs: changelog
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ubuntu-latest]
|
||||
go-version: [1.18]
|
||||
name: Release
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: v2
|
||||
path: alist
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install upx
|
||||
run: |
|
||||
docker pull techknowlogick/xgo:latest
|
||||
go install src.techknowlogick.com/xgo@latest
|
||||
sudo apt install upx
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
mv alist/build.sh .
|
||||
bash build.sh cdn
|
||||
mv dist/* alist/public
|
||||
bash build.sh release
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: alist/build/compress/*
|
||||
45
.github/workflows/release_docker.yml
vendored
Normal file
45
.github/workflows/release_docker.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: release_docker
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
docker_release:
|
||||
name: Docker
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: xhofe/alist
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Build web
|
||||
run: |
|
||||
bash build.sh cdn
|
||||
mv dist/* public
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: xhofe
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -1,7 +1,7 @@
|
||||
.idea/
|
||||
.DS_Store
|
||||
output/
|
||||
/dist/
|
||||
dist/
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
@@ -20,15 +20,11 @@ output/
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
/bin/*
|
||||
bin/*
|
||||
/alist
|
||||
/alist.exe
|
||||
*.json
|
||||
/build
|
||||
/data/
|
||||
/tmp/
|
||||
/log/
|
||||
/lang/
|
||||
/daemon/
|
||||
/public/dist/*
|
||||
/!public/dist/README.md
|
||||
|
||||
.VSCodeCounter
|
||||
public/*.html
|
||||
public/assets/
|
||||
public/public/
|
||||
data/
|
||||
@@ -2,36 +2,33 @@
|
||||
|
||||
## Setup your machine
|
||||
|
||||
`OpenList` is written in [Go](https://golang.org/) and [React](https://reactjs.org/).
|
||||
`alist` is written in [Go](https://golang.org/) and [React](https://reactjs.org/).
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- [git](https://git-scm.com)
|
||||
- [Go 1.20+](https://golang.org/doc/install)
|
||||
- [git](https://nodejs.org/zh-cn/)
|
||||
- [Go 1.18+](https://golang.org/doc/install)
|
||||
- [gcc](https://gcc.gnu.org/)
|
||||
- [nodejs](https://nodejs.org/)
|
||||
|
||||
Clone `OpenList` and `OpenList-Frontend` anywhere:
|
||||
Clone `alist` and `alist-web` anywhere:
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/OpenListTeam/OpenList.git
|
||||
$ git clone --recurse-submodules https://github.com/OpenListTeam/OpenList-Frontend.git
|
||||
$ git clone https://github.com/Xhofe/alist.git
|
||||
$ git clone https://github.com/Xhofe/alist-web.git
|
||||
```
|
||||
You should switch to the `main` branch for development.
|
||||
You should switch to the dev branch for development.
|
||||
|
||||
## Preview your change
|
||||
### backend
|
||||
```shell
|
||||
$ go run main.go
|
||||
$ go run alist.go
|
||||
```
|
||||
### frontend
|
||||
```shell
|
||||
$ pnpm dev
|
||||
$ yarn dev
|
||||
```
|
||||
|
||||
## Add a new driver
|
||||
Copy `drivers/template` folder and rename it, and follow the comments in it.
|
||||
|
||||
## Create a commit
|
||||
|
||||
Commit messages should be well formatted, and to make that "standardized".
|
||||
@@ -76,6 +73,7 @@ Must be one of the following:
|
||||
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
|
||||
generation
|
||||
* **release**: Release a new version
|
||||
* **workflow**: Workflow related file modification
|
||||
|
||||
### Scope
|
||||
The scope could be anything specifying place of the commit change. For example `$location`,
|
||||
@@ -103,5 +101,5 @@ The rest of the commit message is then used for this.
|
||||
|
||||
## Submit a pull request
|
||||
|
||||
Push your branch to your `openlist` fork and open a pull request against the
|
||||
`main` branch.
|
||||
Push your branch to your `alist` fork and open a pull request against the
|
||||
`dev` branch.
|
||||
33
CONTRIBUTORS.md
Normal file
33
CONTRIBUTORS.md
Normal file
@@ -0,0 +1,33 @@
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="http://nn.ci"><img src="https://avatars.githubusercontent.com/u/36558727?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Xhofe</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=Xhofe" title="Code">💻</a> <a href="#ideas-Xhofe" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/alist-org/alist/commits?author=Xhofe" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/foxxorcat"><img src="https://avatars.githubusercontent.com/u/95907542?v=4?s=100" width="100px;" alt=""/><br /><sub><b>foxxorcat</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=foxxorcat" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.iflu.cf/"><img src="https://avatars.githubusercontent.com/u/63903027?v=4?s=100" width="100px;" alt=""/><br /><sub><b>道辰</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=DaoChen6" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://vg-land.github.io/"><img src="https://avatars.githubusercontent.com/u/16739728?v=4?s=100" width="100px;" alt=""/><br /><sub><b>vg-land</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=vg-land" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://c5y.moe"><img src="https://avatars.githubusercontent.com/u/18461360?v=4?s=100" width="100px;" alt=""/><br /><sub><b>凌莞~(=^▽^=)</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=Clansty" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Windman1320"><img src="https://avatars.githubusercontent.com/u/9999486?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Windman</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=Windman1320" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ericarena"><img src="https://avatars.githubusercontent.com/u/4518927?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ericarena</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=ericarena" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/WntFlm"><img src="https://avatars.githubusercontent.com/u/34620278?v=4?s=100" width="100px;" alt=""/><br /><sub><b>WntFlm</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=WntFlm" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/XZB-1248"><img src="https://avatars.githubusercontent.com/u/28593573?v=4?s=100" width="100px;" alt=""/><br /><sub><b>XZB-1248</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=XZB-1248" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM alpine:edge as builder
|
||||
LABEL stage=go-builder
|
||||
WORKDIR /app/
|
||||
COPY ./ ./
|
||||
RUN apk add --no-cache bash git go gcc musl-dev; \
|
||||
bash build.sh docker
|
||||
|
||||
FROM alpine:edge
|
||||
LABEL MAINTAINER="i@nn.ci"
|
||||
VOLUME /opt/alist/data/
|
||||
WORKDIR /opt/alist/
|
||||
COPY --from=builder /app/bin/alist ./
|
||||
EXPOSE 5244
|
||||
CMD [ "./alist", "-docker" ]
|
||||
92
README.md
Executable file
92
README.md
Executable file
@@ -0,0 +1,92 @@
|
||||
<div align="center">
|
||||
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||
<p><em>🗂️Another file list program that supports multiple storage, powered by Gin and React.</em></p>
|
||||
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/release/Xhofe/alist?style=flat-square" alt="latest version"></a>
|
||||
<a href="https://github.com/Xhofe/alist/discussions"><img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936&style=flat-square" alt="discussions"></a>
|
||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build?style=flat-square" alt="Build status"></a>
|
||||
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/downloads/Xhofe/alist/total?style=flat-square&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_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md)
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Multiple storage
|
||||
- [x] Local storage
|
||||
- [x] [Aliyundrive](https://www.aliyundrive.com/)
|
||||
- [x] OneDrive / Sharepoint ([global](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us)
|
||||
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
|
||||
- [x] [GoogleDrive](https://drive.google.com/)
|
||||
- [x] [123pan](https://www.123pan.com/)
|
||||
- [x] [Lanzou](https://pc.woozooo.com/)
|
||||
- [x] [Alist](https://github.com/Xhofe/alist)
|
||||
- [x] FTP
|
||||
- [x] [PikPak](https://www.mypikpak.com/)
|
||||
- [x] [ShandianPan](https://shandianpan.com/)
|
||||
- [x] [S3](https://aws.amazon.com/s3/)
|
||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
||||
- [x] [Mediatrack](https://www.mediatrack.cn/)
|
||||
- [x] [139yun](https://yun.139.com/) (Personal, Family)
|
||||
- [x] [Yandex.Disk](https://disk.yandex.com/)
|
||||
- [x] [Baidu Disk](http://pan.baidu.com/)
|
||||
- [x] [Quark](https://pan.quark.cn)
|
||||
- [x] [XunleiCloud](https://pan.xunlei.com/)
|
||||
- [x] SFTP
|
||||
- [x] [Baidu.Photo](https://photo.baidu.com/)
|
||||
- [x] Easy to deploy and out-of-the-box
|
||||
- [x] File preview (PDF, markdown, code, plain text, ...)
|
||||
- [x] Image preview in gallery mode
|
||||
- [x] Video and audio preview (mp4, mp3, ...)
|
||||
- [x] Office documents preview (docx, pptx, xlsx, ...)
|
||||
- [x] `README.md` preview rendering
|
||||
- [x] File permalink copy and direct file download
|
||||
- [x] Dark mode
|
||||
- [x] I18n
|
||||
- [x] Protected routes (password protection and authentication)
|
||||
- [x] WebDav (see https://alist-doc.nn.ci/en/docs/webdav for details)
|
||||
- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist)
|
||||
- [x] Cloudflare workers proxy
|
||||
- [x] File/Folder package download
|
||||
- [x] Support video list playback and subtitles(ass,srt,vtt)
|
||||
- [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy
|
||||
|
||||
## Discussion
|
||||
|
||||
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports and feature request only.**
|
||||
|
||||
## Demo
|
||||
|
||||
Available at: <https://alist.nn.ci>.
|
||||
|
||||

|
||||
|
||||
## Document
|
||||
|
||||
<https://alist-doc.nn.ci/en/>
|
||||
|
||||
## Special sponsors
|
||||
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.la/)
|
||||
- [KinhDown 百度云盘不限速下载!永久免费!以稳定运行3年!非常可靠!](https://kinhdown.com/?Type=Tutorials)
|
||||
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
|
||||
|
||||
## License
|
||||
|
||||
The `AList` is open-source software licensed under the AGPL-3.0 license.
|
||||
|
||||
## Disclaimer
|
||||
- This program is a free and open source project. It is designed to share files on the network disk, which is convenient for downloading and learning golang. Please abide by relevant laws and regulations when using it, and do not abuse it;
|
||||
- This program is implemented by calling the official sdk/interface, without destroying the official interface behavior;
|
||||
- This program only does 302 redirect/traffic forwarding, and does not intercept, store, or tamper with any user data;
|
||||
- Before using this program, you should understand and bear the corresponding risks, including but not limited to account ban, download speed limit, etc., which is none of this program's business;
|
||||
- If there is any infringement, please contact me by [email](mailto:i@nn.ci), and it will be dealt with in time.
|
||||
|
||||
---
|
||||
|
||||
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@QQGroup](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)
|
||||
92
README_cn.md
Normal file
92
README_cn.md
Normal file
@@ -0,0 +1,92 @@
|
||||
<div align="center">
|
||||
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||
<p><em>🗂️一个支持多存储的文件列表程序,使用 Gin 和 React 。</em></p>
|
||||
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/release/Xhofe/alist?style=flat-square" alt="latest version"></a>
|
||||
<a href="https://github.com/Xhofe/alist/discussions"><img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936&style=flat-square" alt="discussions"></a>
|
||||
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build?style=flat-square" alt="Build status"></a>
|
||||
<a href="https://github.com/Xhofe/alist/releases"><img src="https://img.shields.io/github/downloads/Xhofe/alist/total?style=flat-square&color=%239F7AEA" alt="Downloads"></a>
|
||||
<a href="https://github.com/Xhofe/alist/blob/v2/LICENSE"><img src="https://img.shields.io/github/license/Xhofe/alist?style=flat-square" alt="License"></a>
|
||||
<a href="https://pay.xhofe.top">
|
||||
<img src="https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat-square" alt="donate">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
[English](./README.md) | 中文 | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md)
|
||||
|
||||
## 支持
|
||||
|
||||
- [x] 多种存储
|
||||
- [x] 本地存储
|
||||
- [x] [阿里云盘](https://www.aliyundrive.com/)
|
||||
- [x] OneDrive / Sharepoint([国际版](https://www.office.com/), [世纪互联](https://portal.partner.microsoftonline.cn),de,us)
|
||||
- [x] [天翼云盘](https://cloud.189.cn) (个人云, 家庭云)
|
||||
- [x] [GoogleDrive](https://drive.google.com/)
|
||||
- [x] [123云盘](https://www.123pan.com/)
|
||||
- [x] [蓝奏云](https://pc.woozooo.com/)
|
||||
- [x] [Alist](https://github.com/Xhofe/alist)
|
||||
- [x] FTP
|
||||
- [x] [PikPak](https://www.mypikpak.com/)
|
||||
- [x] [闪电盘](https://shandianpan.com/)
|
||||
- [x] [S3](https://aws.amazon.com/cn/s3/)
|
||||
- [x] WebDav(支持无API的OneDrive/SharePoint)
|
||||
- [x] Teambition([中国](https://www.teambition.com/ ),[国际](https://us.teambition.com/ ))
|
||||
- [x] [分秒帧](https://www.mediatrack.cn/)
|
||||
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云)
|
||||
- [x] [Yandex.Disk](https://disk.yandex.com/)
|
||||
- [x] [百度网盘](http://pan.baidu.com/)
|
||||
- [x] [夸克网盘](https://pan.quark.cn)
|
||||
- [x] [迅雷云盘](https://pan.xunlei.com/)
|
||||
- [x] SFTP
|
||||
- [x] [一刻相册](https://photo.baidu.com/)
|
||||
- [x] 部署方便,开箱即用
|
||||
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
||||
- [x] 画廊模式下的图像预览
|
||||
- [x] 视频和音频预览(mp4、mp3 等)
|
||||
- [x] Office 文档预览(docx、pptx、xlsx、...)
|
||||
- [x] `README.md` 预览渲染
|
||||
- [x] 文件永久链接复制和直接文件下载
|
||||
- [x] 黑暗模式
|
||||
- [x] 国际化
|
||||
- [x] 受保护的路由(密码保护和身份验证)
|
||||
- [x] WebDav(具体见https://alist-doc.nn.ci/docs/webdav )
|
||||
- [x] [Docker 部署](https://hub.docker.com/r/xhofe/alist)
|
||||
- [x] Cloudflare workers 中转
|
||||
- [x] 文件/文件夹打包下载
|
||||
- [x] 支持视频列表播放和字幕(ass,srt,vtt)
|
||||
- [x] 网页上传(可以允许访客上传),删除,新建文件夹,重命名,移动,复制
|
||||
|
||||
## 讨论
|
||||
|
||||
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) ,**issue仅针对错误报告和功能请求。**
|
||||
|
||||
## 演示
|
||||
|
||||
<https://alist.nn.ci>。
|
||||
|
||||

|
||||
|
||||
## 文档
|
||||
|
||||
<https://alist-doc.nn.ci/>
|
||||
|
||||
## 特别赞助
|
||||
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.la/)
|
||||
- [KinhDown 百度云盘不限速下载!永久免费!以稳定运行3年!非常可靠!](https://kinhdown.com/?Type=Tutorials)
|
||||
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
|
||||
|
||||
## 许可
|
||||
|
||||
`AList` 是在 AGPL-3.0 许可下许可的开源软件。
|
||||
|
||||
## 免责声明
|
||||
- 本程序为免费开源项目,旨在分享网盘文件,方便下载以及学习golang,使用时请遵守相关法律法规,请勿滥用;
|
||||
- 本程序通过调用官方sdk/接口实现,无破坏官方接口行为;
|
||||
- 本程序仅做302重定向/流量转发,不拦截、存储、篡改任何用户数据;
|
||||
- 在使用本程序之前,你应了解并承担相应的风险,包括但不限于账号被ban,下载限速等,与本程序无关;
|
||||
- 如有侵权,请通过[邮件](mailto:i@nn.ci)与我联系,会及时处理。
|
||||
|
||||
---
|
||||
|
||||
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@QQ群](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)
|
||||
59
alist.go
Normal file
59
alist.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/bootstrap"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
_ "github.com/Xhofe/alist/drivers"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/server"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func Init() bool {
|
||||
bootstrap.InitConf()
|
||||
bootstrap.InitCron()
|
||||
bootstrap.InitModel()
|
||||
if conf.Password {
|
||||
pass, err := model.GetSettingByKey("password")
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
return false
|
||||
}
|
||||
fmt.Printf("your password: %s\n", pass.Value)
|
||||
return false
|
||||
}
|
||||
server.InitIndex()
|
||||
bootstrap.InitSettings()
|
||||
bootstrap.InitAccounts()
|
||||
bootstrap.InitCache()
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
if conf.Version {
|
||||
fmt.Printf("Built At: %s\nGo Version: %s\nAuthor: %s\nCommit ID: %s\nVersion: %s\nWebVersion: %s\n",
|
||||
conf.BuiltAt, conf.GoVersion, conf.GitAuthor, conf.GitCommit, conf.GitTag, conf.WebTag)
|
||||
return
|
||||
}
|
||||
if !Init() {
|
||||
return
|
||||
}
|
||||
if !conf.Debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
r := gin.Default()
|
||||
server.InitApiRouter(r)
|
||||
base := fmt.Sprintf("%s:%d", conf.Conf.Address, conf.Conf.Port)
|
||||
log.Infof("start server @ %s", base)
|
||||
var err error
|
||||
if conf.Conf.Scheme.Https {
|
||||
err = r.RunTLS(base, conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||
} else {
|
||||
err = r.Run(base)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("failed to start: %s", err.Error())
|
||||
}
|
||||
}
|
||||
33
bootstrap/account.go
Normal file
33
bootstrap/account.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/drivers/operate"
|
||||
"github.com/Xhofe/alist/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func InitAccounts() {
|
||||
log.Infof("init accounts...")
|
||||
var accounts []model.Account
|
||||
if err := conf.DB.Find(&accounts).Error; err != nil {
|
||||
log.Fatalf("failed sync init accounts")
|
||||
}
|
||||
for i, account := range accounts {
|
||||
model.RegisterAccount(account)
|
||||
driver, ok := base.GetDriver(account.Type)
|
||||
if !ok {
|
||||
log.Errorf("no [%s] driver", account.Type)
|
||||
} else {
|
||||
log.Infof("start init account: [%s], type: [%s]", account.Name, account.Type)
|
||||
//err := driver.Save(&accounts[i], nil)
|
||||
err := operate.Save(driver, &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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
bootstrap/cache.go
Normal file
22
bootstrap/cache.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/eko/gocache/v2/cache"
|
||||
"github.com/eko/gocache/v2/store"
|
||||
goCache "github.com/patrickmn/go-cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InitCache init cache
|
||||
func InitCache() {
|
||||
log.Infof("init cache...")
|
||||
c := conf.Conf.Cache
|
||||
if c.Expiration == 0 {
|
||||
c.Expiration, c.CleanupInterval = 60, 120
|
||||
}
|
||||
goCacheClient := goCache.New(time.Duration(c.Expiration)*time.Minute, time.Duration(c.CleanupInterval)*time.Minute)
|
||||
goCacheStore := store.NewGoCache(goCacheClient, nil)
|
||||
conf.Cache = cache.New(goCacheStore)
|
||||
}
|
||||
71
bootstrap/conf.go
Normal file
71
bootstrap/conf.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/caarlos0/env/v6"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// InitConf init config
|
||||
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")
|
||||
}
|
||||
} else {
|
||||
config, err := ioutil.ReadFile(conf.ConfigFile)
|
||||
if err != nil {
|
||||
log.Fatalf("reading config file error:%s", err.Error())
|
||||
}
|
||||
conf.Conf = conf.DefaultConfig()
|
||||
err = utils.Json.Unmarshal(config, conf.Conf)
|
||||
if err != nil {
|
||||
log.Fatalf("load config error: %s", err.Error())
|
||||
}
|
||||
log.Debugf("config:%+v", conf.Conf)
|
||||
// update config.json struct
|
||||
confBody, err := utils.Json.MarshalIndent(conf.Conf, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("marshal config error:%s", err.Error())
|
||||
}
|
||||
err = ioutil.WriteFile(conf.ConfigFile, confBody, 0777)
|
||||
if err != nil {
|
||||
log.Fatalf("update config struct error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
if !conf.Conf.Force {
|
||||
confFromEnv()
|
||||
}
|
||||
err := os.RemoveAll(filepath.Join(conf.Conf.TempDir))
|
||||
if err != nil {
|
||||
log.Errorln("failed delete temp file:", err)
|
||||
}
|
||||
err = os.MkdirAll(conf.Conf.TempDir, 0700)
|
||||
if err != nil {
|
||||
log.Fatalf("create temp dir error: %s", err.Error())
|
||||
}
|
||||
log.Debugf("config: %+v", conf.Conf)
|
||||
}
|
||||
|
||||
func confFromEnv() {
|
||||
prefix := "ALIST_"
|
||||
if conf.Docker {
|
||||
prefix = ""
|
||||
}
|
||||
if err := env.Parse(conf.Conf, env.Options{
|
||||
Prefix: prefix,
|
||||
}); err != nil {
|
||||
log.Fatalf("load config from env error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
14
bootstrap/cron.go
Normal file
14
bootstrap/cron.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/robfig/cron/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// InitCron init cron
|
||||
func InitCron() {
|
||||
log.Infof("init cron...")
|
||||
conf.Cron = cron.New()
|
||||
conf.Cron.Start()
|
||||
}
|
||||
36
bootstrap/log.go
Normal file
36
bootstrap/log.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// InitLog init log
|
||||
func InitLog() {
|
||||
if conf.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetReportCaller(true)
|
||||
}
|
||||
if conf.Password || conf.Version {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
//DisableColors: true,
|
||||
ForceColors: true,
|
||||
EnvironmentOverrideColors: true,
|
||||
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.BoolVar(&conf.Docker, "docker", false, "is using docker")
|
||||
flag.Parse()
|
||||
InitLog()
|
||||
}
|
||||
84
bootstrap/model.go
Normal file
84
bootstrap/model.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
log2 "log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func InitModel() {
|
||||
log.Infof("init model...")
|
||||
var err error
|
||||
databaseConfig := conf.Conf.Database
|
||||
newLogger := logger.New(
|
||||
log2.New(os.Stdout, "\r\n", log2.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: time.Second,
|
||||
LogLevel: logger.Silent,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
Colorful: true,
|
||||
},
|
||||
)
|
||||
gormConfig := &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: databaseConfig.TablePrefix,
|
||||
},
|
||||
Logger: newLogger,
|
||||
}
|
||||
switch databaseConfig.Type {
|
||||
case "sqlite3":
|
||||
{
|
||||
if !(strings.HasSuffix(databaseConfig.DBFile, ".db") && len(databaseConfig.DBFile) > 3) {
|
||||
log.Fatalf("db name error.")
|
||||
}
|
||||
db, err := gorm.Open(sqlite.Open(databaseConfig.DBFile), gormConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to connect database:%s", err.Error())
|
||||
}
|
||||
conf.DB = db
|
||||
}
|
||||
case "mysql":
|
||||
{
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&tls=%s",
|
||||
databaseConfig.User, databaseConfig.Password, databaseConfig.Host, databaseConfig.Port, databaseConfig.Name, databaseConfig.SslMode)
|
||||
db, err := gorm.Open(mysql.Open(dsn), gormConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to connect database:%s", err.Error())
|
||||
}
|
||||
conf.DB = db
|
||||
}
|
||||
case "postgres":
|
||||
{
|
||||
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=Asia/Shanghai",
|
||||
databaseConfig.Host, databaseConfig.User, databaseConfig.Password, databaseConfig.Name, databaseConfig.Port, databaseConfig.SslMode)
|
||||
db, err := gorm.Open(postgres.Open(dsn), gormConfig)
|
||||
if err != nil {
|
||||
log.Errorf("failed to connect database:%s", err.Error())
|
||||
}
|
||||
conf.DB = db
|
||||
}
|
||||
default:
|
||||
log.Fatalf("not supported database type: %s", databaseConfig.Type)
|
||||
}
|
||||
log.Infof("auto migrate model...")
|
||||
if databaseConfig.Type == "mysql" {
|
||||
err = conf.DB.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4").
|
||||
AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{}, &model.SearchFile{})
|
||||
} else {
|
||||
err = conf.DB.AutoMigrate(&model.SettingItem{}, &model.Account{}, &model.Meta{}, &model.SearchFile{})
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed to auto migrate: %s", err.Error())
|
||||
}
|
||||
}
|
||||
323
bootstrap/setting.go
Normal file
323
bootstrap/setting.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func InitSettings() {
|
||||
log.Infof("init settings...")
|
||||
|
||||
err := model.SaveSetting(model.Version)
|
||||
if err != nil {
|
||||
log.Fatalf("failed write setting: %s", err.Error())
|
||||
}
|
||||
|
||||
settings := []model.SettingItem{
|
||||
{
|
||||
Key: "title",
|
||||
Value: "Alist",
|
||||
Description: "title",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "password",
|
||||
Value: utils.RandomStr(8),
|
||||
Description: "password",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "logo",
|
||||
Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/can_circle.svg",
|
||||
Description: "logo",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "favicon",
|
||||
Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg",
|
||||
Description: "favicon",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "icon color",
|
||||
Value: "#1890ff",
|
||||
Description: "icon's color",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "announcement",
|
||||
Value: "This is a test announcement.",
|
||||
Description: "announcement message (support markdown)",
|
||||
Type: "text",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "text types",
|
||||
Value: strings.Join(conf.TextTypes, ","),
|
||||
Type: "string",
|
||||
Description: "text type extensions",
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "audio types",
|
||||
Value: strings.Join(conf.AudioTypes, ","),
|
||||
Type: "string",
|
||||
Description: "audio type extensions",
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "video types",
|
||||
Value: strings.Join(conf.VideoTypes, ","),
|
||||
Type: "string",
|
||||
Description: "video type extensions",
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "d_proxy types",
|
||||
Value: strings.Join(conf.DProxyTypes, ","),
|
||||
Type: "string",
|
||||
Description: "/d but proxy",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "hide files",
|
||||
Value: "/\\/README.md/i",
|
||||
Type: "text",
|
||||
Description: "hide files, support RegExp, one per line",
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "music cover",
|
||||
Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/circle_center.svg",
|
||||
Description: "music cover image",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "site beian",
|
||||
Description: "chinese beian info",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "global readme url",
|
||||
Description: "Default display when directory has no readme",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "pdf viewer url",
|
||||
Type: "string",
|
||||
Value: "https://alist-org.github.io/pdf.js/web/viewer.html?file=$url",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "autoplay video",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "autoplay audio",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "check parent folder",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Description: "check parent folder password",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "customize head",
|
||||
Value: "",
|
||||
Type: "text",
|
||||
Description: "Customize head, placed at the beginning of the head",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "customize body",
|
||||
Value: "",
|
||||
Type: "text",
|
||||
Description: "Customize script, placed at the end of the body",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "home emoji",
|
||||
Value: "🏠",
|
||||
Type: "string",
|
||||
Description: "emoji in front of home in nav",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "animation",
|
||||
Value: "true",
|
||||
Type: "bool",
|
||||
Description: "when there are a lot of files, the animation will freeze when opening",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "check down link",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Description: "check down link password, your link will be 'https://alist.com/d/filename?pw=xxx'",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "WebDAV username",
|
||||
Value: "admin",
|
||||
Description: "WebDAV username",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "WebDAV password",
|
||||
Value: utils.RandomStr(8),
|
||||
Description: "WebDAV password",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "artplayer whitelist",
|
||||
Value: "*",
|
||||
Description: "refer to https://artplayer.org/document/options#whitelist",
|
||||
Type: "string",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "artplayer autoSize",
|
||||
Value: "true",
|
||||
Description: "refer to https://artplayer.org/document/options#autosize",
|
||||
Type: "bool",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "Visitor WebDAV username",
|
||||
Value: "guest",
|
||||
Description: "Visitor WebDAV username",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "Visitor WebDAV password",
|
||||
Value: "guest",
|
||||
Description: "Visitor WebDAV password",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "load type",
|
||||
Value: "all",
|
||||
Type: "select",
|
||||
Values: "all,load more,auto load more,pagination",
|
||||
Description: "Not recommended to choose to auto load more, it has bugs now",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "default page size",
|
||||
Value: "30",
|
||||
Type: "number",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.FRONT,
|
||||
},
|
||||
{
|
||||
Key: "ocr api",
|
||||
Value: "https://api.nn.ci/ocr/file/json",
|
||||
Description: "Used to identify verification codes",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "enable search",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Access: model.PUBLIC,
|
||||
Group: model.BACK,
|
||||
Description: "Experimental function, not recommended as it's still under development",
|
||||
},
|
||||
{
|
||||
Key: "Aria2 RPC url",
|
||||
Value: "http://localhost:6800/jsonrpc",
|
||||
Description: "Aria2 RPC url, e.g. 'http://aria2.example.com:6800/jsonrpc'",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
{
|
||||
Key: "Aria2 RPC secret",
|
||||
Value: "",
|
||||
Description: "Aria2 RPC secret, e.g. '123456'",
|
||||
Type: "string",
|
||||
Access: model.PRIVATE,
|
||||
Group: model.BACK,
|
||||
},
|
||||
}
|
||||
for i, _ := range settings {
|
||||
v := settings[i]
|
||||
v.Version = conf.GitTag
|
||||
o, err := model.GetSettingByKey(v.Key)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
err = model.SaveSetting(v)
|
||||
if v.Key == "password" {
|
||||
log.Infof("Initial password: %s", conf.C.Sprintf(v.Value))
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed write setting: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Fatalf("can't get setting: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
//o.Version = conf.GitTag
|
||||
//err = model.SaveSetting(*o)
|
||||
v.Value = o.Value
|
||||
err = model.SaveSetting(v)
|
||||
if err != nil {
|
||||
log.Fatalf("failed write setting: %s", err.Error())
|
||||
}
|
||||
if v.Key == "password" {
|
||||
log.Infof("Your password: %s", conf.C.Sprintf(v.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
model.LoadSettings()
|
||||
}
|
||||
11
buf.gen.yaml
11
buf.gen.yaml
@@ -1,11 +0,0 @@
|
||||
version: v1
|
||||
plugins:
|
||||
- plugin: buf.build/protocolbuffers/go:v1.36.7
|
||||
out: .
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- plugin: buf.build/grpc/go:v1.5.1
|
||||
out: .
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- require_unimplemented_servers=false
|
||||
157
build.sh
Normal file
157
build.sh
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 构建前端,在当前目录产生一个dist文件夹
|
||||
BUILD_WEB() {
|
||||
git clone https://github.com/alist-org/web-v2.git
|
||||
cd web-v2
|
||||
yarn
|
||||
yarn build
|
||||
sed -i -e "s/\/CDN_URL\//\//g" dist/index.html
|
||||
sed -i -e "s/assets/\/assets/g" dist/index.html
|
||||
rm -f dist/index.html-e
|
||||
mv dist ..
|
||||
cd .. || exit
|
||||
rm -rf web-v2
|
||||
}
|
||||
|
||||
CDN_WEB() {
|
||||
curl -L https://github.com/alist-org/web-v2/releases/latest/download/dist.tar.gz -o dist.tar.gz
|
||||
tar -zxvf dist.tar.gz
|
||||
rm -f dist.tar.gz
|
||||
}
|
||||
|
||||
# 在DOCKER中构建
|
||||
BUILD_DOCKER() {
|
||||
appName="alist"
|
||||
builtAt="$(date +'%F %T %z')"
|
||||
goVersion=$(go version | sed 's/go version //')
|
||||
gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
|
||||
gitCommit=$(git log --pretty=format:"%h" -1)
|
||||
gitTag=$(git describe --long --tags --dirty --always)
|
||||
webTag=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/web-v2/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||
ldflags="\
|
||||
-w -s \
|
||||
-X 'github.com/Xhofe/alist/conf.BuiltAt=$builtAt' \
|
||||
-X 'github.com/Xhofe/alist/conf.GoVersion=$goVersion' \
|
||||
-X 'github.com/Xhofe/alist/conf.GitAuthor=$gitAuthor' \
|
||||
-X 'github.com/Xhofe/alist/conf.GitCommit=$gitCommit' \
|
||||
-X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \
|
||||
-X 'github.com/Xhofe/alist/conf.WebTag=$webTag' \
|
||||
"
|
||||
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter alist.go
|
||||
}
|
||||
|
||||
BUILD() {
|
||||
cd alist
|
||||
appName="alist"
|
||||
builtAt="$(date +'%F %T %z')"
|
||||
goVersion=$(go version | sed 's/go version //')
|
||||
gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
|
||||
gitCommit=$(git log --pretty=format:"%h" -1)
|
||||
gitTag=$(git describe --long --tags --dirty --always)
|
||||
webTag=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/web-v2/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||
echo "build version: $gitTag"
|
||||
|
||||
ldflags="\
|
||||
-w -s \
|
||||
-X 'github.com/Xhofe/alist/conf.BuiltAt=$builtAt' \
|
||||
-X 'github.com/Xhofe/alist/conf.GoVersion=$goVersion' \
|
||||
-X 'github.com/Xhofe/alist/conf.GitAuthor=$gitAuthor' \
|
||||
-X 'github.com/Xhofe/alist/conf.GitCommit=$gitCommit' \
|
||||
-X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \
|
||||
-X 'github.com/Xhofe/alist/conf.WebTag=$webTag' \
|
||||
"
|
||||
rm -rf .git/
|
||||
if [ "$1" == "release" ]; then
|
||||
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
||||
else
|
||||
xgo -targets=linux/amd64,windows/amd64,darwin/amd64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
||||
fi
|
||||
mkdir -p "build"
|
||||
mv alist-* build
|
||||
if [ "$1" != "release" ]; then
|
||||
cd build
|
||||
upx -9 ./alist-linux*
|
||||
upx -9 ./alist-windows*
|
||||
find . -type f -print0 | xargs -0 md5sum >md5.txt
|
||||
cat md5.txt
|
||||
cd .. || exit
|
||||
fi
|
||||
cd .. || exit
|
||||
}
|
||||
|
||||
BUILD_MUSL() {
|
||||
BASE="https://musl.cc/"
|
||||
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross arm-linux-musleabihf-cross mips-linux-musl-cross mips64-linux-musl-cross mips64el-linux-musl-cross mipsel-linux-musl-cross powerpc64le-linux-musl-cross s390x-linux-musl-cross)
|
||||
for i in "${FILES[@]}"; do
|
||||
url="${BASE}${i}.tgz"
|
||||
curl -L -o "${i}.tgz" "${url}"
|
||||
sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
|
||||
done
|
||||
cd alist
|
||||
appName="alist"
|
||||
builtAt="$(date +'%F %T %z')"
|
||||
goVersion=$(go version | sed 's/go version //')
|
||||
gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
|
||||
gitCommit=$(git log --pretty=format:"%h" -1)
|
||||
gitTag=$(git describe --long --tags --dirty --always)
|
||||
webTag=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/web-v2/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||
ldflags="\
|
||||
-w -s --extldflags '-static -fpic' \
|
||||
-X 'github.com/Xhofe/alist/conf.BuiltAt=$builtAt' \
|
||||
-X 'github.com/Xhofe/alist/conf.GoVersion=$goVersion' \
|
||||
-X 'github.com/Xhofe/alist/conf.GitAuthor=$gitAuthor' \
|
||||
-X 'github.com/Xhofe/alist/conf.GitCommit=$gitCommit' \
|
||||
-X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \
|
||||
-X 'github.com/Xhofe/alist/conf.WebTag=$webTag' \
|
||||
"
|
||||
OS_ARCHES=(linux-musl-amd64 linux-musl-arm64 linux-musl-arm linux-musl-mips linux-musl-mips64 linux-musl-mips64le linux-musl-mipsle linux-musl-ppc64le linux-musl-s390x)
|
||||
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc arm-linux-musleabihf-gcc mips-linux-musl-gcc mips64-linux-musl-gcc mips64el-linux-musl-gcc mipsel-linux-musl-gcc powerpc64le-linux-musl-gcc s390x-linux-musl-gcc)
|
||||
for i in "${!OS_ARCHES[@]}"; do
|
||||
os_arch=${OS_ARCHES[$i]}
|
||||
cgo_cc=${CGO_ARGS[$i]}
|
||||
echo building for ${os_arch}
|
||||
export GOOS=${os_arch%%-*}
|
||||
export GOARCH=${os_arch##*-}
|
||||
export CC=${cgo_cc}
|
||||
export CGO_ENABLED=1
|
||||
go build -o ./build/$appName-$os_arch -ldflags="$ldflags" -tags=jsoniter alist.go
|
||||
done
|
||||
cd .. || exit
|
||||
}
|
||||
|
||||
RELEASE() {
|
||||
cd alist/build
|
||||
upx -9 ./alist-linux-amd64
|
||||
upx -9 ./alist-windows*
|
||||
find . -type f -print0 | xargs -0 md5sum >md5.txt
|
||||
cat md5.txt
|
||||
mkdir compress
|
||||
mv md5.txt compress
|
||||
for i in $(find . -type f -name "$appName-linux-*"); do
|
||||
tar -czvf compress/"$i".tar.gz "$i"
|
||||
done
|
||||
for i in $(find . -type f -name "$appName-darwin-*"); do
|
||||
tar -czvf compress/"$i".tar.gz "$i"
|
||||
done
|
||||
for i in $(find . -type f -name "$appName-windows-*"); do
|
||||
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip "$i"
|
||||
done
|
||||
cd ../.. || exit
|
||||
}
|
||||
|
||||
if [ "$1" = "web" ]; then
|
||||
BUILD_WEB
|
||||
elif [ "$1" = "cdn" ]; then
|
||||
CDN_WEB
|
||||
elif [ "$1" = "docker" ]; then
|
||||
BUILD_DOCKER
|
||||
elif [ "$1" = "build" ]; then
|
||||
BUILD build
|
||||
elif [ "$1" = "release" ]; then
|
||||
BUILD_MUSL
|
||||
BUILD release
|
||||
RELEASE
|
||||
else
|
||||
echo -e "${RED_COLOR} Parameter error ${RES}"
|
||||
fi
|
||||
@@ -1,42 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
||||
"github.com/OpenListTeam/OpenList/v5/internal/bootstrap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) {
|
||||
if flags.Dev {
|
||||
flags.Debug = true
|
||||
}
|
||||
initLogrus()
|
||||
bootstrap.InitConfig()
|
||||
bootstrap.InitDriverPlugins()
|
||||
}
|
||||
|
||||
func Release() {
|
||||
|
||||
}
|
||||
|
||||
func initLog(l *logrus.Logger) {
|
||||
if flags.Debug {
|
||||
l.SetLevel(logrus.DebugLevel)
|
||||
l.SetReportCaller(true)
|
||||
} else {
|
||||
l.SetLevel(logrus.InfoLevel)
|
||||
l.SetReportCaller(false)
|
||||
}
|
||||
}
|
||||
func initLogrus() {
|
||||
formatter := logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
EnvironmentOverrideColors: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
FullTimestamp: true,
|
||||
}
|
||||
logrus.SetFormatter(&formatter)
|
||||
initLog(logrus.StandardLogger())
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
ConfigFile string
|
||||
Debug bool
|
||||
NoPrefix bool
|
||||
Dev bool
|
||||
ForceBinDir bool
|
||||
LogStd bool
|
||||
|
||||
pwd string
|
||||
)
|
||||
|
||||
// Program working directory
|
||||
func PWD() string {
|
||||
if pwd != "" {
|
||||
return pwd
|
||||
}
|
||||
if ForceBinDir {
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
pwd = filepath.Dir(ex)
|
||||
return pwd
|
||||
}
|
||||
d, err := os.Getwd()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
pwd = d
|
||||
return d
|
||||
}
|
||||
33
cmd/root.go
33
cmd/root.go
@@ -1,33 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "openlist",
|
||||
Short: "A file list program that supports multiple storage.",
|
||||
Long: `A file list program that supports multiple storage,
|
||||
built with love by OpenListTeam.
|
||||
Complete documentation is available at https://doc.oplist.org/`,
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := RootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.PersistentFlags().StringVarP(&flags.ConfigFile, "config", "c", "data/config.json", "config file")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
|
||||
RootCmd.PersistentFlags().BoolVarP(&flags.ForceBinDir, "force-bin-dir", "f", false, "force to use the directory where the binary file is located as data directory")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "force to log to std")
|
||||
}
|
||||
162
cmd/server.go
162
cmd/server.go
@@ -1,162 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
||||
"github.com/OpenListTeam/OpenList/v5/internal/conf"
|
||||
"github.com/OpenListTeam/OpenList/v5/server"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
// ServerCmd represents the server command
|
||||
var ServerCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Start the server at the specified address",
|
||||
Long: `Start the server at the specified address
|
||||
the address is defined in config file`,
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
serverCtx, serverCancel := context.WithCancel(context.Background())
|
||||
defer serverCancel()
|
||||
Init(serverCtx)
|
||||
|
||||
if !flags.Debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
r := gin.New()
|
||||
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out))
|
||||
r.Use(gin.RecoveryWithWriter(log.StandardLogger().Out))
|
||||
server.Init(r)
|
||||
|
||||
var httpHandler http.Handler = r
|
||||
if conf.Conf.Scheme.EnableH2c {
|
||||
httpHandler = h2c.NewHandler(r, &http2.Server{})
|
||||
}
|
||||
var httpSrv, httpsSrv, unixSrv *http.Server
|
||||
if conf.Conf.Scheme.HttpPort > 0 {
|
||||
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
|
||||
log.Infoln("start HTTP server", "@", httpBase)
|
||||
httpSrv = &http.Server{Addr: httpBase, Handler: httpHandler}
|
||||
go func() {
|
||||
err := httpSrv.ListenAndServe()
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Errorln("start HTTP server", ":", err)
|
||||
serverCancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
if conf.Conf.Scheme.HttpsPort > 0 {
|
||||
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
|
||||
log.Infoln("start HTTPS server", "@", httpsBase)
|
||||
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
|
||||
go func() {
|
||||
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Errorln("start HTTPS server", ":", err)
|
||||
serverCancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
if conf.Conf.Scheme.UnixFile != "" {
|
||||
log.Infoln("start Unix server", "@", conf.Conf.Scheme.UnixFile)
|
||||
unixSrv = &http.Server{Handler: httpHandler}
|
||||
go func() {
|
||||
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
|
||||
if err != nil {
|
||||
log.Errorln("start Unix server", ":", err)
|
||||
serverCancel()
|
||||
return
|
||||
}
|
||||
|
||||
mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)
|
||||
if err != nil {
|
||||
log.Errorln("parse unix_file_perm", ":", err)
|
||||
} else {
|
||||
err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))
|
||||
if err != nil {
|
||||
log.Errorln("chmod socket file", ":", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = unixSrv.Serve(listener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Errorln("start Unix server", ":", err)
|
||||
serverCancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
// kill (no param) default send syscanll.SIGTERM
|
||||
// kill -2 is syscall.SIGINT
|
||||
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
select {
|
||||
case <-quit:
|
||||
case <-serverCtx.Done():
|
||||
}
|
||||
|
||||
log.Println("shutdown server...")
|
||||
Release()
|
||||
|
||||
quitCtx, quitCancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer quitCancel()
|
||||
var wg sync.WaitGroup
|
||||
if httpSrv != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := httpSrv.Shutdown(quitCtx); err != nil {
|
||||
log.Errorln("shutdown HTTP server", ":", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if httpsSrv != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := httpsSrv.Shutdown(quitCtx); err != nil {
|
||||
log.Errorln("shutdown HTTPS server", ":", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if unixSrv != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := unixSrv.Shutdown(quitCtx); err != nil {
|
||||
log.Errorln("shutdown Unix server", ":", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
log.Println("server exit")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(ServerCmd)
|
||||
}
|
||||
|
||||
// OutOpenListInit 暴露用于外部启动server的函数
|
||||
func OutOpenListInit() {
|
||||
var (
|
||||
cmd *cobra.Command
|
||||
args []string
|
||||
)
|
||||
ServerCmd.Run(cmd, args)
|
||||
}
|
||||
58
conf/config.go
Normal file
58
conf/config.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package conf
|
||||
|
||||
type Database struct {
|
||||
Type string `json:"type" env:"DB_TYPE"`
|
||||
Host string `json:"host" env:"DB_HOST"`
|
||||
Port int `json:"port" env:"DB_PORT"`
|
||||
User string `json:"user" env:"DB_USER"`
|
||||
Password string `json:"password" env:"DB_PASS"`
|
||||
Name string `json:"name" env:"DB_NAME"`
|
||||
DBFile string `json:"db_file" env:"DB_FILE"`
|
||||
TablePrefix string `json:"table_prefix" env:"DB_TABLE_PREFIX"`
|
||||
SslMode string `json:"ssl_mode" env:"DB_SLL_MODE"`
|
||||
}
|
||||
|
||||
type Scheme struct {
|
||||
Https bool `json:"https" env:"HTTPS"`
|
||||
CertFile string `json:"cert_file" env:"CERT_FILE"`
|
||||
KeyFile string `json:"key_file" env:"KEY_FILE"`
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Expiration int64 `json:"expiration" env:"CACHE_EXPIRATION"`
|
||||
CleanupInterval int64 `json:"cleanup_interval" env:"CLEANUP_INTERVAL"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Force bool `json:"force"`
|
||||
Address string `json:"address" env:"ADDR"`
|
||||
Port int `json:"port" env:"PORT"`
|
||||
Assets string `json:"assets" env:"ASSETS"`
|
||||
LocalAssets string `json:"localassets" env:"LOCALASSETS"`
|
||||
SubFolder string `json:"subfolder" env:"SUBFOLDER"`
|
||||
Database Database `json:"database"`
|
||||
Scheme Scheme `json:"scheme"`
|
||||
Cache CacheConfig `json:"cache"`
|
||||
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Address: "0.0.0.0",
|
||||
Port: 5244,
|
||||
Assets: "https://npm.elemecdn.com/alist-web@$version/dist",
|
||||
SubFolder: "",
|
||||
LocalAssets: "",
|
||||
TempDir: "data/temp",
|
||||
Database: Database{
|
||||
Type: "sqlite3",
|
||||
Port: 0,
|
||||
TablePrefix: "x_",
|
||||
DBFile: "data/data.db",
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
Expiration: 60,
|
||||
CleanupInterval: 120,
|
||||
},
|
||||
}
|
||||
}
|
||||
11
conf/const.go
Normal file
11
conf/const.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package conf
|
||||
|
||||
const (
|
||||
UNKNOWN = iota
|
||||
FOLDER
|
||||
OFFICE
|
||||
VIDEO
|
||||
AUDIO
|
||||
TEXT
|
||||
IMAGE
|
||||
)
|
||||
104
conf/var.go
Normal file
104
conf/var.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/eko/gocache/v2/cache"
|
||||
"github.com/fatih/color"
|
||||
"github.com/robfig/cron/v3"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
BuiltAt string
|
||||
GoVersion string
|
||||
GitAuthor string
|
||||
GitCommit string
|
||||
GitTag string = "dev"
|
||||
WebTag string
|
||||
)
|
||||
|
||||
var (
|
||||
ConfigFile string // config file
|
||||
Conf *Config
|
||||
Debug bool
|
||||
Version bool
|
||||
Password bool
|
||||
Docker bool
|
||||
|
||||
DB *gorm.DB
|
||||
Cache *cache.Cache
|
||||
Ctx = context.TODO()
|
||||
Cron *cron.Cron
|
||||
|
||||
C = color.New(color.FgHiBlue, color.Bold, color.BgHiWhite, color.Underline)
|
||||
)
|
||||
|
||||
var (
|
||||
TextTypes = []string{"txt", "htm", "html", "xml", "java", "properties", "sql",
|
||||
"js", "md", "json", "conf", "ini", "vue", "php", "py", "bat", "gitignore", "yml",
|
||||
"go", "sh", "c", "cpp", "h", "hpp", "tsx", "vtt", "srt", "ass"}
|
||||
DProxyTypes = []string{"m3u8"}
|
||||
OfficeTypes = []string{"doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf"}
|
||||
VideoTypes = []string{"mp4", "mkv", "avi", "mov", "rmvb", "webm", "flv", "m4v"}
|
||||
AudioTypes = []string{"mp3", "flac", "ogg", "m4a", "wav", "opus"}
|
||||
ImageTypes = []string{"jpg", "tiff", "jpeg", "png", "gif", "bmp", "svg", "ico", "swf", "webp"}
|
||||
)
|
||||
|
||||
var settingsMap = make(map[string]string)
|
||||
|
||||
func Set(key string, value string) {
|
||||
settingsMap[key] = value
|
||||
}
|
||||
|
||||
func GetStr(key string) string {
|
||||
value, ok := settingsMap[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func GetBool(key string) bool {
|
||||
value, ok := settingsMap[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return value == "true"
|
||||
}
|
||||
|
||||
func GetInt(key string, defaultV int) int {
|
||||
value, ok := settingsMap[key]
|
||||
if !ok {
|
||||
return defaultV
|
||||
}
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return defaultV
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
var (
|
||||
LoadSettings = []string{
|
||||
"check parent folder", "check down link", "WebDAV username", "WebDAV password",
|
||||
"Visitor WebDAV username", "Visitor WebDAV password",
|
||||
"default page size", "load type",
|
||||
"ocr api", "favicon",
|
||||
"enable search",
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
RawIndexHtml string
|
||||
ManageHtml string
|
||||
IndexHtml string
|
||||
Token string
|
||||
|
||||
//CheckParent bool
|
||||
//CheckDown bool
|
||||
//DavUsername string
|
||||
//DavPassword string
|
||||
//VisitorDavUsername string
|
||||
//VisitorDavPassword string
|
||||
)
|
||||
178
drivers/123/123.go
Normal file
178
drivers/123/123.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package _23
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
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 *File) *model.File {
|
||||
f := &model.File{
|
||||
Id: strconv.FormatInt(file.FileId, 10),
|
||||
Name: file.FileName,
|
||||
Size: file.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: file.UpdateAt,
|
||||
Thumbnail: file.DownloadUrl,
|
||||
}
|
||||
f.Type = file.GetType()
|
||||
return f
|
||||
}
|
||||
|
||||
func (driver Pan123) GetFiles(parentId string, account *model.Account) ([]File, error) {
|
||||
next := "0"
|
||||
res := make([]File, 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/new",
|
||||
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) (*File, 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_.([]File)
|
||||
for _, file := range parentFiles {
|
||||
if file.FileName == name {
|
||||
//if file.Type != conf.FOLDER {
|
||||
// return &file, err
|
||||
//} else {
|
||||
// return nil, base.ErrNotFile
|
||||
//}
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
//func HMAC(message string, secret string) string {
|
||||
// key := []byte(secret)
|
||||
// h := hmac.New(sha256.New, key)
|
||||
// h.Write([]byte(message))
|
||||
// // fmt.Println(h.Sum(nil))
|
||||
// //sha := hex.EncodeToString(h.Sum(nil))
|
||||
// // fmt.Println(sha)
|
||||
// //return sha
|
||||
// return string(h.Sum(nil))
|
||||
//}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Pan123{})
|
||||
}
|
||||
461
drivers/123/driver.go
Normal file
461
drivers/123/driver.go
Normal file
@@ -0,0 +1,461 @@
|
||||
package _23
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Pan123 struct{}
|
||||
|
||||
func (driver Pan123) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "123Pan",
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Pan123) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "username",
|
||||
Label: "username",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "account username/phone number",
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "password",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "account password",
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "name,fileId,updateAt,createAt",
|
||||
Required: true,
|
||||
Default: "name",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "asc,desc",
|
||||
Required: true,
|
||||
Default: "asc",
|
||||
},
|
||||
{
|
||||
Name: "bool_1",
|
||||
Label: "stream upload",
|
||||
Type: base.TypeBool,
|
||||
Description: "io stream upload (test)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Pan123) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if account.RootFolder == "" {
|
||||
account.RootFolder = "0"
|
||||
}
|
||||
err := driver.Login(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Pan123) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var rawFiles []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, len(rawFiles))
|
||||
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 !utils.IsLocalIPAddr(args.IP) {
|
||||
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()).Head(u_)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
link := base.Link{
|
||||
Url: resp.Data.DownloadUrl,
|
||||
}
|
||||
log.Debugln("res code: ", res.StatusCode())
|
||||
if res.StatusCode() == 302 {
|
||||
link.Url = res.Header().Get("location")
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Pan123) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("pan123 path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver Pan123) Proxy(r *http.Request, account *model.Account) {
|
||||
// r.Header.Del("origin")
|
||||
//}
|
||||
|
||||
func (driver Pan123) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Pan123) MakeDir(path string, account *model.Account) error {
|
||||
dir, name := filepath.Split(path)
|
||||
parentFile, err := driver.File(dir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
parentFileId, _ := strconv.Atoi(parentFile.Id)
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"etag": "",
|
||||
"fileName": name,
|
||||
"parentFileId": parentFileId,
|
||||
"size": 0,
|
||||
"type": 1,
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/upload_request",
|
||||
base.Post, nil, nil, &data, nil, false, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) Move(src string, dst string, account *model.Account) error {
|
||||
dstDir, _ := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileId, _ := strconv.Atoi(srcFile.Id)
|
||||
|
||||
dstDirFile, err := driver.File(dstDir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parentFileId, _ := strconv.Atoi(dstDirFile.Id)
|
||||
data := base.Json{
|
||||
"fileIdList": []base.Json{{"FileId": fileId}},
|
||||
"parentFileId": parentFileId,
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/mod_pid",
|
||||
base.Post, nil, nil, &data, nil, false, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) Rename(src string, dst string, account *model.Account) error {
|
||||
_, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileId, _ := strconv.Atoi(srcFile.Id)
|
||||
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"fileId": fileId,
|
||||
"fileName": dstName,
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/rename",
|
||||
base.Post, nil, nil, &data, nil, false, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Pan123) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.GetFile(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugln("delete 123 file: ", file)
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"operation": true,
|
||||
"fileTrashInfoList": []File{*file},
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/b/api/file/trash",
|
||||
base.Post, nil, nil, &data, nil, false, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Pan123) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
|
||||
const DEFAULT int64 = 10485760
|
||||
var uploadFile io.Reader
|
||||
h := md5.New()
|
||||
if account.Bool1 && file.GetSize() > uint64(DEFAULT) {
|
||||
// 只计算前10MIB
|
||||
buf := bytes.NewBuffer(make([]byte, 0, DEFAULT))
|
||||
if n, err := io.CopyN(io.MultiWriter(buf, h), file, DEFAULT); err != io.EOF && n == 0 {
|
||||
return err
|
||||
}
|
||||
// 增加额外参数防止MD5碰撞
|
||||
h.Write([]byte(file.Name))
|
||||
num := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(num, file.Size)
|
||||
h.Write(num)
|
||||
// 拼装
|
||||
uploadFile = io.MultiReader(buf, file)
|
||||
} else {
|
||||
// 计算完整文件MD5
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
|
||||
if _, err = io.Copy(io.MultiWriter(tempFile, h), file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadFile = tempFile
|
||||
}
|
||||
etag := hex.EncodeToString(h.Sum(nil))
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"duplicate": 2, // 2->覆盖 1->重命名 0->默认
|
||||
"etag": etag,
|
||||
"fileName": file.GetFileName(),
|
||||
"parentFileId": parentFile.Id,
|
||||
"size": file.GetSize(),
|
||||
"type": 0,
|
||||
}
|
||||
var resp UploadResp
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/upload_request",
|
||||
base.Post, map[string]string{"app-version": "1.1"}, nil, &data, &resp, false, account)
|
||||
//res, err := driver.Post("https://www.123pan.com/api/file/upload_request", data, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Data.Key == "" {
|
||||
return nil
|
||||
}
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),
|
||||
Region: aws.String("123pan"),
|
||||
Endpoint: aws.String("file.123pan.com"),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
}
|
||||
s, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: &resp.Data.Bucket,
|
||||
Key: &resp.Data.Key,
|
||||
Body: uploadFile,
|
||||
}
|
||||
_, err = uploader.Upload(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request("https://www.123pan.com/api/file/upload_complete", base.Post, nil, nil, &base.Json{
|
||||
"fileId": resp.Data.FileId,
|
||||
}, nil, false, account)
|
||||
return err
|
||||
}
|
||||
|
||||
//type UploadResp struct {
|
||||
// XMLName xml.Name `xml:"InitiateMultipartUploadResult"`
|
||||
// Bucket string `xml:"Bucket"`
|
||||
// Key string `xml:"Key"`
|
||||
// UploadId string `xml:"UploadId"`
|
||||
//}
|
||||
|
||||
// TODO unfinished
|
||||
//func (driver Pan123) Upload(file *model.FileStream, account *model.Account) error {
|
||||
// return base.ErrNotImplement
|
||||
// parentFile, err := driver.File(file.ParentPath, account)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if !parentFile.IsDir() {
|
||||
// return base.ErrNotFolder
|
||||
// }
|
||||
// parentFileId, _ := strconv.Atoi(parentFile.Id)
|
||||
// data := base.Json{
|
||||
// "driveId": 0,
|
||||
// "duplicate": true,
|
||||
// "etag": RandStr(32), //maybe file's md5
|
||||
// "fileName": file.GetFileName(),
|
||||
// "parentFileId": parentFileId,
|
||||
// "size": file.GetSize(),
|
||||
// "type": 0,
|
||||
// }
|
||||
// res, err := driver.Request("https://www.123pan.com/api/file/upload_request",
|
||||
// base.Post, nil, nil, &data, nil, false, account)
|
||||
// //res, err := driver.Post("https://www.123pan.com/api/file/upload_request", data, account)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// baseUrl := fmt.Sprintf("https://file.123pan.com/%s/%s", jsoniter.Get(res, "data.Bucket").ToString(), jsoniter.Get(res, "data.Key").ToString())
|
||||
// var resp UploadResp
|
||||
// kSecret := jsoniter.Get(res, "data.SecretAccessKey").ToString()
|
||||
// nowTimeStr := time.Now().String()
|
||||
// Date := strings.ReplaceAll(strings.Split(nowTimeStr, "T")[0], "-", "")
|
||||
//
|
||||
// StringToSign := fmt.Sprintf("%s\n%s\n%s\n%s",
|
||||
// "AWS4-HMAC-SHA256",
|
||||
// nowTimeStr,
|
||||
// fmt.Sprintf("%s/us-east-1/s3/aws4_request", Date),
|
||||
// )
|
||||
//
|
||||
// kDate := HMAC("AWS4"+kSecret, Date)
|
||||
// kRegion := HMAC(kDate, "us-east-1")
|
||||
// kService := HMAC(kRegion, "s3")
|
||||
// kSigning := HMAC(kService, "aws4_request")
|
||||
// _, err = base.RestyClient.R().SetResult(&resp).SetHeaders(map[string]string{
|
||||
// "Authorization": fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=%s",
|
||||
// jsoniter.Get(res, "data.AccessKeyId"),
|
||||
// Date,
|
||||
// hex.EncodeToString([]byte(HMAC(StringToSign, kSigning)))),
|
||||
// "X-Amz-Content-Sha256": "UNSIGNED-PAYLOAD",
|
||||
// "X-Amz-Date": nowTimeStr,
|
||||
// "x-amz-security-token": jsoniter.Get(res, "data.SessionToken").ToString(),
|
||||
// }).Post(fmt.Sprintf("%s?uploads", baseUrl))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return base.ErrNotImplement
|
||||
//}
|
||||
|
||||
var _ base.Driver = (*Pan123)(nil)
|
||||
74
drivers/123/types.go
Normal file
74
drivers/123/types.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package _23
|
||||
|
||||
import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
FileName string `json:"FileName"`
|
||||
Size int64 `json:"Size"`
|
||||
UpdateAt *time.Time `json:"UpdateAt"`
|
||||
FileId int64 `json:"FileId"`
|
||||
Type int `json:"Type"`
|
||||
Etag string `json:"Etag"`
|
||||
S3KeyFlag string `json:"S3KeyFlag"`
|
||||
DownloadUrl string `json:"DownloadUrl"`
|
||||
}
|
||||
|
||||
func (f File) GetSize() uint64 {
|
||||
return uint64(f.Size)
|
||||
}
|
||||
|
||||
func (f File) GetName() string {
|
||||
return f.FileName
|
||||
}
|
||||
|
||||
func (f File) GetType() int {
|
||||
if f.Type == 1 {
|
||||
return conf.FOLDER
|
||||
}
|
||||
return utils.GetFileType(path.Ext(f.FileName))
|
||||
}
|
||||
|
||||
type BaseResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Pan123TokenResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type Pan123Files struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
InfoList []File `json:"InfoList"`
|
||||
Next string `json:"Next"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type Pan123DownResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
DownloadUrl string `json:"DownloadUrl"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
AccessKeyId string `json:"AccessKeyId"`
|
||||
Bucket string `json:"Bucket"`
|
||||
Key string `json:"Key"`
|
||||
SecretAccessKey string `json:"SecretAccessKey"`
|
||||
SessionToken string `json:"SessionToken"`
|
||||
FileId int64 `json:"FileId"`
|
||||
} `json:"data"`
|
||||
}
|
||||
175
drivers/139/139.go
Normal file
175
drivers/139/139.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package _39
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (driver Cloud139) Request(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
url := "https://yun.139.com" + pathname
|
||||
req := base.RestyClient.R()
|
||||
randStr := utils.RandomStr(16)
|
||||
ts := time.Now().Format("2006-01-02 15:04:05")
|
||||
log.Debugf("%+v", data)
|
||||
body, err := utils.Json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sign := calSign(string(body), ts, randStr)
|
||||
svcType := "1"
|
||||
if isFamily(account) {
|
||||
svcType = "2"
|
||||
}
|
||||
req.SetHeaders(map[string]string{
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"CMS-DEVICE": "default",
|
||||
"Cookie": account.AccessToken,
|
||||
"mcloud-channel": "1000101",
|
||||
"mcloud-client": "10701",
|
||||
//"mcloud-route": "001",
|
||||
"mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
|
||||
//"mcloud-skey":"",
|
||||
"mcloud-version": "6.6.0",
|
||||
"Origin": "https://yun.139.com",
|
||||
"Referer": "https://yun.139.com/w/",
|
||||
"x-DeviceInfo": "||9|6.6.0|chrome|95.0.4638.69|uwIy75obnsRPIwlJSd7D9GhUvFwG96ce||macos 10.15.2||zh-CN|||",
|
||||
"x-huawei-channelSrc": "10000034",
|
||||
"x-inner-ntwk": "2",
|
||||
"x-m4c-caller": "PC",
|
||||
"x-m4c-src": "10002",
|
||||
"x-SvcType": svcType,
|
||||
})
|
||||
if headers != nil {
|
||||
req.SetHeaders(headers)
|
||||
}
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
if form != nil {
|
||||
req.SetFormData(form)
|
||||
}
|
||||
if data != nil {
|
||||
req.SetBody(data)
|
||||
}
|
||||
var e BaseResp
|
||||
//var err error
|
||||
var res *resty.Response
|
||||
req.SetResult(&e)
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
case base.Put:
|
||||
res, err = req.Put(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugln(res.String())
|
||||
if !e.Success {
|
||||
return nil, errors.New(e.Message)
|
||||
}
|
||||
if resp != nil {
|
||||
err = utils.Json.Unmarshal(res.Body(), resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (driver Cloud139) Post(pathname string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
return driver.Request(pathname, base.Post, nil, nil, nil, data, resp, account)
|
||||
}
|
||||
|
||||
func (driver Cloud139) GetFiles(catalogID string, account *model.Account) ([]model.File, error) {
|
||||
start := 0
|
||||
limit := 100
|
||||
files := make([]model.File, 0)
|
||||
for {
|
||||
data := base.Json{
|
||||
"catalogID": catalogID,
|
||||
"sortDirection": 1,
|
||||
"startNumber": start + 1,
|
||||
"endNumber": start + limit,
|
||||
"filterType": 0,
|
||||
"catalogSortType": 0,
|
||||
"contentSortType": 0,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
var resp GetDiskResp
|
||||
_, err := driver.Post("/orchestration/personalCloud/catalog/v1.0/getDisk", data, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, catalog := range resp.Data.GetDiskResult.CatalogList {
|
||||
f := model.File{
|
||||
Id: catalog.CatalogID,
|
||||
Name: catalog.CatalogName,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(catalog.UpdateTime),
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
for _, content := range resp.Data.GetDiskResult.ContentList {
|
||||
f := model.File{
|
||||
Id: content.ContentID,
|
||||
Name: content.ContentName,
|
||||
Size: content.ContentSize,
|
||||
Type: utils.GetFileType(path.Ext(content.ContentName)),
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(content.UpdateTime),
|
||||
Thumbnail: content.ThumbnailURL,
|
||||
//Thumbnail: content.BigthumbnailURL,
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
if start+limit >= resp.Data.GetDiskResult.NodeCount {
|
||||
break
|
||||
}
|
||||
start += limit
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Cloud139) GetLink(contentId string, account *model.Account) (string, error) {
|
||||
data := base.Json{
|
||||
"appName": "",
|
||||
"contentID": contentId,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
res, err := driver.Post("/orchestration/personalCloud/uploadAndDownload/v1.0/downloadRequest",
|
||||
data, nil, account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Cloud139{})
|
||||
}
|
||||
457
drivers/139/driver.go
Normal file
457
drivers/139/driver.go
Normal file
@@ -0,0 +1,457 @@
|
||||
package _39
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Cloud139 struct{}
|
||||
|
||||
func (driver Cloud139) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "139Yun",
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Cloud139) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "username",
|
||||
Label: "phone",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "phone number",
|
||||
},
|
||||
{
|
||||
Name: "access_token",
|
||||
Label: "Cookie",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "Unknown expiration time",
|
||||
},
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "139yun type",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "Personal,Family",
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "site_id",
|
||||
Label: "cloud_id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Cloud139) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := driver.Request("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Post, nil, nil, nil, base.Json{
|
||||
"qryUserExternInfoReq": base.Json{
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
},
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Cloud139) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var files []model.File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ = cache.([]model.File)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isFamily(account) {
|
||||
files, err = driver.familyGetFiles(file.Id, account)
|
||||
} else {
|
||||
files, err = driver.GetFiles(file.Id, account)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Cloud139) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var u string
|
||||
//if isFamily(account) {
|
||||
// u, err = driver.familyLink(file.Id, account)
|
||||
//} else {
|
||||
u, err = driver.GetLink(file.Id, account)
|
||||
//}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{Url: u}, nil
|
||||
}
|
||||
|
||||
func (driver Cloud139) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("139 path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver Cloud139) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver Cloud139) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Cloud139) MakeDir(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"createCatalogExtReq": base.Json{
|
||||
"parentCatalogID": parentFile.Id,
|
||||
"newCatalogName": utils.Base(path),
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt"
|
||||
if isFamily(account) {
|
||||
data = base.Json{
|
||||
"cloudID": account.SiteId,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
"docLibName": utils.Base(path),
|
||||
}
|
||||
pathname = "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc"
|
||||
}
|
||||
_, err = driver.Post(pathname,
|
||||
data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) Move(src string, dst string, account *model.Account) error {
|
||||
if isFamily(account) {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstParentFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var contentInfoList []string
|
||||
var catalogInfoList []string
|
||||
if srcFile.IsDir() {
|
||||
catalogInfoList = append(catalogInfoList, srcFile.Id)
|
||||
} else {
|
||||
contentInfoList = append(contentInfoList, srcFile.Id)
|
||||
}
|
||||
data := base.Json{
|
||||
"createBatchOprTaskReq": base.Json{
|
||||
"taskType": 3,
|
||||
"actionType": "304",
|
||||
"taskInfo": base.Json{
|
||||
"contentInfoList": contentInfoList,
|
||||
"catalogInfoList": catalogInfoList,
|
||||
"newCatalogID": dstParentFile.Id,
|
||||
},
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
|
||||
_, err = driver.Post(pathname, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) Rename(src string, dst string, account *model.Account) error {
|
||||
if isFamily(account) {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var data base.Json
|
||||
var pathname string
|
||||
if srcFile.IsDir() {
|
||||
data = base.Json{
|
||||
"catalogID": srcFile.Id,
|
||||
"catalogName": utils.Base(dst),
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo"
|
||||
} else {
|
||||
data = base.Json{
|
||||
"contentID": srcFile.Id,
|
||||
"contentName": utils.Base(dst),
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo"
|
||||
}
|
||||
_, err = driver.Post(pathname, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) Copy(src string, dst string, account *model.Account) error {
|
||||
if isFamily(account) {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstParentFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var contentInfoList []string
|
||||
var catalogInfoList []string
|
||||
if srcFile.IsDir() {
|
||||
catalogInfoList = append(catalogInfoList, srcFile.Id)
|
||||
} else {
|
||||
contentInfoList = append(contentInfoList, srcFile.Id)
|
||||
}
|
||||
data := base.Json{
|
||||
"createBatchOprTaskReq": base.Json{
|
||||
"taskType": 3,
|
||||
"actionType": 309,
|
||||
"taskInfo": base.Json{
|
||||
"contentInfoList": contentInfoList,
|
||||
"catalogInfoList": catalogInfoList,
|
||||
"newCatalogID": dstParentFile.Id,
|
||||
},
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
|
||||
_, err = driver.Post(pathname, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var contentInfoList []string
|
||||
var catalogInfoList []string
|
||||
if file.IsDir() {
|
||||
catalogInfoList = append(catalogInfoList, file.Id)
|
||||
} else {
|
||||
contentInfoList = append(contentInfoList, file.Id)
|
||||
}
|
||||
data := base.Json{
|
||||
"createBatchOprTaskReq": base.Json{
|
||||
"taskType": 2,
|
||||
"actionType": 201,
|
||||
"taskInfo": base.Json{
|
||||
"newCatalogID": "",
|
||||
"contentInfoList": contentInfoList,
|
||||
"catalogInfoList": catalogInfoList,
|
||||
},
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
|
||||
if isFamily(account) {
|
||||
data = base.Json{
|
||||
"catalogList": catalogInfoList,
|
||||
"contentList": contentInfoList,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
"sourceCatalogType": 1002,
|
||||
"taskType": 2,
|
||||
}
|
||||
pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask"
|
||||
}
|
||||
_, err = driver.Post(pathname, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud139) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
data := base.Json{
|
||||
"manualRename": 2,
|
||||
"operation": 0,
|
||||
"fileCount": 1,
|
||||
"totalSize": file.Size,
|
||||
"uploadContentList": []base.Json{{
|
||||
"contentName": file.Name,
|
||||
"contentSize": file.Size,
|
||||
// "digest": "5a3231986ce7a6b46e408612d385bafa"
|
||||
}},
|
||||
"parentCatalogID": parentFile.Id,
|
||||
"newCatalogName": "",
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
|
||||
if isFamily(account) {
|
||||
data = newJson(base.Json{
|
||||
"fileCount": 1,
|
||||
"manualRename": 2,
|
||||
"operation": 0,
|
||||
"path": "",
|
||||
"seqNo": "",
|
||||
"totalSize": file.Size,
|
||||
"uploadContentList": []base.Json{{
|
||||
"contentName": file.Name,
|
||||
"contentSize": file.Size,
|
||||
// "digest": "5a3231986ce7a6b46e408612d385bafa"
|
||||
}},
|
||||
}, account)
|
||||
pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL"
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
var resp UploadResp
|
||||
_, err = driver.Post(pathname, data, &resp, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var Default uint64 = 10485760
|
||||
part := int(math.Ceil(float64(file.Size) / float64(Default)))
|
||||
var start uint64 = 0
|
||||
for i := 0; i < part; i++ {
|
||||
byteSize := file.Size - start
|
||||
if byteSize > Default {
|
||||
byteSize = Default
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
_, err = io.ReadFull(file, byteData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, bytes.NewBuffer(byteData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headers := map[string]string{
|
||||
"Accept": "*/*",
|
||||
"Content-Type": "text/plain;name=" + unicode(file.Name),
|
||||
"contentSize": strconv.FormatUint(file.Size, 10),
|
||||
"range": fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1),
|
||||
"content-length": strconv.FormatUint(byteSize, 10),
|
||||
"uploadtaskID": resp.Data.UploadResult.UploadTaskID,
|
||||
"rangeType": "0",
|
||||
"Referer": "https://yun.139.com/",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.44",
|
||||
"x-SvcType": "1",
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("%+v", res)
|
||||
res.Body.Close()
|
||||
start += byteSize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Cloud139)(nil)
|
||||
74
drivers/139/family.go
Normal file
74
drivers/139/family.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package _39
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"path"
|
||||
)
|
||||
|
||||
func (driver Cloud139) familyGetFiles(catalogID string, account *model.Account) ([]model.File, error) {
|
||||
pageNum := 1
|
||||
files := make([]model.File, 0)
|
||||
for {
|
||||
data := newJson(base.Json{
|
||||
"catalogID": catalogID,
|
||||
"contentSortType": 0,
|
||||
"pageInfo": base.Json{
|
||||
"pageNum": pageNum,
|
||||
"pageSize": 100,
|
||||
},
|
||||
"sortDirection": 1,
|
||||
}, account)
|
||||
|
||||
var resp QueryContentListResp
|
||||
_, err := driver.Post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, catalog := range resp.Data.CloudCatalogList {
|
||||
f := model.File{
|
||||
Id: catalog.CatalogID,
|
||||
Name: catalog.CatalogName,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(catalog.LastUpdateTime),
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
for _, content := range resp.Data.CloudContentList {
|
||||
f := model.File{
|
||||
Id: content.ContentID,
|
||||
Name: content.ContentName,
|
||||
Size: content.ContentSize,
|
||||
Type: utils.GetFileType(path.Ext(content.ContentName)),
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(content.LastUpdateTime),
|
||||
Thumbnail: content.ThumbnailURL,
|
||||
//Thumbnail: content.BigthumbnailURL,
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
if 100*pageNum > resp.Data.TotalCount {
|
||||
break
|
||||
}
|
||||
pageNum++
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Cloud139) familyLink(contentId string, account *model.Account) (string, error) {
|
||||
data := newJson(base.Json{
|
||||
"contentID": contentId,
|
||||
//"path":"",
|
||||
}, account)
|
||||
res, err := driver.Post("/orchestration/familyCloud/content/v1.0/getFileDownLoadURL",
|
||||
data, nil, account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
|
||||
}
|
||||
@@ -1,15 +1,4 @@
|
||||
package _139
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
const (
|
||||
MetaPersonal string = "personal"
|
||||
MetaFamily string = "family"
|
||||
MetaGroup string = "group"
|
||||
MetaPersonalNew string = "personal_new"
|
||||
)
|
||||
package _39
|
||||
|
||||
type BaseResp struct {
|
||||
Success bool `json:"success"`
|
||||
@@ -21,7 +10,7 @@ type Catalog struct {
|
||||
CatalogID string `json:"catalogID"`
|
||||
CatalogName string `json:"catalogName"`
|
||||
//CatalogType int `json:"catalogType"`
|
||||
CreateTime string `json:"createTime"`
|
||||
//CreateTime string `json:"createTime"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
//IsShared bool `json:"isShared"`
|
||||
//CatalogLevel int `json:"catalogLevel"`
|
||||
@@ -55,7 +44,6 @@ type Content struct {
|
||||
//ContentDesc string `json:"contentDesc"`
|
||||
//ContentType int `json:"contentType"`
|
||||
//ContentOrigin int `json:"contentOrigin"`
|
||||
CreateTime string `json:"createTime"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
//CommentCount int `json:"commentCount"`
|
||||
ThumbnailURL string `json:"thumbnailURL"`
|
||||
@@ -75,7 +63,7 @@ type Content struct {
|
||||
//ParentCatalogID string `json:"parentCatalogId"`
|
||||
//Channel string `json:"channel"`
|
||||
//GeoLocFlag string `json:"geoLocFlag"`
|
||||
Digest string `json:"digest"`
|
||||
//Digest string `json:"digest"`
|
||||
//Version string `json:"version"`
|
||||
//FileEtag string `json:"fileEtag"`
|
||||
//FileVersion string `json:"fileVersion"`
|
||||
@@ -143,13 +131,6 @@ type UploadResp struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type InterLayerUploadResult struct {
|
||||
XMLName xml.Name `xml:"result"`
|
||||
Text string `xml:",chardata"`
|
||||
ResultCode int `xml:"resultCode"`
|
||||
Msg string `xml:"msg"`
|
||||
}
|
||||
|
||||
type CloudContent struct {
|
||||
ContentID string `json:"contentID"`
|
||||
//Modifier string `json:"modifier"`
|
||||
@@ -160,7 +141,7 @@ type CloudContent struct {
|
||||
//ContentSuffix string `json:"contentSuffix"`
|
||||
ContentSize int64 `json:"contentSize"`
|
||||
//ContentDesc string `json:"contentDesc"`
|
||||
CreateTime string `json:"createTime"`
|
||||
//CreateTime string `json:"createTime"`
|
||||
//Shottime interface{} `json:"shottime"`
|
||||
LastUpdateTime string `json:"lastUpdateTime"`
|
||||
ThumbnailURL string `json:"thumbnailURL"`
|
||||
@@ -184,7 +165,7 @@ type CloudCatalog struct {
|
||||
CatalogID string `json:"catalogID"`
|
||||
CatalogName string `json:"catalogName"`
|
||||
//CloudID string `json:"cloudID"`
|
||||
CreateTime string `json:"createTime"`
|
||||
//CreateTime string `json:"createTime"`
|
||||
LastUpdateTime string `json:"lastUpdateTime"`
|
||||
//Creator string `json:"creator"`
|
||||
//CreatorNickname string `json:"creatorNickname"`
|
||||
@@ -204,111 +185,3 @@ type QueryContentListResp struct {
|
||||
RecallContent interface{} `json:"recallContent"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type QueryGroupContentListResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Result struct {
|
||||
ResultCode string `json:"resultCode"`
|
||||
ResultDesc string `json:"resultDesc"`
|
||||
} `json:"result"`
|
||||
GetGroupContentResult struct {
|
||||
ParentCatalogID string `json:"parentCatalogID"` // 根目录是"0"
|
||||
CatalogList []struct {
|
||||
Catalog
|
||||
Path string `json:"path"`
|
||||
} `json:"catalogList"`
|
||||
ContentList []Content `json:"contentList"`
|
||||
NodeCount int `json:"nodeCount"` // 文件+文件夹数量
|
||||
CtlgCnt int `json:"ctlgCnt"` // 文件夹数量
|
||||
ContCnt int `json:"contCnt"` // 文件数量
|
||||
} `json:"getGroupContentResult"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ParallelHashCtx struct {
|
||||
PartOffset int64 `json:"partOffset"`
|
||||
}
|
||||
|
||||
type PartInfo struct {
|
||||
PartNumber int64 `json:"partNumber"`
|
||||
PartSize int64 `json:"partSize"`
|
||||
ParallelHashCtx ParallelHashCtx `json:"parallelHashCtx"`
|
||||
}
|
||||
|
||||
type PersonalThumbnail struct {
|
||||
Style string `json:"style"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type PersonalFileItem struct {
|
||||
FileId string `json:"fileId"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Type string `json:"type"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Thumbnails []PersonalThumbnail `json:"thumbnailUrls"`
|
||||
}
|
||||
|
||||
type PersonalListResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Items []PersonalFileItem `json:"items"`
|
||||
NextPageCursor string `json:"nextPageCursor"`
|
||||
}
|
||||
}
|
||||
|
||||
type PersonalPartInfo struct {
|
||||
PartNumber int `json:"partNumber"`
|
||||
UploadUrl string `json:"uploadUrl"`
|
||||
}
|
||||
|
||||
type PersonalUploadResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
FileId string `json:"fileId"`
|
||||
FileName string `json:"fileName"`
|
||||
PartInfos []PersonalPartInfo `json:"partInfos"`
|
||||
Exist bool `json:"exist"`
|
||||
RapidUpload bool `json:"rapidUpload"`
|
||||
UploadId string `json:"uploadId"`
|
||||
}
|
||||
}
|
||||
|
||||
type PersonalUploadUrlResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
FileId string `json:"fileId"`
|
||||
UploadId string `json:"uploadId"`
|
||||
PartInfos []PersonalPartInfo `json:"partInfos"`
|
||||
}
|
||||
}
|
||||
|
||||
type QueryRoutePolicyResp struct {
|
||||
Success bool `json:"success"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
RoutePolicyList []struct {
|
||||
SiteID string `json:"siteID"`
|
||||
SiteCode string `json:"siteCode"`
|
||||
ModName string `json:"modName"`
|
||||
HttpUrl string `json:"httpUrl"`
|
||||
HttpsUrl string `json:"httpsUrl"`
|
||||
EnvID string `json:"envID"`
|
||||
ExtInfo string `json:"extInfo"`
|
||||
HashName string `json:"hashName"`
|
||||
ModAddrType int `json:"modAddrType"`
|
||||
} `json:"routePolicyList"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type RefreshTokenResp struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Return string `xml:"return"`
|
||||
Token string `xml:"token"`
|
||||
Expiretime int32 `xml:"expiretime"`
|
||||
AccessToken string `xml:"accessToken"`
|
||||
Desc string `xml:"desc"`
|
||||
}
|
||||
70
drivers/139/util.go
Normal file
70
drivers/139/util.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package _39
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func encodeURIComponent(str string) string {
|
||||
r := url.QueryEscape(str)
|
||||
r = strings.Replace(r, "+", "%20", -1)
|
||||
return r
|
||||
}
|
||||
|
||||
func calSign(body, ts, randStr string) string {
|
||||
body = strings.ReplaceAll(body, "\n", "")
|
||||
body = strings.ReplaceAll(body, " ", "")
|
||||
body = encodeURIComponent(body)
|
||||
strs := strings.Split(body, "")
|
||||
sort.Strings(strs)
|
||||
body = strings.Join(strs, "")
|
||||
body = base64.StdEncoding.EncodeToString([]byte(body))
|
||||
res := utils.GetMD5Encode(body) + utils.GetMD5Encode(ts+":"+randStr)
|
||||
res = strings.ToUpper(utils.GetMD5Encode(res))
|
||||
return res
|
||||
}
|
||||
|
||||
func getTime(t string) *time.Time {
|
||||
stamp, _ := time.ParseInLocation("20060102150405", t, time.Local)
|
||||
return &stamp
|
||||
}
|
||||
|
||||
func isFamily(account *model.Account) bool {
|
||||
return account.InternalType == "Family"
|
||||
}
|
||||
|
||||
func unicode(str string) string {
|
||||
textQuoted := strconv.QuoteToASCII(str)
|
||||
textUnquoted := textQuoted[1 : len(textQuoted)-1]
|
||||
return textUnquoted
|
||||
}
|
||||
|
||||
func MergeMap(mObj ...map[string]interface{}) map[string]interface{} {
|
||||
newObj := map[string]interface{}{}
|
||||
for _, m := range mObj {
|
||||
for k, v := range m {
|
||||
newObj[k] = v
|
||||
}
|
||||
}
|
||||
return newObj
|
||||
}
|
||||
|
||||
func newJson(data map[string]interface{}, account *model.Account) map[string]interface{} {
|
||||
common := map[string]interface{}{
|
||||
"catalogType": 3,
|
||||
"cloudID": account.SiteId,
|
||||
"cloudType": 1,
|
||||
"commonAccountInfo": base.Json{
|
||||
"account": account.Username,
|
||||
"accountType": 1,
|
||||
},
|
||||
}
|
||||
return MergeMap(data, common)
|
||||
}
|
||||
620
drivers/189/189.go
Normal file
620
drivers/189/189.go
Normal file
@@ -0,0 +1,620 @@
|
||||
package _89
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var client189Map map[string]*resty.Client
|
||||
var infoMap = make(map[string]Rsa)
|
||||
|
||||
func (driver Cloud189) getClient(account *model.Account) (*resty.Client, error) {
|
||||
client, ok := client189Map[account.Name]
|
||||
if ok {
|
||||
return client, nil
|
||||
}
|
||||
err := driver.Login(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, ok = client189Map[account.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find [%s] client", account.Name)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) FormatFile(file *Cloud189File) *model.File {
|
||||
f := &model.File{
|
||||
Id: strconv.FormatInt(file.Id, 10),
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: nil,
|
||||
Thumbnail: file.Icon.SmallUrl,
|
||||
Url: file.Url,
|
||||
}
|
||||
loc, _ := time.LoadLocation("Local")
|
||||
lastOpTime, err := time.ParseInLocation("2006-01-02 15:04:05", file.LastOpTime, loc)
|
||||
if err == nil {
|
||||
f.UpdatedAt = &lastOpTime
|
||||
}
|
||||
if file.Size == -1 {
|
||||
f.Size = 0
|
||||
}
|
||||
f.Type = file.GetType()
|
||||
return f
|
||||
}
|
||||
|
||||
//func (c Cloud189) GetFile(path string, account *model.Account) (*Cloud189File, error) {
|
||||
// dir, name := filepath.Split(path)
|
||||
// dir = utils.ParsePath(dir)
|
||||
// _, _, err := c.ParentPath(dir, account)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// parentFiles_, _ := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, dir))
|
||||
// parentFiles, _ := parentFiles_.([]Cloud189File)
|
||||
// for _, file := range parentFiles {
|
||||
// if file.Name == name {
|
||||
// if file.Size != -1 {
|
||||
// return &file, err
|
||||
// } else {
|
||||
// return nil, ErrNotFile
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return nil, ErrPathNotFound
|
||||
//}
|
||||
|
||||
type LoginResp struct {
|
||||
Msg string `json:"msg"`
|
||||
Result int `json:"result"`
|
||||
ToUrl string `json:"toUrl"`
|
||||
}
|
||||
|
||||
// Login refer to PanIndex
|
||||
func (driver Cloud189) Login(account *model.Account) error {
|
||||
client := resty.New()
|
||||
//client.SetCookieJar(cookieJar)
|
||||
client.SetTimeout(base.DefaultTimeout)
|
||||
client.SetRetryCount(3)
|
||||
client.SetHeader("Referer", "https://cloud.189.cn/")
|
||||
client.SetHeader("User-Agent", base.UserAgent)
|
||||
url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
|
||||
b := ""
|
||||
lt := ""
|
||||
ltText := regexp.MustCompile(`lt = "(.+?)"`)
|
||||
var res *resty.Response
|
||||
var err error
|
||||
for i := 0; i < 3; i++ {
|
||||
res, err = client.R().Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 已经登陆
|
||||
if res.RawResponse.Request.URL.String() == "https://cloud.189.cn/web/main" {
|
||||
return nil
|
||||
}
|
||||
b = res.String()
|
||||
ltTextArr := ltText.FindStringSubmatch(b)
|
||||
if len(ltTextArr) > 0 {
|
||||
lt = ltTextArr[1]
|
||||
break
|
||||
} else {
|
||||
<-time.After(time.Second)
|
||||
}
|
||||
}
|
||||
if lt == "" {
|
||||
return fmt.Errorf("get page: %s \nstatus: %d \nrequest url: %s\nredirect url: %s",
|
||||
b, res.StatusCode(), res.RawResponse.Request.URL.String(), res.Header().Get("location"))
|
||||
}
|
||||
captchaToken := regexp.MustCompile(`captchaToken' value='(.+?)'`).FindStringSubmatch(b)[1]
|
||||
returnUrl := regexp.MustCompile(`returnUrl = '(.+?)'`).FindStringSubmatch(b)[1]
|
||||
paramId := regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(b)[1]
|
||||
//reqId := regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(b)[1]
|
||||
jRsakey := regexp.MustCompile(`j_rsaKey" value="(\S+)"`).FindStringSubmatch(b)[1]
|
||||
vCodeID := regexp.MustCompile(`picCaptcha\.do\?token\=([A-Za-z0-9\&\=]+)`).FindStringSubmatch(b)[1]
|
||||
vCodeRS := ""
|
||||
if vCodeID != "" {
|
||||
// need ValidateCode
|
||||
log.Debugf("try to identify verification codes")
|
||||
timeStamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
|
||||
u := "https://open.e.189.cn/api/logbox/oauth2/picCaptcha.do?token=" + vCodeID + timeStamp
|
||||
imgRes, err := client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/76.0",
|
||||
"Referer": "https://open.e.189.cn/api/logbox/oauth2/unifyAccountLogin.do",
|
||||
"Sec-Fetch-Dest": "image",
|
||||
"Sec-Fetch-Mode": "no-cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
}).Get(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vRes, err := client.R().SetMultipartField(
|
||||
"image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
|
||||
Post(conf.GetStr("ocr api"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 {
|
||||
return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString())
|
||||
}
|
||||
vCodeRS = jsoniter.Get(vRes.Body(), "result").ToString()
|
||||
log.Debugln("code: ", vCodeRS)
|
||||
}
|
||||
userRsa := RsaEncode([]byte(account.Username), jRsakey, true)
|
||||
passwordRsa := RsaEncode([]byte(account.Password), jRsakey, true)
|
||||
url = "https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do"
|
||||
var loginResp LoginResp
|
||||
res, err = client.R().
|
||||
SetHeaders(map[string]string{
|
||||
"lt": lt,
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
|
||||
"Referer": "https://open.e.189.cn/",
|
||||
"accept": "application/json;charset=UTF-8",
|
||||
}).SetFormData(map[string]string{
|
||||
"appKey": "cloud",
|
||||
"accountType": "01",
|
||||
"userName": "{RSA}" + userRsa,
|
||||
"password": "{RSA}" + passwordRsa,
|
||||
"validateCode": vCodeRS,
|
||||
"captchaToken": captchaToken,
|
||||
"returnUrl": returnUrl,
|
||||
"mailSuffix": "@pan.cn",
|
||||
"paramId": paramId,
|
||||
"clientType": "10010",
|
||||
"dynamicCheck": "FALSE",
|
||||
"cb_SaveName": "1",
|
||||
"isOauth2": "false",
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = utils.Json.Unmarshal(res.Body(), &loginResp)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
if loginResp.Result != 0 {
|
||||
return fmt.Errorf(loginResp.Msg)
|
||||
}
|
||||
_, err = client.R().Get(loginResp.ToUrl)
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
return err
|
||||
}
|
||||
client189Map[account.Name] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) isFamily(account *model.Account) bool {
|
||||
return account.InternalType == "Family"
|
||||
}
|
||||
|
||||
func (driver Cloud189) GetFiles(fileId string, account *model.Account) ([]Cloud189File, error) {
|
||||
res := make([]Cloud189File, 0)
|
||||
pageNum := 1
|
||||
|
||||
for {
|
||||
var resp Cloud189Files
|
||||
body, err := driver.Request("https://cloud.189.cn/api/open/file/listFiles.action", base.Get, map[string]string{
|
||||
//"noCache": random(),
|
||||
"pageSize": "60",
|
||||
"pageNum": strconv.Itoa(pageNum),
|
||||
"mediaType": "0",
|
||||
"folderId": fileId,
|
||||
"iconOption": "5",
|
||||
"orderBy": "lastOpTime", //account.OrderBy
|
||||
"descending": "true", //account.OrderDirection
|
||||
}, nil, nil, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = utils.Json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.ResCode != 0 {
|
||||
return nil, fmt.Errorf(resp.ResMessage)
|
||||
}
|
||||
if resp.FileListAO.Count == 0 {
|
||||
break
|
||||
}
|
||||
for _, folder := range resp.FileListAO.FolderList {
|
||||
res = append(res, Cloud189File{
|
||||
Id: folder.Id,
|
||||
LastOpTime: folder.LastOpTime,
|
||||
Name: folder.Name,
|
||||
Size: -1,
|
||||
})
|
||||
}
|
||||
res = append(res, resp.FileListAO.FileList...)
|
||||
pageNum++
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) Request(url string, method int, query, form map[string]string, headers map[string]string, account *model.Account) ([]byte, error) {
|
||||
client, err := driver.getClient(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//var resp base.Json
|
||||
if driver.isFamily(account) {
|
||||
url = strings.Replace(url, "/api/open", "/api/open/family", 1)
|
||||
if query != nil {
|
||||
query["familyId"] = account.SiteId
|
||||
}
|
||||
if form != nil {
|
||||
form["familyId"] = account.SiteId
|
||||
}
|
||||
}
|
||||
var e Cloud189Error
|
||||
req := client.R().SetError(&e).
|
||||
SetHeader("Accept", "application/json;charset=UTF-8").
|
||||
SetQueryParams(map[string]string{
|
||||
"noCache": random(),
|
||||
})
|
||||
if query != nil {
|
||||
req = req.SetQueryParams(query)
|
||||
}
|
||||
if form != nil {
|
||||
req = req.SetFormData(form)
|
||||
}
|
||||
if headers != nil {
|
||||
req = req.SetHeaders(headers)
|
||||
}
|
||||
var res *resty.Response
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//log.Debug(res.String())
|
||||
if e.ErrorCode != "" {
|
||||
if e.ErrorCode == "InvalidSessionKey" {
|
||||
err = driver.Login(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return driver.Request(url, method, query, form, nil, account)
|
||||
}
|
||||
}
|
||||
if jsoniter.Get(res.Body(), "res_code").ToInt() != 0 {
|
||||
err = errors.New(jsoniter.Get(res.Body(), "res_message").ToString())
|
||||
}
|
||||
return res.Body(), err
|
||||
}
|
||||
|
||||
func (driver Cloud189) GetSessionKey(account *model.Account) (string, error) {
|
||||
//info, ok := infoMap[account.Name]
|
||||
//if !ok {
|
||||
// info = Info{}
|
||||
// infoMap[account.Name] = info
|
||||
//} else {
|
||||
// log.Debugf("hit")
|
||||
//}
|
||||
//if info.SessionKey != "" {
|
||||
// return info.SessionKey, nil
|
||||
//}
|
||||
resp, err := driver.Request("https://cloud.189.cn/v2/getUserBriefInfo.action", base.Get, nil, nil, nil, account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sessionKey := jsoniter.Get(resp, "sessionKey").ToString()
|
||||
//info.SessionKey = sessionKey
|
||||
return sessionKey, nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) GetResKey(account *model.Account) (string, string, error) {
|
||||
rsa, ok := infoMap[account.Name]
|
||||
if !ok {
|
||||
rsa = Rsa{}
|
||||
infoMap[account.Name] = rsa
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
if rsa.Expire > now {
|
||||
return rsa.PubKey, rsa.PkId, nil
|
||||
}
|
||||
resp, err := driver.Request("https://cloud.189.cn/api/security/generateRsaKey.action", base.Get, nil, nil, nil, account)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
pubKey, pkId := jsoniter.Get(resp, "pubKey").ToString(), jsoniter.Get(resp, "pkId").ToString()
|
||||
rsa.PubKey, rsa.PkId = pubKey, pkId
|
||||
rsa.Expire = jsoniter.Get(resp, "expire").ToInt64()
|
||||
return pubKey, pkId, nil
|
||||
}
|
||||
|
||||
//func (driver Cloud189) UploadRequest1(uri string, form map[string]string, account *model.Account, resp interface{}) ([]byte, error) {
|
||||
// //sessionKey, err := driver.GetSessionKey(account)
|
||||
// //if err != nil {
|
||||
// // return nil, err
|
||||
// //}
|
||||
// sessionKey := account.DriveId
|
||||
// pubKey, pkId, err := driver.GetResKey(account)
|
||||
// log.Debugln(sessionKey, pubKey, pkId)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// xRId := Random("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx")
|
||||
// pkey := Random("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx")[0 : 16+int(16*mathRand.Float32())]
|
||||
// params := hex.EncodeToString(AesEncrypt([]byte(qs(form)), []byte(pkey[0:16])))
|
||||
// date := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
// a := make(url.Values)
|
||||
// a.Set("SessionKey", sessionKey)
|
||||
// a.Set("Operate", http.MethodGet)
|
||||
// a.Set("RequestURI", uri)
|
||||
// a.Set("Date", date)
|
||||
// a.Set("params", params)
|
||||
// signature := hex.EncodeToString(SHA1(EncodeParam(a), pkey))
|
||||
// encryptionText := RsaEncode([]byte(pkey), pubKey, false)
|
||||
// headers := map[string]string{
|
||||
// "signature": signature,
|
||||
// "sessionKey": sessionKey,
|
||||
// "encryptionText": encryptionText,
|
||||
// "pkId": pkId,
|
||||
// "x-request-id": xRId,
|
||||
// "x-request-date": date,
|
||||
// }
|
||||
// req := base.RestyClient.R().SetHeaders(headers).SetQueryParam("params", params)
|
||||
// if resp != nil {
|
||||
// req.SetResult(resp)
|
||||
// }
|
||||
// res, err := req.Get("https://upload.cloud.189.cn" + uri)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// //log.Debug(res.String())
|
||||
// data := res.Body()
|
||||
// if jsoniter.Get(data, "code").ToString() != "SUCCESS" {
|
||||
// return nil, errors.New(uri + "---" + jsoniter.Get(data, "msg").ToString())
|
||||
// }
|
||||
// return data, nil
|
||||
//}
|
||||
//
|
||||
//func (driver Cloud189) UploadRequest2(uri string, form map[string]string, account *model.Account, resp interface{}) ([]byte, error) {
|
||||
// c := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
// r := Random("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx")
|
||||
// l := Random("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx")
|
||||
// l = l[0 : 16+int(16*mathRand.Float32())]
|
||||
//
|
||||
// e := qs(form)
|
||||
// data := AesEncrypt([]byte(e), []byte(l[0:16]))
|
||||
// h := hex.EncodeToString(data)
|
||||
//
|
||||
// sessionKey := account.DriveId
|
||||
// a := make(url.Values)
|
||||
// a.Set("SessionKey", sessionKey)
|
||||
// a.Set("Operate", http.MethodGet)
|
||||
// a.Set("RequestURI", uri)
|
||||
// a.Set("Date", c)
|
||||
// a.Set("params", h)
|
||||
// g := SHA1(EncodeParam(a), l)
|
||||
//
|
||||
// pubKey, pkId, err := driver.GetResKey(account)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// b := RsaEncode([]byte(l), pubKey, false)
|
||||
// client, err := driver.getClient(account)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// req := client.R()
|
||||
// req.Header.Set("accept", "application/json;charset=UTF-8")
|
||||
// req.Header.Set("SessionKey", sessionKey)
|
||||
// req.Header.Set("Signature", hex.EncodeToString(g))
|
||||
// req.Header.Set("X-Request-Date", c)
|
||||
// req.Header.Set("X-Request-ID", r)
|
||||
// req.Header.Set("EncryptionText", b)
|
||||
// req.Header.Set("PkId", pkId)
|
||||
// if resp != nil {
|
||||
// req.SetResult(resp)
|
||||
// }
|
||||
// res, err := req.Get("https://upload.cloud.189.cn" + uri + "?params=" + h)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// //log.Debug(res.String())
|
||||
// data = res.Body()
|
||||
// if jsoniter.Get(data, "code").ToString() != "SUCCESS" {
|
||||
// return nil, errors.New(uri + "---" + jsoniter.Get(data, "msg").ToString())
|
||||
// }
|
||||
// return data, nil
|
||||
//}
|
||||
|
||||
func (driver Cloud189) UploadRequest(uri string, form map[string]string, account *model.Account, resp interface{}) ([]byte, error) {
|
||||
c := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
r := Random("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx")
|
||||
l := Random("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx")
|
||||
l = l[0 : 16+int(16*utils.Rand.Float32())]
|
||||
|
||||
e := qs(form)
|
||||
data := AesEncrypt([]byte(e), []byte(l[0:16]))
|
||||
h := hex.EncodeToString(data)
|
||||
|
||||
sessionKey := account.DriveId
|
||||
signature := hmacSha1(fmt.Sprintf("SessionKey=%s&Operate=GET&RequestURI=%s&Date=%s¶ms=%s", sessionKey, uri, c, h), l)
|
||||
|
||||
pubKey, pkId, err := driver.GetResKey(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := RsaEncode([]byte(l), pubKey, false)
|
||||
client, err := driver.getClient(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := client.R()
|
||||
req.Header.Set("accept", "application/json;charset=UTF-8")
|
||||
req.Header.Set("SessionKey", sessionKey)
|
||||
req.Header.Set("Signature", signature)
|
||||
req.Header.Set("X-Request-Date", c)
|
||||
req.Header.Set("X-Request-ID", r)
|
||||
req.Header.Set("EncryptionText", b)
|
||||
req.Header.Set("PkId", pkId)
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
res, err := req.Get("https://upload.cloud.189.cn" + uri + "?params=" + h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//log.Debug(res.String())
|
||||
data = res.Body()
|
||||
if jsoniter.Get(data, "code").ToString() != "SUCCESS" {
|
||||
return nil, errors.New(uri + "---" + jsoniter.Get(data, "msg").ToString())
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// NewUpload Error: signature check false
|
||||
func (driver Cloud189) NewUpload(file *model.FileStream, account *model.Account) error {
|
||||
sessionKey, err := driver.GetSessionKey(account)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
} else {
|
||||
account.Status = "work"
|
||||
account.DriveId = sessionKey
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
const DEFAULT uint64 = 10485760
|
||||
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
|
||||
var finish uint64 = 0
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
res, err := driver.UploadRequest("/person/initMultiUpload", map[string]string{
|
||||
"parentFolderId": parentFile.Id,
|
||||
"fileName": encode(file.Name),
|
||||
"fileSize": strconv.FormatInt(int64(file.Size), 10),
|
||||
"sliceSize": strconv.FormatInt(int64(DEFAULT), 10),
|
||||
"lazyCheck": "1",
|
||||
}, account, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadFileId := jsoniter.Get(res, "data", "uploadFileId").ToString()
|
||||
//_, err = driver.UploadRequest("/person/getUploadedPartsInfo", map[string]string{
|
||||
// "uploadFileId": uploadFileId,
|
||||
//}, account, nil)
|
||||
var i int64
|
||||
var byteSize uint64
|
||||
md5s := make([]string, 0)
|
||||
md5Sum := md5.New()
|
||||
for i = 1; i <= count; i++ {
|
||||
byteSize = file.GetSize() - finish
|
||||
if DEFAULT < byteSize {
|
||||
byteSize = DEFAULT
|
||||
}
|
||||
//log.Debugf("%d,%d", byteSize, finish)
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(file, byteData)
|
||||
//log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
finish += uint64(n)
|
||||
md5Bytes := getMd5(byteData)
|
||||
md5Hex := hex.EncodeToString(md5Bytes)
|
||||
md5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)
|
||||
md5s = append(md5s, strings.ToUpper(md5Hex))
|
||||
md5Sum.Write(byteData)
|
||||
//log.Debugf("md5Bytes: %+v,md5Str:%s,md5Base64:%s", md5Bytes, md5Hex, md5Base64)
|
||||
var resp UploadUrlsResp
|
||||
res, err = driver.UploadRequest("/person/getMultiUploadUrls", map[string]string{
|
||||
"partInfo": fmt.Sprintf("%s-%s", strconv.FormatInt(i, 10), md5Base64),
|
||||
"uploadFileId": uploadFileId,
|
||||
}, account, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadData := resp.UploadUrls["partNumber_"+strconv.FormatInt(i, 10)]
|
||||
log.Debugf("uploadData: %+v", uploadData)
|
||||
requestURL := uploadData.RequestURL
|
||||
uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&")
|
||||
req, _ := http.NewRequest(http.MethodPut, requestURL, bytes.NewReader(byteData))
|
||||
for _, v := range uploadHeaders {
|
||||
i := strings.Index(v, "=")
|
||||
req.Header.Set(v[0:i], v[i+1:])
|
||||
}
|
||||
|
||||
r, err := base.HttpClient.Do(req)
|
||||
log.Debugf("%+v %+v", r, r.Request.Header)
|
||||
r.Body.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fileMd5 := hex.EncodeToString(md5Sum.Sum(nil))
|
||||
sliceMd5 := fileMd5
|
||||
if file.GetSize() > DEFAULT {
|
||||
sliceMd5 = utils.GetMD5Encode(strings.Join(md5s, "\n"))
|
||||
}
|
||||
res, err = driver.UploadRequest("/person/commitMultiUploadFile", map[string]string{
|
||||
"uploadFileId": uploadFileId,
|
||||
"fileMd5": fileMd5,
|
||||
"sliceMd5": sliceMd5,
|
||||
"lazyCheck": "1",
|
||||
}, account, nil)
|
||||
account.DriveId, _ = driver.GetSessionKey(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) OldUpload(file *model.FileStream, account *model.Account) error {
|
||||
//return base.ErrNotImplement
|
||||
client, err := driver.getClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// api refer to PanIndex
|
||||
res, err := client.R().SetMultipartFormData(map[string]string{
|
||||
"parentId": parentFile.Id,
|
||||
"sessionKey": account.DriveId,
|
||||
"opertype": "1",
|
||||
"fname": file.GetFileName(),
|
||||
}).SetMultipartField("Filedata", file.GetFileName(), file.GetMIMEType(), file).Post("https://hb02.upload.cloud.189.cn/v1/DCIWebUploadAction")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if jsoniter.Get(res.Body(), "MD5").ToString() != "" {
|
||||
return nil
|
||||
}
|
||||
log.Debugf(res.String())
|
||||
return errors.New(res.String())
|
||||
}
|
||||
382
drivers/189/driver.go
Normal file
382
drivers/189/driver.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package _89
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type Cloud189 struct{}
|
||||
|
||||
func (driver Cloud189) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "189Cloud",
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Cloud189) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "username",
|
||||
Label: "username",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "account username/phone number",
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "password",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "account password",
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
//{
|
||||
// Name: "internal_type",
|
||||
// Label: "189cloud type",
|
||||
// Type: base.TypeSelect,
|
||||
// Required: true,
|
||||
// Values: "Personal,Family",
|
||||
//},
|
||||
//{
|
||||
// Name: "site_id",
|
||||
// Label: "family id",
|
||||
// Type: base.TypeString,
|
||||
//},
|
||||
//{
|
||||
// Name: "order_by",
|
||||
// Label: "order_by",
|
||||
// Type: base.TypeSelect,
|
||||
// Values: "name,size,lastOpTime,createdDate",
|
||||
// Required: true,
|
||||
//},
|
||||
//{
|
||||
// Name: "order_direction",
|
||||
// Label: "desc",
|
||||
// Type: base.TypeSelect,
|
||||
// Values: "true,false",
|
||||
// Required: true,
|
||||
//},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Cloud189) Save(account *model.Account, old *model.Account) error {
|
||||
if old != nil {
|
||||
delete(client189Map, old.Name)
|
||||
}
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if err := driver.Login(account); err != nil {
|
||||
account.Status = err.Error()
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
sessionKey, err := driver.GetSessionKey(account)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
} else {
|
||||
account.Status = "work"
|
||||
account.DriveId = sessionKey
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Cloud189) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var rawFiles []Cloud189File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
rawFiles, _ = cache.([]Cloud189File)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawFiles, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawFiles) > 0 {
|
||||
_ = base.SetCache(path, rawFiles, account)
|
||||
}
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(utils.ParsePath(args.Path), account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.Type == conf.FOLDER {
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
var resp DownResp
|
||||
u := "https://cloud.189.cn/api/portal/getFileInfo.action"
|
||||
body, err := driver.Request(u, base.Get, map[string]string{
|
||||
"fileId": file.Id,
|
||||
}, nil, nil, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugln(string(body))
|
||||
err = utils.Json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.ResCode != 0 {
|
||||
return nil, fmt.Errorf(resp.ResMessage)
|
||||
}
|
||||
client, err := driver.getClient(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client = resty.NewWithClient(client.GetClient()).SetRedirectPolicy(
|
||||
resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}))
|
||||
res, err := client.R().SetHeader("User-Agent", base.UserAgent).Get("https:" + resp.FileDownloadUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugln(res.Status())
|
||||
log.Debugln(res.String())
|
||||
link := base.Link{
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: base.UserAgent},
|
||||
//{Name: "Authorization", Value: ""},
|
||||
},
|
||||
}
|
||||
log.Debugln("first url:", resp.FileDownloadUrl)
|
||||
if res.StatusCode() == 302 {
|
||||
link.Url = res.Header().Get("location")
|
||||
log.Debugln("second url:", link.Url)
|
||||
_, _ = client.R().Get(link.Url)
|
||||
if res.StatusCode() == 302 {
|
||||
link.Url = res.Header().Get("location")
|
||||
}
|
||||
log.Debugln("third url:", link.Url)
|
||||
} else {
|
||||
link.Url = resp.FileDownloadUrl
|
||||
}
|
||||
link.Url = strings.Replace(link.Url, "http://", "https://", 1)
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("189 path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver Cloud189) Proxy(r *http.Request, account *model.Account) {
|
||||
// r.Header.Del("Origin")
|
||||
//}
|
||||
|
||||
func (driver Cloud189) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Cloud189) MakeDir(path string, account *model.Account) error {
|
||||
dir, name := filepath.Split(path)
|
||||
parent, err := driver.File(dir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parent.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
form := map[string]string{
|
||||
"parentFolderId": parent.Id,
|
||||
"folderName": name,
|
||||
}
|
||||
_, err = driver.Request("https://cloud.189.cn/api/open/file/createFolder.action", base.Post, nil, form, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Move(src string, dst string, account *model.Account) error {
|
||||
dstDir, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstDirFile, err := driver.File(dstDir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isFolder := 0
|
||||
if srcFile.IsDir() {
|
||||
isFolder = 1
|
||||
}
|
||||
taskInfos := []base.Json{
|
||||
{
|
||||
"fileId": srcFile.Id,
|
||||
"fileName": dstName,
|
||||
"isFolder": isFolder,
|
||||
},
|
||||
}
|
||||
taskInfosBytes, err := utils.Json.Marshal(taskInfos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form := map[string]string{
|
||||
"type": "MOVE",
|
||||
"targetFolderId": dstDirFile.Id,
|
||||
"taskInfos": string(taskInfosBytes),
|
||||
}
|
||||
_, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", base.Post, nil, form, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Rename(src string, dst string, account *model.Account) error {
|
||||
_, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := "https://cloud.189.cn/api/open/file/renameFile.action"
|
||||
idKey := "fileId"
|
||||
nameKey := "destFileName"
|
||||
if srcFile.IsDir() {
|
||||
url = "https://cloud.189.cn/api/open/file/renameFolder.action"
|
||||
idKey = "folderId"
|
||||
nameKey = "destFolderName"
|
||||
}
|
||||
form := map[string]string{
|
||||
idKey: srcFile.Id,
|
||||
nameKey: dstName,
|
||||
}
|
||||
_, err = driver.Request(url, base.Post, nil, form, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Copy(src string, dst string, account *model.Account) error {
|
||||
dstDir, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstDirFile, err := driver.File(dstDir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isFolder := 0
|
||||
if srcFile.IsDir() {
|
||||
isFolder = 1
|
||||
}
|
||||
taskInfos := []base.Json{
|
||||
{
|
||||
"fileId": srcFile.Id,
|
||||
"fileName": dstName,
|
||||
"isFolder": isFolder,
|
||||
},
|
||||
}
|
||||
taskInfosBytes, err := utils.Json.Marshal(taskInfos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form := map[string]string{
|
||||
"type": "COPY",
|
||||
"targetFolderId": dstDirFile.Id,
|
||||
"taskInfos": string(taskInfosBytes),
|
||||
}
|
||||
_, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", base.Post, nil, form, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Delete(path string, account *model.Account) error {
|
||||
path = utils.ParsePath(path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isFolder := 0
|
||||
if file.IsDir() {
|
||||
isFolder = 1
|
||||
}
|
||||
taskInfos := []base.Json{
|
||||
{
|
||||
"fileId": file.Id,
|
||||
"fileName": file.Name,
|
||||
"isFolder": isFolder,
|
||||
},
|
||||
}
|
||||
taskInfosBytes, err := utils.Json.Marshal(taskInfos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form := map[string]string{
|
||||
"type": "DELETE",
|
||||
"targetFolderId": "",
|
||||
"taskInfos": string(taskInfosBytes),
|
||||
}
|
||||
_, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", base.Post, nil, form, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) error {
|
||||
//return base.ErrNotImplement
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
return driver.NewUpload(file, account)
|
||||
//return driver.OldUpload(file, account)
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Cloud189)(nil)
|
||||
@@ -1,17 +1,17 @@
|
||||
package _189
|
||||
package _89
|
||||
|
||||
type LoginResp struct {
|
||||
Msg string `json:"msg"`
|
||||
Result int `json:"result"`
|
||||
ToUrl string `json:"toUrl"`
|
||||
}
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"path"
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
type Cloud189Error struct {
|
||||
ErrorCode string `json:"errorCode"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
type Cloud189File struct {
|
||||
Id int64 `json:"id"`
|
||||
LastOpTime string `json:"lastOpTime"`
|
||||
Name string `json:"name"`
|
||||
@@ -23,19 +23,37 @@ type File struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type Folder struct {
|
||||
func (f Cloud189File) GetSize() uint64 {
|
||||
if f.Size == -1 {
|
||||
return 0
|
||||
}
|
||||
return uint64(f.Size)
|
||||
}
|
||||
|
||||
func (f Cloud189File) GetName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f Cloud189File) GetType() int {
|
||||
if f.Size == -1 {
|
||||
return conf.FOLDER
|
||||
}
|
||||
return utils.GetFileType(path.Ext(f.Name))
|
||||
}
|
||||
|
||||
type Cloud189Folder struct {
|
||||
Id int64 `json:"id"`
|
||||
LastOpTime string `json:"lastOpTime"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Files struct {
|
||||
type Cloud189Files struct {
|
||||
ResCode int `json:"res_code"`
|
||||
ResMessage string `json:"res_message"`
|
||||
FileListAO struct {
|
||||
Count int `json:"count"`
|
||||
FileList []File `json:"fileList"`
|
||||
FolderList []Folder `json:"folderList"`
|
||||
Count int `json:"count"`
|
||||
FileList []Cloud189File `json:"fileList"`
|
||||
FolderList []Cloud189Folder `json:"folderList"`
|
||||
} `json:"fileListAO"`
|
||||
}
|
||||
|
||||
@@ -49,13 +67,18 @@ type Part struct {
|
||||
RequestHeader string `json:"requestHeader"`
|
||||
}
|
||||
|
||||
//type Info struct {
|
||||
// SessionKey string
|
||||
// Rsa Rsa
|
||||
//}
|
||||
|
||||
type Rsa struct {
|
||||
Expire int64 `json:"expire"`
|
||||
PkId string `json:"pkId"`
|
||||
PubKey string `json:"pubKey"`
|
||||
}
|
||||
|
||||
type Down struct {
|
||||
type Cloud189Down struct {
|
||||
ResCode int `json:"res_code"`
|
||||
ResMessage string `json:"res_message"`
|
||||
FileDownloadUrl string `json:"fileDownloadUrl"`
|
||||
@@ -1,4 +1,4 @@
|
||||
package _189
|
||||
package _89
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -13,17 +13,18 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
myrand "github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func random() string {
|
||||
return fmt.Sprintf("0.%17v", myrand.Rand.Int63n(100000000000000000))
|
||||
return fmt.Sprintf("0.%17v", utils.Rand.Int63n(100000000000000000))
|
||||
}
|
||||
|
||||
func RsaEncode(origData []byte, j_rsakey string, hex bool) string {
|
||||
@@ -164,6 +165,11 @@ func getMd5(data []byte) []byte {
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Cloud189{})
|
||||
client189Map = make(map[string]*resty.Client)
|
||||
}
|
||||
|
||||
func decodeURIComponent(str string) string {
|
||||
r, _ := url.PathUnescape(str)
|
||||
//r = strings.ReplaceAll(r, " ", "+")
|
||||
@@ -174,7 +180,7 @@ func Random(v string) string {
|
||||
reg := regexp.MustCompilePOSIX("[xy]")
|
||||
data := reg.ReplaceAllFunc([]byte(v), func(msg []byte) []byte {
|
||||
var i int64
|
||||
t := int64(16 * myrand.Rand.Float32())
|
||||
t := int64(16 * utils.Rand.Float32())
|
||||
if msg[0] == 120 {
|
||||
i = t
|
||||
} else {
|
||||
@@ -184,3 +190,10 @@ func Random(v string) string {
|
||||
})
|
||||
return string(data)
|
||||
}
|
||||
|
||||
//func SHA1(v, l string) []byte {
|
||||
// key := []byte(l)
|
||||
// mac := hmac.New(sha1.New, key)
|
||||
// mac.Write([]byte(v))
|
||||
// return mac.Sum(nil)
|
||||
//}
|
||||
318
drivers/189pc/189.go
Normal file
318
drivers/189pc/189.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package _189
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"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"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var userStateCache = struct {
|
||||
sync.Mutex
|
||||
States map[string]*State
|
||||
}{States: make(map[string]*State)}
|
||||
|
||||
func GetState(account *model.Account) *State {
|
||||
userStateCache.Lock()
|
||||
defer userStateCache.Unlock()
|
||||
if v, ok := userStateCache.States[account.Username]; ok && v != nil {
|
||||
return v
|
||||
}
|
||||
state := &State{client: resty.New().
|
||||
SetHeaders(map[string]string{
|
||||
"Accept": "application/json;charset=UTF-8",
|
||||
"User-Agent": base.UserAgent,
|
||||
}).SetTimeout(base.DefaultTimeout),
|
||||
}
|
||||
userStateCache.States[account.Username] = state
|
||||
return state
|
||||
}
|
||||
|
||||
type State struct {
|
||||
sync.Mutex
|
||||
client *resty.Client
|
||||
|
||||
RsaPublicKey string
|
||||
|
||||
SessionKey string
|
||||
SessionSecret string
|
||||
FamilySessionKey string
|
||||
FamilySessionSecret string
|
||||
|
||||
AccessToken string
|
||||
|
||||
//怎么刷新的???
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
func (s *State) login(account *model.Account) error {
|
||||
// 清除cookie
|
||||
jar, _ := cookiejar.New(nil)
|
||||
s.client.SetCookieJar(jar)
|
||||
|
||||
var err error
|
||||
var res *resty.Response
|
||||
defer func() {
|
||||
account.Status = "work"
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
}
|
||||
model.SaveAccount(account)
|
||||
if res != nil {
|
||||
log.Debug(res.String())
|
||||
}
|
||||
}()
|
||||
|
||||
var param *LoginParam
|
||||
param, err = s.getLoginParam()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 提交登录
|
||||
s.RsaPublicKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", param.jRsaKey)
|
||||
res, err = s.client.R().
|
||||
SetHeaders(map[string]string{
|
||||
"Referer": AUTH_URL,
|
||||
"REQID": param.ReqId,
|
||||
"lt": param.Lt,
|
||||
}).
|
||||
SetFormData(map[string]string{
|
||||
"appKey": APP_ID,
|
||||
"accountType": "02",
|
||||
"userName": "{RSA}" + rsaEncrypt(s.RsaPublicKey, account.Username),
|
||||
"password": "{RSA}" + rsaEncrypt(s.RsaPublicKey, account.Password),
|
||||
"validateCode": param.vCodeRS,
|
||||
"captchaToken": param.CaptchaToken,
|
||||
"returnUrl": RETURN_URL,
|
||||
"mailSuffix": "@189.cn",
|
||||
"dynamicCheck": "FALSE",
|
||||
"clientType": CLIENT_TYPE,
|
||||
"cb_SaveName": "1",
|
||||
"isOauth2": "false",
|
||||
"state": "",
|
||||
"paramId": param.ParamId,
|
||||
}).
|
||||
Post(AUTH_URL + "/api/logbox/oauth2/loginSubmit.do")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toUrl := utils.Json.Get(res.Body(), "toUrl").ToString()
|
||||
if toUrl == "" {
|
||||
log.Error(res.String())
|
||||
return fmt.Errorf(res.String())
|
||||
}
|
||||
|
||||
// 获取Session
|
||||
var erron Erron
|
||||
var sessionResp appSessionResp
|
||||
res, err = s.client.R().
|
||||
SetResult(&sessionResp).SetError(&erron).
|
||||
SetQueryParams(clientSuffix()).
|
||||
SetQueryParam("redirectURL", url.QueryEscape(toUrl)).
|
||||
Post(API_URL + "/getSessionForPC.action")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if erron.ResCode != "" {
|
||||
err = fmt.Errorf(erron.ResMessage)
|
||||
return err
|
||||
}
|
||||
if sessionResp.ResCode != 0 {
|
||||
err = fmt.Errorf(sessionResp.ResMessage)
|
||||
return err
|
||||
}
|
||||
s.SessionKey = sessionResp.SessionKey
|
||||
s.SessionSecret = sessionResp.SessionSecret
|
||||
s.FamilySessionKey = sessionResp.FamilySessionKey
|
||||
s.FamilySessionSecret = sessionResp.FamilySessionSecret
|
||||
s.AccessToken = sessionResp.AccessToken
|
||||
s.RefreshToken = sessionResp.RefreshToken
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *State) getLoginParam() (*LoginParam, error) {
|
||||
res, err := s.client.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"appId": APP_ID,
|
||||
"clientType": CLIENT_TYPE,
|
||||
"returnURL": RETURN_URL,
|
||||
"timeStamp": fmt.Sprint(timestamp()),
|
||||
}).
|
||||
Get(WEB_URL + "/api/portal/unifyLoginForPC.action")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
param := &LoginParam{
|
||||
CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],
|
||||
Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
||||
ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
||||
ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
||||
jRsaKey: regexp.MustCompile(`"j_rsaKey" value="(.+?)"`).FindStringSubmatch(res.String())[1],
|
||||
|
||||
vCodeID: regexp.MustCompile(`token=([A-Za-z0-9&=]+)`).FindStringSubmatch(res.String())[1],
|
||||
}
|
||||
|
||||
imgRes, err := s.client.R().Get(fmt.Sprint(AUTH_URL, "/api/logbox/oauth2/picCaptcha.do?token=", param.vCodeID, timestamp()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(imgRes.Body()) > 0 {
|
||||
vRes, err := resty.New().R().
|
||||
SetMultipartField("image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
|
||||
Post(conf.GetStr("ocr api"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if utils.Json.Get(vRes.Body(), "status").ToInt() != 200 {
|
||||
return nil, errors.New("ocr error:" + utils.Json.Get(vRes.Body(), "msg").ToString())
|
||||
}
|
||||
param.vCodeRS = utils.Json.Get(vRes.Body(), "result").ToString()
|
||||
log.Debugln("code: ", param.vCodeRS)
|
||||
}
|
||||
return param, nil
|
||||
}
|
||||
|
||||
func (s *State) refreshSession(account *model.Account) error {
|
||||
var erron Erron
|
||||
var userSessionResp UserSessionResp
|
||||
res, err := s.client.R().
|
||||
SetResult(&userSessionResp).SetError(&erron).
|
||||
SetQueryParams(clientSuffix()).
|
||||
SetQueryParams(map[string]string{
|
||||
"appId": APP_ID,
|
||||
"accessToken": s.AccessToken,
|
||||
}).
|
||||
SetHeader("X-Request-ID", uuid.NewString()).
|
||||
Get(API_URL + "/getSessionForPC.action")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if erron.ResCode != "" {
|
||||
return fmt.Errorf(erron.ResMessage)
|
||||
}
|
||||
|
||||
switch userSessionResp.ResCode {
|
||||
case 0:
|
||||
s.SessionKey = userSessionResp.SessionKey
|
||||
s.SessionSecret = userSessionResp.SessionSecret
|
||||
s.FamilySessionKey = userSessionResp.FamilySessionKey
|
||||
s.FamilySessionSecret = userSessionResp.FamilySessionSecret
|
||||
case 11, 18:
|
||||
return s.login(account)
|
||||
default:
|
||||
account.Status = userSessionResp.ResMessage
|
||||
_ = model.SaveAccount(account)
|
||||
return fmt.Errorf(userSessionResp.ResMessage)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *State) IsLogin(account *model.Account) bool {
|
||||
_, err := s.Request(http.MethodGet, API_URL+"/getUserInfo.action", nil, func(r *resty.Request) { r.SetQueryParams(clientSuffix()) }, account)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *State) Login(account *model.Account) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.login(account)
|
||||
}
|
||||
|
||||
func (s *State) RefreshSession(account *model.Account) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.refreshSession(account)
|
||||
}
|
||||
|
||||
func (s *State) Request(method string, fullUrl string, params Params, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
|
||||
s.Lock()
|
||||
dateOfGmt := getHttpDateStr()
|
||||
sessionKey := s.SessionKey
|
||||
sessionSecret := s.SessionSecret
|
||||
if isFamily(account) {
|
||||
sessionKey = s.FamilySessionKey
|
||||
sessionSecret = s.FamilySessionSecret
|
||||
}
|
||||
|
||||
req := s.client.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
"Date": dateOfGmt,
|
||||
"SessionKey": sessionKey,
|
||||
"X-Request-ID": uuid.NewString(),
|
||||
})
|
||||
|
||||
// 设置params
|
||||
var paramsData string
|
||||
if params != nil {
|
||||
paramsData = AesECBEncrypt(params.Encode(), s.SessionSecret[:16])
|
||||
req.SetQueryParam("params", paramsData)
|
||||
}
|
||||
req.SetHeader("Signature", signatureOfHmac(sessionSecret, sessionKey, method, fullUrl, dateOfGmt, paramsData))
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
s.Unlock()
|
||||
|
||||
res, err := req.Execute(method, fullUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
|
||||
var erron Erron
|
||||
utils.Json.Unmarshal(res.Body(), &erron)
|
||||
if erron.ResCode != "" {
|
||||
return nil, fmt.Errorf(erron.ResMessage)
|
||||
}
|
||||
if erron.Code != "" && erron.Code != "SUCCESS" {
|
||||
if erron.Msg == "" {
|
||||
if erron.Message == "" {
|
||||
return nil, fmt.Errorf(res.String())
|
||||
}
|
||||
return nil, fmt.Errorf(erron.Message)
|
||||
}
|
||||
return nil, fmt.Errorf(erron.Msg)
|
||||
}
|
||||
if erron.ErrorCode != "" {
|
||||
switch erron.ErrorCode {
|
||||
case "InvalidSessionKey":
|
||||
if err := s.RefreshSession(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.Request(method, fullUrl, params, callback, account)
|
||||
}
|
||||
return nil, fmt.Errorf(erron.ErrorMsg)
|
||||
}
|
||||
|
||||
switch utils.Json.Get(res.Body(), "res_code").ToInt64() {
|
||||
case 11, 18:
|
||||
if err := s.RefreshSession(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.Request(method, fullUrl, params, callback, account)
|
||||
case 0:
|
||||
if res.StatusCode() == http.StatusOK {
|
||||
return res, nil
|
||||
}
|
||||
return nil, fmt.Errorf(res.String())
|
||||
default:
|
||||
return nil, fmt.Errorf(utils.Json.Get(res.Body(), "res_message").ToString())
|
||||
}
|
||||
}
|
||||
923
drivers/189pc/driver.go
Normal file
923
drivers/189pc/driver.go
Normal file
@@ -0,0 +1,923 @@
|
||||
package _189
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(new(Cloud189))
|
||||
}
|
||||
|
||||
type Cloud189 struct {
|
||||
}
|
||||
|
||||
func (driver Cloud189) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "189CloudPC",
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "189cloud type",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "Personal,Family",
|
||||
},
|
||||
{
|
||||
Name: "site_id",
|
||||
Label: "family id",
|
||||
Type: base.TypeString,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "filename,filesize,lastOpTime",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "desc",
|
||||
Type: base.TypeSelect,
|
||||
Values: "true,false",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "bool_1",
|
||||
Label: "fast upload",
|
||||
Type: base.TypeBool,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Cloud189) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isFamily(account) && account.RootFolder == "" {
|
||||
account.RootFolder = "-11"
|
||||
account.SiteId = ""
|
||||
}
|
||||
if isFamily(account) && account.RootFolder == "-11" {
|
||||
account.RootFolder = ""
|
||||
}
|
||||
|
||||
state := GetState(account)
|
||||
if !state.IsLogin(account) {
|
||||
if err := state.Login(account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isFamily(account) {
|
||||
list, err := driver.getFamilyInfoList(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, l := range list {
|
||||
if account.SiteId == "" {
|
||||
account.SiteId = fmt.Sprint(l.FamilyID)
|
||||
}
|
||||
log.Infof("天翼家庭云 用户名:%s FamilyID %d\n", l.RemarkName, l.FamilyID)
|
||||
}
|
||||
}
|
||||
|
||||
account.Status = "work"
|
||||
model.SaveAccount(account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) getFamilyInfoList(account *model.Account) ([]FamilyInfoResp, error) {
|
||||
var resp FamilyInfoListResp
|
||||
_, err := GetState(account).Request(http.MethodGet, API_URL+"/family/manage/getFamilyList.action", nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(clientSuffix())
|
||||
r.SetResult(&resp)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.FamilyInfoResp, 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)
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ := cache.([]model.File)
|
||||
return files, nil
|
||||
}
|
||||
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fullUrl := API_URL
|
||||
if isFamily(account) {
|
||||
fullUrl += "/family/file"
|
||||
}
|
||||
fullUrl += "/listFiles.action"
|
||||
|
||||
files := make([]model.File, 0)
|
||||
client := GetState(account)
|
||||
for pageNum := 1; ; pageNum++ {
|
||||
var resp Cloud189FilesResp
|
||||
_, err = client.Request(http.MethodGet, fullUrl, nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(clientSuffix()).
|
||||
SetQueryParams(map[string]string{
|
||||
"folderId": file.Id,
|
||||
"fileType": "0",
|
||||
"mediaAttr": "0",
|
||||
"iconOption": "5",
|
||||
"pageNum": fmt.Sprint(pageNum),
|
||||
"pageSize": "130",
|
||||
})
|
||||
if isFamily(account) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"familyId": account.SiteId,
|
||||
"orderBy": toFamilyOrderBy(account.OrderBy),
|
||||
"descending": account.OrderDirection,
|
||||
})
|
||||
} else {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"recursive": "0",
|
||||
"orderBy": account.OrderBy,
|
||||
"descending": account.OrderDirection,
|
||||
})
|
||||
}
|
||||
r.SetResult(&resp)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 获取完毕跳出
|
||||
if resp.FileListAO.Count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, folder := range resp.FileListAO.FolderList {
|
||||
files = append(files, model.File{
|
||||
Id: fmt.Sprint(folder.ID),
|
||||
Name: folder.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: MustParseTime(folder.LastOpTime),
|
||||
})
|
||||
}
|
||||
for _, file := range resp.FileListAO.FileList {
|
||||
files = append(files, model.File{
|
||||
Id: fmt.Sprint(file.ID),
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
Type: utils.GetFileType(filepath.Ext(file.Name)),
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: MustParseTime(file.LastOpTime),
|
||||
Thumbnail: file.Icon.SmallUrl,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Cloud189) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("189PC 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) 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
|
||||
}
|
||||
|
||||
fullUrl := API_URL
|
||||
if isFamily(account) {
|
||||
fullUrl += "/family/file"
|
||||
}
|
||||
fullUrl += "/getFileDownloadUrl.action"
|
||||
|
||||
var downloadUrl struct {
|
||||
URL string `json:"fileDownloadUrl"`
|
||||
}
|
||||
_, err = GetState(account).Request(http.MethodGet, fullUrl, nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(clientSuffix()).SetQueryParam("fileId", file.Id)
|
||||
if isFamily(account) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"familyId": account.SiteId,
|
||||
})
|
||||
} else {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"dt": "3",
|
||||
"flag": "1",
|
||||
})
|
||||
}
|
||||
r.SetResult(&downloadUrl)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: base.UserAgent},
|
||||
},
|
||||
Url: strings.ReplaceAll(downloadUrl.URL, "&", "&"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
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)
|
||||
parentFile, err := driver.File(dir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
|
||||
fullUrl := API_URL
|
||||
if isFamily(account) {
|
||||
fullUrl += "/family/file"
|
||||
}
|
||||
fullUrl += "/createFolder.action"
|
||||
|
||||
_, err = GetState(account).Request(http.MethodPost, fullUrl, nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(clientSuffix()).SetQueryParams(map[string]string{
|
||||
"folderName": name,
|
||||
"relativePath": "",
|
||||
})
|
||||
if isFamily(account) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"familyId": account.SiteId,
|
||||
"parentId": parentFile.Id,
|
||||
})
|
||||
} else {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"parentFolderId": parentFile.Id,
|
||||
})
|
||||
}
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Move(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstDirFile, err := driver.File(filepath.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = GetState(account).Request(http.MethodPost, API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
|
||||
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
|
||||
"type": "MOVE",
|
||||
"taskInfos": string(MustToBytes(utils.Json.Marshal(
|
||||
[]*BatchTaskInfo{
|
||||
{
|
||||
FileId: srcFile.Id,
|
||||
FileName: srcFile.Name,
|
||||
IsFolder: BoolToNumber(srcFile.IsDir()),
|
||||
},
|
||||
}))),
|
||||
"targetFolderId": dstDirFile.Id,
|
||||
})
|
||||
if isFamily(account) {
|
||||
r.SetFormData(map[string]string{
|
||||
"familyId": account.SiteId,
|
||||
})
|
||||
}
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
func (driver Cloud189) Move(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstDirFile, err := driver.File(filepath.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var queryParam map[string]string
|
||||
fullUrl := API_URL
|
||||
method := http.MethodPost
|
||||
if isFamily(account) {
|
||||
fullUrl += "/family/file"
|
||||
method = http.MethodGet
|
||||
}
|
||||
if srcFile.IsDir() {
|
||||
fullUrl += "/moveFolder.action"
|
||||
queryParam = map[string]string{
|
||||
"folderId": srcFile.Id,
|
||||
"destFolderName": srcFile.Name,
|
||||
}
|
||||
} else {
|
||||
fullUrl += "/moveFile.action"
|
||||
queryParam = map[string]string{
|
||||
"fileId": srcFile.Id,
|
||||
"destFileName": srcFile.Name,
|
||||
}
|
||||
}
|
||||
|
||||
_, err = GetState(account).Request(method, fullUrl, nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(queryParam).SetQueryParams(clientSuffix())
|
||||
if isFamily(account) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"familyId": account.SiteId,
|
||||
"destParentId": dstDirFile.Id,
|
||||
})
|
||||
} else {
|
||||
r.SetQueryParam("destParentFolderId", dstDirFile.Id)
|
||||
}
|
||||
}, account)
|
||||
return err
|
||||
}*/
|
||||
|
||||
func (driver Cloud189) Rename(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var queryParam map[string]string
|
||||
fullUrl := API_URL
|
||||
method := http.MethodPost
|
||||
if isFamily(account) {
|
||||
fullUrl += "/family/file"
|
||||
method = http.MethodGet
|
||||
}
|
||||
if srcFile.IsDir() {
|
||||
fullUrl += "/renameFolder.action"
|
||||
queryParam = map[string]string{
|
||||
"folderId": srcFile.Id,
|
||||
"destFolderName": filepath.Base(dst),
|
||||
}
|
||||
} else {
|
||||
fullUrl += "/renameFile.action"
|
||||
queryParam = map[string]string{
|
||||
"fileId": srcFile.Id,
|
||||
"destFileName": filepath.Base(dst),
|
||||
}
|
||||
}
|
||||
|
||||
_, err = GetState(account).Request(method, fullUrl, nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(queryParam).SetQueryParams(clientSuffix())
|
||||
if isFamily(account) {
|
||||
r.SetQueryParam("familyId", account.SiteId)
|
||||
}
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Copy(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstDirFile, err := driver.File(filepath.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = GetState(account).Request(http.MethodPost, API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
|
||||
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
|
||||
"type": "COPY",
|
||||
"taskInfos": string(MustToBytes(utils.Json.Marshal(
|
||||
[]*BatchTaskInfo{
|
||||
{
|
||||
FileId: srcFile.Id,
|
||||
FileName: srcFile.Name,
|
||||
IsFolder: BoolToNumber(srcFile.IsDir()),
|
||||
},
|
||||
}))),
|
||||
"targetFolderId": dstDirFile.Id,
|
||||
"targetFileName": filepath.Base(dst),
|
||||
})
|
||||
if isFamily(account) {
|
||||
r.SetFormData(map[string]string{
|
||||
"familyId": account.SiteId,
|
||||
})
|
||||
}
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Delete(path string, account *model.Account) error {
|
||||
path = utils.ParsePath(path)
|
||||
srcFile, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = GetState(account).Request(http.MethodPost, API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) {
|
||||
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
|
||||
"type": "DELETE",
|
||||
"taskInfos": string(MustToBytes(utils.Json.Marshal(
|
||||
[]*BatchTaskInfo{
|
||||
{
|
||||
FileId: srcFile.Id,
|
||||
FileName: srcFile.Name,
|
||||
IsFolder: BoolToNumber(srcFile.IsDir()),
|
||||
},
|
||||
}))),
|
||||
})
|
||||
|
||||
if isFamily(account) {
|
||||
r.SetFormData(map[string]string{
|
||||
"familyId": account.SiteId,
|
||||
})
|
||||
}
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
|
||||
if account.Bool1 {
|
||||
return driver.FastUpload(file, parentFile, account)
|
||||
}
|
||||
return driver.CommonUpload(file, parentFile, account)
|
||||
/*
|
||||
if isFamily(account) {
|
||||
return driver.uploadFamily(file, parentFile, account)
|
||||
}
|
||||
return driver.uploadPerson(file, parentFile, account)
|
||||
*/
|
||||
}
|
||||
|
||||
func (driver Cloud189) CommonUpload(file *model.FileStream, parentFile *model.File, account *model.Account) error {
|
||||
// 初始化上传
|
||||
state := GetState(account)
|
||||
const DEFAULT int64 = 10485760
|
||||
count := int(math.Ceil(float64(file.Size) / float64(DEFAULT)))
|
||||
|
||||
params := Params{
|
||||
"parentFolderId": parentFile.Id,
|
||||
"fileName": url.PathEscape(file.Name),
|
||||
"fileSize": fmt.Sprint(file.Size),
|
||||
"sliceSize": fmt.Sprint(DEFAULT),
|
||||
"lazyCheck": "1",
|
||||
}
|
||||
|
||||
fullUrl := UPLOAD_URL
|
||||
if isFamily(account) {
|
||||
params.Set("familyId", account.SiteId)
|
||||
fullUrl += "/family"
|
||||
} else {
|
||||
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
|
||||
fullUrl += "/person"
|
||||
}
|
||||
|
||||
var initMultiUpload InitMultiUploadResp
|
||||
_, err := state.Request(http.MethodGet, fullUrl+"/initMultiUpload", params, func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&initMultiUpload) }, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileMd5 := md5.New()
|
||||
silceMd5 := md5.New()
|
||||
silceMd5Hexs := make([]string, 0, count)
|
||||
byteData := bytes.NewBuffer(make([]byte, DEFAULT))
|
||||
for i := 1; i <= count; i++ {
|
||||
byteData.Reset()
|
||||
silceMd5.Reset()
|
||||
if n, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5, byteData), file, DEFAULT); err != io.EOF && n == 0 {
|
||||
return err
|
||||
}
|
||||
md5Bytes := silceMd5.Sum(nil)
|
||||
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Bytes)))
|
||||
silceMd5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)
|
||||
|
||||
var uploadUrl UploadUrlsResp
|
||||
_, err = state.Request(http.MethodGet, fullUrl+"/getMultiUploadUrls",
|
||||
Params{"partInfo": fmt.Sprintf("%d-%s", i, silceMd5Base64), "uploadFileId": initMultiUpload.Data.UploadFileID},
|
||||
func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&uploadUrl) },
|
||||
account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploadData := uploadUrl.UploadUrls[fmt.Sprint("partNumber_", i)]
|
||||
req, _ := http.NewRequest(http.MethodPut, uploadData.RequestURL, byteData)
|
||||
req.Header.Set("User-Agent", "")
|
||||
for k, v := range ParseHttpHeader(uploadData.RequestHeader) {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
for k, v := range clientSuffix() {
|
||||
req.URL.RawQuery += fmt.Sprintf("&%s=%s", k, v)
|
||||
}
|
||||
r, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.StatusCode != http.StatusOK {
|
||||
data, _ := io.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
return fmt.Errorf(string(data))
|
||||
}
|
||||
r.Body.Close()
|
||||
}
|
||||
|
||||
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
|
||||
sliceMd5Hex := fileMd5Hex
|
||||
if int64(file.Size) > DEFAULT {
|
||||
sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n")))
|
||||
}
|
||||
|
||||
_, err = state.Request(http.MethodGet, fullUrl+"/commitMultiUploadFile",
|
||||
Params{
|
||||
"uploadFileId": initMultiUpload.Data.UploadFileID,
|
||||
"fileMd5": fileMd5Hex,
|
||||
"sliceMd5": sliceMd5Hex,
|
||||
"lazyCheck": "1",
|
||||
"isLog": "0",
|
||||
"opertype": "3",
|
||||
},
|
||||
func(r *resty.Request) { r.SetQueryParams(clientSuffix()) }, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) FastUpload(file *model.FileStream, parentFile *model.File, account *model.Account) error {
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
// 初始化上传
|
||||
state := GetState(account)
|
||||
|
||||
const DEFAULT int64 = 10485760
|
||||
count := int(math.Ceil(float64(file.Size) / float64(DEFAULT)))
|
||||
|
||||
// 优先计算所需信息
|
||||
fileMd5 := md5.New()
|
||||
silceMd5 := md5.New()
|
||||
silceMd5Hexs := make([]string, 0, count)
|
||||
silceMd5Base64s := make([]string, 0, count)
|
||||
for i := 1; i <= count; i++ {
|
||||
silceMd5.Reset()
|
||||
if n, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5, tempFile), file, DEFAULT); err != nil && n == 0 {
|
||||
return err
|
||||
}
|
||||
md5Byte := silceMd5.Sum(nil)
|
||||
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte)))
|
||||
silceMd5Base64s = append(silceMd5Base64s, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte)))
|
||||
}
|
||||
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
|
||||
sliceMd5Hex := fileMd5Hex
|
||||
if int64(file.Size) > DEFAULT {
|
||||
sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n")))
|
||||
}
|
||||
|
||||
params := Params{
|
||||
"parentFolderId": parentFile.Id,
|
||||
"fileName": url.PathEscape(file.Name),
|
||||
"fileSize": fmt.Sprint(file.Size),
|
||||
"fileMd5": fileMd5Hex,
|
||||
"sliceSize": fmt.Sprint(DEFAULT),
|
||||
"sliceMd5": sliceMd5Hex,
|
||||
}
|
||||
|
||||
fullUrl := UPLOAD_URL
|
||||
if isFamily(account) {
|
||||
params.Set("familyId", account.SiteId)
|
||||
fullUrl += "/family"
|
||||
} else {
|
||||
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
|
||||
fullUrl += "/person"
|
||||
}
|
||||
|
||||
var uploadInfo InitMultiUploadResp
|
||||
_, err = state.Request(http.MethodGet, fullUrl+"/initMultiUpload", params, func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&uploadInfo) }, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if uploadInfo.Data.FileDataExists != 1 {
|
||||
var uploadUrls UploadUrlsResp
|
||||
_, err := state.Request(http.MethodGet, fullUrl+"/getMultiUploadUrls",
|
||||
Params{
|
||||
"uploadFileId": uploadInfo.Data.UploadFileID,
|
||||
"partInfo": strings.Join(silceMd5Base64s, ","),
|
||||
},
|
||||
func(r *resty.Request) { r.SetQueryParams(clientSuffix()).SetResult(&uploadUrls) },
|
||||
account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 1; i <= count; i++ {
|
||||
uploadData := uploadUrls.UploadUrls[fmt.Sprint("partNumber_", i)]
|
||||
req, _ := http.NewRequest(http.MethodPut, uploadData.RequestURL, io.NewSectionReader(tempFile, int64(i-1)*DEFAULT, DEFAULT))
|
||||
req.Header.Set("User-Agent", "")
|
||||
for k, v := range ParseHttpHeader(uploadData.RequestHeader) {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
for k, v := range clientSuffix() {
|
||||
req.URL.RawQuery += fmt.Sprintf("&%s=%s", k, v)
|
||||
}
|
||||
r, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.StatusCode != http.StatusOK {
|
||||
data, _ := io.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
return fmt.Errorf(string(data))
|
||||
}
|
||||
r.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
_, err = state.Request(http.MethodGet, fullUrl+"/commitMultiUploadFile",
|
||||
Params{
|
||||
"uploadFileId": uploadInfo.Data.UploadFileID,
|
||||
"isLog": "0",
|
||||
"opertype": "3",
|
||||
},
|
||||
func(r *resty.Request) { r.SetQueryParams(clientSuffix()) },
|
||||
account)
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
func (driver Cloud189) uploadFamily(file *model.FileStream, parentFile *model.File, account *model.Account) error {
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
|
||||
fileMd5 := md5.New()
|
||||
if _, err = io.Copy(io.MultiWriter(fileMd5, tempFile), file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := GetState(account)
|
||||
var createUpload CreateUploadFileResult
|
||||
_, err = client.Request(http.MethodGet, API_URL+"/family/file/createFamilyFile.action", nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"fileMd5": hex.EncodeToString(fileMd5.Sum(nil)),
|
||||
"fileName": file.Name,
|
||||
"familyId": account.SiteId,
|
||||
"parentId": parentFile.Id,
|
||||
"resumePolicy": "1",
|
||||
"fileSize": fmt.Sprint(file.Size),
|
||||
})
|
||||
r.SetQueryParams(clientSuffix())
|
||||
r.SetResult(&createUpload)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if createUpload.FileDataExists != 1 {
|
||||
if createUpload.UploadFileId, err = driver.uploadFileData(file, tempFile, createUpload, account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = client.Request(http.MethodGet, createUpload.FileCommitUrl, nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(clientSuffix())
|
||||
r.SetHeaders(map[string]string{
|
||||
"FamilyId": account.SiteId,
|
||||
"uploadFileId": fmt.Sprint(createUpload.UploadFileId),
|
||||
"ResumePolicy": "1",
|
||||
})
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) uploadPerson(file *model.FileStream, parentFile *model.File, account *model.Account) error {
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
|
||||
fileMd5 := md5.New()
|
||||
if _, err = io.Copy(io.MultiWriter(fileMd5, tempFile), file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := GetState(account)
|
||||
var createUpload CreateUploadFileResult
|
||||
_, err = client.Request(http.MethodPost, API_URL+"/createUploadFile.action", nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(clientSuffix())
|
||||
r.SetFormData(clientSuffix()).SetFormData(map[string]string{
|
||||
"parentFolderId": parentFile.Id,
|
||||
"baseFileId": "",
|
||||
"fileName": file.Name,
|
||||
"size": fmt.Sprint(file.Size),
|
||||
"md5": hex.EncodeToString(fileMd5.Sum(nil)),
|
||||
// "lastWrite": param.LastWrite,
|
||||
// "localPath": strings.ReplaceAll(file.ParentPath, "\\", "/"),
|
||||
"opertype": "1",
|
||||
"flag": "1",
|
||||
"resumePolicy": "1",
|
||||
"isLog": "0",
|
||||
"fileExt": "",
|
||||
})
|
||||
r.SetResult(&createUpload)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if createUpload.FileDataExists != 1 {
|
||||
if createUpload.UploadFileId, err = driver.uploadFileData(file, tempFile, createUpload, account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = client.Request(http.MethodPost, createUpload.FileCommitUrl, nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(clientSuffix())
|
||||
r.SetFormData(map[string]string{
|
||||
"uploadFileId": fmt.Sprint(createUpload.UploadFileId),
|
||||
"opertype": "5", //5 覆盖 1 重命名
|
||||
"ResumePolicy": "1",
|
||||
"isLog": "0",
|
||||
})
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Cloud189) uploadFileData(file *model.FileStream, tempFile *os.File, createUpload CreateUploadFileResult, account *model.Account) (int64, error) {
|
||||
uploadFileState, err := driver.getUploadFileState(createUpload.UploadFileId, account)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if uploadFileState.FileDataExists == 1 || uploadFileState.DataSize == int64(file.Size) {
|
||||
return uploadFileState.UploadFileId, nil
|
||||
}
|
||||
|
||||
if _, err = tempFile.Seek(uploadFileState.DataSize, io.SeekStart); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_, err = GetState(account).Request("PUT", uploadFileState.FileUploadUrl, nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(clientSuffix())
|
||||
r.SetHeaders(map[string]string{
|
||||
"Content-Type": "application/octet-stream",
|
||||
"ResumePolicy": "1",
|
||||
"Edrive-UploadFileRange": fmt.Sprintf("bytes=%d-%d", uploadFileState.DataSize, file.Size),
|
||||
"Expect": "100-continue",
|
||||
})
|
||||
if isFamily(account) {
|
||||
r.SetHeaders(map[string]string{
|
||||
"familyId": account.SiteId,
|
||||
"UploadFileId": fmt.Sprint(uploadFileState.UploadFileId),
|
||||
})
|
||||
} else {
|
||||
r.SetHeader("Edrive-UploadFileId", fmt.Sprint(uploadFileState.UploadFileId))
|
||||
}
|
||||
r.SetBody(tempFile)
|
||||
}, account)
|
||||
return uploadFileState.UploadFileId, err
|
||||
}
|
||||
|
||||
func (driver Cloud189) getUploadFileState(uploadFileId int64, account *model.Account) (*UploadFileStatusResult, error) {
|
||||
fullUrl := API_URL
|
||||
if isFamily(account) {
|
||||
fullUrl += "/family/file/getFamilyFileStatus.action"
|
||||
} else {
|
||||
fullUrl += "/getUploadFileStatus.action"
|
||||
}
|
||||
var uploadFileState UploadFileStatusResult
|
||||
_, err := GetState(account).Request(http.MethodGet, fullUrl, nil, func(r *resty.Request) {
|
||||
r.SetQueryParams(clientSuffix())
|
||||
r.SetQueryParams(map[string]string{
|
||||
"uploadFileId": fmt.Sprint(uploadFileId),
|
||||
"resumePolicy": "1",
|
||||
})
|
||||
if isFamily(account) {
|
||||
r.SetQueryParam("familyId", account.SiteId)
|
||||
}
|
||||
r.SetResult(&uploadFileState)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &uploadFileState, nil
|
||||
}*/
|
||||
|
||||
var _ base.Driver = (*Cloud189)(nil)
|
||||
180
drivers/189pc/type.go
Normal file
180
drivers/189pc/type.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package _189
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
type LoginParam struct {
|
||||
CaptchaToken string
|
||||
Lt string
|
||||
ParamId string
|
||||
ReqId string
|
||||
jRsaKey string
|
||||
|
||||
vCodeID string
|
||||
vCodeRS string
|
||||
}
|
||||
|
||||
// 居然有四种返回方式
|
||||
type Erron struct {
|
||||
ResCode string `json:"res_code"`
|
||||
ResMessage string `json:"res_message"`
|
||||
|
||||
XMLName xml.Name `xml:"error"`
|
||||
Code string `json:"code" xml:"code"`
|
||||
Message string `json:"message" xml:"message"`
|
||||
|
||||
// Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
|
||||
ErrorCode string `json:"errorCode"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
}
|
||||
|
||||
// 刷新session返回
|
||||
type UserSessionResp struct {
|
||||
ResCode int `json:"res_code"`
|
||||
ResMessage string `json:"res_message"`
|
||||
|
||||
LoginName string `json:"loginName"`
|
||||
|
||||
KeepAlive int `json:"keepAlive"`
|
||||
GetFileDiffSpan int `json:"getFileDiffSpan"`
|
||||
GetUserInfoSpan int `json:"getUserInfoSpan"`
|
||||
|
||||
// 个人云
|
||||
SessionKey string `json:"sessionKey"`
|
||||
SessionSecret string `json:"sessionSecret"`
|
||||
// 家庭云
|
||||
FamilySessionKey string `json:"familySessionKey"`
|
||||
FamilySessionSecret string `json:"familySessionSecret"`
|
||||
}
|
||||
|
||||
//登录返回
|
||||
type appSessionResp struct {
|
||||
UserSessionResp
|
||||
|
||||
IsSaveName string `json:"isSaveName"`
|
||||
|
||||
// 会话刷新Token
|
||||
AccessToken string `json:"accessToken"`
|
||||
//Token刷新
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
|
||||
type FamilyInfoListResp struct {
|
||||
FamilyInfoResp []FamilyInfoResp `json:"familyInfoResp"`
|
||||
}
|
||||
type FamilyInfoResp struct {
|
||||
Count int `json:"count"`
|
||||
CreateTime string `json:"createTime"`
|
||||
FamilyID int `json:"familyId"`
|
||||
RemarkName string `json:"remarkName"`
|
||||
Type int `json:"type"`
|
||||
UseFlag int `json:"useFlag"`
|
||||
UserRole int `json:"userRole"`
|
||||
}
|
||||
|
||||
/*文件部分*/
|
||||
// 文件
|
||||
type Cloud189File struct {
|
||||
CreateDate string `json:"createDate"`
|
||||
FileCata int64 `json:"fileCata"`
|
||||
Icon struct {
|
||||
//iconOption 5
|
||||
SmallUrl string `json:"smallUrl"`
|
||||
LargeUrl string `json:"largeUrl"`
|
||||
|
||||
// iconOption 10
|
||||
Max600 string `json:"max600"`
|
||||
MediumURL string `json:"mediumUrl"`
|
||||
} `json:"icon"`
|
||||
ID int64 `json:"id"`
|
||||
LastOpTime string `json:"lastOpTime"`
|
||||
Md5 string `json:"md5"`
|
||||
MediaType int `json:"mediaType"`
|
||||
Name string `json:"name"`
|
||||
Orientation int64 `json:"orientation"`
|
||||
Rev string `json:"rev"`
|
||||
Size int64 `json:"size"`
|
||||
StarLabel int64 `json:"starLabel"`
|
||||
}
|
||||
|
||||
// 文件夹
|
||||
type Cloud189Folder struct {
|
||||
ID int64 `json:"id"`
|
||||
ParentID int64 `json:"parentId"`
|
||||
Name string `json:"name"`
|
||||
|
||||
FileCata int64 `json:"fileCata"`
|
||||
FileCount int64 `json:"fileCount"`
|
||||
|
||||
LastOpTime string `json:"lastOpTime"`
|
||||
CreateDate string `json:"createDate"`
|
||||
|
||||
FileListSize int64 `json:"fileListSize"`
|
||||
Rev string `json:"rev"`
|
||||
StarLabel int64 `json:"starLabel"`
|
||||
}
|
||||
|
||||
type Cloud189FilesResp 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"`
|
||||
}
|
||||
|
||||
// TaskInfo 任务信息
|
||||
type BatchTaskInfo struct {
|
||||
// FileId 文件ID
|
||||
FileId string `json:"fileId"`
|
||||
// FileName 文件名
|
||||
FileName string `json:"fileName"`
|
||||
// IsFolder 是否是文件夹,0-否,1-是
|
||||
IsFolder int `json:"isFolder"`
|
||||
// SrcParentId 文件所在父目录ID
|
||||
//SrcParentId string `json:"srcParentId"`
|
||||
}
|
||||
|
||||
/*
|
||||
type CreateUploadFileResult struct {
|
||||
// UploadFileId 上传文件请求ID
|
||||
UploadFileId int64 `json:"uploadFileId"`
|
||||
// FileUploadUrl 上传文件数据的URL路径
|
||||
FileUploadUrl string `json:"fileUploadUrl"`
|
||||
// FileCommitUrl 上传文件完成后确认路径
|
||||
FileCommitUrl string `json:"fileCommitUrl"`
|
||||
// FileDataExists 文件是否已存在云盘中,0-未存在,1-已存在
|
||||
FileDataExists int `json:"fileDataExists"`
|
||||
}
|
||||
|
||||
type UploadFileStatusResult struct {
|
||||
// 上传文件的ID
|
||||
UploadFileId int64 `json:"uploadFileId"`
|
||||
// 已上传的大小
|
||||
DataSize int64 `json:"dataSize"`
|
||||
FileUploadUrl string `json:"fileUploadUrl"`
|
||||
FileCommitUrl string `json:"fileCommitUrl"`
|
||||
FileDataExists int `json:"fileDataExists"`
|
||||
}
|
||||
*/
|
||||
|
||||
type InitMultiUploadResp struct {
|
||||
//Code string `json:"code"`
|
||||
Data struct {
|
||||
UploadType int `json:"uploadType"`
|
||||
UploadHost string `json:"uploadHost"`
|
||||
UploadFileID string `json:"uploadFileId"`
|
||||
FileDataExists int `json:"fileDataExists"`
|
||||
} `json:"data"`
|
||||
}
|
||||
type UploadUrlsResp struct {
|
||||
Code string `json:"code"`
|
||||
UploadUrls map[string]Part `json:"uploadUrls"`
|
||||
}
|
||||
|
||||
type Part struct {
|
||||
RequestURL string `json:"requestURL"`
|
||||
RequestHeader string `json:"requestHeader"`
|
||||
}
|
||||
177
drivers/189pc/util.go
Normal file
177
drivers/189pc/util.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package _189
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
rand2 "math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/alist/model"
|
||||
)
|
||||
|
||||
const (
|
||||
APP_ID = "8025431004"
|
||||
CLIENT_TYPE = "10020"
|
||||
VERSION = "6.2"
|
||||
|
||||
WEB_URL = "https://cloud.189.cn"
|
||||
AUTH_URL = "https://open.e.189.cn"
|
||||
API_URL = "https://api.cloud.189.cn"
|
||||
UPLOAD_URL = "https://upload.cloud.189.cn"
|
||||
|
||||
RETURN_URL = "https://m.cloud.189.cn/zhuanti/2020/loginErrorPc/index.html"
|
||||
|
||||
PC = "TELEPC"
|
||||
MAC = "TELEMAC"
|
||||
|
||||
CHANNEL_ID = "web_cloud.189.cn"
|
||||
)
|
||||
|
||||
func clientSuffix() map[string]string {
|
||||
return map[string]string{
|
||||
"clientType": PC,
|
||||
"version": VERSION,
|
||||
"channelId": CHANNEL_ID,
|
||||
"rand": fmt.Sprintf("%d_%d", rand2.Int63n(1e5), rand2.Int63n(1e10)),
|
||||
}
|
||||
}
|
||||
|
||||
// 带params的SignatureOfHmac HMAC签名
|
||||
func signatureOfHmac(sessionSecret, sessionKey, operate, fullUrl, dateOfGmt, param string) string {
|
||||
u, _ := url.Parse(fullUrl)
|
||||
mac := hmac.New(sha1.New, []byte(sessionSecret))
|
||||
data := fmt.Sprintf("SessionKey=%s&Operate=%s&RequestURI=%s&Date=%s", sessionKey, operate, u.Path, dateOfGmt)
|
||||
if param != "" {
|
||||
data += fmt.Sprintf("¶ms=%s", param)
|
||||
}
|
||||
mac.Write([]byte(data))
|
||||
return strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))
|
||||
}
|
||||
|
||||
// 获取http规范的时间
|
||||
func getHttpDateStr() string {
|
||||
return time.Now().UTC().Format(http.TimeFormat)
|
||||
}
|
||||
|
||||
// RAS 加密用户名密码
|
||||
func rsaEncrypt(publicKey, origData string) string {
|
||||
block, _ := pem.Decode([]byte(publicKey))
|
||||
pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
data, _ := rsa.EncryptPKCS1v15(rand.Reader, pubInterface.(*rsa.PublicKey), []byte(origData))
|
||||
return base64ToHex(base64.StdEncoding.EncodeToString(data))
|
||||
}
|
||||
|
||||
// aes 加密params
|
||||
func AesECBEncrypt(data, key string) string {
|
||||
block, _ := aes.NewCipher([]byte(key))
|
||||
paddingData := PKCS7Padding([]byte(data), block.BlockSize())
|
||||
decrypted := make([]byte, len(paddingData))
|
||||
size := block.BlockSize()
|
||||
for src, dst := paddingData, decrypted; len(src) > 0; src, dst = src[size:], dst[size:] {
|
||||
block.Encrypt(dst[:size], src[:size])
|
||||
}
|
||||
return strings.ToUpper(hex.EncodeToString(decrypted))
|
||||
}
|
||||
|
||||
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
// 时间戳
|
||||
func timestamp() int64 {
|
||||
return time.Now().UTC().UnixNano() / 1e6
|
||||
}
|
||||
|
||||
func base64ToHex(a string) string {
|
||||
v, _ := base64.StdEncoding.DecodeString(a)
|
||||
return strings.ToUpper(hex.EncodeToString(v))
|
||||
}
|
||||
|
||||
func isFamily(account *model.Account) bool {
|
||||
return account.InternalType == "Family"
|
||||
}
|
||||
|
||||
func toFamilyOrderBy(o string) string {
|
||||
switch o {
|
||||
case "filename":
|
||||
return "1"
|
||||
case "filesize":
|
||||
return "2"
|
||||
case "lastOpTime":
|
||||
return "3"
|
||||
default:
|
||||
return "1"
|
||||
}
|
||||
}
|
||||
|
||||
func ParseHttpHeader(str string) map[string]string {
|
||||
header := make(map[string]string)
|
||||
for _, value := range strings.Split(str, "&") {
|
||||
i := strings.Index(value, "=")
|
||||
header[strings.TrimSpace(value[0:i])] = strings.TrimSpace(value[i+1:])
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
func MustString(str string, err error) string {
|
||||
return str
|
||||
}
|
||||
|
||||
func MustToBytes(b []byte, err error) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func BoolToNumber(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func MustParseTime(str string) *time.Time {
|
||||
loc, _ := time.LoadLocation("Local")
|
||||
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", str, loc)
|
||||
return &lastOpTime
|
||||
}
|
||||
|
||||
type Params map[string]string
|
||||
|
||||
func (p Params) Set(k, v string) {
|
||||
p[k] = v
|
||||
}
|
||||
|
||||
func (p Params) Encode() string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
var buf strings.Builder
|
||||
keys := make([]string, 0, len(p))
|
||||
for k := range p {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte('&')
|
||||
}
|
||||
buf.WriteString(k)
|
||||
buf.WriteByte('=')
|
||||
buf.WriteString(p[k])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
209
drivers/alidrive/alidrive.go
Normal file
209
drivers/alidrive/alidrive.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package alidrive
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
var aliClient = resty.New()
|
||||
|
||||
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,
|
||||
}
|
||||
f.Type = file.GetType()
|
||||
return f
|
||||
}
|
||||
|
||||
func (driver AliDrive) GetFiles(fileId string, account *model.Account) ([]AliFile, error) {
|
||||
marker := "first"
|
||||
res := make([]AliFile, 0)
|
||||
for marker != "" {
|
||||
if marker == "first" {
|
||||
marker = ""
|
||||
}
|
||||
var resp AliFiles
|
||||
var e AliRespError
|
||||
_, err := aliClient.R().
|
||||
SetResult(&resp).
|
||||
SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"fields": "*",
|
||||
"image_thumbnail_process": "image/resize,w_400/format,jpeg",
|
||||
"image_url_process": "image/resize,w_1920/format,jpeg",
|
||||
"limit": account.Limit,
|
||||
"marker": marker,
|
||||
"order_by": account.OrderBy,
|
||||
"order_direction": account.OrderDirection,
|
||||
"parent_file_id": fileId,
|
||||
"video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300",
|
||||
"url_expire_sec": 14400,
|
||||
}).Post("https://api.aliyundrive.com/v2/file/list")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.GetFiles(fileId, account)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
marker = resp.NextMarker
|
||||
res = append(res, resp.Items...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) GetFile(path string, account *model.Account) (*AliFile, error) {
|
||||
dir, name := filepath.Split(path)
|
||||
dir = utils.ParsePath(dir)
|
||||
_, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentFiles_, _ := base.GetCache(dir, account)
|
||||
parentFiles, _ := parentFiles_.([]AliFile)
|
||||
for _, file := range parentFiles {
|
||||
if file.Name == name {
|
||||
if file.Type == "file" {
|
||||
return &file, err
|
||||
} else {
|
||||
return nil, fmt.Errorf("not file")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver AliDrive) RefreshToken(account *model.Account) error {
|
||||
url := "https://auth.aliyundrive.com/v2/account/token"
|
||||
var resp base.TokenResp
|
||||
var e AliRespError
|
||||
_, err := aliClient.R().
|
||||
//ForceContentType("application/json").
|
||||
SetBody(base.Json{"refresh_token": account.RefreshToken, "grant_type": "refresh_token"}).
|
||||
SetResult(&resp).
|
||||
SetError(&e).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
return err
|
||||
}
|
||||
log.Debugf("%+v,%+v", resp, e)
|
||||
if e.Code != "" {
|
||||
account.Status = e.Message
|
||||
return fmt.Errorf("failed to refresh token: %s", e.Message)
|
||||
} else {
|
||||
account.Status = "work"
|
||||
}
|
||||
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) rename(fileId, name string, account *model.Account) error {
|
||||
var resp base.Json
|
||||
var e AliRespError
|
||||
_, err := aliClient.R().SetResult(&resp).SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"check_name_mode": "refuse",
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": fileId,
|
||||
"name": name,
|
||||
}).Post("https://api.aliyundrive.com/v3/file/update")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.rename(fileId, name, account)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
if resp["name"] == name {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%+v", resp)
|
||||
}
|
||||
|
||||
func (driver AliDrive) batch(srcId, dstId string, url string, account *model.Account) error {
|
||||
var e AliRespError
|
||||
res, err := aliClient.R().SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"requests": []base.Json{
|
||||
{
|
||||
"headers": base.Json{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
"method": "POST",
|
||||
"id": srcId,
|
||||
"body": base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": srcId,
|
||||
"to_drive_id": account.DriveId,
|
||||
"to_parent_file_id": dstId,
|
||||
},
|
||||
"url": url,
|
||||
},
|
||||
},
|
||||
"resource": "file",
|
||||
}).Post("https://api.aliyundrive.com/v3/batch")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.batch(srcId, dstId, url, account)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
status := jsoniter.Get(res.Body(), "responses", 0, "status").ToInt()
|
||||
if status < 400 && status >= 100 {
|
||||
return nil
|
||||
}
|
||||
return errors.New(res.String())
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&AliDrive{})
|
||||
aliClient.
|
||||
SetTimeout(base.DefaultTimeout).
|
||||
SetRetryCount(3).
|
||||
SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36").
|
||||
SetHeader("content-type", "application/json").
|
||||
SetHeader("origin", "https://www.aliyundrive.com")
|
||||
}
|
||||
566
drivers/alidrive/driver.go
Normal file
566
drivers/alidrive/driver.go
Normal file
@@ -0,0 +1,566 @@
|
||||
package alidrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/robfig/cron/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AliDrive struct{}
|
||||
|
||||
func (driver AliDrive) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "AliDrive",
|
||||
}
|
||||
}
|
||||
|
||||
func (driver AliDrive) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "refresh_token",
|
||||
Label: "refresh token",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "name,size,updated_at,created_at",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "ASC,DESC",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "limit",
|
||||
Label: "limit",
|
||||
Type: base.TypeNumber,
|
||||
Required: false,
|
||||
Description: ">0 and <=200",
|
||||
},
|
||||
{
|
||||
Name: "bool_1",
|
||||
Label: "fast upload",
|
||||
Type: base.TypeBool,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver AliDrive) Save(account *model.Account, old *model.Account) error {
|
||||
if old != nil {
|
||||
conf.Cron.Remove(cron.EntryID(old.CronId))
|
||||
}
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if account.RootFolder == "" {
|
||||
account.RootFolder = "root"
|
||||
}
|
||||
if account.Limit == 0 {
|
||||
account.Limit = 200
|
||||
}
|
||||
err := driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var resp base.Json
|
||||
_, _ = aliClient.R().SetResult(&resp).
|
||||
SetBody("{}").
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
Post("https://api.aliyundrive.com/v2/user/get")
|
||||
log.Debugf("user info: %+v", resp)
|
||||
account.DriveId = resp["default_drive_id"].(string)
|
||||
cronId, err := conf.Cron.AddFunc("@every 2h", func() {
|
||||
id := account.ID
|
||||
log.Debugf("ali account id: %d", id)
|
||||
newAccount, err := model.GetAccountById(id)
|
||||
log.Debugf("ali account: %+v", newAccount)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = driver.RefreshToken(newAccount)
|
||||
_ = model.SaveAccount(newAccount)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.CronId = int(cronId)
|
||||
err = model.SaveAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver AliDrive) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var rawFiles []AliFile
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
rawFiles, _ = cache.([]AliFile)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawFiles, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawFiles) > 0 {
|
||||
_ = base.SetCache(path, rawFiles, account)
|
||||
}
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp base.Json
|
||||
var e AliRespError
|
||||
_, err = aliClient.R().SetResult(&resp).
|
||||
SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": file.Id,
|
||||
"expire_sec": 14400,
|
||||
}).Post("https://api.aliyundrive.com/v2/file/get_download_url")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.Link(args, account)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
return &base.Link{
|
||||
Headers: []base.Header{
|
||||
{
|
||||
Name: "Referer",
|
||||
Value: "https://www.aliyundrive.com/",
|
||||
},
|
||||
},
|
||||
Url: resp["url"].(string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("ali path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver AliDrive) Proxy(r *http.Request, account *model.Account) {
|
||||
// r.Header.Del("Origin")
|
||||
// r.Header.Set("Referer", "https://www.aliyundrive.com/")
|
||||
//}
|
||||
|
||||
func (driver AliDrive) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
file, err := driver.GetFile(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// office
|
||||
var resp base.Json
|
||||
var e AliRespError
|
||||
var url string
|
||||
req := base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": file.FileId,
|
||||
}
|
||||
switch file.Category {
|
||||
case "doc":
|
||||
{
|
||||
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
|
||||
req["access_token"] = account.AccessToken
|
||||
}
|
||||
case "video":
|
||||
{
|
||||
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
|
||||
req["category"] = "live_transcoding"
|
||||
}
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
_, err = aliClient.R().SetResult(&resp).SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(req).Post(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Code != "" {
|
||||
return nil, fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (driver AliDrive) MakeDir(path string, account *model.Account) error {
|
||||
dir, name := filepath.Split(path)
|
||||
parentFile, err := driver.File(dir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
var resp base.Json
|
||||
var e AliRespError
|
||||
_, err = aliClient.R().SetResult(&resp).SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"check_name_mode": "refuse",
|
||||
"drive_id": account.DriveId,
|
||||
"name": name,
|
||||
"parent_file_id": parentFile.Id,
|
||||
"type": "folder",
|
||||
}).Post("https://api.aliyundrive.com/adrive/v2/file/createWithFolders")
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.MakeDir(path, account)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
if resp["file_name"] == name {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%+v", resp)
|
||||
}
|
||||
|
||||
func (driver AliDrive) Move(src string, dst string, account *model.Account) error {
|
||||
dstDir, _ := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstDirFile, err := driver.File(dstDir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = driver.batch(srcFile.Id, dstDirFile.Id, "/file/move", account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver AliDrive) Rename(src string, dst string, account *model.Account) error {
|
||||
_, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = driver.rename(srcFile.Id, dstName, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver AliDrive) Copy(src string, dst string, account *model.Account) error {
|
||||
dstDir, _ := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstDirFile, err := driver.File(dstDir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = driver.batch(srcFile.Id, dstDirFile.Id, "/file/copy", account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver AliDrive) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var e AliRespError
|
||||
res, err := aliClient.R().SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": file.Id,
|
||||
}).Post("https://api.aliyundrive.com/v2/recyclebin/trash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.Delete(path, account)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
if res.StatusCode() < 400 {
|
||||
return nil
|
||||
}
|
||||
return errors.New(res.String())
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
FileId string `json:"file_id"`
|
||||
UploadId string `json:"upload_id"`
|
||||
PartInfoList []struct {
|
||||
UploadUrl string `json:"upload_url"`
|
||||
} `json:"part_info_list"`
|
||||
|
||||
RapidUpload bool `json:"rapid_upload"`
|
||||
}
|
||||
|
||||
func (driver AliDrive) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
|
||||
const DEFAULT int64 = 10485760
|
||||
var count = int(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
|
||||
|
||||
partInfoList := make([]base.Json, 0, count)
|
||||
for i := 1; i <= count; i++ {
|
||||
partInfoList = append(partInfoList, base.Json{"part_number": i})
|
||||
}
|
||||
|
||||
reqBody := base.Json{
|
||||
"check_name_mode": "overwrite",
|
||||
"drive_id": account.DriveId,
|
||||
"name": file.GetFileName(),
|
||||
"parent_file_id": parentFile.Id,
|
||||
"part_info_list": partInfoList,
|
||||
"size": file.GetSize(),
|
||||
"type": "file",
|
||||
}
|
||||
|
||||
if account.Bool1 {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
io.CopyN(buf, file, 1024)
|
||||
reqBody["pre_hash"] = utils.GetSHA1Encode(buf.String())
|
||||
// 把头部拼接回去
|
||||
file.File = struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
Reader: io.MultiReader(buf, file.File),
|
||||
Closer: file.File,
|
||||
}
|
||||
} else {
|
||||
reqBody["content_hash_name"] = "none"
|
||||
reqBody["proof_version"] = "v1"
|
||||
}
|
||||
|
||||
var resp UploadResp
|
||||
var e AliRespError
|
||||
client := aliClient.R().SetResult(&resp).SetError(&e).SetHeader("authorization", "Bearer\t"+account.AccessToken).SetBody(reqBody)
|
||||
|
||||
_, err = client.Post("https://api.aliyundrive.com/adrive/v2/file/createWithFolders")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" && e.Code != "PreHashMatched" {
|
||||
if e.Code == "AccessTokenInvalid" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
_ = model.SaveAccount(account)
|
||||
return driver.Upload(file, account)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
|
||||
if account.Bool1 && e.Code == "PreHashMatched" {
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
|
||||
delete(reqBody, "pre_hash")
|
||||
h := sha1.New()
|
||||
if _, err = io.Copy(io.MultiWriter(tempFile, h), file.File); err != nil {
|
||||
return err
|
||||
}
|
||||
reqBody["content_hash"] = hex.EncodeToString(h.Sum(nil))
|
||||
reqBody["content_hash_name"] = "sha1"
|
||||
reqBody["proof_version"] = "v1"
|
||||
|
||||
/*
|
||||
js 隐性转换太坑不知道有没有bug
|
||||
var n = e.access_token,
|
||||
r = new BigNumber('0x'.concat(md5(n).slice(0, 16))),
|
||||
i = new BigNumber(t.file.size),
|
||||
o = i ? r.mod(i) : new gt.BigNumber(0);
|
||||
(t.file.slice(o.toNumber(), Math.min(o.plus(8).toNumber(), t.file.size)))
|
||||
*/
|
||||
buf := make([]byte, 8)
|
||||
r, _ := new(big.Int).SetString(utils.GetMD5Encode(account.AccessToken)[:16], 16)
|
||||
i := new(big.Int).SetUint64(file.Size)
|
||||
o := r.Mod(r, i)
|
||||
n, _ := io.NewSectionReader(tempFile, o.Int64(), 8).Read(buf[:8])
|
||||
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
|
||||
|
||||
_, err = client.Post("https://api.aliyundrive.com/adrive/v2/file/createWithFolders")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" && e.Code != "PreHashMatched" {
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
|
||||
if resp.RapidUpload {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 秒传失败
|
||||
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
file.File = tempFile
|
||||
}
|
||||
|
||||
for _, partInfo := range resp.PartInfoList {
|
||||
req, err := http.NewRequest("PUT", partInfo.UploadUrl, io.LimitReader(file.File, DEFAULT))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("%+v", res)
|
||||
res.Body.Close()
|
||||
//res, err := base.BaseClient.R().
|
||||
// SetHeader("Content-Type","").
|
||||
// SetBody(byteData).Put(resp.PartInfoList[i].UploadUrl)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//log.Debugf("put to %s : %d,%s", resp.PartInfoList[i].UploadUrl, res.StatusCode(),res.String())
|
||||
}
|
||||
var resp2 base.Json
|
||||
_, err = aliClient.R().SetResult(&resp2).SetError(&e).
|
||||
SetHeader("authorization", "Bearer\t"+account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"drive_id": account.DriveId,
|
||||
"file_id": resp.FileId,
|
||||
"upload_id": resp.UploadId,
|
||||
}).Post("https://api.aliyundrive.com/v2/file/complete")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Code != "" && e.Code != "PreHashMatched" {
|
||||
//if e.Code == "AccessTokenInvalid" {
|
||||
// err = driver.RefreshToken(account)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// } else {
|
||||
// _ = model.SaveAccount(account)
|
||||
// return driver.Upload(file, account)
|
||||
// }
|
||||
//}
|
||||
return fmt.Errorf("%s", e.Message)
|
||||
}
|
||||
if resp2["file_id"] == resp.FileId {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%+v", resp2)
|
||||
}
|
||||
|
||||
var _ base.Driver = (*AliDrive)(nil)
|
||||
53
drivers/alidrive/types.go
Normal file
53
drivers/alidrive/types.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package alidrive
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AliRespError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type AliFiles struct {
|
||||
Items []AliFile `json:"items"`
|
||||
NextMarker string `json:"next_marker"`
|
||||
}
|
||||
|
||||
type AliFile struct {
|
||||
DriveId string `json:"drive_id"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
FileExtension string `json:"file_extension"`
|
||||
FileId string `json:"file_id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
ParentFileId string `json:"parent_file_id"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
Size int64 `json:"size"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
func (f AliFile) GetSize() uint64 {
|
||||
return uint64(f.Size)
|
||||
}
|
||||
|
||||
func (f AliFile) GetName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f AliFile) GetType() int {
|
||||
if f.Type == "folder" {
|
||||
return conf.FOLDER
|
||||
}
|
||||
if f.Category == "video" {
|
||||
return conf.VIDEO
|
||||
}
|
||||
if f.Category == "image" {
|
||||
return conf.IMAGE
|
||||
}
|
||||
return utils.GetFileType(f.FileExtension)
|
||||
}
|
||||
44
drivers/alist/alist.go
Normal file
44
drivers/alist/alist.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package alist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
)
|
||||
|
||||
type BaseResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type PathResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Type string `json:"type"`
|
||||
//Meta Meta `json:"meta"`
|
||||
Files []model.File `json:"files"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type PreviewResp struct {
|
||||
BaseResp
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func (driver *Alist) Login(account *model.Account) error {
|
||||
var resp BaseResp
|
||||
_, err := base.RestyClient.R().SetResult(&resp).
|
||||
SetHeader("Authorization", account.AccessToken).
|
||||
Get(account.SiteUrl + "/api/admin/login")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
return errors.New(resp.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Alist{})
|
||||
}
|
||||
192
drivers/alist/driver.go
Normal file
192
drivers/alist/driver.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package alist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Alist struct{}
|
||||
|
||||
func (driver Alist) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Alist",
|
||||
NoNeedSetLink: true,
|
||||
NoCors: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Alist) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "site_url",
|
||||
Label: "alist site url",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "access_token",
|
||||
Label: "token",
|
||||
Type: base.TypeString,
|
||||
Description: "admin token",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Alist) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
account.SiteUrl = strings.TrimRight(account.SiteUrl, "/")
|
||||
if account.RootFolder == "" {
|
||||
account.RootFolder = "/"
|
||||
}
|
||||
err := driver.Login(account)
|
||||
if err == nil {
|
||||
account.Status = "work"
|
||||
} else {
|
||||
account.Status = err.Error()
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Alist) File(path string, account *model.Account) (*model.File, error) {
|
||||
now := time.Now()
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: "root",
|
||||
Name: "root",
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: &now,
|
||||
}, nil
|
||||
}
|
||||
_, files, err := driver.Path(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if files == nil {
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
name := utils.Base(path)
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Alist) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
//return nil, base.ErrNotImplement
|
||||
_, files, err := driver.Path(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if files == nil {
|
||||
return nil, base.ErrNotFolder
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Alist) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
path := args.Path
|
||||
path = utils.ParsePath(path)
|
||||
name := utils.Base(path)
|
||||
flag := "d"
|
||||
if utils.GetFileType(filepath.Ext(path)) == conf.TEXT {
|
||||
flag = "p"
|
||||
}
|
||||
link := base.Link{}
|
||||
link.Url = fmt.Sprintf("%s/%s%s?sign=%s", account.SiteUrl, flag, utils.Join(utils.ParsePath(account.RootFolder), path), utils.SignWithToken(name, conf.Token))
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Alist) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
path = filepath.Join(account.RootFolder, path)
|
||||
path = strings.ReplaceAll(path, "\\", "/")
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files := cache.([]model.File)
|
||||
return nil, files, nil
|
||||
}
|
||||
var resp PathResp
|
||||
_, err = base.RestyClient.R().SetResult(&resp).
|
||||
SetHeader("Authorization", account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"path": path,
|
||||
}).Post(account.SiteUrl + "/api/public/path")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
return nil, nil, errors.New(resp.Message)
|
||||
}
|
||||
if resp.Data.Type == "file" {
|
||||
return &resp.Data.Files[0], nil, nil
|
||||
}
|
||||
if len(resp.Data.Files) > 0 {
|
||||
_ = base.SetCache(path, resp.Data.Files, account)
|
||||
}
|
||||
return nil, resp.Data.Files, nil
|
||||
}
|
||||
|
||||
//func (driver Alist) Proxy(r *http.Request, account *model.Account) {}
|
||||
|
||||
func (driver Alist) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
var resp PathResp
|
||||
_, err := base.RestyClient.R().SetResult(&resp).
|
||||
SetHeader("Authorization", account.AccessToken).
|
||||
SetBody(base.Json{
|
||||
"path": path,
|
||||
}).Post(account.SiteUrl + "/api/public/preview")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
return nil, errors.New(resp.Message)
|
||||
}
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
func (driver Alist) MakeDir(path string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver Alist) Move(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver Alist) Rename(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver Alist) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver Alist) Delete(path string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver Alist) Upload(file *model.FileStream, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Alist)(nil)
|
||||
52
drivers/all.go
Normal file
52
drivers/all.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package drivers
|
||||
|
||||
import (
|
||||
_ "github.com/Xhofe/alist/drivers/123"
|
||||
_ "github.com/Xhofe/alist/drivers/139"
|
||||
_ "github.com/Xhofe/alist/drivers/189"
|
||||
_ "github.com/Xhofe/alist/drivers/189pc"
|
||||
_ "github.com/Xhofe/alist/drivers/alidrive"
|
||||
_ "github.com/Xhofe/alist/drivers/alist"
|
||||
_ "github.com/Xhofe/alist/drivers/baidu"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
_ "github.com/Xhofe/alist/drivers/ftp"
|
||||
_ "github.com/Xhofe/alist/drivers/google"
|
||||
_ "github.com/Xhofe/alist/drivers/lanzou"
|
||||
_ "github.com/Xhofe/alist/drivers/mediatrack"
|
||||
_ "github.com/Xhofe/alist/drivers/native"
|
||||
_ "github.com/Xhofe/alist/drivers/onedrive"
|
||||
_ "github.com/Xhofe/alist/drivers/pikpak"
|
||||
_ "github.com/Xhofe/alist/drivers/quark"
|
||||
_ "github.com/Xhofe/alist/drivers/s3"
|
||||
_ "github.com/Xhofe/alist/drivers/sftp"
|
||||
_ "github.com/Xhofe/alist/drivers/shandian"
|
||||
_ "github.com/Xhofe/alist/drivers/teambition"
|
||||
_ "github.com/Xhofe/alist/drivers/uss"
|
||||
_ "github.com/Xhofe/alist/drivers/webdav"
|
||||
_ "github.com/Xhofe/alist/drivers/xunlei"
|
||||
_ "github.com/Xhofe/alist/drivers/yandex"
|
||||
_ "github.com/Xhofe/alist/drivers/baiduphoto"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var NoCors string
|
||||
var NoUpload string
|
||||
|
||||
func GetConfig() {
|
||||
for k, v := range base.GetDriversMap() {
|
||||
if v.Config().NoCors {
|
||||
NoCors += k + ","
|
||||
}
|
||||
if v.Upload(nil, nil) != base.ErrEmptyFile {
|
||||
NoUpload += k + ","
|
||||
}
|
||||
}
|
||||
NoCors = strings.Trim(NoCors, ",")
|
||||
NoUpload += "root"
|
||||
}
|
||||
|
||||
func init() {
|
||||
log.Debug("all init")
|
||||
GetConfig()
|
||||
}
|
||||
185
drivers/baidu/baidu.go
Normal file
185
drivers/baidu/baidu.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package baidu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (driver Baidu) RefreshToken(account *model.Account) error {
|
||||
err := driver.refreshToken(account)
|
||||
if err != nil && err == base.ErrEmptyToken {
|
||||
err = driver.refreshToken(account)
|
||||
}
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Baidu) refreshToken(account *model.Account) error {
|
||||
u := "https://openapi.baidu.com/oauth/2.0/token"
|
||||
var resp base.TokenResp
|
||||
var e TokenErrResp
|
||||
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": account.RefreshToken,
|
||||
"client_id": account.ClientId,
|
||||
"client_secret": account.ClientSecret,
|
||||
}).Get(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Error != "" {
|
||||
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
|
||||
}
|
||||
if resp.RefreshToken == "" {
|
||||
return base.ErrEmptyToken
|
||||
}
|
||||
account.Status = "work"
|
||||
account.AccessToken, account.RefreshToken = resp.AccessToken, resp.RefreshToken
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Baidu) Request(fullurl string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetQueryParam("access_token", account.AccessToken)
|
||||
if headers != nil {
|
||||
req.SetHeaders(headers)
|
||||
}
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
if form != nil {
|
||||
req.SetFormData(form)
|
||||
}
|
||||
if data != nil {
|
||||
req.SetBody(data)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var res *resty.Response
|
||||
var err error
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(fullurl)
|
||||
case base.Post:
|
||||
res, err = req.Post(fullurl)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(fullurl)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(fullurl)
|
||||
case base.Put:
|
||||
res, err = req.Put(fullurl)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//log.Debug(res.String())
|
||||
errno := jsoniter.Get(res.Body(), "errno").ToInt()
|
||||
if errno != 0 {
|
||||
if errno == -6 {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return driver.Request(fullurl, method, headers, query, form, data, resp, account)
|
||||
}
|
||||
return nil, fmt.Errorf("errno: %d, refer to https://pan.baidu.com/union/doc/", errno)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (driver Baidu) Get(pathname string, params map[string]string, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
return driver.Request("https://pan.baidu.com/rest/2.0"+pathname, base.Get, nil, params, nil, nil, resp, account)
|
||||
}
|
||||
|
||||
func (driver Baidu) Post(pathname string, params map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
return driver.Request("https://pan.baidu.com/rest/2.0"+pathname, base.Post, nil, params, nil, data, resp, account)
|
||||
}
|
||||
|
||||
func (driver Baidu) manage(opera string, filelist interface{}, account *model.Account) ([]byte, error) {
|
||||
params := map[string]string{
|
||||
"method": "filemanager",
|
||||
"opera": opera,
|
||||
}
|
||||
marshal, err := utils.Json.Marshal(filelist)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := fmt.Sprintf("async=0&filelist=%s&ondup=newcopy", string(marshal))
|
||||
return driver.Post("/xpan/file", params, data, nil, account)
|
||||
}
|
||||
|
||||
func (driver Baidu) GetFiles(dir string, account *model.Account) ([]model.File, error) {
|
||||
dir = utils.Join(account.RootFolder, dir)
|
||||
start := 0
|
||||
limit := 200
|
||||
params := map[string]string{
|
||||
"method": "list",
|
||||
"dir": dir,
|
||||
"web": "web",
|
||||
}
|
||||
if account.OrderBy != "" {
|
||||
params["order"] = account.OrderBy
|
||||
if account.OrderDirection == "desc" {
|
||||
params["desc"] = "1"
|
||||
}
|
||||
}
|
||||
res := make([]model.File, 0)
|
||||
for {
|
||||
params["start"] = strconv.Itoa(start)
|
||||
params["limit"] = strconv.Itoa(limit)
|
||||
start += limit
|
||||
var resp ListResp
|
||||
_, err := driver.Get("/xpan/file", params, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.List) == 0 {
|
||||
break
|
||||
}
|
||||
for _, f := range resp.List {
|
||||
file := model.File{
|
||||
Id: strconv.FormatInt(f.FsId, 10),
|
||||
Name: f.ServerFilename,
|
||||
Size: f.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(f.ServerMtime),
|
||||
Thumbnail: f.Thumbs.Url3,
|
||||
}
|
||||
if f.Isdir == 1 {
|
||||
file.Type = conf.FOLDER
|
||||
} else {
|
||||
file.Type = utils.GetFileType(path.Ext(f.ServerFilename))
|
||||
}
|
||||
res = append(res, file)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver Baidu) create(path string, size uint64, isdir int, uploadid, block_list string, account *model.Account) ([]byte, error) {
|
||||
params := map[string]string{
|
||||
"method": "create",
|
||||
}
|
||||
data := fmt.Sprintf("path=%s&size=%d&isdir=%d", path, size, isdir)
|
||||
if uploadid != "" {
|
||||
data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list)
|
||||
}
|
||||
return driver.Post("/xpan/file", params, data, nil, account)
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Baidu{})
|
||||
}
|
||||
399
drivers/baidu/driver.go
Normal file
399
drivers/baidu/driver.go
Normal file
@@ -0,0 +1,399 @@
|
||||
package baidu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Baidu struct{}
|
||||
|
||||
func (driver Baidu) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Baidu.Disk",
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Baidu) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "refresh_token",
|
||||
Label: "refresh token",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Default: "/",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Default: "name",
|
||||
Values: "name,time,size",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "asc,desc",
|
||||
Default: "asc",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "download api",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "official,crack",
|
||||
Default: "official",
|
||||
},
|
||||
{
|
||||
Name: "client_id",
|
||||
Label: "client id",
|
||||
Default: "iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "client_secret",
|
||||
Label: "client secret",
|
||||
Default: "jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Baidu) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
return driver.RefreshToken(account)
|
||||
}
|
||||
|
||||
func (driver Baidu) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Baidu) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ := cache.([]model.File)
|
||||
return files, nil
|
||||
}
|
||||
files, err := driver.GetFiles(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Baidu) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
if account.InternalType == "crack" {
|
||||
return driver.LinkCrack(args, account)
|
||||
}
|
||||
return driver.LinkOfficial(args, account)
|
||||
}
|
||||
|
||||
func (driver Baidu) LinkOfficial(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.IsDir() {
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
var resp DownloadResp
|
||||
params := map[string]string{
|
||||
"method": "filemetas",
|
||||
"fsids": fmt.Sprintf("[%s]", file.Id),
|
||||
"dlink": "1",
|
||||
}
|
||||
_, err = driver.Get("/xpan/multimedia", params, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := fmt.Sprintf("%s&access_token=%s", resp.List[0].Dlink, account.AccessToken)
|
||||
res, err := base.NoRedirectClient.R().SetHeader("User-Agent", "pan.baidu.com").Head(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//if res.StatusCode() == 302 {
|
||||
u = res.Header().Get("location")
|
||||
//}
|
||||
return &base.Link{
|
||||
Url: u,
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: "pan.baidu.com"},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (driver Baidu) LinkCrack(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.IsDir() {
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
var resp DownloadResp2
|
||||
param := map[string]string{
|
||||
"target": fmt.Sprintf("[\"%s\"]", utils.Join(account.RootFolder, args.Path)),
|
||||
"dlink": "1",
|
||||
"web": "5",
|
||||
"origin": "dlna",
|
||||
}
|
||||
_, err = driver.Request("https://pan.baidu.com/api/filemetas", base.Get, nil, param, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{
|
||||
Url: resp.Info[0].Dlink,
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: "pan.baidu.com"},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (driver Baidu) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver Baidu) Proxy(r *http.Request, account *model.Account) {
|
||||
// r.Header.Set("User-Agent", "pan.baidu.com")
|
||||
//}
|
||||
|
||||
func (driver Baidu) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Baidu) MakeDir(path string, account *model.Account) error {
|
||||
_, err := driver.create(utils.Join(account.RootFolder, path), 0, 1, "", "", account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Baidu) Move(src string, dst string, account *model.Account) error {
|
||||
path := utils.Join(account.RootFolder, src)
|
||||
dest, newname := utils.Split(utils.Join(account.RootFolder, dst))
|
||||
data := []base.Json{
|
||||
{
|
||||
"path": path,
|
||||
"dest": dest,
|
||||
"newname": newname,
|
||||
},
|
||||
}
|
||||
_, err := driver.manage("move", data, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Baidu) Rename(src string, dst string, account *model.Account) error {
|
||||
path := utils.Join(account.RootFolder, src)
|
||||
newname := utils.Base(dst)
|
||||
data := []base.Json{
|
||||
{
|
||||
"path": path,
|
||||
"newname": newname,
|
||||
},
|
||||
}
|
||||
_, err := driver.manage("rename", data, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Baidu) Copy(src string, dst string, account *model.Account) error {
|
||||
path := utils.Join(account.RootFolder, src)
|
||||
dest, newname := utils.Split(utils.Join(account.RootFolder, dst))
|
||||
data := []base.Json{
|
||||
{
|
||||
"path": path,
|
||||
"dest": dest,
|
||||
"newname": newname,
|
||||
},
|
||||
}
|
||||
_, err := driver.manage("copy", data, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Baidu) Delete(path string, account *model.Account) error {
|
||||
path = utils.Join(account.RootFolder, path)
|
||||
data := []string{path}
|
||||
_, err := driver.manage("delete", data, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Baidu) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
_, err = io.Copy(tempFile, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var Default uint64 = 4 * 1024 * 1024
|
||||
defaultByteData := make([]byte, Default)
|
||||
count := int(math.Ceil(float64(file.GetSize()) / float64(Default)))
|
||||
var SliceSize uint64 = 256 * 1024
|
||||
// cal md5
|
||||
h1 := md5.New()
|
||||
h2 := md5.New()
|
||||
block_list := make([]string, 0)
|
||||
content_md5 := ""
|
||||
slice_md5 := ""
|
||||
left := file.GetSize()
|
||||
for i := 0; i < count; i++ {
|
||||
byteSize := Default
|
||||
var byteData []byte
|
||||
if left < Default {
|
||||
byteSize = left
|
||||
byteData = make([]byte, byteSize)
|
||||
} else {
|
||||
byteData = defaultByteData
|
||||
}
|
||||
left -= byteSize
|
||||
_, err = io.ReadFull(tempFile, byteData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h1.Write(byteData)
|
||||
h2.Write(byteData)
|
||||
block_list = append(block_list, fmt.Sprintf("\"%s\"", hex.EncodeToString(h2.Sum(nil))))
|
||||
h2.Reset()
|
||||
}
|
||||
content_md5 = hex.EncodeToString(h1.Sum(nil))
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if file.GetSize() <= SliceSize {
|
||||
slice_md5 = content_md5
|
||||
} else {
|
||||
sliceData := make([]byte, SliceSize)
|
||||
_, err = io.ReadFull(tempFile, sliceData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h2.Write(sliceData)
|
||||
slice_md5 = hex.EncodeToString(h2.Sum(nil))
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
path := encodeURIComponent(utils.Join(account.RootFolder, file.ParentPath, file.Name))
|
||||
block_list_str := fmt.Sprintf("[%s]", strings.Join(block_list, ","))
|
||||
data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&block_list=%s&content-md5=%s&slice-md5=%s",
|
||||
path, file.GetSize(),
|
||||
block_list_str,
|
||||
content_md5, slice_md5)
|
||||
params := map[string]string{
|
||||
"method": "precreate",
|
||||
}
|
||||
var precreateResp PrecreateResp
|
||||
_, err = driver.Post("/xpan/file", params, data, &precreateResp, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("%+v", precreateResp)
|
||||
if precreateResp.ReturnType == 2 {
|
||||
return nil
|
||||
}
|
||||
params = map[string]string{
|
||||
"method": "upload",
|
||||
"access_token": account.AccessToken,
|
||||
"type": "tmpfile",
|
||||
"path": path,
|
||||
"uploadid": precreateResp.Uploadid,
|
||||
}
|
||||
left = file.GetSize()
|
||||
for _, partseq := range precreateResp.BlockList {
|
||||
byteSize := Default
|
||||
var byteData []byte
|
||||
if left < Default {
|
||||
byteSize = left
|
||||
byteData = make([]byte, byteSize)
|
||||
} else {
|
||||
byteData = defaultByteData
|
||||
}
|
||||
left -= byteSize
|
||||
_, err = io.ReadFull(tempFile, byteData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := "https://d.pcs.baidu.com/rest/2.0/pcs/superfile2"
|
||||
params["partseq"] = strconv.Itoa(partseq)
|
||||
res, err := base.RestyClient.R().SetQueryParams(params).SetFileReader("file", file.Name, bytes.NewReader(byteData)).Post(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugln(res.String())
|
||||
}
|
||||
_, err = driver.create(path, file.GetSize(), 0, precreateResp.Uploadid, block_list_str, account)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Baidu)(nil)
|
||||
@@ -1,13 +1,4 @@
|
||||
package baidu_netdisk
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
)
|
||||
package baidu
|
||||
|
||||
type TokenErrResp struct {
|
||||
ErrorDescription string `json:"error_description"`
|
||||
@@ -17,10 +8,12 @@ type TokenErrResp struct {
|
||||
type File struct {
|
||||
//TkbindId int `json:"tkbind_id"`
|
||||
//OwnerType int `json:"owner_type"`
|
||||
Category int `json:"category"`
|
||||
//Category int `json:"category"`
|
||||
//RealCategory string `json:"real_category"`
|
||||
FsId int64 `json:"fs_id"`
|
||||
FsId int64 `json:"fs_id"`
|
||||
ServerMtime int64 `json:"server_mtime"`
|
||||
//OperId int `json:"oper_id"`
|
||||
//ServerCtime int `json:"server_ctime"`
|
||||
Thumbs struct {
|
||||
//Icon string `json:"icon"`
|
||||
Url3 string `json:"url3"`
|
||||
@@ -28,55 +21,19 @@ type File struct {
|
||||
//Url1 string `json:"url1"`
|
||||
} `json:"thumbs"`
|
||||
//Wpfile int `json:"wpfile"`
|
||||
|
||||
//LocalMtime int `json:"local_mtime"`
|
||||
Size int64 `json:"size"`
|
||||
//ExtentTinyint7 int `json:"extent_tinyint7"`
|
||||
Path string `json:"path"`
|
||||
//Share int `json:"share"`
|
||||
//ServerAtime int `json:"server_atime"`
|
||||
//Pl int `json:"pl"`
|
||||
//LocalCtime int `json:"local_ctime"`
|
||||
ServerFilename string `json:"server_filename"`
|
||||
Md5 string `json:"md5"`
|
||||
//Md5 string `json:"md5"`
|
||||
//OwnerId int `json:"owner_id"`
|
||||
//Unlist int `json:"unlist"`
|
||||
Isdir int `json:"isdir"`
|
||||
|
||||
// list resp
|
||||
ServerCtime int64 `json:"server_ctime"`
|
||||
ServerMtime int64 `json:"server_mtime"`
|
||||
LocalMtime int64 `json:"local_mtime"`
|
||||
LocalCtime int64 `json:"local_ctime"`
|
||||
//ServerAtime int64 `json:"server_atime"` `
|
||||
|
||||
// only create and precreate resp
|
||||
Ctime int64 `json:"ctime"`
|
||||
Mtime int64 `json:"mtime"`
|
||||
}
|
||||
|
||||
func fileToObj(f File) *model.ObjThumb {
|
||||
if f.ServerFilename == "" {
|
||||
f.ServerFilename = path.Base(f.Path)
|
||||
}
|
||||
if f.ServerCtime == 0 {
|
||||
f.ServerCtime = f.Ctime
|
||||
}
|
||||
if f.ServerMtime == 0 {
|
||||
f.ServerMtime = f.Mtime
|
||||
}
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: strconv.FormatInt(f.FsId, 10),
|
||||
Path: f.Path,
|
||||
Name: f.ServerFilename,
|
||||
Size: f.Size,
|
||||
Modified: time.Unix(f.ServerMtime, 0),
|
||||
Ctime: time.Unix(f.ServerCtime, 0),
|
||||
IsFolder: f.Isdir == 1,
|
||||
|
||||
// 直接获取的MD5是错误的
|
||||
HashInfo: utils.NewHashInfo(utils.MD5, DecryptMd5(f.Md5)),
|
||||
},
|
||||
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbs.Url3},
|
||||
}
|
||||
}
|
||||
|
||||
type ListResp struct {
|
||||
@@ -177,15 +134,10 @@ type DownloadResp2 struct {
|
||||
}
|
||||
|
||||
type PrecreateResp struct {
|
||||
Errno int `json:"errno"`
|
||||
RequestId int64 `json:"request_id"`
|
||||
ReturnType int `json:"return_type"`
|
||||
|
||||
// return_type=1
|
||||
Path string `json:"path"`
|
||||
Uploadid string `json:"uploadid"`
|
||||
BlockList []int `json:"block_list"`
|
||||
|
||||
// return_type=2
|
||||
File File `json:"info"`
|
||||
Path string `json:"path"`
|
||||
Uploadid string `json:"uploadid"`
|
||||
ReturnType int `json:"return_type"`
|
||||
BlockList []int `json:"block_list"`
|
||||
Errno int `json:"errno"`
|
||||
RequestId int64 `json:"request_id"`
|
||||
}
|
||||
18
drivers/baidu/util.go
Normal file
18
drivers/baidu/util.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package baidu
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getTime(t int64) *time.Time {
|
||||
tm := time.Unix(t, 0)
|
||||
return &tm
|
||||
}
|
||||
|
||||
func encodeURIComponent(str string) string {
|
||||
r := url.QueryEscape(str)
|
||||
r = strings.ReplaceAll(r, "+", "%20")
|
||||
return r
|
||||
}
|
||||
259
drivers/baiduphoto/baidu.go
Normal file
259
drivers/baiduphoto/baidu.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package baiduphoto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (driver Baidu) RefreshToken(account *model.Account) error {
|
||||
err := driver.refreshToken(account)
|
||||
if err != nil && err == base.ErrEmptyToken {
|
||||
err = driver.refreshToken(account)
|
||||
}
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Baidu) refreshToken(account *model.Account) error {
|
||||
u := "https://openapi.baidu.com/oauth/2.0/token"
|
||||
var resp base.TokenResp
|
||||
var e TokenErrResp
|
||||
_, err := base.RestyClient.R().
|
||||
SetResult(&resp).
|
||||
SetError(&e).
|
||||
SetQueryParams(map[string]string{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": account.RefreshToken,
|
||||
"client_id": account.ClientId,
|
||||
"client_secret": account.ClientSecret,
|
||||
}).Get(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ErrorMsg != "" {
|
||||
return &e
|
||||
}
|
||||
if resp.RefreshToken == "" {
|
||||
return base.ErrEmptyToken
|
||||
}
|
||||
account.Status = "work"
|
||||
account.AccessToken, account.RefreshToken = resp.AccessToken, resp.RefreshToken
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Baidu) Request(method string, url string, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetQueryParam("access_token", account.AccessToken)
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
|
||||
res, err := req.Execute(method, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
|
||||
var erron Erron
|
||||
if err = utils.Json.Unmarshal(res.Body(), &erron); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch erron.Errno {
|
||||
case 0:
|
||||
return res, nil
|
||||
case -6:
|
||||
if err = driver.RefreshToken(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron.Errno)
|
||||
}
|
||||
return driver.Request(method, url, callback, account)
|
||||
}
|
||||
|
||||
// 获取所有根文件
|
||||
func (driver Baidu) GetAllFile(account *model.Account) (files []File, err error) {
|
||||
var cursor string
|
||||
|
||||
for {
|
||||
var resp FileListResp
|
||||
_, err = driver.Request(http.MethodGet, FILE_API_URL_V1+"/list", func(r *resty.Request) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"need_thumbnail": "1",
|
||||
"need_filter_hidden": "0",
|
||||
"cursor": cursor,
|
||||
})
|
||||
r.SetResult(&resp)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cursor = resp.Cursor
|
||||
files = append(files, resp.List...)
|
||||
|
||||
if !resp.HasNextPage() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有相册
|
||||
func (driver Baidu) GetAllAlbum(account *model.Account) (albums []Album, err error) {
|
||||
var cursor string
|
||||
for {
|
||||
var resp AlbumListResp
|
||||
_, err = driver.Request(http.MethodGet, ALBUM_API_URL+"/list", func(r *resty.Request) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"need_amount": "1",
|
||||
"limit": "100",
|
||||
"cursor": cursor,
|
||||
})
|
||||
r.SetResult(&resp)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if albums == nil {
|
||||
albums = make([]Album, 0, resp.TotalCount)
|
||||
}
|
||||
|
||||
cursor = resp.Cursor
|
||||
albums = append(albums, resp.List...)
|
||||
|
||||
if !resp.HasNextPage() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取相册中所有文件
|
||||
func (driver Baidu) GetAllAlbumFile(albumID string, account *model.Account) (files []AlbumFile, err error) {
|
||||
var cursor string
|
||||
for {
|
||||
var resp AlbumFileListResp
|
||||
_, err = driver.Request(http.MethodGet, ALBUM_API_URL+"/listfile", func(r *resty.Request) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"album_id": splitID(albumID)[0],
|
||||
"need_amount": "1",
|
||||
"limit": "1000",
|
||||
"cursor": cursor,
|
||||
})
|
||||
r.SetResult(&resp)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if files == nil {
|
||||
files = make([]AlbumFile, 0, resp.TotalCount)
|
||||
}
|
||||
|
||||
cursor = resp.Cursor
|
||||
files = append(files, resp.List...)
|
||||
|
||||
if !resp.HasNextPage() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建相册
|
||||
func (driver Baidu) CreateAlbum(name string, account *model.Account) error {
|
||||
if !checkName(name) {
|
||||
return ErrNotSupportName
|
||||
}
|
||||
_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/create", func(r *resty.Request) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"title": name,
|
||||
"tid": getTid(),
|
||||
"source": "0",
|
||||
})
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
// 相册改名
|
||||
func (driver Baidu) SetAlbumName(albumID string, name string, account *model.Account) error {
|
||||
if !checkName(name) {
|
||||
return ErrNotSupportName
|
||||
}
|
||||
|
||||
e := splitID(albumID)
|
||||
_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/settitle", func(r *resty.Request) {
|
||||
r.SetFormData(map[string]string{
|
||||
"title": name,
|
||||
"album_id": e[0],
|
||||
"tid": e[1],
|
||||
})
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除相册
|
||||
func (driver Baidu) DeleteAlbum(albumID string, account *model.Account) error {
|
||||
e := splitID(albumID)
|
||||
_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/delete", func(r *resty.Request) {
|
||||
r.SetFormData(map[string]string{
|
||||
"album_id": e[0],
|
||||
"tid": e[1],
|
||||
"delete_origin_image": "0", // 是否删除原图 0 不删除
|
||||
})
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除相册文件
|
||||
func (driver Baidu) DeleteAlbumFile(albumID string, account *model.Account, fileIDs ...string) error {
|
||||
e := splitID(albumID)
|
||||
_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/delfile", func(r *resty.Request) {
|
||||
r.SetFormData(map[string]string{
|
||||
"album_id": e[0],
|
||||
"tid": e[1],
|
||||
"list": fsidsFormat(fileIDs...),
|
||||
"del_origin": "0", // 是否删除原图 0 不删除 1 删除
|
||||
})
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
// 增加相册文件
|
||||
func (driver Baidu) AddAlbumFile(albumID string, account *model.Account, fileIDs ...string) error {
|
||||
e := splitID(albumID)
|
||||
_, err := driver.Request(http.MethodGet, ALBUM_API_URL+"/addfile", func(r *resty.Request) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"album_id": e[0],
|
||||
"tid": e[1],
|
||||
"list": fsidsFormatNotUk(fileIDs...),
|
||||
})
|
||||
}, account)
|
||||
return err
|
||||
}
|
||||
|
||||
// 保存相册文件为根文件
|
||||
func (driver Baidu) CopyAlbumFile(albumID string, account *model.Account, fileID string) (*CopyFile, error) {
|
||||
var resp CopyFileResp
|
||||
e := splitID(fileID)
|
||||
_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/copyfile", func(r *resty.Request) {
|
||||
r.SetFormData(map[string]string{
|
||||
"album_id": splitID(albumID)[0],
|
||||
"tid": e[2],
|
||||
"uk": e[1],
|
||||
"list": fsidsFormatNotUk(fileID),
|
||||
})
|
||||
r.SetResult(&resp)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.List[0], err
|
||||
}
|
||||
502
drivers/baiduphoto/driver.go
Normal file
502
drivers/baiduphoto/driver.go
Normal file
@@ -0,0 +1,502 @@
|
||||
package baiduphoto
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type Baidu struct{}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(new(Baidu))
|
||||
}
|
||||
|
||||
func (driver Baidu) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Baidu.Photo",
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Baidu) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "refresh_token",
|
||||
Label: "refresh token",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "album_id",
|
||||
Type: base.TypeString,
|
||||
},
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "download api",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "file,album",
|
||||
Default: "album",
|
||||
},
|
||||
{
|
||||
Name: "client_id",
|
||||
Label: "client id",
|
||||
Default: "iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "client_secret",
|
||||
Label: "client secret",
|
||||
Default: "jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Baidu) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
return driver.RefreshToken(account)
|
||||
}
|
||||
|
||||
func (driver Baidu) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
dir, name := utils.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Baidu) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var files []model.File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ = cache.([]model.File)
|
||||
return files, nil
|
||||
}
|
||||
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if IsAlbum(file) {
|
||||
albumFiles, err := driver.GetAllAlbumFile(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = make([]model.File, 0, len(albumFiles))
|
||||
for _, file := range albumFiles {
|
||||
var thumbnail string
|
||||
if len(file.Thumburl) > 0 {
|
||||
thumbnail = file.Thumburl[0]
|
||||
}
|
||||
files = append(files, model.File{
|
||||
Id: joinID(file.Fsid, file.Uk, file.Tid),
|
||||
Name: file.Name(),
|
||||
Size: file.Size,
|
||||
Type: utils.GetFileType(utils.Ext(file.Path)),
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(file.Mtime),
|
||||
Thumbnail: thumbnail,
|
||||
})
|
||||
}
|
||||
} else if IsRoot(file) {
|
||||
albums, err := driver.GetAllAlbum(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files = make([]model.File, 0, len(albums))
|
||||
for _, album := range albums {
|
||||
files = append(files, model.File{
|
||||
Id: joinID(album.AlbumID, album.Tid),
|
||||
Name: album.Title,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(album.Mtime),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Baidu) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
if account.InternalType == "file" {
|
||||
return driver.LinkFile(args, account)
|
||||
}
|
||||
return driver.LinkAlbum(args, account)
|
||||
}
|
||||
|
||||
func (driver Baidu) LinkAlbum(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !IsAlbumFile(file) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
album, err := driver.File(utils.Dir(utils.ParsePath(args.Path)), account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := splitID(file.Id)
|
||||
res, err := base.NoRedirectClient.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"access_token": account.AccessToken,
|
||||
"album_id": splitID(album.Id)[0],
|
||||
"tid": e[2],
|
||||
"fsid": e[0],
|
||||
"uk": e[1],
|
||||
}).
|
||||
Head(ALBUM_API_URL + "/download")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: base.UserAgent},
|
||||
},
|
||||
Url: res.Header().Get("location"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver Baidu) LinkFile(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !IsAlbumFile(file) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
album, err := driver.File(utils.Dir(utils.ParsePath(args.Path)), account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 拷贝到根目录
|
||||
cfile, err := driver.CopyAlbumFile(album.Id, account, file.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := driver.Request(http.MethodGet, FILE_API_URL_V2+"/download", func(r *resty.Request) {
|
||||
r.SetQueryParams(map[string]string{
|
||||
"fsid": fmt.Sprint(cfile.Fsid),
|
||||
})
|
||||
}, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: base.UserAgent},
|
||||
},
|
||||
Url: utils.Json.Get(res.Body(), "dlink").ToString(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver Baidu) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
func (driver Baidu) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Baidu) Rename(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if IsAlbum(srcFile) {
|
||||
return driver.SetAlbumName(srcFile.Id, utils.Base(dst), account)
|
||||
}
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Baidu) MakeDir(path string, account *model.Account) error {
|
||||
dir, name := utils.Split(path)
|
||||
parentFile, err := driver.File(dir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !IsRoot(parentFile) {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
return driver.CreateAlbum(name, account)
|
||||
}
|
||||
|
||||
func (driver Baidu) Move(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if IsAlbumFile(srcFile) {
|
||||
// 移动相册文件
|
||||
dstAlbum, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !IsAlbum(dstAlbum) {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
srcAlbum, err := driver.File(utils.Dir(src), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newFile, err := driver.CopyAlbumFile(srcAlbum.Id, account, srcFile.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = driver.DeleteAlbumFile(srcAlbum.Id, account, srcFile.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = driver.AddAlbumFile(dstAlbum.Id, account, joinID(newFile.Fsid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Baidu) Copy(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if IsAlbumFile(srcFile) {
|
||||
// 复制相册文件
|
||||
dstAlbum, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !IsAlbum(dstAlbum) {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
srcAlbum, err := driver.File(utils.Dir(src), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newFile, err := driver.CopyAlbumFile(srcAlbum.Id, account, srcFile.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = driver.AddAlbumFile(dstAlbum.Id, account, joinID(newFile.Fsid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Baidu) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除相册
|
||||
if IsAlbum(file) {
|
||||
return driver.DeleteAlbum(file.Id, account)
|
||||
}
|
||||
|
||||
// 生成相册文件
|
||||
if IsAlbumFile(file) {
|
||||
// 删除相册文件
|
||||
album, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return driver.DeleteAlbumFile(album.Id, account, file.Id)
|
||||
}
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Baidu) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !IsAlbum(parentFile) {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
tempFile.Close()
|
||||
os.Remove(tempFile.Name())
|
||||
}()
|
||||
|
||||
// 计算需要的数据
|
||||
const DEFAULT = 1 << 22
|
||||
const SliceSize = 1 << 18
|
||||
count := int(math.Ceil(float64(file.Size) / float64(DEFAULT)))
|
||||
|
||||
sliceMD5List := make([]string, 0, count)
|
||||
fileMd5 := md5.New()
|
||||
sliceMd5 := md5.New()
|
||||
for i := 1; i <= count; i++ {
|
||||
if n, err := io.CopyN(io.MultiWriter(fileMd5, sliceMd5, tempFile), file, DEFAULT); err != io.EOF && n == 0 {
|
||||
return err
|
||||
}
|
||||
sliceMD5List = append(sliceMD5List, hex.EncodeToString(sliceMd5.Sum(nil)))
|
||||
sliceMd5.Reset()
|
||||
}
|
||||
|
||||
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content_md5 := hex.EncodeToString(fileMd5.Sum(nil))
|
||||
slice_md5 := content_md5
|
||||
if file.GetSize() > SliceSize {
|
||||
sliceData := make([]byte, SliceSize)
|
||||
if _, err = io.ReadFull(tempFile, sliceData); err != nil {
|
||||
return err
|
||||
}
|
||||
sliceMd5.Write(sliceData)
|
||||
slice_md5 = hex.EncodeToString(sliceMd5.Sum(nil))
|
||||
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 开始执行上传
|
||||
params := map[string]string{
|
||||
"autoinit": "1",
|
||||
"isdir": "0",
|
||||
"rtype": "1",
|
||||
"ctype": "11",
|
||||
"path": utils.ParsePath(file.Name),
|
||||
"size": fmt.Sprint(file.Size),
|
||||
"slice-md5": slice_md5,
|
||||
"content-md5": content_md5,
|
||||
"block_list": MustString(utils.Json.MarshalToString(sliceMD5List)),
|
||||
}
|
||||
|
||||
// 预上传
|
||||
var precreateResp PrecreateResp
|
||||
_, err = driver.Request(http.MethodPost, FILE_API_URL_V1+"/precreate", func(r *resty.Request) {
|
||||
r.SetFormData(params)
|
||||
r.SetResult(&precreateResp)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch precreateResp.ReturnType {
|
||||
case 1: // 上传文件
|
||||
uploadParams := map[string]string{
|
||||
"method": "upload",
|
||||
"path": params["path"],
|
||||
"uploadid": precreateResp.UploadID,
|
||||
}
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
uploadParams["partseq"] = fmt.Sprint(i)
|
||||
_, err = driver.Request(http.MethodPost, "https://c3.pcs.baidu.com/rest/2.0/pcs/superfile2", func(r *resty.Request) {
|
||||
r.SetQueryParams(uploadParams)
|
||||
r.SetFileReader("file", file.Name, io.LimitReader(tempFile, DEFAULT))
|
||||
}, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
case 2: // 创建文件
|
||||
params["uploadid"] = precreateResp.UploadID
|
||||
_, err = driver.Request(http.MethodPost, FILE_API_URL_V1+"/create", func(r *resty.Request) {
|
||||
r.SetFormData(params)
|
||||
r.SetResult(&precreateResp)
|
||||
}, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fallthrough
|
||||
case 3: // 增加到相册
|
||||
err = driver.AddAlbumFile(parentFile.Id, account, joinID(precreateResp.Data.FsID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Baidu)(nil)
|
||||
126
drivers/baiduphoto/types.go
Normal file
126
drivers/baiduphoto/types.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package baiduphoto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Xhofe/alist/utils"
|
||||
)
|
||||
|
||||
type TokenErrResp struct {
|
||||
ErrorDescription string `json:"error_description"`
|
||||
ErrorMsg string `json:"error"`
|
||||
}
|
||||
|
||||
func (e *TokenErrResp) Error() string {
|
||||
return fmt.Sprint(e.ErrorMsg, " : ", e.ErrorDescription)
|
||||
}
|
||||
|
||||
type Erron struct {
|
||||
Errno int `json:"errno"`
|
||||
RequestID int `json:"request_id"`
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
HasMore int `json:"has_more"`
|
||||
Cursor string `json:"cursor"`
|
||||
}
|
||||
|
||||
func (p Page) HasNextPage() bool {
|
||||
return p.HasMore == 1
|
||||
}
|
||||
|
||||
type (
|
||||
FileListResp struct {
|
||||
Page
|
||||
List []File `json:"list"`
|
||||
}
|
||||
|
||||
File struct {
|
||||
Fsid int64 `json:"fsid"` // 文件ID
|
||||
Path string `json:"path"` // 文件路径
|
||||
Size int64 `json:"size"`
|
||||
Ctime int64 `json:"ctime"` // 创建时间 s
|
||||
Mtime int64 `json:"mtime"` // 修改时间 s
|
||||
Thumburl []string `json:"thumburl"`
|
||||
}
|
||||
)
|
||||
|
||||
func (f File) Name() string {
|
||||
return utils.Base(f.Path)
|
||||
}
|
||||
|
||||
/*相册部分*/
|
||||
type (
|
||||
AlbumListResp struct {
|
||||
Page
|
||||
List []Album `json:"list"`
|
||||
Reset int64 `json:"reset"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
Album struct {
|
||||
AlbumID string `json:"album_id"`
|
||||
Tid int64 `json:"tid"`
|
||||
Title string `json:"title"`
|
||||
JoinTime int64 `json:"join_time"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
Mtime int64 `json:"mtime"`
|
||||
}
|
||||
|
||||
AlbumFileListResp struct {
|
||||
Page
|
||||
List []AlbumFile `json:"list"`
|
||||
Reset int64 `json:"reset"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
AlbumFile struct {
|
||||
File
|
||||
Tid int64 `json:"tid"`
|
||||
Uk int64 `json:"uk"`
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
CopyFileResp struct {
|
||||
List []CopyFile `json:"list"`
|
||||
}
|
||||
CopyFile struct {
|
||||
FromFsid int64 `json:"from_fsid"` // 源ID
|
||||
Fsid int64 `json:"fsid"` // 目标ID
|
||||
Path string `json:"path"`
|
||||
ShootTime int `json:"shoot_time"`
|
||||
}
|
||||
)
|
||||
|
||||
/*上传部分*/
|
||||
type (
|
||||
UploadFile struct {
|
||||
FsID int64 `json:"fs_id"`
|
||||
Size int64 `json:"size"`
|
||||
Md5 string `json:"md5"`
|
||||
ServerFilename string `json:"server_filename"`
|
||||
Path string `json:"path"`
|
||||
Ctime int `json:"ctime"`
|
||||
Mtime int `json:"mtime"`
|
||||
Isdir int `json:"isdir"`
|
||||
Category int `json:"category"`
|
||||
ServerMd5 string `json:"server_md5"`
|
||||
ShootTime int `json:"shoot_time"`
|
||||
}
|
||||
|
||||
CreateFileResp struct {
|
||||
Data UploadFile `json:"data"`
|
||||
}
|
||||
|
||||
PrecreateResp struct {
|
||||
ReturnType int `json:"return_type"` //存在返回2 不存在返回1 已经保存3
|
||||
//存在返回
|
||||
CreateFileResp
|
||||
|
||||
//不存在返回
|
||||
Path string `json:"path"`
|
||||
UploadID string `json:"uploadid"`
|
||||
Blocklist []int64 `json:"block_list"`
|
||||
}
|
||||
)
|
||||
84
drivers/baiduphoto/util.go
Normal file
84
drivers/baiduphoto/util.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package baiduphoto
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/alist/model"
|
||||
)
|
||||
|
||||
const (
|
||||
API_URL = "https://photo.baidu.com/youai"
|
||||
ALBUM_API_URL = API_URL + "/album/v1"
|
||||
FILE_API_URL_V1 = API_URL + "/file/v1"
|
||||
FILE_API_URL_V2 = API_URL + "/file/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotSupportName = errors.New("only chinese and english, numbers and underscores are supported, and the length is no more than 20")
|
||||
)
|
||||
|
||||
//Tid生成
|
||||
func getTid() string {
|
||||
return fmt.Sprintf("3%d%.0f", time.Now().Unix(), math.Floor(9000000*rand.Float64()+1000000))
|
||||
}
|
||||
|
||||
// 检查名称
|
||||
func checkName(name string) bool {
|
||||
return len(name) <= 20 && regexp.MustCompile("[\u4e00-\u9fa5A-Za-z0-9_]").MatchString(name)
|
||||
}
|
||||
|
||||
func getTime(t int64) *time.Time {
|
||||
tm := time.Unix(t, 0)
|
||||
return &tm
|
||||
}
|
||||
|
||||
func fsidsFormat(ids ...string) string {
|
||||
var buf []string
|
||||
for _, id := range ids {
|
||||
e := strings.Split(id, "|")
|
||||
buf = append(buf, fmt.Sprintf("{\"fsid\":%s,\"uk\":%s}", e[0], e[1]))
|
||||
}
|
||||
return fmt.Sprintf("[%s]", strings.Join(buf, ","))
|
||||
}
|
||||
|
||||
func fsidsFormatNotUk(ids ...string) string {
|
||||
var buf []string
|
||||
for _, id := range ids {
|
||||
buf = append(buf, fmt.Sprintf("{\"fsid\":%s}", strings.Split(id, "|")[0]))
|
||||
}
|
||||
return fmt.Sprintf("[%s]", strings.Join(buf, ","))
|
||||
}
|
||||
|
||||
func splitID(id string) []string {
|
||||
return strings.SplitN(id, "|", 3)[:3]
|
||||
}
|
||||
|
||||
func joinID(ids ...interface{}) string {
|
||||
idsStr := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
idsStr = append(idsStr, fmt.Sprint(id))
|
||||
}
|
||||
return strings.Join(idsStr, "|")
|
||||
}
|
||||
|
||||
func IsAlbum(file *model.File) bool {
|
||||
return file.Id != "" && file.IsDir()
|
||||
}
|
||||
|
||||
func IsAlbumFile(file *model.File) bool {
|
||||
return file.Id != "" && !file.IsDir()
|
||||
}
|
||||
|
||||
func IsRoot(file *model.File) bool {
|
||||
return file.Id == "" && file.IsDir()
|
||||
}
|
||||
|
||||
func MustString(str string, err error) string {
|
||||
return str
|
||||
}
|
||||
61
drivers/base/base.go
Normal file
61
drivers/base/base.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package base
|
||||
|
||||
import "github.com/Xhofe/alist/model"
|
||||
|
||||
type Base struct{}
|
||||
|
||||
func (b Base) Config() DriverConfig {
|
||||
return DriverConfig{}
|
||||
}
|
||||
|
||||
func (b Base) Items() []Item {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Base) Save(account *model.Account, old *model.Account) error {
|
||||
return ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) File(path string, account *model.Account) (*model.File, error) {
|
||||
return nil, ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
return nil, ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) Link(args Args, account *model.Account) (*Link, error) {
|
||||
return nil, ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
return nil, nil, ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) MakeDir(path string, account *model.Account) error {
|
||||
return ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) Move(src string, dst string, account *model.Account) error {
|
||||
return ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) Rename(src string, dst string, account *model.Account) error {
|
||||
return ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) Copy(src string, dst string, account *model.Account) error {
|
||||
return ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) Delete(path string, account *model.Account) error {
|
||||
return ErrNotImplement
|
||||
}
|
||||
|
||||
func (b Base) Upload(file *model.FileStream, account *model.Account) error {
|
||||
return ErrNotImplement
|
||||
}
|
||||
58
drivers/base/cache.go
Normal file
58
drivers/base/cache.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func KeyCache(path string, account *model.Account) string {
|
||||
//path = utils.ParsePath(path)
|
||||
key := utils.ParsePath(utils.Join(account.Name, path))
|
||||
log.Debugln("cache key: ", key)
|
||||
return key
|
||||
}
|
||||
|
||||
func SaveSearchFiles[T model.ISearchFile](key string, obj []T) {
|
||||
if strings.Contains(key, ".balance") {
|
||||
return
|
||||
}
|
||||
err := model.DeleteSearchFilesByPath(key)
|
||||
if err != nil {
|
||||
log.Errorln("failed create search files", err)
|
||||
return
|
||||
}
|
||||
files := make([]model.SearchFile, len(obj))
|
||||
for i := 0; i < len(obj); i++ {
|
||||
files[i] = model.SearchFile{
|
||||
Path: key,
|
||||
Name: obj[i].GetName(),
|
||||
Size: obj[i].GetSize(),
|
||||
Type: obj[i].GetType(),
|
||||
}
|
||||
}
|
||||
err = model.CreateSearchFiles(files)
|
||||
if err != nil {
|
||||
log.Errorln("failed create search files", err)
|
||||
}
|
||||
}
|
||||
|
||||
func SetCache[T model.ISearchFile](path string, obj []T, account *model.Account) error {
|
||||
key := KeyCache(path, account)
|
||||
if conf.GetBool("enable search") {
|
||||
go SaveSearchFiles(key, obj)
|
||||
}
|
||||
return conf.Cache.Set(conf.Ctx, key, obj, nil)
|
||||
}
|
||||
|
||||
func GetCache(path string, account *model.Account) (interface{}, error) {
|
||||
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
|
||||
}
|
||||
181
drivers/base/driver.go
Normal file
181
drivers/base/driver.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DriverConfig struct {
|
||||
Name string
|
||||
OnlyProxy bool // 必须使用代理(本机或者其他机器)
|
||||
OnlyLocal bool // 必须本机返回的
|
||||
ApiProxy bool // 使用API中转的
|
||||
NoNeedSetLink bool // 不需要设置链接的
|
||||
NoCors bool // 不可以跨域
|
||||
LocalSort bool // 本地排序
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
Path string
|
||||
IP string
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
type Driver interface {
|
||||
// Config 配置
|
||||
Config() DriverConfig
|
||||
// Items 账号所需参数
|
||||
Items() []Item
|
||||
// Save 保存时处理
|
||||
Save(account *model.Account, old *model.Account) error
|
||||
// File 取文件
|
||||
File(path string, account *model.Account) (*model.File, error)
|
||||
// Files 取文件夹
|
||||
Files(path string, account *model.Account) ([]model.File, error)
|
||||
// Link 取链接
|
||||
Link(args Args, account *model.Account) (*Link, error)
|
||||
// Path 取路径(文件或文件夹)
|
||||
Path(path string, account *model.Account) (*model.File, []model.File, error)
|
||||
// Deprecated Proxy 代理处理
|
||||
//Proxy(r *http.Request, account *model.Account)
|
||||
// Preview 预览
|
||||
Preview(path string, account *model.Account) (interface{}, error)
|
||||
// MakeDir 创建文件夹
|
||||
MakeDir(path string, account *model.Account) error
|
||||
// Move 移动/改名
|
||||
Move(src string, dst string, account *model.Account) error
|
||||
// Rename 改名
|
||||
Rename(src string, dst string, account *model.Account) error
|
||||
// Copy 拷贝
|
||||
Copy(src string, dst string, account *model.Account) error
|
||||
// Delete 删除
|
||||
Delete(path string, account *model.Account) error
|
||||
// Upload 上传
|
||||
Upload(file *model.FileStream, account *model.Account) error
|
||||
// TODO
|
||||
//Search(path string, keyword string, account *model.Account) ([]*model.File, error)
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"`
|
||||
Default string `json:"default"`
|
||||
Values string `json:"values"`
|
||||
Required bool `json:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
var driversMap = map[string]Driver{}
|
||||
|
||||
func RegisterDriver(driver Driver) {
|
||||
log.Infof("register driver: [%s]", driver.Config().Name)
|
||||
driversMap[driver.Config().Name] = driver
|
||||
}
|
||||
|
||||
func GetDriver(name string) (driver Driver, ok bool) {
|
||||
driver, ok = driversMap[name]
|
||||
return
|
||||
}
|
||||
|
||||
func GetDriversMap() map[string]Driver {
|
||||
return driversMap
|
||||
}
|
||||
|
||||
func GetDrivers() map[string][]Item {
|
||||
res := make(map[string][]Item)
|
||||
for k, v := range driversMap {
|
||||
webdavDirect := Item{
|
||||
Name: "webdav_direct",
|
||||
Label: "webdav direct",
|
||||
Type: TypeBool,
|
||||
Required: true,
|
||||
Description: "Transfer the WebDAV of this account through the native",
|
||||
}
|
||||
if v.Config().OnlyProxy {
|
||||
res[k] = append([]Item{
|
||||
webdavDirect,
|
||||
}, v.Items()...)
|
||||
} else {
|
||||
res[k] = append([]Item{
|
||||
{
|
||||
Name: "proxy",
|
||||
Label: "proxy",
|
||||
Type: TypeBool,
|
||||
Required: true,
|
||||
Description: "web proxy",
|
||||
},
|
||||
{
|
||||
Name: "webdav_proxy",
|
||||
Label: "webdav proxy",
|
||||
Type: TypeBool,
|
||||
Required: true,
|
||||
Description: "Transfer the WebDAV of this account through the server",
|
||||
},
|
||||
webdavDirect,
|
||||
}, v.Items()...)
|
||||
}
|
||||
res[k] = append([]Item{
|
||||
{
|
||||
Name: "down_proxy_url",
|
||||
Label: "down_proxy_url",
|
||||
Type: TypeText,
|
||||
},
|
||||
{
|
||||
Name: "extract_folder",
|
||||
Label: "extract_folder",
|
||||
Values: "front,back",
|
||||
Type: TypeSelect,
|
||||
},
|
||||
}, res[k]...)
|
||||
if v.Config().ApiProxy {
|
||||
res[k] = append([]Item{
|
||||
{
|
||||
Name: "api_proxy_url",
|
||||
Label: "api_proxy_url",
|
||||
Type: TypeString,
|
||||
},
|
||||
}, res[k]...)
|
||||
}
|
||||
if v.Config().LocalSort {
|
||||
res[k] = append(res[k], []Item{
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: TypeSelect,
|
||||
Values: "name,size,updated_at",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: TypeSelect,
|
||||
Values: "ASC,DESC",
|
||||
Required: false,
|
||||
},
|
||||
}...)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
var NoRedirectClient *resty.Client
|
||||
var RestyClient = resty.New()
|
||||
var HttpClient = &http.Client{}
|
||||
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||
var DefaultTimeout = time.Second * 20
|
||||
|
||||
func init() {
|
||||
NoRedirectClient = resty.New().SetRedirectPolicy(
|
||||
resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}),
|
||||
)
|
||||
NoRedirectClient.SetHeader("user-agent", UserAgent)
|
||||
RestyClient.SetHeader("user-agent", UserAgent)
|
||||
RestyClient.SetRetryCount(3)
|
||||
RestyClient.SetTimeout(DefaultTimeout)
|
||||
}
|
||||
55
drivers/base/types.go
Normal file
55
drivers/base/types.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPathNotFound = errors.New("path not found")
|
||||
ErrNotFile = errors.New("not file")
|
||||
ErrNotImplement = errors.New("not implement")
|
||||
ErrNotSupport = errors.New("not support")
|
||||
ErrNotFolder = errors.New("not a folder")
|
||||
ErrEmptyFile = errors.New("empty file")
|
||||
ErrRelativePath = errors.New("access using relative path is not allowed")
|
||||
ErrEmptyToken = errors.New("empty token")
|
||||
)
|
||||
|
||||
const (
|
||||
TypeString = "string"
|
||||
TypeSelect = "select"
|
||||
TypeBool = "bool"
|
||||
TypeNumber = "number"
|
||||
TypeText = "text"
|
||||
)
|
||||
|
||||
const (
|
||||
Get = iota
|
||||
Post
|
||||
Put
|
||||
Delete
|
||||
Patch
|
||||
)
|
||||
|
||||
type Json map[string]interface{}
|
||||
|
||||
type TokenResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Url string `json:"url"`
|
||||
Headers []Header `json:"headers"`
|
||||
Data io.ReadCloser
|
||||
FilePath string `json:"path"` // for native
|
||||
Status int
|
||||
Header http.Header
|
||||
}
|
||||
262
drivers/ftp/driver.go
Normal file
262
drivers/ftp/driver.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/jlaffaye/ftp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type FTP struct{}
|
||||
|
||||
func (driver FTP) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "FTP",
|
||||
OnlyProxy: true,
|
||||
OnlyLocal: true,
|
||||
NoNeedSetLink: true,
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver FTP) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "site_url",
|
||||
Label: "ftp host url",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "username",
|
||||
Label: "username",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "password",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver FTP) Save(account *model.Account, old *model.Account) error {
|
||||
if old != nil {
|
||||
conn, ok := connMap[old.Name]
|
||||
if ok {
|
||||
err := conn.Quit()
|
||||
log.Error("ftp:", err)
|
||||
delete(connMap, old.Name)
|
||||
}
|
||||
}
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if account.RootFolder == "" {
|
||||
account.RootFolder = "/"
|
||||
}
|
||||
_, err := driver.Login(account)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
} else {
|
||||
account.Status = "work"
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver FTP) File(path string, account *model.Account) (*model.File, error) {
|
||||
log.Debugf("file: %s", path)
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver FTP) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
log.Debugf("files: %s", path)
|
||||
path = utils.ParsePath(path)
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ := cache.([]model.File)
|
||||
return files, nil
|
||||
}
|
||||
realPath := utils.Join(account.RootFolder, path)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
entries, err := conn.List(realPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make([]model.File, 0)
|
||||
for i, _ := range entries {
|
||||
entry := entries[i]
|
||||
if entry.Name == "." || entry.Name == ".." {
|
||||
continue
|
||||
}
|
||||
f := model.File{
|
||||
Name: entry.Name,
|
||||
Size: int64(entry.Size),
|
||||
UpdatedAt: &entry.Time,
|
||||
Driver: driver.Config().Name,
|
||||
}
|
||||
if entry.Type == ftp.EntryTypeFolder {
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
f.Type = utils.GetFileType(filepath.Ext(entry.Name))
|
||||
}
|
||||
res = append(res, f)
|
||||
}
|
||||
if len(res) > 0 {
|
||||
_ = base.SetCache(path, res, account)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver FTP) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
path := args.Path
|
||||
path = utils.ParsePath(path)
|
||||
realPath := utils.Join(account.RootFolder, path)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
resp, err := conn.Retr(realPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//defer func() { _ = resp.Close() }()
|
||||
//data, err := ioutil.ReadAll(resp)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
return &base.Link{
|
||||
Data: resp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver FTP) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
log.Debugf("ftp path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
//file.Url, _ = driver.Link(path, account)
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver FTP) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver FTP) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver FTP) MakeDir(path string, account *model.Account) error {
|
||||
path = utils.ParsePath(path)
|
||||
realPath := utils.Join(account.RootFolder, path)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
err = conn.MakeDir(realPath)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver FTP) Move(src string, dst string, account *model.Account) error {
|
||||
realSrc := utils.Join(account.RootFolder, src)
|
||||
realDst := utils.Join(account.RootFolder, dst)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
err = conn.Rename(realSrc, realDst)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver FTP) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver FTP) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver FTP) Delete(path string, account *model.Account) error {
|
||||
path = utils.ParsePath(path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
realPath := utils.Join(account.RootFolder, path)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
if file.IsDir() {
|
||||
err = conn.RemoveDirRecur(realPath)
|
||||
} else {
|
||||
err = conn.Delete(realPath)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver FTP) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
realPath := utils.Join(account.RootFolder, file.ParentPath, file.Name)
|
||||
conn, err := driver.Login(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//defer func() { _ = conn.Quit() }()
|
||||
err = conn.Stor(realPath, file)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*FTP)(nil)
|
||||
36
drivers/ftp/ftp.go
Normal file
36
drivers/ftp/ftp.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/jlaffaye/ftp"
|
||||
)
|
||||
|
||||
var connMap map[string]*ftp.ServerConn
|
||||
|
||||
func (driver FTP) Login(account *model.Account) (*ftp.ServerConn, error) {
|
||||
conn, ok := connMap[account.Name]
|
||||
if ok {
|
||||
_, err := conn.CurrentDir()
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
} else {
|
||||
delete(connMap, account.Name)
|
||||
}
|
||||
}
|
||||
conn, err := ftp.Connect(account.SiteUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.Login(account.Username, account.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connMap[account.Name] = conn
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&FTP{})
|
||||
connMap = make(map[string]*ftp.ServerConn)
|
||||
}
|
||||
289
drivers/google/driver.go
Normal file
289
drivers/google/driver.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type GoogleDrive struct{}
|
||||
|
||||
func (driver GoogleDrive) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "GoogleDrive",
|
||||
OnlyProxy: true,
|
||||
ApiProxy: true,
|
||||
NoNeedSetLink: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "client_id",
|
||||
Label: "client id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Default: "202264815644.apps.googleusercontent.com",
|
||||
},
|
||||
{
|
||||
Name: "client_secret",
|
||||
Label: "client secret",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Default: "X4Z3ca8xfWDb1Voo-F9a7ZxJ",
|
||||
},
|
||||
{
|
||||
Name: "refresh_token",
|
||||
Label: "refresh token",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
Description: "such as: folder,name,modifiedTime",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "asc,desc",
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
account.Proxy = true
|
||||
err := driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
if account.RootFolder == "" {
|
||||
account.RootFolder = "root"
|
||||
}
|
||||
account.Status = "work"
|
||||
_ = model.SaveAccount(account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var rawFiles []File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
rawFiles, _ = cache.([]File)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawFiles, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawFiles) > 0 {
|
||||
_ = base.SetCache(path, rawFiles, account)
|
||||
}
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file, account))
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.Type == conf.FOLDER {
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
url := fmt.Sprintf("https://www.googleapis.com/drive/v3/files/%s?includeItemsFromAllDrives=true&supportsAllDrives=true", file.Id)
|
||||
_, err = driver.Request(url, base.Get, nil, nil, nil, nil, nil, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link := base.Link{
|
||||
Url: url + "&alt=media",
|
||||
Headers: []base.Header{
|
||||
{
|
||||
Name: "Authorization",
|
||||
Value: "Bearer " + account.AccessToken,
|
||||
},
|
||||
},
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("google path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver GoogleDrive) Proxy(r *http.Request, account *model.Account) {
|
||||
// r.Header.Add("Authorization", "Bearer "+account.AccessToken)
|
||||
//}
|
||||
|
||||
func (driver GoogleDrive) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) MakeDir(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"name": utils.Base(path),
|
||||
"parents": []string{parentFile.Id},
|
||||
"mimeType": "application/vnd.google-apps.folder",
|
||||
}
|
||||
_, err = driver.Request("https://www.googleapis.com/drive/v3/files", base.Post, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Move(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
url := "https://www.googleapis.com/drive/v3/files/" + srcFile.Id
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstParentFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query := map[string]string{
|
||||
"addParents": dstParentFile.Id,
|
||||
"removeParents": "root",
|
||||
}
|
||||
_, err = driver.Request(url, base.Patch, nil, query, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Rename(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
url := "https://www.googleapis.com/drive/v3/files/" + srcFile.Id
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"name": utils.Base(dst),
|
||||
}
|
||||
_, err = driver.Request(url, base.Patch, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.File(path, account)
|
||||
url := "https://www.googleapis.com/drive/v3/files/" + file.Id
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request(url, base.Delete, nil, nil, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"name": file.Name,
|
||||
"parents": []string{parentFile.Id},
|
||||
}
|
||||
var e Error
|
||||
url := "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true"
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
res, err := base.NoRedirectClient.R().SetHeader("Authorization", "Bearer "+account.AccessToken).
|
||||
SetError(&e).SetBody(data).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.Error.Code != 0 {
|
||||
if e.Error.Code == 401 {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
return driver.Upload(file, account)
|
||||
}
|
||||
return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
|
||||
}
|
||||
putUrl := res.Header().Get("location")
|
||||
byteData, _ := ioutil.ReadAll(file)
|
||||
_, err = driver.Request(putUrl, base.Put, nil, nil, nil, byteData, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*GoogleDrive)(nil)
|
||||
175
drivers/google/googledrive.go
Normal file
175
drivers/google/googledrive.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) FormatFile(file *File, account *model.Account) *model.File {
|
||||
f := &model.File{
|
||||
Id: file.Id,
|
||||
Name: file.Name,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: file.ModifiedTime,
|
||||
Url: "",
|
||||
}
|
||||
f.Size = int64(file.GetSize())
|
||||
f.Type = file.GetType()
|
||||
if file.ThumbnailLink != "" {
|
||||
if account.APIProxyUrl != "" {
|
||||
f.Thumbnail = fmt.Sprintf("%s/%s", account.APIProxyUrl, file.ThumbnailLink)
|
||||
} else {
|
||||
f.Thumbnail = file.ThumbnailLink
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type Files struct {
|
||||
NextPageToken string `json:"nextPageToken"`
|
||||
Files []File `json:"files"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Error struct {
|
||||
Errors []struct {
|
||||
Domain string `json:"domain"`
|
||||
Reason string `json:"reason"`
|
||||
Message string `json:"message"`
|
||||
LocationType string `json:"location_type"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) GetFiles(id string, account *model.Account) ([]File, error) {
|
||||
pageToken := "first"
|
||||
res := make([]File, 0)
|
||||
for pageToken != "" {
|
||||
if pageToken == "first" {
|
||||
pageToken = ""
|
||||
}
|
||||
var resp Files
|
||||
orderBy := "folder,name,modifiedTime desc"
|
||||
if account.OrderBy != "" {
|
||||
orderBy = account.OrderBy + " " + account.OrderDirection
|
||||
}
|
||||
query := map[string]string{
|
||||
"orderBy": orderBy,
|
||||
"fields": "files(id,name,mimeType,size,modifiedTime,thumbnailLink),nextPageToken",
|
||||
"pageSize": "1000",
|
||||
"q": fmt.Sprintf("'%s' in parents and trashed = false", id),
|
||||
//"includeItemsFromAllDrives": "true",
|
||||
//"supportsAllDrives": "true",
|
||||
"pageToken": pageToken,
|
||||
}
|
||||
_, err := driver.Request("https://www.googleapis.com/drive/v3/files",
|
||||
base.Get, nil, query, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pageToken = resp.NextPageToken
|
||||
res = append(res, resp.Files...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver GoogleDrive) Request(url string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
rawUrl := url
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+account.AccessToken)
|
||||
req.SetQueryParam("includeItemsFromAllDrives", "true")
|
||||
req.SetQueryParam("supportsAllDrives", "true")
|
||||
if headers != nil {
|
||||
req.SetHeaders(headers)
|
||||
}
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
if form != nil {
|
||||
req.SetFormData(form)
|
||||
}
|
||||
if data != nil {
|
||||
req.SetBody(data)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var res *resty.Response
|
||||
var err error
|
||||
var e Error
|
||||
req.SetError(&e)
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
case base.Put:
|
||||
res, err = req.Put(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if e.Error.Code != 0 {
|
||||
if e.Error.Code == 401 {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
_ = model.SaveAccount(account)
|
||||
return nil, err
|
||||
}
|
||||
return driver.Request(rawUrl, method, headers, query, form, data, resp, account)
|
||||
}
|
||||
return nil, fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&GoogleDrive{})
|
||||
}
|
||||
38
drivers/google/types.go
Normal file
38
drivers/google/types.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MimeType string `json:"mimeType"`
|
||||
ModifiedTime *time.Time `json:"modifiedTime"`
|
||||
Size string `json:"size"`
|
||||
ThumbnailLink string `json:"thumbnailLink"`
|
||||
}
|
||||
|
||||
func (f File) GetSize() uint64 {
|
||||
if f.GetType() == conf.FOLDER {
|
||||
return 0
|
||||
}
|
||||
size, _ := strconv.ParseUint(f.Size, 10, 64)
|
||||
return size
|
||||
}
|
||||
|
||||
func (f File) GetName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f File) GetType() int {
|
||||
mimeType := f.MimeType
|
||||
if mimeType == "application/vnd.google-apps.folder" || mimeType == "application/vnd.google-apps.shortcut" {
|
||||
return conf.FOLDER
|
||||
}
|
||||
return utils.GetFileType(path.Ext(f.Name))
|
||||
}
|
||||
202
drivers/lanzou/driver.go
Normal file
202
drivers/lanzou/driver.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package lanzou
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Lanzou struct{}
|
||||
|
||||
func (driver Lanzou) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Lanzou",
|
||||
NoCors: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Lanzou) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "lanzou type",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "cookie,url",
|
||||
},
|
||||
{
|
||||
Name: "access_token",
|
||||
Label: "cookie",
|
||||
Type: base.TypeString,
|
||||
Description: "about 15 days valid",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "site_url",
|
||||
Label: "share url",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "share password",
|
||||
Type: base.TypeString,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Lanzou) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if account.InternalType == "cookie" {
|
||||
if account.RootFolder == "" {
|
||||
account.RootFolder = "-1"
|
||||
}
|
||||
}
|
||||
account.Status = "work"
|
||||
_ = model.SaveAccount(account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Lanzou) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Lanzou) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var rawFiles []LanZouFile
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
rawFiles, _ = cache.([]LanZouFile)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawFiles, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rawFiles) > 0 {
|
||||
_ = base.SetCache(path, rawFiles, account)
|
||||
}
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Lanzou) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("down file: %+v", file)
|
||||
downId := file.Id
|
||||
pwd := ""
|
||||
if account.InternalType == "cookie" {
|
||||
downId, pwd, err = driver.GetDownPageId(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var url string
|
||||
//if pwd != "" {
|
||||
//url, err = driver.GetLinkWithPassword(downId, pwd, account)
|
||||
//} else {
|
||||
url, err = driver.GetLink(downId, pwd, account)
|
||||
//}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link := base.Link{
|
||||
Url: url,
|
||||
Headers: []base.Header{
|
||||
{Name: "User-Agent", Value: base.UserAgent},
|
||||
},
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Lanzou) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("lanzou path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver Lanzou) Proxy(r *http.Request, account *model.Account) {
|
||||
// r.Header.Del("Origin")
|
||||
//}
|
||||
|
||||
func (driver Lanzou) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver *Lanzou) MakeDir(path string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver *Lanzou) Move(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver *Lanzou) Rename(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver *Lanzou) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver *Lanzou) Delete(path string, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver *Lanzou) Upload(file *model.FileStream, account *model.Account) error {
|
||||
return base.ErrNotImplement
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Lanzou)(nil)
|
||||
271
drivers/lanzou/lanzou.go
Normal file
271
drivers/lanzou/lanzou.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package lanzou
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
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.Id = file.FolId
|
||||
} else {
|
||||
f.Name = file.NameAll
|
||||
}
|
||||
f.Type = file.GetType()
|
||||
return f
|
||||
}
|
||||
|
||||
type LanZouFilesResp struct {
|
||||
Zt int `json:"zt"`
|
||||
Info interface{} `json:"info"`
|
||||
Text []LanZouFile `json:"text"`
|
||||
}
|
||||
|
||||
func (driver *Lanzou) GetFiles(folderId string, account *model.Account) ([]LanZouFile, error) {
|
||||
if account.InternalType == "cookie" {
|
||||
files := make([]LanZouFile, 0)
|
||||
var resp LanZouFilesResp
|
||||
// folders
|
||||
res, err := base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
SetFormData(map[string]string{
|
||||
"task": "47",
|
||||
"folder_id": folderId,
|
||||
}).Post("https://pc.woozooo.com/doupload.php")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if resp.Zt != 1 && resp.Zt != 2 {
|
||||
return nil, fmt.Errorf("%v", resp.Info)
|
||||
}
|
||||
for _, file := range resp.Text {
|
||||
file.Folder = true
|
||||
files = append(files, file)
|
||||
}
|
||||
// files
|
||||
pg := 1
|
||||
for {
|
||||
_, err = base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
SetFormData(map[string]string{
|
||||
"task": "5",
|
||||
"folder_id": folderId,
|
||||
"pg": strconv.Itoa(pg),
|
||||
}).Post("https://pc.woozooo.com/doupload.php")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Zt != 1 {
|
||||
return nil, fmt.Errorf("%v", resp.Info)
|
||||
}
|
||||
if len(resp.Text) == 0 {
|
||||
break
|
||||
}
|
||||
files = append(files, resp.Text...)
|
||||
pg++
|
||||
}
|
||||
return files, nil
|
||||
} else {
|
||||
return driver.GetFilesByUrl(account)
|
||||
}
|
||||
}
|
||||
|
||||
func (driver *Lanzou) GetFilesByUrl(account *model.Account) ([]LanZouFile, error) {
|
||||
files := make([]LanZouFile, 0)
|
||||
shareUrl := account.SiteUrl
|
||||
u, err := url.Parse(shareUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := base.RestyClient.R().Get(shareUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lxArr := regexp.MustCompile(`'lx':(.+?),`).FindStringSubmatch(res.String())
|
||||
if len(lxArr) == 0 {
|
||||
return nil, fmt.Errorf("get empty page")
|
||||
}
|
||||
lx := lxArr[1]
|
||||
fid := regexp.MustCompile(`'fid':(.+?),`).FindStringSubmatch(res.String())[1]
|
||||
uid := regexp.MustCompile(`'uid':'(.+?)',`).FindStringSubmatch(res.String())[1]
|
||||
rep := regexp.MustCompile(`'rep':'(.+?)',`).FindStringSubmatch(res.String())[1]
|
||||
up := regexp.MustCompile(`'up':(.+?),`).FindStringSubmatch(res.String())[1]
|
||||
ls := ""
|
||||
if account.Password != "" {
|
||||
ls = regexp.MustCompile(`'ls':(.+?),`).FindStringSubmatch(res.String())[1]
|
||||
}
|
||||
tName := regexp.MustCompile(`'t':(.+?),`).FindStringSubmatch(res.String())[1]
|
||||
kName := regexp.MustCompile(`'k':(.+?),`).FindStringSubmatch(res.String())[1]
|
||||
t := regexp.MustCompile(`var ` + tName + ` = '(.+?)';`).FindStringSubmatch(res.String())[1]
|
||||
k := regexp.MustCompile(`var ` + kName + ` = '(.+?)';`).FindStringSubmatch(res.String())[1]
|
||||
pg := 1
|
||||
for {
|
||||
var resp LanZouFilesResp
|
||||
res, err = base.RestyClient.R().SetResult(&resp).SetFormData(map[string]string{
|
||||
"lx": lx,
|
||||
"fid": fid,
|
||||
"uid": uid,
|
||||
"pg": strconv.Itoa(pg),
|
||||
"rep": rep,
|
||||
"t": t,
|
||||
"k": k,
|
||||
"up": up,
|
||||
"ls": ls,
|
||||
"pwd": account.Password,
|
||||
}).Post(fmt.Sprintf("https://%s/filemoreajax.php", u.Host))
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
break
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if resp.Zt != 1 {
|
||||
return nil, fmt.Errorf("%v", resp.Info)
|
||||
}
|
||||
if len(resp.Text) == 0 {
|
||||
break
|
||||
}
|
||||
pg++
|
||||
time.Sleep(time.Second)
|
||||
files = append(files, resp.Text...)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
//type LanzouDownInfo struct {
|
||||
// FId string `json:"f_id"`
|
||||
// IsNewd string `json:"is_newd"`
|
||||
//}
|
||||
|
||||
// GetDownPageId 获取下载页面的ID
|
||||
func (driver *Lanzou) GetDownPageId(fileId string, account *model.Account) (string, string, error) {
|
||||
var resp DownPageResp
|
||||
res, err := base.RestyClient.R().SetResult(&resp).SetHeader("Cookie", account.AccessToken).
|
||||
SetFormData(map[string]string{
|
||||
"task": "22",
|
||||
"file_id": fileId,
|
||||
}).Post("https://pc.woozooo.com/doupload.php")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if resp.Zt != 1 {
|
||||
return "", "", fmt.Errorf("%v", resp.Info)
|
||||
}
|
||||
return resp.Info.FId, resp.Info.Pwd, nil
|
||||
}
|
||||
|
||||
type LanzouLinkResp struct {
|
||||
Dom string `json:"dom"`
|
||||
Url string `json:"url"`
|
||||
Zt int `json:"zt"`
|
||||
}
|
||||
|
||||
func (driver *Lanzou) GetLink(downId string, pwd string, account *model.Account) (string, error) {
|
||||
shareUrl := account.SiteUrl
|
||||
u, err := url.Parse(shareUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Debugln(fmt.Sprintf("https://%s/%s", u.Host, downId))
|
||||
res, err := base.RestyClient.R().Get(fmt.Sprintf("https://%s/%s", u.Host, downId))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
iframe := regexp.MustCompile(`<iframe class="ifr2" name=".{2,20}" src="(.+?)"`).FindStringSubmatch(res.String())
|
||||
if len(iframe) == 0 {
|
||||
return driver.GetLinkWithPassword(downId, pwd, res.String(), account)
|
||||
}
|
||||
iframeUrl := fmt.Sprintf("https://%s%s", u.Host, iframe[1])
|
||||
res, err = base.RestyClient.R().Get(iframeUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Debugln(res.String())
|
||||
ajaxdata := regexp.MustCompile(`var ajaxdata = '(.+?)'`).FindStringSubmatch(res.String())
|
||||
if len(ajaxdata) == 0 {
|
||||
return "", fmt.Errorf("get iframe empty page")
|
||||
}
|
||||
signs := ajaxdata[1]
|
||||
//sign := regexp.MustCompile(`var ispostdowns = '(.+?)';`).FindStringSubmatch(res.String())[1]
|
||||
sign := regexp.MustCompile(`'sign':'(.+?)',`).FindStringSubmatch(res.String())[1]
|
||||
//websign := regexp.MustCompile(`'websign':'(.+?)'`).FindStringSubmatch(res.String())[1]
|
||||
websign := ""
|
||||
websignR := regexp.MustCompile(`var websign = '(.+?)'`).FindStringSubmatch(res.String())
|
||||
if len(websignR) > 1 {
|
||||
websign = websignR[1]
|
||||
}
|
||||
//websign := ""
|
||||
//websignkey := regexp.MustCompile(`'websignkey':'(.+?)'`).FindStringSubmatch(res.String())[1]
|
||||
websignkey := regexp.MustCompile(`var websignkey = '(.+?)';`).FindStringSubmatch(res.String())[1]
|
||||
var resp LanzouLinkResp
|
||||
form := map[string]string{
|
||||
"action": "downprocess",
|
||||
"signs": signs,
|
||||
"sign": sign,
|
||||
"ves": "1",
|
||||
"websign": websign,
|
||||
"websignkey": websignkey,
|
||||
}
|
||||
log.Debugf("form: %+v", form)
|
||||
res, err = base.RestyClient.R().SetResult(&resp).
|
||||
SetHeader("origin", "https://"+u.Host).
|
||||
SetHeader("referer", iframeUrl).
|
||||
SetFormData(form).Post(fmt.Sprintf("https://%s/ajaxm.php", u.Host))
|
||||
log.Debug(res.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.Zt == 1 {
|
||||
return resp.Dom + "/file/" + resp.Url, nil
|
||||
}
|
||||
return "", fmt.Errorf("failed get link")
|
||||
}
|
||||
|
||||
func (driver *Lanzou) GetLinkWithPassword(downId string, pwd string, html string, account *model.Account) (string, error) {
|
||||
shareUrl := account.SiteUrl
|
||||
u, err := url.Parse(shareUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if html == "" {
|
||||
log.Debugln(fmt.Sprintf("https://%s/%s", u.Host, downId))
|
||||
res, err := base.RestyClient.R().Get(fmt.Sprintf("https://%s/%s", u.Host, downId))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
html = res.String()
|
||||
}
|
||||
|
||||
data := regexp.MustCompile(`data : '(.+?)'\+pwd,`).FindStringSubmatch(html)[1] + pwd
|
||||
var resp LanzouLinkResp
|
||||
_, err = base.RestyClient.R().SetResult(&resp).SetHeaders(map[string]string{
|
||||
"Referer": fmt.Sprintf("https://%s/%s", u.Host, downId),
|
||||
"Origin": "https://" + u.Host,
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
}).SetBody(data).Post(fmt.Sprintf("https://%s/ajaxm.php", u.Host))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.Zt == 1 {
|
||||
return resp.Dom + "/file/" + resp.Url, nil
|
||||
}
|
||||
return "", fmt.Errorf("failed get link with password")
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Lanzou{})
|
||||
}
|
||||
47
drivers/lanzou/types.go
Normal file
47
drivers/lanzou/types.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package lanzou
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"path"
|
||||
)
|
||||
|
||||
type LanZouFile struct {
|
||||
Name string `json:"name"`
|
||||
NameAll string `json:"name_all"`
|
||||
Id string `json:"id"`
|
||||
FolId string `json:"fol_id"`
|
||||
Size string `json:"size"`
|
||||
Time string `json:"time"`
|
||||
Folder bool
|
||||
}
|
||||
|
||||
func (f LanZouFile) GetSize() uint64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (f LanZouFile) GetName() string {
|
||||
if f.Folder {
|
||||
return f.Name
|
||||
}
|
||||
return f.NameAll
|
||||
}
|
||||
|
||||
func (f LanZouFile) GetType() int {
|
||||
if f.Folder {
|
||||
return conf.FOLDER
|
||||
}
|
||||
return utils.GetFileType(path.Ext(f.NameAll))
|
||||
}
|
||||
|
||||
type DownPageResp struct {
|
||||
Zt int `json:"zt"`
|
||||
Info struct {
|
||||
Pwd string `json:"pwd"`
|
||||
Onof string `json:"onof"`
|
||||
FId string `json:"f_id"`
|
||||
Taoc string `json:"taoc"`
|
||||
IsNewd string `json:"is_newd"`
|
||||
} `json:"info"`
|
||||
Text interface{} `json:"text"`
|
||||
}
|
||||
317
drivers/mediatrack/driver.go
Normal file
317
drivers/mediatrack/driver.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package mediatrack
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/google/uuid"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type MediaTrack struct{}
|
||||
|
||||
func (driver MediaTrack) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "MediaTrack",
|
||||
}
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "access_token",
|
||||
Label: "Token",
|
||||
Type: base.TypeString,
|
||||
Description: "Unknown expiration time",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "updated_at,title,size",
|
||||
Required: true,
|
||||
Description: "title",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "desc",
|
||||
Type: base.TypeSelect,
|
||||
Values: "true,false",
|
||||
Required: true,
|
||||
Default: "false",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver MediaTrack) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var files []model.File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ = cache.([]model.File)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pre := "https://jayce.api.mediatrack.cn/v3/assets/" + file.Id
|
||||
body, err := driver.Request(pre+"/token", base.Get, nil, nil, nil, nil, nil, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := pre + "/raw"
|
||||
res, err := base.NoRedirectClient.R().SetQueryParam("token", jsoniter.Get(body, "data").ToString()).Get(url)
|
||||
return &base.Link{Url: res.Header().Get("location")}, nil
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("MediaTrack path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver MediaTrack) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver MediaTrack) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver MediaTrack) MakeDir(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := fmt.Sprintf("https://jayce.api.mediatrack.cn/v3/assets/%s/children", parentFile.Id)
|
||||
_, err = driver.Request(url, base.Post, nil, nil, nil, base.Json{
|
||||
"type": 1,
|
||||
"title": utils.Base(path),
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Move(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstParentFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"parent_id": dstParentFile.Id,
|
||||
"ids": []string{srcFile.Id},
|
||||
}
|
||||
url := "https://jayce.api.mediatrack.cn/v4/assets/batch/move"
|
||||
_, err = driver.Request(url, base.Post, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Rename(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := "https://jayce.api.mediatrack.cn/v3/assets/" + srcFile.Id
|
||||
data := base.Json{
|
||||
"title": utils.Base(dst),
|
||||
}
|
||||
_, err = driver.Request(url, base.Put, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Copy(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstParentFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"parent_id": dstParentFile.Id,
|
||||
"ids": []string{srcFile.Id},
|
||||
}
|
||||
url := "https://jayce.api.mediatrack.cn/v4/assets/batch/clone"
|
||||
_, err = driver.Request(url, base.Post, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Delete(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"origin_id": parentFile.Id,
|
||||
"ids": []string{file.Id},
|
||||
}
|
||||
url := "https://jayce.api.mediatrack.cn/v4/assets/batch/delete"
|
||||
_, err = driver.Request(url, base.Delete, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
src := "assets/" + uuid.New().String()
|
||||
var resp UploadResp
|
||||
_, err = driver.Request("https://jayce.api.mediatrack.cn/v3/storage/tokens/asset", base.Get, nil, map[string]string{
|
||||
"src": src,
|
||||
}, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
credential := resp.Data.Credentials
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(credential.TmpSecretID, credential.TmpSecretKey, credential.Token),
|
||||
Region: &resp.Data.Region,
|
||||
Endpoint: aws.String("cos.accelerate.myqcloud.com"),
|
||||
}
|
||||
s, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
_, err = io.Copy(tempFile, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: &resp.Data.Bucket,
|
||||
Key: &resp.Data.Object,
|
||||
Body: tempFile,
|
||||
}
|
||||
_, err = uploader.Upload(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := fmt.Sprintf("https://jayce.api.mediatrack.cn/v3/assets/%s/children", parentFile.Id)
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h := md5.New()
|
||||
_, err = io.Copy(h, tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash := hex.EncodeToString(h.Sum(nil))
|
||||
data := base.Json{
|
||||
"category": 0,
|
||||
"description": file.GetFileName(),
|
||||
"hash": hash,
|
||||
"mime": file.MIMEType,
|
||||
"size": file.GetSize(),
|
||||
"src": src,
|
||||
"title": file.GetFileName(),
|
||||
"type": 0,
|
||||
}
|
||||
_, err = driver.Request(url, base.Post, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*MediaTrack)(nil)
|
||||
183
drivers/mediatrack/mediatrack.go
Normal file
183
drivers/mediatrack/mediatrack.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package mediatrack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseResp struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type T struct {
|
||||
BaseResp
|
||||
}
|
||||
|
||||
func (driver MediaTrack) Request(url string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+account.AccessToken)
|
||||
if headers != nil {
|
||||
req.SetHeaders(headers)
|
||||
}
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
if form != nil {
|
||||
req.SetFormData(form)
|
||||
}
|
||||
if data != nil {
|
||||
req.SetBody(data)
|
||||
}
|
||||
var e BaseResp
|
||||
req.SetResult(&e)
|
||||
var err error
|
||||
var res *resty.Response
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
case base.Put:
|
||||
res, err = req.Put(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugln(res.String())
|
||||
if e.Status != "SUCCESS" {
|
||||
return nil, errors.New(e.Message)
|
||||
}
|
||||
if resp != nil {
|
||||
err = utils.Json.Unmarshal(res.Body(), resp)
|
||||
}
|
||||
return res.Body(), err
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Category int `json:"category"`
|
||||
ChildAssets []interface{} `json:"childAssets"`
|
||||
CommentCount int `json:"comment_count"`
|
||||
CoverAsset interface{} `json:"cover_asset"`
|
||||
CoverAssetID string `json:"cover_asset_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt string `json:"deleted_at"`
|
||||
Description string `json:"description"`
|
||||
File *struct {
|
||||
Cover string `json:"cover"`
|
||||
Src string `json:"src"`
|
||||
} `json:"file"`
|
||||
//FileID string `json:"file_id"`
|
||||
ID string `json:"id"`
|
||||
|
||||
Size string `json:"size"`
|
||||
Thumbnails []interface{} `json:"thumbnails"`
|
||||
Title string `json:"title"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (driver MediaTrack) formatFile(file *File) *model.File {
|
||||
f := model.File{
|
||||
Id: file.ID,
|
||||
Name: file.Title,
|
||||
Size: 0,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: &file.UpdatedAt,
|
||||
}
|
||||
if file.File == nil {
|
||||
// folder
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
size, _ := strconv.ParseInt(file.Size, 10, 64)
|
||||
f.Size = size
|
||||
f.Type = utils.GetFileType(path.Ext(file.Title))
|
||||
if file.File.Cover != "" {
|
||||
f.Thumbnail = "https://nano.mtres.cn/" + file.File.Cover
|
||||
}
|
||||
}
|
||||
return &f
|
||||
}
|
||||
|
||||
type ChildrenResp struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Total int `json:"total"`
|
||||
Assets []File `json:"assets"`
|
||||
} `json:"data"`
|
||||
Path string `json:"path"`
|
||||
TraceID string `json:"trace_id"`
|
||||
RequestID string `json:"requestId"`
|
||||
}
|
||||
|
||||
func (driver MediaTrack) GetFiles(parentId string, account *model.Account) ([]model.File, error) {
|
||||
files := make([]model.File, 0)
|
||||
url := fmt.Sprintf("https://jayce.api.mediatrack.cn/v4/assets/%s/children", parentId)
|
||||
sort := ""
|
||||
if account.OrderBy != "" {
|
||||
if account.OrderDirection == "true" {
|
||||
sort = "-"
|
||||
}
|
||||
sort += account.OrderBy
|
||||
}
|
||||
page := 1
|
||||
for {
|
||||
var resp ChildrenResp
|
||||
_, err := driver.Request(url, base.Get, nil, map[string]string{
|
||||
"page": strconv.Itoa(page),
|
||||
"size": "50",
|
||||
"sort": sort,
|
||||
}, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.Data.Assets) == 0 {
|
||||
break
|
||||
}
|
||||
page++
|
||||
for _, file := range resp.Data.Assets {
|
||||
files = append(files, *driver.formatFile(&file))
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Credentials struct {
|
||||
TmpSecretID string `json:"TmpSecretId"`
|
||||
TmpSecretKey string `json:"TmpSecretKey"`
|
||||
Token string `json:"Token"`
|
||||
ExpiredTime int `json:"ExpiredTime"`
|
||||
Expiration time.Time `json:"Expiration"`
|
||||
StartTime int `json:"StartTime"`
|
||||
} `json:"credentials"`
|
||||
Object string `json:"object"`
|
||||
Bucket string `json:"bucket"`
|
||||
Region string `json:"region"`
|
||||
URL string `json:"url"`
|
||||
Size string `json:"size"`
|
||||
} `json:"data"`
|
||||
Path string `json:"path"`
|
||||
TraceID string `json:"trace_id"`
|
||||
RequestID string `json:"requestId"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&MediaTrack{})
|
||||
}
|
||||
269
drivers/native/driver.go
Normal file
269
drivers/native/driver.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package native
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Native struct{}
|
||||
|
||||
func (driver Native) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Native",
|
||||
OnlyProxy: true,
|
||||
OnlyLocal: true,
|
||||
NoNeedSetLink: true,
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Native) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Native) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
log.Debugf("save a account: [%s]", account.Name)
|
||||
if !utils.Exists(account.RootFolder) {
|
||||
account.Status = fmt.Sprintf("[%s] not exist", account.RootFolder)
|
||||
_ = model.SaveAccount(account)
|
||||
return fmt.Errorf("[%s] not exist", account.RootFolder)
|
||||
}
|
||||
account.Status = "work"
|
||||
account.Proxy = true
|
||||
err := model.SaveAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Native) File(path string, account *model.Account) (*model.File, error) {
|
||||
if utils.IsContain(strings.Split(path, "/"), "..") {
|
||||
return nil, base.ErrRelativePath
|
||||
}
|
||||
fullPath := filepath.Join(account.RootFolder, path)
|
||||
if !utils.Exists(fullPath) {
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
f, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
time := f.ModTime()
|
||||
file := &model.File{
|
||||
Name: f.Name(),
|
||||
UpdatedAt: &time,
|
||||
Driver: driver.Config().Name,
|
||||
}
|
||||
if f.IsDir() {
|
||||
file.Type = conf.FOLDER
|
||||
} else {
|
||||
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
|
||||
file.Size = f.Size()
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (driver Native) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
if utils.IsContain(strings.Split(path, "/"), "..") {
|
||||
return nil, base.ErrRelativePath
|
||||
}
|
||||
fullPath := filepath.Join(account.RootFolder, path)
|
||||
if !utils.Exists(fullPath) {
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
rawFiles, err := ioutil.ReadDir(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range rawFiles {
|
||||
if strings.HasPrefix(f.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
time := f.ModTime()
|
||||
file := model.File{
|
||||
Name: f.Name(),
|
||||
Type: 0,
|
||||
UpdatedAt: &time,
|
||||
Driver: driver.Config().Name,
|
||||
}
|
||||
if f.IsDir() {
|
||||
file.Type = conf.FOLDER
|
||||
} else {
|
||||
file.Type = utils.GetFileType(filepath.Ext(f.Name()))
|
||||
file.Size = f.Size()
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
_, err = base.GetCache(path, account)
|
||||
if len(files) != 0 && err != nil {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Native) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
_, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fullPath := filepath.Join(account.RootFolder, args.Path)
|
||||
s, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.IsDir() {
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
link := base.Link{
|
||||
FilePath: fullPath,
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Native) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
log.Debugf("native path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
//file.Url, _ = driver.Link(path, account)
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
//model.SortFiles(files, account)
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver Native) Proxy(r *http.Request, account *model.Account) {
|
||||
// // unnecessary
|
||||
//}
|
||||
|
||||
func (driver Native) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Native) MakeDir(path string, account *model.Account) error {
|
||||
if utils.IsContain(strings.Split(path, "/"), "..") {
|
||||
return base.ErrRelativePath
|
||||
}
|
||||
fullPath := filepath.Join(account.RootFolder, path)
|
||||
err := os.MkdirAll(fullPath, 0700)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Native) Move(src string, dst string, account *model.Account) error {
|
||||
if utils.IsContain(strings.Split(src+"/"+dst, "/"), "..") {
|
||||
return base.ErrRelativePath
|
||||
}
|
||||
fullSrc := filepath.Join(account.RootFolder, src)
|
||||
fullDst := filepath.Join(account.RootFolder, dst)
|
||||
return os.Rename(fullSrc, fullDst)
|
||||
}
|
||||
|
||||
func (driver Native) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver Native) Copy(src string, dst string, account *model.Account) error {
|
||||
if utils.IsContain(strings.Split(src+"/"+dst, "/"), "..") {
|
||||
return base.ErrRelativePath
|
||||
}
|
||||
fullSrc := filepath.Join(account.RootFolder, src)
|
||||
fullDst := filepath.Join(account.RootFolder, dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstFile, err := driver.File(dst, account)
|
||||
if err == nil {
|
||||
if !dstFile.IsDir() {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
}
|
||||
if srcFile.IsDir() {
|
||||
return driver.CopyDir(fullSrc, fullDst)
|
||||
}
|
||||
return driver.CopyFile(fullSrc, fullDst)
|
||||
}
|
||||
|
||||
func (driver Native) Delete(path string, account *model.Account) error {
|
||||
if utils.IsContain(strings.Split(path, "/"), "..") {
|
||||
return base.ErrRelativePath
|
||||
}
|
||||
fullPath := filepath.Join(account.RootFolder, path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if file.IsDir() {
|
||||
return os.RemoveAll(fullPath)
|
||||
}
|
||||
return os.Remove(fullPath)
|
||||
}
|
||||
|
||||
func (driver Native) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
if utils.IsContain(strings.Split(file.ParentPath, "/"), "..") {
|
||||
return base.ErrRelativePath
|
||||
}
|
||||
fullPath := filepath.Join(account.RootFolder, file.ParentPath, file.Name)
|
||||
_, err := driver.File(filepath.Join(file.ParentPath, file.Name), account)
|
||||
if err == nil {
|
||||
// TODO overwrite?
|
||||
}
|
||||
basePath := filepath.Dir(fullPath)
|
||||
if !utils.Exists(basePath) {
|
||||
err := os.MkdirAll(basePath, 0744)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
out, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = out.Close()
|
||||
}()
|
||||
//var buf bytes.Buffer
|
||||
//reader := io.TeeReader(file, &buf)
|
||||
//h := md5.New()
|
||||
//_, err = io.Copy(h, reader)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//hash := hex.EncodeToString(h.Sum(nil))
|
||||
//log.Debugln("md5:", hash)
|
||||
//_, err = io.Copy(out, &buf)
|
||||
_, err = io.Copy(out, file)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Native)(nil)
|
||||
74
drivers/native/native.go
Normal file
74
drivers/native/native.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package native
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
// File copies a single file from src to dst
|
||||
func (driver *Native) CopyFile(src, dst string) error {
|
||||
var err error
|
||||
var srcfd *os.File
|
||||
var dstfd *os.File
|
||||
var srcinfo os.FileInfo
|
||||
|
||||
if srcfd, err = os.Open(src); err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcfd.Close()
|
||||
|
||||
if dstfd, err = os.Create(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstfd.Close()
|
||||
|
||||
if _, err = io.Copy(dstfd, srcfd); err != nil {
|
||||
return err
|
||||
}
|
||||
if srcinfo, err = os.Stat(src); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(dst, srcinfo.Mode())
|
||||
}
|
||||
|
||||
// Dir copies a whole directory recursively
|
||||
func (driver *Native) CopyDir(src string, dst string) error {
|
||||
var err error
|
||||
var fds []os.FileInfo
|
||||
var srcinfo os.FileInfo
|
||||
|
||||
if srcinfo, err = os.Stat(src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fds, err = ioutil.ReadDir(src); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fd := range fds {
|
||||
srcfp := path.Join(src, fd.Name())
|
||||
dstfp := path.Join(dst, fd.Name())
|
||||
|
||||
if fd.IsDir() {
|
||||
if err = driver.CopyDir(srcfp, dstfp); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
} else {
|
||||
if err = driver.CopyFile(srcfp, dstfp); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Native{})
|
||||
}
|
||||
279
drivers/onedrive/driver.go
Normal file
279
drivers/onedrive/driver.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package onedrive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Onedrive struct{}
|
||||
|
||||
func (driver Onedrive) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Onedrive",
|
||||
NoNeedSetLink: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Onedrive) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "zone",
|
||||
Label: "zone",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "global,cn,us,de",
|
||||
Description: "",
|
||||
},
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "onedrive type",
|
||||
Type: base.TypeSelect,
|
||||
Required: true,
|
||||
Values: "onedrive,sharepoint",
|
||||
},
|
||||
{
|
||||
Name: "client_id",
|
||||
Label: "client id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "client_secret",
|
||||
Label: "client secret",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "redirect_uri",
|
||||
Label: "redirect uri",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Default: "https://tool.nn.ci/onedrive/callback",
|
||||
},
|
||||
{
|
||||
Name: "refresh_token",
|
||||
Label: "refresh token",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "site_id",
|
||||
Label: "site id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "name,size,lastModifiedDateTime",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "asc,desc",
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Onedrive) Save(account *model.Account, old *model.Account) error {
|
||||
//if old != nil {
|
||||
// conf.Cron.Remove(cron.EntryID(old.CronId))
|
||||
//}
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
_, ok := onedriveHostMap[account.Zone]
|
||||
if !ok {
|
||||
return fmt.Errorf("no [%s] zone", account.Zone)
|
||||
}
|
||||
account.RootFolder = utils.ParsePath(account.RootFolder)
|
||||
err := driver.RefreshToken(account)
|
||||
_ = model.SaveAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//cronId, err := conf.Cron.AddFunc("@every 1h", func() {
|
||||
// name := account.Name
|
||||
// log.Debugf("onedrive account name: %s", name)
|
||||
// newAccount, ok := model.GetAccount(name)
|
||||
// log.Debugf("onedrive account: %+v", newAccount)
|
||||
// if !ok {
|
||||
// return
|
||||
// }
|
||||
// err = driver.RefreshToken(&newAccount)
|
||||
// _ = model.SaveAccount(&newAccount)
|
||||
//})
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//account.CronId = int(cronId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Onedrive) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ := cache.([]model.File)
|
||||
return files, nil
|
||||
}
|
||||
rawFiles, err := driver.GetFiles(account, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.GetFile(account, args.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.File == nil {
|
||||
return nil, base.ErrNotFile
|
||||
}
|
||||
link := base.Link{
|
||||
Url: file.Url,
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
log.Debugf("onedrive path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver Onedrive) Proxy(r *http.Request, account *model.Account) {
|
||||
// r.Header.Del("Origin")
|
||||
//}
|
||||
|
||||
func (driver Onedrive) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Onedrive) MakeDir(path string, account *model.Account) error {
|
||||
url := driver.GetMetaUrl(account, false, utils.Dir(path)) + "/children"
|
||||
data := base.Json{
|
||||
"name": utils.Base(path),
|
||||
"folder": base.Json{},
|
||||
"@microsoft.graph.conflictBehavior": "rename",
|
||||
}
|
||||
_, err := driver.Request(url, base.Post, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) Move(src string, dst string, account *model.Account) error {
|
||||
dstParentFile, err := driver.GetFile(account, utils.Dir(dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"parentReference": base.Json{
|
||||
"id": dstParentFile.Id,
|
||||
},
|
||||
"name": utils.Base(dst),
|
||||
}
|
||||
url := driver.GetMetaUrl(account, false, src)
|
||||
_, err = driver.Request(url, base.Patch, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver Onedrive) Copy(src string, dst string, account *model.Account) error {
|
||||
dstParentFile, err := driver.GetFile(account, utils.Dir(dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"parentReference": base.Json{
|
||||
"driveId": dstParentFile.ParentReference.DriveId,
|
||||
"id": dstParentFile.Id,
|
||||
},
|
||||
"name": utils.Base(dst),
|
||||
}
|
||||
url := driver.GetMetaUrl(account, false, src) + "/copy"
|
||||
_, err = driver.Request(url, base.Post, nil, nil, nil, &data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) Delete(path string, account *model.Account) error {
|
||||
url := driver.GetMetaUrl(account, false, path)
|
||||
_, err := driver.Request(url, base.Delete, nil, nil, nil, nil, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
var err error
|
||||
if file.GetSize() <= 4*1024*1024 {
|
||||
err = driver.UploadSmall(file, account)
|
||||
} else {
|
||||
err = driver.UploadBig(file, account)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Onedrive)(nil)
|
||||
320
drivers/onedrive/onedrive.go
Normal file
320
drivers/onedrive/onedrive.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package onedrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Host struct {
|
||||
Oauth string
|
||||
Api string
|
||||
}
|
||||
|
||||
var onedriveHostMap = map[string]Host{
|
||||
"global": {
|
||||
Oauth: "https://login.microsoftonline.com",
|
||||
Api: "https://graph.microsoft.com",
|
||||
},
|
||||
"cn": {
|
||||
Oauth: "https://login.chinacloudapi.cn",
|
||||
Api: "https://microsoftgraph.chinacloudapi.cn",
|
||||
},
|
||||
"us": {
|
||||
Oauth: "https://login.microsoftonline.us",
|
||||
Api: "https://graph.microsoft.us",
|
||||
},
|
||||
"de": {
|
||||
Oauth: "https://login.microsoftonline.de",
|
||||
Api: "https://graph.microsoft.de",
|
||||
},
|
||||
}
|
||||
|
||||
func (driver Onedrive) GetMetaUrl(account *model.Account, auth bool, path string) string {
|
||||
path = filepath.Join(account.RootFolder, path)
|
||||
//log.Debugf(path)
|
||||
host, _ := onedriveHostMap[account.Zone]
|
||||
if auth {
|
||||
return host.Oauth
|
||||
}
|
||||
switch account.InternalType {
|
||||
case "onedrive":
|
||||
{
|
||||
if path == "/" || path == "\\" {
|
||||
return fmt.Sprintf("%s/v1.0/me/drive/root", host.Api)
|
||||
} else {
|
||||
return fmt.Sprintf("%s/v1.0/me/drive/root:%s:", host.Api, path)
|
||||
}
|
||||
}
|
||||
case "sharepoint":
|
||||
{
|
||||
if path == "/" || path == "\\" {
|
||||
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root", host.Api, account.SiteId)
|
||||
} else {
|
||||
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root:%s:", host.Api, account.SiteId, path)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type OneTokenErr struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
func (driver Onedrive) RefreshToken(account *model.Account) error {
|
||||
err := driver.refreshToken(account)
|
||||
if err != nil && err == base.ErrEmptyToken {
|
||||
return driver.refreshToken(account)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) refreshToken(account *model.Account) error {
|
||||
url := driver.GetMetaUrl(account, true, "") + "/common/oauth2/v2.0/token"
|
||||
var resp base.TokenResp
|
||||
var e OneTokenErr
|
||||
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": account.ClientId,
|
||||
"client_secret": account.ClientSecret,
|
||||
"redirect_uri": account.RedirectUri,
|
||||
"refresh_token": account.RefreshToken,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
return err
|
||||
}
|
||||
if e.Error != "" {
|
||||
account.Status = e.ErrorDescription
|
||||
return fmt.Errorf("%s", e.ErrorDescription)
|
||||
} else {
|
||||
account.Status = "work"
|
||||
}
|
||||
if resp.RefreshToken == "" {
|
||||
account.Status = base.ErrEmptyToken.Error()
|
||||
return base.ErrEmptyToken
|
||||
}
|
||||
account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken
|
||||
return nil
|
||||
}
|
||||
|
||||
type OneFile struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
LastModifiedDateTime *time.Time `json:"lastModifiedDateTime"`
|
||||
Url string `json:"@microsoft.graph.downloadUrl"`
|
||||
File *struct {
|
||||
MimeType string `json:"mimeType"`
|
||||
} `json:"file"`
|
||||
Thumbnails []struct {
|
||||
Medium struct {
|
||||
Url string `json:"url"`
|
||||
} `json:"medium"`
|
||||
} `json:"thumbnails"`
|
||||
ParentReference struct {
|
||||
DriveId string `json:"driveId"`
|
||||
} `json:"parentReference"`
|
||||
}
|
||||
|
||||
type OneFiles struct {
|
||||
Value []OneFile `json:"value"`
|
||||
NextLink string `json:"@odata.nextLink"`
|
||||
}
|
||||
|
||||
type OneRespErr struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (driver Onedrive) FormatFile(file *OneFile) *model.File {
|
||||
f := &model.File{
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
UpdatedAt: file.LastModifiedDateTime,
|
||||
Driver: driver.Config().Name,
|
||||
Url: file.Url,
|
||||
Id: file.Id,
|
||||
}
|
||||
if len(file.Thumbnails) > 0 {
|
||||
f.Thumbnail = file.Thumbnails[0].Medium.Url
|
||||
}
|
||||
if file.File == nil {
|
||||
f.Type = conf.FOLDER
|
||||
} else {
|
||||
f.Type = utils.GetFileType(filepath.Ext(file.Name))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (driver Onedrive) GetFiles(account *model.Account, path string) ([]OneFile, error) {
|
||||
var res []OneFile
|
||||
nextLink := driver.GetMetaUrl(account, false, path) + "/children?$expand=thumbnails"
|
||||
if account.OrderBy != "" {
|
||||
nextLink += fmt.Sprintf("&orderby=%s", account.OrderBy)
|
||||
if account.OrderDirection != "" {
|
||||
nextLink += fmt.Sprintf("%%20%s", account.OrderDirection)
|
||||
}
|
||||
}
|
||||
for nextLink != "" {
|
||||
var files OneFiles
|
||||
_, err := driver.Request(nextLink, base.Get, nil, nil, nil, nil, &files, account)
|
||||
//var e OneRespErr
|
||||
//_, err := oneClient.R().SetResult(&files).SetError(&e).
|
||||
// SetHeader("Authorization", "Bearer "+account.AccessToken).
|
||||
// Get(nextLink)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//if e.Error.Code != "" {
|
||||
// return nil, fmt.Errorf("%s", e.Error.Message)
|
||||
//}
|
||||
res = append(res, files.Value...)
|
||||
nextLink = files.NextLink
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) GetFile(account *model.Account, path string) (*OneFile, error) {
|
||||
var file OneFile
|
||||
//var e OneRespErr
|
||||
u := driver.GetMetaUrl(account, false, path)
|
||||
_, err := driver.Request(u, base.Get, nil, nil, nil, nil, &file, account)
|
||||
//_, err := oneClient.R().SetResult(&file).SetError(&e).
|
||||
// SetHeader("Authorization", "Bearer "+account.AccessToken).
|
||||
// Get(driver.GetMetaUrl(account, false, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//if e.Error.Code != "" {
|
||||
// return nil, fmt.Errorf("%s", e.Error.Message)
|
||||
//}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) Request(url string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
rawUrl := url
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+account.AccessToken)
|
||||
if headers != nil {
|
||||
req.SetHeaders(headers)
|
||||
}
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
if form != nil {
|
||||
req.SetFormData(form)
|
||||
}
|
||||
if data != nil {
|
||||
req.SetBody(data)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var res *resty.Response
|
||||
var err error
|
||||
var e OneRespErr
|
||||
req.SetError(&e)
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(url)
|
||||
case base.Put:
|
||||
res, err = req.Put(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//log.Debug(res.String())
|
||||
if e.Error.Code != "" {
|
||||
if e.Error.Code == "InvalidAuthenticationToken" {
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
_ = model.SaveAccount(account)
|
||||
return nil, err
|
||||
}
|
||||
return driver.Request(rawUrl, method, headers, query, form, data, resp, account)
|
||||
}
|
||||
return nil, errors.New(e.Error.Message)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (driver Onedrive) UploadSmall(file *model.FileStream, account *model.Account) error {
|
||||
url := driver.GetMetaUrl(account, false, utils.Join(file.ParentPath, file.Name)) + "/content"
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request(url, base.Put, nil, nil, nil, data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Onedrive) UploadBig(file *model.FileStream, account *model.Account) error {
|
||||
url := driver.GetMetaUrl(account, false, utils.Join(file.ParentPath, file.Name)) + "/createUploadSession"
|
||||
res, err := driver.Request(url, base.Post, nil, nil, nil, nil, nil, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
|
||||
var finish uint64 = 0
|
||||
const DEFAULT = 4 * 1024 * 1024
|
||||
for finish < file.GetSize() {
|
||||
log.Debugf("upload: %d", finish)
|
||||
var byteSize uint64 = DEFAULT
|
||||
left := file.GetSize() - finish
|
||||
if left < DEFAULT {
|
||||
byteSize = left
|
||||
}
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(file, byteData)
|
||||
log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("PUT", uploadUrl, bytes.NewBuffer(byteData))
|
||||
req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
|
||||
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, file.Size))
|
||||
finish += byteSize
|
||||
res, err := base.HttpClient.Do(req)
|
||||
if res.StatusCode != 201 && res.StatusCode != 202 {
|
||||
data, _ := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(string(data))
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Onedrive{})
|
||||
}
|
||||
111
drivers/operate/operate.go
Normal file
111
drivers/operate/operate.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package operate
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
func Save(driver base.Driver, account, old *model.Account) error {
|
||||
return driver.Save(account, old)
|
||||
}
|
||||
|
||||
func Path(driver base.Driver, account *model.Account, path string) (*model.File, []model.File, error) {
|
||||
return driver.Path(path, account)
|
||||
}
|
||||
|
||||
func Files(driver base.Driver, account *model.Account, path string) ([]model.File, error) {
|
||||
_, files, err := Path(driver, account, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if files == nil {
|
||||
return nil, base.ErrNotFolder
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func File(driver base.Driver, account *model.Account, path string) (*model.File, error) {
|
||||
return driver.File(path, account)
|
||||
}
|
||||
|
||||
func MakeDir(driver base.Driver, account *model.Account, path string, clearCache bool) error {
|
||||
log.Debugf("mkdir: %s", path)
|
||||
_, err := Files(driver, account, path)
|
||||
if err != base.ErrPathNotFound {
|
||||
return nil
|
||||
}
|
||||
err = driver.MakeDir(path, account)
|
||||
if err == nil && clearCache {
|
||||
_ = base.DeleteCache(utils.Dir(path), account)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("mkdir error: %s", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Move(driver base.Driver, account *model.Account, src, dst string, clearCache bool) error {
|
||||
log.Debugf("move %s to %s", src, dst)
|
||||
rename := false
|
||||
if utils.Dir(src) == utils.Dir(dst) {
|
||||
rename = true
|
||||
}
|
||||
var err error
|
||||
if rename {
|
||||
err = driver.Rename(src, dst, account)
|
||||
} else {
|
||||
err = driver.Move(src, dst, account)
|
||||
}
|
||||
if err == nil && clearCache {
|
||||
_ = base.DeleteCache(utils.Dir(src), account)
|
||||
if !rename {
|
||||
_ = base.DeleteCache(utils.Dir(dst), account)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("move error: %s", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Copy(driver base.Driver, account *model.Account, src, dst string, clearCache bool) error {
|
||||
log.Debugf("copy %s to %s", src, dst)
|
||||
err := driver.Copy(src, dst, account)
|
||||
if err == nil && clearCache {
|
||||
_ = base.DeleteCache(utils.Dir(dst), account)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("copy error: %s", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Delete(driver base.Driver, account *model.Account, path string, clearCache bool) error {
|
||||
log.Debugf("delete %s", path)
|
||||
err := driver.Delete(path, account)
|
||||
if err == nil && clearCache {
|
||||
_ = base.DeleteCache(utils.Dir(path), account)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("delete error: %s", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Upload(driver base.Driver, account *model.Account, file *model.FileStream, clearCache bool) error {
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
err := driver.Upload(file, account)
|
||||
if err == nil && clearCache {
|
||||
_ = base.DeleteCache(file.ParentPath, account)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("upload error: %s", err.Error())
|
||||
}
|
||||
debug.FreeOSMemory()
|
||||
return err
|
||||
}
|
||||
330
drivers/pikpak/driver.go
Normal file
330
drivers/pikpak/driver.go
Normal file
@@ -0,0 +1,330 @@
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PikPak struct{}
|
||||
|
||||
func (driver PikPak) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "PikPak",
|
||||
ApiProxy: true,
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver PikPak) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "username",
|
||||
Label: "username",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "password",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder id",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver PikPak) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
err := driver.Login(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver PikPak) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var files []model.File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ = cache.([]model.File)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawFiles, err := driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = make([]model.File, 0)
|
||||
for _, file := range rawFiles {
|
||||
files = append(files, *driver.FormatFile(&file))
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver PikPak) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
file, err := driver.File(args.Path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp File
|
||||
_, err = driver.Request(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.Id),
|
||||
base.Get, nil, nil, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link := base.Link{
|
||||
Url: resp.WebContentLink,
|
||||
}
|
||||
if len(resp.Medias) > 0 && resp.Medias[0].Link.Url != "" {
|
||||
log.Debugln("use media link")
|
||||
link.Url = resp.Medias[0].Link.Url
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (driver PikPak) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("pikpak path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver PikPak) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver PikPak) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver PikPak) MakeDir(path string, account *model.Account) error {
|
||||
path = utils.ParsePath(path)
|
||||
dir, name := filepath.Split(path)
|
||||
parentFile, err := driver.File(dir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !parentFile.IsDir() {
|
||||
return base.ErrNotFolder
|
||||
}
|
||||
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Post, nil, &base.Json{
|
||||
"kind": "drive#folder",
|
||||
"parent_id": parentFile.Id,
|
||||
"name": name,
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) Move(src string, dst string, account *model.Account) error {
|
||||
dstDir, _ := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstDirFile, err := driver.File(dstDir, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchMove", base.Post, nil, &base.Json{
|
||||
"ids": []string{srcFile.Id},
|
||||
"to": base.Json{
|
||||
"parent_id": dstDirFile.Id,
|
||||
},
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) Rename(src string, dst string, account *model.Account) error {
|
||||
_, dstName := filepath.Split(dst)
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files/"+srcFile.Id, base.Patch, nil, &base.Json{
|
||||
"name": dstName,
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) Copy(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstDirFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchCopy", base.Post, nil, &base.Json{
|
||||
"ids": []string{srcFile.Id},
|
||||
"to": base.Json{
|
||||
"parent_id": dstDirFile.Id,
|
||||
},
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) Delete(path string, account *model.Account) error {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchTrash", base.Post, nil, &base.Json{
|
||||
"ids": []string{file.Id},
|
||||
}, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver PikPak) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"kind": "drive#file",
|
||||
"name": file.GetFileName(),
|
||||
"size": file.GetSize(),
|
||||
"hash": "1CF254FBC456E1B012CD45C546636AA62CF8350E",
|
||||
"upload_type": "UPLOAD_TYPE_RESUMABLE",
|
||||
"objProvider": base.Json{"provider": "UPLOAD_TYPE_UNKNOWN"},
|
||||
"parent_id": parentFile.Id,
|
||||
}
|
||||
res, err := driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Post, nil, &data, nil, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params := jsoniter.Get(res, "resumable").Get("params")
|
||||
endpoint := params.Get("endpoint").ToString()
|
||||
endpointS := strings.Split(endpoint, ".")
|
||||
endpoint = strings.Join(endpointS[1:], ".")
|
||||
accessKeyId := params.Get("access_key_id").ToString()
|
||||
accessKeySecret := params.Get("access_key_secret").ToString()
|
||||
securityToken := params.Get("security_token").ToString()
|
||||
key := params.Get("key").ToString()
|
||||
bucket := params.Get("bucket").ToString()
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyId, accessKeySecret, securityToken),
|
||||
Region: aws.String("pikpak"),
|
||||
Endpoint: &endpoint,
|
||||
}
|
||||
s, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
Body: file,
|
||||
}
|
||||
_, err = uploader.Upload(input)
|
||||
return err
|
||||
}
|
||||
|
||||
// use aliyun-oss-sdk
|
||||
//func (driver PikPak) Upload(file *model.FileStream, account *model.Account) error {
|
||||
// if file == nil {
|
||||
// return base.ErrEmptyFile
|
||||
// }
|
||||
// parentFile, err := driver.File(file.ParentPath, account)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// data := base.Json{
|
||||
// "kind": "drive#file",
|
||||
// "name": file.GetFileName(),
|
||||
// "size": file.GetSize(),
|
||||
// "hash": "1CF254FBC456E1B012CD45C546636AA62CF8350E",
|
||||
// "upload_type": "UPLOAD_TYPE_RESUMABLE",
|
||||
// "objProvider": base.Json{"provider": "UPLOAD_TYPE_UNKNOWN"},
|
||||
// "parent_id": parentFile.Id,
|
||||
// }
|
||||
// res, err := driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Post, nil, &data, nil, account)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// params := jsoniter.Get(res, "resumable").Get("params")
|
||||
// endpoint := params.Get("endpoint").ToString()
|
||||
// endpointS := strings.Split(endpoint, ".")
|
||||
// endpoint = strings.Join(endpointS[1:], ".")
|
||||
// accessKeyId := params.Get("access_key_id").ToString()
|
||||
// accessKeySecret := params.Get("access_key_secret").ToString()
|
||||
// securityToken := params.Get("security_token").ToString()
|
||||
// client, err := oss.New("https://"+endpoint, accessKeyId,
|
||||
// accessKeySecret, oss.SecurityToken(securityToken))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// bucket, err := client.Bucket(params.Get("bucket").ToString())
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// signedURL, err := bucket.SignURL(params.Get("key").ToString(), oss.HTTPPut, 60)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// err = bucket.PutObjectWithURL(signedURL, file)
|
||||
// return err
|
||||
//}
|
||||
|
||||
var _ base.Driver = (*PikPak)(nil)
|
||||
232
drivers/pikpak/pikpak.go
Normal file
232
drivers/pikpak/pikpak.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RespErr struct {
|
||||
ErrorCode int `json:"error_code"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func (driver PikPak) Login(account *model.Account) error {
|
||||
url := "https://user.mypikpak.com/v1/auth/signin"
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
|
||||
"captcha_token": "",
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"username": account.Username,
|
||||
"password": account.Password,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if e.ErrorCode != 0 {
|
||||
account.Status = e.Error
|
||||
err = errors.New(e.Error)
|
||||
} else {
|
||||
data := res.Body()
|
||||
account.Status = "work"
|
||||
account.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
account.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver PikPak) RefreshToken(account *model.Account) error {
|
||||
url := "https://user.mypikpak.com/v1/auth/token"
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
var e RespErr
|
||||
res, err := base.RestyClient.R().SetError(&e).
|
||||
SetHeader("user-agent", "").SetBody(base.Json{
|
||||
"client_id": "YNxT9w7GMdWvEOKa",
|
||||
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": account.RefreshToken,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
return err
|
||||
}
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 4126 {
|
||||
// refresh_token 失效,重新登陆
|
||||
return driver.Login(account)
|
||||
}
|
||||
account.Status = e.Error
|
||||
_ = model.SaveAccount(account)
|
||||
return errors.New(e.Error)
|
||||
}
|
||||
data := res.Body()
|
||||
account.Status = "work"
|
||||
account.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
account.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
log.Debugf("%s\n %+v", res.String(), account)
|
||||
_ = model.SaveAccount(account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver PikPak) Request(url string, method int, query map[string]string, data *base.Json, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
rawUrl := url
|
||||
if account.APIProxyUrl != "" {
|
||||
url = fmt.Sprintf("%s/%s", account.APIProxyUrl, url)
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+account.AccessToken)
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
if data != nil {
|
||||
req.SetBody(data)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var e RespErr
|
||||
req.SetError(&e)
|
||||
var res *resty.Response
|
||||
var err error
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(url)
|
||||
case base.Post:
|
||||
res, err = req.Post(url)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(url)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(res.String())
|
||||
if e.ErrorCode != 0 {
|
||||
if e.ErrorCode == 16 {
|
||||
// login / refresh token
|
||||
err = driver.RefreshToken(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return driver.Request(rawUrl, method, query, data, resp, account)
|
||||
} else {
|
||||
return nil, errors.New(e.Error)
|
||||
}
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Id string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
ModifiedTime *time.Time `json:"modified_time"`
|
||||
Size string `json:"size"`
|
||||
ThumbnailLink string `json:"thumbnail_link"`
|
||||
WebContentLink string `json:"web_content_link"`
|
||||
Medias []Media `json:"medias"`
|
||||
}
|
||||
|
||||
type Media struct {
|
||||
MediaId string `json:"media_id"`
|
||||
MediaName string `json:"media_name"`
|
||||
Video struct {
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
Duration int `json:"duration"`
|
||||
BitRate int `json:"bit_rate"`
|
||||
FrameRate int `json:"frame_rate"`
|
||||
VideoCodec string `json:"video_codec"`
|
||||
AudioCodec string `json:"audio_codec"`
|
||||
VideoType string `json:"video_type"`
|
||||
} `json:"video"`
|
||||
Link struct {
|
||||
Url string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
Expire time.Time `json:"expire"`
|
||||
} `json:"link"`
|
||||
NeedMoreQuota bool `json:"need_more_quota"`
|
||||
VipTypes []interface{} `json:"vip_types"`
|
||||
RedirectLink string `json:"redirect_link"`
|
||||
IconLink string `json:"icon_link"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
Priority int `json:"priority"`
|
||||
IsOrigin bool `json:"is_origin"`
|
||||
ResolutionName string `json:"resolution_name"`
|
||||
IsVisible bool `json:"is_visible"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
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{})
|
||||
}
|
||||
327
drivers/quark/driver.go
Normal file
327
drivers/quark/driver.go
Normal file
@@ -0,0 +1,327 @@
|
||||
package quark
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Quark struct{}
|
||||
|
||||
func (driver Quark) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "Quark",
|
||||
OnlyProxy: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Quark) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "access_token",
|
||||
Label: "Cookie",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Description: "Unknown expiration time",
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder file_id",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
Default: "0",
|
||||
},
|
||||
{
|
||||
Name: "order_by",
|
||||
Label: "order_by",
|
||||
Type: base.TypeSelect,
|
||||
Values: "file_type,file_name,updated_at",
|
||||
Required: true,
|
||||
Default: "file_name",
|
||||
},
|
||||
{
|
||||
Name: "order_direction",
|
||||
Label: "order_direction",
|
||||
Type: base.TypeSelect,
|
||||
Values: "asc,desc",
|
||||
Required: true,
|
||||
Default: "asc",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver Quark) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := driver.Get("/config", nil, nil, account)
|
||||
if err == nil {
|
||||
account.Status = "work"
|
||||
} else {
|
||||
account.Status = err.Error()
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Quark) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver Quark) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var files []model.File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ = cache.([]model.File)
|
||||
} else {
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files, err = driver.GetFiles(file.Id, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver Quark) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
path := args.Path
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := base.Json{
|
||||
"fids": []string{file.Id},
|
||||
}
|
||||
var resp DownResp
|
||||
_, err = driver.Post("/file/download", data, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{
|
||||
Url: resp.Data[0].DownloadUrl,
|
||||
Headers: []base.Header{
|
||||
{Name: "Cookie", Value: account.AccessToken},
|
||||
{Name: "Referer", Value: "https://pan.quark.cn"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver Quark) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("quark path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
func (driver Quark) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Quark) MakeDir(path string, account *model.Account) error {
|
||||
parentFile, err := driver.File(utils.Dir(path), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"dir_init_lock": false,
|
||||
"dir_path": "",
|
||||
"file_name": utils.Base(path),
|
||||
"pdir_fid": parentFile.Id,
|
||||
}
|
||||
_, err = driver.Post("/file", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Quark) Move(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstParentFile, err := driver.File(utils.Dir(dst), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"action_type": 1,
|
||||
"exclude_fids": []string{},
|
||||
"filelist": []string{srcFile.Id},
|
||||
"to_pdir_fid": dstParentFile.Id,
|
||||
}
|
||||
_, err = driver.Post("/file/move", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Quark) Rename(src string, dst string, account *model.Account) error {
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"fid": srcFile.Id,
|
||||
"file_name": utils.Base(dst),
|
||||
}
|
||||
_, err = driver.Post("/file/rename", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Quark) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver Quark) Delete(path string, account *model.Account) error {
|
||||
srcFile, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := base.Json{
|
||||
"action_type": 1,
|
||||
"exclude_fids": []string{},
|
||||
"filelist": []string{srcFile.Id},
|
||||
}
|
||||
_, err = driver.Post("/file/delete", data, nil, account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver Quark) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
parentFile, err := driver.File(file.ParentPath, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
_, err = io.Copy(tempFile, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := md5.New()
|
||||
_, err = io.Copy(m, tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
md5Str := hex.EncodeToString(m.Sum(nil))
|
||||
s := sha1.New()
|
||||
_, err = io.Copy(s, tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sha1Str := hex.EncodeToString(s.Sum(nil))
|
||||
// pre
|
||||
pre, err := driver.UpPre(file, parentFile.Id, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugln("hash: ", md5Str, sha1Str)
|
||||
// hash
|
||||
finish, err := driver.UpHash(md5Str, sha1Str, pre.Data.TaskId, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if finish {
|
||||
return nil
|
||||
}
|
||||
// part up
|
||||
partSize := pre.Metadata.PartSize
|
||||
var bytes []byte
|
||||
md5s := make([]string, 0)
|
||||
defaultBytes := make([]byte, partSize)
|
||||
left := int64(file.GetSize())
|
||||
partNumber := 1
|
||||
for left > 0 {
|
||||
if left > int64(partSize) {
|
||||
bytes = defaultBytes
|
||||
} else {
|
||||
bytes = make([]byte, left)
|
||||
}
|
||||
_, err := io.ReadFull(tempFile, bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
left -= int64(partSize)
|
||||
log.Debugf("left: %d", left)
|
||||
m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account)
|
||||
//m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account, md5Str, sha1Str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m == "finish" {
|
||||
return nil
|
||||
}
|
||||
md5s = append(md5s, m)
|
||||
partNumber++
|
||||
}
|
||||
err = driver.UpCommit(pre, md5s, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return driver.UpFinish(pre, account)
|
||||
}
|
||||
|
||||
var _ base.Driver = (*Quark)(nil)
|
||||
@@ -1,183 +1,135 @@
|
||||
package quark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/Xhofe/alist/utils/cookie"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/cookie"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *QuarkOrUC) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
u := d.conf.api + pathname
|
||||
func (driver Quark) Request(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
u := "https://drive.quark.cn/1/clouddrive" + pathname
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
"Cookie": d.Cookie,
|
||||
"Cookie": account.AccessToken,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": d.conf.referer,
|
||||
"Referer": "https://pan.quark.cn/",
|
||||
})
|
||||
req.SetQueryParam("pr", d.conf.pr)
|
||||
req.SetQueryParam("pr", "ucpro")
|
||||
req.SetQueryParam("fr", "pc")
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
if headers != nil {
|
||||
req.SetHeaders(headers)
|
||||
}
|
||||
if query != nil {
|
||||
req.SetQueryParams(query)
|
||||
}
|
||||
if form != nil {
|
||||
req.SetFormData(form)
|
||||
}
|
||||
if data != nil {
|
||||
req.SetBody(data)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
var e Resp
|
||||
var err error
|
||||
var res *resty.Response
|
||||
req.SetError(&e)
|
||||
res, err := req.Execute(method, u)
|
||||
switch method {
|
||||
case base.Get:
|
||||
res, err = req.Get(u)
|
||||
case base.Post:
|
||||
res, err = req.Post(u)
|
||||
case base.Delete:
|
||||
res, err = req.Delete(u)
|
||||
case base.Patch:
|
||||
res, err = req.Patch(u)
|
||||
case base.Put:
|
||||
res, err = req.Put(u)
|
||||
default:
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
__puus := cookie.GetCookie(res.Cookies(), "__puus")
|
||||
if __puus != nil {
|
||||
d.Cookie = cookie.SetStr(d.Cookie, "__puus", __puus.Value)
|
||||
op.MustSaveDriverStorage(d)
|
||||
account.AccessToken = cookie.SetStr(account.AccessToken, "__puus", __puus.Value)
|
||||
_ = model.SaveAccount(account)
|
||||
}
|
||||
|
||||
if d.UseTransCodingAddress && d.config.Name == "Quark" {
|
||||
__pus := cookie.GetCookie(res.Cookies(), "__pus")
|
||||
if __pus != nil {
|
||||
d.Cookie = cookie.SetStr(d.Cookie, "__pus", __pus.Value)
|
||||
op.MustSaveDriverStorage(d)
|
||||
}
|
||||
}
|
||||
|
||||
//log.Debugf("%s response: %s", pathname, res.String())
|
||||
if e.Status >= 400 || e.Code != 0 {
|
||||
return nil, errors.New(e.Message)
|
||||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *QuarkOrUC) GetFiles(parent string) ([]model.Obj, error) {
|
||||
files := make([]model.Obj, 0)
|
||||
func (driver Quark) Get(pathname string, query map[string]string, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
return driver.Request(pathname, base.Get, nil, query, nil, nil, resp, account)
|
||||
}
|
||||
|
||||
func (driver Quark) Post(pathname string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) {
|
||||
return driver.Request(pathname, base.Post, nil, nil, nil, data, resp, account)
|
||||
}
|
||||
|
||||
func (driver Quark) GetFiles(parent string, account *model.Account) ([]model.File, error) {
|
||||
files := make([]model.File, 0)
|
||||
page := 1
|
||||
size := 100
|
||||
query := map[string]string{
|
||||
"pdir_fid": parent,
|
||||
"_size": strconv.Itoa(size),
|
||||
"_fetch_total": "1",
|
||||
}
|
||||
if d.OrderBy != "none" {
|
||||
query["_sort"] = "file_type:asc," + d.OrderBy + ":" + d.OrderDirection
|
||||
"_sort": "file_type:asc," + account.OrderBy + ":" + account.OrderDirection,
|
||||
}
|
||||
for {
|
||||
query["_page"] = strconv.Itoa(page)
|
||||
var resp SortResp
|
||||
_, err := d.request("/file/sort", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
}, &resp)
|
||||
_, err := driver.Get("/file/sort", query, &resp, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range resp.Data.List {
|
||||
if d.OnlyListVideoFile {
|
||||
// 开启后 只列出视频文件和文件夹
|
||||
if file.IsDir() || file.Category == 1 {
|
||||
files = append(files, &file)
|
||||
}
|
||||
} else {
|
||||
files = append(files, &file)
|
||||
}
|
||||
for _, f := range resp.Data.List {
|
||||
files = append(files, *driver.formatFile(&f))
|
||||
}
|
||||
|
||||
if page*size >= resp.Metadata.Total {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (d *QuarkOrUC) getDownloadLink(file model.Obj) (*model.Link, error) {
|
||||
data := base.Json{
|
||||
"fids": []string{file.GetID()},
|
||||
}
|
||||
var resp DownResp
|
||||
ua := d.conf.ua
|
||||
_, err := d.request("/file/download", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetHeader("User-Agent", ua).
|
||||
SetBody(data)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: resp.Data[0].DownloadUrl,
|
||||
Header: http.Header{
|
||||
"Cookie": []string{d.Cookie},
|
||||
"Referer": []string{d.conf.referer},
|
||||
"User-Agent": []string{ua},
|
||||
},
|
||||
Concurrency: 3,
|
||||
PartSize: 10 * utils.MB,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *QuarkOrUC) getTranscodingLink(file model.Obj) (*model.Link, error) {
|
||||
data := base.Json{
|
||||
"fid": file.GetID(),
|
||||
"resolutions": "low,normal,high,super,2k,4k",
|
||||
"supports": "fmp4_av,m3u8,dolby_vision",
|
||||
}
|
||||
var resp TranscodingResp
|
||||
ua := d.conf.ua
|
||||
|
||||
_, err := d.request("/file/v2/play/project", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetHeader("User-Agent", ua).
|
||||
SetBody(data)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: resp.Data.VideoList[0].VideoInfo.URL,
|
||||
ContentLength: resp.Data.VideoList[0].VideoInfo.Size,
|
||||
Concurrency: 3,
|
||||
PartSize: 10 * utils.MB,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *QuarkOrUC) upPre(file model.FileStreamer, parentId string) (UpPreResp, error) {
|
||||
func (driver Quark) UpPre(file *model.FileStream, parentId string, account *model.Account) (UpPreResp, error) {
|
||||
now := time.Now()
|
||||
data := base.Json{
|
||||
"ccp_hash_update": true,
|
||||
"dir_name": "",
|
||||
"file_name": file.GetName(),
|
||||
"format_type": file.GetMimetype(),
|
||||
"file_name": file.Name,
|
||||
"format_type": file.MIMEType,
|
||||
"l_created_at": now.UnixMilli(),
|
||||
"l_updated_at": now.UnixMilli(),
|
||||
"pdir_fid": parentId,
|
||||
"size": file.GetSize(),
|
||||
//"same_path_reuse": true,
|
||||
"size": file.Size,
|
||||
}
|
||||
log.Debugf("uppre data: %+v", data)
|
||||
var resp UpPreResp
|
||||
_, err := d.request("/file/upload/pre", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, &resp)
|
||||
_, err := driver.Post("/file/upload/pre", data, &resp, account)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (d *QuarkOrUC) upHash(md5, sha1, taskId string) (bool, error) {
|
||||
func (driver Quark) UpHash(md5, sha1, taskId string, account *model.Account) (bool, error) {
|
||||
data := base.Json{
|
||||
"md5": md5,
|
||||
"sha1": sha1,
|
||||
@@ -185,14 +137,12 @@ func (d *QuarkOrUC) upHash(md5, sha1, taskId string) (bool, error) {
|
||||
}
|
||||
log.Debugf("hash: %+v", data)
|
||||
var resp HashResp
|
||||
_, err := d.request("/file/update/hash", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, &resp)
|
||||
_, err := driver.Post("/file/update/hash", data, &resp, account)
|
||||
return resp.Data.Finish, err
|
||||
}
|
||||
|
||||
func (d *QuarkOrUC) upPart(ctx context.Context, pre UpPreResp, mineType string, partNumber int, bytes io.Reader) (string, error) {
|
||||
//func (driver QuarkOrUC) UpPart(pre UpPreResp, mineType string, partNumber int, bytes []byte, account *model.Account, md5Str, sha1Str string) (string, error) {
|
||||
func (driver Quark) UpPart(pre UpPreResp, mineType string, partNumber int, bytes []byte, account *model.Account) (string, error) {
|
||||
//func (driver Quark) UpPart(pre UpPreResp, mineType string, partNumber int, bytes []byte, account *model.Account, md5Str, sha1Str string) (string, error) {
|
||||
timeStr := time.Now().UTC().Format(http.TimeFormat)
|
||||
data := base.Json{
|
||||
"auth_info": pre.Data.AuthInfo,
|
||||
@@ -207,9 +157,7 @@ x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit
|
||||
"task_id": pre.Data.TaskId,
|
||||
}
|
||||
var resp UpAuthResp
|
||||
_, err := d.request("/file/upload/auth", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data).SetContext(ctx)
|
||||
}, &resp)
|
||||
_, err := driver.Post("/file/upload/auth", data, &resp, account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -223,7 +171,7 @@ x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit
|
||||
// }
|
||||
//}
|
||||
u := fmt.Sprintf("https://%s.%s/%s", pre.Data.Bucket, pre.Data.UploadUrl[7:], pre.Data.ObjKey)
|
||||
res, err := base.RestyClient.R().SetContext(ctx).
|
||||
res, err := base.RestyClient.R().
|
||||
SetHeaders(map[string]string{
|
||||
"Authorization": resp.Data.AuthKey,
|
||||
"Content-Type": mineType,
|
||||
@@ -235,16 +183,13 @@ x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit
|
||||
"partNumber": strconv.Itoa(partNumber),
|
||||
"uploadId": pre.Data.UploadId,
|
||||
}).SetBody(bytes).Put(u)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if res.StatusCode() != 200 {
|
||||
return "", fmt.Errorf("up status: %d, error: %s", res.StatusCode(), res.String())
|
||||
}
|
||||
return res.Header().Get("Etag"), nil
|
||||
return res.Header().Get("ETag"), nil
|
||||
}
|
||||
|
||||
func (d *QuarkOrUC) upCommit(pre UpPreResp, md5s []string) error {
|
||||
func (driver Quark) UpCommit(pre UpPreResp, md5s []string, account *model.Account) error {
|
||||
timeStr := time.Now().UTC().Format(http.TimeFormat)
|
||||
log.Debugf("md5s: %+v", md5s)
|
||||
bodyBuilder := strings.Builder{}
|
||||
@@ -285,9 +230,7 @@ x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit
|
||||
log.Debugf("xml: %s", body)
|
||||
log.Debugf("auth data: %+v", data)
|
||||
var resp UpAuthResp
|
||||
_, err = d.request("/file/upload/auth", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, &resp)
|
||||
_, err = driver.Post("/file/upload/auth", data, &resp, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -305,26 +248,25 @@ x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit
|
||||
SetQueryParams(map[string]string{
|
||||
"uploadId": pre.Data.UploadId,
|
||||
}).SetBody(body).Post(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode() != 200 {
|
||||
return fmt.Errorf("up status: %d, error: %s", res.StatusCode(), res.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *QuarkOrUC) upFinish(pre UpPreResp) error {
|
||||
func (driver Quark) UpFinish(pre UpPreResp, account *model.Account) error {
|
||||
data := base.Json{
|
||||
"obj_key": pre.Data.ObjKey,
|
||||
"task_id": pre.Data.TaskId,
|
||||
}
|
||||
_, err := d.request("/file/upload/finish", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
_, err := driver.Post("/file/upload/finish", data, nil, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&Quark{})
|
||||
}
|
||||
134
drivers/quark/types.go
Normal file
134
drivers/quark/types.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package quark
|
||||
|
||||
type Resp struct {
|
||||
Status int `json:"status"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
//ReqId string `json:"req_id"`
|
||||
//Timestamp int `json:"timestamp"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Fid string `json:"fid"`
|
||||
FileName string `json:"file_name"`
|
||||
//PdirFid string `json:"pdir_fid"`
|
||||
//Category int `json:"category"`
|
||||
//FileType int `json:"file_type"`
|
||||
Size int64 `json:"size"`
|
||||
//FormatType string `json:"format_type"`
|
||||
//Status int `json:"status"`
|
||||
//Tags string `json:"tags,omitempty"`
|
||||
//LCreatedAt int64 `json:"l_created_at"`
|
||||
LUpdatedAt int64 `json:"l_updated_at"`
|
||||
//NameSpace int `json:"name_space"`
|
||||
//IncludeItems int `json:"include_items,omitempty"`
|
||||
//RiskType int `json:"risk_type"`
|
||||
//BackupSign int `json:"backup_sign"`
|
||||
//Duration int `json:"duration"`
|
||||
//FileSource string `json:"file_source"`
|
||||
File bool `json:"file"`
|
||||
//CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
//PrivateExtra struct {} `json:"_private_extra"`
|
||||
//ObjCategory string `json:"obj_category,omitempty"`
|
||||
//Thumbnail string `json:"thumbnail,omitempty"`
|
||||
}
|
||||
|
||||
type SortResp struct {
|
||||
Resp
|
||||
Data struct {
|
||||
List []File `json:"list"`
|
||||
} `json:"data"`
|
||||
Metadata struct {
|
||||
Size int `json:"_size"`
|
||||
Page int `json:"_page"`
|
||||
Count int `json:"_count"`
|
||||
Total int `json:"_total"`
|
||||
Way string `json:"way"`
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
type DownResp struct {
|
||||
Resp
|
||||
Data []struct {
|
||||
//Fid string `json:"fid"`
|
||||
//FileName string `json:"file_name"`
|
||||
//PdirFid string `json:"pdir_fid"`
|
||||
//Category int `json:"category"`
|
||||
//FileType int `json:"file_type"`
|
||||
//Size int `json:"size"`
|
||||
//FormatType string `json:"format_type"`
|
||||
//Status int `json:"status"`
|
||||
//Tags string `json:"tags"`
|
||||
//LCreatedAt int64 `json:"l_created_at"`
|
||||
//LUpdatedAt int64 `json:"l_updated_at"`
|
||||
//NameSpace int `json:"name_space"`
|
||||
//Thumbnail string `json:"thumbnail"`
|
||||
DownloadUrl string `json:"download_url"`
|
||||
//Md5 string `json:"md5"`
|
||||
//RiskType int `json:"risk_type"`
|
||||
//RangeSize int `json:"range_size"`
|
||||
//BackupSign int `json:"backup_sign"`
|
||||
//ObjCategory string `json:"obj_category"`
|
||||
//Duration int `json:"duration"`
|
||||
//FileSource string `json:"file_source"`
|
||||
//File bool `json:"file"`
|
||||
//CreatedAt int64 `json:"created_at"`
|
||||
//UpdatedAt int64 `json:"updated_at"`
|
||||
//PrivateExtra struct {
|
||||
//} `json:"_private_extra"`
|
||||
} `json:"data"`
|
||||
//Metadata struct {
|
||||
// Acc2 string `json:"acc2"`
|
||||
// Acc1 string `json:"acc1"`
|
||||
//} `json:"metadata"`
|
||||
}
|
||||
|
||||
type UpPreResp struct {
|
||||
Resp
|
||||
Data struct {
|
||||
TaskId string `json:"task_id"`
|
||||
Finish bool `json:"finish"`
|
||||
UploadId string `json:"upload_id"`
|
||||
ObjKey string `json:"obj_key"`
|
||||
UploadUrl string `json:"upload_url"`
|
||||
Fid string `json:"fid"`
|
||||
Bucket string `json:"bucket"`
|
||||
Callback struct {
|
||||
CallbackUrl string `json:"callbackUrl"`
|
||||
CallbackBody string `json:"callbackBody"`
|
||||
} `json:"callback"`
|
||||
FormatType string `json:"format_type"`
|
||||
Size int `json:"size"`
|
||||
AuthInfo string `json:"auth_info"`
|
||||
} `json:"data"`
|
||||
Metadata struct {
|
||||
PartThread int `json:"part_thread"`
|
||||
Acc2 string `json:"acc2"`
|
||||
Acc1 string `json:"acc1"`
|
||||
PartSize int `json:"part_size"` // 分片大小
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
type HashResp struct {
|
||||
Resp
|
||||
Data struct {
|
||||
Finish bool `json:"finish"`
|
||||
Fid string `json:"fid"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
FormatType string `json:"format_type"`
|
||||
} `json:"data"`
|
||||
Metadata struct {
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
type UpAuthResp struct {
|
||||
Resp
|
||||
Data struct {
|
||||
AuthKey string `json:"auth_key"`
|
||||
Speed int `json:"speed"`
|
||||
Headers []interface{} `json:"headers"`
|
||||
} `json:"data"`
|
||||
Metadata struct {
|
||||
} `json:"metadata"`
|
||||
}
|
||||
31
drivers/quark/util.go
Normal file
31
drivers/quark/util.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package quark
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getTime(t int64) *time.Time {
|
||||
tm := time.UnixMilli(t)
|
||||
//log.Debugln(tm)
|
||||
return &tm
|
||||
}
|
||||
|
||||
func (driver Quark) formatFile(f *File) *model.File {
|
||||
file := model.File{
|
||||
Id: f.Fid,
|
||||
Name: f.FileName,
|
||||
Size: f.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: getTime(f.UpdatedAt),
|
||||
}
|
||||
if f.File {
|
||||
file.Type = utils.GetFileType(path.Ext(f.FileName))
|
||||
} else {
|
||||
file.Type = conf.FOLDER
|
||||
}
|
||||
return &file
|
||||
}
|
||||
304
drivers/s3/driver.go
Normal file
304
drivers/s3/driver.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type S3 struct {
|
||||
}
|
||||
|
||||
func (driver S3) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "S3",
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver S3) Items() []base.Item {
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "bucket",
|
||||
Label: "Bucket",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "endpoint",
|
||||
Label: "Endpoint",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "region",
|
||||
Label: "Region",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "access_key",
|
||||
Label: "Access Key",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "access_secret",
|
||||
Label: "Access Secret",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "custom_host",
|
||||
Label: "Custom Host",
|
||||
Type: base.TypeString,
|
||||
},
|
||||
{
|
||||
Name: "limit",
|
||||
Label: "Sign url expire time(hours)",
|
||||
Type: base.TypeNumber,
|
||||
Description: "default 4 hours",
|
||||
},
|
||||
{
|
||||
Name: "zone",
|
||||
Label: "placeholder filename",
|
||||
Type: base.TypeString,
|
||||
Description: "default empty string",
|
||||
Default: defaultPlaceholderName,
|
||||
},
|
||||
{
|
||||
Name: "bool_1",
|
||||
Label: "S3ForcePathStyle",
|
||||
Type: base.TypeBool,
|
||||
},
|
||||
{
|
||||
Name: "internal_type",
|
||||
Label: "ListObject Version",
|
||||
Type: base.TypeSelect,
|
||||
Values: "v1,v2",
|
||||
Default: "v1",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver S3) Save(account *model.Account, old *model.Account) error {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if account.Limit == 0 {
|
||||
account.Limit = 4
|
||||
}
|
||||
client, err := driver.NewSession(account)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
} else {
|
||||
sessionsMap[account.Name] = client
|
||||
account.Status = "work"
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver S3) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver S3) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
var files []model.File
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ = cache.([]model.File)
|
||||
} else {
|
||||
if account.InternalType == "v2" {
|
||||
files, err = driver.ListV2(path, account)
|
||||
} else {
|
||||
files, err = driver.List(path, account)
|
||||
}
|
||||
if err == nil && len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
}
|
||||
return files, err
|
||||
}
|
||||
|
||||
func (driver S3) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
client, err := driver.GetClient(account, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := driver.GetKey(args.Path, account, false)
|
||||
disposition := fmt.Sprintf(`attachment;filename="%s"`, url.QueryEscape(utils.Base(path)))
|
||||
input := &s3.GetObjectInput{
|
||||
Bucket: &account.Bucket,
|
||||
Key: &path,
|
||||
//ResponseContentDisposition: &disposition,
|
||||
}
|
||||
if account.CustomHost == "" {
|
||||
input.ResponseContentDisposition = &disposition
|
||||
}
|
||||
req, _ := client.GetObjectRequest(input)
|
||||
var link string
|
||||
if account.CustomHost != "" {
|
||||
err = req.Build()
|
||||
link = req.HTTPRequest.URL.String()
|
||||
} else {
|
||||
link, err = req.Presign(time.Hour * time.Duration(account.Limit))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{
|
||||
Url: link,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver S3) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
log.Debugf("s3 path: %s", path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
//func (driver S3) Proxy(r *http.Request, account *model.Account) {
|
||||
//
|
||||
//}
|
||||
|
||||
func (driver S3) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
return nil, base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver S3) MakeDir(path string, account *model.Account) error {
|
||||
// not support, generate a placeholder file
|
||||
_, err := driver.File(path, account)
|
||||
// exist
|
||||
if err != base.ErrPathNotFound {
|
||||
return nil
|
||||
}
|
||||
return driver.Upload(&model.FileStream{
|
||||
File: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
Size: 0,
|
||||
ParentPath: path,
|
||||
Name: getPlaceholderName(account.Zone),
|
||||
MIMEType: "application/octet-stream",
|
||||
}, account)
|
||||
}
|
||||
|
||||
func (driver S3) Move(src string, dst string, account *model.Account) error {
|
||||
err := driver.Copy(src, dst, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return driver.Delete(src, account)
|
||||
}
|
||||
|
||||
func (driver S3) Rename(src string, dst string, account *model.Account) error {
|
||||
return driver.Move(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver S3) Copy(src string, dst string, account *model.Account) error {
|
||||
client, err := driver.GetClient(account, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcFile, err := driver.File(src, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcKey := driver.GetKey(src, account, srcFile.IsDir())
|
||||
dstKey := driver.GetKey(dst, account, srcFile.IsDir())
|
||||
input := &s3.CopyObjectInput{
|
||||
Bucket: &account.Bucket,
|
||||
CopySource: &srcKey,
|
||||
Key: &dstKey,
|
||||
}
|
||||
_, err = client.CopyObject(input)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver S3) Delete(path string, account *model.Account) error {
|
||||
client, err := driver.GetClient(account, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := driver.GetKey(path, account, file.IsDir())
|
||||
input := &s3.DeleteObjectInput{
|
||||
Bucket: &account.Bucket,
|
||||
Key: &key,
|
||||
}
|
||||
_, err = client.DeleteObject(input)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver S3) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
s, ok := sessionsMap[account.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("can't find [%s] session", account.Name)
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
key := driver.GetKey(utils.Join(file.ParentPath, file.GetFileName()), account, false)
|
||||
log.Debugln("key:", key)
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: &account.Bucket,
|
||||
Key: &key,
|
||||
Body: file,
|
||||
}
|
||||
_, err := uploader.Upload(input)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*S3)(nil)
|
||||
195
drivers/s3/s3.go
Normal file
195
drivers/s3/s3.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var sessionsMap map[string]*session.Session
|
||||
|
||||
func (driver S3) NewSession(account *model.Account) (*session.Session, error) {
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(account.AccessKey, account.AccessSecret, ""),
|
||||
Region: &account.Region,
|
||||
Endpoint: &account.Endpoint,
|
||||
S3ForcePathStyle: aws.Bool(account.Bool1),
|
||||
}
|
||||
return session.NewSession(cfg)
|
||||
}
|
||||
|
||||
func (driver S3) GetClient(account *model.Account, link bool) (*s3.S3, error) {
|
||||
s, ok := sessionsMap[account.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find [%s] session", account.Name)
|
||||
}
|
||||
client := s3.New(s)
|
||||
if link && account.CustomHost != "" {
|
||||
cURL, err := url.Parse(account.CustomHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.Handlers.Build.PushBack(func(r *request.Request) {
|
||||
if r.HTTPRequest.Method != http.MethodGet {
|
||||
return
|
||||
}
|
||||
r.HTTPRequest.URL.Scheme = cURL.Scheme
|
||||
r.HTTPRequest.URL.Host = cURL.Host
|
||||
})
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (driver S3) List(prefix string, account *model.Account) ([]model.File, error) {
|
||||
prefix = driver.GetKey(prefix, account, true)
|
||||
log.Debugf("list: %s", prefix)
|
||||
client, err := driver.GetClient(account, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
marker := ""
|
||||
for {
|
||||
input := &s3.ListObjectsInput{
|
||||
Bucket: &account.Bucket,
|
||||
Marker: &marker,
|
||||
Prefix: &prefix,
|
||||
Delimiter: aws.String("/"),
|
||||
}
|
||||
listObjectsResult, err := client.ListObjects(input)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, object := range listObjectsResult.CommonPrefixes {
|
||||
name := utils.Base(strings.Trim(*object.Prefix, "/"))
|
||||
file := model.File{
|
||||
//Id: *object.Key,
|
||||
Name: name,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
TimeStr: "-",
|
||||
Type: conf.FOLDER,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
for _, object := range listObjectsResult.Contents {
|
||||
name := utils.Base(*object.Key)
|
||||
if name == getPlaceholderName(account.Zone) {
|
||||
continue
|
||||
}
|
||||
file := model.File{
|
||||
//Id: *object.Key,
|
||||
Name: name,
|
||||
Size: *object.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: object.LastModified,
|
||||
Type: utils.GetFileType(path.Ext(*object.Key)),
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
if listObjectsResult.IsTruncated == nil {
|
||||
return nil, errors.New("IsTruncated nil")
|
||||
}
|
||||
if *listObjectsResult.IsTruncated {
|
||||
marker = *listObjectsResult.NextMarker
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver S3) ListV2(prefix string, account *model.Account) ([]model.File, error) {
|
||||
prefix = driver.GetKey(prefix, account, true)
|
||||
//if prefix == "" {
|
||||
// prefix = "/"
|
||||
//}
|
||||
log.Debugf("list: %s", prefix)
|
||||
client, err := driver.GetClient(account, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
var continuationToken, startAfter *string
|
||||
for {
|
||||
input := &s3.ListObjectsV2Input{
|
||||
Bucket: &account.Bucket,
|
||||
ContinuationToken: continuationToken,
|
||||
Prefix: &prefix,
|
||||
Delimiter: aws.String("/"),
|
||||
StartAfter: startAfter,
|
||||
}
|
||||
listObjectsResult, err := client.ListObjectsV2(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("resp: %+v", listObjectsResult)
|
||||
for _, object := range listObjectsResult.CommonPrefixes {
|
||||
name := utils.Base(strings.Trim(*object.Prefix, "/"))
|
||||
file := model.File{
|
||||
//Id: *object.Key,
|
||||
Name: name,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
TimeStr: "-",
|
||||
Type: conf.FOLDER,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
for _, object := range listObjectsResult.Contents {
|
||||
name := utils.Base(*object.Key)
|
||||
if name == getPlaceholderName(account.Zone) {
|
||||
continue
|
||||
}
|
||||
file := model.File{
|
||||
//Id: *object.Key,
|
||||
Name: name,
|
||||
Size: *object.Size,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: object.LastModified,
|
||||
Type: utils.GetFileType(path.Ext(*object.Key)),
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
if !aws.BoolValue(listObjectsResult.IsTruncated) {
|
||||
break
|
||||
}
|
||||
if listObjectsResult.NextContinuationToken != nil {
|
||||
continuationToken = listObjectsResult.NextContinuationToken
|
||||
continue
|
||||
}
|
||||
if len(listObjectsResult.Contents) == 0 {
|
||||
break
|
||||
}
|
||||
startAfter = listObjectsResult.Contents[len(listObjectsResult.Contents)-1].Key
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver S3) GetKey(path string, account *model.Account, dir bool) string {
|
||||
path = utils.Join(account.RootFolder, path)
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
if path != "" && dir {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func init() {
|
||||
sessionsMap = make(map[string]*session.Session)
|
||||
base.RegisterDriver(&S3{})
|
||||
}
|
||||
10
drivers/s3/util.go
Normal file
10
drivers/s3/util.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package s3
|
||||
|
||||
var defaultPlaceholderName = ".placeholder"
|
||||
|
||||
func getPlaceholderName(placeholder string) string {
|
||||
if placeholder == "" {
|
||||
return defaultPlaceholderName
|
||||
}
|
||||
return placeholder
|
||||
}
|
||||
220
drivers/sftp/driver.go
Normal file
220
drivers/sftp/driver.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"io"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type SFTP struct {
|
||||
}
|
||||
|
||||
func (driver SFTP) Config() base.DriverConfig {
|
||||
return base.DriverConfig{
|
||||
Name: "SFTP",
|
||||
OnlyProxy: true,
|
||||
OnlyLocal: true,
|
||||
LocalSort: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver SFTP) Items() []base.Item {
|
||||
// TODO fill need info
|
||||
return []base.Item{
|
||||
{
|
||||
Name: "site_url",
|
||||
Label: "ip/host",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "limit",
|
||||
Label: "port",
|
||||
Type: base.TypeNumber,
|
||||
Required: true,
|
||||
Default: "22",
|
||||
},
|
||||
{
|
||||
Name: "username",
|
||||
Label: "username",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "password",
|
||||
Type: base.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "root_folder",
|
||||
Label: "root folder path",
|
||||
Type: base.TypeString,
|
||||
Default: "/",
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (driver SFTP) Save(account *model.Account, old *model.Account) error {
|
||||
if old != nil {
|
||||
clientsMap.Lock()
|
||||
defer clientsMap.Unlock()
|
||||
delete(clientsMap.clients, old.Name)
|
||||
}
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := GetClient(account)
|
||||
if err != nil {
|
||||
account.Status = err.Error()
|
||||
} else {
|
||||
account.Status = "work"
|
||||
}
|
||||
_ = model.SaveAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (driver SFTP) File(path string, account *model.Account) (*model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
if path == "/" {
|
||||
return &model.File{
|
||||
Id: account.RootFolder,
|
||||
Name: account.Name,
|
||||
Size: 0,
|
||||
Type: conf.FOLDER,
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
dir, name := filepath.Split(path)
|
||||
files, err := driver.Files(dir, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Name == name {
|
||||
return &file, nil
|
||||
}
|
||||
}
|
||||
return nil, base.ErrPathNotFound
|
||||
}
|
||||
|
||||
func (driver SFTP) Files(path string, account *model.Account) ([]model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
remotePath := utils.Join(account.RootFolder, path)
|
||||
cache, err := base.GetCache(path, account)
|
||||
if err == nil {
|
||||
files, _ := cache.([]model.File)
|
||||
return files, nil
|
||||
}
|
||||
client, err := GetClient(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make([]model.File, 0)
|
||||
rawFiles, err := client.Files(remotePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < len(rawFiles); i++ {
|
||||
files = append(files, driver.formatFile(rawFiles[i]))
|
||||
}
|
||||
if len(files) > 0 {
|
||||
_ = base.SetCache(path, files, account)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (driver SFTP) Link(args base.Args, account *model.Account) (*base.Link, error) {
|
||||
client, err := GetClient(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteFileName := utils.Join(account.RootFolder, args.Path)
|
||||
remoteFile, err := client.Open(remoteFileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &base.Link{
|
||||
Data: remoteFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (driver SFTP) Path(path string, account *model.Account) (*model.File, []model.File, error) {
|
||||
path = utils.ParsePath(path)
|
||||
file, err := driver.File(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !file.IsDir() {
|
||||
return file, nil, nil
|
||||
}
|
||||
files, err := driver.Files(path, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, files, nil
|
||||
}
|
||||
|
||||
func (driver SFTP) Preview(path string, account *model.Account) (interface{}, error) {
|
||||
//TODO preview interface if driver support
|
||||
return nil, base.ErrNotImplement
|
||||
}
|
||||
|
||||
func (driver SFTP) MakeDir(path string, account *model.Account) error {
|
||||
client, err := GetClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.MkdirAll(utils.Join(account.RootFolder, path))
|
||||
}
|
||||
|
||||
func (driver SFTP) Move(src string, dst string, account *model.Account) error {
|
||||
return driver.Rename(src, dst, account)
|
||||
}
|
||||
|
||||
func (driver SFTP) Rename(src string, dst string, account *model.Account) error {
|
||||
client, err := GetClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Rename(utils.Join(account.RootFolder, src), utils.Join(account.RootFolder, dst))
|
||||
}
|
||||
|
||||
func (driver SFTP) Copy(src string, dst string, account *model.Account) error {
|
||||
return base.ErrNotSupport
|
||||
}
|
||||
|
||||
func (driver SFTP) Delete(path string, account *model.Account) error {
|
||||
client, err := GetClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.remove(utils.Join(account.RootFolder, path))
|
||||
}
|
||||
|
||||
func (driver SFTP) Upload(file *model.FileStream, account *model.Account) error {
|
||||
if file == nil {
|
||||
return base.ErrEmptyFile
|
||||
}
|
||||
client, err := GetClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstFile, err := client.Create(path.Join(account.RootFolder, file.ParentPath, file.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = dstFile.Close()
|
||||
}()
|
||||
_, err = io.Copy(dstFile, file)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ base.Driver = (*SFTP)(nil)
|
||||
110
drivers/sftp/sftp.go
Normal file
110
drivers/sftp/sftp.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Xhofe/alist/conf"
|
||||
"github.com/Xhofe/alist/drivers/base"
|
||||
"github.com/Xhofe/alist/model"
|
||||
"github.com/Xhofe/alist/utils"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var clientsMap = struct {
|
||||
sync.Mutex
|
||||
clients map[string]*Client
|
||||
}{clients: make(map[string]*Client)}
|
||||
|
||||
func GetClient(account *model.Account) (*Client, error) {
|
||||
clientsMap.Lock()
|
||||
defer clientsMap.Unlock()
|
||||
if v, ok := clientsMap.clients[account.Name]; ok {
|
||||
return v, nil
|
||||
}
|
||||
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", account.SiteUrl, account.Limit), &ssh.ClientConfig{
|
||||
User: account.Username,
|
||||
Auth: []ssh.AuthMethod{ssh.Password(account.Password)},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := sftp.NewClient(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &Client{client}
|
||||
clientsMap.clients[account.Name] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
*sftp.Client
|
||||
}
|
||||
|
||||
func (client *Client) Files(remotePath string) ([]os.FileInfo, error) {
|
||||
return client.ReadDir(remotePath)
|
||||
}
|
||||
|
||||
func (client *Client) remove(remotePath string) error {
|
||||
f, err := client.Stat(remotePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if f.IsDir() {
|
||||
return client.removeDirectory(remotePath)
|
||||
} else {
|
||||
return client.removeFile(remotePath)
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) removeDirectory(remotePath string) error {
|
||||
//打不开,说明要么文件路径错误了,要么是第一次部署
|
||||
remoteFiles, err := client.ReadDir(remotePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, backupDir := range remoteFiles {
|
||||
remoteFilePath := path.Join(remotePath, backupDir.Name())
|
||||
if backupDir.IsDir() {
|
||||
err := client.removeDirectory(remoteFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := client.Remove(path.Join(remoteFilePath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return client.RemoveDirectory(remotePath)
|
||||
}
|
||||
|
||||
func (client *Client) removeFile(remotePath string) error {
|
||||
return client.Remove(utils.Join(remotePath))
|
||||
}
|
||||
|
||||
func (driver SFTP) formatFile(f os.FileInfo) model.File {
|
||||
t := f.ModTime()
|
||||
file := model.File{
|
||||
//Id: f.Id,
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
Driver: driver.Config().Name,
|
||||
UpdatedAt: &t,
|
||||
}
|
||||
if f.IsDir() {
|
||||
file.Type = conf.FOLDER
|
||||
} else {
|
||||
file.Type = utils.GetFileType(path.Ext(f.Name()))
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
func init() {
|
||||
base.RegisterDriver(&SFTP{})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user