mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-09-20 20:56:20 +08:00
Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
3936e736e6 | |||
68433d4f5b | |||
cc16cb35bf | |||
d3bc6321f4 | |||
cbbb5ad231 | |||
c1d03c5bcc | |||
61a8ed515f | |||
bbb7c06504 | |||
8bbdb272d4 | |||
c15ae94307 | |||
f1a5048558 | |||
1fe26bff9a | |||
433dcd156b | |||
e97f0a289e | |||
89f35170b3 | |||
8188fb2d7d | |||
87cf95f50b | |||
8ab26cb823 | |||
5880c8e1af | |||
14bf4ecb4c | |||
04a5e58781 | |||
bbd4389345 | |||
f350ccdf95 | |||
4f2de9395e | |||
b0dbbebfb0 | |||
0c27b4bd47 | |||
736cd9e5f2 | |||
c7a603c926 | |||
a28d6d5693 | |||
e59d2233e2 | |||
01914a06ef | |||
6499374d1c | |||
b054919d5c | |||
048ee9c2e5 | |||
23394548ca | |||
b04677b806 | |||
e4c902dd93 | |||
5d8bd258c0 | |||
08c5283c8c | |||
10a14f10cd | |||
f86ebc52a0 | |||
016ed90efa | |||
d76407b201 | |||
5de6b660f2 | |||
71ada3b656 | |||
dc42f0e226 | |||
74bf9f6467 |
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<!--
|
||||||
|
Provide a general summary of your changes in the Title above.
|
||||||
|
The PR title must start with `feat(): `, `docs(): `, `fix(): `, `style(): `, or `refactor(): `, `chore(): `. For example: `feat(component): add new feature`.
|
||||||
|
If it spans multiple components, use the main component as the prefix and enumerate in the title, describe in the body.
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
在上方标题中提供您更改的总体摘要。
|
||||||
|
PR 标题需以 `feat(): `, `docs(): `, `fix(): `, `style(): `, `refactor(): `, `chore(): ` 其中之一开头,例如:`feat(component): 新增功能`。
|
||||||
|
如果跨多个组件,请使用主要组件作为前缀,并在标题中枚举、描述中说明。
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Description / 描述
|
||||||
|
|
||||||
|
<!-- Describe your changes in detail -->
|
||||||
|
<!-- 详细描述您的更改 -->
|
||||||
|
|
||||||
|
## Motivation and Context / 背景
|
||||||
|
|
||||||
|
<!-- Why is this change required? What problem does it solve? -->
|
||||||
|
<!-- 为什么需要此更改?它解决了什么问题? -->
|
||||||
|
|
||||||
|
<!-- If it fixes an open issue, please link to the issue here. -->
|
||||||
|
<!-- 如果修复了一个打开的issue,请在此处链接到该issue -->
|
||||||
|
|
||||||
|
Closes #XXXX
|
||||||
|
|
||||||
|
<!-- or -->
|
||||||
|
<!-- 或者 -->
|
||||||
|
|
||||||
|
Relates to #XXXX
|
||||||
|
|
||||||
|
## How Has This Been Tested? / 测试
|
||||||
|
|
||||||
|
<!-- Please describe in detail how you tested your changes. -->
|
||||||
|
<!-- 请详细描述您如何测试更改 -->
|
||||||
|
|
||||||
|
## Checklist / 检查清单
|
||||||
|
|
||||||
|
<!-- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||||
|
<!-- 检查以下所有要点,并在所有适用的框中打`x` -->
|
||||||
|
|
||||||
|
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||||
|
<!-- 如果您对其中任何一项不确定,请不要犹豫提问。我们会帮助您! -->
|
||||||
|
|
||||||
|
- [ ] I have read the [CONTRIBUTING](https://github.com/OpenListTeam/OpenList/blob/main/CONTRIBUTING.md) document.
|
||||||
|
我已阅读 [CONTRIBUTING](https://github.com/OpenListTeam/OpenList/blob/main/CONTRIBUTING.md) 文档。
|
||||||
|
- [ ] I have formatted my code with `go fmt` or [prettier](https://prettier.io/).
|
||||||
|
我已使用 `go fmt` 或 [prettier](https://prettier.io/) 格式化提交的代码。
|
||||||
|
- [ ] I have added appropriate labels to this PR (or mentioned needed labels in the description if lacking permissions).
|
||||||
|
我已为此 PR 添加了适当的标签(如无权限或需要的标签不存在,请在描述中说明,管理员将后续处理)。
|
||||||
|
- [ ] I have requested review from relevant code authors using the "Request review" feature when applicable.
|
||||||
|
我已在适当情况下使用"Request review"功能请求相关代码作者进行审查。
|
||||||
|
- [ ] I have updated the repository accordingly (If it’s needed).
|
||||||
|
我已相应更新了相关仓库(若适用)。
|
||||||
|
- [ ] [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend) #XXXX
|
||||||
|
- [ ] [OpenList-Docs](https://github.com/OpenListTeam/OpenList-Docs) #XXXX
|
@ -87,7 +87,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.5"
|
go-version: "1.25.0"
|
||||||
|
|
||||||
- name: Setup web
|
- name: Setup web
|
||||||
run: bash build.sh dev web
|
run: bash build.sh dev web
|
@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.5"
|
go-version: "1.25.0"
|
||||||
|
|
||||||
- name: Setup web
|
- name: Setup web
|
||||||
run: bash build.sh dev web
|
run: bash build.sh dev web
|
@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version: '1.25.0'
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@ -73,4 +73,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: build/compress/*
|
files: build/compress/*
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
tag_name: ${{ github.event.release.tag_name }}
|
||||||
|
|
@ -47,7 +47,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: '1.25.0'
|
||||||
|
|
||||||
- name: Cache Musl
|
- name: Cache Musl
|
||||||
id: cache-musl
|
id: cache-musl
|
||||||
@ -87,7 +87,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: '1.25.0'
|
||||||
|
|
||||||
- name: Cache Musl
|
- name: Cache Musl
|
||||||
id: cache-musl
|
id: cache-musl
|
38
.github/workflows/sync_repo.yml
vendored
Normal file
38
.github/workflows/sync_repo.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
name: Sync to Gitee
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Sync GitHub to Gitee
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.GITEE_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
ssh-keyscan gitee.com >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Create single commit and push
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
|
||||||
|
# Create a new branch
|
||||||
|
git checkout --orphan new-main
|
||||||
|
git add .
|
||||||
|
git commit -m "Sync from GitHub: $(date)"
|
||||||
|
|
||||||
|
# Add Gitee remote and force push
|
||||||
|
git remote add gitee ${{ vars.GITEE_REPO_URL }}
|
||||||
|
git push --force gitee new-main:main
|
@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: '1.25.0'
|
||||||
|
|
||||||
- name: Cache Musl
|
- name: Cache Musl
|
||||||
id: cache-musl
|
id: cache-musl
|
77
CONTRIBUTING.md
Normal file
77
CONTRIBUTING.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
## Setup your machine
|
||||||
|
|
||||||
|
`OpenList` is written in [Go](https://golang.org/) and [SolidJS](https://www.solidjs.com/).
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- [git](https://git-scm.com)
|
||||||
|
- [Go 1.24+](https://golang.org/doc/install)
|
||||||
|
- [gcc](https://gcc.gnu.org/)
|
||||||
|
- [nodejs](https://nodejs.org/)
|
||||||
|
|
||||||
|
## Cloning a fork
|
||||||
|
|
||||||
|
Fork and clone `OpenList` and `OpenList-Frontend` anywhere:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ git clone https://github.com/<your-username>/OpenList.git
|
||||||
|
$ git clone --recurse-submodules https://github.com/<your-username>/OpenList-Frontend.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a branch
|
||||||
|
|
||||||
|
Create a new branch from the `main` branch, with an appropriate name.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ git checkout -b <branch-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preview your change
|
||||||
|
|
||||||
|
### backend
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### frontend
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ pnpm 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".
|
||||||
|
|
||||||
|
Submit your pull request. For PR titles, follow [Conventional Commits](https://www.conventionalcommits.org).
|
||||||
|
|
||||||
|
https://github.com/OpenListTeam/OpenList/issues/376
|
||||||
|
|
||||||
|
It's suggested to sign your commits. See: [How to sign commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
|
||||||
|
|
||||||
|
## Submit a pull request
|
||||||
|
|
||||||
|
Please make sure your code has been formatted with `go fmt` or [prettier](https://prettier.io/) before submitting.
|
||||||
|
|
||||||
|
Push your branch to your `openlist` fork and open a pull request against the `main` branch.
|
||||||
|
|
||||||
|
## Merge your pull request
|
||||||
|
|
||||||
|
Your pull request will be merged after review. Please wait for the maintainer to merge your pull request after review.
|
||||||
|
|
||||||
|
At least 1 approving review is required by reviewers with write access. You can also request a review from maintainers.
|
||||||
|
|
||||||
|
## Delete your branch
|
||||||
|
|
||||||
|
(Optional) After your pull request is merged, you can delete your branch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thank you for your contribution! Let's make OpenList better together!
|
@ -20,11 +20,12 @@ ARG GID=1001
|
|||||||
|
|
||||||
WORKDIR /opt/openlist/
|
WORKDIR /opt/openlist/
|
||||||
|
|
||||||
COPY --chmod=755 --from=builder /app/bin/openlist ./
|
RUN addgroup -g ${GID} ${USER} && \
|
||||||
COPY --chmod=755 entrypoint.sh /entrypoint.sh
|
adduser -D -u ${UID} -G ${USER} ${USER} && \
|
||||||
RUN adduser -u ${UID} -g ${GID} -h /opt/openlist/data -D -s /bin/sh ${USER} \
|
mkdir -p /opt/openlist/data
|
||||||
&& chown -R ${UID}:${GID} /opt \
|
|
||||||
&& chown -R ${UID}:${GID} /entrypoint.sh
|
COPY --from=builder --chmod=755 --chown=${UID}:${GID} /app/bin/openlist ./
|
||||||
|
COPY --chmod=755 --chown=${UID}:${GID} entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
USER ${USER}
|
USER ${USER}
|
||||||
RUN /entrypoint.sh version
|
RUN /entrypoint.sh version
|
@ -10,12 +10,12 @@ ARG GID=1001
|
|||||||
|
|
||||||
WORKDIR /opt/openlist/
|
WORKDIR /opt/openlist/
|
||||||
|
|
||||||
COPY --chmod=755 /build/${TARGETPLATFORM}/openlist ./
|
RUN addgroup -g ${GID} ${USER} && \
|
||||||
COPY --chmod=755 entrypoint.sh /entrypoint.sh
|
adduser -D -u ${UID} -G ${USER} ${USER} && \
|
||||||
|
mkdir -p /opt/openlist/data
|
||||||
|
|
||||||
RUN adduser -u ${UID} -g ${GID} -h /opt/openlist/data -D -s /bin/sh ${USER} \
|
COPY --chmod=755 --chown=${UID}:${GID} /build/${TARGETPLATFORM}/openlist ./
|
||||||
&& chown -R ${UID}:${GID} /opt \
|
COPY --chmod=755 --chown=${UID}:${GID} entrypoint.sh /entrypoint.sh
|
||||||
&& chown -R ${UID}:${GID} /entrypoint.sh
|
|
||||||
|
|
||||||
USER ${USER}
|
USER ${USER}
|
||||||
RUN /entrypoint.sh version
|
RUN /entrypoint.sh version
|
@ -74,7 +74,6 @@ Thank you for your support and understanding of the OpenList project.
|
|||||||
- [x] [Thunder](https://pan.xunlei.com)
|
- [x] [Thunder](https://pan.xunlei.com)
|
||||||
- [x] [Lanzou](https://www.lanzou.com)
|
- [x] [Lanzou](https://www.lanzou.com)
|
||||||
- [x] [ILanzou](https://www.ilanzou.com)
|
- [x] [ILanzou](https://www.ilanzou.com)
|
||||||
- [x] [Aliyundrive share](https://www.alipan.com)
|
|
||||||
- [x] [Google photo](https://photos.google.com)
|
- [x] [Google photo](https://photos.google.com)
|
||||||
- [x] [Mega.nz](https://mega.nz)
|
- [x] [Mega.nz](https://mega.nz)
|
||||||
- [x] [Baidu photo](https://photo.baidu.com)
|
- [x] [Baidu photo](https://photo.baidu.com)
|
||||||
@ -85,6 +84,16 @@ Thank you for your support and understanding of the OpenList project.
|
|||||||
- [x] [FeijiPan](https://www.feijipan.com)
|
- [x] [FeijiPan](https://www.feijipan.com)
|
||||||
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
||||||
- [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)
|
- [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)
|
||||||
|
- [x] [Chaoxing](https://www.chaoxing.com)
|
||||||
|
- [x] [CNB](https://cnb.cool/)
|
||||||
|
- [x] [Degoo](https://degoo.com)
|
||||||
|
- [x] [Doubao](https://www.doubao.com)
|
||||||
|
- [x] [Febbox](https://www.febbox.com)
|
||||||
|
- [x] [GitHub](https://github.com)
|
||||||
|
- [x] [OpenList](https://github.com/OpenListTeam/OpenList)
|
||||||
|
- [x] [Teldrive](https://github.com/tgdrive/teldrive)
|
||||||
|
- [x] [Weiyun](https://www.weiyun.com)
|
||||||
|
|
||||||
- [x] Easy to deploy and out-of-the-box
|
- [x] Easy to deploy and out-of-the-box
|
||||||
- [x] File preview (PDF, markdown, code, plain text, ...)
|
- [x] File preview (PDF, markdown, code, plain text, ...)
|
||||||
- [x] Image preview in gallery mode
|
- [x] Image preview in gallery mode
|
@ -74,7 +74,6 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3
|
|||||||
- [x] [迅雷网盘](https://pan.xunlei.com)
|
- [x] [迅雷网盘](https://pan.xunlei.com)
|
||||||
- [x] [蓝奏云](https://www.lanzou.com)
|
- [x] [蓝奏云](https://www.lanzou.com)
|
||||||
- [x] [蓝奏云优享版](https://www.ilanzou.com)
|
- [x] [蓝奏云优享版](https://www.ilanzou.com)
|
||||||
- [x] [阿里云盘分享](https://www.alipan.com)
|
|
||||||
- [x] [Google 相册](https://photos.google.com)
|
- [x] [Google 相册](https://photos.google.com)
|
||||||
- [x] [Mega.nz](https://mega.nz)
|
- [x] [Mega.nz](https://mega.nz)
|
||||||
- [x] [百度相册](https://photo.baidu.com)
|
- [x] [百度相册](https://photo.baidu.com)
|
||||||
@ -85,6 +84,15 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3
|
|||||||
- [x] [飞机盘](https://www.feijipan.com)
|
- [x] [飞机盘](https://www.feijipan.com)
|
||||||
- [x] [多吉云](https://www.dogecloud.com/product/oss)
|
- [x] [多吉云](https://www.dogecloud.com/product/oss)
|
||||||
- [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)
|
- [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)
|
||||||
|
- [x] [超星](https://www.chaoxing.com)
|
||||||
|
- [x] [CNB](https://cnb.cool/)
|
||||||
|
- [x] [Degoo](https://degoo.com)
|
||||||
|
- [x] [豆包](https://www.doubao.com)
|
||||||
|
- [x] [Febbox](https://www.febbox.com)
|
||||||
|
- [x] [GitHub](https://github.com)
|
||||||
|
- [x] [OpenList](https://github.com/OpenListTeam/OpenList)
|
||||||
|
- [x] [Teldrive](https://github.com/tgdrive/teldrive)
|
||||||
|
- [x] [微云](https://www.weiyun.com)
|
||||||
- [x] 部署方便,开箱即用
|
- [x] 部署方便,开箱即用
|
||||||
- [x] 文件预览(PDF、markdown、代码、纯文本等)
|
- [x] 文件预览(PDF、markdown、代码、纯文本等)
|
||||||
- [x] 画廊模式下的图片预览
|
- [x] 画廊模式下的图片预览
|
@ -74,7 +74,6 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい
|
|||||||
- [x] [Thunder](https://pan.xunlei.com)
|
- [x] [Thunder](https://pan.xunlei.com)
|
||||||
- [x] [Lanzou](https://www.lanzou.com)
|
- [x] [Lanzou](https://www.lanzou.com)
|
||||||
- [x] [ILanzou](https://www.ilanzou.com)
|
- [x] [ILanzou](https://www.ilanzou.com)
|
||||||
- [x] [Aliyundrive share](https://www.alipan.com)
|
|
||||||
- [x] [Google photo](https://photos.google.com)
|
- [x] [Google photo](https://photos.google.com)
|
||||||
- [x] [Mega.nz](https://mega.nz)
|
- [x] [Mega.nz](https://mega.nz)
|
||||||
- [x] [Baidu photo](https://photo.baidu.com)
|
- [x] [Baidu photo](https://photo.baidu.com)
|
||||||
@ -85,6 +84,15 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい
|
|||||||
- [x] [FeijiPan](https://www.feijipan.com)
|
- [x] [FeijiPan](https://www.feijipan.com)
|
||||||
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
||||||
- [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)
|
- [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)
|
||||||
|
- [x] [Chaoxing](https://www.chaoxing.com)
|
||||||
|
- [x] [CNB](https://cnb.cool/)
|
||||||
|
- [x] [Degoo](https://degoo.com)
|
||||||
|
- [x] [Doubao](https://www.doubao.com)
|
||||||
|
- [x] [Febbox](https://www.febbox.com)
|
||||||
|
- [x] [GitHub](https://github.com)
|
||||||
|
- [x] [OpenList](https://github.com/OpenListTeam/OpenList)
|
||||||
|
- [x] [Teldrive](https://github.com/tgdrive/teldrive)
|
||||||
|
- [x] [Weiyun](https://www.weiyun.com)
|
||||||
- [x] 簡単にデプロイでき、すぐに使える
|
- [x] 簡単にデプロイでき、すぐに使える
|
||||||
- [x] ファイルプレビュー(PDF、markdown、コード、テキストなど)
|
- [x] ファイルプレビュー(PDF、markdown、コード、テキストなど)
|
||||||
- [x] ギャラリーモードでの画像プレビュー
|
- [x] ギャラリーモードでの画像プレビュー
|
@ -74,7 +74,6 @@ Dank u voor uw ondersteuning en begrip
|
|||||||
- [x] [Thunder](https://pan.xunlei.com)
|
- [x] [Thunder](https://pan.xunlei.com)
|
||||||
- [x] [Lanzou](https://www.lanzou.com)
|
- [x] [Lanzou](https://www.lanzou.com)
|
||||||
- [x] [ILanzou](https://www.ilanzou.com)
|
- [x] [ILanzou](https://www.ilanzou.com)
|
||||||
- [x] [Aliyundrive share](https://www.alipan.com)
|
|
||||||
- [x] [Google photo](https://photos.google.com)
|
- [x] [Google photo](https://photos.google.com)
|
||||||
- [x] [Mega.nz](https://mega.nz)
|
- [x] [Mega.nz](https://mega.nz)
|
||||||
- [x] [Baidu photo](https://photo.baidu.com)
|
- [x] [Baidu photo](https://photo.baidu.com)
|
||||||
@ -85,6 +84,15 @@ Dank u voor uw ondersteuning en begrip
|
|||||||
- [x] [FeijiPan](https://www.feijipan.com)
|
- [x] [FeijiPan](https://www.feijipan.com)
|
||||||
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
||||||
- [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)
|
- [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)
|
||||||
|
- [x] [Chaoxing](https://www.chaoxing.com)
|
||||||
|
- [x] [CNB](https://cnb.cool/)
|
||||||
|
- [x] [Degoo](https://degoo.com)
|
||||||
|
- [x] [Doubao](https://www.doubao.com)
|
||||||
|
- [x] [Febbox](https://www.febbox.com)
|
||||||
|
- [x] [GitHub](https://github.com)
|
||||||
|
- [x] [OpenList](https://github.com/OpenListTeam/OpenList)
|
||||||
|
- [x] [Teldrive](https://github.com/tgdrive/teldrive)
|
||||||
|
- [x] [Weiyun](https://www.weiyun.com)
|
||||||
- [x] Eenvoudig te implementeren en direct te gebruiken
|
- [x] Eenvoudig te implementeren en direct te gebruiken
|
||||||
- [x] Bestandsvoorbeeld (PDF, markdown, code, platte tekst, ...)
|
- [x] Bestandsvoorbeeld (PDF, markdown, code, platte tekst, ...)
|
||||||
- [x] Afbeeldingsvoorbeeld in galerijweergave
|
- [x] Afbeeldingsvoorbeeld in galerijweergave
|
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
|
|
@ -236,7 +236,7 @@ BuildRelease() {
|
|||||||
BuildLoongGLIBC() {
|
BuildLoongGLIBC() {
|
||||||
local target_abi="$2"
|
local target_abi="$2"
|
||||||
local output_file="$1"
|
local output_file="$1"
|
||||||
local oldWorldGoVersion="1.24.3"
|
local oldWorldGoVersion="1.25.0"
|
||||||
|
|
||||||
if [ "$target_abi" = "abi1.0" ]; then
|
if [ "$target_abi" = "abi1.0" ]; then
|
||||||
echo building for linux-loong64-abi1.0
|
echo building for linux-loong64-abi1.0
|
||||||
@ -254,13 +254,13 @@ BuildLoongGLIBC() {
|
|||||||
|
|
||||||
# Download and setup patched Go compiler for old-world
|
# Download and setup patched Go compiler for old-world
|
||||||
if ! curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
|
if ! curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250722/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
|
"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250821/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
|
||||||
-o go-loong64-abi1.0.tar.gz; then
|
-o go-loong64-abi1.0.tar.gz; then
|
||||||
echo "Error: Failed to download patched Go compiler for old-world ABI1.0"
|
echo "Error: Failed to download patched Go compiler for old-world ABI1.0"
|
||||||
if [ -n "$GITHUB_TOKEN" ]; then
|
if [ -n "$GITHUB_TOKEN" ]; then
|
||||||
echo "Error output from curl:"
|
echo "Error output from curl:"
|
||||||
curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
|
curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250722/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
|
"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250821/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
|
||||||
-o go-loong64-abi1.0.tar.gz || true
|
-o go-loong64-abi1.0.tar.gz || true
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
@ -1,42 +1,51 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
|
||||||
"github.com/OpenListTeam/OpenList/v5/internal/bootstrap"
|
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/data"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/OpenListTeam/OpenList/v4/internal/db"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(ctx context.Context) {
|
func Init() {
|
||||||
if flags.Dev {
|
|
||||||
flags.Debug = true
|
|
||||||
}
|
|
||||||
initLogrus()
|
|
||||||
bootstrap.InitConfig()
|
bootstrap.InitConfig()
|
||||||
bootstrap.InitDriverPlugins()
|
bootstrap.Log()
|
||||||
|
bootstrap.InitDB()
|
||||||
|
data.InitData()
|
||||||
|
bootstrap.InitStreamLimit()
|
||||||
|
bootstrap.InitIndex()
|
||||||
|
bootstrap.InitUpgradePatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Release() {
|
func Release() {
|
||||||
|
db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initLog(l *logrus.Logger) {
|
var pid = -1
|
||||||
if flags.Debug {
|
var pidFile string
|
||||||
l.SetLevel(logrus.DebugLevel)
|
|
||||||
l.SetReportCaller(true)
|
func initDaemon() {
|
||||||
} else {
|
ex, err := os.Executable()
|
||||||
l.SetLevel(logrus.InfoLevel)
|
if err != nil {
|
||||||
l.SetReportCaller(false)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
exPath := filepath.Dir(ex)
|
||||||
|
_ = os.MkdirAll(filepath.Join(exPath, "daemon"), 0700)
|
||||||
|
pidFile = filepath.Join(exPath, "daemon/pid")
|
||||||
|
if utils.Exists(pidFile) {
|
||||||
|
bytes, err := os.ReadFile(pidFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to read pid file", err)
|
||||||
|
}
|
||||||
|
id, err := strconv.Atoi(string(bytes))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to parse pid data", err)
|
||||||
|
}
|
||||||
|
pid = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func initLogrus() {
|
|
||||||
formatter := logrus.TextFormatter{
|
|
||||||
ForceColors: true,
|
|
||||||
EnvironmentOverrideColors: true,
|
|
||||||
TimestampFormat: "2006-01-02 15:04:05",
|
|
||||||
FullTimestamp: true,
|
|
||||||
}
|
|
||||||
logrus.SetFormatter(&formatter)
|
|
||||||
initLog(logrus.StandardLogger())
|
|
||||||
}
|
|
||||||
|
@ -1,40 +1,10 @@
|
|||||||
package flags
|
package flags
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ConfigFile string
|
DataDir string
|
||||||
Debug bool
|
Debug bool
|
||||||
NoPrefix bool
|
NoPrefix bool
|
||||||
Dev bool
|
Dev bool
|
||||||
ForceBinDir bool
|
ForceBinDir bool
|
||||||
LogStd bool
|
LogStd bool
|
||||||
|
|
||||||
pwd string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Program working directory
|
|
||||||
func PWD() string {
|
|
||||||
if pwd != "" {
|
|
||||||
return pwd
|
|
||||||
}
|
|
||||||
if ForceBinDir {
|
|
||||||
ex, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
pwd = filepath.Dir(ex)
|
|
||||||
return pwd
|
|
||||||
}
|
|
||||||
d, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
pwd = d
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
11
cmd/root.go
11
cmd/root.go
@ -4,7 +4,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
"github.com/OpenListTeam/OpenList/v4/cmd/flags"
|
||||||
|
_ "github.com/OpenListTeam/OpenList/v4/drivers"
|
||||||
|
_ "github.com/OpenListTeam/OpenList/v4/internal/archive"
|
||||||
|
_ "github.com/OpenListTeam/OpenList/v4/internal/offline_download"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,10 +27,10 @@ func Execute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.PersistentFlags().StringVarP(&flags.ConfigFile, "config", "c", "data/config.json", "config file")
|
RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "data folder")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
|
RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
|
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
|
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
|
||||||
RootCmd.PersistentFlags().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.ForceBinDir, "force-bin-dir", false, "Force to use the directory where the binary file is located as data directory")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "force to log to std")
|
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "Force to log to std")
|
||||||
}
|
}
|
||||||
|
199
cmd/server.go
199
cmd/server.go
@ -13,9 +13,15 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
"github.com/OpenListTeam/OpenList/v4/cmd/flags"
|
||||||
"github.com/OpenListTeam/OpenList/v5/internal/conf"
|
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
|
||||||
"github.com/OpenListTeam/OpenList/v5/server"
|
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/fs"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/server"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/server/middlewares"
|
||||||
|
"github.com/OpenListTeam/sftpd-openlist"
|
||||||
|
ftpserver "github.com/fclairamb/ftpserverlib"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -29,127 +35,220 @@ var ServerCmd = &cobra.Command{
|
|||||||
Short: "Start the server at the specified address",
|
Short: "Start the server at the specified address",
|
||||||
Long: `Start the server at the specified address
|
Long: `Start the server at the specified address
|
||||||
the address is defined in config file`,
|
the address is defined in config file`,
|
||||||
Run: func(_ *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
serverCtx, serverCancel := context.WithCancel(context.Background())
|
Init()
|
||||||
defer serverCancel()
|
if conf.Conf.DelayedStart != 0 {
|
||||||
Init(serverCtx)
|
utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
|
||||||
|
time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
|
||||||
if !flags.Debug {
|
}
|
||||||
|
bootstrap.InitOfflineDownloadTools()
|
||||||
|
bootstrap.LoadStorages()
|
||||||
|
bootstrap.InitTaskManager()
|
||||||
|
if !flags.Debug && !flags.Dev {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out))
|
|
||||||
r.Use(gin.RecoveryWithWriter(log.StandardLogger().Out))
|
|
||||||
server.Init(r)
|
|
||||||
|
|
||||||
|
// gin log
|
||||||
|
if conf.Conf.Log.Filter.Enable {
|
||||||
|
r.Use(middlewares.FilteredLogger())
|
||||||
|
} else {
|
||||||
|
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out))
|
||||||
|
}
|
||||||
|
r.Use(gin.RecoveryWithWriter(log.StandardLogger().Out))
|
||||||
|
|
||||||
|
server.Init(r)
|
||||||
var httpHandler http.Handler = r
|
var httpHandler http.Handler = r
|
||||||
if conf.Conf.Scheme.EnableH2c {
|
if conf.Conf.Scheme.EnableH2c {
|
||||||
httpHandler = h2c.NewHandler(r, &http2.Server{})
|
httpHandler = h2c.NewHandler(r, &http2.Server{})
|
||||||
}
|
}
|
||||||
var httpSrv, httpsSrv, unixSrv *http.Server
|
var httpSrv, httpsSrv, unixSrv *http.Server
|
||||||
if conf.Conf.Scheme.HttpPort > 0 {
|
if conf.Conf.Scheme.HttpPort != -1 {
|
||||||
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
|
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
|
||||||
log.Infoln("start HTTP server", "@", httpBase)
|
fmt.Printf("start HTTP server @ %s\n", httpBase)
|
||||||
|
utils.Log.Infof("start HTTP server @ %s", httpBase)
|
||||||
httpSrv = &http.Server{Addr: httpBase, Handler: httpHandler}
|
httpSrv = &http.Server{Addr: httpBase, Handler: httpHandler}
|
||||||
go func() {
|
go func() {
|
||||||
err := httpSrv.ListenAndServe()
|
err := httpSrv.ListenAndServe()
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Errorln("start HTTP server", ":", err)
|
utils.Log.Fatalf("failed to start http: %s", err.Error())
|
||||||
serverCancel()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if conf.Conf.Scheme.HttpsPort > 0 {
|
if conf.Conf.Scheme.HttpsPort != -1 {
|
||||||
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
|
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
|
||||||
log.Infoln("start HTTPS server", "@", httpsBase)
|
fmt.Printf("start HTTPS server @ %s\n", httpsBase)
|
||||||
|
utils.Log.Infof("start HTTPS server @ %s", httpsBase)
|
||||||
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
|
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
|
||||||
go func() {
|
go func() {
|
||||||
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Errorln("start HTTPS server", ":", err)
|
utils.Log.Fatalf("failed to start https: %s", err.Error())
|
||||||
serverCancel()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if conf.Conf.Scheme.UnixFile != "" {
|
if conf.Conf.Scheme.UnixFile != "" {
|
||||||
log.Infoln("start Unix server", "@", conf.Conf.Scheme.UnixFile)
|
fmt.Printf("start unix server @ %s\n", conf.Conf.Scheme.UnixFile)
|
||||||
|
utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile)
|
||||||
unixSrv = &http.Server{Handler: httpHandler}
|
unixSrv = &http.Server{Handler: httpHandler}
|
||||||
go func() {
|
go func() {
|
||||||
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
|
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("start Unix server", ":", err)
|
utils.Log.Fatalf("failed to listen unix: %+v", err)
|
||||||
serverCancel()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
// set socket file permission
|
||||||
mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)
|
mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("parse unix_file_perm", ":", err)
|
utils.Log.Errorf("failed to parse socket file permission: %+v", err)
|
||||||
} else {
|
} else {
|
||||||
err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))
|
err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("chmod socket file", ":", err)
|
utils.Log.Errorf("failed to chmod socket file: %+v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = unixSrv.Serve(listener)
|
err = unixSrv.Serve(listener)
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Errorln("start Unix server", ":", err)
|
utils.Log.Fatalf("failed to start unix: %s", err.Error())
|
||||||
serverCancel()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
|
||||||
|
s3r := gin.New()
|
||||||
|
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
|
||||||
|
server.InitS3(s3r)
|
||||||
|
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
|
||||||
|
fmt.Printf("start S3 server @ %s\n", s3Base)
|
||||||
|
utils.Log.Infof("start S3 server @ %s", s3Base)
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
if conf.Conf.S3.SSL {
|
||||||
|
httpsSrv = &http.Server{Addr: s3Base, Handler: s3r}
|
||||||
|
err = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||||
|
}
|
||||||
|
if !conf.Conf.S3.SSL {
|
||||||
|
httpSrv = &http.Server{Addr: s3Base, Handler: s3r}
|
||||||
|
err = httpSrv.ListenAndServe()
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
utils.Log.Fatalf("failed to start s3 server: %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
var ftpDriver *server.FtpMainDriver
|
||||||
|
var ftpServer *ftpserver.FtpServer
|
||||||
|
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable {
|
||||||
|
var err error
|
||||||
|
ftpDriver, err = server.NewMainDriver()
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Fatalf("failed to start ftp driver: %s", err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Printf("start ftp server on %s\n", conf.Conf.FTP.Listen)
|
||||||
|
utils.Log.Infof("start ftp server on %s", conf.Conf.FTP.Listen)
|
||||||
|
go func() {
|
||||||
|
ftpServer = ftpserver.NewFtpServer(ftpDriver)
|
||||||
|
err = ftpServer.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Fatalf("problem ftp server listening: %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var sftpDriver *server.SftpDriver
|
||||||
|
var sftpServer *sftpd.SftpServer
|
||||||
|
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable {
|
||||||
|
var err error
|
||||||
|
sftpDriver, err = server.NewSftpDriver()
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Fatalf("failed to start sftp driver: %s", err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Printf("start sftp server on %s", conf.Conf.SFTP.Listen)
|
||||||
|
utils.Log.Infof("start sftp server on %s", conf.Conf.SFTP.Listen)
|
||||||
|
go func() {
|
||||||
|
sftpServer = sftpd.NewSftpServer(sftpDriver)
|
||||||
|
err = sftpServer.RunServer()
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Fatalf("problem sftp server listening: %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wait for interrupt signal to gracefully shutdown the server with
|
||||||
|
// a timeout of 1 second.
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
// kill (no param) default send syscanll.SIGTERM
|
// kill (no param) default send syscanll.SIGTERM
|
||||||
// kill -2 is syscall.SIGINT
|
// kill -2 is syscall.SIGINT
|
||||||
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
|
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
select {
|
<-quit
|
||||||
case <-quit:
|
utils.Log.Println("Shutdown server...")
|
||||||
case <-serverCtx.Done():
|
fs.ArchiveContentUploadTaskManager.RemoveAll()
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("shutdown server...")
|
|
||||||
Release()
|
Release()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
quitCtx, quitCancel := context.WithTimeout(context.Background(), time.Second)
|
defer cancel()
|
||||||
defer quitCancel()
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
if httpSrv != nil {
|
if conf.Conf.Scheme.HttpPort != -1 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := httpSrv.Shutdown(quitCtx); err != nil {
|
if err := httpSrv.Shutdown(ctx); err != nil {
|
||||||
log.Errorln("shutdown HTTP server", ":", err)
|
utils.Log.Fatal("HTTP server shutdown err: ", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if httpsSrv != nil {
|
if conf.Conf.Scheme.HttpsPort != -1 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := httpsSrv.Shutdown(quitCtx); err != nil {
|
if err := httpsSrv.Shutdown(ctx); err != nil {
|
||||||
log.Errorln("shutdown HTTPS server", ":", err)
|
utils.Log.Fatal("HTTPS server shutdown err: ", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if unixSrv != nil {
|
if conf.Conf.Scheme.UnixFile != "" {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := unixSrv.Shutdown(quitCtx); err != nil {
|
if err := unixSrv.Shutdown(ctx); err != nil {
|
||||||
log.Errorln("shutdown Unix server", ":", err)
|
utils.Log.Fatal("Unix server shutdown err: ", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable && ftpServer != nil && ftpDriver != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
ftpDriver.Stop()
|
||||||
|
if err := ftpServer.Stop(); err != nil {
|
||||||
|
utils.Log.Fatal("FTP server shutdown err: ", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable && sftpServer != nil && sftpDriver != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := sftpServer.Close(); err != nil {
|
||||||
|
utils.Log.Fatal("SFTP server shutdown err: ", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
log.Println("server exit")
|
utils.Log.Println("Server exit")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.AddCommand(ServerCmd)
|
RootCmd.AddCommand(ServerCmd)
|
||||||
|
|
||||||
|
// Here you will define your flags and configuration settings.
|
||||||
|
|
||||||
|
// Cobra supports Persistent Flags which will work for this command
|
||||||
|
// and all subcommands, e.g.:
|
||||||
|
// serverCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||||
|
|
||||||
|
// Cobra supports local flags which will only run when this command
|
||||||
|
// is called directly, e.g.:
|
||||||
|
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
}
|
}
|
||||||
|
|
||||||
// OutOpenListInit 暴露用于外部启动server的函数
|
// OutOpenListInit 暴露用于外部启动server的函数
|
||||||
|
@ -6,10 +6,9 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '5244:5244'
|
- '5244:5244'
|
||||||
- '5245:5245'
|
- '5245:5245'
|
||||||
|
user: '0:0'
|
||||||
environment:
|
environment:
|
||||||
- PUID=0
|
|
||||||
- PGID=0
|
|
||||||
- UMASK=022
|
- UMASK=022
|
||||||
- TZ=UTC
|
- TZ=Asia/Shanghai
|
||||||
container_name: openlist
|
container_name: openlist
|
||||||
image: 'openlistteam/openlist:latest'
|
image: 'openlistteam/openlist:latest'
|
60
drivers/115/appver.go
Normal file
60
drivers/115/appver.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package _115
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
md5Salt = "Qclm8MGWUv59TnrR0XPg"
|
||||||
|
appVer = "35.6.0.3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Pan115) getAppVersion() (string, error) {
|
||||||
|
result := VersionResp{}
|
||||||
|
res, err := base.RestyClient.R().Get(driver115.ApiGetVersion)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = utils.Json.Unmarshal(res.Body(), &result)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(result.Error) > 0 {
|
||||||
|
return "", errors.New(result.Error)
|
||||||
|
}
|
||||||
|
return result.Data.Win.Version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) getAppVer() string {
|
||||||
|
ver, err := d.getAppVersion()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("[115] get app version failed: %v", err)
|
||||||
|
return appVer
|
||||||
|
}
|
||||||
|
if len(ver) > 0 {
|
||||||
|
return ver
|
||||||
|
}
|
||||||
|
return appVer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Pan115) initAppVer() {
|
||||||
|
appVer = d.getAppVer()
|
||||||
|
log.Debugf("use app version: %v", appVer)
|
||||||
|
}
|
||||||
|
|
||||||
|
type VersionResp struct {
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Data Versions `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Versions struct {
|
||||||
|
Win Version `json:"win"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Version struct {
|
||||||
|
Version string `json:"version_code"`
|
||||||
|
}
|
@ -337,6 +337,27 @@ func (d *Open115) OfflineList(ctx context.Context) (*sdk.OfflineTaskListResp, er
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Open115) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
|
||||||
|
userInfo, err := d.client.UserInfo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
total, err := userInfo.RtSpaceInfo.AllTotal.Size.Int64()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
free, err := userInfo.RtSpaceInfo.AllRemain.Size.Int64()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.StorageDetails{
|
||||||
|
DiskUsage: model.DiskUsage{
|
||||||
|
TotalSpace: uint64(total),
|
||||||
|
FreeSpace: uint64(free),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
// func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||||
// // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
|
// // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
|
||||||
// return nil, errs.NotImplement
|
// return nil, errs.NotImplement
|
@ -28,7 +28,7 @@ func (f File) CreateTime() time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f File) GetHash() utils.HashInfo {
|
func (f File) GetHash() utils.HashInfo {
|
||||||
return utils.HashInfo{}
|
return utils.NewHashInfo(utils.MD5, f.Etag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f File) GetPath() string {
|
func (f File) GetPath() string {
|
@ -17,6 +17,7 @@ import (
|
|||||||
type Open123 struct {
|
type Open123 struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
|
UID uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Open123) Config() driver.Config {
|
func (d *Open123) Config() driver.Config {
|
||||||
@ -69,13 +70,45 @@ func (d *Open123) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
|||||||
func (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
fileId, _ := strconv.ParseInt(file.GetID(), 10, 64)
|
fileId, _ := strconv.ParseInt(file.GetID(), 10, 64)
|
||||||
|
|
||||||
|
if d.DirectLink {
|
||||||
|
res, err := d.getDirectLink(fileId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.DirectLinkPrivateKey == "" {
|
||||||
|
duration := 365 * 24 * time.Hour // 缓存1年
|
||||||
|
return &model.Link{
|
||||||
|
URL: res.Data.URL,
|
||||||
|
Expiration: &duration,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := d.getUID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Duration(d.DirectLinkValidDuration) * time.Minute
|
||||||
|
|
||||||
|
newURL, err := d.SignURL(res.Data.URL, d.DirectLinkPrivateKey,
|
||||||
|
uid, duration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Link{
|
||||||
|
URL: newURL,
|
||||||
|
Expiration: &duration,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
res, err := d.getDownloadInfo(fileId)
|
res, err := d.getDownloadInfo(fileId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
link := model.Link{URL: res.Data.DownloadUrl}
|
return &model.Link{URL: res.Data.DownloadUrl}, nil
|
||||||
return &link, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Open123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
func (d *Open123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
@ -181,5 +214,20 @@ func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
|
|||||||
return nil, fmt.Errorf("upload complete timeout")
|
return nil, fmt.Errorf("upload complete timeout")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Open123) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
|
||||||
|
userInfo, err := d.getUserInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
total := userInfo.Data.SpacePermanent + userInfo.Data.SpaceTemp
|
||||||
|
free := total - userInfo.Data.SpaceUsed
|
||||||
|
return &model.StorageDetails{
|
||||||
|
DiskUsage: model.DiskUsage{
|
||||||
|
TotalSpace: total,
|
||||||
|
FreeSpace: free,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*Open123)(nil)
|
var _ driver.Driver = (*Open123)(nil)
|
||||||
var _ driver.PutResult = (*Open123)(nil)
|
var _ driver.PutResult = (*Open123)(nil)
|
@ -23,6 +23,11 @@ type Addition struct {
|
|||||||
// 上传线程数
|
// 上传线程数
|
||||||
UploadThread int `json:"UploadThread" type:"number" default:"3" help:"the threads of upload"`
|
UploadThread int `json:"UploadThread" type:"number" default:"3" help:"the threads of upload"`
|
||||||
|
|
||||||
|
// 使用直链
|
||||||
|
DirectLink bool `json:"DirectLink" type:"bool" default:"false" required:"false" help:"use direct link when download file"`
|
||||||
|
DirectLinkPrivateKey string `json:"DirectLinkPrivateKey" required:"false" help:"private key for direct link, if URL authentication is enabled"`
|
||||||
|
DirectLinkValidDuration int64 `json:"DirectLinkValidDuration" type:"number" default:"30" required:"false" help:"minutes, if URL authentication is enabled"`
|
||||||
|
|
||||||
driver.RootID
|
driver.RootID
|
||||||
}
|
}
|
||||||
|
|
@ -127,19 +127,19 @@ type RefreshTokenResp struct {
|
|||||||
type UserInfoResp struct {
|
type UserInfoResp struct {
|
||||||
BaseResp
|
BaseResp
|
||||||
Data struct {
|
Data struct {
|
||||||
UID int64 `json:"uid"`
|
UID uint64 `json:"uid"`
|
||||||
Username string `json:"username"`
|
// Username string `json:"username"`
|
||||||
DisplayName string `json:"displayName"`
|
// DisplayName string `json:"displayName"`
|
||||||
HeadImage string `json:"headImage"`
|
// HeadImage string `json:"headImage"`
|
||||||
Passport string `json:"passport"`
|
// Passport string `json:"passport"`
|
||||||
Mail string `json:"mail"`
|
// Mail string `json:"mail"`
|
||||||
SpaceUsed int64 `json:"spaceUsed"`
|
SpaceUsed uint64 `json:"spaceUsed"`
|
||||||
SpacePermanent int64 `json:"spacePermanent"`
|
SpacePermanent uint64 `json:"spacePermanent"`
|
||||||
SpaceTemp int64 `json:"spaceTemp"`
|
SpaceTemp uint64 `json:"spaceTemp"`
|
||||||
SpaceTempExpr string `json:"spaceTempExpr"`
|
// SpaceTempExpr int64 `json:"spaceTempExpr"`
|
||||||
Vip bool `json:"vip"`
|
// Vip bool `json:"vip"`
|
||||||
DirectTraffic int64 `json:"directTraffic"`
|
// DirectTraffic int64 `json:"directTraffic"`
|
||||||
IsHideUID bool `json:"isHideUID"`
|
// IsHideUID bool `json:"isHideUID"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +158,13 @@ type DownloadInfoResp struct {
|
|||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DirectLinkResp struct {
|
||||||
|
BaseResp
|
||||||
|
Data struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
// 创建文件V2返回
|
// 创建文件V2返回
|
||||||
type UploadCreateResp struct {
|
type UploadCreateResp struct {
|
||||||
BaseResp
|
BaseResp
|
@ -70,6 +70,8 @@ func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createRes
|
|||||||
var reader *stream.SectionReader
|
var reader *stream.SectionReader
|
||||||
var rateLimitedRd io.Reader
|
var rateLimitedRd io.Reader
|
||||||
sliceMD5 := ""
|
sliceMD5 := ""
|
||||||
|
// 表单
|
||||||
|
b := bytes.NewBuffer(make([]byte, 0, 2048))
|
||||||
threadG.GoWithLifecycle(errgroup.Lifecycle{
|
threadG.GoWithLifecycle(errgroup.Lifecycle{
|
||||||
Before: func(ctx context.Context) error {
|
Before: func(ctx context.Context) error {
|
||||||
if reader == nil {
|
if reader == nil {
|
||||||
@ -84,7 +86,6 @@ func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createRes
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rateLimitedRd = driver.NewLimitedUploadStream(ctx, reader)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@ -92,9 +93,8 @@ func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createRes
|
|||||||
// 重置分片reader位置,因为HashReader、上一次失败已经读取到分片EOF
|
// 重置分片reader位置,因为HashReader、上一次失败已经读取到分片EOF
|
||||||
reader.Seek(0, io.SeekStart)
|
reader.Seek(0, io.SeekStart)
|
||||||
|
|
||||||
// 创建表单数据
|
b.Reset()
|
||||||
var b bytes.Buffer
|
w := multipart.NewWriter(b)
|
||||||
w := multipart.NewWriter(&b)
|
|
||||||
// 添加表单字段
|
// 添加表单字段
|
||||||
err = w.WriteField("preuploadID", createResp.Data.PreuploadID)
|
err = w.WriteField("preuploadID", createResp.Data.PreuploadID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -109,21 +109,20 @@ func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createRes
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 写入文件内容
|
// 写入文件内容
|
||||||
fw, err := w.CreateFormFile("slice", fmt.Sprintf("%s.part%d", file.GetName(), partNumber))
|
_, err = w.CreateFormFile("slice", fmt.Sprintf("%s.part%d", file.GetName(), partNumber))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = utils.CopyWithBuffer(fw, rateLimitedRd)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
headSize := b.Len()
|
||||||
err = w.Close()
|
err = w.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
head := bytes.NewReader(b.Bytes()[:headSize])
|
||||||
|
tail := bytes.NewReader(b.Bytes()[headSize:])
|
||||||
|
rateLimitedRd = driver.NewLimitedUploadStream(ctx, io.MultiReader(head, reader, tail))
|
||||||
// 创建请求并设置header
|
// 创建请求并设置header
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadDomain+"/upload/v2/file/slice", &b)
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadDomain+"/upload/v2/file/slice", rateLimitedRd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
@ -1,15 +1,20 @@
|
|||||||
package _123_open
|
package _123_open
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,7 +25,8 @@ var ( //不同情况下获取的AccessTokenQPS限制不同 如下模块化易于
|
|||||||
RefreshToken = InitApiInfo(Api+"/api/v1/oauth2/access_token", 1)
|
RefreshToken = InitApiInfo(Api+"/api/v1/oauth2/access_token", 1)
|
||||||
UserInfo = InitApiInfo(Api+"/api/v1/user/info", 1)
|
UserInfo = InitApiInfo(Api+"/api/v1/user/info", 1)
|
||||||
FileList = InitApiInfo(Api+"/api/v2/file/list", 3)
|
FileList = InitApiInfo(Api+"/api/v2/file/list", 3)
|
||||||
DownloadInfo = InitApiInfo(Api+"/api/v1/file/download_info", 0)
|
DownloadInfo = InitApiInfo(Api+"/api/v1/file/download_info", 5)
|
||||||
|
DirectLink = InitApiInfo(Api+"/api/v1/direct-link/url", 5)
|
||||||
Mkdir = InitApiInfo(Api+"/upload/v1/file/mkdir", 2)
|
Mkdir = InitApiInfo(Api+"/upload/v1/file/mkdir", 2)
|
||||||
Move = InitApiInfo(Api+"/api/v1/file/move", 1)
|
Move = InitApiInfo(Api+"/api/v1/file/move", 1)
|
||||||
Rename = InitApiInfo(Api+"/api/v1/file/name", 1)
|
Rename = InitApiInfo(Api+"/api/v1/file/name", 1)
|
||||||
@ -80,8 +86,24 @@ func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCall
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Open123) flushAccessToken() error {
|
func (d *Open123) flushAccessToken() error {
|
||||||
if d.Addition.ClientID != "" {
|
if d.ClientID != "" {
|
||||||
if d.Addition.ClientSecret != "" {
|
if d.RefreshToken != "" {
|
||||||
|
var resp RefreshTokenResp
|
||||||
|
_, err := d.Request(RefreshToken, http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetQueryParam("client_id", d.ClientID)
|
||||||
|
if d.ClientSecret != "" {
|
||||||
|
req.SetQueryParam("client_secret", d.ClientSecret)
|
||||||
|
}
|
||||||
|
req.SetQueryParam("grant_type", "refresh_token")
|
||||||
|
req.SetQueryParam("refresh_token", d.RefreshToken)
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.AccessToken = resp.AccessToken
|
||||||
|
d.RefreshToken = resp.RefreshToken
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
} else if d.ClientSecret != "" {
|
||||||
var resp AccessTokenResp
|
var resp AccessTokenResp
|
||||||
_, err := d.Request(AccessToken, http.MethodPost, func(req *resty.Request) {
|
_, err := d.Request(AccessToken, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
@ -94,24 +116,38 @@ func (d *Open123) flushAccessToken() error {
|
|||||||
}
|
}
|
||||||
d.AccessToken = resp.Data.AccessToken
|
d.AccessToken = resp.Data.AccessToken
|
||||||
op.MustSaveDriverStorage(d)
|
op.MustSaveDriverStorage(d)
|
||||||
} else if d.Addition.RefreshToken != "" {
|
|
||||||
var resp RefreshTokenResp
|
|
||||||
_, err := d.Request(RefreshToken, http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetQueryParam("client_id", d.ClientID)
|
|
||||||
req.SetQueryParam("grant_type", "refresh_token")
|
|
||||||
req.SetQueryParam("refresh_token", d.Addition.RefreshToken)
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.AccessToken = resp.AccessToken
|
|
||||||
d.RefreshToken = resp.RefreshToken
|
|
||||||
op.MustSaveDriverStorage(d)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Open123) SignURL(originURL, privateKey string, uid uint64, validDuration time.Duration) (newURL string, err error) {
|
||||||
|
// 生成Unix时间戳
|
||||||
|
ts := time.Now().Add(validDuration).Unix()
|
||||||
|
|
||||||
|
// 生成随机数(建议使用UUID,不能包含中划线(-))
|
||||||
|
rand := strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||||
|
|
||||||
|
// 解析URL
|
||||||
|
objURL, err := url.Parse(originURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 待签名字符串,格式:path-timestamp-rand-uid-privateKey
|
||||||
|
unsignedStr := fmt.Sprintf("%s-%d-%s-%d-%s", objURL.Path, ts, rand, uid, privateKey)
|
||||||
|
md5Hash := md5.Sum([]byte(unsignedStr))
|
||||||
|
// 生成鉴权参数,格式:timestamp-rand-uid-md5hash
|
||||||
|
authKey := fmt.Sprintf("%d-%s-%d-%x", ts, rand, uid, md5Hash)
|
||||||
|
|
||||||
|
// 添加鉴权参数到URL查询参数
|
||||||
|
v := objURL.Query()
|
||||||
|
v.Add("auth_key", authKey)
|
||||||
|
objURL.RawQuery = v.Encode()
|
||||||
|
|
||||||
|
return objURL.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Open123) getUserInfo() (*UserInfoResp, error) {
|
func (d *Open123) getUserInfo() (*UserInfoResp, error) {
|
||||||
var resp UserInfoResp
|
var resp UserInfoResp
|
||||||
|
|
||||||
@ -122,6 +158,18 @@ func (d *Open123) getUserInfo() (*UserInfoResp, error) {
|
|||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Open123) getUID() (uint64, error) {
|
||||||
|
if d.UID != 0 {
|
||||||
|
return d.UID, nil
|
||||||
|
}
|
||||||
|
resp, err := d.getUserInfo()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
d.UID = resp.Data.UID
|
||||||
|
return resp.Data.UID, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*FileListResp, error) {
|
func (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*FileListResp, error) {
|
||||||
var resp FileListResp
|
var resp FileListResp
|
||||||
|
|
||||||
@ -159,6 +207,21 @@ func (d *Open123) getDownloadInfo(fileId int64) (*DownloadInfoResp, error) {
|
|||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Open123) getDirectLink(fileId int64) (*DirectLinkResp, error) {
|
||||||
|
var resp DirectLinkResp
|
||||||
|
|
||||||
|
_, err := d.Request(DirectLink, http.MethodGet, func(req *resty.Request) {
|
||||||
|
req.SetQueryParams(map[string]string{
|
||||||
|
"fileID": strconv.FormatInt(fileId, 10),
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Open123) mkdir(parentID int64, name string) error {
|
func (d *Open123) mkdir(parentID int64, name string) error {
|
||||||
_, err := d.Request(Mkdir, http.MethodPost, func(req *resty.Request) {
|
_, err := d.Request(Mkdir, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
@ -24,7 +24,7 @@ type File struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f File) GetHash() utils.HashInfo {
|
func (f File) GetHash() utils.HashInfo {
|
||||||
return utils.HashInfo{}
|
return utils.NewHashInfo(utils.MD5, f.Etag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f File) GetPath() string {
|
func (f File) GetPath() string {
|
@ -534,16 +534,15 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
if size > partSize {
|
if size > partSize {
|
||||||
part = (size + partSize - 1) / partSize
|
part = (size + partSize - 1) / partSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成所有 partInfos
|
||||||
partInfos := make([]PartInfo, 0, part)
|
partInfos := make([]PartInfo, 0, part)
|
||||||
for i := int64(0); i < part; i++ {
|
for i := int64(0); i < part; i++ {
|
||||||
if utils.IsCanceled(ctx) {
|
if utils.IsCanceled(ctx) {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
start := i * partSize
|
start := i * partSize
|
||||||
byteSize := size - start
|
byteSize := min(size-start, partSize)
|
||||||
if byteSize > partSize {
|
|
||||||
byteSize = partSize
|
|
||||||
}
|
|
||||||
partNumber := i + 1
|
partNumber := i + 1
|
||||||
partInfo := PartInfo{
|
partInfo := PartInfo{
|
||||||
PartNumber: partNumber,
|
PartNumber: partNumber,
|
||||||
@ -591,17 +590,20 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
// resp.Data.RapidUpload: true 支持快传,但此处直接检测是否返回分片的上传地址
|
// resp.Data.RapidUpload: true 支持快传,但此处直接检测是否返回分片的上传地址
|
||||||
// 快传的情况下同样需要手动处理冲突
|
// 快传的情况下同样需要手动处理冲突
|
||||||
if resp.Data.PartInfos != nil {
|
if resp.Data.PartInfos != nil {
|
||||||
// 读取前100个分片的上传地址
|
// Progress
|
||||||
uploadPartInfos := resp.Data.PartInfos
|
p := driver.NewProgress(size, up)
|
||||||
|
rateLimited := driver.NewLimitedUploadStream(ctx, stream)
|
||||||
|
|
||||||
// 获取后续分片的上传地址
|
// 先上传前100个分片
|
||||||
for i := 101; i < len(partInfos); i += 100 {
|
err = d.uploadPersonalParts(ctx, partInfos, resp.Data.PartInfos, rateLimited, p)
|
||||||
end := i + 100
|
if err != nil {
|
||||||
if end > len(partInfos) {
|
return err
|
||||||
end = len(partInfos)
|
}
|
||||||
}
|
|
||||||
|
// 如果还有剩余分片,分批获取上传地址并上传
|
||||||
|
for i := 100; i < len(partInfos); i += 100 {
|
||||||
|
end := min(i+100, len(partInfos))
|
||||||
batchPartInfos := partInfos[i:end]
|
batchPartInfos := partInfos[i:end]
|
||||||
|
|
||||||
moredata := base.Json{
|
moredata := base.Json{
|
||||||
"fileId": resp.Data.FileId,
|
"fileId": resp.Data.FileId,
|
||||||
"uploadId": resp.Data.UploadId,
|
"uploadId": resp.Data.UploadId,
|
||||||
@ -617,44 +619,13 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
uploadPartInfos = append(uploadPartInfos, moreresp.Data.PartInfos...)
|
err = d.uploadPersonalParts(ctx, partInfos, moreresp.Data.PartInfos, rateLimited, p)
|
||||||
}
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
p := driver.NewProgress(size, up)
|
|
||||||
|
|
||||||
rateLimited := driver.NewLimitedUploadStream(ctx, stream)
|
|
||||||
// 上传所有分片
|
|
||||||
for _, uploadPartInfo := range uploadPartInfos {
|
|
||||||
index := uploadPartInfo.PartNumber - 1
|
|
||||||
partSize := partInfos[index].PartSize
|
|
||||||
log.Debugf("[139] uploading part %+v/%+v", index, len(uploadPartInfos))
|
|
||||||
limitReader := io.LimitReader(rateLimited, partSize)
|
|
||||||
|
|
||||||
// Update Progress
|
|
||||||
r := io.TeeReader(limitReader, p)
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadPartInfo.UploadUrl, r)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
|
||||||
req.Header.Set("Content-Length", fmt.Sprint(partSize))
|
|
||||||
req.Header.Set("Origin", "https://yun.139.com")
|
|
||||||
req.Header.Set("Referer", "https://yun.139.com/")
|
|
||||||
req.ContentLength = partSize
|
|
||||||
|
|
||||||
res, err := base.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = res.Body.Close()
|
|
||||||
log.Debugf("[139] uploaded: %+v", res)
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全部分片上传完毕后,complete
|
||||||
data = base.Json{
|
data = base.Json{
|
||||||
"contentHash": fullHash,
|
"contentHash": fullHash,
|
||||||
"contentHashAlgorithm": "SHA256",
|
"contentHashAlgorithm": "SHA256",
|
@ -1,9 +1,11 @@
|
|||||||
package _139
|
package _139
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
@ -13,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
@ -623,3 +626,47 @@ func (d *Yun139) getPersonalCloudHost() string {
|
|||||||
}
|
}
|
||||||
return d.PersonalCloudHost
|
return d.PersonalCloudHost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Yun139) uploadPersonalParts(ctx context.Context, partInfos []PartInfo, uploadPartInfos []PersonalPartInfo, rateLimited *driver.RateLimitReader, p *driver.Progress) error {
|
||||||
|
// 确保数组以 PartNumber 从小到大排序
|
||||||
|
sort.Slice(uploadPartInfos, func(i, j int) bool {
|
||||||
|
return uploadPartInfos[i].PartNumber < uploadPartInfos[j].PartNumber
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, uploadPartInfo := range uploadPartInfos {
|
||||||
|
index := uploadPartInfo.PartNumber - 1
|
||||||
|
if index < 0 || index >= len(partInfos) {
|
||||||
|
return fmt.Errorf("invalid PartNumber %d: index out of bounds (partInfos length: %d)", uploadPartInfo.PartNumber, len(partInfos))
|
||||||
|
}
|
||||||
|
partSize := partInfos[index].PartSize
|
||||||
|
log.Debugf("[139] uploading part %+v/%+v", index, len(partInfos))
|
||||||
|
limitReader := io.LimitReader(rateLimited, partSize)
|
||||||
|
r := io.TeeReader(limitReader, p)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadPartInfo.UploadUrl, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
|
req.Header.Set("Content-Length", fmt.Sprint(partSize))
|
||||||
|
req.Header.Set("Origin", "https://yun.139.com")
|
||||||
|
req.Header.Set("Referer", "https://yun.139.com/")
|
||||||
|
req.ContentLength = partSize
|
||||||
|
err = func() error {
|
||||||
|
res, err := base.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
log.Debugf("[139] uploaded: %+v", res)
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
return fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
package _189_tv
|
package _189_tv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/ring"
|
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -12,18 +11,20 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/cron"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cloud189TV struct {
|
type Cloud189TV struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
client *resty.Client
|
client *resty.Client
|
||||||
tokenInfo *AppSessionResp
|
tokenInfo *AppSessionResp
|
||||||
uploadThread int
|
uploadThread int
|
||||||
familyTransferFolder *ring.Ring
|
storageConfig driver.Config
|
||||||
cleanFamilyTransferFile func()
|
|
||||||
storageConfig driver.Config
|
TempUuid string
|
||||||
|
cron *cron.Cron // 新增 cron 字段
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189TV) Config() driver.Config {
|
func (y *Cloud189TV) Config() driver.Config {
|
||||||
@ -79,10 +80,17 @@ func (y *Cloud189TV) Init(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
y.cron = cron.NewCron(time.Minute * 5)
|
||||||
|
y.cron.Do(y.keepAlive)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189TV) Drop(ctx context.Context) error {
|
func (y *Cloud189TV) Drop(ctx context.Context) error {
|
||||||
|
if y.cron != nil {
|
||||||
|
y.cron.Stop()
|
||||||
|
y.cron = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
|||||||
type Addition struct {
|
type Addition struct {
|
||||||
driver.RootID
|
driver.RootID
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
TempUuid string
|
|
||||||
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
|
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
|
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
|
@ -66,6 +66,10 @@ func (y *Cloud189TV) AppKeySignatureHeader(url, method string) map[string]string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, isFamily ...bool) ([]byte, error) {
|
func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, isFamily ...bool) ([]byte, error) {
|
||||||
|
return y.requestWithRetry(url, method, callback, params, resp, 0, isFamily...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) requestWithRetry(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, retryCount int, isFamily ...bool) ([]byte, error) {
|
||||||
req := y.client.R().SetQueryParams(clientSuffix())
|
req := y.client.R().SetQueryParams(clientSuffix())
|
||||||
|
|
||||||
if params != nil {
|
if params != nil {
|
||||||
@ -91,7 +95,22 @@ func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, para
|
|||||||
|
|
||||||
if strings.Contains(res.String(), "userSessionBO is null") ||
|
if strings.Contains(res.String(), "userSessionBO is null") ||
|
||||||
strings.Contains(res.String(), "InvalidSessionKey") {
|
strings.Contains(res.String(), "InvalidSessionKey") {
|
||||||
return nil, errors.New("session expired")
|
// 限制重试次数,避免无限递归
|
||||||
|
if retryCount >= 3 {
|
||||||
|
y.Addition.AccessToken = ""
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
return nil, errors.New("session expired after retry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试刷新会话
|
||||||
|
if err := y.refreshSession(); err != nil {
|
||||||
|
// 如果刷新失败,说明AccessToken也已过期,需要重新登录
|
||||||
|
y.Addition.AccessToken = ""
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
return nil, errors.New("session expired")
|
||||||
|
}
|
||||||
|
// 如果刷新成功,则重试原始请求(增加重试计数)
|
||||||
|
return y.requestWithRetry(url, method, callback, params, resp, retryCount+1, isFamily...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理错误
|
// 处理错误
|
||||||
@ -131,6 +150,7 @@ func (y *Cloud189TV) put(ctx context.Context, url string, headers map[string]str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 请求完成后http.Client会Close Request.Body
|
||||||
resp, err := base.HttpClient.Do(req)
|
resp, err := base.HttpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -210,7 +230,7 @@ func (y *Cloud189TV) login() (err error) {
|
|||||||
var erron RespErr
|
var erron RespErr
|
||||||
var tokenInfo AppSessionResp
|
var tokenInfo AppSessionResp
|
||||||
if y.Addition.AccessToken == "" {
|
if y.Addition.AccessToken == "" {
|
||||||
if y.Addition.TempUuid == "" {
|
if y.TempUuid == "" {
|
||||||
// 获取登录参数
|
// 获取登录参数
|
||||||
var uuidInfo UuidInfoResp
|
var uuidInfo UuidInfoResp
|
||||||
req.SetResult(&uuidInfo).SetError(&erron)
|
req.SetResult(&uuidInfo).SetError(&erron)
|
||||||
@ -229,7 +249,7 @@ func (y *Cloud189TV) login() (err error) {
|
|||||||
if uuidInfo.Uuid == "" {
|
if uuidInfo.Uuid == "" {
|
||||||
return errors.New("uuidInfo is empty")
|
return errors.New("uuidInfo is empty")
|
||||||
}
|
}
|
||||||
y.Addition.TempUuid = uuidInfo.Uuid
|
y.TempUuid = uuidInfo.Uuid
|
||||||
op.MustSaveDriverStorage(y)
|
op.MustSaveDriverStorage(y)
|
||||||
|
|
||||||
// 展示二维码
|
// 展示二维码
|
||||||
@ -257,7 +277,7 @@ func (y *Cloud189TV) login() (err error) {
|
|||||||
// Signature
|
// Signature
|
||||||
req.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/qrcodeLoginResult.action",
|
req.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/qrcodeLoginResult.action",
|
||||||
http.MethodGet))
|
http.MethodGet))
|
||||||
req.SetQueryParam("uuid", y.Addition.TempUuid)
|
req.SetQueryParam("uuid", y.TempUuid)
|
||||||
_, err = req.Execute(http.MethodGet, ApiUrl+"/family/manage/qrcodeLoginResult.action")
|
_, err = req.Execute(http.MethodGet, ApiUrl+"/family/manage/qrcodeLoginResult.action")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -269,7 +289,6 @@ func (y *Cloud189TV) login() (err error) {
|
|||||||
return errors.New("E189AccessToken is empty")
|
return errors.New("E189AccessToken is empty")
|
||||||
}
|
}
|
||||||
y.Addition.AccessToken = accessTokenResp.E189AccessToken
|
y.Addition.AccessToken = accessTokenResp.E189AccessToken
|
||||||
y.Addition.TempUuid = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 获取SessionKey 和 SessionSecret
|
// 获取SessionKey 和 SessionSecret
|
||||||
@ -293,6 +312,44 @@ func (y *Cloud189TV) login() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refreshSession 尝试使用现有的 AccessToken 刷新会话
|
||||||
|
func (y *Cloud189TV) refreshSession() (err error) {
|
||||||
|
var erron RespErr
|
||||||
|
var tokenInfo AppSessionResp
|
||||||
|
reqb := y.client.R().SetQueryParams(clientSuffix())
|
||||||
|
reqb.SetResult(&tokenInfo).SetError(&erron)
|
||||||
|
// Signature
|
||||||
|
reqb.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/loginFamilyMerge.action",
|
||||||
|
http.MethodGet))
|
||||||
|
reqb.SetQueryParam("e189AccessToken", y.Addition.AccessToken)
|
||||||
|
_, err = reqb.Execute(http.MethodGet, ApiUrl+"/family/manage/loginFamilyMerge.action")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if erron.HasError() {
|
||||||
|
return &erron
|
||||||
|
}
|
||||||
|
|
||||||
|
y.tokenInfo = &tokenInfo
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) keepAlive() {
|
||||||
|
_, err := y.get(ApiUrl+"/keepUserSession.action", func(r *resty.Request) {
|
||||||
|
r.SetQueryParams(clientSuffix())
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Warnf("189tv: Failed to keep user session alive: %v", err)
|
||||||
|
// 如果keepAlive失败,尝试刷新session
|
||||||
|
if refreshErr := y.refreshSession(); refreshErr != nil {
|
||||||
|
utils.Log.Errorf("189tv: Failed to refresh session after keepAlive error: %v", refreshErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.Log.Debugf("189tv: User session kept alive successfully.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (y *Cloud189TV) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
|
func (y *Cloud189TV) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||||
fileMd5 := stream.GetHash().GetHash(utils.MD5)
|
fileMd5 := stream.GetHash().GetHash(utils.MD5)
|
||||||
if len(fileMd5) < utils.MD5.Width {
|
if len(fileMd5) < utils.MD5.Width {
|
||||||
@ -333,6 +390,10 @@ func (y *Cloud189TV) OldUpload(ctx context.Context, dstDir model.Obj, file model
|
|||||||
|
|
||||||
// 网盘中不存在该文件,开始上传
|
// 网盘中不存在该文件,开始上传
|
||||||
status := GetUploadFileStatusResp{CreateUploadFileResp: *uploadInfo}
|
status := GetUploadFileStatusResp{CreateUploadFileResp: *uploadInfo}
|
||||||
|
// driver.RateLimitReader会尝试Close底层的reader
|
||||||
|
// 但这里的tempFile是一个*os.File,Close后就没法继续读了
|
||||||
|
// 所以这里用io.NopCloser包一层
|
||||||
|
rateLimitedRd := driver.NewLimitedUploadStream(ctx, io.NopCloser(tempFile))
|
||||||
for status.GetSize() < file.GetSize() && status.FileDataExists != 1 {
|
for status.GetSize() < file.GetSize() && status.FileDataExists != 1 {
|
||||||
if utils.IsCanceled(ctx) {
|
if utils.IsCanceled(ctx) {
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
@ -350,7 +411,7 @@ func (y *Cloud189TV) OldUpload(ctx context.Context, dstDir model.Obj, file model
|
|||||||
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := y.put(ctx, status.FileUploadUrl, header, true, tempFile, isFamily)
|
_, err := y.put(ctx, status.FileUploadUrl, header, true, rateLimitedRd, isFamily)
|
||||||
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
|
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/cron"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -21,12 +22,12 @@ type Cloud189PC struct {
|
|||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
|
|
||||||
identity string
|
|
||||||
|
|
||||||
client *resty.Client
|
client *resty.Client
|
||||||
|
|
||||||
loginParam *LoginParam
|
loginParam *LoginParam
|
||||||
tokenInfo *AppSessionResp
|
qrcodeParam *QRLoginParam
|
||||||
|
|
||||||
|
tokenInfo *AppSessionResp
|
||||||
|
|
||||||
uploadThread int
|
uploadThread int
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ type Cloud189PC struct {
|
|||||||
|
|
||||||
storageConfig driver.Config
|
storageConfig driver.Config
|
||||||
ref *Cloud189PC
|
ref *Cloud189PC
|
||||||
|
cron *cron.Cron
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Config() driver.Config {
|
func (y *Cloud189PC) Config() driver.Config {
|
||||||
@ -84,14 +86,22 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免重复登陆
|
// 先尝试用Token刷新,之后尝试登陆
|
||||||
identity := utils.GetMD5EncodeStr(y.Username + y.Password)
|
if y.Addition.RefreshToken != "" {
|
||||||
if !y.isLogin() || y.identity != identity {
|
y.tokenInfo = &AppSessionResp{RefreshToken: y.Addition.RefreshToken}
|
||||||
y.identity = identity
|
if err = y.refreshToken(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if err = y.login(); err != nil {
|
if err = y.login(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化并启动 cron 任务
|
||||||
|
y.cron = cron.NewCron(time.Duration(time.Minute * 5))
|
||||||
|
// 每5分钟执行一次 keepAlive
|
||||||
|
y.cron.Do(y.keepAlive)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理家庭云ID
|
// 处理家庭云ID
|
||||||
@ -128,6 +138,10 @@ func (d *Cloud189PC) InitReference(storage driver.Driver) error {
|
|||||||
|
|
||||||
func (y *Cloud189PC) Drop(ctx context.Context) error {
|
func (y *Cloud189PC) Drop(ctx context.Context) error {
|
||||||
y.ref = nil
|
y.ref = nil
|
||||||
|
if y.cron != nil {
|
||||||
|
y.cron.Stop()
|
||||||
|
y.cron = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -80,6 +80,20 @@ func timestamp() int64 {
|
|||||||
return time.Now().UTC().UnixNano() / 1e6
|
return time.Now().UTC().UnixNano() / 1e6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatDate formats a time.Time object into the "YYYY-MM-DDHH:mm:ssSSS" format.
|
||||||
|
func formatDate(t time.Time) string {
|
||||||
|
// The layout string "2006-01-0215:04:05.000" corresponds to:
|
||||||
|
// 2006 -> Year (YYYY)
|
||||||
|
// 01 -> Month (MM)
|
||||||
|
// 02 -> Day (DD)
|
||||||
|
// 15 -> Hour (HH)
|
||||||
|
// 04 -> Minute (mm)
|
||||||
|
// 05 -> Second (ss)
|
||||||
|
// 000 -> Millisecond (SSS) with leading zeros
|
||||||
|
// Note the lack of a separator between the date and hour, matching the desired output.
|
||||||
|
return t.Format("2006-01-0215:04:05.000")
|
||||||
|
}
|
||||||
|
|
||||||
func MustParseTime(str string) *time.Time {
|
func MustParseTime(str string) *time.Time {
|
||||||
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05 -07", str+" +08", time.Local)
|
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05 -07", str+" +08", time.Local)
|
||||||
return &lastOpTime
|
return &lastOpTime
|
@ -6,9 +6,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
Username string `json:"username" required:"true"`
|
LoginType string `json:"login_type" type:"select" options:"password,qrcode" default:"password" required:"true"`
|
||||||
Password string `json:"password" required:"true"`
|
Username string `json:"username" required:"true"`
|
||||||
VCode string `json:"validate_code"`
|
Password string `json:"password" required:"true"`
|
||||||
|
VCode string `json:"validate_code"`
|
||||||
|
RefreshToken string `json:"refresh_token" help:"To switch accounts, please clear this field"`
|
||||||
driver.RootID
|
driver.RootID
|
||||||
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
|
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
@ -68,15 +68,7 @@ func (e *RespErr) Error() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登陆需要的参数
|
type BaseLoginParam struct {
|
||||||
type LoginParam struct {
|
|
||||||
// 加密后的用户名和密码
|
|
||||||
RsaUsername string
|
|
||||||
RsaPassword string
|
|
||||||
|
|
||||||
// rsa密钥
|
|
||||||
jRsaKey string
|
|
||||||
|
|
||||||
// 请求头参数
|
// 请求头参数
|
||||||
Lt string
|
Lt string
|
||||||
ReqId string
|
ReqId string
|
||||||
@ -88,6 +80,27 @@ type LoginParam struct {
|
|||||||
CaptchaToken string
|
CaptchaToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QRLoginParam 用于暂存二维码登录过程中的参数
|
||||||
|
type QRLoginParam struct {
|
||||||
|
BaseLoginParam
|
||||||
|
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
EncodeUUID string `json:"encodeuuid"`
|
||||||
|
EncryUUID string `json:"encryuuid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登陆需要的参数
|
||||||
|
type LoginParam struct {
|
||||||
|
// 加密后的用户名和密码
|
||||||
|
RsaUsername string
|
||||||
|
RsaPassword string
|
||||||
|
|
||||||
|
// rsa密钥
|
||||||
|
jRsaKey string
|
||||||
|
|
||||||
|
BaseLoginParam
|
||||||
|
}
|
||||||
|
|
||||||
// 登陆加密相关
|
// 登陆加密相关
|
||||||
type EncryptConfResp struct {
|
type EncryptConfResp struct {
|
||||||
Result int `json:"result"`
|
Result int `json:"result"`
|
@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
|
"github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
|
||||||
"github.com/avast/retry-go"
|
"github.com/avast/retry-go"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
@ -54,6 +55,9 @@ const (
|
|||||||
MAC = "TELEMAC"
|
MAC = "TELEMAC"
|
||||||
|
|
||||||
CHANNEL_ID = "web_cloud.189.cn"
|
CHANNEL_ID = "web_cloud.189.cn"
|
||||||
|
|
||||||
|
// Error codes
|
||||||
|
UserInvalidOpenTokenError = "UserInvalidOpenToken"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
|
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
|
||||||
@ -264,7 +268,14 @@ func (y *Cloud189PC) findFileByName(ctx context.Context, searchName string, fold
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) login() (err error) {
|
func (y *Cloud189PC) login() error {
|
||||||
|
if y.LoginType == "qrcode" {
|
||||||
|
return y.loginByQRCode()
|
||||||
|
}
|
||||||
|
return y.loginByPassword()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) loginByPassword() (err error) {
|
||||||
// 初始化登陆所需参数
|
// 初始化登陆所需参数
|
||||||
if y.loginParam == nil {
|
if y.loginParam == nil {
|
||||||
if err = y.initLoginParam(); err != nil {
|
if err = y.initLoginParam(); err != nil {
|
||||||
@ -278,10 +289,15 @@ func (y *Cloud189PC) login() (err error) {
|
|||||||
// 销毁登陆参数
|
// 销毁登陆参数
|
||||||
y.loginParam = nil
|
y.loginParam = nil
|
||||||
// 遇到错误,重新加载登陆参数(刷新验证码)
|
// 遇到错误,重新加载登陆参数(刷新验证码)
|
||||||
if err != nil && y.NoUseOcr {
|
if err != nil {
|
||||||
if err1 := y.initLoginParam(); err1 != nil {
|
if y.NoUseOcr {
|
||||||
err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
|
if err1 := y.initLoginParam(); err1 != nil {
|
||||||
|
err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
y.Status = err.Error()
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -336,14 +352,105 @@ func (y *Cloud189PC) login() (err error) {
|
|||||||
err = fmt.Errorf(tokenInfo.ResMessage)
|
err = fmt.Errorf(tokenInfo.ResMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
y.Addition.RefreshToken = tokenInfo.RefreshToken
|
||||||
y.tokenInfo = &tokenInfo
|
y.tokenInfo = &tokenInfo
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 初始化登陆需要的参数
|
func (y *Cloud189PC) loginByQRCode() error {
|
||||||
* 如果遇到验证码返回错误
|
if y.qrcodeParam == nil {
|
||||||
*/
|
if err := y.initQRCodeParam(); err != nil {
|
||||||
func (y *Cloud189PC) initLoginParam() error {
|
// 二维码也通过错误返回
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var state struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
RedirectUrl string `json:"redirectUrl"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
_, err := y.client.R().
|
||||||
|
SetHeaders(map[string]string{
|
||||||
|
"Referer": AUTH_URL,
|
||||||
|
"Reqid": y.qrcodeParam.ReqId,
|
||||||
|
"lt": y.qrcodeParam.Lt,
|
||||||
|
}).
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"appId": APP_ID,
|
||||||
|
"clientType": CLIENT_TYPE,
|
||||||
|
"returnUrl": RETURN_URL,
|
||||||
|
"paramId": y.qrcodeParam.ParamId,
|
||||||
|
"uuid": y.qrcodeParam.UUID,
|
||||||
|
"encryuuid": y.qrcodeParam.EncryUUID,
|
||||||
|
"date": formatDate(now),
|
||||||
|
"timeStamp": fmt.Sprint(now.UTC().UnixNano() / 1e6),
|
||||||
|
}).
|
||||||
|
ForceContentType("application/json;charset=UTF-8").
|
||||||
|
SetResult(&state).
|
||||||
|
Post(AUTH_URL + "/api/logbox/oauth2/qrcodeLoginState.do")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check QR code state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch state.Status {
|
||||||
|
case 0: // 登录成功
|
||||||
|
var tokenInfo AppSessionResp
|
||||||
|
_, err = y.client.R().
|
||||||
|
SetResult(&tokenInfo).
|
||||||
|
SetQueryParams(clientSuffix()).
|
||||||
|
SetQueryParam("redirectURL", state.RedirectUrl).
|
||||||
|
Post(API_URL + "/getSessionForPC.action")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tokenInfo.ResCode != 0 {
|
||||||
|
return fmt.Errorf(tokenInfo.ResMessage)
|
||||||
|
}
|
||||||
|
y.Addition.RefreshToken = tokenInfo.RefreshToken
|
||||||
|
y.tokenInfo = &tokenInfo
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
return nil
|
||||||
|
case -11001: // 二维码过期
|
||||||
|
y.qrcodeParam = nil
|
||||||
|
return errors.New("QR code expired, please try again")
|
||||||
|
case -106: // 等待扫描
|
||||||
|
return y.genQRCode("QR code has not been scanned yet, please scan and save again")
|
||||||
|
case -11002: // 等待确认
|
||||||
|
return y.genQRCode("QR code has been scanned, please confirm the login on your phone and save again")
|
||||||
|
default: // 其他错误
|
||||||
|
y.qrcodeParam = nil
|
||||||
|
return fmt.Errorf("QR code login failed with status %d: %s", state.Status, state.Msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) genQRCode(text string) error {
|
||||||
|
// 展示二维码
|
||||||
|
qrTemplate := `<body>
|
||||||
|
state: %s
|
||||||
|
<br><img src="data:image/jpeg;base64,%s"/>
|
||||||
|
<br>Or Click here: <a href="%s">Login</a>
|
||||||
|
</body>`
|
||||||
|
|
||||||
|
// Generate QR code
|
||||||
|
qrCode, err := qrcode.Encode(y.qrcodeParam.UUID, qrcode.Medium, 256)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate QR code: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode QR code to base64
|
||||||
|
qrCodeBase64 := base64.StdEncoding.EncodeToString(qrCode)
|
||||||
|
|
||||||
|
// Create the HTML page
|
||||||
|
qrPage := fmt.Sprintf(qrTemplate, text, qrCodeBase64, y.qrcodeParam.UUID)
|
||||||
|
return fmt.Errorf("need verify: \n%s", qrPage)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) initBaseParams() (*BaseLoginParam, error) {
|
||||||
// 清除cookie
|
// 清除cookie
|
||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
y.client.SetCookieJar(jar)
|
y.client.SetCookieJar(jar)
|
||||||
@ -357,17 +464,30 @@ func (y *Cloud189PC) initLoginParam() error {
|
|||||||
}).
|
}).
|
||||||
Get(WEB_URL + "/api/portal/unifyLoginForPC.action")
|
Get(WEB_URL + "/api/portal/unifyLoginForPC.action")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
param := LoginParam{
|
return &BaseLoginParam{
|
||||||
CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],
|
CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],
|
||||||
Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
||||||
ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
||||||
ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
||||||
// jRsaKey: regexp.MustCompile(`"j_rsaKey" value="(.+?)"`).FindStringSubmatch(res.String())[1],
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 初始化登陆需要的参数
|
||||||
|
* 如果遇到验证码返回错误
|
||||||
|
*/
|
||||||
|
func (y *Cloud189PC) initLoginParam() error {
|
||||||
|
y.loginParam = nil
|
||||||
|
|
||||||
|
baseParam, err := y.initBaseParams()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
y.loginParam = &LoginParam{BaseLoginParam: *baseParam}
|
||||||
|
|
||||||
// 获取rsa公钥
|
// 获取rsa公钥
|
||||||
var encryptConf EncryptConfResp
|
var encryptConf EncryptConfResp
|
||||||
_, err = y.client.R().
|
_, err = y.client.R().
|
||||||
@ -378,18 +498,17 @@ func (y *Cloud189PC) initLoginParam() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
param.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
|
y.loginParam.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
|
||||||
param.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Username)
|
y.loginParam.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Username)
|
||||||
param.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Password)
|
y.loginParam.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Password)
|
||||||
y.loginParam = ¶m
|
|
||||||
|
|
||||||
// 判断是否需要验证码
|
// 判断是否需要验证码
|
||||||
resp, err := y.client.R().
|
resp, err := y.client.R().
|
||||||
SetHeader("REQID", param.ReqId).
|
SetHeader("REQID", y.loginParam.ReqId).
|
||||||
SetFormData(map[string]string{
|
SetFormData(map[string]string{
|
||||||
"appKey": APP_ID,
|
"appKey": APP_ID,
|
||||||
"accountType": ACCOUNT_TYPE,
|
"accountType": ACCOUNT_TYPE,
|
||||||
"userName": param.RsaUsername,
|
"userName": y.loginParam.RsaUsername,
|
||||||
}).Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do")
|
}).Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -401,8 +520,8 @@ func (y *Cloud189PC) initLoginParam() error {
|
|||||||
// 拉取验证码
|
// 拉取验证码
|
||||||
imgRes, err := y.client.R().
|
imgRes, err := y.client.R().
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"token": param.CaptchaToken,
|
"token": y.loginParam.CaptchaToken,
|
||||||
"REQID": param.ReqId,
|
"REQID": y.loginParam.ReqId,
|
||||||
"rnd": fmt.Sprint(timestamp()),
|
"rnd": fmt.Sprint(timestamp()),
|
||||||
}).
|
}).
|
||||||
Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
|
Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
|
||||||
@ -429,10 +548,38 @@ func (y *Cloud189PC) initLoginParam() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getQRCode 获取并返回二维码
|
||||||
|
func (y *Cloud189PC) initQRCodeParam() (err error) {
|
||||||
|
y.qrcodeParam = nil
|
||||||
|
|
||||||
|
baseParam, err := y.initBaseParams()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var qrcodeParam QRLoginParam
|
||||||
|
_, err = y.client.R().
|
||||||
|
SetFormData(map[string]string{"appId": APP_ID}).
|
||||||
|
ForceContentType("application/json;charset=UTF-8").
|
||||||
|
SetResult(&qrcodeParam).
|
||||||
|
Post(AUTH_URL + "/api/logbox/oauth2/getUUID.do")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
qrcodeParam.BaseLoginParam = *baseParam
|
||||||
|
y.qrcodeParam = &qrcodeParam
|
||||||
|
|
||||||
|
return y.genQRCode("please scan the QR code with the 189 Cloud app, then save the settings again.")
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新会话
|
// 刷新会话
|
||||||
func (y *Cloud189PC) refreshSession() (err error) {
|
func (y *Cloud189PC) refreshSession() (err error) {
|
||||||
|
return y.refreshSessionWithRetry(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) refreshSessionWithRetry(retryCount int) (err error) {
|
||||||
if y.ref != nil {
|
if y.ref != nil {
|
||||||
return y.ref.refreshSession()
|
return y.ref.refreshSessionWithRetry(retryCount)
|
||||||
}
|
}
|
||||||
var erron RespErr
|
var erron RespErr
|
||||||
var userSessionResp UserSessionResp
|
var userSessionResp UserSessionResp
|
||||||
@ -449,37 +596,102 @@ func (y *Cloud189PC) refreshSession() (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误影响正常访问,下线该储存
|
// token生效刷新token
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
y.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
|
||||||
op.MustSaveDriverStorage(y)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if erron.HasError() {
|
if erron.HasError() {
|
||||||
if erron.ResCode == "UserInvalidOpenToken" {
|
if erron.ResCode == UserInvalidOpenTokenError {
|
||||||
if err = y.login(); err != nil {
|
return y.refreshTokenWithRetry(retryCount)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return &erron
|
return &erron
|
||||||
}
|
}
|
||||||
y.tokenInfo.UserSessionResp = userSessionResp
|
y.tokenInfo.UserSessionResp = userSessionResp
|
||||||
return
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshToken 刷新token,失败时返回错误,不再直接调用login
|
||||||
|
func (y *Cloud189PC) refreshToken() (err error) {
|
||||||
|
return y.refreshTokenWithRetry(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) refreshTokenWithRetry(retryCount int) (err error) {
|
||||||
|
if y.ref != nil {
|
||||||
|
return y.ref.refreshTokenWithRetry(retryCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制重试次数,避免无限递归
|
||||||
|
if retryCount >= 3 {
|
||||||
|
if y.Addition.RefreshToken != "" {
|
||||||
|
y.Addition.RefreshToken = ""
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
}
|
||||||
|
return errors.New("refresh token failed after maximum retries")
|
||||||
|
}
|
||||||
|
|
||||||
|
var erron RespErr
|
||||||
|
var tokenInfo AppSessionResp
|
||||||
|
_, err = y.client.R().
|
||||||
|
SetResult(&tokenInfo).
|
||||||
|
ForceContentType("application/json;charset=UTF-8").
|
||||||
|
SetError(&erron).
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"clientId": APP_ID,
|
||||||
|
"refreshToken": y.tokenInfo.RefreshToken,
|
||||||
|
"grantType": "refresh_token",
|
||||||
|
"format": "json",
|
||||||
|
}).
|
||||||
|
Post(AUTH_URL + "/api/oauth2/refreshToken.do")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果刷新失败,返回错误给上层处理
|
||||||
|
if erron.HasError() {
|
||||||
|
if y.Addition.RefreshToken != "" {
|
||||||
|
y.Addition.RefreshToken = ""
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据登录类型决定下一步行为
|
||||||
|
if y.LoginType == "qrcode" {
|
||||||
|
return errors.New("QR code session has expired, please re-scan the code to log in")
|
||||||
|
}
|
||||||
|
// 密码登录模式下,尝试回退到完整登录
|
||||||
|
return y.login()
|
||||||
|
}
|
||||||
|
|
||||||
|
y.Addition.RefreshToken = tokenInfo.RefreshToken
|
||||||
|
y.tokenInfo = &tokenInfo
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
return y.refreshSessionWithRetry(retryCount + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) keepAlive() {
|
||||||
|
_, err := y.get(API_URL+"/keepUserSession.action", func(r *resty.Request) {
|
||||||
|
r.SetQueryParams(clientSuffix())
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Warnf("189pc: Failed to keep user session alive: %v", err)
|
||||||
|
// 如果keepAlive失败,尝试刷新session
|
||||||
|
if refreshErr := y.refreshSession(); refreshErr != nil {
|
||||||
|
utils.Log.Errorf("189pc: Failed to refresh session after keepAlive error: %v", refreshErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.Log.Debugf("189pc: User session kept alive successfully.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通上传
|
// 普通上传
|
||||||
// 无法上传大小为0的文件
|
// 无法上传大小为0的文件
|
||||||
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
|
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||||
size := file.GetSize()
|
// 文件大小
|
||||||
sliceSize := min(size, partSize(size))
|
fileSize := file.GetSize()
|
||||||
|
// 分片大小,不得为文件大小
|
||||||
|
sliceSize := partSize(fileSize)
|
||||||
|
|
||||||
params := Params{
|
params := Params{
|
||||||
"parentFolderId": dstDir.GetID(),
|
"parentFolderId": dstDir.GetID(),
|
||||||
"fileName": url.QueryEscape(file.GetName()),
|
"fileName": url.QueryEscape(file.GetName()),
|
||||||
"fileSize": fmt.Sprint(file.GetSize()),
|
"fileSize": fmt.Sprint(fileSize),
|
||||||
"sliceSize": fmt.Sprint(sliceSize),
|
"sliceSize": fmt.Sprint(sliceSize), // 必须为特定分片大小
|
||||||
"lazyCheck": "1",
|
"lazyCheck": "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -512,10 +724,10 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
|||||||
retry.DelayType(retry.BackOffDelay))
|
retry.DelayType(retry.BackOffDelay))
|
||||||
|
|
||||||
count := 1
|
count := 1
|
||||||
if size > sliceSize {
|
if fileSize > sliceSize {
|
||||||
count = int((size + sliceSize - 1) / sliceSize)
|
count = int((fileSize + sliceSize - 1) / sliceSize)
|
||||||
}
|
}
|
||||||
lastPartSize := size % sliceSize
|
lastPartSize := fileSize % sliceSize
|
||||||
if lastPartSize == 0 {
|
if lastPartSize == 0 {
|
||||||
lastPartSize = sliceSize
|
lastPartSize = sliceSize
|
||||||
}
|
}
|
||||||
@ -535,9 +747,9 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
offset := int64((i)-1) * sliceSize
|
offset := int64((i)-1) * sliceSize
|
||||||
size := sliceSize
|
partSize := sliceSize
|
||||||
if i == count {
|
if i == count {
|
||||||
size = lastPartSize
|
partSize = lastPartSize
|
||||||
}
|
}
|
||||||
partInfo := ""
|
partInfo := ""
|
||||||
var reader *stream.SectionReader
|
var reader *stream.SectionReader
|
||||||
@ -546,14 +758,14 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
|||||||
Before: func(ctx context.Context) error {
|
Before: func(ctx context.Context) error {
|
||||||
if reader == nil {
|
if reader == nil {
|
||||||
var err error
|
var err error
|
||||||
reader, err = ss.GetSectionReader(offset, size)
|
reader, err = ss.GetSectionReader(offset, partSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
silceMd5.Reset()
|
silceMd5.Reset()
|
||||||
w, err := utils.CopyWithBuffer(writers, reader)
|
w, err := utils.CopyWithBuffer(writers, reader)
|
||||||
if w != size {
|
if w != partSize {
|
||||||
return fmt.Errorf("failed to read all data: (expect =%d, actual =%d) %w", size, w, err)
|
return fmt.Errorf("failed to read all data: (expect =%d, actual =%d) %w", partSize, w, err)
|
||||||
}
|
}
|
||||||
// 计算块md5并进行hex和base64编码
|
// 计算块md5并进行hex和base64编码
|
||||||
md5Bytes := silceMd5.Sum(nil)
|
md5Bytes := silceMd5.Sum(nil)
|
||||||
@ -573,8 +785,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
|||||||
|
|
||||||
// step.4 上传切片
|
// step.4 上传切片
|
||||||
uploadUrl := uploadUrls[0]
|
uploadUrl := uploadUrls[0]
|
||||||
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false,
|
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, rateLimitedRd, isFamily)
|
||||||
driver.NewLimitedUploadStream(ctx, rateLimitedRd), isFamily)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -595,7 +806,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
|||||||
fileMd5Hex = strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
|
fileMd5Hex = strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
|
||||||
}
|
}
|
||||||
sliceMd5Hex := fileMd5Hex
|
sliceMd5Hex := fileMd5Hex
|
||||||
if file.GetSize() > sliceSize {
|
if fileSize > sliceSize {
|
||||||
sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(silceMd5Hexs, "\n")))
|
sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(silceMd5Hexs, "\n")))
|
||||||
}
|
}
|
||||||
|
|
@ -79,21 +79,45 @@ func (d *Alias) Get(ctx context.Context, path string) (model.Obj, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, errs.ObjectNotFound
|
return nil, errs.ObjectNotFound
|
||||||
}
|
}
|
||||||
|
var ret *model.Object
|
||||||
|
provider := ""
|
||||||
for _, dst := range dsts {
|
for _, dst := range dsts {
|
||||||
obj, err := fs.Get(ctx, stdpath.Join(dst, sub), &fs.GetArgs{NoLog: true})
|
rawPath := stdpath.Join(dst, sub)
|
||||||
|
obj, err := fs.Get(ctx, rawPath, &fs.GetArgs{NoLog: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return &model.Object{
|
storage, err := fs.GetStorage(rawPath, &fs.GetStoragesArgs{})
|
||||||
Path: path,
|
if ret == nil {
|
||||||
Name: obj.GetName(),
|
ret = &model.Object{
|
||||||
Size: obj.GetSize(),
|
Path: path,
|
||||||
Modified: obj.ModTime(),
|
Name: obj.GetName(),
|
||||||
IsFolder: obj.IsDir(),
|
Size: obj.GetSize(),
|
||||||
HashInfo: obj.GetHash(),
|
Modified: obj.ModTime(),
|
||||||
|
IsFolder: obj.IsDir(),
|
||||||
|
HashInfo: obj.GetHash(),
|
||||||
|
}
|
||||||
|
if !d.ProviderPassThrough || err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
provider = storage.Config().Name
|
||||||
|
} else if err != nil || provider != storage.GetStorage().Driver {
|
||||||
|
provider = ""
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ret == nil {
|
||||||
|
return nil, errs.ObjectNotFound
|
||||||
|
}
|
||||||
|
if provider != "" {
|
||||||
|
return &model.ObjectProvider{
|
||||||
|
Object: *ret,
|
||||||
|
Provider: model.Provider{
|
||||||
|
Provider: provider,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return nil, errs.ObjectNotFound
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
@ -186,6 +210,35 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
|||||||
return nil, errs.ObjectNotFound
|
return nil, errs.ObjectNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Alias) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||||
|
root, sub := d.getRootAndPath(args.Obj.GetPath())
|
||||||
|
dsts, ok := d.pathMap[root]
|
||||||
|
if !ok {
|
||||||
|
return nil, errs.ObjectNotFound
|
||||||
|
}
|
||||||
|
for _, dst := range dsts {
|
||||||
|
rawPath := stdpath.Join(dst, sub)
|
||||||
|
storage, actualPath, err := op.GetStorageAndActualPath(rawPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
other, ok := storage.(driver.Other)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
obj, err := op.GetUnwrap(ctx, storage, actualPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return other.Other(ctx, model.OtherArgs{
|
||||||
|
Obj: obj,
|
||||||
|
Method: args.Method,
|
||||||
|
Data: args.Data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Alias) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
func (d *Alias) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
if !d.Writable {
|
if !d.Writable {
|
||||||
return errs.PermissionDenied
|
return errs.PermissionDenied
|
@ -15,6 +15,7 @@ type Addition struct {
|
|||||||
DownloadConcurrency int `json:"download_concurrency" default:"0" required:"false" type:"number" help:"Need to enable proxy"`
|
DownloadConcurrency int `json:"download_concurrency" default:"0" required:"false" type:"number" help:"Need to enable proxy"`
|
||||||
DownloadPartSize int `json:"download_part_size" default:"0" type:"number" required:"false" help:"Need to enable proxy. Unit: KB"`
|
DownloadPartSize int `json:"download_part_size" default:"0" type:"number" required:"false" help:"Need to enable proxy. Unit: KB"`
|
||||||
Writable bool `json:"writable" type:"bool" default:"false"`
|
Writable bool `json:"writable" type:"bool" default:"false"`
|
||||||
|
ProviderPassThrough bool `json:"provider_pass_through" type:"bool" default:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user