Compare commits

..

13 Commits

Author SHA1 Message Date
Pikachu Ren c0a8321461 Update release_docker.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:36:35 +08:00
Pikachu Ren 680501a8a8 Update release_android.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:36:22 +08:00
Pikachu Ren 83913a8031 Update release.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:36:11 +08:00
Pikachu Ren 929d4e65b9 Update release_freebsd.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:35:51 +08:00
Pikachu Ren 6717d02f94 Update release_linux_musl.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:35:34 +08:00
Pikachu Ren 9d2a71e3eb Update release_linux_musl_arm.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:35:21 +08:00
Pikachu Ren 62de731d37 Update release_freebsd.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:34:35 +08:00
Pikachu Ren 7277163b0a Update release_linux_musl.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:34:04 +08:00
Pikachu Ren 98f65d5478 Update release_linux_musl_arm.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:33:52 +08:00
Suyunmeng 312e04ea69 chore(ci):Fixed CI bugs 2025-06-20 20:13:40 +08:00
Pikachu Ren 6ddb4359d3 Update release.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:07:56 +08:00
Pikachu Ren cf444c2f63 Update release.yml
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2025-06-20 20:05:04 +08:00
Suyunmeng 0176cfb0c9 chore:fixed ci build 2025-06-20 19:32:10 +08:00
752 changed files with 13602 additions and 53035 deletions
@@ -1,90 +0,0 @@
name: "错误报告"
description: 错误报告 / 问题
title: "[BUG] 请修改标题为您遇到的问题"
labels: [bug]
body:
- type: markdown
attributes:
value: |
感谢您花时间填写此错误报告。
请**务必**确认您的问题无重复,且不是因为您的操作、网络或第三方软件问题。
- type: checkboxes
attributes:
label: 请确认以下事项
description: |
您必须阅读、检查、确认、同意以下内容,否则您的问题一定会被直接关闭。
或者您可以去[讨论区](https://github.com/OpenListTeam/OpenList/discussions)。
options:
- label: |
我已确认阅读并同意 [AGPL-3.0 第15条](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=15.%20Disclaimer%20of%20Warranty.) 。
本程序不提供任何明示或暗示的担保,使用风险由您自行承担。
- label: |
我已确认阅读并同意 [AGPL-3.0 第16条](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=16.%20Limitation%20of%20Liability.) 。
无论何种情况,版权持有人或其他分发者均不对使用本程序所造成的任何损失承担责任。
- label: |
我确认我的描述清晰,语法礼貌,能帮助开发者快速定位问题,并符合社区规则。
- label: |
我已确认阅读了[OpenList文档](https://doc.oplist.org)。
- label: |
我已确认没有重复的问题或讨论。
- label: |
我已确认是`OpenList`的问题,而不是其他原因(例如 [网络](https://doc.oplist.org/faq/howto#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host-1) ,`依赖`或`操作`)。
- label: |
我认为此问题必须由`OpenList`处理,而非第三方。
- label: |
我已确认这个问题在最新版本中没有被修复。
- label: |
我没有阅读这个清单,只是闭眼选中了所有的复选框,请关闭这个 Issue 。
- type: input
id: version
attributes:
label: OpenList 版本(必填)
description: |
您使用的是哪个版本的软件?请不要使用`latest`或`master`作为答案。
placeholder: v4.xx.xx
validations:
required: true
- type: input
id: driver
attributes:
label: 使用的存储驱动(必填)
description: |
您使用的是哪个存储驱动?
placeholder: "例如: OneDrive"
validations:
required: true
- type: textarea
id: bug-description
attributes:
label: 问题描述(必填)
validations:
required: true
- type: textarea
id: logs
attributes:
label: 日志(必填)
description: |
请复制粘贴错误日志,或者截图。(可隐藏隐私字段) [查看方法](https://doc.oplist.org/faq/howto#%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8Dbug)
validations:
required: true
- type: textarea
id: config
attributes:
label: 配置文件内容(必填)
description: |
请提供您的`OpenList`应用的配置文件,并截图相关存储配置。(可隐藏隐私字段)
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: 复现链接(可选)
description: |
请提供能复现此问题的链接。
- type: textarea
id: aigenerated
attributes:
label: AI生成内容(可选)
description: |
如果此问题是由AI辅助您发现的,请提供全部聊天记录,包括使用的模型信息。
@@ -1,90 +0,0 @@
name: "Bug Report"
description: Bug Report / Issue
title: "[BUG] Please modify the title to describe the issue you are facing"
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this bug report.
Please **make sure** your issue is not a duplicate and is not caused by your own operation, network, or third-party software.
- type: checkboxes
attributes:
label: Please confirm the following
description: |
You must read, check, confirm, and agree to all the following, otherwise your issue will definitely be closed directly.
Or you can go to the [discussions](https://github.com/OpenListTeam/OpenList/discussions).
options:
- label: |
I have read and agree to [AGPL-3.0 Section 15](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=15.%20Disclaimer%20of%20Warranty.) .
The program is provided "as is" without any warranties; you bear all risks of using it.
- label: |
I have read and agree to [AGPL-3.0 Section 16](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=16.%20Limitation%20of%20Liability.) .
The copyright holders and distributors are not liable for any damages resulting from the use or inability to use the program.
- label: |
I confirm my description is clear, polite, helps developers quickly locate the issue, and complies with community rules.
- label: |
I have read the [OpenList documentation](https://doc.oplist.org).
- label: |
I confirm there are no duplicate issues or discussions.
- label: |
I confirm this is an `OpenList` issue, not caused by other reasons (such as [network](https://doc.oplist.org/faq/howto#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host-1), dependencies, or operation).
- label: |
I believe this issue must be handled by `OpenList` and not by a third party.
- label: |
I confirm this issue is not fixed in the latest version.
- label: |
I have not read these checkboxes and therefore I just ticked them all, Please close this issue.
- type: input
id: version
attributes:
label: OpenList Version (required)
description: |
What version of the software are you using? Please do not use `latest` or `master` as the answer.
placeholder: v4.xx.xx
validations:
required: true
- type: input
id: driver
attributes:
label: Storage Driver Used (required)
description: |
Which storage driver are you using?
placeholder: "e.g. OneDrive"
validations:
required: true
- type: textarea
id: bug-description
attributes:
label: Bug Description (required)
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs (required)
description: |
Please copy and paste any relevant log output or screenshots. (You may mask sensitive fields) [Guide](https://doc.oplist.org/faq/howto#how-to-quickly-locate-bugs)
validations:
required: true
- type: textarea
id: config
attributes:
label: Configuration File Content (required)
description: |
Please provide your `OpenList` application's configuration file and a screenshot of the relevant storage configuration. (You may mask sensitive fields)
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction Link (optional)
description: |
Please provide a link to a repo or page that can reproduce this issue.
- type: textarea
id: aigenerated
attributes:
label: AI Generated Content (optional)
description: |
If this issue was identified with the assistance of AI, please provide the complete chat log, including information about the model used.
@@ -1,56 +0,0 @@
name: "功能请求"
description: 功能请求 / 增强
title: "[Feature] 请修改标题为您的功能名称"
labels: [enhancement]
body:
- type: checkboxes
attributes:
label: 请确认以下事项
description: |
您必须阅读、检查、确认、同意以下内容,否则您的问题可能会被直接关闭。
或者您可以去[讨论区](https://github.com/OpenListTeam/OpenList/discussions)。
options:
- label: |
我已确认阅读并同意 [AGPL-3.0 第15条](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=15.%20Disclaimer%20of%20Warranty.) 。
本程序不提供任何明示或暗示的担保,使用风险由您自行承担。
- label: |
我已确认阅读并同意 [AGPL-3.0 第16条](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=16.%20Limitation%20of%20Liability.) 。
无论何种情况,版权持有人或其他分发者均不对使用本程序所造成的任何损失承担责任。
- label: |
我确认我的描述清晰,语法礼貌,能帮助开发者快速定位问题,并符合社区规则。
- label: |
我已确认阅读了[OpenList文档](https://doc.oplist.org)。
- label: |
我已确认没有重复的问题或讨论。
- label: |
我认为此问题必须由`OpenList`处理,而非第三方。
- label: |
我已确认此功能尚未被实现。
- label: |
我已确认此功能是合理的,且有普遍需求,并非我个人需要。
- label: |
我没有阅读这个清单,只是闭眼选中了所有的复选框,请关闭这个 Issue 。
- type: textarea
id: feature-description
attributes:
label: 需求描述
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: 实现思路
description: |
实现此需求的解决思路。
- type: textarea
id: additional-context
attributes:
label: 附加信息
description: |
相关的任何其他上下文或截图,或者你觉得有帮助的信息
- type: textarea
id: aigenerated
attributes:
label: AI生成内容(可选)
description: |
如果此请求是由AI辅助您提交的,请提供全部聊天记录,包括使用的模型信息。
@@ -1,56 +0,0 @@
name: "Feature Request"
description: Feature Request / Enhancement
title: "[Feature] Please modify the title to your feature name"
labels: [enhancement]
body:
- type: checkboxes
attributes:
label: Please confirm the following
description: |
You must read, check, confirm, and agree to all the following, otherwise your request may be closed directly.
Or you can go to the [discussions](https://github.com/OpenListTeam/OpenList/discussions).
options:
- label: |
I have read and agree to [AGPL-3.0 Section 15](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=15.%20Disclaimer%20of%20Warranty.).
The program is provided "as is" without any warranties; you bear all risks of using it.
- label: |
I have read and agree to [AGPL-3.0 Section 16](https://www.gnu.org/licenses/agpl-3.0.txt#:~:text=16.%20Limitation%20of%20Liability.).
The copyright holders and distributors are not liable for any damages resulting from the use or inability to use the program.
- label: |
I confirm my description is clear, polite, helps developers quickly locate the issue, and complies with community rules.
- label: |
I have read the [OpenList documentation](https://doc.oplist.org).
- label: |
I confirm there are no duplicate issues or discussions.
- label: |
I believe this issue must be handled by `OpenList` and not by a third party.
- label: |
I confirm this feature has not been implemented yet.
- label: |
I confirm this feature is reasonable and has general demand, not just my personal need.
- label: |
I have not read these checkboxes and therefore I just ticked them all, Please close this issue.
- type: textarea
id: feature-description
attributes:
label: Feature Description
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested Solution
description: |
Solution or approach to achieve this feature.
- type: textarea
id: additional-context
attributes:
label: Additional Information
description: |
Any other context or screenshots related to this feature request, or information you find helpful.
- type: textarea
id: aigenerated
attributes:
label: AI Generated Content (optional)
description: |
If this request was submitted with the assistance of an AI, please provide the complete chat log, including information about the model used.
+81
View File
@@ -0,0 +1,81 @@
name: "Bug report"
description: Bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report, please **confirm that your issue is not a duplicate issue and not because of your operation or version issues**
感谢您花时间填写此错误报告,请**务必确认您的issue不是重复的且不是因为您的操作或版本问题**
- type: checkboxes
attributes:
label: Please make sure of the following things
description: |
You must check all the following, otherwise your issue may be closed directly. Or you can go to the [discussions](https://github.com/OpenListTeam/OpenList/discussions)
您必须勾选以下所有内容,否则您的issue可能会被直接关闭。或者您可以去[讨论区](https://github.com/OpenListTeam/OpenList/discussions)
options:
- label: |
I have read the [documentation](https://openlistteam.github.io/docs).
我已经阅读了[文档](https://openlistteam.github.io/docs)。
- label: |
I'm sure there are no duplicate issues or discussions.
我确定没有重复的issue或讨论。
- label: |
I'm sure it's due to `OpenList` and not something else(such as [Network](https://openlistteam.github.io/docs/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host) ,`Dependencies` or `Operational`).
我确定是`OpenList`的问题,而不是其他原因(例如[网络](https://openlistteam.github.io/docs/zh/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host),`依赖`或`操作`)。
- label: |
I'm sure this issue is not fixed in the latest version.
我确定这个问题在最新版本中没有被修复。
- type: input
id: version
attributes:
label: OpenList Version / OpenList 版本
description: |
What version of our software are you running? Do not use `latest` or `master` as an answer.
您使用的是哪个版本的软件?请不要使用`latest`或`master`作为答案。
placeholder: v3.xx.xx
validations:
required: true
- type: input
id: driver
attributes:
label: Driver used / 使用的存储驱动
description: |
What storage driver are you using?
您使用的是哪个存储驱动?
placeholder: "for example: Onedrive"
validations:
required: true
- type: textarea
id: bug-description
attributes:
label: Describe the bug / 问题描述
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction / 复现链接
description: |
Please provide a link to a repo that can reproduce the problem you ran into. Please be aware that your issue may be closed directly if you don't provide it.
请提供能复现此问题的链接,请知悉如果不提供它你的issue可能会被直接关闭。
validations:
required: true
- type: textarea
id: config
attributes:
label: Config / 配置
description: |
Please provide the configuration file of your `OpenList` application and take a screenshot of the relevant storage configuration. (hide privacy field)
请提供您的`OpenList`应用的配置文件,并截图相关存储配置。(隐藏隐私字段)
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs / 日志
description: |
Please copy and paste any relevant log output.
请复制粘贴错误日志,或者截图
+1 -10
View File
@@ -1,14 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: 问题和讨论
url: https://github.com/OpenListTeam/OpenList/discussions
about: 讨论、问题、想法等
- name: Questions & Discussions
url: https://github.com/OpenListTeam/OpenList/discussions
about: Discuss issues, ideas, etc.
- name: 即时聊天
url: https://t.me/OpenListTeam
about: 与我们聊天
- name: Chat
url: https://t.me/OpenListTeam
about: Chat with us
about: Use GitHub discussions for message-board style questions and discussions.
@@ -0,0 +1,33 @@
name: "Feature request"
description: Feature request
labels: [enhancement]
body:
- type: checkboxes
attributes:
label: Please make sure of the following things
description: You may select more than one, even select all.
options:
- label: I have read the [documentation](https://openlistteam.github.io/docs).
- label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure this feature is not implemented.
- label: I'm sure it's a reasonable and popular requirement.
- type: textarea
id: feature-description
attributes:
label: Description of the feature / 需求描述
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested solution / 实现思路
description: |
Solutions to achieve this requirement.
实现此需求的解决思路。
- type: textarea
id: additional-context
attributes:
label: Additional context / 附件
description: |
Any other context or screenshots about the feature request here, or information you find helpful.
相关的任何其他上下文或截图,或者你觉得有帮助的信息
-114
View File
@@ -1,114 +0,0 @@
<!--
PR title / PR 标题:
- Use Conventional Commits: `type(scope): summary`
- Allowed types: `feat`, `docs`, `fix`, `style`, `refactor`, `chore`
- Scope is required by the current PR title check.
- For breaking changes, add `!`: `feat(driver)!: change auth flow`
-->
## Summary / 摘要
<!--
Briefly describe what changed and why.
简要说明改了什么,以及为什么需要改。
-->
<!--
- List user-visible behavior changes.
- List important implementation changes.
- Mention config, storage, API, or compatibility changes if any.
- 列出用户可感知的行为变化。
- 列出重要实现变化。
- 如涉及配置、存储、API 或兼容性变化,请明确说明。
-->
- [ ] This PR has breaking changes.
/ 此 PR 包含破坏性变更。
- [ ] This PR changes public API, config, storage format, or migration behavior.
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
- [ ] This PR requires corresponding changes in related repositories.
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
- OpenList-Frontend:
- OpenList-Docs:
## Related Issues / 关联 Issue
<!--
Use `Closes #123`, `Fixes #123`, or `Relates to #123`.
Remove this section if not applicable.
使用 `Closes #123`、`Fixes #123` 或 `Relates to #123`。
不适用时请删除本节。
-->
## Testing / 测试
<!--
Describe commands, platforms, and manual checks.
If not tested, explain why.
说明执行过的命令、测试平台和手动验证。
如果未测试,请说明原因。
-->
- [ ] `go test ./...`
- [ ] Manual test / 手动测试:
## Checklist / 检查清单
- [ ] I have read [CONTRIBUTING](https://github.com/OpenListTeam/OpenList/blob/main/CONTRIBUTING.md).
/ 我已阅读 [CONTRIBUTING](https://github.com/OpenListTeam/OpenList/blob/main/CONTRIBUTING.md)。
- [ ] I confirm this contribution follows the repository license, contribution policy, and code of conduct.
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
- [ ] I have formatted the changed code with `gofmt`, `go fmt`, or `prettier` where applicable.
/ 我已按适用情况使用 `gofmt`、`go fmt` 或 `prettier` 格式化变更代码。
- [ ] I have requested review from relevant maintainers or code owners where applicable.
/ 我已在适用情况下请求相关维护者或代码所有者审查。
## AI Disclosure / AI 使用声明
<!--
Please disclose any substantial AI assistance used in this PR.
Minor AI assistance, such as typo fixes, autocomplete, formatting suggestions,
or wording polish, does not need to be disclosed.
Remove this section if not applicable.
请披露此 PR 中使用的重要 AI 辅助内容。
轻微 AI 辅助,例如拼写修正、自动补全、格式建议或文字润色,无需披露。
如不适用,请删除本节。
Deliberate non-disclosure may be treated as a trust and compliance issue.
故意隐瞒 AI 使用情况可能被视为信任与合规问题。
-->
- [ ] This PR includes AI-assisted content.
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
- [ ] ChatGPT
- [ ] Codex
- [ ] GitHub Copilot
- [ ] Claude
- [ ] Gemini
- [ ] Other (please specify) / 其他(请注明):
Usage scope / 使用范围:
- [ ] Code generation / 代码生成
- [ ] Refactoring / 重构
- [ ] Documentation / 文档
- [ ] Tests / 测试
- [ ] Translation / 翻译
- [ ] Review assistance / 审查辅助
- [ ] I have reviewed and validated all AI-assisted content included in this PR.
/ 我已审核并验证此 PR 中的所有 AI 辅助内容。
- [ ] I have ensured that all AI-assisted commits include `Co-Authored-By` attribution.
/ 我已确保所有 AI 辅助提交都包含 `Co-Authored-By` 归属信息。
- [ ] I can reproduce all AI-assisted content included in this PR without any AI tools.
/ 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。
+21
View File
@@ -0,0 +1,21 @@
# Configuration for welcome - https://github.com/behaviorbot/welcome
# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
# Comment to be posted to on first time issues
newIssueWelcomeComment: >
Thanks for opening your first issue here! Be sure to follow the issue template!
# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: >
Thanks for opening this pull request! Please check out our contributing guidelines.
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request! We here at behavior bot are proud of you!
# It is recommend to include as many gifs and emojis as possible
+21
View File
@@ -0,0 +1,21 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 44
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 20
# Issues with these labels will never be considered stale
exemptLabels:
- accepted
- security
- working
- pr-welcome
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue was closed due to inactive more than 52 days. You can reopen or
recreate it if you think it should continue. Thank you for your contributions again.
+67 -87
View File
@@ -1,8 +1,8 @@
name: Beta Release builds
name: beta release
on:
push:
branches: ["main"]
branches: [ 'main' ]
workflow_dispatch:
concurrency:
@@ -14,11 +14,15 @@ permissions:
jobs:
changelog:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Beta Release Changelog
runs-on: ubuntu-latest
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -40,18 +44,15 @@ jobs:
npx changelogithub --output CHANGELOG.md
- name: Upload assets to beta release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if ! gh release view beta; then
gh release create beta --prerelease --notes-file CHANGELOG.md
else
gh release edit beta --prerelease --notes-file CHANGELOG.md
fi
gh release upload beta "CHANGELOG.md" --clobber
uses: softprops/action-gh-release@v2
with:
body_path: CHANGELOG.md
files: CHANGELOG.md
prerelease: true
tag_name: beta
- name: Upload assets to github artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: beta changelog
path: ${{ github.workspace }}/CHANGELOG.md
@@ -64,102 +65,51 @@ jobs:
strategy:
matrix:
include:
- target: "!(*musl*|*windows-arm64*|*windows7-*|*android*|*freebsd*)" # xgo and loongarch (exclude mips64le)
- target: '!(*musl*|*windows-arm64*|*android*|*freebsd*)' # xgo
hash: "md5"
flags: ""
goflags: ""
- target: "linux-(mips|mips64|mipsle|mips64le|loong64)-musl*" # musl-compat-family
hash: "md5-linux-musl-mips"
flags: "-ldflags=-linkmode external -extldflags '-static -fpic'"
goflags: ""
musl_static: "true"
- target: "linux-!(arm*|mips|mips64|mipsle|mips64le|loong64)-musl*" # musl-not-arm (exclude compat-family)
- target: 'linux-!(arm*)-musl*' #musl-not-arm
hash: "md5-linux-musl"
flags: "-ldflags=-linkmode external -extldflags '-static -fpic'"
goflags: ""
musl_static: "true"
- target: "linux-arm*-musl*" #musl-arm
- target: 'linux-arm*-musl*' #musl-arm
hash: "md5-linux-musl-arm"
flags: "-ldflags=-linkmode external -extldflags '-static -fpic'"
goflags: ""
musl_static: "true"
- target: "windows-arm64" #win-arm64
- target: 'windows-arm64' #win-arm64
hash: "md5-windows-arm64"
flags: ""
goflags: ""
musl_static: "false"
- target: "windows7-*" #win7
hash: "md5-windows7"
flags: ""
goflags: "-tags=sqlite_cgo_compat"
musl_static: "false"
- target: "android-*" #android
- target: 'android-*' #android
hash: "md5-android"
flags: ""
goflags: ""
musl_static: "false"
- target: "freebsd-*" #freebsd
- target: 'freebsd-*' #freebsd
hash: "md5-freebsd"
flags: ""
goflags: ""
musl_static: "false"
name: Beta Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: "1.26.4"
go-version: '1.22'
- name: Setup web
run: bash build.sh dev web
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Build
uses: OpenListTeam/cgo-actions@v1.2.5
uses: OpenListTeam/cgo-actions@v1.1.2
with:
targets: ${{ matrix.target }}
flags: ${{ matrix.flags || '-ldflags=' }}
static-link-for-musl: true
musl-target-format: $os-$musl-$arch
github-token: ${{ secrets.GITHUB_TOKEN }}
out-dir: build
output: openlist-$target$ext
musl-base-url: "https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
x-flags: |
github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$built_at
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@openlist.team>
github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$git_commit
github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$tag
github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=rolling
env:
GOFLAGS: ${{ matrix.goflags }}
- name: Verify musl binaries are static
if: matrix.musl_static == 'true'
run: |
set -e
shopt -s nullglob
files=(build/openlist-*-musl-*)
if [ ${#files[@]} -eq 0 ]; then
echo "No musl binaries found"
exit 1
fi
for f in "${files[@]}"; do
if readelf -l "$f" | grep -q "Requesting program interpreter"; then
echo "Dynamic binary detected: $f"
readelf -l "$f" | grep "Requesting program interpreter" || true
exit 1
fi
done
github.com/OpenListTeam/OpenList/internal/conf.BuiltAt=$built_at
github.com/OpenListTeam/OpenList/internal/conf.GitAuthor=OpenList
github.com/OpenListTeam/OpenList/internal/conf.GitCommit=$git_commit
github.com/OpenListTeam/OpenList/internal/conf.Version=$tag
github.com/OpenListTeam/OpenList/internal/conf.WebVersion=dev
- name: Compress
run: |
@@ -167,13 +117,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# See above
- name: Upload assets to beta release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for file in build/compress/*; do
gh release upload beta "$file" --clobber
done
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
prerelease: true
tag_name: beta
- name: Clean illegal characters from matrix.target
id: clean_target_name
@@ -185,9 +135,39 @@ jobs:
echo "cleaned_target=$CLEANED_TARGET" >> $GITHUB_ENV
- name: Upload assets to github artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: beta builds for ${{ env.cleaned_target }}
path: ${{ github.workspace }}/build/compress/*
compression-level: 0
if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn`
# TODO: We do not have desktop clients right now. We may need a better way to
# trigger the build of desktop client when we actually have it.
# desktop:
# needs:
# - release
# name: Beta Release Desktop
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repo
# uses: actions/checkout@v4
# with:
# repository: openlistteam/desktop-release
# ref: main
# persist-credentials: false
# fetch-depth: 0
# - name: Commit
# run: |
# git config --local user.email "bot@nn.ci"
# git config --local user.name "IlaBot"
# git commit --allow-empty -m "Trigger build for ${{ github.sha }}"
# - name: Push commit
# uses: ad-m/github-push-action@master
# with:
# github_token: ${{ secrets.MY_TOKEN }}
# branch: main
# repository: openlistteam/desktop-release
+21 -32
View File
@@ -1,8 +1,10 @@
name: Test Build
name: build
on:
push:
branches: [ 'main' ]
pull_request:
branches: ["main"]
branches: [ 'main' ]
workflow_dispatch:
concurrency:
@@ -13,7 +15,8 @@ jobs:
build:
strategy:
matrix:
target:
platform: [ubuntu-latest]
target:
- darwin-amd64
- darwin-arm64
- windows-amd64
@@ -21,55 +24,41 @@ jobs:
- linux-amd64-musl
- windows-arm64
- android-arm64
name: Build ${{ matrix.target }}
runs-on: ubuntu-latest
name: Build
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
id: short-sha
- name: Setup Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: "1.26.4"
go-version: '1.22'
- name: Setup web
run: bash build.sh dev web
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Build
uses: OpenListTeam/cgo-actions@v1.2.5
uses: OpenListTeam/cgo-actions@v1.1.2
with:
targets: ${{ matrix.target }}
flags: ${{ contains(matrix.target, '-musl') && '-ldflags=-linkmode external -extldflags ''-static -fpic''' || '-ldflags=' }}
static-link-for-musl: true
musl-target-format: $os-$musl-$arch
github-token: ${{ secrets.GITHUB_TOKEN }}
out-dir: build
x-flags: |
github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$built_at
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@openlist.team>
github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$git_commit
github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$tag
github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=rolling
output: openlist$ext
- name: Verify musl binary is static
if: contains(matrix.target, '-musl')
run: |
set -e
if readelf -l build/openlist | grep -q "Requesting program interpreter"; then
echo "Dynamic binary detected: build/openlist"
readelf -l build/openlist | grep "Requesting program interpreter" || true
exit 1
fi
github.com/OpenListTeam/OpenList/internal/conf.BuiltAt=$built_at
github.com/OpenListTeam/OpenList/internal/conf.GitAuthor=OpenList
github.com/OpenListTeam/OpenList/internal/conf.GitCommit=$git_commit
github.com/OpenListTeam/OpenList/internal/conf.Version=$tag
github.com/OpenListTeam/OpenList/internal/conf.WebVersion=dev
- name: Upload artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: openlist_${{ steps.short-sha.outputs.sha }}_${{ matrix.target }}
path: build/*
name: openlist_${{ env.SHA }}_${{ matrix.target }}
path: build/*
+3 -6
View File
@@ -1,20 +1,17 @@
name: Release Automatic changelog
name: auto changelog
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
changelog:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -24,4 +21,4 @@ jobs:
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
GITHUB_TOKEN: ${{secrets.MY_TOKEN}}
-215
View File
@@ -1,215 +0,0 @@
name: Container Registry Cleanup
permissions:
contents: read
packages: write
on:
workflow_dispatch:
inputs:
package:
description: 'The name of the package to clean up'
required: true
default: 'openlist-git'
owner:
description: 'The owner of the package (user or organization)'
required: false
default: 'openlistteam'
older_than:
description: 'Delete untagged images older than this many days'
required: false
default: '-1'
untagged_timestamp_tolerance:
description: 'Tolerance in milliseconds for untagged images close to tagged ones'
required: false
# We do this because multi-arch docker containers will be pushed as separate untagged images
# to the container registry and GitHub cannot recognize them as part of a tagged image.
# Setting this option large enough prevents accidental deletion of one of these images.
# Here we set it to 10 seconds
default: '10000'
token:
description: 'GitHub token with permissions to delete packages (leave empty to use default GITHUB_TOKEN)'
required: false
default: ''
jobs:
cleanup:
runs-on: ubuntu-slim
steps:
- name: Check inputs and tools
id: setup
shell: bash
run: |
PACKAGE="${{ github.event.inputs.package || 'openlist-git' }}"
OLDER="${{ github.event.inputs.older_than || '-1' }}"
TOLERANCE_VAL="${{ github.event.inputs.untagged_timestamp_tolerance || '10000' }}"
TOKEN_INPUT="${{ github.event.inputs.token || '' }}"
OWNER="${{ github.event.inputs.owner || 'openlistteam' }}"
echo "package_name=$PACKAGE" >> "$GITHUB_OUTPUT"
echo "older_than=$OLDER" >> "$GITHUB_OUTPUT"
echo "tolerance=$TOLERANCE_VAL" >> "$GITHUB_OUTPUT"
echo "owner=$OWNER" >> "$GITHUB_OUTPUT"
if [[ -z "$TOKEN_INPUT" ]]; then
echo "::notice:: No token provided, using GITHUB_TOKEN instead."
echo "token=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_OUTPUT"
else
echo "::add-mask::$TOKEN_INPUT"
echo "token=$TOKEN_INPUT" >> "$GITHUB_OUTPUT"
fi
if [[ -z "$PACKAGE" || -z "$OWNER" || ( -z "$TOKEN_INPUT" && -z "${{ secrets.GITHUB_TOKEN }}" ) ]]; then
echo "::error:: Missing required inputs (package, token, or owner context)."
exit 1
fi
command -v curl >/dev/null 2>&1 || { echo "::error::curl is required but not installed."; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "::error::jq is required but not installed."; exit 1; }
- name: Determine owner type (user or organization)
id: owner_type
shell: bash
run: |
OWNER="${{ steps.setup.outputs.owner }}"
TOKEN="${{ steps.setup.outputs.token }}"
TYPE=$(curl -s -H "Authorization: token $TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/users/${OWNER}" | jq -r '.type')
if [[ "$TYPE" == "Organization" ]]; then
OWNER_PATH="orgs"
else
OWNER_PATH="users"
fi
echo "owner_path=$OWNER_PATH" >> "$GITHUB_OUTPUT"
echo "Detected owner type: $TYPE (using /${OWNER_PATH}/)"
- name: Access all versions from GitHub
id: fetch
shell: bash
run: |
PACKAGE_NAME="${{ steps.setup.outputs.package_name }}"
OWNER="${{ steps.setup.outputs.owner }}"
TOKEN="${{ steps.setup.outputs.token }}"
OWNER_PATH="${{ steps.owner_type.outputs.owner_path }}"
BASE_URL="https://api.github.com/${OWNER_PATH}/${OWNER}/packages/container/${PACKAGE_NAME}/versions"
URL="${BASE_URL}?per_page=100"
TEMP_FILE="/tmp/all_versions.ndjson"
> "$TEMP_FILE"
PAGE=1
while [[ -n "$URL" && $PAGE -le 10 ]]; do
echo "Fetching: $URL"
curl -s -H "Authorization: token $TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
-D headers.txt \
"$URL" > page.json
jq -c '.[]' page.json >> "$TEMP_FILE"
URL=$(grep -i '^link:' headers.txt | grep -o '<[^>]*>;\s*rel="next"' | sed 's/<\(.*\)>.*/\1/' || true)
PAGE=$((PAGE + 1))
done
rm -f headers.txt page.json
echo "temp_file=$TEMP_FILE" >> "$GITHUB_OUTPUT"
- name: Filter untagged versions
id: filter
shell: bash
run: |
OLDER_THAN="${{ steps.setup.outputs.older_than }}"
TOLERANCE="${{ steps.setup.outputs.tolerance }}"
TEMP_FILE="${{ steps.fetch.outputs.temp_file }}"
TO_DELETE=$(jq -c -s \
--arg older_than "$OLDER_THAN" \
--arg tolerance "$TOLERANCE" \
'
def abs: if . < 0 then -. else . end;
def to_ms: sub("\\.[0-9]+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime * 1000;
. as $all |
($older_than | tonumber) as $older_than_val |
($tolerance | tonumber) as $tolerance_val |
now as $now |
(if $older_than_val > 0 then ($now - ($older_than_val * 86400)) * 1000 else 0 end) as $cutoff_ms |
[ $all[] | select((.metadata.container.tags // []) | length > 0) | .created_at | to_ms ] as $tagged_times |
$all[] |
select((.metadata.container.tags // []) | length == 0) |
. as $item |
(.created_at | to_ms) as $created_ms |
(if $older_than_val > 0 then $created_ms < $cutoff_ms else true end) as $pass_age |
(if $tolerance_val <= 0 then true
else [ $tagged_times[] | select(($created_ms - .) | abs < $tolerance_val) ] | length == 0
end) as $pass_tolerance |
select($pass_age and $pass_tolerance) |
{id, name}
' "$TEMP_FILE")
rm -f "$TEMP_FILE"
if [[ -z "$TO_DELETE" ]]; then
DELETE_COUNT=0
else
DELETE_COUNT=$(echo "$TO_DELETE" | wc -l | tr -d ' ')
fi
echo "Found $DELETE_COUNT untagged images no longer necessary"
{
echo 'to_delete<<EOF'
echo "$TO_DELETE"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
echo "delete_count=$DELETE_COUNT" >> "$GITHUB_OUTPUT"
- name: Delete untagged versions
id: delete
shell: bash
run: |
PACKAGE_NAME="${{ steps.setup.outputs.package_name }}"
OWNER="${{ steps.setup.outputs.owner }}"
TOKEN="${{ steps.setup.outputs.token }}"
OWNER_PATH="${{ steps.owner_type.outputs.owner_path }}"
DELETE_COUNT="${{ steps.filter.outputs.delete_count }}"
TO_DELETE='${{ steps.filter.outputs.to_delete }}'
if [[ "$DELETE_COUNT" -gt 0 && -n "$TO_DELETE" ]]; then
while IFS= read -r item; do
[[ -z "$item" ]] && continue
# $item 是完整的单行 JSON,如 {"id":123,"name":"sha256:..."}
ID=$(echo "$item" | jq -r '.id')
NAME=$(echo "$item" | jq -r '.name')
DELETE_URL="https://api.github.com/${OWNER_PATH}/${OWNER}/packages/container/${PACKAGE_NAME}/versions/${ID}"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token $TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"$DELETE_URL")
if [[ "$HTTP_CODE" == "204" ]]; then
echo "Deleted untagged container image '${NAME}'"
else
echo "::warning:: Failed to delete ${NAME} (ID: ${ID}). HTTP Status: ${HTTP_CODE}"
fi
done <<< "$TO_DELETE"
else
echo "No images to delete."
fi
echo "Cleanup completed."
@@ -0,0 +1,22 @@
name: Close need info
on:
schedule:
- cron: "0 0 */1 * *"
workflow_dispatch:
jobs:
close-need-info:
runs-on: ubuntu-latest
steps:
- name: close-issues
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
token: ${{ secrets.GITHUB_TOKEN }}
labels: 'question'
inactive-day: 3
close-reason: 'not_planned'
body: |
Hello @${{ github.event.issue.user.login }}, this issue was closed due to no activities in 3 days.
你好 @${{ github.event.issue.user.login }},此issue因超过3天未回复被关闭。
+21
View File
@@ -0,0 +1,21 @@
name: Close inactive
on:
schedule:
- cron: "0 0 */7 * *"
workflow_dispatch:
jobs:
close-inactive:
runs-on: ubuntu-latest
steps:
- name: close-issues
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
token: ${{ secrets.GITHUB_TOKEN }}
labels: 'stale'
inactive-day: 8
close-reason: 'not_planned'
body: |
Hello @${{ github.event.issue.user.login }}, this issue was closed due to inactive more than 52 days. You can reopen or recreate it if you think it should continue. Thank you for your contributions again.
+25
View File
@@ -0,0 +1,25 @@
name: Issue Duplicate
on:
issues:
types: [labeled]
jobs:
create-comment:
runs-on: ubuntu-latest
if: github.event.label.name == 'duplicate'
steps:
- name: Create comment
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hello @${{ github.event.issue.user.login }}, your issue is a duplicate and will be closed.
你好 @${{ github.event.issue.user.login }},你的issue是重复的,将被关闭。
- name: Close issue
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issue'
token: ${{ secrets.GITHUB_TOKEN }}
+25
View File
@@ -0,0 +1,25 @@
name: Issue Invalid
on:
issues:
types: [labeled]
jobs:
create-comment:
runs-on: ubuntu-latest
if: github.event.label.name == 'invalid'
steps:
- name: Create comment
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hello @${{ github.event.issue.user.login }}, your issue is invalid and will be closed.
你好 @${{ github.event.issue.user.login }},你的issue无效,将被关闭。
- name: Close issue
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issue'
token: ${{ secrets.GITHUB_TOKEN }}
+17
View File
@@ -0,0 +1,17 @@
name: Remove working label when issue closed
on:
issues:
types: [closed]
jobs:
rm-working:
runs-on: ubuntu-latest
steps:
- name: Remove working label
uses: actions-cool/issues-helper@v3
with:
actions: 'remove-labels'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: 'working,pr-welcome'
-101
View File
@@ -1,101 +0,0 @@
name: Issue or PR Auto Reply
on:
issues:
types: [opened]
pull_request:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
auto-reply:
runs-on: ubuntu-latest
if: github.event_name == 'issues'
steps:
- name: Check issue for unchecked tasks and reply
uses: actions/github-script@v7
with:
script: |
let comment = "";
const issueTitle = context.payload.issue.title || "";
const titleNotEdited = /(请修改标题|Please modify the title)/i.test(issueTitle);
if (titleNotEdited) {
comment = "⚠️ 请修改标题以更好地描述您的问题或需求,并删除示例提示。当前 Issue 将被自动关闭。如需继续提交,请创建新的 Issue。\n";
comment += "⚠️ Please modify the title to better describe your issue or request, and remove the example prompt. This issue will be automatically closed. If you wish to proceed, please create a new issue.\n";
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: comment
});
await github.rest.issues.update({
...context.repo,
issue_number: context.issue.number,
state: 'closed',
state_reason: 'not_planned',
labels: ['invalid']
});
return;
}
const issueBody = context.payload.issue.body || "";
const confirmHasRead = /- \[ \] (?!我没有阅读这个清单|I have not read these checkboxes)/.test(issueBody);
const confirmNotRead = /- \[[xX]\] (?:我没有阅读这个清单|I have not read these checkboxes)/.test(issueBody);
if (confirmNotRead) {
comment = "⚠️ 你的 Issue 不符合提交规则。请先阅读相关规范后再重新提交。当前 Issue 将被自动关闭。如需继续提交,请确认已了解规则后重新打开或创建新的 Issue。\n";
comment += "⚠️ Your issue does not comply with the submission rules. Please read the guidelines before submitting again. This issue will be automatically closed. If you wish to proceed, please confirm that you have reviewed the rules before reopening or creating a new issue.\n";
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: comment
});
await github.rest.issues.update({
...context.repo,
issue_number: context.issue.number,
state: 'closed',
state_reason: 'not_planned',
labels: ['invalid']
});
return;
}
if (confirmHasRead) {
comment = "感谢您联系OpenList。我们会尽快回复您。\n";
comment += "Thanks for contacting OpenList. We will reply to you as soon as possible.\n\n";
comment += "由于您提出的 Issue 中包含部分未确认的项目,为了更好地管理项目,在人工审核后可能会直接关闭此问题。\n";
comment += "如果您能确认并补充相关未确认项目的信息,欢迎随时重新提交。我们会及时关注并处理。感谢您的理解与支持!\n";
comment += "Since your issue contains some unchecked tasks, it may be closed after manual review.\n";
comment += "If you can confirm and provide information for the unchecked tasks, feel free to resubmit.\n";
comment += "We will pay attention and handle it in a timely manner.\n\n";
comment += "感谢您的理解与支持!\n";
comment += "Thank you for your understanding and support!\n";
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: comment
});
}
pr-title-check:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Check PR title for required prefix and comment
uses: actions/github-script@v7
with:
script: |
const title = context.payload.pull_request.title || "";
const ok = /^(feat|docs|fix|style|refactor|chore)\(.+?\)!?: /i.test(title);
if (!ok) {
let comment = "⚠️ PR 标题需以 `feat(): `, `docs(): `, `fix(): `, `style(): `, `refactor(): `, `chore(): ` 其中之一开头,例如:`feat(component): 新增功能`。\n";
comment += "⚠️ The PR title must start with `feat(): `, `docs(): `, `fix(): `, `style(): `, or `refactor(): `, `chore(): `. For example: `feat(component): add new feature`.\n\n";
comment += "如果跨多个组件,请使用主要组件作为前缀,并在标题中枚举、描述中说明。\n";
comment += "If it spans multiple components, use the main component as the prefix and enumerate in the title, describe in the body.\n\n";
comment += "如果是破坏性变更,请在类型后添加 `!`,例如 `feat(component)!: 破坏性变更`。\n";
comment += "For breaking changes, add `!` after the type, e.g., `feat(component)!: breaking change`.\n\n";
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: comment
});
}
+20
View File
@@ -0,0 +1,20 @@
name: Issue Question
on:
issues:
types: [labeled]
jobs:
create-comment:
runs-on: ubuntu-latest
if: github.event.label.name == 'question'
steps:
- name: Create comment
uses: actions-cool/issues-helper@v3.6.0
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hello @${{ github.event.issue.user.login }}, please input issue by template and add detail. Issues labeled by `question` will be closed if no activities in 3 days.
你好 @${{ github.event.issue.user.login }},请按照issue模板填写, 并详细说明问题/日志记录/复现步骤/复现链接/实现思路或提供更多信息等, 3天内未回复issue自动关闭。
+19
View File
@@ -0,0 +1,19 @@
name: Issues Similarity Analysis
on:
issues:
types: [opened, edited]
jobs:
similarity-analysis:
runs-on: ubuntu-latest
steps:
- name: analysis
uses: actions-cool/issues-similarity-analysis@v1
with:
filter-threshold: 0.5
comment-title: '### See'
comment-body: '${index}. ${similarity} #${number}'
show-footer: false
show-mentioned: true
since-days: 730
+13
View File
@@ -0,0 +1,13 @@
name: Translation Helper
on:
pull_request_target:
types: [opened]
issues:
types: [opened]
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions-cool/translation-helper@v1.2.0
+25
View File
@@ -0,0 +1,25 @@
name: Issue Wontfix
on:
issues:
types: [labeled]
jobs:
lock-issue:
runs-on: ubuntu-latest
if: github.event.label.name == 'wontfix'
steps:
- name: Create comment
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hello @${{ github.event.issue.user.login }}, this issue will not be worked on and will be closed.
你好 @${{ github.event.issue.user.login }},这不会被处理,将被关闭。
- name: Close issue
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issue'
token: ${{ secrets.GITHUB_TOKEN }}
+57 -36
View File
@@ -1,41 +1,29 @@
name: Release builds
name: release
on:
release:
types: [ published ]
permissions:
contents: write
permissions: write-all
jobs:
# Set release to prerelease first
prerelease:
name: Set Prerelease
runs-on: ubuntu-latest
steps:
- name: Prerelease
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
prerelease: true
# Main release job for all platforms
release:
needs: prerelease
strategy:
matrix:
build-type: [ 'standard', 'lite' ]
target-platform: [ '', 'android', 'freebsd', 'linux_musl', 'linux_musl_arm' ]
name: Release ${{ matrix.target-platform && format('{0} ', matrix.target-platform) || '' }}${{ matrix.build-type == 'lite' && 'Lite' || '' }}
runs-on: ubuntu-latest
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Free Disk Space (Ubuntu)
if: matrix.target-platform == ''
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
@@ -43,18 +31,24 @@ jobs:
docker-images: true
swap-storage: true
- name: Setup Go
uses: actions/setup-go@v6
- name: Prerelease
uses: irongut/EditRelease@v1.2.0
with:
go-version: '1.26.4'
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
prerelease: true
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
if: matrix.target-platform == ''
run: |
sudo snap install zig --classic --beta
docker pull crazymax/xgo:latest
@@ -63,15 +57,42 @@ jobs:
- name: Build
run: |
bash build.sh release ${{ matrix.build-type == 'lite' && 'lite' || '' }} ${{ matrix.target-platform }}
bash build.sh release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Upload assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for file in build/compress/*; do
gh release upload "${{ github.event.release.tag_name }}" "$file" --clobber
done
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
prerelease: false
# TODO: We do not have desktop clients right now. We may need a better way to
# trigger the build of desktop client when we actually have it.
# release_desktop:
# needs: release
# name: Release desktop
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repo
# uses: actions/checkout@v4
# with:
# repository: openlistteam/desktop-release
# ref: main
# persist-credentials: false
# fetch-depth: 0
# - name: Add tag
# run: |
# git config --local user.email "bot@nn.ci"
# git config --local user.name "IlaBot"
# version=$(wget -qO- -t1 -T2 "https://api.github.com/repos/openlistteam/openlist/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
# git tag -a $version -m "release $version"
# - name: Push tags
# uses: ad-m/github-push-action@master
# with:
# github_token: ${{ secrets.MY_TOKEN }}
# branch: main
# repository: openlistteam/desktop-release
+38
View File
@@ -0,0 +1,38 @@
name: release_android
on:
release:
types: [ published ]
permissions: write-all
jobs:
release_android:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release android
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
+27 -167
View File
@@ -1,20 +1,7 @@
name: Release builds (Docker)
name: release_docker
on:
workflow_dispatch:
inputs:
manual_tag:
description: 'Tag name (like v0.1.0). Required if as_latest is true.'
required: false
type: string
as_latest:
description: 'Tag as latest?'
required: true
default: 'false'
type: choice
options:
- 'true'
- 'false'
push:
tags:
- 'v*'
@@ -24,18 +11,18 @@ concurrency:
cancel-in-progress: true
env:
DOCKERHUB_ORG_NAME: ${{ vars.DOCKERHUB_ORG_NAME || 'openlistteam' }}
GHCR_ORG_NAME: ${{ vars.GHCR_ORG_NAME || 'openlistteam' }}
ORG_NAME: openlistteam
IMAGE_NAME: openlist-git
IMAGE_NAME_DOCKERHUB: openlist
REGISTRY: ghcr.io
ARTIFACT_NAME: 'binaries_docker_release'
ARTIFACT_NAME_LITE: 'binaries_docker_release_lite'
RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/ppc64le,linux/riscv64,linux/loong64' ### Temporarily disable Docker builds for linux/s390x architectures for unknown reasons.
IMAGE_PUSH: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
permissions:
packages: write
RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64'
IMAGE_PUSH: ${{ github.event_name == 'push' }}
IMAGE_IS_PROD: ${{ github.ref_type == 'tag' }}
IMAGE_TAGS_BETA: |
type=raw,value=beta,enable={{is_default_branch}}
permissions: write-all
jobs:
build_binary:
@@ -43,11 +30,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- uses: actions/setup-go@v6
- uses: actions/setup-go@v5
with:
go-version: '1.26'
go-version: 'stable'
- name: Cache Musl
id: cache-musl
@@ -62,14 +49,20 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build go binary (beta)
if: env.IMAGE_IS_PROD != 'true'
run: bash build.sh beta docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build go binary (release)
if: env.IMAGE_IS_PROD == 'true'
run: bash build.sh release docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
overwrite: true
@@ -78,46 +71,6 @@ jobs:
!build/*.tgz
!build/musl-libs/**
build_binary_lite:
name: Build Binaries for Docker Release (Lite)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.26.4'
- name: Cache Musl
id: cache-musl
uses: actions/cache@v4
with:
path: build/musl-libs
key: docker-musl-libs-v2
- name: Download Musl Library
if: steps.cache-musl.outputs.cache-hit != 'true'
run: bash build.sh prepare lite docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build go binary (release)
run: bash build.sh release lite docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: ${{ env.ARTIFACT_NAME_LITE }}
overwrite: true
path: |
build/
!build/*.tgz
!build/musl-libs/**
release_docker:
needs: build_binary
name: Release Docker image
@@ -127,26 +80,22 @@ jobs:
image: ["latest", "ffmpeg", "aria2", "aio"]
include:
- image: "latest"
base_image_tag: "base"
build_arg: ""
tag_favor: ""
- image: "ffmpeg"
base_image_tag: "ffmpeg"
build_arg: INSTALL_FFMPEG=true
tag_favor: "suffix=-ffmpeg,onlatest=true"
- image: "aria2"
base_image_tag: "aria2"
build_arg: INSTALL_ARIA2=true
tag_favor: "suffix=-aria2,onlatest=true"
- image: "aio"
base_image_tag: "aio"
build_arg: |
INSTALL_FFMPEG=true
INSTALL_ARIA2=true
tag_favor: "suffix=-aio,onlatest=true"
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
@@ -170,7 +119,7 @@ jobs:
if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_ORG_NAME_BACKUP || env.DOCKERHUB_ORG_NAME }}
username: ${{ env.ORG_NAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta
@@ -178,14 +127,11 @@ jobs:
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.GHCR_ORG_NAME }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}
tags: >
${{ github.event_name == 'workflow_dispatch'
&& format('type=raw,value={0}', github.event.inputs.manual_tag)
|| format('type=raw,value={0}', github.ref_name) }}
${{ env.REGISTRY }}/${{ env.ORG_NAME }}/${{ env.IMAGE_NAME }}
${{ env.ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}
tags: ${{ env.IMAGE_IS_PROD == 'true' && '' || env.IMAGE_TAGS_BETA }}
flavor: |
latest=${{ github.event_name == 'push' || github.event.inputs.as_latest == 'true' }}
${{ env.IMAGE_IS_PROD == 'true' && 'latest=true' || '' }}
${{ matrix.tag_favor }}
- name: Build and push
@@ -195,93 +141,7 @@ jobs:
context: .
file: Dockerfile.ci
push: ${{ env.IMAGE_PUSH == 'true' }}
build-args: |
BASE_IMAGE_TAG=${{ matrix.base_image_tag }}
${{ matrix.build_arg }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ env.RELEASE_PLATFORMS }}
release_docker_lite:
needs: build_binary_lite
name: Release Docker image (Lite)
runs-on: ubuntu-latest
strategy:
matrix:
image: ["latest", "ffmpeg", "aria2", "aio"]
include:
- image: "latest"
base_image_tag: "base"
build_arg: ""
tag_favor: "suffix=-lite,onlatest=true"
- image: "ffmpeg"
base_image_tag: "ffmpeg"
build_arg: INSTALL_FFMPEG=true
tag_favor: "suffix=-lite-ffmpeg,onlatest=true"
- image: "aria2"
base_image_tag: "aria2"
build_arg: INSTALL_ARIA2=true
tag_favor: "suffix=-lite-aria2,onlatest=true"
- image: "aio"
base_image_tag: "aio"
build_arg: |
INSTALL_FFMPEG=true
INSTALL_ARIA2=true
tag_favor: "suffix=-lite-aio,onlatest=true"
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME_LITE }}
path: 'build/'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub Container Registry
if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_ORG_NAME_BACKUP || env.DOCKERHUB_ORG_NAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.GHCR_ORG_NAME }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}
tags: >
${{ github.event_name == 'workflow_dispatch'
&& format('type=raw,value={0}', github.event.inputs.manual_tag)
|| format('type=raw,value={0}', github.ref_name) }}
flavor: |
latest=${{ github.event_name == 'push' || github.event.inputs.as_latest == 'true' }}
${{ matrix.tag_favor }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ci
push: ${{ env.IMAGE_PUSH == 'true' }}
build-args: |
BASE_IMAGE_TAG=${{ matrix.base_image_tag }}
${{ matrix.build_arg }}
build-args: ${{ matrix.build_arg }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ env.RELEASE_PLATFORMS }}
+38
View File
@@ -0,0 +1,38 @@
name: release_freebsd
on:
release:
types: [ published ]
permissions: write-all
jobs:
release_freebsd:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release freebsd
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
+36
View File
@@ -0,0 +1,36 @@
name: release_linux_musl
on:
release:
types: [ published ]
permissions: write-all
jobs:
release_linux_musl:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release linux_musl
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
@@ -0,0 +1,37 @@
name: release_linux_musl_arm
on:
release:
types: [ published ]
permissions: write-all
jobs:
release_linux_musl_arm:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release linux_musl_arm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
-38
View File
@@ -1,38 +0,0 @@
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@v6
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
+15 -22
View File
@@ -1,4 +1,4 @@
name: Beta Release (Docker)
name: test_docker
on:
workflow_dispatch:
@@ -14,13 +14,12 @@ concurrency:
cancel-in-progress: true
env:
DOCKERHUB_ORG_NAME: ${{ vars.DOCKERHUB_ORG_NAME || 'openlistteam' }}
GHCR_ORG_NAME: ${{ vars.GHCR_ORG_NAME || 'openlistteam' }}
ORG_NAME: openlistteam
IMAGE_NAME: openlist-git
IMAGE_NAME_DOCKERHUB: openlist
REGISTRY: ghcr.io
ARTIFACT_NAME: 'binaries_docker_release'
RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/ppc64le,linux/riscv64,linux/loong64' ### Temporarily disable Docker builds for linux/s390x architectures for unknown reasons.
RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64'
IMAGE_PUSH: ${{ github.event_name == 'push' }}
IMAGE_TAGS_BETA: |
type=ref,event=pr
@@ -28,15 +27,15 @@ env:
jobs:
build_binary:
name: Build Binaries for Docker Release (Beta)
name: Build Binaries for Docker Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- uses: actions/setup-go@v6
- uses: actions/setup-go@v5
with:
go-version: '1.26.4'
go-version: 'stable'
- name: Cache Musl
id: cache-musl
@@ -55,10 +54,9 @@ jobs:
run: bash build.sh beta docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
overwrite: true
@@ -69,35 +67,32 @@ jobs:
release_docker:
needs: build_binary
name: Release Docker image (Beta)
name: Release Docker image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
image: ["latest", "ffmpeg", "aria2", "aio"]
include:
- image: "latest"
base_image_tag: "base"
build_arg: ""
tag_favor: ""
- image: "ffmpeg"
base_image_tag: "ffmpeg"
build_arg: INSTALL_FFMPEG=true
tag_favor: "suffix=-ffmpeg,onlatest=true"
- image: "aria2"
base_image_tag: "aria2"
build_arg: INSTALL_ARIA2=true
tag_favor: "suffix=-aria2,onlatest=true"
- image: "aio"
base_image_tag: "aio"
build_arg: |
INSTALL_FFMPEG=true
INSTALL_ARIA2=true
tag_favor: "suffix=-aio,onlatest=true"
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
@@ -121,7 +116,7 @@ jobs:
if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_ORG_NAME_BACKUP || env.DOCKERHUB_ORG_NAME }}
username: ${{ env.ORG_NAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta
@@ -129,8 +124,8 @@ jobs:
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.GHCR_ORG_NAME }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}
${{ env.REGISTRY }}/${{ env.ORG_NAME }}/${{ env.IMAGE_NAME }}
${{ env.ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}
tags: ${{ env.IMAGE_TAGS_BETA }}
flavor: |
${{ matrix.tag_favor }}
@@ -142,9 +137,7 @@ jobs:
context: .
file: Dockerfile.ci
push: ${{ env.IMAGE_PUSH == 'true' }}
build-args: |
BASE_IMAGE_TAG=${{ matrix.base_image_tag }}
${{ matrix.build_arg }}
build-args: ${{ matrix.build_arg }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ env.RELEASE_PLATFORMS }}
@@ -1,40 +0,0 @@
name: Trigger OpenWRT Update
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to trigger update for'
required: true
type: string
jobs:
trigger-makefile-update:
runs-on: ubuntu-latest
steps:
- name: Trigger Makefile hash update
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }}
repository: ${{ vars.HOOK_REPO || 'OpenListTeam/OpenList-OpenWRT' }}
event-type: update-hashes
client-payload: |
{
"source_repository": "${{ github.repository }}",
"release_tag": "${{ inputs.tag || github.ref_name }}",
"release_name": "${{ inputs.tag || github.ref_name }}",
"release_url": "${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.tag || github.ref_name }}",
"triggered_by": "${{ github.actor }}",
"trigger_reason": "${{ github.event_name }}"
}
- name: Log trigger information
run: |
echo "🚀 Successfully triggered Makefile hash update"
echo "📦 Target repository: OpenListTeam/luci-app-openlist"
echo "🏷️ Tag: ${{ inputs.tag || github.ref_name }}"
echo "👤 Triggered by: ${{ github.actor }}"
echo "📅 Trigger time: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
-60
View File
@@ -1,60 +0,0 @@
# Agent Instructions
## Issues
Before creating an issue, review the available issue templates in the `.github` directory.
When drafting the issue:
- Use the most appropriate template.
- Follow the template structure.
- Fill in all required sections.
- Remove sections that the template explicitly marks as optional or not applicable.
- Do not invent reproduction steps, logs, screenshots, or expected behavior.
## Pull Requests
Before creating a pull request, read `.github/PULL_REQUEST_TEMPLATE.md`.
When drafting the pull request:
- Follow the template structure.
- Use the title format required by the template.
- Fill in or remove each section according to the template guidance.
- Include testing details, or explain why testing was not run.
- Do not invent testing results.
- Do not claim validation, verification, or review steps that were not actually performed.
## Automated Contributions
Fully automated contributions are not considered equivalent to normal community participation.
A contribution may be considered fully automated if it is submitted through an automated agent, or if the submitting account participates in project discussions through an automated agent, without meaningful human review or intervention.
When making this determination, maintainers may consider the overall behavior of the account, including but not limited to disclosed agent usage, interaction patterns, response characteristics, and other available evidence. No single factor is determinative.
Maintainers reserve the right to accept, reject, modify, or reimplement any contribution independently of any action taken against the submitting account. Acceptance of a contribution does not imply acceptance of the submitting account or its contribution method. If an account is determined to be primarily operated through automated processes, we may need to restrict its future participation in contributions until that determination is rescinded.
## Git Commits
When creating commits, follow the repository `git-commit` skill rules:
- Use Conventional Commits title format: `type(scope): subject`.
- Allowed types: `feat`, `fix`, `refactor`, `perf`, `docs`, `style`, `test`, `build`, `ci`, `chore`, `revert`.
- Use a meaningful scope based on the main module, package, or feature.
- Write the subject in imperative mood and describe the actual change.
- Use a concise Markdown list in the commit body, with each item describing one key change.
- Do not invent changes that are not present in the diff.
- Do not describe behavior, refactors, fixes, or tests that are not reflected in the commit.
Include at most one `Co-authored-by` trailer that matches the AI assistant actually used to produce the change.
Examples:
- `Co-authored-by: Codex <267193182+codex@users.noreply.github.com>`
- `Co-authored-by: GitHub Copilot <copilot@github.com>`
- `Co-authored-by: Claude <81847+claude@users.noreply.github.com>`
If you are not one of the listed assistants, do not add a `Co-authored-by` trailer.
Instead, ask the human collaborator to provide the exact `Co-authored-by` trailer to use. Do not invent, infer, or generate one yourself.
+4 -4
View File
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
@@ -116,7 +116,7 @@ the community.
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
@@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
<https://www.contributor-covenant.org/faq>. Translations are available at
<https://www.contributor-covenant.org/translations>.
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+72 -75
View File
@@ -2,109 +2,106 @@
## Setup your machine
`OpenList` is written in [Go](https://golang.org/) and [SolidJS](https://www.solidjs.com/).
`OpenList` is written in [Go](https://golang.org/) and [React](https://reactjs.org/).
Prerequisites:
- [git](https://git-scm.com)
- [Go](https://golang.org/doc/install) version declared in [`go.mod`](./go.mod)
- [Go 1.20+](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:
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>
$ git clone https://github.com/OpenListTeam/OpenList.git
$ git clone --recurse-submodules https://github.com/OpenListTeam/OpenList-Frontend.git
```
You should switch to the `main` branch for development.
## Preview your change
### backend
```shell
go run main.go
$ go run main.go
```
### frontend
```shell
pnpm dev
$ pnpm dev
```
## Add a new driver
Copy `drivers/template` folder and rename it, and follow the comments in it.
## Community and policies
By contributing, you agree to follow the repository's code of conduct and license terms.
- Code of conduct: [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
- License: [LICENSE](./LICENSE)
- Security issues: please report privately according to [SECURITY.md](./SECURITY.md)
If your contribution includes substantial AI-assisted content, disclose the tools used and the scope of assistance in the pull request.
## 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).
### Commit Message Format
Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
format that includes a **type**, a **scope** and a **subject**:
<https://github.com/OpenListTeam/OpenList/issues/376>
```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
It's suggested to sign your commits. See: [How to sign commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
The **header** is mandatory and the **scope** of the header is optional.
Any line of the commit message cannot be longer than 100 characters! This allows the message to be easier
to read on GitHub as well as in various git tools.
### Revert
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header
of the reverted commit.
In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit
being reverted.
### Type
Must be one of the following:
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **perf**: A code change that improves performance
* **test**: Adding missing or correcting existing tests
* **build**: Affects project builds or dependency modifications
* **revert**: Restore the previous commit
* **ci**: Continuous integration of related file modifications
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
* **release**: Release a new version
### Scope
The scope could be anything specifying place of the commit change. For example `$location`,
`$browser`, `$compile`, `$rootScope`, `ngHref`, `ngClick`, `ngView`, etc...
You can use `*` when the change affects more than a single scope.
### Subject
The subject contains succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end
### Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.
### Footer
The footer should contain any information about **Breaking Changes** and is also the place to
[reference GitHub issues that this commit closes](https://help.github.com/articles/closing-issues-via-commit-messages/).
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines.
The rest of the commit message is then used for this.
## 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.
## AI Disclosure
If your pull request includes substantial AI-assisted content, disclose it in the PR description.
The pull request description must follow the repository's pull request template.
Fully automated contributions are not considered equivalent to normal community participation.
A contribution may be considered fully automated if it is submitted through an automated agent, or if the submitting account participates in project discussions through an automated agent, without meaningful human review or intervention.
When making this determination, maintainers may consider the overall behavior of the account, including but not limited to disclosed agent usage, interaction patterns, response characteristics, and other available evidence. No single factor is determinative.
Maintainers reserve the right to accept, reject, modify, or reimplement any contribution independently of any action taken against the submitting account. Acceptance of a contribution does not imply acceptance of the submitting account or its contribution method. If an account is determined to be primarily operated through automated processes, we may need to restrict its future participation in contributions until that determination is rescinded.
Please include:
- Tools used, such as ChatGPT, GitHub Copilot, Claude, Cursor, or other AI tools.
- Usage scope, such as code generation, refactoring, documentation, tests, translation, or review assistance.
- Confirmation that you have reviewed and validated all AI-assisted content before submission.
- Confirmation that the submitted content complies with this repository's license and contribution policies.
Minor AI assistance, such as typo fixes, autocomplete, formatting suggestions, or wording polish, does not need to be disclosed.
---
Thank you for your contribution! Let's make OpenList better together!
Push your branch to your `openlist` fork and open a pull request against the
`main` branch.
+23 -17
View File
@@ -1,7 +1,4 @@
### Default image is base. You can add other support by modifying BASE_IMAGE_TAG. The following parameters are supported: base (default), aria2, ffmpeg, aio
ARG BASE_IMAGE_TAG=base
FROM alpine:edge AS builder
FROM docker.io/library/alpine:edge AS builder
LABEL stage=go-builder
WORKDIR /app/
RUN apk add --no-cache bash curl jq gcc git go musl-dev
@@ -10,27 +7,36 @@ RUN go mod download
COPY ./ ./
RUN bash build.sh release docker
FROM openlistteam/openlist-base-image:${BASE_IMAGE_TAG}
LABEL MAINTAINER="OpenList"
FROM alpine:edge
ARG INSTALL_FFMPEG=false
ARG INSTALL_ARIA2=false
ARG USER=openlist
ARG UID=1001
ARG GID=1001
LABEL MAINTAINER="OpenList"
WORKDIR /opt/openlist/
RUN addgroup -g ${GID} ${USER} && \
adduser -D -u ${UID} -G ${USER} ${USER} && \
mkdir -p /opt/openlist/data
RUN apk update && \
apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates su-exec tzdata; \
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \
[ "$INSTALL_ARIA2" = "true" ] && apk add --no-cache curl aria2 && \
mkdir -p /opt/aria2/.aria2 && \
wget https://github.com/P3TERX/aria2.conf/archive/refs/heads/master.tar.gz -O /tmp/aria-conf.tar.gz && \
tar -zxvf /tmp/aria-conf.tar.gz -C /opt/aria2/.aria2 --strip-components=1 && rm -f /tmp/aria-conf.tar.gz && \
sed -i 's|rpc-secret|#rpc-secret|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \
touch /opt/aria2/.aria2/aria2.session && \
/opt/aria2/.aria2/tracker.sh ; \
rm -rf /var/cache/apk/*
COPY --from=builder --chmod=755 --chown=${UID}:${GID} /app/bin/openlist ./
COPY --chmod=755 --chown=${UID}:${GID} entrypoint.sh /entrypoint.sh
USER ${USER}
COPY --chmod=755 --from=builder /app/bin/openlist ./
COPY --chmod=755 entrypoint.sh /entrypoint.sh
RUN /entrypoint.sh version
ENV UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
VOLUME /opt/openlist/data/
EXPOSE 5244 5245
CMD [ "/entrypoint.sh" ]
+22 -14
View File
@@ -1,26 +1,34 @@
ARG BASE_IMAGE_TAG=base
FROM ghcr.io/openlistteam/openlist-base-image:${BASE_IMAGE_TAG}
LABEL MAINTAINER="OpenList"
FROM docker.io/library/alpine:edge
ARG TARGETPLATFORM
ARG INSTALL_FFMPEG=false
ARG INSTALL_ARIA2=false
ARG USER=openlist
ARG UID=1001
ARG GID=1001
LABEL MAINTAINER="OpenList"
WORKDIR /opt/openlist/
RUN addgroup -g ${GID} ${USER} && \
adduser -D -u ${UID} -G ${USER} ${USER} && \
mkdir -p /opt/openlist/data
RUN apk update && \
apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates su-exec tzdata; \
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \
[ "$INSTALL_ARIA2" = "true" ] && apk add --no-cache curl aria2 && \
mkdir -p /opt/aria2/.aria2 && \
wget https://github.com/P3TERX/aria2.conf/archive/refs/heads/master.tar.gz -O /tmp/aria-conf.tar.gz && \
tar -zxvf /tmp/aria-conf.tar.gz -C /opt/aria2/.aria2 --strip-components=1 && rm -f /tmp/aria-conf.tar.gz && \
sed -i 's|rpc-secret|#rpc-secret|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \
touch /opt/aria2/.aria2/aria2.session && \
/opt/aria2/.aria2/tracker.sh ; \
rm -rf /var/cache/apk/*
COPY --chmod=755 --chown=${UID}:${GID} /build/${TARGETPLATFORM}/openlist ./
COPY --chmod=755 --chown=${UID}:${GID} entrypoint.sh /entrypoint.sh
USER ${USER}
COPY --chmod=755 /build/${TARGETPLATFORM}/openlist ./
COPY --chmod=755 entrypoint.sh /entrypoint.sh
RUN /entrypoint.sh version
ENV UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
VOLUME /opt/openlist/data/
EXPOSE 5244 5245
CMD [ "/entrypoint.sh" ]
+85 -132
View File
@@ -1,101 +1,79 @@
<div align="center">
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
<p><em>OpenList is a resilient, long-term governance, community-driven fork of AList — built to defend open source against trust-based attacks.</em></p>
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" /></a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" /></a>
<a href="https://github.com/OpenListTeam/OpenList/discussions"><img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" /></a>
<img width="100px" alt="logo" src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg"/></a>
<p><em>🗂️A file list program that supports multiple storages, powered by Gin and SolidJS, fork of AList.</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/OpenListTeam/OpenList/v3">
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" />
</a>
</div>
<div>
<a href="https://github.com/OpenListTeam/OpenList/discussions">
<img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
</div>
</div>
---
- English | [中文](./README/README_cn.md) | [日本語](./README/README_ja.md) | [Dutch](./README/README_nl.md) | [한국어](./README/README_ko.md) | [Deutsch](./README/README_de.md) | [Русский](./README/README_ru.md) | [Français](./README/README_fr.md) | [Español](./README/README_es.md) | [العربية](./README/README_ar.md)
> [!IMPORTANT]
>
> Drop-in replacement for AList with long-term governance, no hidden risks, and full transparency, built to defend open source against trust-based attacks.
>
> We sincerely thank the author [Xhofe](https://github.com/Xhofe) of the original project [AlistGo/alist](https://github.com/AlistGo/alist) and all other contributors.
>
> This fork is not yet stable, specific migration progress can be viewed in [OpenList Migration Work Summary](https://github.com/OpenListTeam/OpenList/issues/6).
- [Contributing](./CONTRIBUTING.md)
- [CODE OF CONDUCT](./CODE_OF_CONDUCT.md)
- [LICENSE](./LICENSE)
## Disclaimer
OpenList is an open-source project independently maintained by the OpenList Team, following the AGPL-3.0 license and committed to maintaining complete code openness and modification transparency.
We have noticed the emergence of some third-party projects in the community with names similar to this project, such as OpenListApp/OpenListApp, as well as some paid proprietary software using the same or similar naming. To avoid user confusion, we hereby declare:
- OpenList has no official association with any third-party derivative projects.
- All software, code, and services of this project are maintained by the OpenList Team and are freely available on GitHub.
- Project documentation and API services primarily rely on charitable resources provided by Cloudflare. There are currently no paid plans or commercial deployments, and the use of existing features does not involve any costs.
We respect the community's rights to free use and derivative development, but we also strongly urge downstream projects:
- Should not use the "OpenList" name for impersonation promotion or commercial gain;
- Must not distribute OpenList-based code in a closed-source manner or violate AGPL license terms.
To better maintain healthy ecosystem development, we recommend:
- Clearly indicate the project source and choose appropriate open-source licenses in accordance with the open-source spirit;
- If involving commercial use, please avoid using "OpenList" or any confusing naming as the project name;
- If you need to use materials located under OpenListTeam/Logo, you may modify and use them under compliance with the agreement.
Thank you for your support and understanding of the OpenList project.
English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE OF CONDUCT](./CODE_OF_CONDUCT.md)
## Features
- [x] Multiple storages
- [x] Local storage
- [x] [Aliyundrive](https://www.alipan.com)
- [x] OneDrive / Sharepoint ([Global](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [CN](https://portal.partner.microsoftonline.cn), DE, US)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com)
- [x] [123pan](https://www.123pan.com)
- [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)
- [x] [PikPak](https://www.mypikpak.com)
- [x] [S3](https://aws.amazon.com/s3)
- [x] [Seafile](https://seafile.com)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com)
- [x] [ILanzou](https://www.ilanzou.com)
- [x] [Google photo](https://photos.google.com)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com)
- [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- [x] [115](https://115.com)
- [X] [Cloudreve](https://cloudreve.org)
- [x] [Dropbox](https://www.dropbox.com)
- [x] [FeijiPan](https://www.feijipan.com)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [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] [DingTalk Docs](https://alidocs.dingtalk.com/)
- [x] Local storage
- [x] [Aliyundrive](https://www.alipan.com/)
- [x] OneDrive / Sharepoint ([global](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com/)
- [x] [123pan](https://www.123pan.com/)
- [x] FTP / SFTP
- [x] [PikPak](https://www.mypikpak.com/)
- [x] [S3](https://aws.amazon.com/s3/)
- [x] [Seafile](https://seafile.com/)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
- [x] [Mediatrack](https://www.mediatrack.cn/)
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com/)
- [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/)
- [x] [ILanzou](https://www.ilanzou.com/)
- [x] [Aliyundrive share](https://www.alipan.com/)
- [x] [Google photo](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com/)
- [x] SMB
- [x] [115](https://115.com/)
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] [FeijiPan](https://www.feijipan.com/)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [x] [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
@@ -106,8 +84,8 @@ Thank you for your support and understanding of the OpenList project.
- [x] Dark mode
- [x] I18n
- [x] Protected routes (password protection and authentication)
- [x] WebDAV
- [x] Docker Deploy
- [x] WebDav (waiting for detail documents)
- [ ] Docker Deploy (rebuilding)
- [x] Cloudflare Workers proxy
- [x] File/Folder package download
- [x] Web upload(Can allow visitors to upload), delete, mkdir, rename, move and copy
@@ -117,59 +95,34 @@ Thank you for your support and understanding of the OpenList project.
## Document
- 📘 [Docs](https://doc.oplist.org)
- 🌏 [CN Mirror](https://doc.oplist.org.cn)
- ⚖️ [Terms of Use](https://doc.oplist.org/terms)
- 🔒 [Privacy Policy](https://doc.oplist.org/privacy)
<https://docs.openlist.team>
## Demo
- 🌎 [Global Demo](https://demo.oplist.org)
- 🇨🇳 [CN Demo](https://demo.oplist.org.cn)
N/A (to be rebuilt)
## Discussion
Please refer to [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) for raising general questions, ***Issues* is for bug reports and feature requests only.**
## Sponsor
[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town "VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.")
## Donors
Thanks to the following donors for their generous support:
- [HisAtri](https://github.com/HisAtri)
- 爱发电用户_7jTh
- suka
## License
The `OpenList` is open-source software licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) license.
## Disclaimer
- This project is a free and open-source software designed to facilitate file sharing via net disks, primarily intended to support the downloading and learning of the Go programming language.
- Please comply with all applicable laws and regulations when using this software. Any form of misuse is strictly prohibited.
- The software is based on official SDKs or APIs without any modification, disruption, or interference with their behavior.
- It only performs HTTP 302 redirects or traffic forwarding, and does not intercept, store, or tamper with any user data.
- This project is not affiliated with any official platform or service provider.
- The software is provided "as is", without any warranties of any kind, either express or implied, including but not limited to warranties of merchantability or fitness for a particular purpose.
- The maintainers are not liable for any direct or indirect damages arising from the use of, or inability to use, this software.
- You are solely responsible for any risks associated with using this software, including but not limited to account bans or download speed limitations.
- This project is licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) License. Please see the [LICENSE](./LICENSE) file for details.
## Contact Us
- [@GitHub](https://github.com/OpenListTeam)
- [Telegram Group](https://t.me/OpenListTeam)
- [Telegram Channel](https://t.me/OpenListOfficial)
## Contributors
We sincerely thank the author [Xhofe](https://github.com/Xhofe) of the original project [AlistGo/alist](https://github.com/AlistGo/alist) and all other contributors.
Thanks goes to these wonderful people:
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
## License
The `OpenList` is open-source software licensed under the AGPL-3.0 license.
## Disclaimer
- This program is a free and open source project. It is designed to share files on the network disk, which is convenient for downloading and learning Golang. Please abide by relevant laws and regulations when using it, and do not abuse it;
- This program is implemented by calling the official sdk/interface, without destroying the official interface behavior;
- This program only does 302 redirect/traffic forwarding, and does not intercept, store, or tamper with any user data;
- Before using this program, you should understand and bear the corresponding risks, including but not limited to account ban, download speed limit, etc., which is none of this program's business;
- If there is any infringement, please contact [OpenListTeam](https://github.com/OpenListTeam), and it will be dealt with in time.
---
> [@GitHub](https://github.com/OpenListTeam) · [Telegram Group](https://t.me/OpenListTeam)
-136
View File
@@ -1,136 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
<p><em>OpenList هو تفرّع من AList يتميز بالمرونة والحوكمة طويلة الأمد ويقوده المجتمع، وقد بُني للدفاع عن البرمجيات مفتوحة المصدر ضد الهجمات القائمة على الثقة.</em></p>
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" /></a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" /></a>
<a href="https://github.com/OpenListTeam/OpenList/discussions"><img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" /></a>
</div>
---
- [English](../README.md) | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Dutch](./README_nl.md) | [한국어](./README_ko.md) | [Deutsch](./README_de.md) | [Русский](./README_ru.md) | [Français](./README_fr.md) | [Español](./README_es.md) | العربية
- [Contributing](../CONTRIBUTING.md)
- [CODE OF CONDUCT](../CODE_OF_CONDUCT.md)
- [LICENSE](../LICENSE)
## إخلاء المسؤولية
OpenList is an open-source project independently maintained by the OpenList Team under the AGPL-3.0 license, with a commitment to code openness and modification transparency.
OpenList has no official association with third-party derivative projects that use similar names, including OpenListApp/OpenListApp or paid proprietary software using the same or similar names.
- All software, code, and services of this project are maintained by the OpenList Team and are freely available on GitHub.
- Project documentation and API services primarily rely on charitable resources provided by Cloudflare. There are currently no paid plans or commercial deployments.
- Downstream projects should not use the "OpenList" name for impersonation or commercial confusion, and must not distribute OpenList-based code as closed source in violation of the AGPL license.
- This software is provided "as is" without warranties. Please comply with applicable laws and regulations when using it.
## الميزات
- [x] Multiple storages
- [x] Local storage
- [x] [Aliyundrive](https://www.alipan.com)
- [x] OneDrive / Sharepoint ([Global](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [CN](https://portal.partner.microsoftonline.cn), DE, US)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com)
- [x] [123pan](https://www.123pan.com)
- [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)
- [x] [PikPak](https://www.mypikpak.com)
- [x] [S3](https://aws.amazon.com/s3)
- [x] [Seafile](https://seafile.com)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com)
- [x] [ILanzou](https://www.ilanzou.com)
- [x] [Google photo](https://photos.google.com)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com)
- [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- [x] [115](https://115.com)
- [x] [Cloudreve](https://cloudreve.org)
- [x] [Dropbox](https://www.dropbox.com)
- [x] [FeijiPan](https://www.feijipan.com)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [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] [DingTalk Docs](https://alidocs.dingtalk.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
- [x] Video and audio preview, with lyrics and subtitles support
- [x] Office document preview (docx, pptx, xlsx, ...)
- [x] `README.md` preview rendering
- [x] File permalink copy and direct file download
- [x] Dark mode and i18n
- [x] Protected routes, WebDAV, Docker deploy, and Cloudflare Workers proxy
- [x] File/folder package download and web upload operations
- [x] Offline download, cross-storage copy, and multi-thread download acceleration
## الوثائق
- 📘 [Docs](https://doc.oplist.org)
- 🌏 [CN Mirror](https://doc.oplist.org.cn)
- ⚖️ [Terms of Use](https://doc.oplist.org/terms)
- 🔒 [Privacy Policy](https://doc.oplist.org/privacy)
## العرض التجريبي
- 🌎 [Global Demo](https://demo.oplist.org)
- 🇨🇳 [CN Demo](https://demo.oplist.org.cn)
## النقاش
Please refer to [Discussions](https://github.com/OpenListTeam/OpenList/discussions) for general questions; Issues are for bug reports and feature requests only.
## الراعي
[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town "VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.")
## المتبرعون
Thanks to the following donors for their generous support:
- [HisAtri](https://github.com/HisAtri)
- 爱发电用户_7jTh
- suka
## الترخيص
OpenList is open-source software licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) license.
## تواصل معنا
- [@GitHub](https://github.com/OpenListTeam)
- [Telegram Group](https://t.me/OpenListTeam)
- [Telegram Channel](https://t.me/OpenListOfficial)
## المساهمون
We sincerely thank [Xhofe](https://github.com/Xhofe), author of the original project [AlistGo/alist](https://github.com/AlistGo/alist), and all other contributors.
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
-174
View File
@@ -1,174 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
<p><em>OpenList 是一个有韧性、长期治理、社区驱动的 AList 分支,旨在防御基于信任的开源攻击。</em></p>
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" /></a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" /></a>
<a href="https://github.com/OpenListTeam/OpenList/discussions"><img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" /></a>
</div>
---
- [English](../README.md) | 中文 | [日本語](./README_ja.md) | [Dutch](./README_nl.md) | [한국어](./README_ko.md) | [Deutsch](./README_de.md) | [Русский](./README_ru.md) | [Français](./README_fr.md) | [Español](./README_es.md) | [العربية](./README_ar.md)
- [贡献指南](../CONTRIBUTING.md)
- [行为准则](../CODE_OF_CONDUCT.md)
- [许可证](../LICENSE)
## 免责声明
OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3.0 许可证,致力于保持完整的代码开放性和修改透明性。
我们注意到社区中出现了一些与本项目名称相似的第三方项目,如 OpenListApp/OpenListApp,以及部分采用相同或近似命名的收费专有软件。为避免用户误解,现声明如下:
- OpenList 与任何第三方衍生项目无官方关联。
- 本项目的全部软件、代码与服务由 OpenList 团队维护,可在 GitHub 免费获取。
- 项目文档与 API 服务均主要依托于 Cloudflare 提供的公益资源,目前无任何收费计划或商业部署,现有功能使用不涉及任何支出。
我们尊重社区的自由使用与衍生开发权利,但也强烈呼吁下游项目:
- 不应以“OpenList”名义进行冒名宣传或获取商业利益;
- 不得将基于 OpenList 的代码进行闭源分发或违反 AGPL 许可证条款。
为了更好地维护生态健康发展,我们建议:
- 明确注明项目来源,并以符合开源精神的方式选择适当的开源许可证;
- 如涉及商业用途,请避免使用“OpenList”或任何会产生混淆的方式作为项目名称;
- 若需使用本项目位于 OpenListTeam/Logo 下的素材,可在遵守协议的前提下进行修改后使用。
感谢您对 OpenList 项目的支持与理解。
## 功能
- [x] 多种存储
- [x] 本地存储
- [x] [阿里云盘](https://www.alipan.com)
- [x] OneDrive / Sharepoint ([国际版](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [中国](https://portal.partner.microsoftonline.cn), DE, US)
- [x] [天翼云盘](https://cloud.189.cn)(个人、家庭)
- [x] [GoogleDrive](https://drive.google.com)
- [x] [123云盘](https://www.123pan.com)
- [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)
- [x] [PikPak](https://www.mypikpak.com)
- [x] [S3](https://aws.amazon.com/s3)
- [x] [Seafile](https://seafile.com)
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([中国](https://www.teambition.com), [国际](https://us.teambition.com))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [分秒帧](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [和彩云](https://yun.139.com)(个人、家庭、群组)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [百度网盘](http://pan.baidu.com)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC网盘](https://drive.uc.cn)
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷网盘](https://pan.xunlei.com)
- [x] [蓝奏云](https://www.lanzou.com)
- [x] [蓝奏云优享版](https://www.ilanzou.com)
- [x] [Google 相册](https://photos.google.com)
- [x] [Mega.nz](https://mega.nz)
- [x] [百度相册](https://photo.baidu.com)
- [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- [x] [115](https://115.com)
- [x] [Cloudreve](https://cloudreve.org)
- [x] [Dropbox](https://www.dropbox.com)
- [x] [飞机盘](https://www.feijipan.com)
- [x] [多吉云](https://www.dogecloud.com/product/oss)
- [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] [钉钉文档](https://alidocs.dingtalk.com/)
- [x] 部署方便,开箱即用
- [x] 文件预览(PDF、markdown、代码、纯文本等)
- [x] 画廊模式下的图片预览
- [x] 视频和音频预览,支持歌词和字幕
- [x] Office 文档预览(docx、pptx、xlsx 等)
- [x] `README.md` 预览渲染
- [x] 文件永久链接复制和直接文件下载
- [x] 黑暗模式
- [x] 国际化
- [x] 受保护的路由(密码保护和认证)
- [x] WebDAV
- [x] Docker 部署
- [x] Cloudflare Workers 代理
- [x] 文件/文件夹打包下载
- [x] 网页上传(可允许访客上传)、删除、新建文件夹、重命名、移动和复制
- [x] 离线下载
- [x] 跨存储复制文件
- [x] 单文件多线程下载/流式加速
## 文档
- 📘 [文档](https://doc.oplist.org)
- 🌏 [中国镜像](https://doc.oplist.org.cn)
- ⚖️ [使用条款](https://doc.oplist.org/terms)
- 🔒 [隐私政策](https://doc.oplist.org/privacy)
## Demo
- 🌎 [全球 Demo](https://demo.oplist.org)
- 🇨🇳 [中国 Demo](https://demo.oplist.org.cn)
## 讨论
如有一般性问题请前往 [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) 讨论区,***Issues* 仅用于错误报告和功能请求。**
## 赞助者
[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town "VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.")
## 捐赠者
感谢以下捐赠者的慷慨支持:
- [HisAtri](https://github.com/HisAtri)
- 爱发电用户_7jTh
- suka
## 许可证
`OpenList` 是基于 [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) 许可证的开源软件。
## 免责声明
- 本项目为免费开源软件,旨在通过网盘便捷分享文件,主要用于 Go 语言的下载与学习。
- 使用本软件时请遵守相关法律法规,严禁任何形式的滥用。
- 本软件基于官方 SDK 或 API 实现,未对其行为进行任何修改、破坏或干扰。
- 仅进行 HTTP 302 跳转或流量转发,不拦截、存储或篡改任何用户数据。
- 本项目与任何官方平台或服务提供商无关。
- 本软件按“原样”提供,不附带任何明示或暗示的担保,包括但不限于适销性或特定用途的适用性。
- 维护者不对因使用或无法使用本软件而导致的任何直接或间接损失负责。
- 您需自行承担使用本软件的所有风险,包括但不限于账号被封、下载限速等。
- 本项目遵循 [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) 许可证,详情请参见 [LICENSE](../LICENSE) 文件。
## 联系我们
- [@GitHub](https://github.com/OpenListTeam)
- [Telegram 交流群](https://t.me/OpenListTeam)
- [Telegram 频道](https://t.me/OpenListOfficial)
## 贡献者
我们衷心感谢原项目 [AlistGo/alist](https://github.com/AlistGo/alist) 的作者 [Xhofe](https://github.com/Xhofe) 及所有其他贡献者。
感谢这些优秀的人:
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
-136
View File
@@ -1,136 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
<p><em>OpenList ist ein widerstandsfähiger, langfristig verwalteter und Community-getriebener Fork von AList, entwickelt zum Schutz von Open Source vor vertrauensbasierten Angriffen.</em></p>
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" /></a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" /></a>
<a href="https://github.com/OpenListTeam/OpenList/discussions"><img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" /></a>
</div>
---
- [English](../README.md) | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Dutch](./README_nl.md) | [한국어](./README_ko.md) | Deutsch | [Русский](./README_ru.md) | [Français](./README_fr.md) | [Español](./README_es.md) | [العربية](./README_ar.md)
- [Contributing](../CONTRIBUTING.md)
- [CODE OF CONDUCT](../CODE_OF_CONDUCT.md)
- [LICENSE](../LICENSE)
## Haftungsausschluss
OpenList is an open-source project independently maintained by the OpenList Team under the AGPL-3.0 license, with a commitment to code openness and modification transparency.
OpenList has no official association with third-party derivative projects that use similar names, including OpenListApp/OpenListApp or paid proprietary software using the same or similar names.
- All software, code, and services of this project are maintained by the OpenList Team and are freely available on GitHub.
- Project documentation and API services primarily rely on charitable resources provided by Cloudflare. There are currently no paid plans or commercial deployments.
- Downstream projects should not use the "OpenList" name for impersonation or commercial confusion, and must not distribute OpenList-based code as closed source in violation of the AGPL license.
- This software is provided "as is" without warranties. Please comply with applicable laws and regulations when using it.
## Funktionen
- [x] Multiple storages
- [x] Local storage
- [x] [Aliyundrive](https://www.alipan.com)
- [x] OneDrive / Sharepoint ([Global](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [CN](https://portal.partner.microsoftonline.cn), DE, US)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com)
- [x] [123pan](https://www.123pan.com)
- [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)
- [x] [PikPak](https://www.mypikpak.com)
- [x] [S3](https://aws.amazon.com/s3)
- [x] [Seafile](https://seafile.com)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com)
- [x] [ILanzou](https://www.ilanzou.com)
- [x] [Google photo](https://photos.google.com)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com)
- [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- [x] [115](https://115.com)
- [x] [Cloudreve](https://cloudreve.org)
- [x] [Dropbox](https://www.dropbox.com)
- [x] [FeijiPan](https://www.feijipan.com)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [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] [DingTalk Docs](https://alidocs.dingtalk.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
- [x] Video and audio preview, with lyrics and subtitles support
- [x] Office document preview (docx, pptx, xlsx, ...)
- [x] `README.md` preview rendering
- [x] File permalink copy and direct file download
- [x] Dark mode and i18n
- [x] Protected routes, WebDAV, Docker deploy, and Cloudflare Workers proxy
- [x] File/folder package download and web upload operations
- [x] Offline download, cross-storage copy, and multi-thread download acceleration
## Dokumentation
- 📘 [Docs](https://doc.oplist.org)
- 🌏 [CN Mirror](https://doc.oplist.org.cn)
- ⚖️ [Terms of Use](https://doc.oplist.org/terms)
- 🔒 [Privacy Policy](https://doc.oplist.org/privacy)
## Demo
- 🌎 [Global Demo](https://demo.oplist.org)
- 🇨🇳 [CN Demo](https://demo.oplist.org.cn)
## Diskussion
Please refer to [Discussions](https://github.com/OpenListTeam/OpenList/discussions) for general questions; Issues are for bug reports and feature requests only.
## Sponsor
[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town "VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.")
## Spender
Thanks to the following donors for their generous support:
- [HisAtri](https://github.com/HisAtri)
- 爱发电用户_7jTh
- suka
## Lizenz
OpenList is open-source software licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) license.
## Kontakt
- [@GitHub](https://github.com/OpenListTeam)
- [Telegram Group](https://t.me/OpenListTeam)
- [Telegram Channel](https://t.me/OpenListOfficial)
## Mitwirkende
We sincerely thank [Xhofe](https://github.com/Xhofe), author of the original project [AlistGo/alist](https://github.com/AlistGo/alist), and all other contributors.
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
-136
View File
@@ -1,136 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
<p><em>OpenList es un fork de AList resiliente, con gobernanza a largo plazo e impulsado por la comunidad, creado para defender el código abierto frente a ataques basados en la confianza.</em></p>
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" /></a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" /></a>
<a href="https://github.com/OpenListTeam/OpenList/discussions"><img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" /></a>
</div>
---
- [English](../README.md) | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Dutch](./README_nl.md) | [한국어](./README_ko.md) | [Deutsch](./README_de.md) | [Русский](./README_ru.md) | [Français](./README_fr.md) | Español | [العربية](./README_ar.md)
- [Contributing](../CONTRIBUTING.md)
- [CODE OF CONDUCT](../CODE_OF_CONDUCT.md)
- [LICENSE](../LICENSE)
## Aviso legal
OpenList is an open-source project independently maintained by the OpenList Team under the AGPL-3.0 license, with a commitment to code openness and modification transparency.
OpenList has no official association with third-party derivative projects that use similar names, including OpenListApp/OpenListApp or paid proprietary software using the same or similar names.
- All software, code, and services of this project are maintained by the OpenList Team and are freely available on GitHub.
- Project documentation and API services primarily rely on charitable resources provided by Cloudflare. There are currently no paid plans or commercial deployments.
- Downstream projects should not use the "OpenList" name for impersonation or commercial confusion, and must not distribute OpenList-based code as closed source in violation of the AGPL license.
- This software is provided "as is" without warranties. Please comply with applicable laws and regulations when using it.
## Características
- [x] Multiple storages
- [x] Local storage
- [x] [Aliyundrive](https://www.alipan.com)
- [x] OneDrive / Sharepoint ([Global](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [CN](https://portal.partner.microsoftonline.cn), DE, US)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com)
- [x] [123pan](https://www.123pan.com)
- [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)
- [x] [PikPak](https://www.mypikpak.com)
- [x] [S3](https://aws.amazon.com/s3)
- [x] [Seafile](https://seafile.com)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com)
- [x] [ILanzou](https://www.ilanzou.com)
- [x] [Google photo](https://photos.google.com)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com)
- [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- [x] [115](https://115.com)
- [x] [Cloudreve](https://cloudreve.org)
- [x] [Dropbox](https://www.dropbox.com)
- [x] [FeijiPan](https://www.feijipan.com)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [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] [DingTalk Docs](https://alidocs.dingtalk.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
- [x] Video and audio preview, with lyrics and subtitles support
- [x] Office document preview (docx, pptx, xlsx, ...)
- [x] `README.md` preview rendering
- [x] File permalink copy and direct file download
- [x] Dark mode and i18n
- [x] Protected routes, WebDAV, Docker deploy, and Cloudflare Workers proxy
- [x] File/folder package download and web upload operations
- [x] Offline download, cross-storage copy, and multi-thread download acceleration
## Documentación
- 📘 [Docs](https://doc.oplist.org)
- 🌏 [CN Mirror](https://doc.oplist.org.cn)
- ⚖️ [Terms of Use](https://doc.oplist.org/terms)
- 🔒 [Privacy Policy](https://doc.oplist.org/privacy)
## Demo
- 🌎 [Global Demo](https://demo.oplist.org)
- 🇨🇳 [CN Demo](https://demo.oplist.org.cn)
## Discusión
Please refer to [Discussions](https://github.com/OpenListTeam/OpenList/discussions) for general questions; Issues are for bug reports and feature requests only.
## Patrocinador
[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town "VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.")
## Donantes
Thanks to the following donors for their generous support:
- [HisAtri](https://github.com/HisAtri)
- 爱发电用户_7jTh
- suka
## Licencia
OpenList is open-source software licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) license.
## Contacto
- [@GitHub](https://github.com/OpenListTeam)
- [Telegram Group](https://t.me/OpenListTeam)
- [Telegram Channel](https://t.me/OpenListOfficial)
## Colaboradores
We sincerely thank [Xhofe](https://github.com/Xhofe), author of the original project [AlistGo/alist](https://github.com/AlistGo/alist), and all other contributors.
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
-136
View File
@@ -1,136 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
<p><em>OpenList est un fork d’AList résilient, gouverné sur le long terme et porté par la communauté, conçu pour protéger l’open source contre les attaques fondées sur la confiance.</em></p>
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" /></a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" /></a>
<a href="https://github.com/OpenListTeam/OpenList/discussions"><img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" /></a>
</div>
---
- [English](../README.md) | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Dutch](./README_nl.md) | [한국어](./README_ko.md) | [Deutsch](./README_de.md) | [Русский](./README_ru.md) | Français | [Español](./README_es.md) | [العربية](./README_ar.md)
- [Contributing](../CONTRIBUTING.md)
- [CODE OF CONDUCT](../CODE_OF_CONDUCT.md)
- [LICENSE](../LICENSE)
## Avertissement
OpenList is an open-source project independently maintained by the OpenList Team under the AGPL-3.0 license, with a commitment to code openness and modification transparency.
OpenList has no official association with third-party derivative projects that use similar names, including OpenListApp/OpenListApp or paid proprietary software using the same or similar names.
- All software, code, and services of this project are maintained by the OpenList Team and are freely available on GitHub.
- Project documentation and API services primarily rely on charitable resources provided by Cloudflare. There are currently no paid plans or commercial deployments.
- Downstream projects should not use the "OpenList" name for impersonation or commercial confusion, and must not distribute OpenList-based code as closed source in violation of the AGPL license.
- This software is provided "as is" without warranties. Please comply with applicable laws and regulations when using it.
## Fonctionnalités
- [x] Multiple storages
- [x] Local storage
- [x] [Aliyundrive](https://www.alipan.com)
- [x] OneDrive / Sharepoint ([Global](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [CN](https://portal.partner.microsoftonline.cn), DE, US)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com)
- [x] [123pan](https://www.123pan.com)
- [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)
- [x] [PikPak](https://www.mypikpak.com)
- [x] [S3](https://aws.amazon.com/s3)
- [x] [Seafile](https://seafile.com)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com)
- [x] [ILanzou](https://www.ilanzou.com)
- [x] [Google photo](https://photos.google.com)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com)
- [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- [x] [115](https://115.com)
- [x] [Cloudreve](https://cloudreve.org)
- [x] [Dropbox](https://www.dropbox.com)
- [x] [FeijiPan](https://www.feijipan.com)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [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] [DingTalk Docs](https://alidocs.dingtalk.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
- [x] Video and audio preview, with lyrics and subtitles support
- [x] Office document preview (docx, pptx, xlsx, ...)
- [x] `README.md` preview rendering
- [x] File permalink copy and direct file download
- [x] Dark mode and i18n
- [x] Protected routes, WebDAV, Docker deploy, and Cloudflare Workers proxy
- [x] File/folder package download and web upload operations
- [x] Offline download, cross-storage copy, and multi-thread download acceleration
## Documentation
- 📘 [Docs](https://doc.oplist.org)
- 🌏 [CN Mirror](https://doc.oplist.org.cn)
- ⚖️ [Terms of Use](https://doc.oplist.org/terms)
- 🔒 [Privacy Policy](https://doc.oplist.org/privacy)
## Démo
- 🌎 [Global Demo](https://demo.oplist.org)
- 🇨🇳 [CN Demo](https://demo.oplist.org.cn)
## Discussion
Please refer to [Discussions](https://github.com/OpenListTeam/OpenList/discussions) for general questions; Issues are for bug reports and feature requests only.
## Sponsor
[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town "VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.")
## Donateurs
Thanks to the following donors for their generous support:
- [HisAtri](https://github.com/HisAtri)
- 爱发电用户_7jTh
- suka
## Licence
OpenList is open-source software licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) license.
## Nous contacter
- [@GitHub](https://github.com/OpenListTeam)
- [Telegram Group](https://t.me/OpenListTeam)
- [Telegram Channel](https://t.me/OpenListOfficial)
## Contributeurs
We sincerely thank [Xhofe](https://github.com/Xhofe), author of the original project [AlistGo/alist](https://github.com/AlistGo/alist), and all other contributors.
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
-174
View File
@@ -1,174 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
<p><em>OpenList は、信頼ベースの攻撃からオープンソースを守るために構築された、レジリエントで長期ガバナンス、コミュニティ主導の AList フォークです。</em></p>
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" /></a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" /></a>
<a href="https://github.com/OpenListTeam/OpenList/discussions"><img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" /></a>
</div>
---
- [English](../README.md) | [中文](./README_cn.md) | 日本語 | [Dutch](./README_nl.md) | [한국어](./README_ko.md) | [Deutsch](./README_de.md) | [Русский](./README_ru.md) | [Français](./README_fr.md) | [Español](./README_es.md) | [العربية](./README_ar.md)
- [コントリビュート](../CONTRIBUTING.md)
- [行動規範](../CODE_OF_CONDUCT.md)
- [ライセンス](../LICENSE)
## 免責事項
OpenListは、OpenListチームが独立して維持するオープンソースプロジェクトであり、AGPL-3.0ライセンスに従い、完全なコードの開放性と変更の透明性を維持することに専念しています。
コミュニティ内で、OpenListApp/OpenListAppなど、本プロジェクトと類似した名称を持つサードパーティプロジェクトや、同一または類似した命名を採用する有料専有ソフトウェアが出現していることを確認しています。ユーザーの誤解を避けるため、以下のように宣言いたします:
- OpenListは、いかなるサードパーティ派生プロジェクトとも公式な関連性はありません。
- 本プロジェクトのすべてのソフトウェア、コード、サービスはOpenListチームによって維持され、GitHubで無料で取得できます。
- プロジェクトドキュメントとAPIサービスは主にCloudflareが提供する公益リソースに依存しており、現在有料プランや商業展開はなく、既存機能の使用に費用は発生しません。
私たちはコミュニティの自由な使用と派生開発の権利を尊重しますが、下流プロジェクトに強く呼びかけます:
- 「OpenList」の名前で偽装宣伝や商業利益を得るべきではありません;
- OpenListベースのコードをクローズドソースで配布したり、AGPLライセンス条項に違反してはいけません。
エコシステムの健全な発展をより良く維持するため、以下を推奨します:
- プロジェクトの出典を明確に示し、オープンソース精神に合致する適切なオープンソースライセンスを選択する;
- 商業用途が関わる場合は、「OpenList」や混乱を招く可能性のある名前をプロジェクト名として使用することを避ける;
- OpenListTeam/Logo下の素材を使用する必要がある場合は、協定を遵守した上で修正して使用できます。
OpenListプロジェクトへのご支援とご理解をありがとうございます。
## 特徴
- [x] 複数ストレージ
- [x] ローカルストレージ
- [x] [Aliyundrive](https://www.alipan.com)
- [x] OneDrive / Sharepoint ([グローバル](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [中国](https://portal.partner.microsoftonline.cn), DE, US)
- [x] [189cloud](https://cloud.189.cn)(個人、家族)
- [x] [GoogleDrive](https://drive.google.com)
- [x] [123pan](https://www.123pan.com)
- [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)
- [x] [PikPak](https://www.mypikpak.com)
- [x] [S3](https://aws.amazon.com/s3)
- [x] [Seafile](https://seafile.com)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([中国](https://www.teambition.com), [国際](https://us.teambition.com))
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com)(個人、家族、グループ)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com)
- [x] [ILanzou](https://www.ilanzou.com)
- [x] [Google photo](https://photos.google.com)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com)
- [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- [x] [115](https://115.com)
- [x] [Cloudreve](https://cloudreve.org)
- [x] [Dropbox](https://www.dropbox.com)
- [x] [FeijiPan](https://www.feijipan.com)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [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] [DingTalk ドキュメント](https://alidocs.dingtalk.com/)
- [x] [MediaFire](https://www.mediafire.com)
- [x] 簡単にデプロイでき、すぐに使える
- [x] ファイルプレビュー(PDF、markdown、コード、テキストなど)
- [x] ギャラリーモードでの画像プレビュー
- [x] ビデオ・オーディオプレビュー、歌詞・字幕対応
- [x] Officeドキュメントプレビュー(docx、pptx、xlsxなど)
- [x] `README.md` プレビュー表示
- [x] ファイルのパーマリンクコピーと直接ダウンロード
- [x] ダークモード
- [x] 国際化対応
- [x] 保護されたルート(パスワード保護と認証)
- [x] WebDAV
- [x] Dockerデプロイ
- [x] Cloudflare Workersプロキシ
- [x] ファイル/フォルダのパッケージダウンロード
- [x] Webアップロード(訪問者のアップロード許可可)、削除、フォルダ作成、リネーム、移動、コピー
- [x] オフラインダウンロード
- [x] ストレージ間のファイルコピー
- [x] 単一ファイルのマルチスレッドダウンロード/ストリーム加速
## ドキュメント
- 📘 [ドキュメント](https://doc.oplist.org)
- 🌏 [中国ミラー](https://doc.oplist.org.cn)
- ⚖️ [利用規約](https://doc.oplist.org/terms)
- 🔒 [プライバシーポリシー](https://doc.oplist.org/privacy)
## Demo
- 🌎 [グローバル Demo](https://demo.oplist.org)
- 🇨🇳 [中国 Demo](https://demo.oplist.org.cn)
## ディスカッション
一般的な質問は [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) をご利用ください。***Issues* はバグ報告と機能リクエスト専用です。**
## スポンサー
[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town "VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.")
## 寄付者
以下の方々の寛大なご支援に感謝いたします:
- [HisAtri](https://github.com/HisAtri)
- 爱发电用户_7jTh
- suka
## ライセンス
「OpenList」は [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) ライセンスの下で公開されているオープンソースソフトウェアです。
## 免責事項
- 本プロジェクトは無料のオープンソースソフトウェアであり、ネットワークディスクを通じたファイル共有を容易にすることを目的とし、主に Go 言語のダウンロードと学習をサポートします。
- 本ソフトウェアの利用にあたっては、関連する法令を遵守し、不正利用を固く禁じます。
- 本ソフトウェアは公式 SDK または API に基づいており、その動作を一切改変・破壊・妨害しません。
- 302 リダイレクトまたはトラフィック転送のみを行い、ユーザーデータの傍受・保存・改ざんは一切行いません。
- 本プロジェクトは、いかなる公式プラットフォームやサービスプロバイダーとも関係ありません。
- 本ソフトウェアは「現状有姿」で提供されており、商品性や特定目的への適合性を含むいかなる保証もありません。
- 本ソフトウェアの使用または使用不能によるいかなる直接的・間接的損害についても、メンテナは責任を負いません。
- 本ソフトウェアの利用に伴うすべてのリスク(アカウントの凍結やダウンロード速度制限などを含む)は、利用者自身が負うものとします。
- 本プロジェクトは [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) ライセンスに従います。詳細は [LICENSE](../LICENSE) ファイルをご覧ください。
## お問い合わせ
- [@GitHub](https://github.com/OpenListTeam)
- [Telegram グループ](https://t.me/OpenListTeam)
- [Telegram チャンネル](https://t.me/OpenListOfficial)
## コントリビューター
オリジナルプロジェクト [AlistGo/alist](https://github.com/AlistGo/alist) の作者 [Xhofe](https://github.com/Xhofe) およびその他すべての貢献者に心より感謝いたします。
素晴らしい皆様に感謝します:
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
-136
View File
@@ -1,136 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
<p><em>OpenList는 신뢰 기반 공격으로부터 오픈 소스를 보호하기 위해 만들어진, 탄력적이고 장기 거버넌스를 지향하는 커뮤니티 주도 AList 포크입니다.</em></p>
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" /></a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" /></a>
<a href="https://github.com/OpenListTeam/OpenList/discussions"><img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" /></a>
</div>
---
- [English](../README.md) | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Dutch](./README_nl.md) | 한국어 | [Deutsch](./README_de.md) | [Русский](./README_ru.md) | [Français](./README_fr.md) | [Español](./README_es.md) | [العربية](./README_ar.md)
- [Contributing](../CONTRIBUTING.md)
- [CODE OF CONDUCT](../CODE_OF_CONDUCT.md)
- [LICENSE](../LICENSE)
## 면책 조항
OpenList is an open-source project independently maintained by the OpenList Team under the AGPL-3.0 license, with a commitment to code openness and modification transparency.
OpenList has no official association with third-party derivative projects that use similar names, including OpenListApp/OpenListApp or paid proprietary software using the same or similar names.
- All software, code, and services of this project are maintained by the OpenList Team and are freely available on GitHub.
- Project documentation and API services primarily rely on charitable resources provided by Cloudflare. There are currently no paid plans or commercial deployments.
- Downstream projects should not use the "OpenList" name for impersonation or commercial confusion, and must not distribute OpenList-based code as closed source in violation of the AGPL license.
- This software is provided "as is" without warranties. Please comply with applicable laws and regulations when using it.
## 기능
- [x] Multiple storages
- [x] Local storage
- [x] [Aliyundrive](https://www.alipan.com)
- [x] OneDrive / Sharepoint ([Global](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [CN](https://portal.partner.microsoftonline.cn), DE, US)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com)
- [x] [123pan](https://www.123pan.com)
- [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)
- [x] [PikPak](https://www.mypikpak.com)
- [x] [S3](https://aws.amazon.com/s3)
- [x] [Seafile](https://seafile.com)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com)
- [x] [ILanzou](https://www.ilanzou.com)
- [x] [Google photo](https://photos.google.com)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com)
- [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- [x] [115](https://115.com)
- [x] [Cloudreve](https://cloudreve.org)
- [x] [Dropbox](https://www.dropbox.com)
- [x] [FeijiPan](https://www.feijipan.com)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [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] [DingTalk Docs](https://alidocs.dingtalk.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
- [x] Video and audio preview, with lyrics and subtitles support
- [x] Office document preview (docx, pptx, xlsx, ...)
- [x] `README.md` preview rendering
- [x] File permalink copy and direct file download
- [x] Dark mode and i18n
- [x] Protected routes, WebDAV, Docker deploy, and Cloudflare Workers proxy
- [x] File/folder package download and web upload operations
- [x] Offline download, cross-storage copy, and multi-thread download acceleration
## 문서
- 📘 [Docs](https://doc.oplist.org)
- 🌏 [CN Mirror](https://doc.oplist.org.cn)
- ⚖️ [Terms of Use](https://doc.oplist.org/terms)
- 🔒 [Privacy Policy](https://doc.oplist.org/privacy)
## 데모
- 🌎 [Global Demo](https://demo.oplist.org)
- 🇨🇳 [CN Demo](https://demo.oplist.org.cn)
## 토론
Please refer to [Discussions](https://github.com/OpenListTeam/OpenList/discussions) for general questions; Issues are for bug reports and feature requests only.
## 후원
[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town "VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.")
## 후원자
Thanks to the following donors for their generous support:
- [HisAtri](https://github.com/HisAtri)
- 爱发电用户_7jTh
- suka
## 라이선스
OpenList is open-source software licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) license.
## 문의
- [@GitHub](https://github.com/OpenListTeam)
- [Telegram Group](https://t.me/OpenListTeam)
- [Telegram Channel](https://t.me/OpenListOfficial)
## 기여자
We sincerely thank [Xhofe](https://github.com/Xhofe), author of the original project [AlistGo/alist](https://github.com/AlistGo/alist), and all other contributors.
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
-174
View File
@@ -1,174 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
<p><em>OpenList is een veerkrachtige, langetermijn, door de gemeenschap geleide fork van AList — gebouwd om open source te beschermen tegen op vertrouwen gebaseerde aanvallen.</em></p>
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" /></a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" /></a>
<a href="https://github.com/OpenListTeam/OpenList/discussions"><img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" /></a>
</div>
---
- [English](../README.md) | [中文](./README_cn.md) | [日本語](./README_ja.md) | Dutch | [한국어](./README_ko.md) | [Deutsch](./README_de.md) | [Русский](./README_ru.md) | [Français](./README_fr.md) | [Español](./README_es.md) | [العربية](./README_ar.md)
- [Bijdragen](../CONTRIBUTING.md)
- [Gedragscode](../CODE_OF_CONDUCT.md)
- [Licentie](../LICENSE)
## Disclaimer
OpenList is een open-source project dat onafhankelijk wordt onderhouden door het OpenList Team, volgend op de AGPL-3.0 licentie en toegewijd aan het behouden van volledige code openheid en transparantie van wijzigingen.
We hebben gemerkt dat er in de gemeenschap enkele derde partij projecten zijn verschenen met namen vergelijkbaar met dit project, zoals OpenListApp/OpenListApp, evenals enkele betaalde eigendomssoftware die dezelfde of soortgelijke naamgeving gebruikt. Om verwarring bij gebruikers te voorkomen, verklaren we hierbij:
- OpenList heeft geen officiële associatie met enige derde partij afgeleide projecten.
- Alle software, code en diensten van dit project worden onderhouden door het OpenList Team en zijn gratis beschikbaar op GitHub.
- Projectdocumentatie en API diensten zijn voornamelijk afhankelijk van liefdadigheidsbronnen verstrekt door Cloudflare. Er zijn momenteel geen betaalplannen of commerciële implementaties, en het gebruik van bestaande functies brengt geen kosten met zich mee.
We respecteren de rechten van de gemeenschap voor vrij gebruik en afgeleide ontwikkeling, maar we roepen downstream projecten ook ten zeerste op:
- Mogen niet de "OpenList" naam gebruiken voor namaakpromotie of commercieel gewin;
- Mogen OpenList-gebaseerde code niet distribueren op een closed-source manier of AGPL licentievoorwaarden schenden.
Om een gezonde ecosysteemontwikkeling beter te onderhouden, bevelen we aan:
- Duidelijk de projectbron aangeven en passende open-source licenties kiezen in overeenstemming met de open-source geest;
- Bij commercieel gebruik, vermijd het gebruik van "OpenList" of enige verwarrende naamgeving als projectnaam;
- Als u materialen onder OpenListTeam/Logo moet gebruiken, kunt u deze wijzigen en gebruiken onder naleving van de overeenkomst.
Dank u voor uw ondersteuning en begrip
## Functies
- [x] Meerdere opslagmogelijkheden
- [x] Lokale opslag
- [x] [Aliyundrive](https://www.alipan.com)
- [x] OneDrive / Sharepoint ([Global](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [CN](https://portal.partner.microsoftonline.cn), DE, US)
- [x] [189cloud](https://cloud.189.cn) (Persoonlijk, Familie)
- [x] [GoogleDrive](https://drive.google.com)
- [x] [123pan](https://www.123pan.com)
- [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)
- [x] [PikPak](https://www.mypikpak.com)
- [x] [S3](https://aws.amazon.com/s3)
- [x] [Seafile](https://seafile.com)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([China](https://www.teambition.com), [Internationaal](https://us.teambition.com))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com) (Persoonlijk, Familie, Groep)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com)
- [x] [ILanzou](https://www.ilanzou.com)
- [x] [Google photo](https://photos.google.com)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com)
- [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- [x] [115](https://115.com)
- [x] [Cloudreve](https://cloudreve.org)
- [x] [Dropbox](https://www.dropbox.com)
- [x] [FeijiPan](https://www.feijipan.com)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [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] [DingTalk-documenten](https://alidocs.dingtalk.com/)
- [x] Eenvoudig te implementeren en direct te gebruiken
- [x] Bestandsvoorbeeld (PDF, markdown, code, platte tekst, ...)
- [x] Afbeeldingsvoorbeeld in galerijweergave
- [x] Video- en audiovoorbeeld, ondersteuning voor songteksten en ondertitels
- [x] Office-documenten voorbeeld (docx, pptx, xlsx, ...)
- [x] `README.md` voorbeeldweergave
- [x] Permalink kopiëren en direct downloaden van bestanden
- [x] Donkere modus
- [x] I18n
- [x] Beschermde routes (wachtwoordbeveiliging en authenticatie)
- [x] WebDAV
- [x] Docker implementatie
- [x] Cloudflare Workers proxy
- [x] Bestands-/map-pakket download
- [x] Webupload (bezoekers kunnen uploaden toestaan), verwijderen, map aanmaken, hernoemen, verplaatsen en kopiëren
- [x] Offline download
- [x] Bestanden kopiëren tussen twee opslaglocaties
- [x] Multi-thread downloadversnelling voor enkelvoudige download/stream
## Documentatie
- 📘 [Documentatie](https://doc.oplist.org)
- 🌏 [CN Mirror](https://doc.oplist.org.cn)
- ⚖️ [Gebruiksvoorwaarden](https://doc.oplist.org/terms)
- 🔒 [Privacybeleid](https://doc.oplist.org/privacy)
## Demo
- 🌎 [Global Demo](https://demo.oplist.org)
- 🇨🇳 [CN Demo](https://demo.oplist.org.cn)
## Discussie
Stel algemene vragen in [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions), ***Issues* zijn alleen voor bugmeldingen en feature requests.**
## Sponsoren
[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town "VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.")
## Donatie Bijdragers
Dank aan de volgende donateurs voor hun steun:
- [HisAtri](https://github.com/HisAtri)
- 爱发电用户_7jTh
- suka
## Licentie
`OpenList` is open-source software onder de [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) licentie.
## Disclaimer
- Dit project is gratis en open-source software, ontworpen om het delen van bestanden via netdisks te vergemakkelijken, voornamelijk bedoeld ter ondersteuning van het downloaden en leren van de programmeertaal Go.
- Houd u bij het gebruik van deze software aan alle toepasselijke wetten en voorschriften. Elk misbruik is ten strengste verboden.
- De software is gebaseerd op officiële SDK's of API's zonder enige wijziging, verstoring of beïnvloeding van hun gedrag.
- Het voert alleen HTTP 302-omleidingen of verkeersdoorsturing uit en onderschept, slaat of wijzigt geen gebruikersgegevens.
- Dit project is niet gelieerd aan enig officieel platform of dienstverlener.
- De software wordt geleverd "zoals deze is", zonder enige vorm van garantie, expliciet of impliciet, inclusief maar niet beperkt tot garanties van verkoopbaarheid of geschiktheid voor een bepaald doel.
- De beheerders zijn niet aansprakelijk voor enige directe of indirecte schade die voortvloeit uit het gebruik van of het onvermogen om deze software te gebruiken.
- U bent zelf verantwoordelijk voor alle risico's die gepaard gaan met het gebruik van deze software, inclusief maar niet beperkt tot accountblokkades of downloadbeperkingen.
- Dit project valt onder de [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) licentie. Zie het [LICENSE](../LICENSE) bestand voor details.
## Contact
- [@GitHub](https://github.com/OpenListTeam)
- [Telegram Groep](https://t.me/OpenListTeam)
- [Telegram Kanaal](https://t.me/OpenListOfficial)
## Bijdragers
Wij danken de auteur [Xhofe](https://github.com/Xhofe) van het originele project [AlistGo/alist](https://github.com/AlistGo/alist) en alle andere bijdragers.
Dank aan deze geweldige mensen:
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
-136
View File
@@ -1,136 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
<p><em>OpenList — устойчивый, управляемый сообществом форк AList с долгосрочным управлением, созданный для защиты open source от атак, основанных на доверии.</em></p>
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" /></a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild"><img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" /></a>
<a href="https://github.com/OpenListTeam/OpenList/discussions"><img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" /></a>
<a href="https://github.com/OpenListTeam/OpenList/releases"><img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" /></a>
</div>
---
- [English](../README.md) | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Dutch](./README_nl.md) | [한국어](./README_ko.md) | [Deutsch](./README_de.md) | Русский | [Français](./README_fr.md) | [Español](./README_es.md) | [العربية](./README_ar.md)
- [Contributing](../CONTRIBUTING.md)
- [CODE OF CONDUCT](../CODE_OF_CONDUCT.md)
- [LICENSE](../LICENSE)
## Отказ от ответственности
OpenList is an open-source project independently maintained by the OpenList Team under the AGPL-3.0 license, with a commitment to code openness and modification transparency.
OpenList has no official association with third-party derivative projects that use similar names, including OpenListApp/OpenListApp or paid proprietary software using the same or similar names.
- All software, code, and services of this project are maintained by the OpenList Team and are freely available on GitHub.
- Project documentation and API services primarily rely on charitable resources provided by Cloudflare. There are currently no paid plans or commercial deployments.
- Downstream projects should not use the "OpenList" name for impersonation or commercial confusion, and must not distribute OpenList-based code as closed source in violation of the AGPL license.
- This software is provided "as is" without warranties. Please comply with applicable laws and regulations when using it.
## Возможности
- [x] Multiple storages
- [x] Local storage
- [x] [Aliyundrive](https://www.alipan.com)
- [x] OneDrive / Sharepoint ([Global](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage), [CN](https://portal.partner.microsoftonline.cn), DE, US)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com)
- [x] [123pan](https://www.123pan.com)
- [x] [FTP / SFTP](https://en.wikipedia.org/wiki/File_Transfer_Protocol)
- [x] [PikPak](https://www.mypikpak.com)
- [x] [S3](https://aws.amazon.com/s3)
- [x] [Seafile](https://seafile.com)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com)
- [x] [ILanzou](https://www.ilanzou.com)
- [x] [Google photo](https://photos.google.com)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com)
- [x] [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- [x] [115](https://115.com)
- [x] [Cloudreve](https://cloudreve.org)
- [x] [Dropbox](https://www.dropbox.com)
- [x] [FeijiPan](https://www.feijipan.com)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [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] [DingTalk Docs](https://alidocs.dingtalk.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
- [x] Video and audio preview, with lyrics and subtitles support
- [x] Office document preview (docx, pptx, xlsx, ...)
- [x] `README.md` preview rendering
- [x] File permalink copy and direct file download
- [x] Dark mode and i18n
- [x] Protected routes, WebDAV, Docker deploy, and Cloudflare Workers proxy
- [x] File/folder package download and web upload operations
- [x] Offline download, cross-storage copy, and multi-thread download acceleration
## Документация
- 📘 [Docs](https://doc.oplist.org)
- 🌏 [CN Mirror](https://doc.oplist.org.cn)
- ⚖️ [Terms of Use](https://doc.oplist.org/terms)
- 🔒 [Privacy Policy](https://doc.oplist.org/privacy)
## Демо
- 🌎 [Global Demo](https://demo.oplist.org)
- 🇨🇳 [CN Demo](https://demo.oplist.org.cn)
## Обсуждение
Please refer to [Discussions](https://github.com/OpenListTeam/OpenList/discussions) for general questions; Issues are for bug reports and feature requests only.
## Спонсор
[![VPS.Town](https://vps.town/static/images/sponsor.png)](https://vps.town "VPS.Town - Trust, Effortlessly. Your Cloud, Reimagined.")
## Доноры
Thanks to the following donors for their generous support:
- [HisAtri](https://github.com/HisAtri)
- 爱发电用户_7jTh
- suka
## Лицензия
OpenList is open-source software licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) license.
## Связаться с нами
- [@GitHub](https://github.com/OpenListTeam)
- [Telegram Group](https://t.me/OpenListTeam)
- [Telegram Channel](https://t.me/OpenListOfficial)
## Участники
We sincerely thank [Xhofe](https://github.com/Xhofe), author of the original project [AlistGo/alist](https://github.com/AlistGo/alist), and all other contributors.
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
+126
View File
@@ -0,0 +1,126 @@
<div align="center">
<img width="100px" alt="logo" src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg"/></a>
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 SolidJS,基于 AList 项目 fork 开发</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/OpenListTeam/OpenList/v3">
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" />
</a>
</div>
<div>
<a href="https://github.com/OpenListTeam/OpenList/discussions">
<img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
</div>
</div>
---
> [!IMPORTANT]
>
> 一个更可信、可持续的 AList 开源替代方案,防范未来可能的闭源、黑箱或不可信变更。
>
> 我们诚挚地感谢原项目 [AlistGo/alist](https://github.com/AlistGo/alist) 的作者 [Xhofe](https://github.com/Xhofe) 以及其他所有贡献者。
>
> 本 Fork 尚未稳定, 具体迁移进度可在 [OpenList 迁移工作总结](https://github.com/OpenListTeam/OpenList/issues/6) 中查看。
[English](./README.md) | 中文 | [日本語](./README_ja.md) | [Contributing](./CONTRIBUTING.md) | [CODE OF CONDUCT](./CODE_OF_CONDUCT.md)
## 功能
- [x] 多种存储
- [x] 本地存储
- [x] [阿里云盘](https://www.alipan.com/)
- [x] OneDrive / Sharepoint([国际版](https://www.office.com/), [世纪互联](https://portal.partner.microsoftonline.cn),de,us)
- [x] [天翼云盘](https://cloud.189.cn) (个人云, 家庭云)
- [x] [GoogleDrive](https://drive.google.com/)
- [x] [123云盘](https://www.123pan.com/)
- [x] FTP / SFTP
- [x] [PikPak](https://www.mypikpak.com/)
- [x] [S3](https://aws.amazon.com/cn/s3/)
- [x] [Seafile](https://seafile.com/)
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
- [x] WebDav(支持无API的OneDrive/SharePoint)
- [x] Teambition([中国](https://www.teambition.com/ ),[国际](https://us.teambition.com/ ))
- [x] [分秒帧](https://www.mediatrack.cn/)
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云,共享群组)
- [x] [Yandex.Disk](https://disk.yandex.com/)
- [x] [百度网盘](http://pan.baidu.com/)
- [x] [UC网盘](https://drive.uc.cn)
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷网盘](https://pan.xunlei.com)
- [x] [蓝奏云](https://www.lanzou.com/)
- [x] [蓝奏云优享版](https://www.ilanzou.com/)
- [x] [阿里云盘分享](https://www.alipan.com/)
- [x] [谷歌相册](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
- [x] [一刻相册](https://photo.baidu.com/)
- [x] SMB
- [x] [115](https://115.com/)
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] [飞机盘](https://www.feijipan.com/)
- [x] [多吉云](https://www.dogecloud.com/product/oss)
- [x] 部署方便,开箱即用
- [x] 文件预览(PDF、markdown、代码、纯文本……)
- [x] 画廊模式下的图像预览
- [x] 视频和音频预览,支持歌词和字幕
- [x] Office 文档预览(docx、pptx、xlsx、...)
- [x] `README.md` 预览渲染
- [x] 文件永久链接复制和直接文件下载
- [x] 黑暗模式
- [x] 国际化
- [x] 受保护的路由(密码保护和身份验证)
- [x] WebDav (详细文档待补充)
- [ ] Docker 部署(重建中)
- [x] Cloudflare workers 中转
- [x] 文件/文件夹打包下载
- [x] 网页上传(可以允许访客上传),删除,新建文件夹,重命名,移动,复制
- [x] 离线下载
- [x] 跨存储复制文件
- [x] 单线程下载/串流的多线程下载加速
## 文档
<https://docs.openlist.team>
## Demo
N/A(重建中)
## 讨论
一般问题请到 [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) 讨论,***Issues* 仅针对错误报告和功能请求。**
## 贡献者
感谢这些开源作者们:
[![Contributors](https://contrib.rocks/image?repo=OpenListTeam/OpenList)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
## 许可
`OpenList` 是按 AGPL-3.0 许可证许可的开源软件。
## 免责声明
- 本程序为免费开源项目,旨在分享网盘文件,方便下载以及学习golang,使用时请遵守相关法律法规,请勿滥用;
- 本程序通过调用官方sdk/接口实现,无破坏官方接口行为;
- 本程序仅做302重定向/流量转发,不拦截、存储、篡改任何用户数据;
- 在使用本程序之前,你应了解并承担相应的风险,包括但不限于账号被ban,下载限速等,与本程序无关;
- 如有侵权,请联系[OpenListTeam](https://github.com/OpenListTeam),团队会及时处理。
---
> [@GitHub](https://github.com/OpenListTeam) · [Telegram 交流群](https://t.me/OpenListTeam)
+127
View File
@@ -0,0 +1,127 @@
<div align="center">
<img width="100px" alt="logo" src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg"/></a>
<p><em>🗂複数のストレージをサポートするファイルリストプログラムで、Gin と SolidJS を使用し、AList プロジェクトをフォークして開発されました。</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/OpenListTeam/OpenList/v3">
<img src="https://goreportcard.com/badge/github.com/OpenListTeam/OpenList/v3" alt="latest version" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/OpenListTeam/OpenList" alt="License" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/actions/workflow/status/OpenListTeam/OpenList/build.yml?branch=main" alt="Build status" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/github/release/OpenListTeam/OpenList" alt="latest version" />
</a>
</div>
<div>
<a href="https://github.com/OpenListTeam/OpenList/discussions">
<img src="https://img.shields.io/github/discussions/OpenListTeam/OpenList?color=%23ED8936" alt="discussions" />
</a>
<a href="https://github.com/OpenListTeam/OpenList/releases">
<img src="https://img.shields.io/github/downloads/OpenListTeam/OpenList/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
</div>
</div>
---
> [!IMPORTANT]
>
> より信頼性が高く、持続可能なAListのオープンソース代替案で、将来起こりうる非公開化、ブラックボックス化、または信頼できない変更から保護します。
>
> 元のプロジェクト [AlistGo/alist](https://github.com/AlistGo/alist) の作者 [Xhofe](https://github.com/Xhofe) および他のすべての貢献者に心から感謝いたします。
>
> このForkはまだ安定していません。具体的な移行の進捗状況は [OpenList 移行作業のまとめ](https://github.com/OpenListTeam/OpenList/issues/6) でご確認いただけます。
[English](./README.md) | [中文](./README_cn.md) | 日本語 | [Contributing](./CONTRIBUTING.md) | [CODE OF CONDUCT](./CODE_OF_CONDUCT.md)
## 特徴
- [x] マルチストレージ
- [x] ローカルストレージ
- [x] [Aliyundrive](https://www.alipan.com/)
- [x] OneDrive / Sharepoint ([グローバル](https://www.office.com/), [cn](https://portal.partner.microsoftonline.cn),de,us)
- [x] [189cloud](https://cloud.189.cn) (Personal, Family)
- [x] [GoogleDrive](https://drive.google.com/)
- [x] [123pan](https://www.123pan.com/)
- [x] FTP / SFTP
- [x] [PikPak](https://www.mypikpak.com/)
- [x] [S3](https://aws.amazon.com/s3/)
- [x] [Seafile](https://seafile.com/)
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
- [x] [Mediatrack](https://www.mediatrack.cn/)
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com/)
- [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Terabox](https://www.terabox.com/main)
- [x] [UC](https://drive.uc.cn)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/)
- [x] [ILanzou](https://www.ilanzou.com/)
- [x] [Aliyundrive share](https://www.alipan.com/)
- [x] [Google photo](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com/)
- [x] SMB
- [x] [115](https://115.com/)
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] [FeijiPan](https://www.feijipan.com/)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [x] デプロイが簡単で、すぐに使える
- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)
- [x] ギャラリーモードでの画像プレビュー
- [x] ビデオとオーディオのプレビュー、歌詞と字幕のサポート
- [x] Office ドキュメントのプレビュー (docx, pptx, xlsx, ...)
- [x] `README.md` のプレビューレンダリング
- [x] ファイルのパーマリンクコピーと直接ダウンロード
- [x] ダークモード
- [x] 国際化
- [x] 保護されたルート (パスワード保護と認証)
- [x] WebDav(詳細なドキュメントは今後追加予定)
- [ ] Docker デプロイ(再構築中)
- [x] Cloudflare ワーカープロキシ
- [x] ファイル/フォルダパッケージのダウンロード
- [x] ウェブアップロード(訪問者にアップロードを許可できる), 削除, mkdir, 名前変更, 移動, コピー
- [x] オフラインダウンロード
- [x] 二つのストレージ間でファイルをコピー
- [x] シングルスレッドのダウンロード/ストリーム向けのマルチスレッド ダウンロード アクセラレーション
## ドキュメント
<https://docs.openlist.team>
## デモ
N/A (再構築中)
## ディスカッション
一般的なご質問は [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) をご利用ください。***Issues* はバグ報告と機能リクエストに限定されています。**
## コントリビューター
これらの素晴らしい人々に感謝します:
[![Contributors](https://contrib.rocks/image?repo=openlistteam/openlist)](https://github.com/OpenListTeam/OpenList/graphs/contributors)
## ライセンス
「`OpenList`」は AGPL-3.0 ライセンスの下で公開されているオープンソースソフトウェアです。
## 免責事項
- このプログラムはフリーでオープンソースのプロジェクトです。ネットワークディスク上でファイルを共有するように設計されており、golang のダウンロードや学習に便利です。利用にあたっては関連法規を遵守し、悪用しないようお願いします;
- このプログラムは、公式インターフェースの動作を破壊することなく、公式 sdk/インターフェースを呼び出すことで実装されています;
- このプログラムは、302リダイレクト/トラフィック転送のみを行い、いかなるユーザーデータも傍受、保存、改ざんしません;
- このプログラムを使用する前に、アカウントの禁止、ダウンロード速度の制限など、対応するリスクを理解し、負担する必要があります;
- 権利侵害がある場合は、[OpenListTeam](https://github.com/OpenListTeam) までご連絡ください。チームが迅速に対応いたします。
---
> [@GitHub](https://github.com/OpenListTeam) · [Telegram Group](https://t.me/OpenListTeam)
-89
View File
@@ -1,89 +0,0 @@
# Security Policy
## Supported Versions
Only the latest stable release receives security patches. We strongly recommend always keeping OpenList up to date.
| Version | Supported |
| -------------------- | ------------------ |
| Latest stable (v4.x) | :white_check_mark: |
| Older versions | :x: |
## Reporting a Vulnerability
**Please do NOT report security vulnerabilities through public GitHub Issues.**
If you discover a security vulnerability in OpenList, please report it responsibly by using one of the following channels:
- **GitHub Private Security Advisory** (preferred): [Submit here](https://github.com/OpenListTeam/OpenList/security/advisories/new)
- **Telegram**: Contact a maintainer privately via [@OpenListTeam](https://t.me/OpenListTeam)
When reporting, please include as much of the following as possible:
- A description of the vulnerability and its potential impact
- The affected version(s)
- Step-by-step instructions to reproduce the issue
- Any proof-of-concept code or screenshots (if applicable)
- Suggested mitigation or fix (optional but appreciated)
## Security Best Practices for Users
To keep your OpenList instance secure:
- Always update to the latest release.
- Use a strong, unique admin password and change it after first login.
- Enable HTTPS (TLS) for your deployment — do **not** expose OpenList over plain HTTP on the public internet.
- Limit exposed ports using a reverse proxy (e.g., Nginx, Caddy).
- Set up access controls and avoid enabling guest access unless necessary.
- Regularly review mounted storage permissions and revoke unused API tokens.
- When using Docker, avoid running the container as root if possible.
## Acknowledgments
We sincerely thank all security researchers and community members who responsibly disclose vulnerabilities and help make OpenList safer for everyone.
---
# 安全政策
## 支持的版本
我们仅对最新稳定版本提供安全补丁。强烈建议始终保持 OpenList 为最新版本。
| 版本 | 是否支持 |
| ------------------ | ------------------ |
| 最新稳定版(v4.x) | :white_check_mark: |
| 旧版本 | :x: |
## 报告漏洞
**请勿通过公开的 GitHub Issues 报告安全漏洞。**
如果您在 OpenList 中发现安全漏洞,请通过以下渠道之一负责任地进行报告:
- **GitHub 私密安全公告**(推荐):[点击提交](https://github.com/OpenListTeam/OpenList/security/advisories/new)
- **Telegram**:通过 [@OpenListTeam](https://t.me/OpenListTeam) 私信联系维护者
报告时,请尽量提供以下信息:
- 漏洞描述及其潜在影响
- 受影响的版本
- 复现问题的详细步骤
- 概念验证代码或截图(如有)
- 建议的缓解措施或修复方案(可选,但非常欢迎)
## 用户安全最佳实践
为保障您的 OpenList 实例安全:
- 始终更新至最新版本。
- 使用强且唯一的管理员密码,并在首次登录后立即修改。
- 为您的部署启用 HTTPS(TLS)—— **请勿**在公网上以明文 HTTP 方式暴露 OpenList。
- 使用反向代理(如 Nginx、Caddy)限制对外暴露的端口。
- 配置访问控制,非必要情况下不要开启访客访问。
- 定期检查已挂载存储的权限,并撤销未使用的 API 令牌。
- 使用 Docker 部署时,尽可能避免以 root 用户运行容器。
## 致谢
我们衷心感谢所有负责任地披露漏洞、帮助 OpenList 变得更加安全的安全研究人员和社区成员。
+75 -439
View File
@@ -4,121 +4,58 @@ builtAt="$(date +'%F %T %z')"
gitAuthor="The OpenList Projects Contributors <noreply@openlist.team>"
gitCommit=$(git log --pretty=format:"%h" -1)
# Set frontend repository, default to OpenListTeam/OpenList-Frontend
frontendRepo="${FRONTEND_REPO:-OpenListTeam/OpenList-Frontend}"
githubAuthArgs=""
githubAuthHeader=""
githubAuthValue=""
if [ -n "$GITHUB_TOKEN" ]; then
githubAuthArgs="--header \"Authorization: Bearer $GITHUB_TOKEN\""
fi
# Check for lite parameter
useLite=false
if [[ "$*" == *"lite"* ]]; then
useLite=true
githubAuthHeader="--header"
githubAuthValue="Authorization: Bearer $GITHUB_TOKEN"
fi
if [ "$1" = "dev" ]; then
version="dev"
webVersion="rolling"
webVersion="dev"
elif [ "$1" = "beta" ]; then
version="beta"
webVersion="rolling"
webVersion="dev"
else
git tag -d beta || true
# Always true if there's no tag
version=$(git describe --abbrev=0 --tags 2>/dev/null || echo "v0.0.0")
webVersion=$(eval "curl -fsSL --max-time 2 $githubAuthArgs \"https://api.github.com/repos/$frontendRepo/releases/latest\"" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
webVersion=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
fi
echo "backend version: $version"
echo "frontend version: $webVersion"
if [ "$useLite" = true ]; then
echo "using lite frontend"
else
echo "using standard frontend"
fi
ldflags="\
-w -s \
-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$builtAt' \
-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=$gitAuthor' \
-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$gitCommit' \
-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$version' \
-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=$webVersion' \
-X 'github.com/OpenListTeam/OpenList/internal/conf.BuiltAt=$builtAt' \
-X 'github.com/OpenListTeam/OpenList/internal/conf.GitAuthor=$gitAuthor' \
-X 'github.com/OpenListTeam/OpenList/internal/conf.GitCommit=$gitCommit' \
-X 'github.com/OpenListTeam/OpenList/internal/conf.Version=$version' \
-X 'github.com/OpenListTeam/OpenList/internal/conf.WebVersion=$webVersion' \
"
# Keep sqlite driver tag selection centralized to avoid target drift.
GetBuildTagsForTarget() {
local target="$1"
case "$target" in
linux-loong64|linux-mips|linux-mips64|linux-mips64le|linux-mipsle|linux-musl-loong64|linux-musl-mips|linux-musl-mips64|linux-musl-mips64le|linux-musl-mipsle|windows-386|windows7-386|windows7-amd64)
echo "jsoniter,sqlite_cgo_compat"
;;
*)
echo "jsoniter"
;;
esac
}
# Keep musl static link flags centralized for all musl build paths.
GetMuslStaticLdflags() {
echo "-linkmode external -extldflags '-static -fpic' $ldflags"
}
# Fail fast if a musl build artifact is not fully static.
AssertStaticBinary() {
local binary="$1"
if [ ! -f "$binary" ]; then
echo "Error: binary not found: $binary"
return 1
FetchWebDev() {
pre_release_tag=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases | jq -r 'map(select(.prerelease)) | first | .tag_name')
if [ -z "$pre_release_tag" ] || [ "$pre_release_tag" == "null" ]; then
# fall back to latest release
pre_release_json=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest")
else
pre_release_json=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/tags/$pre_release_tag")
fi
if command -v readelf >/dev/null 2>&1; then
if readelf -l "$binary" 2>/dev/null | grep -q "Requesting program interpreter"; then
echo "Error: binary is not fully static: $binary"
readelf -l "$binary" | grep "Requesting program interpreter" || true
return 1
fi
return 0
fi
if command -v file >/dev/null 2>&1; then
if file "$binary" | grep -qi "dynamically linked"; then
echo "Error: binary is dynamically linked: $binary"
file "$binary"
return 1
fi
return 0
fi
echo "Warning: readelf/file not found, skip static verification for $binary"
return 0
}
FetchWebRolling() {
pre_release_json=$(eval "curl -fsSL --max-time 2 $githubAuthArgs -H \"Accept: application/vnd.github.v3+json\" \"https://api.github.com/repos/$frontendRepo/releases/tags/rolling\"")
pre_release_assets=$(echo "$pre_release_json" | jq -r '.assets[].browser_download_url')
# There is no lite for rolling
pre_release_tar_url=$(echo "$pre_release_assets" | grep "openlist-frontend-dist" | grep -v "lite" | grep "\.tar\.gz$")
curl -fsSL "$pre_release_tar_url" -o dist.tar.gz
pre_release_tar_url=$(echo "$pre_release_assets" | grep "openlist-frontend-dist" | grep "\.tar\.gz$")
curl -fsSL "$pre_release_tar_url" -o web-dist-dev.tar.gz
rm -rf public/dist && mkdir -p public/dist
tar -zxvf dist.tar.gz -C public/dist
rm -rf dist.tar.gz
tar -zxvf web-dist-dev.tar.gz -C public/dist
rm -rf web-dist-dev.tar.gz
}
FetchWebRelease() {
release_json=$(eval "curl -fsSL --max-time 2 $githubAuthArgs -H \"Accept: application/vnd.github.v3+json\" \"https://api.github.com/repos/$frontendRepo/releases/latest\"")
release_json=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/OpenListTeam/OpenList-Frontend/releases/latest")
release_assets=$(echo "$release_json" | jq -r '.assets[].browser_download_url')
if [ "$useLite" = true ]; then
release_tar_url=$(echo "$release_assets" | grep "openlist-frontend-dist-lite" | grep "\.tar\.gz$")
else
release_tar_url=$(echo "$release_assets" | grep "openlist-frontend-dist" | grep -v "lite" | grep "\.tar\.gz$")
fi
release_tar_url=$(echo "$release_assets" | grep "openlist-frontend-dist" | grep "\.tar\.gz$")
curl -fsSL "$release_tar_url" -o dist.tar.gz
rm -rf public/dist && mkdir -p public/dist
tar -zxvf dist.tar.gz -C public/dist
@@ -137,50 +74,10 @@ BuildWinArm64() {
go build -o "$1" -ldflags="$ldflags" -tags=jsoniter .
}
BuildWin7() {
# Setup Win7 Go compiler (patched version that supports Windows 7)
go_version=$(go version | grep -o 'go[0-9]\+\.[0-9]\+\.[0-9]\+' | sed 's/go//')
echo "Detected Go version: $go_version"
curl -fsSL --retry 3 -o go-win7.zip -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://github.com/XTLS/go-win7/releases/download/patched-${go_version}/go-for-win7-linux-amd64.zip"
rm -rf go-win7
unzip go-win7.zip -d go-win7
rm go-win7.zip
# Set permissions for all wrapper files
chmod +x ./wrapper/zcc-win7
chmod +x ./wrapper/zcxx-win7
chmod +x ./wrapper/zcc-win7-386
chmod +x ./wrapper/zcxx-win7-386
# Build for both 386 and amd64 architectures
for arch in "386" "amd64"; do
echo "building for windows7-${arch}"
build_tags=$(GetBuildTagsForTarget "windows7-${arch}")
export GOOS=windows
export GOARCH=${arch}
export CGO_ENABLED=1
# Use architecture-specific wrapper files
if [ "$arch" = "386" ]; then
export CC=$(pwd)/wrapper/zcc-win7-386
export CXX=$(pwd)/wrapper/zcxx-win7-386
else
export CC=$(pwd)/wrapper/zcc-win7
export CXX=$(pwd)/wrapper/zcxx-win7
fi
# Use the patched Go compiler for Win7 compatibility
$(pwd)/go-win7/bin/go build -o "${1}-${arch}.exe" -ldflags="$ldflags" -tags="$build_tags" .
done
}
BuildDev() {
rm -rf .git/
mkdir -p "dist"
muslflags="$(GetMuslStaticLdflags)"
muslflags="--extldflags '-static -fpic' $ldflags"
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross)
for i in "${FILES[@]}"; do
@@ -198,14 +95,13 @@ BuildDev() {
export GOARCH=${os_arch##*-}
export CC=${cgo_cc}
export CGO_ENABLED=1
CGO_LDFLAGS="-static" go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
AssertStaticBinary "./dist/$appName-$os_arch"
go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
done
xgo -targets=windows/amd64,darwin/amd64,darwin/arm64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
mv "$appName"-* dist
cd dist
# cp ./"$appName"-windows-amd64.exe ./"$appName"-windows-amd64-upx.exe
# upx -9 ./"$appName"-windows-amd64-upx.exe
cp ./"$appName"-windows-amd64.exe ./"$appName"-windows-amd64-upx.exe
upx -9 ./"$appName"-windows-amd64-upx.exe
find . -type f -print0 | xargs -0 md5sum >md5.txt
cat md5.txt
}
@@ -217,7 +113,7 @@ BuildDocker() {
PrepareBuildDockerMusl() {
mkdir -p build/musl-libs
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross riscv64-linux-musl-cross powerpc64le-linux-musl-cross loongarch64-linux-musl-cross) ## Disable s390x-linux-musl-cross builds
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross riscv64-linux-musl-cross powerpc64le-linux-musl-cross)
for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz"
lib_tgz="build/${i}.tgz"
@@ -233,23 +129,21 @@ BuildDockerMultiplatform() {
# run PrepareBuildDockerMusl before build
export PATH=$PATH:$PWD/build/musl-libs/bin
docker_lflags="$(GetMuslStaticLdflags)"
docker_lflags="--extldflags '-static -fpic' $ldflags"
export CGO_ENABLED=1
OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-riscv64 linux-ppc64le linux-loong64) ## Disable linux-s390x builds
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc riscv64-linux-musl-gcc powerpc64le-linux-musl-gcc loongarch64-linux-musl-gcc) ## Disable s390x-linux-musl-gcc builds
OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-s390x linux-riscv64 linux-ppc64le)
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc s390x-linux-musl-gcc riscv64-linux-musl-gcc powerpc64le-linux-musl-gcc)
for i in "${!OS_ARCHES[@]}"; do
os_arch=${OS_ARCHES[$i]}
cgo_cc=${CGO_ARGS[$i]}
os=${os_arch%%-*}
arch=${os_arch##*-}
build_tags=$(GetBuildTagsForTarget "$os_arch")
export GOOS=$os
export GOARCH=$arch
export CC=${cgo_cc}
echo "building for $os_arch"
CGO_LDFLAGS="-static" go build -o build/$os/$arch/"$appName" -ldflags="$docker_lflags" -tags="$build_tags" .
AssertStaticBinary "build/$os/$arch/$appName"
go build -o build/$os/$arch/"$appName" -ldflags="$docker_lflags" -tags=jsoniter .
done
DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)
@@ -263,8 +157,7 @@ BuildDockerMultiplatform() {
export GOARM=${GO_ARM[$i]}
export CC=${cgo_cc}
echo "building for $docker_arch"
CGO_LDFLAGS="-static" go build -o build/${docker_arch%%-*}/${docker_arch##*-}/"$appName" -ldflags="$docker_lflags" -tags=jsoniter .
AssertStaticBinary "build/${docker_arch%%-*}/${docker_arch##*-}/$appName"
go build -o build/${docker_arch%%-*}/${docker_arch##*-}/"$appName" -ldflags="$docker_lflags" -tags=jsoniter .
done
}
@@ -272,181 +165,19 @@ BuildRelease() {
rm -rf .git/
mkdir -p "build"
BuildWinArm64 ./build/"$appName"-windows-arm64.exe
BuildWin7 ./build/"$appName"-windows7
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
# why? Because some target platforms seem to have issues with upx compression
# upx -9 ./"$appName"-linux-amd64
# cp ./"$appName"-windows-amd64.exe ./"$appName"-windows-amd64-upx.exe
# upx -9 ./"$appName"-windows-amd64-upx.exe
upx -9 ./"$appName"-linux-amd64
cp ./"$appName"-windows-amd64.exe ./"$appName"-windows-amd64-upx.exe
upx -9 ./"$appName"-windows-amd64-upx.exe
mv "$appName"-* build
# Build LoongArch with glibc (both old world abi1.0 and new world abi2.0)
# Separate from musl builds to avoid cache conflicts
BuildLoongGLIBC ./build/$appName-linux-loong64-abi1.0 abi1.0
BuildLoongGLIBC ./build/$appName-linux-loong64 abi2.0
}
BuildLoongGLIBC() {
local target_abi="$2"
local output_file="$1"
local oldWorldGoVersion="1.26.4"
local loong_tags
loong_tags=$(GetBuildTagsForTarget "linux-loong64")
if [ "$target_abi" = "abi1.0" ]; then
echo building for linux-loong64-abi1.0
else
echo building for linux-loong64-abi2.0
target_abi="abi2.0" # Default to abi2.0 if not specified
fi
# Note: No longer need global cache cleanup since ABI1.0 uses isolated cache directory
echo "Using optimized cache strategy: ABI1.0 has isolated cache, ABI2.0 uses standard cache"
if [ "$target_abi" = "abi1.0" ]; then
# Setup abi1.0 toolchain and patched Go compiler similar to cgo-action implementation
echo "Setting up Loongson old-world ABI1.0 toolchain and patched Go compiler..."
# Download and setup patched Go compiler for old-world
if ! curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://github.com/loong64-abi1-0/golang/releases/download/go${oldWorldGoVersion}/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
-o go-loong64-abi1.0.tar.gz; then
echo "Error: Failed to download patched Go compiler for old-world ABI1.0"
if [ -n "$GITHUB_TOKEN" ]; then
echo "Error output from curl:"
curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://github.com/loong64-abi1-0/golang/releases/download/go${oldWorldGoVersion}/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
-o go-loong64-abi1.0.tar.gz || true
fi
return 1
fi
rm -rf go-loong64-abi1.0
mkdir go-loong64-abi1.0
if ! tar -xzf go-loong64-abi1.0.tar.gz -C go-loong64-abi1.0 --strip-components=1; then
echo "Error: Failed to extract patched Go compiler"
return 1
fi
rm go-loong64-abi1.0.tar.gz
# Download and setup GCC toolchain for old-world
if ! curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250722/loongson-gnu-toolchain-8.3.novec-x86_64-loongarch64-linux-gnu-rc1.1.tar.xz" \
-o gcc8-loong64-abi1.0.tar.xz; then
echo "Error: Failed to download GCC toolchain for old-world ABI1.0"
if [ -n "$GITHUB_TOKEN" ]; then
echo "Error output from curl:"
curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250722/loongson-gnu-toolchain-8.3.novec-x86_64-loongarch64-linux-gnu-rc1.1.tar.xz" \
-o gcc8-loong64-abi1.0.tar.xz || true
fi
return 1
fi
rm -rf gcc8-loong64-abi1.0
mkdir gcc8-loong64-abi1.0
if ! tar -Jxf gcc8-loong64-abi1.0.tar.xz -C gcc8-loong64-abi1.0 --strip-components=1; then
echo "Error: Failed to extract GCC toolchain"
return 1
fi
rm gcc8-loong64-abi1.0.tar.xz
# Setup separate cache directory for ABI1.0 to avoid cache pollution
abi1_cache_dir="$(pwd)/go-loong64-abi1.0-cache"
mkdir -p "$abi1_cache_dir"
echo "Using separate cache directory for ABI1.0: $abi1_cache_dir"
# Use patched Go compiler for old-world build (critical for ABI1.0 compatibility)
echo "Building with patched Go compiler for old-world ABI1.0..."
echo "Using isolated cache directory: $abi1_cache_dir"
# Use env command to set environment variables locally without affecting global environment
if ! env GOOS=linux GOARCH=loong64 \
CC="$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-gcc" \
CXX="$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-g++" \
CGO_ENABLED=1 \
GOCACHE="$abi1_cache_dir" \
$(pwd)/go-loong64-abi1.0/bin/go build -a -o "$output_file" -ldflags="$ldflags" -tags="$loong_tags" .; then
echo "Error: Build failed with patched Go compiler"
echo "Attempting retry with cache cleanup..."
env GOCACHE="$abi1_cache_dir" $(pwd)/go-loong64-abi1.0/bin/go clean -cache
if ! env GOOS=linux GOARCH=loong64 \
CC="$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-gcc" \
CXX="$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-g++" \
CGO_ENABLED=1 \
GOCACHE="$abi1_cache_dir" \
$(pwd)/go-loong64-abi1.0/bin/go build -a -o "$output_file" -ldflags="$ldflags" -tags="$loong_tags" .; then
echo "Error: Build failed again after cache cleanup"
echo "Build environment details:"
echo "GOOS=linux"
echo "GOARCH=loong64"
echo "CC=$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-gcc"
echo "CXX=$(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-g++"
echo "CGO_ENABLED=1"
echo "GOCACHE=$abi1_cache_dir"
echo "Go version: $($(pwd)/go-loong64-abi1.0/bin/go version)"
echo "GCC version: $($(pwd)/gcc8-loong64-abi1.0/bin/loongarch64-linux-gnu-gcc --version | head -1)"
return 1
fi
fi
else
# Setup abi2.0 toolchain for new world glibc build
echo "Setting up new-world ABI2.0 toolchain..."
if ! curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://github.com/loong64/cross-tools/releases/download/20250507/x86_64-cross-tools-loongarch64-unknown-linux-gnu-legacy.tar.xz" \
-o gcc12-loong64-abi2.0.tar.xz; then
echo "Error: Failed to download GCC toolchain for new-world ABI2.0"
if [ -n "$GITHUB_TOKEN" ]; then
echo "Error output from curl:"
curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://github.com/loong64/cross-tools/releases/download/20250507/x86_64-cross-tools-loongarch64-unknown-linux-gnu-legacy.tar.xz" \
-o gcc12-loong64-abi2.0.tar.xz || true
fi
return 1
fi
rm -rf gcc12-loong64-abi2.0
mkdir gcc12-loong64-abi2.0
if ! tar -Jxf gcc12-loong64-abi2.0.tar.xz -C gcc12-loong64-abi2.0 --strip-components=1; then
echo "Error: Failed to extract GCC toolchain"
return 1
fi
rm gcc12-loong64-abi2.0.tar.xz
export GOOS=linux
export GOARCH=loong64
export CC=$(pwd)/gcc12-loong64-abi2.0/bin/loongarch64-unknown-linux-gnu-gcc
export CXX=$(pwd)/gcc12-loong64-abi2.0/bin/loongarch64-unknown-linux-gnu-g++
export CGO_ENABLED=1
# Use standard Go compiler for new-world build
echo "Building with standard Go compiler for new-world ABI2.0..."
if ! go build -a -o "$output_file" -ldflags="$ldflags" -tags="$loong_tags" .; then
echo "Error: Build failed with standard Go compiler"
echo "Attempting retry with cache cleanup..."
go clean -cache
if ! go build -a -o "$output_file" -ldflags="$ldflags" -tags="$loong_tags" .; then
echo "Error: Build failed again after cache cleanup"
echo "Build environment details:"
echo "GOOS=$GOOS"
echo "GOARCH=$GOARCH"
echo "CC=$CC"
echo "CXX=$CXX"
echo "CGO_ENABLED=$CGO_ENABLED"
echo "Go version: $(go version)"
echo "GCC version: $($CC --version | head -1)"
return 1
fi
fi
fi
}
BuildReleaseLinuxMusl() {
rm -rf .git/
mkdir -p "build"
muslflags="$(GetMuslStaticLdflags)"
muslflags="--extldflags '-static -fpic' $ldflags"
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
# Keep mips-family targets enabled; sqlite driver selection is handled by Go build tags.
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross mips-linux-musl-cross mips64-linux-musl-cross mips64el-linux-musl-cross mipsel-linux-musl-cross powerpc64le-linux-musl-cross s390x-linux-musl-cross loongarch64-linux-musl-cross)
for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz"
@@ -459,21 +190,19 @@ BuildReleaseLinuxMusl() {
for i in "${!OS_ARCHES[@]}"; do
os_arch=${OS_ARCHES[$i]}
cgo_cc=${CGO_ARGS[$i]}
build_tags=$(GetBuildTagsForTarget "$os_arch")
echo building for ${os_arch}
export GOOS=${os_arch%%-*}
export GOARCH=${os_arch##*-}
export CC=${cgo_cc}
export CGO_ENABLED=1
CGO_LDFLAGS="-static" go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags="$build_tags" .
AssertStaticBinary "./build/$appName-$os_arch"
go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
done
}
BuildReleaseLinuxMuslArm() {
rm -rf .git/
mkdir -p "build"
muslflags="$(GetMuslStaticLdflags)"
muslflags="--extldflags '-static -fpic' $ldflags"
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
FILES=(arm-linux-musleabi-cross arm-linux-musleabihf-cross armel-linux-musleabi-cross armel-linux-musleabihf-cross armv5l-linux-musleabi-cross armv5l-linux-musleabihf-cross armv6-linux-musleabi-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross armv7m-linux-musleabi-cross armv7r-linux-musleabihf-cross)
for i in "${FILES[@]}"; do
@@ -495,12 +224,10 @@ BuildReleaseLinuxMuslArm() {
export CC=${cgo_cc}
export CGO_ENABLED=1
export GOARM=${arm}
CGO_LDFLAGS="-static" go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
AssertStaticBinary "./build/$appName-$os_arch"
go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
done
}
BuildReleaseAndroid() {
rm -rf .git/
mkdir -p "build"
@@ -527,10 +254,9 @@ BuildReleaseFreeBSD() {
mkdir -p "build/freebsd"
# Get latest FreeBSD 14.x release version from GitHub
freebsd_version=$(eval "curl -fsSL --max-time 2 $githubAuthArgs \"https://api.github.com/repos/freebsd/freebsd-src/tags\"" | \
freebsd_version=$(curl -fsSL --max-time 2 $githubAuthHeader $githubAuthValue "https://api.github.com/repos/freebsd/freebsd-src/tags" | \
jq -r '.[].name' | \
grep '^release/14\.' | \
grep -v -- '-p[0-9]*$' | \
sort -V | \
tail -1 | \
sed 's/release\///' | \
@@ -569,172 +295,82 @@ MakeRelease() {
rm -rv compress
fi
mkdir compress
# Add -lite suffix if useLite is true
liteSuffix=""
if [ "$useLite" = true ]; then
liteSuffix="-lite"
fi
for i in $(find . -type f -name "$appName-linux-*"); do
cp "$i" "$appName"
tar -czvf compress/"$i$liteSuffix".tar.gz "$appName"
tar -czvf compress/"$i".tar.gz "$appName"
rm -f "$appName"
done
for i in $(find . -type f -name "$appName-android-*"); do
cp "$i" "$appName"
tar -czvf compress/"$i$liteSuffix".tar.gz "$appName"
tar -czvf compress/"$i".tar.gz "$appName"
rm -f "$appName"
done
for i in $(find . -type f -name "$appName-darwin-*"); do
cp "$i" "$appName"
tar -czvf compress/"$i$liteSuffix".tar.gz "$appName"
tar -czvf compress/"$i".tar.gz "$appName"
rm -f "$appName"
done
for i in $(find . -type f -name "$appName-freebsd-*"); do
cp "$i" "$appName"
tar -czvf compress/"$i$liteSuffix".tar.gz "$appName"
tar -czvf compress/"$i".tar.gz "$appName"
rm -f "$appName"
done
for i in $(find . -type f \( -name "$appName-windows-*" -o -name "$appName-windows7-*" \)); do
for i in $(find . -type f -name "$appName-windows-*"); do
cp "$i" "$appName".exe
zip compress/$(echo $i | sed 's/\.[^.]*$//')$liteSuffix.zip "$appName".exe
zip compress/$(echo $i | sed 's/\.[^.]*$//').zip "$appName".exe
rm -f "$appName".exe
done
cd compress
# Handle MD5 filename - add -lite suffix only if not already present
md5FileName="$1"
if [ "$useLite" = true ] && [[ "$1" != *"-lite.txt" ]]; then
md5FileName=$(echo "$1" | sed 's/\.txt$/-lite.txt/')
fi
find . -type f -print0 | xargs -0 md5sum >"$md5FileName"
cat "$md5FileName"
find . -type f -print0 | xargs -0 md5sum >"$1"
cat "$1"
cd ../..
}
# Parse parameters to handle lite parameter position flexibility
buildType=""
dockerType=""
otherParam=""
for arg in "$@"; do
case $arg in
dev|beta|release|zip|prepare)
if [ -z "$buildType" ]; then
buildType="$arg"
fi
;;
docker|docker-multiplatform|linux_musl_arm|linux_musl|android|freebsd|web)
if [ -z "$dockerType" ]; then
dockerType="$arg"
fi
;;
lite)
# lite parameter is already handled above
;;
*)
if [ -z "$otherParam" ]; then
otherParam="$arg"
fi
;;
esac
done
if [ "$buildType" = "dev" ]; then
FetchWebRolling
if [ "$dockerType" = "docker" ]; then
if [ "$1" = "dev" ]; then
FetchWebDev
if [ "$2" = "docker" ]; then
BuildDocker
elif [ "$dockerType" = "docker-multiplatform" ]; then
elif [ "$2" = "docker-multiplatform" ]; then
BuildDockerMultiplatform
elif [ "$dockerType" = "web" ]; then
elif [ "$2" = "web" ]; then
echo "web only"
else
BuildDev
fi
elif [ "$buildType" = "release" -o "$buildType" = "beta" ]; then
if [ "$buildType" = "beta" ]; then
FetchWebRolling
elif [ "$1" = "release" -o "$1" = "beta" ]; then
if [ "$1" = "beta" ]; then
FetchWebDev
else
FetchWebRelease
fi
if [ "$dockerType" = "docker" ]; then
if [ "$2" = "docker" ]; then
BuildDocker
elif [ "$dockerType" = "docker-multiplatform" ]; then
elif [ "$2" = "docker-multiplatform" ]; then
BuildDockerMultiplatform
elif [ "$dockerType" = "linux_musl_arm" ]; then
elif [ "$2" = "linux_musl_arm" ]; then
BuildReleaseLinuxMuslArm
if [ "$useLite" = true ]; then
MakeRelease "md5-linux-musl-arm-lite.txt"
else
MakeRelease "md5-linux-musl-arm.txt"
fi
elif [ "$dockerType" = "linux_musl" ]; then
MakeRelease "md5-linux-musl-arm.txt"
elif [ "$2" = "linux_musl" ]; then
BuildReleaseLinuxMusl
if [ "$useLite" = true ]; then
MakeRelease "md5-linux-musl-lite.txt"
else
MakeRelease "md5-linux-musl.txt"
fi
elif [ "$dockerType" = "android" ]; then
MakeRelease "md5-linux-musl.txt"
elif [ "$2" = "android" ]; then
BuildReleaseAndroid
if [ "$useLite" = true ]; then
MakeRelease "md5-android-lite.txt"
else
MakeRelease "md5-android.txt"
fi
elif [ "$dockerType" = "freebsd" ]; then
MakeRelease "md5-android.txt"
elif [ "$2" = "freebsd" ]; then
BuildReleaseFreeBSD
if [ "$useLite" = true ]; then
MakeRelease "md5-freebsd-lite.txt"
else
MakeRelease "md5-freebsd.txt"
fi
elif [ "$dockerType" = "web" ]; then
MakeRelease "md5-freebsd.txt"
elif [ "$2" = "web" ]; then
echo "web only"
else
BuildRelease
if [ "$useLite" = true ]; then
MakeRelease "md5-lite.txt"
else
MakeRelease "md5.txt"
fi
MakeRelease "md5.txt"
fi
elif [ "$buildType" = "prepare" ]; then
if [ "$dockerType" = "docker-multiplatform" ]; then
elif [ "$1" = "prepare" ]; then
if [ "$2" = "docker-multiplatform" ]; then
PrepareBuildDockerMusl
fi
elif [ "$buildType" = "zip" ]; then
if [ -n "$otherParam" ]; then
if [ "$useLite" = true ]; then
MakeRelease "$otherParam-lite.txt"
else
MakeRelease "$otherParam.txt"
fi
elif [ -n "$dockerType" ]; then
if [ "$useLite" = true ]; then
MakeRelease "$dockerType-lite.txt"
else
MakeRelease "$dockerType.txt"
fi
else
if [ "$useLite" = true ]; then
MakeRelease "md5-lite.txt"
else
MakeRelease "md5.txt"
fi
fi
elif [ "$1" = "zip" ]; then
MakeRelease "$2".txt
else
echo -e "Parameter error"
echo -e "Usage: $0 {dev|beta|release|zip|prepare} [docker|docker-multiplatform|linux_musl_arm|linux_musl|android|freebsd|web] [lite] [other_params]"
echo -e "Examples:"
echo -e " $0 dev"
echo -e " $0 dev lite"
echo -e " $0 dev docker"
echo -e " $0 dev docker lite"
echo -e " $0 release"
echo -e " $0 release lite"
echo -e " $0 release docker lite"
echo -e " $0 release linux_musl"
fi
+22 -29
View File
@@ -4,14 +4,11 @@ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
package cmd
import (
"fmt"
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/setting"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
"github.com/OpenListTeam/OpenList/internal/conf"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/OpenListTeam/OpenList/internal/setting"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/pkg/utils/random"
"github.com/spf13/cobra"
)
@@ -21,17 +18,16 @@ var AdminCmd = &cobra.Command{
Aliases: []string{"password"},
Short: "Show admin user's info and some operations about admin user's password",
Run: func(cmd *cobra.Command, args []string) {
bootstrap.Init()
defer bootstrap.Release()
Init()
defer Release()
admin, err := op.GetAdmin()
if err != nil {
utils.Log.Errorf("failed get admin user: %+v", err)
} else {
utils.Log.Infof("get admin user from CLI")
fmt.Println("Admin user's username:", admin.Username)
fmt.Println("The password can only be output at the first startup, and then stored as a hash value, which cannot be reversed")
fmt.Println("You can reset the password with a random string by running [openlist admin random]")
fmt.Println("You can also set a new password by running [openlist admin set NEW_PASSWORD]")
utils.Log.Infof("Admin user's username: %s", admin.Username)
utils.Log.Infof("The password can only be output at the first startup, and then stored as a hash value, which cannot be reversed")
utils.Log.Infof("You can reset the password with a random string by running [openlist admin random]")
utils.Log.Infof("You can also set a new password by running [openlist admin set NEW_PASSWORD]")
}
},
}
@@ -40,7 +36,6 @@ var RandomPasswordCmd = &cobra.Command{
Use: "random",
Short: "Reset admin user's password to a random string",
Run: func(cmd *cobra.Command, args []string) {
utils.Log.Infof("reset admin user's password to a random string from CLI")
newPwd := random.String(8)
setAdminPassword(newPwd)
},
@@ -49,12 +44,12 @@ var RandomPasswordCmd = &cobra.Command{
var SetPasswordCmd = &cobra.Command{
Use: "set",
Short: "Set admin user's password",
RunE: func(cmd *cobra.Command, args []string) error {
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
return fmt.Errorf("Please enter the new password")
utils.Log.Errorf("Please enter the new password")
return
}
setAdminPassword(args[0])
return nil
},
}
@@ -62,17 +57,16 @@ var ShowTokenCmd = &cobra.Command{
Use: "token",
Short: "Show admin token",
Run: func(cmd *cobra.Command, args []string) {
bootstrap.Init()
defer bootstrap.Release()
Init()
defer Release()
token := setting.GetStr(conf.Token)
utils.Log.Infof("show admin token from CLI")
fmt.Println("Admin token:", token)
utils.Log.Infof("Admin token: %s", token)
},
}
func setAdminPassword(pwd string) {
bootstrap.Init()
defer bootstrap.Release()
Init()
defer Release()
admin, err := op.GetAdmin()
if err != nil {
utils.Log.Errorf("failed get admin user: %+v", err)
@@ -83,10 +77,9 @@ func setAdminPassword(pwd string) {
utils.Log.Errorf("failed update admin user: %+v", err)
return
}
utils.Log.Infof("admin user has been update from CLI")
fmt.Println("admin user has been updated:")
fmt.Println("username:", admin.Username)
fmt.Println("password:", pwd)
utils.Log.Infof("admin user has been updated:")
utils.Log.Infof("username: %s", admin.Username)
utils.Log.Infof("password: %s", pwd)
DelAdminCacheOnline()
}
+5 -9
View File
@@ -4,11 +4,8 @@ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
package cmd
import (
"fmt"
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/spf13/cobra"
)
@@ -17,8 +14,8 @@ var Cancel2FACmd = &cobra.Command{
Use: "cancel2fa",
Short: "Delete 2FA of admin user",
Run: func(cmd *cobra.Command, args []string) {
bootstrap.Init()
defer bootstrap.Release()
Init()
defer Release()
admin, err := op.GetAdmin()
if err != nil {
utils.Log.Errorf("failed to get admin user: %+v", err)
@@ -27,8 +24,7 @@ var Cancel2FACmd = &cobra.Command{
if err != nil {
utils.Log.Errorf("failed to cancel 2FA: %+v", err)
} else {
utils.Log.Infof("2FA is canceled from CLI")
fmt.Println("2FA canceled")
utils.Log.Info("2FA canceled")
DelAdminCacheOnline()
}
}
+12 -4
View File
@@ -5,17 +5,25 @@ import (
"path/filepath"
"strconv"
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/bootstrap"
"github.com/OpenListTeam/OpenList/internal/bootstrap/data"
"github.com/OpenListTeam/OpenList/internal/db"
"github.com/OpenListTeam/OpenList/pkg/utils"
log "github.com/sirupsen/logrus"
)
func Init() {
bootstrap.Init()
bootstrap.InitConfig()
bootstrap.Log()
bootstrap.InitDB()
data.InitData()
bootstrap.InitStreamLimit()
bootstrap.InitIndex()
bootstrap.InitUpgradePatch()
}
func Release() {
bootstrap.Release()
db.Close()
}
var pid = -1
-309
View File
@@ -1,309 +0,0 @@
package cmd
import (
"io"
"os"
"path"
"path/filepath"
"strings"
rcCrypt "github.com/rclone/rclone/backend/crypt"
"github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/config/obscure"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// encryption and decryption command format for Crypt driver
type options struct {
op string //decrypt or encrypt
src string //source dir or file
dst string //out destination
pwd string //de/encrypt password
salt string
filenameEncryption string //reference drivers\crypt\meta.go Addtion
dirnameEncryption string
filenameEncode string
suffix string
}
var opt options
// CryptCmd represents the crypt command
var CryptCmd = &cobra.Command{
Use: "crypt",
Short: "Encrypt or decrypt local file or dir",
Example: `openlist crypt -s ./src/encrypt/ --op=de --pwd=123456 --salt=345678`,
Run: func(cmd *cobra.Command, args []string) {
opt.validate()
opt.cryptFileDir()
},
}
func init() {
RootCmd.AddCommand(CryptCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
CryptCmd.Flags().StringVarP(&opt.src, "src", "s", "", "src file or dir to encrypt/decrypt")
CryptCmd.Flags().StringVarP(&opt.dst, "dst", "d", "", "dst dir to output,if not set,output to src dir")
CryptCmd.Flags().StringVar(&opt.op, "op", "", "de or en which stands for decrypt or encrypt")
CryptCmd.Flags().StringVar(&opt.pwd, "pwd", "", "password used to encrypt/decrypt,if not contain ___Obfuscated___ prefix,will be obfuscated before used")
CryptCmd.Flags().StringVar(&opt.salt, "salt", "", "salt used to encrypt/decrypt,if not contain ___Obfuscated___ prefix,will be obfuscated before used")
CryptCmd.Flags().StringVar(&opt.filenameEncryption, "filename-encrypt", "off", "filename encryption mode: off,standard,obfuscate")
CryptCmd.Flags().StringVar(&opt.dirnameEncryption, "dirname-encrypt", "false", "is dirname encryption enabled:true,false")
CryptCmd.Flags().StringVar(&opt.filenameEncode, "filename-encode", "base64", "filename encoding mode: base64,base32,base32768")
CryptCmd.Flags().StringVar(&opt.suffix, "suffix", ".bin", "suffix for encrypted file,default is .bin")
}
func (o *options) validate() {
if o.src == "" {
log.Fatal("src can not be empty")
}
if o.op != "encrypt" && o.op != "decrypt" && o.op != "en" && o.op != "de" {
log.Fatal("op must be encrypt or decrypt")
}
if o.filenameEncryption != "off" && o.filenameEncryption != "standard" && o.filenameEncryption != "obfuscate" {
log.Fatal("filename_encryption must be off,standard,obfuscate")
}
if o.filenameEncode != "base64" && o.filenameEncode != "base32" && o.filenameEncode != "base32768" {
log.Fatal("filename_encode must be base64,base32,base32768")
}
}
func (o *options) cryptFileDir() {
src, _ := filepath.Abs(o.src)
log.Infof("src abs is %v", src)
fileInfo, err := os.Stat(src)
if err != nil {
log.Fatalf("reading file/dir %v failed,err:%v", src, err)
}
pwd := updateObfusParm(o.pwd)
salt := updateObfusParm(o.salt)
//create cipher
config := configmap.Simple{
"password": pwd,
"password2": salt,
"filename_encryption": o.filenameEncryption,
"directory_name_encryption": o.dirnameEncryption,
"filename_encoding": o.filenameEncode,
"suffix": o.suffix,
"pass_bad_blocks": "",
}
log.Infof("config:%v", config)
cipher, err := rcCrypt.NewCipher(config)
if err != nil {
log.Fatalf("create cipher failed,err:%v", err)
}
dst := ""
//check and create dst dir
if o.dst != "" {
dst, _ = filepath.Abs(o.dst)
checkCreateDir(dst)
}
// src is file
if !fileInfo.IsDir() { //file
if dst == "" {
dst = filepath.Dir(src)
}
o.cryptFile(cipher, src, dst)
return
}
// src is dir
if dst == "" {
//if src is dir and not set dst dir ,create ${src}_crypt dir as dst dir
dst = path.Join(filepath.Dir(src), fileInfo.Name()+"_crypt")
}
log.Infof("dst : %v", dst)
dirnameMap := make(map[string]string)
pathSeparator := string(os.PathSeparator)
filepath.Walk(src, func(p string, info os.FileInfo, err error) error {
if err != nil {
log.Errorf("get file %v info failed, err:%v", p, err)
return err
}
if p == src {
return nil
}
log.Infof("current path %v", p)
// relative path
rp := strings.ReplaceAll(p, src, "")
log.Infof("relative path %v", rp)
rpds := strings.Split(rp, pathSeparator)
if info.IsDir() {
// absolute dst dir for current path
dd := ""
if o.dirnameEncryption == "true" {
if o.op == "encrypt" || o.op == "en" {
for i := range rpds {
oname := rpds[i]
if _, ok := dirnameMap[rpds[i]]; ok {
rpds[i] = dirnameMap[rpds[i]]
} else {
rpds[i] = cipher.EncryptDirName(rpds[i])
dirnameMap[oname] = rpds[i]
}
}
dd = path.Join(dst, strings.Join(rpds, pathSeparator))
} else {
for i := range rpds {
oname := rpds[i]
if _, ok := dirnameMap[rpds[i]]; ok {
rpds[i] = dirnameMap[rpds[i]]
} else {
dnn, err := cipher.DecryptDirName(rpds[i])
if err != nil {
log.Fatalf("decrypt dir name %v failed,err:%v", rpds[i], err)
}
rpds[i] = dnn
dirnameMap[oname] = dnn
}
}
dd = path.Join(dst, strings.Join(rpds, pathSeparator))
}
} else {
dd = path.Join(dst, rp)
}
log.Infof("create output dir %v", dd)
checkCreateDir(dd)
return nil
}
// file dst dir
fdd := dst
if o.dirnameEncryption == "true" {
for i := range rpds {
if i == len(rpds)-1 {
break
}
fdd = path.Join(fdd, dirnameMap[rpds[i]])
}
} else {
fdd = path.Join(fdd, strings.Join(rpds[:len(rpds)-1], pathSeparator))
}
log.Infof("file output dir %v", fdd)
o.cryptFile(cipher, p, fdd)
return nil
})
}
func (o *options) cryptFile(cipher *rcCrypt.Cipher, src string, dst string) {
fileInfo, err := os.Stat(src)
if err != nil {
log.Fatalf("get file %v info failed,err:%v", src, err)
}
fd, err := os.OpenFile(src, os.O_RDWR, 0666)
if err != nil {
log.Fatalf("open file %v failed,err:%v", src, err)
}
defer fd.Close()
var cryptSrcReader io.Reader
var outFile string
if o.op == "encrypt" || o.op == "en" {
filename := fileInfo.Name()
if o.filenameEncryption != "off" {
filename = cipher.EncryptFileName(fileInfo.Name())
log.Infof("encrypt file name %v to %v", fileInfo.Name(), filename)
} else {
filename = fileInfo.Name() + o.suffix
}
cryptSrcReader, err = cipher.EncryptData(fd)
if err != nil {
log.Fatalf("encrypt file %v failed,err:%v", src, err)
}
outFile = path.Join(dst, filename)
} else {
filename := fileInfo.Name()
if o.filenameEncryption != "off" {
filename, err = cipher.DecryptFileName(filename)
if err != nil {
log.Fatalf("decrypt file name %v failed,err:%v", src, err)
}
log.Infof("decrypt file name %v to %v, ", fileInfo.Name(), filename)
} else {
filename = strings.TrimSuffix(filename, o.suffix)
}
cryptSrcReader, err = cipher.DecryptData(fd)
if err != nil {
log.Fatalf("decrypt file %v failed,err:%v", src, err)
}
outFile = path.Join(dst, filename)
}
//write new file
wr, err := os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
log.Fatalf("create file %v failed,err:%v", outFile, err)
}
defer wr.Close()
_, err = io.Copy(wr, cryptSrcReader)
if err != nil {
log.Fatalf("write file %v failed,err:%v", outFile, err)
}
}
// check dir exist ,if not ,create
func checkCreateDir(dir string) {
_, err := os.Stat(dir)
if os.IsNotExist(err) {
err := os.MkdirAll(dir, 0755)
if err != nil {
log.Fatalf("create dir %v failed,err:%v", dir, err)
}
return
} else if err != nil {
log.Fatalf("read dir %v err: %v", dir, err)
}
}
func updateObfusParm(str string) string {
obfuscatedPrefix := "___Obfuscated___"
if !strings.HasPrefix(str, obfuscatedPrefix) {
str, err := obscure.Obscure(str)
if err != nil {
log.Fatalf("update obfuscated parameter failed,err:%v", str)
}
} else {
str, _ = strings.CutPrefix(str, obfuscatedPrefix)
}
return str
}
-1
View File
@@ -2,7 +2,6 @@ package flags
var (
DataDir string
ConfigPath string
Debug bool
NoPrefix bool
Dev bool
+9 -26
View File
@@ -8,14 +8,15 @@ import (
"fmt"
"io"
"os"
"reflect"
"strings"
_ "github.com/OpenListTeam/OpenList/v4/drivers"
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap/data"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
_ "github.com/OpenListTeam/OpenList/drivers"
"github.com/OpenListTeam/OpenList/internal/bootstrap"
"github.com/OpenListTeam/OpenList/internal/bootstrap/data"
"github.com/OpenListTeam/OpenList/internal/conf"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/OpenListTeam/OpenList/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@@ -68,33 +69,15 @@ func writeFile(name string, data interface{}) {
log.Errorf("failed to unmarshal json: %+v", err)
return
}
if mergeJson(newData, oldData) {
if reflect.DeepEqual(oldData, newData) {
log.Infof("%s.json no changed, skip", name)
} else {
log.Infof("%s.json changed, update file", name)
//log.Infof("old: %+v\nnew:%+v", oldData, data)
utils.WriteJsonToFile(fmt.Sprintf("lang/%s.json", name), oldData, true)
utils.WriteJsonToFile(fmt.Sprintf("lang/%s.json", name), newData, true)
}
}
func mergeJson(source, target map[string]interface{}) bool {
equal := true
for k, v := range source {
tgtV, tgtOk := target[k]
if !tgtOk {
equal = false
target[k] = v
} else {
srcMap, srcIsMap := v.(map[string]interface{})
tgtMap, tgtIsMap := tgtV.(map[string]interface{})
if srcIsMap && tgtIsMap {
equal = mergeJson(srcMap, tgtMap) && equal
}
}
}
return equal
}
func generateDriversJson() {
drivers := make(Drivers)
drivers["drivers"] = make(KV[interface{}])
+7 -8
View File
@@ -4,10 +4,10 @@ import (
"fmt"
"os"
"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/OpenListTeam/OpenList/cmd/flags"
_ "github.com/OpenListTeam/OpenList/drivers"
_ "github.com/OpenListTeam/OpenList/internal/archive"
_ "github.com/OpenListTeam/OpenList/internal/offline_download"
"github.com/spf13/cobra"
)
@@ -15,8 +15,8 @@ var RootCmd = &cobra.Command{
Use: "openlist",
Short: "A file list program that supports multiple storage.",
Long: `A file list program that supports multiple storage,
built with love by OpenListTeam.
Complete documentation is available at https://doc.oplist.org/`,
built with love by Xhofe and friends in Go/Solid.js.
Complete documentation is available at https://docs.openlist.team/`,
}
func Execute() {
@@ -27,8 +27,7 @@ func Execute() {
}
func init() {
RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "data directory (relative paths are resolved against the current working directory)")
RootCmd.PersistentFlags().StringVar(&flags.ConfigPath, "config", "", "path to config.json (relative to current working directory; defaults to [data directory]/config.json, where [data directory] is set by --data)")
RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "data folder")
RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
+195 -5
View File
@@ -1,13 +1,31 @@
package cmd
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"sync"
"syscall"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
"github.com/OpenListTeam/OpenList/cmd/flags"
"github.com/OpenListTeam/OpenList/internal/bootstrap"
"github.com/OpenListTeam/OpenList/internal/conf"
"github.com/OpenListTeam/OpenList/internal/fs"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/server"
"github.com/OpenListTeam/sftpd-openlist"
ftpserver "github.com/fclairamb/ftpserverlib"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
// ServerCmd represents the server command
@@ -17,9 +35,128 @@ var ServerCmd = &cobra.Command{
Long: `Start the server at the specified address
the address is defined in config file`,
Run: func(cmd *cobra.Command, args []string) {
bootstrap.Init()
defer bootstrap.Release()
bootstrap.Start()
Init()
if conf.Conf.DelayedStart != 0 {
utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
}
bootstrap.InitOfflineDownloadTools()
bootstrap.LoadStorages()
bootstrap.InitTaskManager()
if !flags.Debug && !flags.Dev {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.Init(r)
var httpHandler http.Handler = r
if conf.Conf.Scheme.EnableH2c {
httpHandler = h2c.NewHandler(r, &http2.Server{})
}
var httpSrv, httpsSrv, unixSrv *http.Server
if conf.Conf.Scheme.HttpPort != -1 {
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
utils.Log.Infof("start HTTP server @ %s", httpBase)
httpSrv = &http.Server{Addr: httpBase, Handler: httpHandler}
go func() {
err := httpSrv.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
utils.Log.Fatalf("failed to start http: %s", err.Error())
}
}()
}
if conf.Conf.Scheme.HttpsPort != -1 {
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
utils.Log.Infof("start HTTPS server @ %s", httpsBase)
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
go func() {
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
utils.Log.Fatalf("failed to start https: %s", err.Error())
}
}()
}
if conf.Conf.Scheme.UnixFile != "" {
utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile)
unixSrv = &http.Server{Handler: httpHandler}
go func() {
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
if err != nil {
utils.Log.Fatalf("failed to listen unix: %+v", err)
}
// set socket file permission
mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)
if err != nil {
utils.Log.Errorf("failed to parse socket file permission: %+v", err)
} else {
err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))
if err != nil {
utils.Log.Errorf("failed to chmod socket file: %+v", err)
}
}
err = unixSrv.Serve(listener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
utils.Log.Fatalf("failed to start unix: %s", err.Error())
}
}()
}
if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
s3r := gin.New()
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.InitS3(s3r)
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
utils.Log.Infof("start S3 server @ %s", s3Base)
go func() {
var err error
if conf.Conf.S3.SSL {
httpsSrv = &http.Server{Addr: s3Base, Handler: s3r}
err = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
}
if !conf.Conf.S3.SSL {
httpSrv = &http.Server{Addr: s3Base, Handler: s3r}
err = httpSrv.ListenAndServe()
}
if err != nil && !errors.Is(err, http.ErrServerClosed) {
utils.Log.Fatalf("failed to start s3 server: %s", err.Error())
}
}()
}
var ftpDriver *server.FtpMainDriver
var ftpServer *ftpserver.FtpServer
if conf.Conf.FTP.Listen != "" && conf.Conf.FTP.Enable {
var err error
ftpDriver, err = server.NewMainDriver()
if err != nil {
utils.Log.Fatalf("failed to start ftp driver: %s", err.Error())
} else {
utils.Log.Infof("start ftp server on %s", conf.Conf.FTP.Listen)
go func() {
ftpServer = ftpserver.NewFtpServer(ftpDriver)
err = ftpServer.ListenAndServe()
if err != nil {
utils.Log.Fatalf("problem ftp server listening: %s", err.Error())
}
}()
}
}
var sftpDriver *server.SftpDriver
var sftpServer *sftpd.SftpServer
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable {
var err error
sftpDriver, err = server.NewSftpDriver()
if err != nil {
utils.Log.Fatalf("failed to start sftp driver: %s", err.Error())
} else {
utils.Log.Infof("start sftp server on %s", conf.Conf.SFTP.Listen)
go func() {
sftpServer = sftpd.NewSftpServer(sftpDriver)
err = sftpServer.RunServer()
if err != nil {
utils.Log.Fatalf("problem sftp server listening: %s", err.Error())
}
}()
}
}
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 1 second.
quit := make(chan os.Signal, 1)
@@ -28,7 +165,60 @@ the address is defined in config file`,
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
bootstrap.Shutdown(1 * time.Second)
utils.Log.Println("Shutdown server...")
fs.ArchiveContentUploadTaskManager.RemoveAll()
Release()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
var wg sync.WaitGroup
if conf.Conf.Scheme.HttpPort != -1 {
wg.Add(1)
go func() {
defer wg.Done()
if err := httpSrv.Shutdown(ctx); err != nil {
utils.Log.Fatal("HTTP server shutdown err: ", err)
}
}()
}
if conf.Conf.Scheme.HttpsPort != -1 {
wg.Add(1)
go func() {
defer wg.Done()
if err := httpsSrv.Shutdown(ctx); err != nil {
utils.Log.Fatal("HTTPS server shutdown err: ", err)
}
}()
}
if conf.Conf.Scheme.UnixFile != "" {
wg.Add(1)
go func() {
defer wg.Done()
if err := unixSrv.Shutdown(ctx); err != nil {
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()
utils.Log.Println("Server exit")
},
}
+23 -61
View File
@@ -4,13 +4,11 @@ Copyright © 2023 NAME HERE <EMAIL ADDRESS>
package cmd
import (
"fmt"
"os"
"strconv"
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
"github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/db"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
@@ -24,61 +22,28 @@ var storageCmd = &cobra.Command{
}
var disableStorageCmd = &cobra.Command{
Use: "disable [mount path]",
Short: "Disable a storage by mount path",
RunE: func(cmd *cobra.Command, args []string) error {
Use: "disable",
Short: "Disable a storage",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
return fmt.Errorf("mount path is required")
utils.Log.Errorf("mount path is required")
return
}
mountPath := args[0]
bootstrap.Init()
defer bootstrap.Release()
Init()
defer Release()
storage, err := db.GetStorageByMountPath(mountPath)
if err != nil {
return fmt.Errorf("failed to query storage: %+v", err)
}
storage.Disabled = true
err = db.UpdateStorage(storage)
if err != nil {
return fmt.Errorf("failed to update storage: %+v", err)
}
utils.Log.Infof("Storage with mount path [%s] has been disabled from CLI", mountPath)
fmt.Printf("Storage with mount path [%s] has been disabled\n", mountPath)
return nil
},
}
var deleteStorageCmd = &cobra.Command{
Use: "delete [id]",
Short: "Delete a storage by id",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("id is required")
}
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("id must be a number")
}
if force, _ := cmd.Flags().GetBool("force"); force {
fmt.Printf("Are you sure you want to delete storage with id [%d]? [y/N]: ", id)
var confirm string
fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
fmt.Println("Delete operation cancelled.")
return nil
utils.Log.Errorf("failed to query storage: %+v", err)
} else {
storage.Disabled = true
err = db.UpdateStorage(storage)
if err != nil {
utils.Log.Errorf("failed to update storage: %+v", err)
} else {
utils.Log.Infof("Storage with mount path [%s] have been disabled", mountPath)
}
}
bootstrap.Init()
defer bootstrap.Release()
err = db.DeleteStorageById(uint(id))
if err != nil {
return fmt.Errorf("failed to delete storage by id: %+v", err)
}
utils.Log.Infof("Storage with id [%d] have been deleted from CLI", id)
fmt.Printf("Storage with id [%d] have been deleted\n", id)
return nil
},
}
@@ -123,14 +88,14 @@ var storageTableHeight int
var listStorageCmd = &cobra.Command{
Use: "list",
Short: "List all storages",
RunE: func(cmd *cobra.Command, args []string) error {
bootstrap.Init()
defer bootstrap.Release()
Run: func(cmd *cobra.Command, args []string) {
Init()
defer Release()
storages, _, err := db.GetStorages(1, -1)
if err != nil {
return fmt.Errorf("failed to query storages: %+v", err)
utils.Log.Errorf("failed to query storages: %+v", err)
} else {
fmt.Printf("Found %d storages\n", len(storages))
utils.Log.Infof("Found %d storages", len(storages))
columns := []table.Column{
{Title: "ID", Width: 4},
{Title: "Driver", Width: 16},
@@ -173,11 +138,10 @@ var listStorageCmd = &cobra.Command{
m := model{t}
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Printf("failed to run program: %+v\n", err)
utils.Log.Errorf("failed to run program: %+v", err)
os.Exit(1)
}
}
return nil
},
}
@@ -187,8 +151,6 @@ func init() {
storageCmd.AddCommand(disableStorageCmd)
storageCmd.AddCommand(listStorageCmd)
storageCmd.PersistentFlags().IntVarP(&storageTableHeight, "height", "H", 10, "Table height")
storageCmd.AddCommand(deleteStorageCmd)
deleteStorageCmd.Flags().BoolP("force", "f", false, "Force delete without confirmation")
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
+4 -4
View File
@@ -5,10 +5,10 @@ import (
"fmt"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/setting"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/conf"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/OpenListTeam/OpenList/internal/setting"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2"
)
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"os"
"runtime"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/internal/conf"
"github.com/spf13/cobra"
)
+5 -3
View File
@@ -1,3 +1,4 @@
version: '3.3'
services:
openlist:
restart: always
@@ -6,9 +7,10 @@ services:
ports:
- '5244:5244'
- '5245:5245'
user: '0:0'
environment:
- PUID=0
- PGID=0
- UMASK=022
- TZ=Asia/Shanghai
- TZ=UTC
container_name: openlist
image: 'openlistteam/openlist:latest'
image: 'ghcr.io/openlistteam/openlist:latest'
+16 -33
View File
@@ -1,60 +1,43 @@
package _115
import (
"errors"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/drivers/base"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
log "github.com/sirupsen/logrus"
)
var (
md5Salt = "Qclm8MGWUv59TnrR0XPg"
appVer = "35.6.0.3"
appVer = "27.0.5.7"
)
func (d *Pan115) getAppVersion() (string, error) {
result := VersionResp{}
res, err := base.RestyClient.R().Get(driver115.ApiGetVersion)
func (d *Pan115) getAppVersion() ([]driver115.AppVersion, error) {
result := driver115.VersionResp{}
resp, err := base.RestyClient.R().Get(driver115.ApiGetVersion)
err = driver115.CheckErr(err, &result, resp)
if err != nil {
return "", err
return nil, 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
return result.Data.GetAppVersions(), nil
}
func (d *Pan115) getAppVer() string {
ver, err := d.getAppVersion()
// todo add some cache?
vers, err := d.getAppVersion()
if err != nil {
log.Warnf("[115] get app version failed: %v", err)
return appVer
}
if len(ver) > 0 {
return ver
for _, ver := range vers {
if ver.AppName == "win" {
return ver.Version
}
}
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"`
}
+12 -25
View File
@@ -5,12 +5,10 @@ import (
"strings"
"sync"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model"
streamPkg "github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/http_range"
"github.com/OpenListTeam/OpenList/pkg/utils"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/pkg/errors"
"golang.org/x/time/rate"
@@ -69,10 +67,8 @@ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
return nil, err
}
userAgent := args.Header.Get("User-Agent")
if userAgent == "" {
userAgent = base.UserAgent
}
downloadInfo, err := d.client.DownloadWithUA(file.(*FileObj).PickCode, userAgent)
downloadInfo, err := d.
DownloadWithUA(file.(*FileObj).PickCode, userAgent)
if err != nil {
return nil, err
}
@@ -188,8 +184,12 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
}
preHash = strings.ToUpper(preHash)
fullHash := stream.GetHash().GetHash(utils.SHA1)
if len(fullHash) != utils.SHA1.Width {
_, fullHash, err = streamPkg.CacheFullAndHash(stream, &up, utils.SHA1)
if len(fullHash) <= 0 {
tmpF, err := stream.CacheFullInTempFile()
if err != nil {
return nil, err
}
fullHash, err = utils.HashFile(utils.SHA1, tmpF)
if err != nil {
return nil, err
}
@@ -248,17 +248,4 @@ func (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, delete
return d.client.DeleteOfflineTasks(hashes, deleteFiles)
}
func (d *Pan115) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
info, err := d.client.GetInfo()
if err != nil {
return nil, err
}
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: info.SpaceInfo.AllTotal.Size,
UsedSpace: info.SpaceInfo.AllUse.Size,
},
}, nil
}
var _ driver.Driver = (*Pan115)(nil)
+7 -5
View File
@@ -1,8 +1,8 @@
package _115
import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/op"
)
type Addition struct {
@@ -15,9 +15,11 @@ type Addition struct {
}
var config = driver.Config{
Name: "115 Cloud",
DefaultRoot: "0",
LinkCacheMode: driver.LinkCacheUA,
Name: "115 Cloud",
DefaultRoot: "0",
// OnlyProxy: true,
// OnlyLocal: true,
// NoOverwriteUpload: true,
}
func init() {
+2 -6
View File
@@ -3,8 +3,8 @@ package _115
import (
"time"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/SheltonZhu/115driver/pkg/driver"
)
@@ -22,10 +22,6 @@ func (f *FileObj) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.SHA1, f.Sha1)
}
func (f *FileObj) Thumb() string {
return f.ThumbURL
}
type UploadResult struct {
driver.BasicResp
Data struct {
+68 -13
View File
@@ -9,6 +9,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
@@ -16,15 +17,16 @@ import (
"sync/atomic"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model"
netutil "github.com/OpenListTeam/OpenList/v4/internal/net"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
cipher "github.com/SheltonZhu/115driver/pkg/crypto/ec115"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/OpenListTeam/OpenList/internal/conf"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/http_range"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
cipher "github.com/SheltonZhu/115driver/pkg/crypto/ec115"
crypto "github.com/SheltonZhu/115driver/pkg/crypto/m115"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/pkg/errors"
)
@@ -106,6 +108,60 @@ func (d *Pan115) getUA() string {
return fmt.Sprintf("Mozilla/5.0 115Browser/%s", appVer)
}
func (d *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
key := crypto.GenerateKey()
result := driver115.DownloadResp{}
params, err := utils.Json.Marshal(map[string]string{"pick_code": pickCode})
if err != nil {
return nil, err
}
data := crypto.Encode(params, key)
bodyReader := strings.NewReader(url.Values{"data": []string{data}}.Encode())
reqUrl := fmt.Sprintf("%s?t=%s", driver115.AndroidApiDownloadGetUrl, driver115.Now().String())
req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cookie", d.Cookie)
req.Header.Set("User-Agent", ua)
resp, err := d.client.Client.GetClient().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err := utils.Json.Unmarshal(body, &result); err != nil {
return nil, err
}
if err = result.Err(string(body)); err != nil {
return nil, err
}
b, err := crypto.Decode(string(result.EncodedData), key)
if err != nil {
return nil, err
}
downloadInfo := struct {
Url string `json:"url"`
}{}
if err := utils.Json.Unmarshal(b, &downloadInfo); err != nil {
return nil, err
}
info := &driver115.DownloadInfo{}
info.PickCode = pickCode
info.Header = resp.Request.Header
info.Url.Url = downloadInfo.Url
return info, nil
}
func (c *Pan115) GenerateToken(fileID, preID, timeStamp, fileSize, signKey, signVal string) string {
userID := strconv.FormatInt(c.client.UserID, 10)
userIDMd5 := md5.Sum([]byte(userID))
@@ -223,7 +279,7 @@ func (c *Pan115) UploadByOSS(ctx context.Context, params *driver115.UploadOSSPar
if err != nil {
return nil, err
}
ossClient, err := netutil.NewOSSClient(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)
ossClient, err := oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)
if err != nil {
return nil, err
}
@@ -253,8 +309,7 @@ func (c *Pan115) UploadByOSS(ctx context.Context, params *driver115.UploadOSSPar
// UploadByMultipart upload by mutipart blocks
func (d *Pan115) UploadByMultipart(ctx context.Context, params *driver115.UploadOSSParams, fileSize int64, s model.FileStreamer,
dirID string, up driver.UpdateProgress, opts ...driver115.UploadMultipartOption,
) (*UploadResult, error) {
dirID string, up driver.UpdateProgress, opts ...driver115.UploadMultipartOption) (*UploadResult, error) {
var (
chunks []oss.FileChunk
parts []oss.UploadPart
@@ -266,7 +321,7 @@ func (d *Pan115) UploadByMultipart(ctx context.Context, params *driver115.Upload
err error
)
tmpF, err := s.CacheFullAndWriter(&up, nil)
tmpF, err := s.CacheFullInTempFile()
if err != nil {
return nil, err
}
@@ -284,7 +339,7 @@ func (d *Pan115) UploadByMultipart(ctx context.Context, params *driver115.Upload
return nil, err
}
if ossClient, err = netutil.NewOSSClient(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
return nil, err
}
+49 -133
View File
@@ -2,34 +2,28 @@ package _115_open
import (
"context"
"errors"
"fmt"
"io"
"net/http"
stdpath "path"
"slices"
"strconv"
"strings"
"time"
sdk "github.com/OpenListTeam/115-sdk-go"
"github.com/OpenListTeam/OpenList/v4/cmd/flags"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/cmd/flags"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/OpenListTeam/OpenList/pkg/utils"
sdk "github.com/xhofe/115-sdk-go"
"golang.org/x/time/rate"
)
type Open115 struct {
model.Storage
Addition
client *sdk.Client
limiter *rate.Limiter
parentPath string
client *sdk.Client
limiter *rate.Limiter
}
func (d *Open115) Config() driver.Config {
@@ -58,34 +52,6 @@ func (d *Open115) Init(ctx context.Context) error {
if d.Addition.LimitRate > 0 {
d.limiter = rate.NewLimiter(rate.Limit(d.Addition.LimitRate), 1)
}
if d.PageSize <= 0 {
d.PageSize = 200
} else if d.PageSize > 1150 {
d.PageSize = 1150
}
// add parent path
d.parentPath = "/"
if d.GetRootId() != d.Config().DefaultRoot {
folderInfo, err := d.client.GetFolderInfo(ctx, d.GetRootId())
if err != nil {
return err
}
if folderInfo.FileID != d.Config().DefaultRoot {
d.parentPath = stdpath.Join(d.parentPath, folderInfo.FileName)
}
parentPaths := folderInfo.Paths
slices.Reverse(parentPaths)
for _, parentPathInfo := range parentPaths {
if parentPathInfo.FileID == d.Config().DefaultRoot {
d.parentPath = stdpath.Join("/", d.parentPath)
} else {
d.parentPath = stdpath.Join("/", parentPathInfo.FileName, d.parentPath)
}
}
}
return nil
}
@@ -102,7 +68,7 @@ func (d *Open115) Drop(ctx context.Context) error {
func (d *Open115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
var res []model.Obj
pageSize := int64(d.PageSize)
pageSize := int64(200)
offset := int64(0)
for {
if err := d.WaitLimit(ctx); err != nil {
@@ -164,27 +130,6 @@ func (d *Open115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
}, nil
}
func (d *Open115) Get(ctx context.Context, path string) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
path = stdpath.Join(d.parentPath, path)
resp, err := d.client.GetFolderInfoByPath(ctx, path)
if err != nil {
if errors.Is(err, sdk.ErrObjectNotFound) {
return nil, errs.ObjectNotFound
}
return nil, err
}
return &Obj{
Fid: resp.FileID,
Fn: resp.FileName,
Fc: resp.FileCategory,
Sha1: resp.Sha1,
Pc: resp.PickCode,
}, nil
}
func (d *Open115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return nil, err
@@ -204,15 +149,18 @@ func (d *Open115) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
}, nil
}
func (d *Open115) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
func (d *Open115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return err
return nil, err
}
_, err := d.client.Move(ctx, &sdk.MoveReq{
FileIDs: srcObj.GetID(),
ToCid: dstDir.GetID(),
})
return err
if err != nil {
return nil, err
}
return srcObj, nil
}
func (d *Open115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
@@ -220,8 +168,8 @@ func (d *Open115) Rename(ctx context.Context, srcObj model.Obj, newName string)
return nil, err
}
_, err := d.client.UpdateFile(ctx, &sdk.UpdateFileReq{
FileID: srcObj.GetID(),
FileName: newName,
FileID: srcObj.GetID(),
FileNma: newName,
})
if err != nil {
return nil, err
@@ -229,21 +177,23 @@ func (d *Open115) Rename(ctx context.Context, srcObj model.Obj, newName string)
obj, ok := srcObj.(*Obj)
if ok {
obj.Fn = newName
return srcObj, nil
}
return nil, nil
return srcObj, nil
}
func (d *Open115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
func (d *Open115) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
if err := d.WaitLimit(ctx); err != nil {
return err
return nil, err
}
_, err := d.client.Copy(ctx, &sdk.CopyReq{
PID: dstDir.GetID(),
FileID: srcObj.GetID(),
NoDupli: "1",
})
return err
if err != nil {
return nil, err
}
return srcObj, nil
}
func (d *Open115) Remove(ctx context.Context, obj model.Obj) error {
@@ -265,27 +215,28 @@ func (d *Open115) Remove(ctx context.Context, obj model.Obj) error {
}
func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
err := d.WaitLimit(ctx)
if err := d.WaitLimit(ctx); err != nil {
return err
}
tempF, err := file.CacheFullInTempFile()
if err != nil {
return err
}
sha1 := file.GetHash().GetHash(utils.SHA1)
if len(sha1) != utils.SHA1.Width {
_, sha1, err = stream.CacheFullAndHash(file, &up, utils.SHA1)
if err != nil {
return err
}
}
const PreHashSize int64 = 128 * utils.KB
hashSize := PreHashSize
if file.GetSize() < PreHashSize {
hashSize = file.GetSize()
}
reader, err := file.RangeRead(http_range.Range{Start: 0, Length: hashSize})
// cal full sha1
sha1, err := utils.HashReader(utils.SHA1, tempF)
if err != nil {
return err
}
sha1128k, err := utils.HashReader(utils.SHA1, reader)
_, err = tempF.Seek(0, io.SeekStart)
if err != nil {
return err
}
// pre 128k sha1
sha1128k, err := utils.HashReader(utils.SHA1, io.LimitReader(tempF, 128*1024))
if err != nil {
return err
}
_, err = tempF.Seek(0, io.SeekStart)
if err != nil {
return err
}
@@ -301,7 +252,6 @@ func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
return err
}
if resp.Status == 2 {
up(100)
return nil
}
// 2. two way verify
@@ -315,11 +265,15 @@ func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
if err != nil {
return err
}
reader, err = file.RangeRead(http_range.Range{Start: start, Length: end - start + 1})
_, err = tempF.Seek(start, io.SeekStart)
if err != nil {
return err
}
signVal, err := utils.HashReader(utils.SHA1, reader)
signVal, err := utils.HashReader(utils.SHA1, io.LimitReader(tempF, end-start+1))
if err != nil {
return err
}
_, err = tempF.Seek(0, io.SeekStart)
if err != nil {
return err
}
@@ -336,7 +290,6 @@ func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
return err
}
if resp.Status == 2 {
up(100)
return nil
}
}
@@ -346,50 +299,13 @@ func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
return err
}
// 4. upload
err = d.multpartUpload(ctx, file, up, tokenResp, resp)
err = d.multpartUpload(ctx, tempF, file, up, tokenResp, resp)
if err != nil {
return err
}
return nil
}
func (d *Open115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {
return d.client.AddOfflineTaskURIs(ctx, uris, dstDir.GetID())
}
func (d *Open115) DeleteOfflineTask(ctx context.Context, infoHash string, deleteFiles bool) error {
return d.client.DeleteOfflineTask(ctx, infoHash, deleteFiles)
}
func (d *Open115) OfflineList(ctx context.Context) (*sdk.OfflineTaskListResp, error) {
resp, err := d.client.OfflineTaskList(ctx, 1)
if err != nil {
return nil, err
}
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 := ParseInt64(userInfo.RtSpaceInfo.AllTotal.Size)
if err != nil {
return nil, err
}
used, err := ParseInt64(userInfo.RtSpaceInfo.AllUse.Size)
if err != nil {
return nil, err
}
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: total,
UsedSpace: used,
},
}, nil
}
// func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
// // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
// return nil, errs.NotImplement
+14 -7
View File
@@ -1,8 +1,8 @@
package _115_open
import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/op"
)
type Addition struct {
@@ -11,16 +11,23 @@ type Addition struct {
// define other
OrderBy string `json:"order_by" type:"select" options:"file_name,file_size,user_utime,file_type"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"`
LimitRate float64 `json:"limit_rate" type:"float" default:"1" help:"limit all api request rate ([limit]r/1s)"`
PageSize int64 `json:"page_size" type:"number" default:"200" help:"list api per page size of 115open driver"`
LimitRate float64 `json:"limit_rate,string" type:"float" default:"1" help:"limit all api request rate ([limit]r/1s)"`
AccessToken string `json:"access_token" required:"true"`
RefreshToken string `json:"refresh_token" required:"true"`
}
var config = driver.Config{
Name: "115 Open",
DefaultRoot: "0",
LinkCacheMode: driver.LinkCacheUA,
Name: "115 Open",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "0",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
}
func init() {
+3 -3
View File
@@ -3,9 +3,9 @@ package _115_open
import (
"time"
sdk "github.com/OpenListTeam/115-sdk-go"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
sdk "github.com/xhofe/115-sdk-go"
)
type Obj sdk.GetFilesResp_File
+15 -25
View File
@@ -6,14 +6,12 @@ import (
"io"
"time"
sdk "github.com/OpenListTeam/115-sdk-go"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model"
netutil "github.com/OpenListTeam/OpenList/v4/internal/net"
streamPkg "github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/avast/retry-go"
sdk "github.com/xhofe/115-sdk-go"
)
func calPartSize(fileSize int64) int64 {
@@ -37,7 +35,7 @@ func calPartSize(fileSize int64) int64 {
}
func (d *Open115) singleUpload(ctx context.Context, tempF model.File, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {
ossClient, err := netutil.NewOSSClient(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
ossClient, err := oss.New(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
if err != nil {
return err
}
@@ -70,8 +68,11 @@ func (d *Open115) singleUpload(ctx context.Context, tempF model.File, tokenResp
// } `json:"data"`
// }
func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer, up driver.UpdateProgress, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {
ossClient, err := netutil.NewOSSClient(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
func (d *Open115) multpartUpload(ctx context.Context, tempF model.File, stream model.FileStreamer, up driver.UpdateProgress, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {
fileSize := stream.GetSize()
chunkSize := calPartSize(fileSize)
ossClient, err := oss.New(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
if err != nil {
return err
}
@@ -85,13 +86,6 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
return err
}
fileSize := stream.GetSize()
chunkSize := calPartSize(fileSize)
ss, err := streamPkg.NewStreamSectionReader(stream, int(chunkSize), &up)
if err != nil {
return err
}
partNum := (stream.GetSize() + chunkSize - 1) / chunkSize
parts := make([]oss.UploadPart, partNum)
offset := int64(0)
@@ -104,24 +98,20 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
if i == partNum {
partSize = fileSize - (i-1)*chunkSize
}
rd, err := ss.GetSectionReader(offset, partSize)
if err != nil {
return err
}
rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
err = retry.Do(func() error {
rd.Seek(0, io.SeekStart)
part, err := bucket.UploadPart(imur, driver.NewLimitedUploadStream(ctx, rd), partSize, int(i))
_ = rd.Reset()
rateLimitedRd := driver.NewLimitedUploadStream(ctx, rd)
part, err := bucket.UploadPart(imur, rateLimitedRd, partSize, int(i))
if err != nil {
return err
}
parts[i-1] = part
return nil
},
retry.Context(ctx),
retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Second))
ss.FreeSectionReader(rd)
if err != nil {
return err
}
@@ -131,7 +121,7 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
} else {
offset += partSize
}
up(float64(offset) * 100 / float64(fileSize))
up(float64(offset) / float64(fileSize))
}
// callbackRespBytes := make([]byte, 1024)
+1 -13
View File
@@ -1,15 +1,3 @@
package _115_open
import "encoding/json"
func ParseInt64(v json.Number) (int64, error) {
i, err := v.Int64()
if err == nil {
return i, nil
}
f, e1 := v.Float64()
if e1 == nil {
return int64(f), nil
}
return int64(0), err
}
// do others that not defined in Driver interface
+9 -29
View File
@@ -2,13 +2,11 @@ package _115_share
import (
"context"
"net/http"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"golang.org/x/time/rate"
)
@@ -51,16 +49,9 @@ func (d *Pan115Share) List(ctx context.Context, dir model.Obj, args model.ListAr
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
var ua string
// TODO: will use user agent from header
// if args.Header != nil {
// ua = args.Header.Get("User-Agent")
// }
if ua == "" {
ua = base.UserAgentNT
}
files := make([]driver115.ShareFile, 0)
fileResp, err := d.client.GetShareSnapWithUA(ua, d.ShareCode, d.ReceiveCode, dir.GetID(), driver115.QueryLimit(int(d.PageSize)))
fileResp, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, dir.GetID(), driver115.QueryLimit(int(d.PageSize)))
if err != nil {
return nil, err
}
@@ -86,23 +77,12 @@ func (d *Pan115Share) Link(ctx context.Context, file model.Obj, args model.LinkA
if err := d.WaitLimit(ctx); err != nil {
return nil, err
}
var ua string
if args.Header != nil {
ua = args.Header.Get("User-Agent")
}
if ua == "" {
ua = base.UserAgent
}
downloadInfo, err := d.client.DownloadByShareCodeWithUA(ua, d.ShareCode, d.ReceiveCode, file.GetID())
downloadInfo, err := d.client.DownloadByShareCode(d.ShareCode, d.ReceiveCode, file.GetID())
if err != nil {
return nil, err
}
header := http.Header{}
header.Set("User-Agent", ua)
return &model.Link{
URL: downloadInfo.URL.URL,
Header: header,
}, nil
return &model.Link{URL: downloadInfo.URL.URL}, nil
}
func (d *Pan115Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
+8 -3
View File
@@ -1,8 +1,8 @@
package _115_share
import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/op"
)
type Addition struct {
@@ -19,7 +19,12 @@ type Addition struct {
var config = driver.Config{
Name: "115 Share",
DefaultRoot: "0",
NoUpload: true,
// OnlyProxy: true,
// OnlyLocal: true,
CheckStatus: false,
Alert: "",
NoOverwriteUpload: true,
NoUpload: true,
}
func init() {
+5 -10
View File
@@ -5,9 +5,8 @@ import (
"strconv"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/pkg/errors"
)
@@ -21,7 +20,6 @@ type FileObj struct {
FileName string
isDir bool
FileID string
ThumbURL string
}
func (f *FileObj) CreateTime() time.Time {
@@ -56,10 +54,6 @@ func (f *FileObj) GetPath() string {
return ""
}
func (f *FileObj) Thumb() string {
return f.ThumbURL
}
func transFunc(sf driver115.ShareFile) (model.Obj, error) {
timeInt, err := strconv.ParseInt(sf.UpdateTime, 10, 64)
if err != nil {
@@ -80,14 +74,15 @@ func transFunc(sf driver115.ShareFile) (model.Obj, error) {
FileName: string(sf.FileName),
isDir: isDir,
FileID: fileID,
ThumbURL: sf.ThumbURL,
}, nil
}
var UserAgent = driver115.UA115Browser
func (d *Pan115Share) login() error {
var err error
opts := []driver115.Option{
driver115.UA(base.UserAgentNT),
driver115.UA(UserAgent),
}
d.client = driver115.New(opts...)
if _, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, ""); err != nil {
+25 -34
View File
@@ -6,18 +6,17 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/internal/stream"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
@@ -41,9 +40,7 @@ func (d *Pan123) GetAddition() driver.Additional {
}
func (d *Pan123) Init(ctx context.Context) error {
_, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {
req.SetHeader("platform", "web")
}, nil)
_, err := d.Request(UserInfo, http.MethodGet, nil, nil)
return err
}
@@ -66,6 +63,14 @@ func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if f, ok := file.(File); ok {
//var resp DownResp
var headers map[string]string
if !utils.IsLocalIPAddr(args.IP) {
headers = map[string]string{
//"X-Real-IP": "1.1.1.1",
"X-Forwarded-For": args.IP,
}
}
data := base.Json{
"driveId": 0,
"etag": f.Etag,
@@ -76,29 +81,28 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
"type": f.Type,
}
resp, err := d.Request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
req.SetBody(data).SetHeaders(headers)
}, nil)
if err != nil {
return nil, err
}
downloadUrl := utils.Json.Get(resp, "data", "DownloadUrl").ToString()
ou, err := url.Parse(downloadUrl)
u, err := url.Parse(downloadUrl)
if err != nil {
return nil, err
}
u_ := ou.String()
nu := ou.Query().Get("params")
nu := u.Query().Get("params")
if nu != "" {
du, _ := base64.StdEncoding.DecodeString(nu)
u, err := url.Parse(string(du))
u, err = url.Parse(string(du))
if err != nil {
return nil, err
}
u_ = u.String()
}
u_ := u.String()
log.Debug("download url: ", u_)
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://yun.123pan.com/").Get(u_)
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
if err != nil {
return nil, err
}
@@ -113,7 +117,7 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
}
link.Header = http.Header{
"Referer": []string{fmt.Sprintf("%s://%s/", ou.Scheme, ou.Host)},
"Referer": []string{"https://www.123pan.com/"},
}
return &link, nil
} else {
@@ -183,7 +187,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, file model.FileStrea
etag := file.GetHash().GetHash(utils.MD5)
var err error
if len(etag) < utils.MD5.Width {
_, etag, err = stream.CacheFullAndHash(file, &up, utils.MD5)
_, etag, err = stream.CacheFullInTempFileAndHash(file, utils.MD5)
if err != nil {
return err
}
@@ -191,7 +195,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, file model.FileStrea
data := base.Json{
"driveId": 0,
"duplicate": 2, // 2->覆盖 1->重命名 0->默认
"etag": strings.ToLower(etag),
"etag": etag,
"fileName": file.GetName(),
"parentFileId": dstDir.GetID(),
"size": file.GetSize(),
@@ -255,17 +259,4 @@ func (d *Pan123) APIRateLimit(ctx context.Context, api string) error {
return limiter.Wait(ctx)
}
func (d *Pan123) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
userInfo, err := d.getUserInfo(ctx)
if err != nil {
return nil, err
}
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: userInfo.Data.SpacePermanent + userInfo.Data.SpaceTemp,
UsedSpace: userInfo.Data.SpaceUsed,
},
}, nil
}
var _ driver.Driver = (*Pan123)(nil)
+4 -13
View File
@@ -1,8 +1,8 @@
package _123
import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/op"
)
type Addition struct {
@@ -11,26 +11,17 @@ type Addition struct {
driver.RootID
//OrderBy string `json:"order_by" type:"select" options:"file_id,file_name,size,update_at" default:"file_name"`
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
AccessToken string
UploadThread int `json:"UploadThread" type:"number" default:"3" help:"the threads of upload"`
Platform string `json:"platform" type:"string" default:"web" help:"the platform header value, sent with API requests"`
AccessToken string
}
var config = driver.Config{
Name: "123Pan",
DefaultRoot: "0",
LocalSort: true,
PreferProxy: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
// 新增默认选项 要在RegisterDriver初始化设置 才会对正在使用的用户生效
return &Pan123{
Addition: Addition{
UploadThread: 3,
Platform: "web",
},
}
return &Pan123{}
})
}
+3 -59
View File
@@ -7,9 +7,9 @@ import (
"strings"
"time"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/internal/model"
)
type File struct {
@@ -28,7 +28,7 @@ func (f File) CreateTime() time.Time {
}
func (f File) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.MD5, f.Etag)
return utils.HashInfo{}
}
func (f File) GetPath() string {
@@ -122,59 +122,3 @@ type S3PreSignedURLs struct {
PreSignedUrls map[string]string `json:"presignedUrls"`
} `json:"data"`
}
type UserInfoResp struct {
Data struct {
Uid int64 `json:"UID"`
Nickname string `json:"Nickname"`
SpaceUsed int64 `json:"SpaceUsed"`
SpacePermanent int64 `json:"SpacePermanent"`
SpaceTemp int64 `json:"SpaceTemp"`
FileCount int `json:"FileCount"`
} `json:"data"`
}
type offlineResolveResp struct {
Data struct {
List []struct {
Result int `json:"result"`
ID int64 `json:"id"`
ErrCode int `json:"err_code"`
ErrMsg string `json:"err_msg"`
Files []struct {
ID int64 `json:"id"`
} `json:"files"`
} `json:"list"`
} `json:"data"`
}
type offlineSubmitResp struct {
Data struct {
TaskList []struct {
TaskID int64 `json:"task_id"`
Result int `json:"result"`
} `json:"task_list"`
} `json:"data"`
}
type offlineTaskListResp struct {
Data struct {
HasRun bool `json:"has_run"`
List []offlineTask `json:"list"`
Total int `json:"total"`
} `json:"data"`
}
type offlineTask struct {
TaskID int64 `json:"task_id"`
Name string `json:"name"`
Status int `json:"status"`
Size int64 `json:"size"`
ThirdTask string `json:"third_task_id"`
Downloaded int64 `json:"downloaded"`
Progress float64 `json:"progress"`
UploadIDR int64 `json:"upload_idr"`
UploadName string `json:"upload_name"`
Type string `json:"type"`
Speed int64 `json:"speed"`
}
+65 -90
View File
@@ -6,16 +6,11 @@ import (
"io"
"net/http"
"strconv"
"time"
"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/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
"github.com/OpenListTeam/OpenList/v4/pkg/singleflight"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/avast/retry-go"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2"
)
@@ -74,21 +69,18 @@ func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp, file model.F
}
func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, up driver.UpdateProgress) error {
// fetch s3 pre signed urls
size := file.GetSize()
chunkSize := int64(16 * utils.MB)
chunkCount := 1
if size > chunkSize {
chunkCount = int((size + chunkSize - 1) / chunkSize)
}
ss, err := stream.NewStreamSectionReader(file, int(chunkSize), &up)
tmpF, err := file.CacheFullInTempFile()
if err != nil {
return err
}
// fetch s3 pre signed urls
size := file.GetSize()
chunkSize := min(size, 16*utils.MB)
chunkCount := int(size / chunkSize)
lastChunkSize := size % chunkSize
if lastChunkSize == 0 {
if lastChunkSize > 0 {
chunkCount++
} else {
lastChunkSize = chunkSize
}
// only 1 batch is allowed
@@ -98,90 +90,73 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
batchSize = 10
getS3UploadUrl = d.getS3PreSignedUrls
}
thread := min(int(chunkCount), d.UploadThread)
threadG, uploadCtx := errgroup.NewOrderedGroupWithContext(ctx, thread,
retry.Attempts(3),
retry.Delay(time.Second),
retry.DelayType(retry.BackOffDelay))
for i := 1; i <= chunkCount; i += batchSize {
if utils.IsCanceled(uploadCtx) {
break
if utils.IsCanceled(ctx) {
return ctx.Err()
}
start := i
end := min(i+batchSize, chunkCount+1)
s3PreSignedUrls, err := getS3UploadUrl(uploadCtx, upReq, start, end)
s3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, start, end)
if err != nil {
return err
}
// upload each chunk
for cur := start; cur < end; cur++ {
if utils.IsCanceled(uploadCtx) {
break
for j := start; j < end; j++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
offset := int64(cur-1) * chunkSize
curSize := chunkSize
if cur == chunkCount {
if j == chunkCount {
curSize = lastChunkSize
}
var reader io.ReadSeeker
threadG.GoWithLifecycle(errgroup.Lifecycle{
Before: func(ctx context.Context) (err error) {
reader, err = ss.GetSectionReader(offset, curSize)
return
},
Do: func(ctx context.Context) (err error) {
reader.Seek(0, io.SeekStart)
uploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]
if uploadUrl == "" {
return fmt.Errorf("upload url is empty, s3PreSignedUrls: %+v", s3PreSignedUrls)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, reader))
if err != nil {
return err
}
req.ContentLength = curSize
//req.Header.Set("Content-Length", strconv.FormatInt(curSize, 10))
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode == http.StatusForbidden {
_, err, _ = singleflight.AnyGroup.Do(fmt.Sprintf("Pan123.newUpload_%p", threadG), func() (any, error) {
newS3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, cur, end)
if err != nil {
return nil, err
}
s3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls
return nil, nil
})
if err != nil {
return err
}
return fmt.Errorf("upload s3 chunk %d failed, status code: %d", cur, res.StatusCode)
}
if res.StatusCode != http.StatusOK {
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
return fmt.Errorf("upload s3 chunk %d failed, status code: %d, body: %s", cur, res.StatusCode, body)
}
progress := 100 * float64(threadG.Success()+1) / float64(chunkCount+1)
up(progress)
return nil
},
After: func(err error) {
ss.FreeSectionReader(reader)
},
})
err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.NewSectionReader(tmpF, chunkSize*int64(j-1), curSize), curSize, false, getS3UploadUrl)
if err != nil {
return err
}
up(float64(j) * 100 / float64(chunkCount))
}
}
if err := threadG.Wait(); err != nil {
return err
}
defer up(100)
// complete s3 upload
return d.completeS3(ctx, upReq, file, chunkCount > 1)
}
func (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSignedUrls *S3PreSignedURLs, cur, end int, reader *io.SectionReader, curSize int64, retry bool, getS3UploadUrl func(ctx context.Context, upReq *UploadResp, start int, end int) (*S3PreSignedURLs, error)) error {
uploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]
if uploadUrl == "" {
return fmt.Errorf("upload url is empty, s3PreSignedUrls: %+v", s3PreSignedUrls)
}
req, err := http.NewRequest("PUT", uploadUrl, driver.NewLimitedUploadStream(ctx, reader))
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = curSize
//req.Header.Set("Content-Length", strconv.FormatInt(curSize, 10))
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode == http.StatusForbidden {
if retry {
return fmt.Errorf("upload s3 chunk %d failed, status code: %d", cur, res.StatusCode)
}
// refresh s3 pre signed urls
newS3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, cur, end)
if err != nil {
return err
}
s3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls
// retry
reader.Seek(0, io.SeekStart)
return d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, cur, end, reader, curSize, true, getS3UploadUrl)
}
if res.StatusCode != http.StatusOK {
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
return fmt.Errorf("upload s3 chunk %d failed, status code: %d, body: %s", cur, res.StatusCode, body)
}
return nil
}
+11 -139
View File
@@ -13,9 +13,8 @@ import (
"strings"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
@@ -24,9 +23,9 @@ import (
// do others that not defined in Driver interface
const (
Api = "https://yun.123pan.com/api"
AApi = "https://yun.123pan.com/a/api"
BApi = "https://yun.123pan.com/b/api"
Api = "https://www.123pan.com/api"
AApi = "https://www.123pan.com/a/api"
BApi = "https://www.123pan.com/b/api"
LoginApi = "https://login.123pan.com/api"
MainApi = BApi
SignIn = LoginApi + "/user/sign_in"
@@ -44,16 +43,9 @@ const (
S3Auth = MainApi + "/file/s3_upload_object/auth"
UploadCompleteV2 = MainApi + "/file/upload_complete/v2"
S3Complete = MainApi + "/file/s3_complete_multipart_upload"
OfflineResolve = MainApi + "/v2/offline_download/task/resolve"
OfflineSubmit = MainApi + "/v2/offline_download/task/submit"
OfflineTaskList = MainApi + "/offline_download/task/list"
OfflineTaskDelete = MainApi + "/offline_download/task/delete"
// AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
)
var ErrOfflineTaskNotFound = errors.New("offline task not found")
func signPath(path string, os string, version string) (k string, v string) {
table := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}
random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
@@ -169,8 +161,8 @@ func (d *Pan123) login() error {
}
res, err := base.RestyClient.R().
SetHeaders(map[string]string{
"origin": "https://yun.123pan.com",
"referer": "https://yun.123pan.com/",
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"user-agent": "Dart/2.19(dart:io)-openlist",
"platform": "web",
"app-version": "3",
@@ -207,11 +199,11 @@ func (d *Pan123) Request(url string, method string, callback base.ReqCallback, r
do:
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"origin": "https://yun.123pan.com",
"referer": "https://yun.123pan.com/",
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken,
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",
"platform": d.Platform,
"platform": "web",
"app-version": "3",
//"user-agent": base.UserAgent,
})
@@ -246,115 +238,6 @@ do:
return body, nil
}
func (d *Pan123) OfflineDownload(ctx context.Context, uri string, dstDir model.Obj) (int64, error) {
var resolveResp offlineResolveResp
_, err := d.Request(OfflineResolve, http.MethodPost, func(req *resty.Request) {
req.SetContext(ctx).SetBody(base.Json{
"urls": uri,
})
}, &resolveResp)
if err != nil {
return 0, err
}
if len(resolveResp.Data.List) == 0 {
return 0, fmt.Errorf("offline resolve failed: empty response")
}
if resolveResp.Data.List[0].Result != 0 {
msg := resolveResp.Data.List[0].ErrMsg
if msg == "" {
msg = "offline resolve failed"
}
return 0, fmt.Errorf("%s", msg)
}
resourceID := resolveResp.Data.List[0].ID
if resourceID == 0 {
return 0, fmt.Errorf("offline resolve failed: empty resource id")
}
selectFileIDs := make([]int64, 0, len(resolveResp.Data.List[0].Files))
for _, f := range resolveResp.Data.List[0].Files {
if f.ID > 0 {
selectFileIDs = append(selectFileIDs, f.ID)
}
}
if len(selectFileIDs) == 0 {
return 0, fmt.Errorf("offline resolve failed: empty file list")
}
uploadDir, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid destination dir id: %s", dstDir.GetID())
}
var submitResp offlineSubmitResp
_, err = d.Request(OfflineSubmit, http.MethodPost, func(req *resty.Request) {
req.SetContext(ctx).SetBody(base.Json{
"resource_list": []base.Json{
{
"resource_id": resourceID,
"select_file_id": selectFileIDs,
},
},
"upload_dir": uploadDir,
})
}, &submitResp)
if err != nil {
return 0, err
}
if len(submitResp.Data.TaskList) == 0 {
return 0, fmt.Errorf("offline submit failed: empty task list")
}
if submitResp.Data.TaskList[0].Result != 0 {
return 0, fmt.Errorf("offline submit failed")
}
if submitResp.Data.TaskList[0].TaskID == 0 {
return 0, fmt.Errorf("offline submit failed: empty task id")
}
return submitResp.Data.TaskList[0].TaskID, nil
}
func (d *Pan123) GetOfflineTask(ctx context.Context, taskID int64) (*offlineTask, error) {
if taskID == 0 {
return nil, fmt.Errorf("invalid task id")
}
page := 1
pageSize := 100
statusArr := []int{0, 1, 2, 3}
for {
var listResp offlineTaskListResp
_, err := d.Request(OfflineTaskList, http.MethodPost, func(req *resty.Request) {
req.SetContext(ctx).SetBody(base.Json{
"current_page": page,
"page_size": pageSize,
"status_arr": statusArr,
})
}, &listResp)
if err != nil {
return nil, err
}
for i := range listResp.Data.List {
if listResp.Data.List[i].TaskID == taskID {
return &listResp.Data.List[i], nil
}
}
if len(listResp.Data.List) == 0 || page*pageSize >= listResp.Data.Total {
break
}
page++
}
return nil, ErrOfflineTaskNotFound
}
func (d *Pan123) DeleteOfflineTasks(ctx context.Context, taskIDs []int64) error {
if len(taskIDs) == 0 {
return nil
}
_, err := d.Request(OfflineTaskDelete, http.MethodPost, func(req *resty.Request) {
req.SetContext(ctx).SetBody(base.Json{
"task_ids": taskIDs,
})
}, nil)
return err
}
func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]File, error) {
page := 1
total := 0
@@ -399,14 +282,3 @@ func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]
}
return res, nil
}
func (d *Pan123) getUserInfo(ctx context.Context) (*UserInfoResp, error) {
var resp UserInfoResp
_, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
+4 -8
View File
@@ -5,10 +5,10 @@ import (
stdpath "path"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
)
type Pan123Link struct {
@@ -39,10 +39,6 @@ func (d *Pan123Link) Drop(ctx context.Context) error {
return nil
}
func (Addition) GetRootPath() string {
return "/"
}
func (d *Pan123Link) Get(ctx context.Context, path string) (model.Obj, error) {
node := GetNodeFromRootByPath(d.root, path)
return nodeToObj(node, path)
+2 -2
View File
@@ -1,8 +1,8 @@
package _123Link
import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/op"
)
type Addition struct {
+2 -2
View File
@@ -3,8 +3,8 @@ package _123Link
import (
"time"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
)
// Node is a node in the folder tree
+17 -164
View File
@@ -2,23 +2,19 @@ package _123_open
import (
"context"
"fmt"
"strconv"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/OpenListTeam/OpenList/internal/stream"
"github.com/OpenListTeam/OpenList/pkg/utils"
)
type Open123 struct {
model.Storage
Addition
UID uint64
tm *tokenManager
}
func (d *Open123) Config() driver.Config {
@@ -34,24 +30,6 @@ func (d *Open123) Init(ctx context.Context) error {
d.UploadThread = 3
}
if (d.UseOnlineAPI && d.RefreshToken != "" && len(d.APIAddress) > 0) || (d.ClientID != "" && d.ClientSecret != "") {
// proactive refresh by renewapi or client credentials
d.AccessToken = ""
d.tm = &tokenManager{}
} else {
// 避免个人 token 刷新产生的多个登录,被动刷新
// 默认过期时间90天,jwt exp 不可靠
d.tm = &tokenManager{
// accessToken: d.AccessToken,
expiredAt: time.Now().Add(90 * 24 * time.Hour),
}
}
_, err := d.getAccessToken(false)
if err != nil {
return fmt.Errorf("init get access token error: %w", err)
}
return nil
}
@@ -89,45 +67,13 @@ 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) {
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(ctx)
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)
if err != nil {
return nil, err
}
return &model.Link{URL: res.Data.DownloadUrl}, nil
link := model.Link{URL: res.Data.DownloadUrl}
return &link, nil
}
func (d *Open123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
@@ -149,22 +95,6 @@ func (d *Open123) Rename(ctx context.Context, srcObj model.Obj, newName string)
}
func (d *Open123) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
// 尝试使用上传+MD5秒传功能实现复制
// 1. 创建文件
// parentFileID 父目录id,上传到根目录时填写 0
parentFileId, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
if err != nil {
return fmt.Errorf("parse parentFileID error: %v", err)
}
etag := srcObj.(File).Etag
createResp, err := d.create(parentFileId, srcObj.GetName(), etag, srcObj.GetSize(), 2, false)
if err != nil {
return err
}
// 是否秒传
if createResp.Data.Reuse {
return nil
}
return errs.NotSupport
}
@@ -174,103 +104,26 @@ func (d *Open123) Remove(ctx context.Context, obj model.Obj) error {
return d.trash(fileId)
}
func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
// 1. 创建文件
// parentFileID 父目录id,上传到根目录时填写 0
func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
parentFileId, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
if err != nil {
return nil, fmt.Errorf("parse parentFileID error: %v", err)
}
// 尝试 SHA1 秒传
sha1Hash := file.GetHash().GetHash(utils.SHA1)
if len(sha1Hash) == utils.SHA1.Width {
resp, err := d.sha1Reuse(parentFileId, file.GetName(), sha1Hash, file.GetSize(), 2)
if err == nil && resp.Data.Reuse {
return File{
FileName: file.GetName(),
Size: file.GetSize(),
FileId: resp.Data.FileID,
Type: 2,
SHA1: sha1Hash,
}, nil
}
}
// etag 文件md5
etag := file.GetHash().GetHash(utils.MD5)
if len(etag) < utils.MD5.Width {
_, etag, err = stream.CacheFullAndHash(file, &up, utils.MD5)
_, etag, err = stream.CacheFullInTempFileAndHash(file, utils.MD5)
if err != nil {
return nil, err
return err
}
}
createResp, err := d.create(parentFileId, file.GetName(), etag, file.GetSize(), 2, false)
if err != nil {
return nil, err
return err
}
// 是否秒传
if createResp.Data.Reuse {
// 秒传成功才会返回正确的 FileID,否则为 0
if createResp.Data.FileID != 0 {
return File{
FileName: file.GetName(),
Size: file.GetSize(),
FileId: createResp.Data.FileID,
Type: 2,
Etag: etag,
}, nil
}
return nil
}
up(10)
// 2. 上传分片
err = d.Upload(ctx, file, createResp, up)
if err != nil {
return nil, err
}
// 3. 上传完毕
for range 60 {
uploadCompleteResp, err := d.complete(createResp.Data.PreuploadID)
// 返回错误代码未知,如:20103,文档也没有具体说
if err == nil && uploadCompleteResp.Data.Completed && uploadCompleteResp.Data.FileID != 0 {
up(100)
return File{
FileName: file.GetName(),
Size: file.GetSize(),
FileId: uploadCompleteResp.Data.FileID,
Type: 2,
Etag: etag,
}, nil
}
// 若接口返回的completed为 false 时,则需间隔1秒继续轮询此接口,获取上传最终结果。
time.Sleep(time.Second)
}
return nil, fmt.Errorf("upload complete timeout")
return d.Upload(ctx, file, createResp, up)
}
func (d *Open123) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
userInfo, err := d.getUserInfo(ctx)
if err != nil {
return nil, err
}
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: userInfo.Data.SpacePermanent + userInfo.Data.SpaceTemp,
UsedSpace: userInfo.Data.SpaceUsed,
},
}, nil
}
func (d *Open123) OfflineDownload(ctx context.Context, url string, dir model.Obj, callback string) (int, error) {
return d.createOfflineDownloadTask(ctx, url, dir.GetID(), callback)
}
func (d *Open123) OfflineDownloadProcess(ctx context.Context, taskID int) (float64, int, error) {
return d.queryOfflineDownloadStatus(ctx, taskID)
}
var (
_ driver.Driver = (*Open123)(nil)
_ driver.PutResult = (*Open123)(nil)
)
var _ driver.Driver = (*Open123)(nil)
+6 -16
View File
@@ -1,25 +1,21 @@
package _123_open
import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/op"
)
type Addition struct {
// refresh_token方式的AccessToken 【对个人开发者暂未开放】
RefreshToken string `json:"RefreshToken" required:"false"`
// 通过 https://www.123pan.com/developer 申请
ClientID string `json:"ClientID" required:"false"`
ClientSecret string `json:"ClientSecret" required:"false"`
// 直接写入AccessToken, AccessToken有过期时间,不建议直接填写
// 直接写入AccessToken
AccessToken string `json:"AccessToken" required:"false"`
// refresh_token方式的AccessToken 【对个人开发者暂未开放】
RefreshToken string `json:"RefreshToken" required:"false"`
// 使用在线API
UseOnlineAPI bool `json:"use_online_api" default:"true"`
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/123cloud/renewapi"`
// 用户名+密码方式登录的AccessToken可以兼容
//Username string `json:"username" required:"false"`
//Password string `json:"password" required:"false"`
@@ -27,11 +23,6 @@ type Addition struct {
// 上传线程数
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
}
@@ -39,7 +30,6 @@ var config = driver.Config{
Name: "123 Open",
DefaultRoot: "0",
LocalSort: true,
PreferProxy: true,
}
func init() {
-129
View File
@@ -1,129 +0,0 @@
package _123_open
import (
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/op"
)
var (
AccessToken = "https://open-api.123pan.com/api/v1/access_token"
)
func expiresInToExpiredAt(expiresIn int64) (time.Time, error) {
if expiresIn <= 0 {
return time.Time{}, errors.New("invalid expires_in from official API")
}
return time.Now().UTC().Add(time.Duration(expiresIn) * time.Second), nil
}
type tokenManager struct {
// accessToken string
expiredAt time.Time
mu sync.Mutex
blockRefresh bool
}
func (d *Open123) getAccessToken(forceRefresh bool) (string, error) {
tm := d.tm
tm.mu.Lock()
defer tm.mu.Unlock()
if tm.blockRefresh {
return "", errors.New("Authentication expired")
}
if !forceRefresh && d.AccessToken != "" && time.Now().Before(tm.expiredAt.Add(-5*time.Minute)) {
return d.AccessToken, nil
}
if err := d.flushAccessToken(); err != nil {
// token expired and failed to refresh, block further refresh attempts
tm.blockRefresh = true
return "", err
}
return d.AccessToken, nil
}
func (d *Open123) flushAccessToken() error {
// Official app renewapi response contains access_token, refresh_token and expires_in.
if d.UseOnlineAPI && d.RefreshToken != "" && len(d.APIAddress) > 0 {
var resp RefreshTokenResp
_, err := base.RestyClient.R().
SetResult(&resp).
SetQueryParams(map[string]string{
"refresh_ui": d.RefreshToken,
"server_use": "true",
"driver_txt": "123cloud_oa",
}).
Get(d.APIAddress)
if err != nil {
return err
}
if resp.AccessToken == "" || resp.RefreshToken == "" {
errMessage := resp.ErrorDescription
if errMessage == "" {
errMessage = resp.Text
}
if errMessage == "" {
errMessage = resp.Message
}
if errMessage == "" {
errMessage = resp.Error
}
if errMessage != "" {
return fmt.Errorf("failed to refresh token: %s", errMessage)
}
return fmt.Errorf("empty access_token or refresh_token returned from official API")
}
expiredAt, err := expiresInToExpiredAt(resp.ExpiresIn)
if err != nil {
return err
}
d.AccessToken = resp.AccessToken
d.RefreshToken = resp.RefreshToken
d.tm.expiredAt = expiredAt
op.MustSaveDriverStorage(d)
d.tm.blockRefresh = false
return nil
}
// Developer API response contains code/message/data(accessToken, expiredAt).
if d.ClientID != "" && d.ClientSecret != "" {
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"platform": "open_platform",
"Content-Type": "application/json",
})
var resp AccessTokenResp
req.SetBody(base.Json{
"clientID": d.ClientID,
"clientSecret": d.ClientSecret,
})
req.SetResult(&resp)
_, err := req.Execute(http.MethodPost, AccessToken)
if err != nil {
return err
}
if resp.Code != 0 {
return fmt.Errorf("get access token failed: %s", resp.Message)
}
if resp.Data.AccessToken == "" || resp.Data.ExpiredAt == "" {
return errors.New("invalid token payload from developer API")
}
expiredAt, err := time.Parse(time.RFC3339, resp.Data.ExpiredAt)
if err != nil {
return fmt.Errorf("parse expire time failed: %w", err)
}
d.AccessToken = resp.Data.AccessToken
d.tm.expiredAt = expiredAt.UTC()
op.MustSaveDriverStorage(d)
d.tm.blockRefresh = false
return nil
}
return errors.New("no valid authentication method available")
}
+53 -69
View File
@@ -4,8 +4,8 @@ import (
"strconv"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
)
type ApiInfo struct {
@@ -19,7 +19,6 @@ func (a *ApiInfo) Require() {
a.token <- struct{}{}
}
}
func (a *ApiInfo) Release() {
if a.qps > 0 {
time.AfterFunc(time.Second, func() {
@@ -27,16 +26,13 @@ func (a *ApiInfo) Release() {
})
}
}
func (a *ApiInfo) SetQPS(qps int) {
a.qps = qps
a.token = make(chan struct{}, qps)
}
func (a *ApiInfo) NowLen() int {
return len(a.token)
}
func InitApiInfo(url string, qps int) *ApiInfo {
return &ApiInfo{
url: url,
@@ -58,13 +54,9 @@ type File struct {
Category int `json:"category"`
Status int `json:"status"`
Trashed int `json:"trashed"`
SHA1 string
}
func (f File) GetHash() utils.HashInfo {
if len(f.SHA1) == utils.SHA1.Width && len(f.Etag) != utils.MD5.Width {
return utils.NewHashInfo(utils.SHA1, f.SHA1)
}
return utils.NewHashInfo(utils.MD5, f.Etag)
}
@@ -81,9 +73,7 @@ func (f File) GetName() string {
}
func (f File) CreateTime() time.Time {
// 返回的时间没有时区信息,默认 UTC+8
loc := time.FixedZone("UTC+8", 8*60*60)
parsedTime, err := time.ParseInLocation("2006-01-02 15:04:05", f.CreateAt, loc)
parsedTime, err := time.Parse("2006-01-02 15:04:05", f.CreateAt)
if err != nil {
return time.Now()
}
@@ -91,9 +81,7 @@ func (f File) CreateTime() time.Time {
}
func (f File) ModTime() time.Time {
// 返回的时间没有时区信息,默认 UTC+8
loc := time.FixedZone("UTC+8", 8*60*60)
parsedTime, err := time.ParseInLocation("2006-01-02 15:04:05", f.UpdateAt, loc)
parsedTime, err := time.Parse("2006-01-02 15:04:05", f.UpdateAt)
if err != nil {
return time.Now()
}
@@ -125,32 +113,29 @@ type AccessTokenResp struct {
}
type RefreshTokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
Code int `json:"code"`
Message string `json:"message"`
ErrorDescription string `json:"error_description"`
Error string `json:"error"`
Text string `json:"text"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}
type UserInfoResp struct {
BaseResp
Data struct {
UID uint64 `json:"uid"`
// Username string `json:"username"`
// DisplayName string `json:"displayName"`
// HeadImage string `json:"headImage"`
// Passport string `json:"passport"`
// Mail string `json:"mail"`
SpaceUsed int64 `json:"spaceUsed"`
SpacePermanent int64 `json:"spacePermanent"`
SpaceTemp int64 `json:"spaceTemp"`
// SpaceTempExpr int64 `json:"spaceTempExpr"`
// Vip bool `json:"vip"`
// DirectTraffic int64 `json:"directTraffic"`
// IsHideUID bool `json:"isHideUID"`
UID int64 `json:"uid"`
Username string `json:"username"`
DisplayName string `json:"displayName"`
HeadImage string `json:"headImage"`
Passport string `json:"passport"`
Mail string `json:"mail"`
SpaceUsed int64 `json:"spaceUsed"`
SpacePermanent int64 `json:"spacePermanent"`
SpaceTemp int64 `json:"spaceTemp"`
SpaceTempExpr string `json:"spaceTempExpr"`
Vip bool `json:"vip"`
DirectTraffic int64 `json:"directTraffic"`
IsHideUID bool `json:"isHideUID"`
} `json:"data"`
}
@@ -169,27 +154,33 @@ type DownloadInfoResp struct {
} `json:"data"`
}
type DirectLinkResp struct {
BaseResp
Data struct {
URL string `json:"url"`
} `json:"data"`
}
// 创建文件V2返回
type UploadCreateResp struct {
BaseResp
Data struct {
FileID int64 `json:"fileID"`
PreuploadID string `json:"preuploadID"`
Reuse bool `json:"reuse"`
SliceSize int64 `json:"sliceSize"`
Servers []string `json:"servers"`
FileID int64 `json:"fileID"`
PreuploadID string `json:"preuploadID"`
Reuse bool `json:"reuse"`
SliceSize int64 `json:"sliceSize"`
} `json:"data"`
}
// 上传完毕V2返回
type UploadUrlResp struct {
BaseResp
Data struct {
PresignedURL string `json:"presignedURL"`
}
}
type UploadCompleteResp struct {
BaseResp
Data struct {
Async bool `json:"async"`
Completed bool `json:"completed"`
FileID int64 `json:"fileID"`
} `json:"data"`
}
type UploadAsyncResp struct {
BaseResp
Data struct {
Completed bool `json:"completed"`
@@ -197,25 +188,18 @@ type UploadCompleteResp struct {
} `json:"data"`
}
type SHA1ReuseResp struct {
type UploadResp struct {
BaseResp
Data struct {
FileID int64 `json:"fileID"`
Reuse bool `json:"reuse"`
} `json:"data"`
}
type OfflineDownloadResp struct {
BaseResp
Data struct {
TaskID int `json:"taskID"`
} `json:"data"`
}
type OfflineDownloadProcessResp struct {
BaseResp
Data struct {
Process float64 `json:"process"`
Status int `json:"status"`
AccessKeyId string `json:"AccessKeyId"`
Bucket string `json:"Bucket"`
Key string `json:"Key"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken"`
FileId int64 `json:"FileId"`
Reuse bool `json:"Reuse"`
EndPoint string `json:"EndPoint"`
StorageNode string `json:"StorageNode"`
UploadId string `json:"UploadId"`
} `json:"data"`
}
+98 -150
View File
@@ -1,35 +1,27 @@
package _123_open
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"strconv"
"strings"
"time"
"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/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/errgroup"
"github.com/OpenListTeam/OpenList/pkg/http_range"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/avast/retry-go"
"github.com/go-resty/resty/v2"
)
// 创建文件 V2
func (d *Open123) create(parentFileID int64, filename string, etag string, size int64, duplicate int, containDir bool) (*UploadCreateResp, error) {
var resp UploadCreateResp
_, err := d.Request(UploadCreate, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"parentFileId": parentFileID,
"filename": filename,
"etag": strings.ToLower(etag),
"etag": etag,
"size": size,
"duplicate": duplicate,
"containDir": containDir,
@@ -41,136 +33,21 @@ func (d *Open123) create(parentFileID int64, filename string, etag string, size
return &resp, nil
}
// 上传分片 V2
func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createResp *UploadCreateResp, up driver.UpdateProgress) error {
uploadDomain := createResp.Data.Servers[0]
size := file.GetSize()
chunkSize := createResp.Data.SliceSize
ss, err := stream.NewStreamSectionReader(file, int(chunkSize), &up)
if err != nil {
return err
}
uploadNums := (size + chunkSize - 1) / chunkSize
thread := min(int(uploadNums), d.UploadThread)
threadG, uploadCtx := errgroup.NewOrderedGroupWithContext(ctx, thread,
retry.Attempts(3),
retry.Delay(time.Second),
retry.DelayType(retry.BackOffDelay))
for partIndex := range uploadNums {
if utils.IsCanceled(uploadCtx) {
break
}
partIndex := partIndex
partNumber := partIndex + 1 // 分片号从1开始
offset := partIndex * chunkSize
size := min(chunkSize, size-offset)
var reader io.ReadSeeker
var rateLimitedRd io.Reader
sliceMD5 := ""
// 表单
b := bytes.NewBuffer(make([]byte, 0, 2048))
threadG.GoWithLifecycle(errgroup.Lifecycle{
Before: func(ctx context.Context) (err error) {
reader, err = ss.GetSectionReader(offset, size)
return
},
Do: func(ctx context.Context) (err error) {
reader.Seek(0, io.SeekStart)
if sliceMD5 == "" {
// 把耗时的计算放在这里,避免阻塞其他协程
sliceMD5, err = utils.HashReader(utils.MD5, reader)
if err != nil {
return err
}
reader.Seek(0, io.SeekStart)
}
b.Reset()
w := multipart.NewWriter(b)
// 添加表单字段
err = w.WriteField("preuploadID", createResp.Data.PreuploadID)
if err != nil {
return err
}
err = w.WriteField("sliceNo", strconv.FormatInt(partNumber, 10))
if err != nil {
return err
}
err = w.WriteField("sliceMD5", sliceMD5)
if err != nil {
return err
}
// 写入文件内容
_, err = w.CreateFormFile("slice", fmt.Sprintf("%s.part%d", file.GetName(), partNumber))
if err != nil {
return err
}
headSize := b.Len()
err = w.Close()
if err != nil {
return err
}
head := bytes.NewReader(b.Bytes()[:headSize])
tail := bytes.NewReader(b.Bytes()[headSize:])
rateLimitedRd = driver.NewLimitedUploadStream(ctx, io.MultiReader(head, reader, tail))
token, err := d.getAccessToken(false)
if err != nil {
return err
}
// 创建请求并设置header
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadDomain+"/upload/v2/file/slice", rateLimitedRd)
if err != nil {
return err
}
// 设置请求头
req.Header.Add("Authorization", "Bearer "+token)
req.Header.Add("Content-Type", w.FormDataContentType())
req.Header.Add("Platform", "open_platform")
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("slice %d upload failed, status code: %d", partNumber, res.StatusCode)
}
b.Reset()
_, err = b.ReadFrom(res.Body)
if err != nil {
return err
}
var resp BaseResp
err = json.Unmarshal(b.Bytes(), &resp)
if err != nil {
return err
}
if resp.Code != 0 {
return fmt.Errorf("slice %d upload failed: %s", partNumber, resp.Message)
}
progress := 100 * float64(threadG.Success()+1) / float64(uploadNums+1)
up(progress)
return nil
},
After: func(err error) {
ss.FreeSectionReader(reader)
},
func (d *Open123) url(preuploadID string, sliceNo int64) (string, error) {
// get upload url
var resp UploadUrlResp
_, err := d.Request(UploadUrl, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"preuploadId": preuploadID,
"sliceNo": sliceNo,
})
}, &resp)
if err != nil {
return "", err
}
if err := threadG.Wait(); err != nil {
return err
}
return nil
return resp.Data.PresignedURL, nil
}
// 上传完毕
func (d *Open123) complete(preuploadID string) (*UploadCompleteResp, error) {
var resp UploadCompleteResp
_, err := d.Request(UploadComplete, http.MethodPost, func(req *resty.Request) {
@@ -184,16 +61,11 @@ func (d *Open123) complete(preuploadID string) (*UploadCompleteResp, error) {
return &resp, nil
}
// SHA1 秒传
func (d *Open123) sha1Reuse(parentFileID int64, filename string, sha1Hash string, size int64, duplicate int) (*SHA1ReuseResp, error) {
var resp SHA1ReuseResp
_, err := d.Request(UploadSHA1Reuse, http.MethodPost, func(req *resty.Request) {
func (d *Open123) async(preuploadID string) (*UploadAsyncResp, error) {
var resp UploadAsyncResp
_, err := d.Request(UploadAsync, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"parentFileID": parentFileID,
"filename": filename,
"sha1": strings.ToLower(sha1Hash),
"size": size,
"duplicate": duplicate,
"preuploadID": preuploadID,
})
}, &resp)
if err != nil {
@@ -201,3 +73,79 @@ func (d *Open123) sha1Reuse(parentFileID int64, filename string, sha1Hash string
}
return &resp, nil
}
func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createResp *UploadCreateResp, up driver.UpdateProgress) error {
size := file.GetSize()
chunkSize := createResp.Data.SliceSize
uploadNums := (size + chunkSize - 1) / chunkSize
threadG, uploadCtx := errgroup.NewGroupWithContext(ctx, d.UploadThread,
retry.Attempts(3),
retry.Delay(time.Second),
retry.DelayType(retry.BackOffDelay))
threadG.SetLimit(3)
for partIndex := int64(0); partIndex < uploadNums; partIndex++ {
if utils.IsCanceled(uploadCtx) {
return ctx.Err()
}
partIndex := partIndex
partNumber := partIndex + 1 // 分片号从1开始
offset := partIndex * chunkSize
size := min(chunkSize, size-offset)
limitedReader, err := file.RangeRead(http_range.Range{
Start: offset,
Length: size})
if err != nil {
return err
}
limitedReader = driver.NewLimitedUploadStream(ctx, limitedReader)
threadG.Go(func(ctx context.Context) error {
uploadPartUrl, err := d.url(createResp.Data.PreuploadID, partNumber)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, "PUT", uploadPartUrl, limitedReader)
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = size
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
_ = res.Body.Close()
progress := 10.0 + 85.0*float64(threadG.Success())/float64(uploadNums)
up(progress)
return nil
})
}
if err := threadG.Wait(); err != nil {
return err
}
uploadCompleteResp, err := d.complete(createResp.Data.PreuploadID)
if err != nil {
return err
}
if uploadCompleteResp.Data.Async == false || uploadCompleteResp.Data.Completed {
return nil
}
for {
uploadAsyncResp, err := d.async(createResp.Data.PreuploadID)
if err != nil {
return err
}
if uploadAsyncResp.Data.Completed {
break
}
}
up(100)
return nil
}
+55 -116
View File
@@ -1,51 +1,42 @@
package _123_open
import (
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
var ( // 不同情况下获取的AccessTokenQPS限制不同 如下模块化易于拓展
var ( //不同情况下获取的AccessTokenQPS限制不同 如下模块化易于拓展
Api = "https://open-api.123pan.com"
UserInfo = InitApiInfo(Api+"/api/v1/user/info", 1)
FileList = InitApiInfo(Api+"/api/v2/file/list", 3)
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)
Move = InitApiInfo(Api+"/api/v1/file/move", 1)
Rename = InitApiInfo(Api+"/api/v1/file/name", 1)
Trash = InitApiInfo(Api+"/api/v1/file/trash", 2)
UploadCreate = InitApiInfo(Api+"/upload/v2/file/create", 2)
UploadComplete = InitApiInfo(Api+"/upload/v2/file/upload_complete", 0)
UploadSHA1Reuse = InitApiInfo(Api+"/upload/v2/file/sha1_reuse", 2)
OfflineDownload = InitApiInfo(Api+"/api/v1/offline/download", 1)
OfflineDownloadProcess = InitApiInfo(Api+"/api/v1/offline/download/process", 5)
AccessToken = InitApiInfo(Api+"/api/v1/access_token", 1)
RefreshToken = InitApiInfo(Api+"/api/v1/oauth2/access_token", 1)
UserInfo = InitApiInfo(Api+"/api/v1/user/info", 1)
FileList = InitApiInfo(Api+"/api/v2/file/list", 4)
DownloadInfo = InitApiInfo(Api+"/api/v1/file/download_info", 0)
Mkdir = InitApiInfo(Api+"/upload/v1/file/mkdir", 2)
Move = InitApiInfo(Api+"/api/v1/file/move", 1)
Rename = InitApiInfo(Api+"/api/v1/file/name", 1)
Trash = InitApiInfo(Api+"/api/v1/file/trash", 2)
UploadCreate = InitApiInfo(Api+"/upload/v1/file/create", 2)
UploadUrl = InitApiInfo(Api+"/upload/v1/file/get_upload_url", 0)
UploadComplete = InitApiInfo(Api+"/upload/v1/file/upload_complete", 0)
UploadAsync = InitApiInfo(Api+"/upload/v1/file/upload_async_result", 1)
)
func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
retryToken := true
for {
token, err := d.getAccessToken(false)
if err != nil {
return nil, err
}
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"authorization": "Bearer " + token,
"authorization": "Bearer " + d.AccessToken,
"platform": "open_platform",
"Content-Type": "application/json",
})
@@ -75,9 +66,9 @@ func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCall
if baseResp.Code == 0 {
return body, nil
} else if baseResp.Code == 401 {
// 强制刷新Token, 有小概率会 race condition 导致多次刷新Token,但不影响正确运行
if _, err := d.getAccessToken(true); err != nil {
} else if baseResp.Code == 401 && retryToken {
retryToken = false
if err := d.flushAccessToken(); err != nil {
return nil, err
}
} else if baseResp.Code == 429 {
@@ -87,59 +78,52 @@ func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCall
return nil, errors.New(baseResp.Message)
}
}
}
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
func (d *Open123) flushAccessToken() error {
if d.Addition.ClientID != "" {
if d.Addition.ClientSecret != "" {
var resp AccessTokenResp
_, err := d.Request(AccessToken, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"clientID": d.ClientID,
"clientSecret": d.ClientSecret,
})
}, &resp)
if err != nil {
return err
}
d.AccessToken = resp.Data.AccessToken
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)
}
}
// 待签名字符串,格式: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
return nil
}
func (d *Open123) getUserInfo(ctx context.Context) (*UserInfoResp, error) {
func (d *Open123) getUserInfo() (*UserInfoResp, error) {
var resp UserInfoResp
if _, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
}, &resp); err != nil {
if _, err := d.Request(UserInfo, http.MethodGet, nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (d *Open123) getUID(ctx context.Context) (uint64, error) {
if d.UID != 0 {
return d.UID, nil
}
resp, err := d.getUserInfo(ctx)
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) {
var resp FileListResp
@@ -154,6 +138,7 @@ func (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*Fi
"searchData": "",
})
}, &resp)
if err != nil {
return nil, err
}
@@ -176,21 +161,6 @@ func (d *Open123) getDownloadInfo(fileId int64) (*DownloadInfoResp, error) {
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 {
_, err := d.Request(Mkdir, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
@@ -245,34 +215,3 @@ func (d *Open123) trash(fileId int64) error {
return nil
}
func (d *Open123) createOfflineDownloadTask(ctx context.Context, url string, dirID, callback string) (taskID int, err error) {
body := base.Json{
"url": url,
"dirID": dirID,
}
if len(callback) > 0 {
body["callBackUrl"] = callback
}
var resp OfflineDownloadResp
_, err = d.Request(OfflineDownload, http.MethodPost, func(req *resty.Request) {
req.SetBody(body)
}, &resp)
if err != nil {
return 0, err
}
return resp.Data.TaskID, nil
}
func (d *Open123) queryOfflineDownloadStatus(ctx context.Context, taskID int) (process float64, status int, err error) {
var resp OfflineDownloadProcessResp
_, err = d.Request(OfflineDownloadProcess, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"taskID": strconv.Itoa(taskID),
})
}, &resp)
if err != nil {
return .0, 0, err
}
return resp.Data.Process, resp.Data.Status, nil
}
+21 -15
View File
@@ -11,12 +11,12 @@ import (
"golang.org/x/time/rate"
_123 "github.com/OpenListTeam/OpenList/v4/drivers/123"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
_123 "github.com/OpenListTeam/OpenList/drivers/123"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
@@ -70,6 +70,14 @@ func (d *Pan123Share) List(ctx context.Context, dir model.Obj, args model.ListAr
func (d *Pan123Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
// TODO return link of file, required
if f, ok := file.(File); ok {
//var resp DownResp
var headers map[string]string
if !utils.IsLocalIPAddr(args.IP) {
headers = map[string]string{
//"X-Real-IP": "1.1.1.1",
"X-Forwarded-For": args.IP,
}
}
data := base.Json{
"shareKey": d.ShareKey,
"SharePwd": d.SharePwd,
@@ -79,29 +87,27 @@ func (d *Pan123Share) Link(ctx context.Context, file model.Obj, args model.LinkA
"size": f.Size,
}
resp, err := d.request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
req.SetBody(data).SetHeaders(headers)
}, nil)
if err != nil {
return nil, err
}
downloadUrl := utils.Json.Get(resp, "data", "DownloadURL").ToString()
ou, err := url.Parse(downloadUrl)
u, err := url.Parse(downloadUrl)
if err != nil {
return nil, err
}
u_ := ou.String()
nu := ou.Query().Get("params")
nu := u.Query().Get("params")
if nu != "" {
du, _ := base64.StdEncoding.DecodeString(nu)
u, err := url.Parse(string(du))
u, err = url.Parse(string(du))
if err != nil {
return nil, err
}
u_ = u.String()
}
u_ := u.String()
log.Debug("download url: ", u_)
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://yun.123pan.com/").Get(u_)
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
if err != nil {
return nil, err
}
@@ -116,7 +122,7 @@ func (d *Pan123Share) Link(ctx context.Context, file model.Obj, args model.LinkA
link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
}
link.Header = http.Header{
"Referer": []string{fmt.Sprintf("%s://%s/", ou.Scheme, ou.Host)},
"Referer": []string{"https://www.123pan.com/"},
}
return &link, nil
}
+13 -7
View File
@@ -1,8 +1,8 @@
package _123Share
import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/op"
)
type Addition struct {
@@ -15,11 +15,17 @@ type Addition struct {
}
var config = driver.Config{
Name: "123PanShare",
LocalSort: true,
NoUpload: true,
DefaultRoot: "0",
PreferProxy: true,
Name: "123PanShare",
LocalSort: true,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: true,
NeedMs: false,
DefaultRoot: "0",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
}
func init() {
+3 -3
View File
@@ -7,9 +7,9 @@ import (
"strings"
"time"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/internal/model"
)
type File struct {
@@ -24,7 +24,7 @@ type File struct {
}
func (f File) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.MD5, f.Etag)
return utils.HashInfo{}
}
func (f File) GetPath() string {
+8 -8
View File
@@ -13,16 +13,16 @@ import (
"strings"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
)
const (
Api = "https://yun.123pan.com/api"
AApi = "https://yun.123pan.com/a/api"
BApi = "https://yun.123pan.com/b/api"
Api = "https://www.123pan.com/api"
AApi = "https://www.123pan.com/a/api"
BApi = "https://www.123pan.com/b/api"
MainApi = BApi
FileList = MainApi + "/share/get"
DownloadInfo = MainApi + "/share/download/info"
@@ -58,10 +58,10 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
}
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"origin": "https://yun.123pan.com",
"referer": "https://yun.123pan.com/",
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken,
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) opnelist-client",
"platform": "web",
"app-version": "3",
//"user-agent": base.UserAgent,
+89 -179
View File
@@ -10,15 +10,14 @@ import (
"strconv"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
streamPkg "github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/cron"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/errs"
"github.com/OpenListTeam/OpenList/internal/model"
streamPkg "github.com/OpenListTeam/OpenList/internal/stream"
"github.com/OpenListTeam/OpenList/pkg/cron"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/pkg/utils/random"
log "github.com/sirupsen/logrus"
)
@@ -29,7 +28,6 @@ type Yun139 struct {
Account string
ref *Yun139
PersonalCloudHost string
RootPath string
}
func (d *Yun139) Config() driver.Config {
@@ -43,16 +41,7 @@ func (d *Yun139) GetAddition() driver.Additional {
func (d *Yun139) Init(ctx context.Context) error {
if d.ref == nil {
if len(d.Authorization) == 0 {
if d.Username != "" && d.Password != "" {
log.Infof("139yun: authorization is empty, trying to login with password.")
newAuth, err := d.loginWithPassword()
log.Debugf("newAuth: Ok: %s", newAuth)
if err != nil {
return fmt.Errorf("login with password failed: %w", err)
}
} else {
return fmt.Errorf("authorization is empty and username/password is not provided")
}
return fmt.Errorf("authorization is empty")
}
err := d.refreshToken()
if err != nil {
@@ -65,8 +54,7 @@ func (d *Yun139) Init(ctx context.Context) error {
"userInfo": base.Json{
"userType": 1,
"accountType": 1,
"accountName": d.Account,
},
"accountName": d.Account},
"modAddrType": 1,
}, &resp)
if err != nil {
@@ -103,22 +91,7 @@ func (d *Yun139) Init(ctx context.Context) error {
if len(d.Addition.RootFolderID) == 0 {
d.RootFolderID = d.CloudID
}
_, err := d.groupGetFiles(d.RootFolderID)
if err != nil {
return err
}
case MetaFamily:
if len(d.Addition.RootFolderID) == 0 {
// Attempt to obtain data.path as the root via a query and persist it.
if root, err := d.getFamilyRootPath(d.CloudID); err == nil && root != "" {
d.RootFolderID = root
op.MustSaveDriverStorage(d)
}
}
_, err := d.familyGetFiles(d.RootFolderID)
if err != nil {
return err
}
default:
return errs.NotImplement
}
@@ -305,42 +278,6 @@ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj,
return nil, err
}
return srcObj, nil
case MetaFamily:
pathname := "/isbo/openApi/createBatchOprTask"
var contentList []string
var catalogList []string
if srcObj.IsDir() {
catalogList = append(catalogList, path.Join(srcObj.GetPath(), srcObj.GetID()))
} else {
contentList = append(contentList, path.Join(srcObj.GetPath(), srcObj.GetID()))
}
body := base.Json{
"catalogList": catalogList,
"accountInfo": base.Json{
"accountName": d.getAccount(),
"accountType": "1",
},
"contentList": contentList,
"destCatalogID": dstDir.GetID(),
"destGroupID": d.CloudID,
"destPath": path.Join(dstDir.GetPath(), dstDir.GetID()),
"destType": 0,
"srcGroupID": d.CloudID,
"srcType": 0,
"taskType": 3,
}
var resp CreateBatchOprTaskResp
_, err := d.isboPost(pathname, body, &resp)
if err != nil {
return nil, err
}
log.Debugf("[139] Move MetaFamily CreateBatchOprTaskResp.Result.ResultCode: %s", resp.Result.ResultCode)
if resp.Result.ResultCode != "0" {
return nil, fmt.Errorf("failed to move in family cloud: %s", resp.Result.ResultDesc)
}
return srcObj, nil
default:
return nil, errs.NotImplement
}
@@ -415,27 +352,19 @@ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) e
var data base.Json
var pathname string
if srcObj.IsDir() {
pathname = "/modifyCloudDocV2"
data = base.Json{
"catalogType": 3,
"cloudID": d.CloudID,
"commonAccountInfo": base.Json{
"account": d.getAccount(),
"accountType": "1",
},
"docLibName": newName,
"docLibraryID": srcObj.GetID(),
"path": path.Join(srcObj.GetPath(), srcObj.GetID()),
}
var resp ModifyCloudDocV2Resp
_, err = d.andAlbumRequest(pathname, data, &resp)
if err != nil {
return err
}
if resp.Result.ResultCode != "0" {
return fmt.Errorf("failed to rename family folder: %s", resp.Result.ResultDesc)
}
return nil
// 网页接口不支持重命名家庭云文件夹
// data = base.Json{
// "catalogType": 3,
// "catalogID": srcObj.GetID(),
// "catalogName": newName,
// "commonAccountInfo": base.Json{
// "account": d.getAccount(),
// "accountType": 1,
// },
// "path": srcObj.GetPath(),
// }
// pathname = "/orchestration/familyCloud-rebuild/photoContent/v1.0/modifyCatalogInfo"
return errs.NotImplement
} else {
data = base.Json{
"contentID": srcObj.GetID(),
@@ -491,33 +420,6 @@ func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
}
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
_, err = d.post(pathname, data, nil)
case MetaGroup:
err = d.handleMetaGroupCopy(ctx, srcObj, dstDir)
case MetaFamily:
pathname := "/copyContentCatalog"
var sourceContentIDs []string
var sourceCatalogIDs []string
if srcObj.IsDir() {
sourceCatalogIDs = append(sourceCatalogIDs, srcObj.GetID())
} else {
sourceContentIDs = append(sourceContentIDs, srcObj.GetID())
}
body := base.Json{
"commonAccountInfo": base.Json{
"accountType": "1",
"accountUserId": d.ref.UserDomainID,
},
"destCatalogID": dstDir.GetID(),
"destCloudID": d.CloudID,
"sourceCatalogIDs": sourceCatalogIDs,
"sourceCloudID": d.CloudID,
"sourceContentIDs": sourceContentIDs,
}
var resp base.Json // Assuming a generic JSON response for success/failure
_, err = d.andAlbumRequest(pathname, body, &resp)
// For now, we assume no error means success.
default:
err = errs.NotImplement
}
@@ -620,27 +522,30 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
var err error
fullHash := stream.GetHash().GetHash(utils.SHA256)
if len(fullHash) != utils.SHA256.Width {
_, fullHash, err = streamPkg.CacheFullAndHash(stream, &up, utils.SHA256)
_, fullHash, err = streamPkg.CacheFullInTempFileAndHash(stream, utils.SHA256)
if err != nil {
return err
}
}
size := stream.GetSize()
partSize := d.getPartSize(size)
part := int64(1)
if size > partSize {
part = (size + partSize - 1) / partSize
var partSize = d.getPartSize(size)
part := size / partSize
if size%partSize > 0 {
part++
} else if part == 0 {
part = 1
}
// 生成所有 partInfos
partInfos := make([]PartInfo, 0, part)
for i := int64(0); i < part; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
start := i * partSize
byteSize := min(size-start, partSize)
byteSize := size - start
if byteSize > partSize {
byteSize = partSize
}
partNumber := i + 1
partInfo := PartInfo{
PartNumber: partNumber,
@@ -688,20 +593,17 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
// resp.Data.RapidUpload: true 支持快传,但此处直接检测是否返回分片的上传地址
// 快传的情况下同样需要手动处理冲突
if resp.Data.PartInfos != nil {
// Progress
p := driver.NewProgress(size, up)
rateLimited := driver.NewLimitedUploadStream(ctx, stream)
// 读取前100个分片的上传地址
uploadPartInfos := resp.Data.PartInfos
// 先上传前100个分片
err = d.uploadPersonalParts(ctx, partInfos, resp.Data.PartInfos, rateLimited, p)
if err != nil {
return err
}
// 如果还有剩余分片,分批获取上传地址并上传
for i := 100; i < len(partInfos); i += 100 {
end := min(i+100, len(partInfos))
// 获取后续分片的上传地址
for i := 101; i < len(partInfos); i += 100 {
end := i + 100
if end > len(partInfos) {
end = len(partInfos)
}
batchPartInfos := partInfos[i:end]
moredata := base.Json{
"fileId": resp.Data.FileId,
"uploadId": resp.Data.UploadId,
@@ -717,13 +619,45 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
if err != nil {
return err
}
err = d.uploadPersonalParts(ctx, partInfos, moreresp.Data.PartInfos, rateLimited, p)
uploadPartInfos = append(uploadPartInfos, moreresp.Data.PartInfos...)
}
// 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.NewRequest("PUT", uploadPartInfo.UploadUrl, r)
if err != nil {
return err
}
req = req.WithContext(ctx)
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{
"contentHash": fullHash,
"contentHashAlgorithm": "SHA256",
@@ -777,8 +711,6 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return nil
case MetaPersonal:
fallthrough
case MetaGroup:
fallthrough
case MetaFamily:
// 处理冲突
// 获取文件列表
@@ -826,18 +758,13 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
},
}
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
if d.isFamily() || d.Addition.Type == MetaGroup {
uploadPath := path.Join(dstDir.GetPath(), dstDir.GetID())
// if dstDir is root folder
if dstDir.GetID() == d.RootFolderID {
uploadPath = d.RootPath
}
if d.isFamily() {
data = d.newJson(base.Json{
"fileCount": 1,
"manualRename": 2,
"operation": 0,
"path": uploadPath,
"seqNo": random.String(32), // 序列号不能为空
"path": path.Join(dstDir.GetPath(), dstDir.GetID()),
"seqNo": random.String(32), //序列号不能为空
"totalSize": reportSize,
"uploadContentList": []base.Json{{
"contentName": stream.GetName(),
@@ -848,7 +775,6 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
pathname = "/orchestration/familyCloud-rebuild/content/v1.0/getFileUploadURL"
}
var resp UploadResp
log.Debugf("[139] upload request body: %+v", data)
_, err = d.post(pathname, data, &resp)
if err != nil {
return err
@@ -860,10 +786,12 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
size := stream.GetSize()
// Progress
p := driver.NewProgress(size, up)
partSize := d.getPartSize(size)
part := int64(1)
if size > partSize {
part = (size + partSize - 1) / partSize
var partSize = d.getPartSize(size)
part := size / partSize
if size%partSize > 0 {
part++
} else if part == 0 {
part = 1
}
rateLimited := driver.NewLimitedUploadStream(ctx, stream)
for i := int64(0); i < part; i++ {
@@ -877,10 +805,12 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
limitReader := io.LimitReader(rateLimited, byteSize)
// Update Progress
r := io.TeeReader(limitReader, p)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, resp.Data.UploadResult.RedirectionURL, r)
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r)
if err != nil {
return err
}
req = req.WithContext(ctx)
req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName()))
req.Header.Set("contentSize", strconv.FormatInt(size, 10))
req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1))
@@ -940,24 +870,4 @@ func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{},
}
}
func (d *Yun139) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
if d.UserDomainID == "" {
return nil, errs.NotImplement
}
detail, err := d.getDiskQuotaDetail(ctx)
if err != nil {
return nil, err
}
total := detail.Data.DiskSize * utils.MB
used := (detail.Data.DiskSize - detail.Data.FreeDiskSize) * utils.MB
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: total,
UsedSpace: used,
},
}, nil
}
var _ driver.Driver = (*Yun139)(nil)
+2 -6
View File
@@ -1,20 +1,16 @@
package _139
import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/op"
)
type Addition struct {
//Account string `json:"account" required:"true"`
Authorization string `json:"authorization" type:"text" required:"true"`
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true" secret:"true"`
MailCookies string `json:"mail_cookies" required:"true" type:"text" help:"Cookies from mail.139.com used for login authentication."`
driver.RootID
Type string `json:"type" type:"select" options:"personal_new,family,group,personal" default:"personal_new"`
CloudID string `json:"cloud_id"`
UserDomainID string `json:"user_domain_id" help:"ud_id in Cookie, fill in to show disk usage"`
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
ReportRealSize bool `json:"report_real_size" type:"bool" default:"true" help:"Enable to report the real file size during upload"`
UseLargeThumbnail bool `json:"use_large_thumbnail" type:"bool" default:"false" help:"Enable to use large thumbnail for images"`
-72
View File
@@ -312,75 +312,3 @@ type RefreshTokenResp struct {
AccessToken string `xml:"accessToken"`
Desc string `xml:"desc"`
}
type DiskQuotaDetail struct {
BaseResp
Data struct {
FreeDiskSize int64 `json:"freeDiskSize"`
DiskSize int64 `json:"diskSize"`
// QuotaList []struct {
// DriveType int `json:"driveType"`
// DriveName string `json:"driveName"`
// UsedSize int64 `json:"usedSize"`
// } `json:"quotaList"`
} `json:"data"`
}
type AndAlbumUploadResp struct {
Result struct {
ResultCode string `json:"resultCode"`
ResultDesc string `json:"resultDesc"`
} `json:"result"`
UploadResult struct {
UploadTaskID string `json:"uploadTaskID"`
RedirectionURL string `json:"redirectionUrl"`
NewContentIDList []struct {
ContentID string `json:"contentID"`
ContentName string `json:"contentName"`
} `json:"newContentIDList"`
} `json:"uploadResult"`
}
type ModifyCloudDocV2Req struct {
CatalogType int `json:"catalogType"`
CloudID string `json:"cloudID"`
CommonAccountInfo struct {
Account string `json:"account"`
AccountType string `json:"accountType"`
} `json:"commonAccountInfo"`
DocLibName string `json:"docLibName"`
DocLibraryID string `json:"docLibraryID"`
Path string `json:"path"`
}
type ModifyCloudDocV2Resp struct {
Result struct {
ResultCode string `json:"resultCode"`
ResultDesc string `json:"resultDesc"`
} `json:"result"`
}
type CreateBatchOprTaskReq struct {
CatalogList []string `json:"catalogList"`
CommonAccountInfo struct {
Account string `json:"account"`
AccountType string `json:"accountType"`
} `json:"commonAccountInfo"`
ContentList []string `json:"contentList"`
DestCatalogID string `json:"destCatalogID"`
DestGroupID string `json:"destGroupID"`
DestPath string `json:"destPath"`
DestType int `json:"destType"`
SourceCatalogType int `json:"sourceCatalogType"`
SourceCloudID string `json:"sourceCloudID"`
SourceType int `json:"sourceType"`
TaskType int `json:"taskType"`
}
type CreateBatchOprTaskResp struct {
Result struct {
ResultCode string `json:"resultCode"`
ResultDesc string `json:"resultDesc"`
} `json:"result"`
TaskID string `json:"taskID"`
}
+28 -780
View File
@@ -1,42 +1,27 @@
package _139
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
crypto_rand "crypto/rand"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"regexp"
"sort"
"strconv"
"strings"
"time"
"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/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/internal/op"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/OpenListTeam/OpenList/pkg/utils/random"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
)
const (
KEY_HEX_1 = "73634235495062495331515373756c734e7253306c673d3d" // 第一层 AES 解密密钥
KEY_HEX_2 = "7150714477323633586746674c337538" // 第二层 AES 解密密钥
)
// do others that not defined in Driver interface
func (d *Yun139) isFamily() bool {
return d.Type == "family"
@@ -108,22 +93,19 @@ func (d *Yun139) refreshToken() error {
SetBody(reqBody).
SetResult(&resp).
Post(url)
if err != nil || resp.Return != "0" {
log.Warnf("139yun: failed to refresh token with old token: %v, desc: %s. trying to login with password.", err, resp.Desc)
newAuth, loginErr := d.loginWithPassword()
log.Debugf("newAuth: Ok: %s", newAuth)
if loginErr != nil {
return fmt.Errorf("failed to login with password after refresh failed: %w", loginErr)
}
return nil
if err != nil {
return err
}
if resp.Return != "0" {
return fmt.Errorf("failed to refresh token: %s", resp.Desc)
}
d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token))
op.MustSaveDriverStorage(d)
return nil
}
func (d *Yun139) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
url := "https://yun.139.com" + pathname
req := base.RestyClient.R()
randStr := random.String(16)
ts := time.Now().Format("2006-01-02 15:04:05")
@@ -162,29 +144,10 @@ func (d *Yun139) request(url string, method string, callback base.ReqCallback, r
var e BaseResp
req.SetResult(&e)
log.Debugf("[139] request: %s %s, body: %s", method, url, string(body))
res, err := req.Execute(method, url)
if err != nil {
log.Debugf("[139] request error: %v", err)
return nil, err
}
log.Debugf("[139] response body: %s", res.String())
log.Debugln(res.String())
if !e.Success {
// Always try to unmarshal to the specific response type first if 'resp' is provided.
if resp != nil {
err = utils.Json.Unmarshal(res.Body(), resp)
if err != nil {
log.Debugf("[139] failed to unmarshal response to specific type: %v", err)
return nil, err // Return unmarshal error
}
if createBatchOprTaskResp, ok := resp.(*CreateBatchOprTaskResp); ok {
log.Debugf("[139] CreateBatchOprTaskResp.Result.ResultCode: %s", createBatchOprTaskResp.Result.ResultCode)
if createBatchOprTaskResp.Result.ResultCode == "0" {
goto SUCCESS_PROCESS
}
}
}
return nil, errors.New(e.Message) // Fallback to original error if not handled
return nil, errors.New(e.Message)
}
if resp != nil {
err = utils.Json.Unmarshal(res.Body(), resp)
@@ -192,7 +155,6 @@ func (d *Yun139) request(url string, method string, callback base.ReqCallback, r
return nil, err
}
}
SUCCESS_PROCESS:
return res.Body(), nil
}
@@ -204,7 +166,9 @@ func (d *Yun139) requestRoute(data interface{}, resp interface{}) ([]byte, error
callback := func(req *resty.Request) {
req.SetBody(data)
}
callback(req)
if callback != nil {
callback(req)
}
body, err := utils.Json.Marshal(req.Body)
if err != nil {
return nil, err
@@ -252,7 +216,7 @@ func (d *Yun139) requestRoute(data interface{}, resp interface{}) ([]byte, error
}
func (d *Yun139) post(pathname string, data interface{}, resp interface{}) ([]byte, error) {
return d.request("https://yun.139.com"+pathname, http.MethodPost, func(req *resty.Request) {
return d.request(pathname, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, resp)
}
@@ -301,7 +265,7 @@ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
HashInfo: utils.NewHashInfo(utils.MD5, content.Digest),
},
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
// Thumbnail: content.BigthumbnailURL,
//Thumbnail: content.BigthumbnailURL,
}
files = append(files, &f)
}
@@ -345,9 +309,6 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
return nil, err
}
path := resp.Data.Path
if catalogID == d.RootFolderID {
d.RootPath = path
}
for _, catalog := range resp.Data.CloudCatalogList {
f := model.Object{
ID: catalog.CatalogID,
@@ -371,7 +332,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
Path: path, // 文件所在目录的Path
},
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
// Thumbnail: content.BigthumbnailURL,
//Thumbnail: content.BigthumbnailURL,
}
files = append(files, &f)
}
@@ -403,9 +364,6 @@ func (d *Yun139) groupGetFiles(catalogID string) ([]model.Obj, error) {
return nil, err
}
path := resp.Data.GetGroupContentResult.ParentCatalogID
if catalogID == d.RootFolderID {
d.RootPath = path
}
for _, catalog := range resp.Data.GetGroupContentResult.CatalogList {
f := model.Object{
ID: catalog.CatalogID,
@@ -429,7 +387,7 @@ func (d *Yun139) groupGetFiles(catalogID string) ([]model.Obj, error) {
Path: path, // 文件所在目录的Path
},
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
// Thumbnail: content.BigthumbnailURL,
//Thumbnail: content.BigthumbnailURL,
}
files = append(files, &f)
}
@@ -457,7 +415,6 @@ func (d *Yun139) getLink(contentId string) (string, error) {
}
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
}
func (d *Yun139) familyGetLink(contentId string, path string) (string, error) {
data := d.newJson(base.Json{
"contentID": contentId,
@@ -534,13 +491,11 @@ func (d *Yun139) personalRequest(pathname string, method string, callback base.R
var e BaseResp
req.SetResult(&e)
log.Debugf("[139] personal request: %s %s, body: %s", method, url, string(body))
res, err := req.Execute(method, url)
if err != nil {
log.Debugf("[139] personal request error: %v", err)
return nil, err
}
log.Debugf("[139] personal response body: %s", res.String())
log.Debugln(res.String())
if !e.Success {
return nil, errors.New(e.Message)
}
@@ -552,20 +507,12 @@ func (d *Yun139) personalRequest(pathname string, method string, callback base.R
}
return res.Body(), nil
}
func (d *Yun139) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) {
return d.personalRequest(pathname, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, resp)
}
func (d *Yun139) isboPost(pathname string, data interface{}, resp interface{}) ([]byte, error) {
url := "https://group.yun.139.com/hcy/mutual/adapter" + pathname
return d.request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, resp)
}
func getPersonalTime(t string) time.Time {
stamp, err := time.ParseInLocation("2006-01-02T15:04:05.999-07:00", t, utils.CNLoc)
if err != nil {
@@ -595,7 +542,7 @@ func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
}
nextPageCursor = resp.Data.NextPageCursor
for _, item := range resp.Data.Items {
isFolder := (item.Type == "folder")
var isFolder = (item.Type == "folder")
var f model.Obj
if isFolder {
f = &model.Object{
@@ -607,7 +554,7 @@ func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
IsFolder: isFolder,
}
} else {
Thumbnails := item.Thumbnails
var Thumbnails = item.Thumbnails
var ThumbnailUrl string
if d.UseLargeThumbnail {
for _, thumb := range Thumbnails {
@@ -650,14 +597,12 @@ func (d *Yun139) personalGetLink(fileId string) (string, error) {
if err != nil {
return "", err
}
cdnUrl := jsoniter.Get(res, "data", "cdnUrl").ToString()
var cdnUrl = jsoniter.Get(res, "data", "cdnUrl").ToString()
if cdnUrl != "" {
cdnSwitch := jsoniter.Get(res, "data", "cdnSwitch").ToBool()
if cdnSwitch {
return cdnUrl, nil
}
return cdnUrl, nil
} else {
return jsoniter.Get(res, "data", "url").ToString(), nil
}
return jsoniter.Get(res, "data", "url").ToString(), nil
}
func (d *Yun139) getAuthorization() string {
@@ -666,712 +611,15 @@ func (d *Yun139) getAuthorization() string {
}
return d.Authorization
}
func (d *Yun139) getAccount() string {
if d.ref != nil {
return d.ref.getAccount()
}
return d.Account
}
func (d *Yun139) getPersonalCloudHost() string {
if d.ref != nil {
return d.ref.getPersonalCloudHost()
}
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
}
func (d *Yun139) getDiskQuotaDetail(ctx context.Context) (*DiskQuotaDetail, error) {
data := map[string]interface{}{
"userDomainId": d.UserDomainID,
}
var resp DiskQuotaDetail
_, err := d.request("https://user-njs.yun.139.com/user/disk/quota/detail", http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
req.SetContext(ctx)
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *Yun139) step1_password_login() (string, error) {
log.Debugf("--- 执行步骤 1: 登录 API ---")
loginURL := "https://mail.10086.cn/Login/Login.ashx"
// 密码 SHA1 哈希
hashedPassword := sha1Hash(fmt.Sprintf("fetion.com.cn:%s", d.Password))
log.Debugf("DEBUG: 原始密码: %s", d.Password)
log.Debugf("DEBUG: SHA1 输入: fetion.com.cn:%s", d.Password)
log.Debugf("DEBUG: 生成的 Password 哈希: %s", hashedPassword)
cguid := strconv.FormatInt(time.Now().UnixMilli(), 10) // 随机生成 cguid
loginHeaders := map[string]string{
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "zh-CN,zh;q=0.9,zh-TW;q=0.8,en-US;q=0.7,en;q=0.6,en-GB;q=0.5",
"cache-control": "max-age=0",
"content-type": "application/x-www-form-urlencoded",
"dnt": "1",
"origin": "https://mail.10086.cn",
"priority": "u=0, i",
"referer": fmt.Sprintf("https://mail.10086.cn/default.html?&s=1&v=0&u=%s&m=1&ec=S001&resource=indexLogin&clientid=1003&auto=on&cguid=%s&mtime=45", base64.StdEncoding.EncodeToString([]byte(d.Username)), cguid),
"sec-ch-ua": "\"Microsoft Edge\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "same-origin",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0",
"Cookie": d.MailCookies,
}
loginData := url.Values{}
loginData.Set("UserName", d.Username)
loginData.Set("passOld", "")
loginData.Set("auto", "on")
loginData.Set("Password", hashedPassword)
loginData.Set("webIndexPagePwdLogin", "1")
loginData.Set("pwdType", "1")
loginData.Set("clientId", "1003")
loginData.Set("authType", "2")
log.Debugf("DEBUG: 登录请求 URL: %s", loginURL)
log.Debugf("DEBUG: 登录请求 Headers: %+v", loginHeaders)
log.Debugf("DEBUG: 登录请求 Body: %s", loginData.Encode())
// 设置客户端不跟随重定向
client := base.RestyClient.SetRedirectPolicy(resty.NoRedirectPolicy())
res, err := client.R().
SetHeaders(loginHeaders).
SetFormDataFromValues(loginData).
Post(loginURL)
if err != nil {
// 如果是重定向错误,则不作为失败处理,因为我们禁止了自动重定向
if res != nil && res.StatusCode() >= 300 && res.StatusCode() < 400 {
log.Debugf("DEBUG: 登录响应 Status Code: %d (Redirect)", res.StatusCode())
} else {
return "", fmt.Errorf("step1 login request failed: %w", err)
}
} else {
log.Debugf("DEBUG: 登录响应 Status Code: %d", res.StatusCode())
}
// 恢复客户端的默认重定向策略,以免影响后续请求
base.RestyClient.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10))
log.Debugf("DEBUG: 登录响应 Headers: %+v", res.Header())
var sid, extractedCguid string
// 从 Location 头部提取 sid 和 cguid
locationHeader := res.Header().Get("Location")
if locationHeader != "" {
sidMatch := regexp.MustCompile(`sid=([^&]+)`).FindStringSubmatch(locationHeader)
cguidMatch := regexp.MustCompile(`cguid=([^&]+)`).FindStringSubmatch(locationHeader)
if len(sidMatch) > 1 {
sid = sidMatch[1]
log.Debugf("DEBUG: 从 Location 提取到 sid: %s", sid)
}
if len(cguidMatch) > 1 {
extractedCguid = cguidMatch[1]
log.Debugf("DEBUG: 从 Location 提取到 cguid: %s", extractedCguid)
}
}
// 如果 Location 中没有,尝试从 Set-Cookie 中提取
if sid == "" || extractedCguid == "" {
setCookieHeaders := res.Header().Values("Set-Cookie")
for _, cookieStr := range setCookieHeaders {
ssoSidMatch := regexp.MustCompile(`Os_SSo_Sid=([^;]+)`).FindStringSubmatch(cookieStr)
cookieCguidMatch := regexp.MustCompile(`cguid=([^;]+)`).FindStringSubmatch(cookieStr)
if len(ssoSidMatch) > 1 && sid == "" {
sid = ssoSidMatch[1]
log.Debugf("DEBUG: 从 Set-Cookie 提取到 sid: %s", sid)
}
if len(cookieCguidMatch) > 1 && extractedCguid == "" {
extractedCguid = cookieCguidMatch[1]
log.Debugf("DEBUG: 从 Set-Cookie 提取到 cguid: %s", extractedCguid)
}
}
}
if sid == "" || extractedCguid == "" {
return "", errors.New("failed to extract sid or cguid from login response")
}
// 提取并记录 cookies
loginUrlObj, _ := url.Parse(loginURL)
cookies := base.RestyClient.GetClient().Jar.Cookies(loginUrlObj)
var cookieStrings []string
for _, cookie := range cookies {
cookieStrings = append(cookieStrings, cookie.Name+"="+cookie.Value)
}
cookieStr := strings.Join(cookieStrings, "; ")
log.Debugf("DEBUG: 提取到的 Cookies: %s", cookieStr)
d.MailCookies = cookieStr
return sid, nil
}
func (d *Yun139) step2_get_single_token(sid string) (string, error) {
log.Debugf("\n--- 执行步骤 2: 换artifact API ---")
cguid := strconv.FormatInt(time.Now().UnixMilli(), 10)
exchangeArtifactURL := fmt.Sprintf("https://smsrebuild1.mail.10086.cn/setting/s?func=%s&sid=%s&cguid=%s", url.QueryEscape("umc:getArtifact"), sid, cguid)
// 从 MailCookies 中提取 RMKEY
var rmkey string
cookies := strings.Split(d.MailCookies, ";")
for _, cookie := range cookies {
cookie = strings.TrimSpace(cookie)
if strings.HasPrefix(cookie, "RMKEY=") {
rmkey = cookie
break
}
}
if rmkey == "" {
return "", errors.New("RMKEY not found in MailCookies")
}
exchangePassidHeaders := map[string]string{
"Host": "smsrebuild1.mail.10086.cn",
"Cookie": rmkey,
"Content-Type": "text/xml; charset=utf-8",
"Accept-Encoding": "gzip",
"User-Agent": "okhttp/4.12.0",
}
log.Debugf("DEBUG: 换passid 请求 URL: %s", exchangeArtifactURL)
log.Debugf("DEBUG: 换passid 请求 Headers: %+v", exchangePassidHeaders)
res, err := base.RestyClient.R().
SetHeaders(exchangePassidHeaders).
Post(exchangeArtifactURL)
if err != nil {
return "", fmt.Errorf("step2 exchange artifact request failed: %w", err)
}
log.Debugf("DEBUG: 换passid 响应 Status Code: %d", res.StatusCode())
log.Debugf("DEBUG: 换passid 响应 Headers: %+v", res.Header())
log.Debugf("DEBUG: 换passid 响应 Body: %s...", res.String()[:min(len(res.String()), 500)])
dycpwd := jsoniter.Get(res.Body(), "var", "artifact").ToString()
if dycpwd == "" {
return "", errors.New("failed to extract dycpwd from artifact exchange response")
}
log.Debugf("DEBUG: 提取到 dycpwd: %s", dycpwd)
return dycpwd, nil
}
// --- 辅助函数:加密/解密 ---
// sha1Hash 计算 SHA1 哈希值,返回十六进制字符串。
func sha1Hash(data string) string {
h := sha1.New()
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
// pkcs7_pad PKCS7 填充
func pkcs7_pad(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, padtext...)
}
// pkcs7_unpad PKCS7 去填充
func pkcs7_unpad(data []byte) ([]byte, error) {
length := len(data)
if length == 0 {
return nil, errors.New("pkcs7: data is empty")
}
unpadding := int(data[length-1])
if unpadding > length {
return nil, errors.New("pkcs7: invalid padding")
}
return data[:(length - unpadding)], nil
}
// aes_ecb_decrypt AES/ECB/Pkcs7 解密,输入为十六进制字符串。
func aes_ecb_decrypt(ciphertext []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext)%block.BlockSize() != 0 {
return nil, errors.New("AES ECB decrypt: ciphertext is not a multiple of the block size")
}
decrypted := make([]byte, len(ciphertext))
blockSize := block.BlockSize()
for bs, be := 0, blockSize; bs < len(ciphertext); bs, be = bs+blockSize, be+blockSize {
block.Decrypt(decrypted[bs:be], ciphertext[bs:be])
}
return pkcs7_unpad(decrypted)
}
// 以下提供 camelCase 的 AES CBC 加解密,供文件中其它位置调用(并支持传入 IV)。
func aesCbcEncrypt(plaintext []byte, key []byte, iv []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(iv) != block.BlockSize() {
return nil, fmt.Errorf("aesCbcEncrypt: iv length %d does not match block size %d", len(iv), block.BlockSize())
}
padded := pkcs7_pad(plaintext, block.BlockSize())
ciphertext := make([]byte, len(padded))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext, padded)
return ciphertext, nil
}
func aesCbcDecrypt(ciphertext []byte, key []byte, iv []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(iv) != block.BlockSize() {
return nil, fmt.Errorf("aesCbcDecrypt: iv length %d does not match block size %d", len(iv), block.BlockSize())
}
if len(ciphertext)%block.BlockSize() != 0 {
return nil, errors.New("aesCbcDecrypt: ciphertext is not a multiple of the block size")
}
decrypted := make([]byte, len(ciphertext))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(decrypted, ciphertext)
return pkcs7_unpad(decrypted)
}
// sortedJsonStringify 对 JSON 对象进行排序并字符串化。
func sortedJsonStringify(obj interface{}) (string, error) {
if obj == nil {
return "null", nil
}
switch v := obj.(type) {
case string:
// 尝试解析为 JSON,如果成功则递归处理
var parsed interface{}
if err := jsoniter.Unmarshal([]byte(v), &parsed); err == nil {
return sortedJsonStringify(parsed)
}
// 如果不是 JSON 字符串,则直接返回 JSON 字符串化的结果
return jsoniter.MarshalToString(v)
case int, float64, bool:
return fmt.Sprintf("%v", v), nil
case []interface{}:
var items []string
for _, item := range v {
s, err := sortedJsonStringify(item)
if err != nil {
return "", err
}
items = append(items, s)
}
return fmt.Sprintf("[%s]", strings.Join(items, ",")), nil
case map[string]interface{}:
sortedKeys := make([]string, 0, len(v))
for key := range v {
sortedKeys = append(sortedKeys, key)
}
sort.Strings(sortedKeys)
var pairs []string
for _, key := range sortedKeys {
value := v[key]
s, err := sortedJsonStringify(value)
if err != nil {
return "", err
}
// Use jsoniter.MarshalToString for the key to ensure it's quoted correctly
keyStr, err := jsoniter.MarshalToString(key)
if err != nil {
return "", err
}
pairs = append(pairs, fmt.Sprintf("%s:%s", keyStr, s))
}
return fmt.Sprintf("{%s}", strings.Join(pairs, ",")), nil
default:
// Fallback for other types, e.g., numbers, booleans, or unhandled complex types
// Use jsoniter's default marshalling for these
return jsoniter.MarshalToString(v)
}
}
// yun139EncryptedRequest handles the common encrypted request/response flow.
func (d *Yun139) yun139EncryptedRequest(url string, body interface{}, headers map[string]string, aesKeyHex string, resp interface{}) ([]byte, error) {
// 1. Decode AES key
aesKey, err := hex.DecodeString(aesKeyHex)
if err != nil {
return nil, fmt.Errorf("yun139EncryptedRequest: failed to decode AES key: %w", err)
}
// 2. Marshal and sort the request body
sortedJson, err := sortedJsonStringify(body)
if err != nil {
return nil, fmt.Errorf("yun139EncryptedRequest: failed to marshal and sort body: %w", err)
}
log.Debugf("yun139EncryptedRequest: Request Body (plaintext): %s", sortedJson)
// 3. Encrypt the body using AES/CBC
iv := make([]byte, 16) // 16 bytes for AES-128
if _, err := crypto_rand.Read(iv); err != nil {
return nil, fmt.Errorf("yun139EncryptedRequest: failed to generate IV: %w", err)
}
encryptedBody, err := aesCbcEncrypt([]byte(sortedJson), aesKey, iv)
if err != nil {
return nil, fmt.Errorf("yun139EncryptedRequest: failed to encrypt body: %w", err)
}
payload := base64.StdEncoding.EncodeToString(append(iv, encryptedBody...))
// 4. Make the request
res, err := base.RestyClient.R().
SetHeaders(headers).
SetBody(payload).
Post(url)
if err != nil {
return nil, fmt.Errorf("yun139EncryptedRequest: http request failed: %w", err)
}
if res.StatusCode() != 200 {
return nil, fmt.Errorf("yun139EncryptedRequest: unexpected status code %d: %s", res.StatusCode(), res.String())
}
// 5. Decrypt the response
respBody := res.Body()
var decryptedBytes []byte
if len(respBody) > 0 && respBody[0] == '{' {
log.Warnf("yun139EncryptedRequest: received a plain JSON response, not an encrypted string. Body: %s", string(respBody))
decryptedBytes = respBody
} else {
decodedResp, err := base64.StdEncoding.DecodeString(string(respBody))
if err != nil {
return nil, fmt.Errorf("yun139EncryptedRequest: response base64 decode failed: %w. Body: '%s'", err, string(respBody))
}
if len(decodedResp) < 16 {
return nil, fmt.Errorf("yun139EncryptedRequest: decoded response is too short to be encrypted. Length: %d", len(decodedResp))
}
respIv := decodedResp[:16]
respCiphertext := decodedResp[16:]
decryptedBytes, err = aesCbcDecrypt(respCiphertext, aesKey, respIv)
if err != nil {
return nil, fmt.Errorf("yun139EncryptedRequest: response aes decrypt failed: %w", err)
}
}
log.Debugf("yun139EncryptedRequest: Response Body (decrypted): %s", string(decryptedBytes))
// 6. Unmarshal to the final response struct
if resp != nil {
err = utils.Json.Unmarshal(decryptedBytes, resp)
if err != nil {
return nil, fmt.Errorf("yun139EncryptedRequest: failed to unmarshal decrypted response: %w", err)
}
}
return decryptedBytes, nil
}
func (d *Yun139) step3_third_party_login(dycpwd string) (string, error) {
log.Debugf("\n--- 执行步骤 3: 单点登录 API ---")
ssoLoginURL := "https://user-njs.yun.139.com/user/thirdlogin"
// 构建原始请求体
ssoRequestBodyRaw := base.Json{
"clientkey_decrypt": "l3TryM&Q+X7@dzwk)qP",
"clienttype": "886",
"cpid": "507",
"dycpwd": dycpwd,
"extInfo": base.Json{"ifOpenAccount": "0"},
"loginMode": "0",
"msisdn": d.Username,
"pintype": "13",
"secinfo": strings.ToUpper(sha1Hash(fmt.Sprintf("fetion.com.cn:%s", dycpwd))),
"version": "20250901",
}
ssoLoginHeaders := map[string]string{
"hcy-cool-flag": "1",
"x-huawei-channelSrc": "10246600",
"x-sdk-channelSrc": "",
"x-MM-Source": "0",
"x-UserAgent": "android|23116PN5BC|android15|1.2.6|||1440x3200|10246600",
"x-DeviceInfo": "4|127.0.0.1|5|1.2.6|Xiaomi|23116PN5BC||02-00-00-00-00-00|android 15|1440x3200|android|||",
"Content-Type": "text/plain;charset=UTF-8",
"Host": "user-njs.yun.139.com",
"Accept-Encoding": "gzip",
"User-Agent": "okhttp/3.12.2",
}
// 使用通用加密请求函数
decryptedLayer1StrBytes, err := d.yun139EncryptedRequest(ssoLoginURL, ssoRequestBodyRaw, ssoLoginHeaders, KEY_HEX_1, nil)
if err != nil {
return "", fmt.Errorf("step3 encrypted request failed: %w", err)
}
hexInner := jsoniter.Get(decryptedLayer1StrBytes, "data").ToString()
if hexInner == "" {
return "", errors.New("missing data field in first layer decryption result")
}
log.Debugf("DEBUG: 第一层解密提取到 hex_inner: %s...", hexInner[:min(len(hexInner), 50)])
// 第二层解密
key2, err := hex.DecodeString(KEY_HEX_2)
if err != nil {
return "", fmt.Errorf("failed to decode KEY_HEX_2: %w", err)
}
hexInnerBytes, err := hex.DecodeString(hexInner)
if err != nil {
return "", fmt.Errorf("failed to decode hex_inner: %w", err)
}
finalJsonStrBytes, err := aes_ecb_decrypt(hexInnerBytes, key2)
if err != nil {
return "", fmt.Errorf("step3 response layer2 aes ecb decrypt failed: %w", err)
}
log.Debugf("DEBUG: 最终解密结果: %s", string(finalJsonStrBytes))
// 提取 authToken
authToken := jsoniter.Get(finalJsonStrBytes, "authToken").ToString()
if authToken == "" {
return "", errors.New("failed to extract authToken from final decryption result")
}
log.Debugf("DEBUG: 提取到 authToken: %s", authToken)
// 提取 account 和 userDomainId
account := jsoniter.Get(finalJsonStrBytes, "account").ToString()
userDomainId := jsoniter.Get(finalJsonStrBytes, "userDomainId").ToString()
if account == "" || userDomainId == "" {
return "", errors.New("failed to extract account or userDomainId from final decryption result")
}
d.UserDomainID = userDomainId
newAuthorization := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("pc:%s:%s", account, authToken)))
return newAuthorization, nil
}
func (d *Yun139) loginWithPassword() (string, error) {
if d.Username == "" || d.Password == "" || d.MailCookies == "" {
return "", errors.New("username, password or mail_cookies is empty")
}
passId, err := d.step1_password_login()
if err != nil {
return "", err
}
log.Infof("Step 1 success, passId: %s", passId)
token, err := d.step2_get_single_token(passId)
if err != nil {
return "", err
}
log.Infof("Step 2 success, token: %s", token)
newAuth, err := d.step3_third_party_login(token)
if err != nil {
return "", err
}
log.Infof("Step 3 success, new authorization generated.")
d.Authorization = newAuth // Ensure Authorization is also updated before saving
op.MustSaveDriverStorage(d)
return newAuth, nil
}
func (d *Yun139) andAlbumRequest(pathname string, body interface{}, resp interface{}) ([]byte, error) {
url := "https://group.yun.139.com/hcy/family/adapter/andAlbum/openApi" + pathname
headers := map[string]string{
"Host": "group.yun.139.com",
"authorization": "Basic " + d.getAuthorization(),
"x-svctype": "2",
"hcy-cool-flag": "1",
"api-version": "v2",
"x-huawei-channelsrc": "10246600",
"x-sdk-channelsrc": "",
"x-mm-source": "0",
"x-deviceinfo": "1|127.0.0.1|1|12.3.2|Xiaomi|23116PN5BC||02-00-00-00-00-00|android 15|1440x3200|android|zh||||032|0|", //重要参数
"content-type": "application/json; charset=utf-8",
"user-agent": "okhttp/4.11.0",
"accept-encoding": "gzip",
}
return d.yun139EncryptedRequest(url, body, headers, KEY_HEX_1, resp)
}
func (d *Yun139) handleMetaGroupCopy(ctx context.Context, srcObj, dstDir model.Obj) error {
pathname := "/copyContentCatalog"
var sourceContentIDs []string
var sourceCatalogIDs []string
if srcObj.IsDir() {
sourceCatalogIDs = append(sourceCatalogIDs, path.Join("root:/", srcObj.GetPath(), srcObj.GetID()))
} else {
sourceContentIDs = append(sourceContentIDs, path.Join("root:/", srcObj.GetPath(), srcObj.GetID()))
}
destCatalogID := path.Join("root:/", dstDir.GetPath(), dstDir.GetID())
log.Debugf("[139Yun Group Copy] srcObj ID: %s, srcObj Path: %s, dstDir ID: %s, dstDir Path: %s, destCatalogID: %s", srcObj.GetID(), srcObj.GetPath(), dstDir.GetID(), dstDir.GetPath(), destCatalogID)
body := base.Json{
"commonAccountInfo": base.Json{
"accountType": "1",
"accountUserId": d.UserDomainID,
},
"destCatalogID": destCatalogID,
"destCloudID": d.CloudID,
"sourceCatalogIDs": sourceCatalogIDs,
"sourceCloudID": d.CloudID,
"sourceContentIDs": sourceContentIDs,
}
var resp base.Json
_, err := d.andAlbumRequest(pathname, body, &resp)
return err
}
// getGroupRootByCloudID 查询 group 上层信息,优先返回 parentCatalogID,回退到 catalogList[0].path
func (d *Yun139) getGroupRootByCloudID(cloudID string) (string, error) {
pathname := "/orchestration/group-rebuild/catalog/v1.0/queryGroupContentList"
body := base.Json{
"groupID": cloudID,
"commonAccountInfo": base.Json{
"account": d.getAccount(),
"accountType": 1,
},
"pageInfo": base.Json{
"pageNum": 1,
"pageSize": 1,
},
}
var resp base.Json
_, err := d.post(pathname, body, &resp)
if err != nil {
return "", err
}
dataObj, _ := resp["data"].(map[string]interface{})
if dataObj == nil {
return "", fmt.Errorf("invalid group response data")
}
if gcr, ok := dataObj["getGroupContentResult"].(map[string]interface{}); ok {
if pid, ok := gcr["parentCatalogID"].(string); ok && pid != "" {
return pid, nil
}
if cl, ok := gcr["catalogList"].([]interface{}); ok && len(cl) > 0 {
if first, ok := cl[0].(map[string]interface{}); ok {
if p, ok := first["path"].(string); ok && p != "" {
return p, nil
}
}
}
}
return "", fmt.Errorf("no root found in group response")
}
// getFamilyRootPath 查询 family 的上层 path(data.path)
// 返回值已去除前缀 "root:/"(或 "root:"),直接返回纯 ID 或 path 部分,便于持久化为 RootFolderID。
func (d *Yun139) getFamilyRootPath(cloudID string) (string, error) {
// 使用 v1.2 接口(代码日志中已有该请求),pageSize 取 1 足够获取 path 字段
pathname := "/orchestration/familyCloud-rebuild/content/v1.2/queryContentList"
body := base.Json{
"catalogID": "",
"catalogType": 3,
"cloudID": cloudID,
"cloudType": 1,
"commonAccountInfo": base.Json{
"account": d.getAccount(),
"accountType": 1,
},
"contentSortType": 0,
"pageInfo": base.Json{
"pageNum": 1,
"pageSize": 1,
},
"sortDirection": 1,
}
var resp base.Json
_, err := d.post(pathname, body, &resp)
if err != nil {
return "", err
}
dataObj, _ := resp["data"].(map[string]interface{})
if dataObj == nil {
return "", fmt.Errorf("invalid family response data")
}
// helper to strip "root:/" or "root:" prefix
stripRoot := func(s string) string {
s = strings.TrimSpace(s)
s = strings.TrimPrefix(s, "root:/")
s = strings.TrimPrefix(s, "root:")
return s
}
if p, ok := dataObj["path"].(string); ok && p != "" {
return stripRoot(p), nil
}
// 回退:有时 path 在 cloudCatalogList.catalogList 中
if cl, ok := dataObj["cloudCatalogList"].([]interface{}); ok && len(cl) > 0 {
if first, ok := cl[0].(map[string]interface{}); ok {
if p, ok := first["path"].(string); ok && p != "" {
return stripRoot(p), nil
}
}
}
return "", fmt.Errorf("no path found in family response")
}
+4 -17
View File
@@ -5,10 +5,10 @@ import (
"net/http"
"strings"
"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/pkg/utils"
"github.com/OpenListTeam/OpenList/drivers/base"
"github.com/OpenListTeam/OpenList/internal/driver"
"github.com/OpenListTeam/OpenList/internal/model"
"github.com/OpenListTeam/OpenList/pkg/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
@@ -194,17 +194,4 @@ func (d *Cloud189) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return d.newUpload(ctx, dstDir, stream, up)
}
func (d *Cloud189) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
capacityInfo, err := d.getCapacityInfo(ctx)
if err != nil {
return nil, err
}
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: capacityInfo.CloudCapacityInfo.TotalSize,
UsedSpace: capacityInfo.CloudCapacityInfo.UsedSize,
},
}, nil
}
var _ driver.Driver = (*Cloud189)(nil)

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