mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 13:03:09 +08:00
Compare commits
306 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f977f32dba | |||
| c8306256ff | |||
| 5273a77854 | |||
| cc3d8a06a7 | |||
| 925a2422d6 | |||
| 33015eee9f | |||
| 1be74776f6 | |||
| 31810f556c | |||
| 30922b6d52 | |||
| 759adfca21 | |||
| bfbc441fa6 | |||
| 866793f485 | |||
| 40b39000ab | |||
| 6eedff851c | |||
| aea77c5967 | |||
| 009d834e71 | |||
| e9ea991584 | |||
| d4aa8b6a15 | |||
| 9e55a0111b | |||
| f869afc71f | |||
| e960035b65 | |||
| a0f001c168 | |||
| c8e7f35761 | |||
| 6ed7db1ee7 | |||
| 5a12c8d2ba | |||
| b86d10fa2f | |||
| 004a15bc5e | |||
| aa534a955b | |||
| d403f75da2 | |||
| 9ac2f3b7f0 | |||
| 756862d1e4 | |||
| 3abdf7990b | |||
| 3b017c2b5c | |||
| a42e367660 | |||
| f45473357e | |||
| dcfefce85d | |||
| d349765617 | |||
| ace7482bc2 | |||
| eadf03a4f8 | |||
| da84a800ab | |||
| b05a51a062 | |||
| ffe3f3cfa3 | |||
| 7e55f11e4a | |||
| e7bccc636a | |||
| b7f9e8ef0a | |||
| d7c332ca13 | |||
| 64d39114ee | |||
| 88d256de73 | |||
| 5a92dc2834 | |||
| 726bbf4bd2 | |||
| bd6cf5bc1e | |||
| 089315118d | |||
| 9737006a8d | |||
| 054db9f5eb | |||
| 9b587ee165 | |||
| 9282e8ef09 | |||
| ac80fe19bf | |||
| cd6643a642 | |||
| ba6f96ca4e | |||
| 9cc5dd969b | |||
| 98c32d3be4 | |||
| 4c77b9c7bf | |||
| 3b1760e959 | |||
| b28208bd76 | |||
| ffbbc1ccdf | |||
| 1d071c0fd8 | |||
| 948e87159d | |||
| c3df8da577 | |||
| 7cfb25558f | |||
| 31b41f99f8 | |||
| 9eae62581a | |||
| daad21ef91 | |||
| 219564b56c | |||
| ea19bed247 | |||
| b6db83ed5e | |||
| 0726d16632 | |||
| 7feec2b7d9 | |||
| c7c0cfaeb7 | |||
| d3a6b06566 | |||
| e87e028a49 | |||
| bfda719e64 | |||
| 7e37c40516 | |||
| e28406fa2c | |||
| 9033b5502d | |||
| 07506b003e | |||
| 201a206ed9 | |||
| 2d2d9ae213 | |||
| 376638978a | |||
| 29ec90e8e8 | |||
| ece1518eac | |||
| a5ba6a0e9d | |||
| 331f575c17 | |||
| b31ae9c9f6 | |||
| 8d39d636be | |||
| da26e72bee | |||
| e11b8a82e7 | |||
| 12c9bdbd56 | |||
| 9e49adc353 | |||
| 9fdba3a730 | |||
| 7bea29c18e | |||
| 29447a41a3 | |||
| d85f084acb | |||
| d598ef7569 | |||
| e41b683efb | |||
| 9a2ba1dabe | |||
| f3428e65bc | |||
| 5eaef96078 | |||
| b5626b275b | |||
| 795a18b565 | |||
| 82ae2d5890 | |||
| db0e2ec103 | |||
| e0ee737080 | |||
| 0673a74302 | |||
| a8d1c0ddbf | |||
| 8431c1b1e3 | |||
| a121f861dc | |||
| 6861cb4c3b | |||
| 5d9fc8359d | |||
| f542187654 | |||
| e3c664f81d | |||
| 7b78fed106 | |||
| d685bbfa9a | |||
| 27732ccc88 | |||
| 29fcf5904a | |||
| f0e53d18a8 | |||
| a2573fb285 | |||
| 031b719bb0 | |||
| d8417e050c | |||
| 02aec71383 | |||
| fca993a830 | |||
| a79d8347bd | |||
| f057846e01 | |||
| 27fdd03ec3 | |||
| ea4b5e3251 | |||
| 378e37b082 | |||
| 5c82f55200 | |||
| b553dc35b6 | |||
| e6db0bd6ed | |||
| 85c69d853f | |||
| c6bd437242 | |||
| e192149480 | |||
| 0d99e16b59 | |||
| 744dbd5e26 | |||
| 7088fa1f69 | |||
| 642acf8bca | |||
| 747993e2ca | |||
| 1f9a7e3a91 | |||
| 874234449b | |||
| 5fe267089a | |||
| 2442e302ad | |||
| 0612271732 | |||
| c261ce78fb | |||
| 7398e7d45e | |||
| 6e2d499ca9 | |||
| 4680ece2d9 | |||
| 8a4f3769d8 | |||
| cc5172e70b | |||
| a32ae97860 | |||
| d6dd62dfe5 | |||
| 216f071e64 | |||
| f47df5f9b2 | |||
| ff3c4b885c | |||
| f86c7c844c | |||
| 5db2172ed6 | |||
| c4c121befc | |||
| b4542753ba | |||
| 2a99c97d52 | |||
| 0a407c3d8b | |||
| b2596fdc24 | |||
| 2dbe1b00d3 | |||
| 1fc9c83df1 | |||
| d31e1a333d | |||
| e1bba7072b | |||
| 94c7d68413 | |||
| 9ed77a5875 | |||
| 7d6d3b8f55 | |||
| 5480d61f70 | |||
| 96cd714385 | |||
| e29d92f92e | |||
| c5f57bbcc5 | |||
| 9835afc645 | |||
| 1f373eac8d | |||
| ede96a314c | |||
| 72206ac9f6 | |||
| 62dedb2a2e | |||
| 7189c5b461 | |||
| 1a445f9d3f | |||
| aa22884079 | |||
| 316d4caf37 | |||
| 60a489eb68 | |||
| b22e211044 | |||
| ca401b9af9 | |||
| addce8b691 | |||
| 42fc841dc1 | |||
| 4c0916b64b | |||
| 3989d35abd | |||
| 72e2ae1f14 | |||
| 3e37f575d8 | |||
| c0d480366d | |||
| 9de7561154 | |||
| 0866b9075f | |||
| 055696f576 | |||
| 854415160c | |||
| 8f4f7d1291 | |||
| ee2c77acd8 | |||
| fc90ec1b53 | |||
| 7d78944d14 | |||
| f2e0fe8589 | |||
| 39dcf9bd19 | |||
| 25f38df4ca | |||
| a1f1f98f94 | |||
| affc499913 | |||
| c7574b545c | |||
| 9e852ba12d | |||
| 174eae802a | |||
| b9f058fcc9 | |||
| 6de15b6310 | |||
| 2844797684 | |||
| 9f4e439478 | |||
| 9d09ee133d | |||
| d88f0e8f3c | |||
| 0857478516 | |||
| 66d9809057 | |||
| db8a7e8caf | |||
| 8f18e34da0 | |||
| 525f26dc23 | |||
| a0fcfa3ed2 | |||
| 15f276537c | |||
| 623a12050e | |||
| ae2d2d1021 | |||
| a109152a13 | |||
| febbcd6027 | |||
| 549e60136b | |||
| 14d2b8290a | |||
| cdc069d8e7 | |||
| fb5094f688 | |||
| 670e0bdc45 | |||
| 89235012af | |||
| 2bfbad2874 | |||
| 4ba7696032 | |||
| 66645516e5 | |||
| eb2ff2d2ca | |||
| 4153245f2c | |||
| 6fe9af7819 | |||
| 2edc446ced | |||
| c3c7983f7b | |||
| 22deb4df30 | |||
| da0c734aa3 | |||
| 189cebe4c9 | |||
| 9d3da44a99 | |||
| 8f17d35ed5 | |||
| 89759b6e3b | |||
| a2fc38be8d | |||
| e0414e7110 | |||
| b486af0031 | |||
| ea09ce4b8f | |||
| d465da43e3 | |||
| 84ed487950 | |||
| 3c07144211 | |||
| 3936e736e6 | |||
| 68433d4f5b | |||
| cc16cb35bf | |||
| d3bc6321f4 | |||
| cbbb5ad231 | |||
| c1d03c5bcc | |||
| 61a8ed515f | |||
| bbb7c06504 | |||
| 8bbdb272d4 | |||
| c15ae94307 | |||
| f1a5048558 | |||
| 1fe26bff9a | |||
| 433dcd156b | |||
| e97f0a289e | |||
| 89f35170b3 | |||
| 8188fb2d7d | |||
| 87cf95f50b | |||
| 8ab26cb823 | |||
| 5880c8e1af | |||
| 14bf4ecb4c | |||
| 04a5e58781 | |||
| bbd4389345 | |||
| f350ccdf95 | |||
| 4f2de9395e | |||
| b0dbbebfb0 | |||
| 0c27b4bd47 | |||
| 736cd9e5f2 | |||
| c7a603c926 | |||
| a28d6d5693 | |||
| e59d2233e2 | |||
| 01914a06ef | |||
| 6499374d1c | |||
| b054919d5c | |||
| 048ee9c2e5 | |||
| 23394548ca | |||
| b04677b806 | |||
| e4c902dd93 | |||
| 5d8bd258c0 | |||
| 08c5283c8c | |||
| 10a14f10cd | |||
| f86ebc52a0 | |||
| 016ed90efa | |||
| d76407b201 | |||
| 5de6b660f2 | |||
| 71ada3b656 | |||
| dc42f0e226 | |||
| 74bf9f6467 |
+17
-8
@@ -13,7 +13,7 @@ body:
|
||||
attributes:
|
||||
label: 请确认以下事项
|
||||
description: |
|
||||
您必须勾选以下内容,否则您的问题可能会被直接关闭。
|
||||
您必须阅读、检查、确认、同意以下内容,否则您的问题一定会被直接关闭。
|
||||
或者您可以去[讨论区](https://github.com/OpenListTeam/OpenList/discussions)。
|
||||
options:
|
||||
- label: |
|
||||
@@ -34,7 +34,8 @@ body:
|
||||
我认为此问题必须由`OpenList`处理,而非第三方。
|
||||
- label: |
|
||||
我已确认这个问题在最新版本中没有被修复。
|
||||
|
||||
- label: |
|
||||
我没有阅读这个清单,只是闭眼选中了所有的复选框,请关闭这个 Issue 。
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
@@ -59,6 +60,14 @@ body:
|
||||
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:
|
||||
@@ -67,15 +76,15 @@ body:
|
||||
请提供您的`OpenList`应用的配置文件,并截图相关存储配置。(可隐藏隐私字段)
|
||||
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)
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 复现链接(可选)
|
||||
description: |
|
||||
请提供能复现此问题的链接。
|
||||
- type: textarea
|
||||
id: aigenerated
|
||||
attributes:
|
||||
label: AI生成内容(可选)
|
||||
description: |
|
||||
如果此问题是由AI辅助您发现的,请提供全部聊天记录,包括使用的模型信息。
|
||||
+17
-8
@@ -13,7 +13,7 @@ body:
|
||||
attributes:
|
||||
label: Please confirm the following
|
||||
description: |
|
||||
You must check all the following, otherwise your issue may be closed directly.
|
||||
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: |
|
||||
@@ -34,7 +34,8 @@ body:
|
||||
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:
|
||||
@@ -59,6 +60,14 @@ body:
|
||||
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:
|
||||
@@ -67,15 +76,15 @@ body:
|
||||
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: logs
|
||||
attributes:
|
||||
label: Logs (optional)
|
||||
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)
|
||||
- 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.
|
||||
+9
-1
@@ -7,7 +7,7 @@ body:
|
||||
attributes:
|
||||
label: 请确认以下事项
|
||||
description: |
|
||||
您必须勾选以下内容,否则您的问题可能会被直接关闭。
|
||||
您必须阅读、检查、确认、同意以下内容,否则您的问题可能会被直接关闭。
|
||||
或者您可以去[讨论区](https://github.com/OpenListTeam/OpenList/discussions)。
|
||||
options:
|
||||
- label: |
|
||||
@@ -28,6 +28,8 @@ body:
|
||||
我已确认此功能尚未被实现。
|
||||
- label: |
|
||||
我已确认此功能是合理的,且有普遍需求,并非我个人需要。
|
||||
- label: |
|
||||
我没有阅读这个清单,只是闭眼选中了所有的复选框,请关闭这个 Issue 。
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
@@ -46,3 +48,9 @@ body:
|
||||
label: 附加信息
|
||||
description: |
|
||||
相关的任何其他上下文或截图,或者你觉得有帮助的信息
|
||||
- type: textarea
|
||||
id: aigenerated
|
||||
attributes:
|
||||
label: AI生成内容(可选)
|
||||
description: |
|
||||
如果此请求是由AI辅助您提交的,请提供全部聊天记录,包括使用的模型信息。
|
||||
+10
-2
@@ -1,13 +1,13 @@
|
||||
name: "Feature Request"
|
||||
description: Feature Request / Enhancement
|
||||
title: "[Feature] Please change the title to your feature name"
|
||||
title: "[Feature] Please modify the title to your feature name"
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please confirm the following
|
||||
description: |
|
||||
You must check all the following, otherwise your request may be closed directly.
|
||||
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: |
|
||||
@@ -28,6 +28,8 @@ body:
|
||||
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:
|
||||
@@ -46,3 +48,9 @@ body:
|
||||
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.
|
||||
@@ -0,0 +1,114 @@
|
||||
<!--
|
||||
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 辅助内容。
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -40,15 +40,18 @@ jobs:
|
||||
npx changelogithub --output CHANGELOG.md
|
||||
|
||||
- name: Upload assets to beta release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: CHANGELOG.md
|
||||
files: CHANGELOG.md
|
||||
prerelease: true
|
||||
tag_name: beta
|
||||
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
|
||||
|
||||
- name: Upload assets to github artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: beta changelog
|
||||
path: ${{ github.workspace }}/CHANGELOG.md
|
||||
@@ -61,33 +64,58 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: "!(*musl*|*windows-arm64*|*windows7-*|*android*|*freebsd*)" # xgo and loongarch
|
||||
- target: "!(*musl*|*windows-arm64*|*windows7-*|*android*|*freebsd*)" # xgo and loongarch (exclude mips64le)
|
||||
hash: "md5"
|
||||
- target: "linux-!(arm*)-musl*" #musl-not-arm
|
||||
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)
|
||||
hash: "md5-linux-musl"
|
||||
flags: "-ldflags=-linkmode external -extldflags '-static -fpic'"
|
||||
goflags: ""
|
||||
musl_static: "true"
|
||||
- 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
|
||||
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
|
||||
hash: "md5-android"
|
||||
flags: ""
|
||||
goflags: ""
|
||||
musl_static: "false"
|
||||
- target: "freebsd-*" #freebsd
|
||||
hash: "md5-freebsd"
|
||||
flags: ""
|
||||
goflags: ""
|
||||
musl_static: "false"
|
||||
|
||||
name: Beta Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.5"
|
||||
go-version: "1.26.4"
|
||||
|
||||
- name: Setup web
|
||||
run: bash build.sh dev web
|
||||
@@ -96,9 +124,11 @@ jobs:
|
||||
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
|
||||
|
||||
- name: Build
|
||||
uses: OpenListTeam/cgo-actions@v1.2.2
|
||||
uses: OpenListTeam/cgo-actions@v1.2.5
|
||||
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
|
||||
@@ -110,6 +140,26 @@ jobs:
|
||||
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
|
||||
|
||||
- name: Compress
|
||||
run: |
|
||||
@@ -117,13 +167,13 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# See above
|
||||
- name: Upload assets to beta release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/compress/*
|
||||
prerelease: true
|
||||
tag_name: beta
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
for file in build/compress/*; do
|
||||
gh release upload beta "$file" --clobber
|
||||
done
|
||||
|
||||
- name: Clean illegal characters from matrix.target
|
||||
id: clean_target_name
|
||||
@@ -135,7 +185,7 @@ jobs:
|
||||
echo "cleaned_target=$CLEANED_TARGET" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload assets to github artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: beta builds for ${{ env.cleaned_target }}
|
||||
path: ${{ github.workspace }}/build/compress/*
|
||||
@@ -25,15 +25,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: benjlevesque/short-sha@v3.0
|
||||
id: short-sha
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.5"
|
||||
go-version: "1.26.4"
|
||||
|
||||
- name: Setup web
|
||||
run: bash build.sh dev web
|
||||
@@ -42,9 +42,11 @@ jobs:
|
||||
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
|
||||
|
||||
- name: Build
|
||||
uses: OpenListTeam/cgo-actions@v1.2.2
|
||||
uses: OpenListTeam/cgo-actions@v1.2.5
|
||||
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
|
||||
@@ -56,8 +58,18 @@ jobs:
|
||||
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
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: openlist_${{ steps.short-sha.outputs.sha }}_${{ matrix.target }}
|
||||
path: build/*
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
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,101 @@
|
||||
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
|
||||
});
|
||||
}
|
||||
@@ -44,12 +44,12 @@ jobs:
|
||||
swap-storage: true
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.24'
|
||||
go-version: '1.26.4'
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -69,8 +69,9 @@ jobs:
|
||||
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
|
||||
|
||||
- name: Upload assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/compress/*
|
||||
prerelease: false
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
for file in build/compress/*; do
|
||||
gh release upload "${{ github.event.release.tag_name }}" "$file" --clobber
|
||||
done
|
||||
+10
-10
@@ -43,11 +43,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version: '1.26'
|
||||
|
||||
- name: Cache Musl
|
||||
id: cache-musl
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
overwrite: true
|
||||
@@ -83,11 +83,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version: '1.26.4'
|
||||
|
||||
- name: Cache Musl
|
||||
id: cache-musl
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME_LITE }}
|
||||
overwrite: true
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
tag_favor: "suffix=-aio,onlatest=true"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
tag_favor: "suffix=-lite-aio,onlatest=true"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME_LITE }}
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Sync to Gitee
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
name: Sync GitHub to Gitee
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@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
|
||||
@@ -32,11 +32,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version: '1.26.4'
|
||||
|
||||
- name: Cache Musl
|
||||
id: cache-musl
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
overwrite: true
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
tag_favor: "suffix=-aio,onlatest=true"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
@@ -0,0 +1,60 @@
|
||||
# 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.
|
||||
@@ -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>.
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# Contributing
|
||||
|
||||
## Setup your machine
|
||||
|
||||
`OpenList` is written in [Go](https://golang.org/) and [SolidJS](https://www.solidjs.com/).
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- [git](https://git-scm.com)
|
||||
- [Go](https://golang.org/doc/install) version declared in [`go.mod`](./go.mod)
|
||||
- [gcc](https://gcc.gnu.org/)
|
||||
- [nodejs](https://nodejs.org/)
|
||||
|
||||
## Cloning a fork
|
||||
|
||||
Fork and clone `OpenList` and `OpenList-Frontend` anywhere:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/<your-username>/OpenList.git
|
||||
git clone --recurse-submodules https://github.com/<your-username>/OpenList-Frontend.git
|
||||
```
|
||||
|
||||
## Creating a branch
|
||||
|
||||
Create a new branch from the `main` branch, with an appropriate name.
|
||||
|
||||
```shell
|
||||
git checkout -b <branch-name>
|
||||
```
|
||||
|
||||
## Preview your change
|
||||
|
||||
### backend
|
||||
|
||||
```shell
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### frontend
|
||||
|
||||
```shell
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Add a new driver
|
||||
|
||||
Copy `drivers/template` folder and rename it, and follow the comments in it.
|
||||
|
||||
## 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).
|
||||
|
||||
<https://github.com/OpenListTeam/OpenList/issues/376>
|
||||
|
||||
It's suggested to sign your commits. See: [How to sign commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
|
||||
|
||||
## Submit a pull request
|
||||
|
||||
Please make sure your code has been formatted with `go fmt` or [prettier](https://prettier.io/) before submitting.
|
||||
|
||||
Push your branch to your `openlist` fork and open a pull request against the `main` branch.
|
||||
|
||||
## Merge your pull request
|
||||
|
||||
Your pull request will be merged after review. Please wait for the maintainer to merge your pull request after review.
|
||||
|
||||
At least 1 approving review is required by reviewers with write access. You can also request a review from maintainers.
|
||||
|
||||
## Delete your branch
|
||||
|
||||
(Optional) After your pull request is merged, you can delete your branch.
|
||||
|
||||
## 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!
|
||||
@@ -20,11 +20,12 @@ ARG GID=1001
|
||||
|
||||
WORKDIR /opt/openlist/
|
||||
|
||||
COPY --chmod=755 --from=builder /app/bin/openlist ./
|
||||
COPY --chmod=755 entrypoint.sh /entrypoint.sh
|
||||
RUN adduser -u ${UID} -g ${GID} -h /opt/openlist/data -D -s /bin/sh ${USER} \
|
||||
&& chown -R ${UID}:${GID} /opt \
|
||||
&& chown -R ${UID}:${GID} /entrypoint.sh
|
||||
RUN addgroup -g ${GID} ${USER} && \
|
||||
adduser -D -u ${UID} -G ${USER} ${USER} && \
|
||||
mkdir -p /opt/openlist/data
|
||||
|
||||
COPY --from=builder --chmod=755 --chown=${UID}:${GID} /app/bin/openlist ./
|
||||
COPY --chmod=755 --chown=${UID}:${GID} entrypoint.sh /entrypoint.sh
|
||||
|
||||
USER ${USER}
|
||||
RUN /entrypoint.sh version
|
||||
@@ -10,12 +10,12 @@ ARG GID=1001
|
||||
|
||||
WORKDIR /opt/openlist/
|
||||
|
||||
COPY --chmod=755 /build/${TARGETPLATFORM}/openlist ./
|
||||
COPY --chmod=755 entrypoint.sh /entrypoint.sh
|
||||
RUN addgroup -g ${GID} ${USER} && \
|
||||
adduser -D -u ${UID} -G ${USER} ${USER} && \
|
||||
mkdir -p /opt/openlist/data
|
||||
|
||||
RUN adduser -u ${UID} -g ${GID} -h /opt/openlist/data -D -s /bin/sh ${USER} \
|
||||
&& chown -R ${UID}:${GID} /opt \
|
||||
&& chown -R ${UID}:${GID} /entrypoint.sh
|
||||
COPY --chmod=755 --chown=${UID}:${GID} /build/${TARGETPLATFORM}/openlist ./
|
||||
COPY --chmod=755 --chown=${UID}:${GID} entrypoint.sh /entrypoint.sh
|
||||
|
||||
USER ${USER}
|
||||
RUN /entrypoint.sh version
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img style="width: 128px; height: 128px;" src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" alt="logo" />
|
||||
<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>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
---
|
||||
|
||||
- English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Dutch](./README_nl.md)
|
||||
- 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)
|
||||
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
- [CODE OF CONDUCT](./CODE_OF_CONDUCT.md)
|
||||
@@ -64,7 +64,9 @@ Thank you for your support and understanding of the OpenList project.
|
||||
- [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)
|
||||
@@ -74,7 +76,6 @@ Thank you for your support and understanding of the OpenList project.
|
||||
- [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)
|
||||
@@ -85,6 +86,16 @@ Thank you for your support and understanding of the OpenList project.
|
||||
- [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
|
||||
@@ -106,18 +117,32 @@ Thank you for your support and understanding of the OpenList project.
|
||||
|
||||
## Document
|
||||
|
||||
- 📘 [Global Site](https://doc.oplist.org)
|
||||
- 📚 [Backup Site](https://doc.openlist.team)
|
||||
- 🌏 [CN Site](https://doc.oplist.org.cn)
|
||||
- 📘 [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
|
||||
|
||||
N/A (to be rebuilt)
|
||||
- 🌎 [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 raising general questions, ***Issues* is for bug reports and feature requests only.**
|
||||
|
||||
## Sponsor
|
||||
|
||||
[](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.
|
||||
@@ -147,3 +172,4 @@ We sincerely thank the author [Xhofe](https://github.com/Xhofe) of the original
|
||||
Thanks goes to these wonderful people:
|
||||
|
||||
[](https://github.com/OpenListTeam/OpenList/graphs/contributors)
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<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.
|
||||
|
||||
## الراعي
|
||||
|
||||
[](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.
|
||||
|
||||
[](https://github.com/OpenListTeam/OpenList/graphs/contributors)
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img style="width: 128px; height: 128px;" src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" alt="logo" />
|
||||
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
|
||||
|
||||
<p><em>OpenList 是一个有韧性、长期治理、社区驱动的 AList 分支,旨在防御基于信任的开源攻击。</em></p>
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
---
|
||||
|
||||
- [English](./README.md) | 中文 | [日本語](./README_ja.md) | [Dutch](./README_nl.md)
|
||||
- [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)
|
||||
- [贡献指南](../CONTRIBUTING.md)
|
||||
- [行为准则](../CODE_OF_CONDUCT.md)
|
||||
- [许可证](../LICENSE)
|
||||
|
||||
## 免责声明
|
||||
|
||||
@@ -64,7 +64,9 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3
|
||||
- [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)
|
||||
@@ -74,7 +76,6 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3
|
||||
- [x] [迅雷网盘](https://pan.xunlei.com)
|
||||
- [x] [蓝奏云](https://www.lanzou.com)
|
||||
- [x] [蓝奏云优享版](https://www.ilanzou.com)
|
||||
- [x] [阿里云盘分享](https://www.alipan.com)
|
||||
- [x] [Google 相册](https://photos.google.com)
|
||||
- [x] [Mega.nz](https://mega.nz)
|
||||
- [x] [百度相册](https://photo.baidu.com)
|
||||
@@ -85,6 +86,16 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3
|
||||
- [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] 画廊模式下的图片预览
|
||||
@@ -106,18 +117,32 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3
|
||||
|
||||
## 文档
|
||||
|
||||
- 🌏 [国内站点](https://doc.oplist.org.cn)
|
||||
- 📘 [海外站点](https://doc.oplist.org)
|
||||
- 📚 [备用站点](https://doc.openlist.team)
|
||||
- 📘 [文档](https://doc.oplist.org)
|
||||
- 🌏 [中国镜像](https://doc.oplist.org.cn)
|
||||
- ⚖️ [使用条款](https://doc.oplist.org/terms)
|
||||
- 🔒 [隐私政策](https://doc.oplist.org/privacy)
|
||||
|
||||
## 演示
|
||||
## Demo
|
||||
|
||||
N/A(待重建)
|
||||
- 🌎 [全球 Demo](https://demo.oplist.org)
|
||||
- 🇨🇳 [中国 Demo](https://demo.oplist.org.cn)
|
||||
|
||||
## 讨论
|
||||
|
||||
如有一般性问题请前往 [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) 讨论区,***Issues* 仅用于错误报告和功能请求。**
|
||||
|
||||
## 赞助者
|
||||
|
||||
[](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) 许可证的开源软件。
|
||||
@@ -132,7 +157,7 @@ N/A(待重建)
|
||||
- 本软件按“原样”提供,不附带任何明示或暗示的担保,包括但不限于适销性或特定用途的适用性。
|
||||
- 维护者不对因使用或无法使用本软件而导致的任何直接或间接损失负责。
|
||||
- 您需自行承担使用本软件的所有风险,包括但不限于账号被封、下载限速等。
|
||||
- 本项目遵循 [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) 许可证,详情请参见 [LICENSE](./LICENSE) 文件。
|
||||
- 本项目遵循 [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) 许可证,详情请参见 [LICENSE](../LICENSE) 文件。
|
||||
|
||||
## 联系我们
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<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
|
||||
|
||||
[](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.
|
||||
|
||||
[](https://github.com/OpenListTeam/OpenList/graphs/contributors)
|
||||
@@ -0,0 +1,136 @@
|
||||
<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
|
||||
|
||||
[](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.
|
||||
|
||||
[](https://github.com/OpenListTeam/OpenList/graphs/contributors)
|
||||
@@ -0,0 +1,136 @@
|
||||
<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
|
||||
|
||||
[](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.
|
||||
|
||||
[](https://github.com/OpenListTeam/OpenList/graphs/contributors)
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img style="width: 128px; height: 128px;" src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" alt="logo" />
|
||||
<img src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" width="128" height="128" alt="logo" />
|
||||
|
||||
<p><em>OpenList は、信頼ベースの攻撃からオープンソースを守るために構築された、レジリエントで長期ガバナンス、コミュニティ主導の AList フォークです。</em></p>
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
---
|
||||
|
||||
- [English](./README.md) | [中文](./README_cn.md) | 日本語 | [Dutch](./README_nl.md)
|
||||
- [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)
|
||||
- [コントリビュート](../CONTRIBUTING.md)
|
||||
- [行動規範](../CODE_OF_CONDUCT.md)
|
||||
- [ライセンス](../LICENSE)
|
||||
|
||||
## 免責事項
|
||||
|
||||
@@ -65,6 +65,7 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい
|
||||
- [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)
|
||||
@@ -74,7 +75,6 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい
|
||||
- [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)
|
||||
@@ -85,6 +85,17 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい
|
||||
- [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] ギャラリーモードでの画像プレビュー
|
||||
@@ -106,18 +117,32 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい
|
||||
|
||||
## ドキュメント
|
||||
|
||||
- 📘 [グローバルサイト](https://doc.oplist.org)
|
||||
- 📚 [バックアップサイト](https://doc.openlist.team)
|
||||
- 🌏 [CNサイト](https://doc.oplist.org.cn)
|
||||
- 📘 [ドキュメント](https://doc.oplist.org)
|
||||
- 🌏 [中国ミラー](https://doc.oplist.org.cn)
|
||||
- ⚖️ [利用規約](https://doc.oplist.org/terms)
|
||||
- 🔒 [プライバシーポリシー](https://doc.oplist.org/privacy)
|
||||
|
||||
## デモ
|
||||
## Demo
|
||||
|
||||
N/A(再構築中)
|
||||
- 🌎 [グローバル Demo](https://demo.oplist.org)
|
||||
- 🇨🇳 [中国 Demo](https://demo.oplist.org.cn)
|
||||
|
||||
## ディスカッション
|
||||
|
||||
一般的な質問は [*Discussions*](https://github.com/OpenListTeam/OpenList/discussions) をご利用ください。***Issues* はバグ報告と機能リクエスト専用です。**
|
||||
|
||||
## スポンサー
|
||||
|
||||
[](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) ライセンスの下で公開されているオープンソースソフトウェアです。
|
||||
@@ -132,7 +157,7 @@ N/A(再構築中)
|
||||
- 本ソフトウェアは「現状有姿」で提供されており、商品性や特定目的への適合性を含むいかなる保証もありません。
|
||||
- 本ソフトウェアの使用または使用不能によるいかなる直接的・間接的損害についても、メンテナは責任を負いません。
|
||||
- 本ソフトウェアの利用に伴うすべてのリスク(アカウントの凍結やダウンロード速度制限などを含む)は、利用者自身が負うものとします。
|
||||
- 本プロジェクトは [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) ライセンスに従います。詳細は [LICENSE](./LICENSE) ファイルをご覧ください。
|
||||
- 本プロジェクトは [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.txt) ライセンスに従います。詳細は [LICENSE](../LICENSE) ファイルをご覧ください。
|
||||
|
||||
## お問い合わせ
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<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.
|
||||
|
||||
## 후원
|
||||
|
||||
[](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.
|
||||
|
||||
[](https://github.com/OpenListTeam/OpenList/graphs/contributors)
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img style="width: 128px; height: 128px;" src="https://raw.githubusercontent.com/OpenListTeam/Logo/main/logo.svg" alt="logo" />
|
||||
<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>
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
---
|
||||
|
||||
- [English](./README.md) | [中文](./README_cn.md) | [日本語](./README_ja.md) | Dutch
|
||||
- [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)
|
||||
- [Bijdragen](../CONTRIBUTING.md)
|
||||
- [Gedragscode](../CODE_OF_CONDUCT.md)
|
||||
- [Licentie](../LICENSE)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
@@ -64,7 +64,9 @@ Dank u voor uw ondersteuning en begrip
|
||||
- [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)
|
||||
@@ -74,7 +76,6 @@ Dank u voor uw ondersteuning en begrip
|
||||
- [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)
|
||||
@@ -85,6 +86,16 @@ Dank u voor uw ondersteuning en begrip
|
||||
- [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
|
||||
@@ -106,18 +117,32 @@ Dank u voor uw ondersteuning en begrip
|
||||
|
||||
## Documentatie
|
||||
|
||||
- 📘 [Global Site](https://doc.oplist.org)
|
||||
- 📚 [Backup Site](https://doc.openlist.team)
|
||||
- 🌏 [CN Site](https://doc.oplist.org.cn)
|
||||
- 📘 [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
|
||||
|
||||
N.v.t. (wordt opnieuw opgebouwd)
|
||||
- 🌎 [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
|
||||
|
||||
[](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.
|
||||
@@ -132,7 +157,7 @@ Stel algemene vragen in [*Discussions*](https://github.com/OpenListTeam/OpenList
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<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.
|
||||
|
||||
## Спонсор
|
||||
|
||||
[](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.
|
||||
|
||||
[](https://github.com/OpenListTeam/OpenList/graphs/contributors)
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# 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 变得更加安全的安全研究人员和社区成员。
|
||||
@@ -1,11 +0,0 @@
|
||||
version: v1
|
||||
plugins:
|
||||
- plugin: buf.build/protocolbuffers/go:v1.36.7
|
||||
out: .
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- plugin: buf.build/grpc/go:v1.5.1
|
||||
out: .
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- require_unimplemented_servers=false
|
||||
+76
-17
@@ -48,6 +48,54 @@ ldflags="\
|
||||
-X 'github.com/OpenListTeam/OpenList/v4/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
|
||||
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')
|
||||
@@ -110,6 +158,7 @@ BuildWin7() {
|
||||
# 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
|
||||
@@ -124,14 +173,14 @@ BuildWin7() {
|
||||
fi
|
||||
|
||||
# Use the patched Go compiler for Win7 compatibility
|
||||
$(pwd)/go-win7/bin/go build -o "${1}-${arch}.exe" -ldflags="$ldflags" -tags=jsoniter .
|
||||
$(pwd)/go-win7/bin/go build -o "${1}-${arch}.exe" -ldflags="$ldflags" -tags="$build_tags" .
|
||||
done
|
||||
}
|
||||
|
||||
BuildDev() {
|
||||
rm -rf .git/
|
||||
mkdir -p "dist"
|
||||
muslflags="--extldflags '-static -fpic' $ldflags"
|
||||
muslflags="$(GetMuslStaticLdflags)"
|
||||
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
|
||||
@@ -149,7 +198,8 @@ BuildDev() {
|
||||
export GOARCH=${os_arch##*-}
|
||||
export CC=${cgo_cc}
|
||||
export CGO_ENABLED=1
|
||||
go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
||||
CGO_LDFLAGS="-static" go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
||||
AssertStaticBinary "./dist/$appName-$os_arch"
|
||||
done
|
||||
xgo -targets=windows/amd64,darwin/amd64,darwin/arm64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
|
||||
mv "$appName"-* dist
|
||||
@@ -183,7 +233,7 @@ BuildDockerMultiplatform() {
|
||||
# run PrepareBuildDockerMusl before build
|
||||
export PATH=$PATH:$PWD/build/musl-libs/bin
|
||||
|
||||
docker_lflags="--extldflags '-static -fpic' $ldflags"
|
||||
docker_lflags="$(GetMuslStaticLdflags)"
|
||||
export CGO_ENABLED=1
|
||||
|
||||
OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-riscv64 linux-ppc64le linux-loong64) ## Disable linux-s390x builds
|
||||
@@ -193,11 +243,13 @@ BuildDockerMultiplatform() {
|
||||
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"
|
||||
go build -o build/$os/$arch/"$appName" -ldflags="$docker_lflags" -tags=jsoniter .
|
||||
CGO_LDFLAGS="-static" go build -o build/$os/$arch/"$appName" -ldflags="$docker_lflags" -tags="$build_tags" .
|
||||
AssertStaticBinary "build/$os/$arch/$appName"
|
||||
done
|
||||
|
||||
DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)
|
||||
@@ -211,7 +263,8 @@ BuildDockerMultiplatform() {
|
||||
export GOARM=${GO_ARM[$i]}
|
||||
export CC=${cgo_cc}
|
||||
echo "building for $docker_arch"
|
||||
go build -o build/${docker_arch%%-*}/${docker_arch##*-}/"$appName" -ldflags="$docker_lflags" -tags=jsoniter .
|
||||
CGO_LDFLAGS="-static" go build -o build/${docker_arch%%-*}/${docker_arch##*-}/"$appName" -ldflags="$docker_lflags" -tags=jsoniter .
|
||||
AssertStaticBinary "build/${docker_arch%%-*}/${docker_arch##*-}/$appName"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -236,7 +289,9 @@ BuildRelease() {
|
||||
BuildLoongGLIBC() {
|
||||
local target_abi="$2"
|
||||
local output_file="$1"
|
||||
local oldWorldGoVersion="1.24.3"
|
||||
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
|
||||
@@ -254,13 +309,13 @@ BuildLoongGLIBC() {
|
||||
|
||||
# Download and setup patched Go compiler for old-world
|
||||
if ! curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250722/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
|
||||
"https://github.com/loong64-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/loong64-abi1.0-toolchains/releases/download/20250722/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
|
||||
"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
|
||||
@@ -311,7 +366,7 @@ BuildLoongGLIBC() {
|
||||
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=jsoniter .; then
|
||||
$(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
|
||||
@@ -320,7 +375,7 @@ BuildLoongGLIBC() {
|
||||
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=jsoniter .; then
|
||||
$(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"
|
||||
@@ -366,11 +421,11 @@ BuildLoongGLIBC() {
|
||||
|
||||
# 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=jsoniter .; then
|
||||
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=jsoniter .; then
|
||||
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"
|
||||
@@ -389,8 +444,9 @@ BuildLoongGLIBC() {
|
||||
BuildReleaseLinuxMusl() {
|
||||
rm -rf .git/
|
||||
mkdir -p "build"
|
||||
muslflags="--extldflags '-static -fpic' $ldflags"
|
||||
muslflags="$(GetMuslStaticLdflags)"
|
||||
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"
|
||||
@@ -403,19 +459,21 @@ 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
|
||||
go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
||||
CGO_LDFLAGS="-static" go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags="$build_tags" .
|
||||
AssertStaticBinary "./build/$appName-$os_arch"
|
||||
done
|
||||
}
|
||||
|
||||
BuildReleaseLinuxMuslArm() {
|
||||
rm -rf .git/
|
||||
mkdir -p "build"
|
||||
muslflags="--extldflags '-static -fpic' $ldflags"
|
||||
muslflags="$(GetMuslStaticLdflags)"
|
||||
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
|
||||
@@ -437,7 +495,8 @@ BuildReleaseLinuxMuslArm() {
|
||||
export CC=${cgo_cc}
|
||||
export CGO_ENABLED=1
|
||||
export GOARM=${arm}
|
||||
go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
||||
CGO_LDFLAGS="-static" go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
|
||||
AssertStaticBinary "./build/$appName-$os_arch"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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"
|
||||
@@ -20,8 +21,8 @@ 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) {
|
||||
Init()
|
||||
defer Release()
|
||||
bootstrap.Init()
|
||||
defer bootstrap.Release()
|
||||
admin, err := op.GetAdmin()
|
||||
if err != nil {
|
||||
utils.Log.Errorf("failed get admin user: %+v", err)
|
||||
@@ -61,8 +62,8 @@ var ShowTokenCmd = &cobra.Command{
|
||||
Use: "token",
|
||||
Short: "Show admin token",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Init()
|
||||
defer Release()
|
||||
bootstrap.Init()
|
||||
defer bootstrap.Release()
|
||||
token := setting.GetStr(conf.Token)
|
||||
utils.Log.Infof("show admin token from CLI")
|
||||
fmt.Println("Admin token:", token)
|
||||
@@ -70,8 +71,8 @@ var ShowTokenCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func setAdminPassword(pwd string) {
|
||||
Init()
|
||||
defer Release()
|
||||
bootstrap.Init()
|
||||
defer bootstrap.Release()
|
||||
admin, err := op.GetAdmin()
|
||||
if err != nil {
|
||||
utils.Log.Errorf("failed get admin user: %+v", err)
|
||||
@@ -6,6 +6,7 @@ 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/spf13/cobra"
|
||||
@@ -16,8 +17,8 @@ var Cancel2FACmd = &cobra.Command{
|
||||
Use: "cancel2fa",
|
||||
Short: "Delete 2FA of admin user",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Init()
|
||||
defer Release()
|
||||
bootstrap.Init()
|
||||
defer bootstrap.Release()
|
||||
admin, err := op.GetAdmin()
|
||||
if err != nil {
|
||||
utils.Log.Errorf("failed to get admin user: %+v", err)
|
||||
+30
-29
@@ -1,42 +1,43 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
||||
"github.com/OpenListTeam/OpenList/v5/internal/bootstrap"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) {
|
||||
if flags.Dev {
|
||||
flags.Debug = true
|
||||
}
|
||||
initLogrus()
|
||||
bootstrap.InitConfig()
|
||||
bootstrap.InitDriverPlugins()
|
||||
func Init() {
|
||||
bootstrap.Init()
|
||||
}
|
||||
|
||||
func Release() {
|
||||
|
||||
bootstrap.Release()
|
||||
}
|
||||
|
||||
func initLog(l *logrus.Logger) {
|
||||
if flags.Debug {
|
||||
l.SetLevel(logrus.DebugLevel)
|
||||
l.SetReportCaller(true)
|
||||
} else {
|
||||
l.SetLevel(logrus.InfoLevel)
|
||||
l.SetReportCaller(false)
|
||||
var pid = -1
|
||||
var pidFile string
|
||||
|
||||
func initDaemon() {
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
exPath := filepath.Dir(ex)
|
||||
_ = os.MkdirAll(filepath.Join(exPath, "daemon"), 0700)
|
||||
pidFile = filepath.Join(exPath, "daemon/pid")
|
||||
if utils.Exists(pidFile) {
|
||||
bytes, err := os.ReadFile(pidFile)
|
||||
if err != nil {
|
||||
log.Fatal("failed to read pid file", err)
|
||||
}
|
||||
id, err := strconv.Atoi(string(bytes))
|
||||
if err != nil {
|
||||
log.Fatal("failed to parse pid data", err)
|
||||
}
|
||||
pid = id
|
||||
}
|
||||
}
|
||||
func initLogrus() {
|
||||
formatter := logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
EnvironmentOverrideColors: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
FullTimestamp: true,
|
||||
}
|
||||
logrus.SetFormatter(&formatter)
|
||||
initLog(logrus.StandardLogger())
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
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
|
||||
+2
-31
@@ -1,40 +1,11 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
ConfigFile string
|
||||
DataDir string
|
||||
ConfigPath string
|
||||
Debug bool
|
||||
NoPrefix bool
|
||||
Dev bool
|
||||
ForceBinDir bool
|
||||
LogStd bool
|
||||
|
||||
pwd string
|
||||
)
|
||||
|
||||
// Program working directory
|
||||
func PWD() string {
|
||||
if pwd != "" {
|
||||
return pwd
|
||||
}
|
||||
if ForceBinDir {
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
pwd = filepath.Dir(ex)
|
||||
return pwd
|
||||
}
|
||||
d, err := os.Getwd()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
pwd = d
|
||||
return d
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
_ "github.com/OpenListTeam/OpenList/v4/drivers"
|
||||
@@ -69,15 +68,33 @@ func writeFile(name string, data interface{}) {
|
||||
log.Errorf("failed to unmarshal json: %+v", err)
|
||||
return
|
||||
}
|
||||
if reflect.DeepEqual(oldData, newData) {
|
||||
if mergeJson(newData, oldData) {
|
||||
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), newData, true)
|
||||
utils.WriteJsonToFile(fmt.Sprintf("lang/%s.json", name), oldData, 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{}])
|
||||
+8
-4
@@ -4,7 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
||||
"github.com/OpenListTeam/OpenList/v4/cmd/flags"
|
||||
_ "github.com/OpenListTeam/OpenList/v4/drivers"
|
||||
_ "github.com/OpenListTeam/OpenList/v4/internal/archive"
|
||||
_ "github.com/OpenListTeam/OpenList/v4/internal/offline_download"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -24,10 +27,11 @@ func Execute() {
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.PersistentFlags().StringVarP(&flags.ConfigFile, "config", "c", "data/config.json", "config file")
|
||||
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().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
|
||||
RootCmd.PersistentFlags().BoolVarP(&flags.ForceBinDir, "force-bin-dir", "f", false, "force to use the directory where the binary file is located as data directory")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "force to log to std")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.ForceBinDir, "force-bin-dir", false, "Force to use the directory where the binary file is located as data directory")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "Force to log to std")
|
||||
}
|
||||
|
||||
+19
-125
@@ -1,26 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v5/cmd/flags"
|
||||
"github.com/OpenListTeam/OpenList/v5/internal/conf"
|
||||
"github.com/OpenListTeam/OpenList/v5/server"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
// ServerCmd represents the server command
|
||||
@@ -29,127 +16,34 @@ var ServerCmd = &cobra.Command{
|
||||
Short: "Start the server at the specified address",
|
||||
Long: `Start the server at the specified address
|
||||
the address is defined in config file`,
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
serverCtx, serverCancel := context.WithCancel(context.Background())
|
||||
defer serverCancel()
|
||||
Init(serverCtx)
|
||||
|
||||
if !flags.Debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
r := gin.New()
|
||||
r.Use(gin.LoggerWithWriter(log.StandardLogger().Out))
|
||||
r.Use(gin.RecoveryWithWriter(log.StandardLogger().Out))
|
||||
server.Init(r)
|
||||
|
||||
var httpHandler http.Handler = r
|
||||
if conf.Conf.Scheme.EnableH2c {
|
||||
httpHandler = h2c.NewHandler(r, &http2.Server{})
|
||||
}
|
||||
var httpSrv, httpsSrv, unixSrv *http.Server
|
||||
if conf.Conf.Scheme.HttpPort > 0 {
|
||||
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
|
||||
log.Infoln("start HTTP server", "@", httpBase)
|
||||
httpSrv = &http.Server{Addr: httpBase, Handler: httpHandler}
|
||||
go func() {
|
||||
err := httpSrv.ListenAndServe()
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Errorln("start HTTP server", ":", err)
|
||||
serverCancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
if conf.Conf.Scheme.HttpsPort > 0 {
|
||||
httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
|
||||
log.Infoln("start HTTPS server", "@", httpsBase)
|
||||
httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
|
||||
go func() {
|
||||
err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Errorln("start HTTPS server", ":", err)
|
||||
serverCancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
if conf.Conf.Scheme.UnixFile != "" {
|
||||
log.Infoln("start Unix server", "@", conf.Conf.Scheme.UnixFile)
|
||||
unixSrv = &http.Server{Handler: httpHandler}
|
||||
go func() {
|
||||
listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
|
||||
if err != nil {
|
||||
log.Errorln("start Unix server", ":", err)
|
||||
serverCancel()
|
||||
return
|
||||
}
|
||||
|
||||
mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)
|
||||
if err != nil {
|
||||
log.Errorln("parse unix_file_perm", ":", err)
|
||||
} else {
|
||||
err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))
|
||||
if err != nil {
|
||||
log.Errorln("chmod socket file", ":", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = unixSrv.Serve(listener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Errorln("start Unix server", ":", err)
|
||||
serverCancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
bootstrap.Init()
|
||||
defer bootstrap.Release()
|
||||
bootstrap.Start()
|
||||
// Wait for interrupt signal to gracefully shutdown the server with
|
||||
// a timeout of 1 second.
|
||||
quit := make(chan os.Signal, 1)
|
||||
// kill (no param) default send syscanll.SIGTERM
|
||||
// kill -2 is syscall.SIGINT
|
||||
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
select {
|
||||
case <-quit:
|
||||
case <-serverCtx.Done():
|
||||
}
|
||||
|
||||
log.Println("shutdown server...")
|
||||
Release()
|
||||
|
||||
quitCtx, quitCancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer quitCancel()
|
||||
var wg sync.WaitGroup
|
||||
if httpSrv != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := httpSrv.Shutdown(quitCtx); err != nil {
|
||||
log.Errorln("shutdown HTTP server", ":", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if httpsSrv != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := httpsSrv.Shutdown(quitCtx); err != nil {
|
||||
log.Errorln("shutdown HTTPS server", ":", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if unixSrv != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := unixSrv.Shutdown(quitCtx); err != nil {
|
||||
log.Errorln("shutdown Unix server", ":", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
log.Println("server exit")
|
||||
<-quit
|
||||
bootstrap.Shutdown(1 * time.Second)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(ServerCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// serverCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
|
||||
// OutOpenListInit 暴露用于外部启动server的函数
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"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/charmbracelet/bubbles/table"
|
||||
@@ -30,8 +31,8 @@ var disableStorageCmd = &cobra.Command{
|
||||
return fmt.Errorf("mount path is required")
|
||||
}
|
||||
mountPath := args[0]
|
||||
Init()
|
||||
defer Release()
|
||||
bootstrap.Init()
|
||||
defer bootstrap.Release()
|
||||
storage, err := db.GetStorageByMountPath(mountPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query storage: %+v", err)
|
||||
@@ -69,8 +70,8 @@ var deleteStorageCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
Init()
|
||||
defer Release()
|
||||
bootstrap.Init()
|
||||
defer bootstrap.Release()
|
||||
err = db.DeleteStorageById(uint(id))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete storage by id: %+v", err)
|
||||
@@ -123,8 +124,8 @@ var listStorageCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all storages",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
Init()
|
||||
defer Release()
|
||||
bootstrap.Init()
|
||||
defer bootstrap.Release()
|
||||
storages, _, err := db.GetStorages(1, -1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query storages: %+v", err)
|
||||
@@ -6,10 +6,9 @@ services:
|
||||
ports:
|
||||
- '5244:5244'
|
||||
- '5245:5245'
|
||||
user: '0:0'
|
||||
environment:
|
||||
- PUID=0
|
||||
- PGID=0
|
||||
- UMASK=022
|
||||
- TZ=UTC
|
||||
- TZ=Asia/Shanghai
|
||||
container_name: openlist
|
||||
image: 'openlistteam/openlist:latest'
|
||||
@@ -0,0 +1,60 @@
|
||||
package _115
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
md5Salt = "Qclm8MGWUv59TnrR0XPg"
|
||||
appVer = "35.6.0.3"
|
||||
)
|
||||
|
||||
func (d *Pan115) getAppVersion() (string, error) {
|
||||
result := VersionResp{}
|
||||
res, err := base.RestyClient.R().Get(driver115.ApiGetVersion)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = utils.Json.Unmarshal(res.Body(), &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(result.Error) > 0 {
|
||||
return "", errors.New(result.Error)
|
||||
}
|
||||
return result.Data.Win.Version, nil
|
||||
}
|
||||
|
||||
func (d *Pan115) getAppVer() string {
|
||||
ver, err := d.getAppVersion()
|
||||
if err != nil {
|
||||
log.Warnf("[115] get app version failed: %v", err)
|
||||
return appVer
|
||||
}
|
||||
if len(ver) > 0 {
|
||||
return ver
|
||||
}
|
||||
return appVer
|
||||
}
|
||||
|
||||
func (d *Pan115) initAppVer() {
|
||||
appVer = d.getAppVer()
|
||||
log.Debugf("use app version: %v", appVer)
|
||||
}
|
||||
|
||||
type VersionResp struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
Data Versions `json:"data"`
|
||||
}
|
||||
|
||||
type Versions struct {
|
||||
Win Version `json:"win"`
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
Version string `json:"version_code"`
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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"
|
||||
@@ -68,8 +69,10 @@ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
return nil, err
|
||||
}
|
||||
userAgent := args.Header.Get("User-Agent")
|
||||
downloadInfo, err := d.
|
||||
DownloadWithUA(file.(*FileObj).PickCode, userAgent)
|
||||
if userAgent == "" {
|
||||
userAgent = base.UserAgent
|
||||
}
|
||||
downloadInfo, err := d.client.DownloadWithUA(file.(*FileObj).PickCode, userAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -245,4 +248,17 @@ 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)
|
||||
@@ -15,10 +15,9 @@ type Addition struct {
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "115 Cloud",
|
||||
DefaultRoot: "0",
|
||||
// OnlyProxy: true,
|
||||
// NoOverwriteUpload: true,
|
||||
Name: "115 Cloud",
|
||||
DefaultRoot: "0",
|
||||
LinkCacheMode: driver.LinkCacheUA,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -22,6 +22,10 @@ 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 {
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -20,13 +19,12 @@ import (
|
||||
"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"
|
||||
"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/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -108,60 +106,6 @@ 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))
|
||||
@@ -279,7 +223,7 @@ func (c *Pan115) UploadByOSS(ctx context.Context, params *driver115.UploadOSSPar
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ossClient, err := oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)
|
||||
ossClient, err := netutil.NewOSSClient(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -309,7 +253,8 @@ 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
|
||||
@@ -339,7 +284,7 @@ func (d *Pan115) UploadByMultipart(ctx context.Context, params *driver115.Upload
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
|
||||
if ossClient, err = netutil.NewOSSClient(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ package _115_open
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
stdpath "path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -12,6 +15,7 @@ import (
|
||||
"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"
|
||||
@@ -23,8 +27,9 @@ import (
|
||||
type Open115 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
client *sdk.Client
|
||||
limiter *rate.Limiter
|
||||
client *sdk.Client
|
||||
limiter *rate.Limiter
|
||||
parentPath string
|
||||
}
|
||||
|
||||
func (d *Open115) Config() driver.Config {
|
||||
@@ -53,6 +58,34 @@ 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
|
||||
}
|
||||
|
||||
@@ -69,7 +102,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(200)
|
||||
pageSize := int64(d.PageSize)
|
||||
offset := int64(0)
|
||||
for {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
@@ -131,12 +164,16 @@ func (d *Open115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Open115) GetObjInfo(ctx context.Context, path string) (model.Obj, error) {
|
||||
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{
|
||||
@@ -167,18 +204,15 @@ func (d *Open115) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Open115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
func (d *Open115) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
_, err := d.client.Move(ctx, &sdk.MoveReq{
|
||||
FileIDs: srcObj.GetID(),
|
||||
ToCid: dstDir.GetID(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return srcObj, nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Open115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
@@ -186,8 +220,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(),
|
||||
FileNma: newName,
|
||||
FileID: srcObj.GetID(),
|
||||
FileName: newName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -195,23 +229,21 @@ 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 srcObj, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *Open115) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
func (d *Open115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
_, err := d.client.Copy(ctx, &sdk.CopyReq{
|
||||
PID: dstDir.GetID(),
|
||||
FileID: srcObj.GetID(),
|
||||
NoDupli: "1",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return srcObj, nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Open115) Remove(ctx context.Context, obj model.Obj) error {
|
||||
@@ -337,6 +369,27 @@ func (d *Open115) OfflineList(ctx context.Context) (*sdk.OfflineTaskListResp, er
|
||||
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
|
||||
@@ -12,13 +12,15 @@ type Addition struct {
|
||||
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"`
|
||||
AccessToken string `json:"access_token" required:"true"`
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "115 Open",
|
||||
DefaultRoot: "0",
|
||||
Name: "115 Open",
|
||||
DefaultRoot: "0",
|
||||
LinkCacheMode: driver.LinkCacheUA,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -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"
|
||||
sdk "github.com/OpenListTeam/115-sdk-go"
|
||||
)
|
||||
|
||||
type Obj sdk.GetFilesResp_File
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
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/aliyun/aliyun-oss-go-sdk/oss"
|
||||
@@ -36,7 +37,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 := oss.New(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
|
||||
ossClient, err := netutil.NewOSSClient(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -70,7 +71,7 @@ func (d *Open115) singleUpload(ctx context.Context, tempF model.File, tokenResp
|
||||
// }
|
||||
|
||||
func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer, up driver.UpdateProgress, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {
|
||||
ossClient, err := oss.New(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
|
||||
ossClient, err := netutil.NewOSSClient(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -107,16 +108,16 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rateLimitedRd := driver.NewLimitedUploadStream(ctx, rd)
|
||||
err = retry.Do(func() error {
|
||||
rd.Seek(0, io.SeekStart)
|
||||
part, err := bucket.UploadPart(imur, rateLimitedRd, partSize, int(i))
|
||||
part, err := bucket.UploadPart(imur, driver.NewLimitedUploadStream(ctx, rd), 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))
|
||||
@@ -0,0 +1,15 @@
|
||||
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
|
||||
}
|
||||
@@ -2,7 +2,9 @@ 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"
|
||||
@@ -49,9 +51,16 @@ 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.GetShareSnap(d.ShareCode, d.ReceiveCode, dir.GetID(), driver115.QueryLimit(int(d.PageSize)))
|
||||
fileResp, err := d.client.GetShareSnapWithUA(ua, d.ShareCode, d.ReceiveCode, dir.GetID(), driver115.QueryLimit(int(d.PageSize)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -77,12 +86,23 @@ func (d *Pan115Share) Link(ctx context.Context, file model.Obj, args model.LinkA
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
downloadInfo, err := d.client.DownloadByShareCode(d.ShareCode, d.ReceiveCode, file.GetID())
|
||||
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())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Link{URL: downloadInfo.URL.URL}, nil
|
||||
header := http.Header{}
|
||||
header.Set("User-Agent", ua)
|
||||
return &model.Link{
|
||||
URL: downloadInfo.URL.URL,
|
||||
Header: header,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Pan115Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||||
@@ -20,6 +21,7 @@ type FileObj struct {
|
||||
FileName string
|
||||
isDir bool
|
||||
FileID string
|
||||
ThumbURL string
|
||||
}
|
||||
|
||||
func (f *FileObj) CreateTime() time.Time {
|
||||
@@ -54,6 +56,10 @@ 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 {
|
||||
@@ -74,15 +80,14 @@ 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(UserAgent),
|
||||
driver115.UA(base.UserAgentNT),
|
||||
}
|
||||
d.client = driver115.New(opts...)
|
||||
if _, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, ""); err != nil {
|
||||
@@ -41,7 +41,9 @@ func (d *Pan123) GetAddition() driver.Additional {
|
||||
}
|
||||
|
||||
func (d *Pan123) Init(ctx context.Context) error {
|
||||
_, err := d.Request(UserInfo, http.MethodGet, nil, nil)
|
||||
_, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {
|
||||
req.SetHeader("platform", "web")
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -74,7 +76,6 @@ 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)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
@@ -97,7 +98,7 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
||||
}
|
||||
|
||||
log.Debug("download url: ", u_)
|
||||
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
|
||||
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://yun.123pan.com/").Get(u_)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -254,4 +255,17 @@ 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)
|
||||
@@ -12,13 +12,15 @@ type Addition struct {
|
||||
//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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "123Pan",
|
||||
DefaultRoot: "0",
|
||||
LocalSort: true,
|
||||
PreferProxy: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -27,6 +29,7 @@ func init() {
|
||||
return &Pan123{
|
||||
Addition: Addition{
|
||||
UploadThread: 3,
|
||||
Platform: "web",
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -28,7 +28,7 @@ func (f File) CreateTime() time.Time {
|
||||
}
|
||||
|
||||
func (f File) GetHash() utils.HashInfo {
|
||||
return utils.HashInfo{}
|
||||
return utils.NewHashInfo(utils.MD5, f.Etag)
|
||||
}
|
||||
|
||||
func (f File) GetPath() string {
|
||||
@@ -122,3 +122,59 @@ 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"`
|
||||
}
|
||||
@@ -124,28 +124,19 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
|
||||
if cur == chunkCount {
|
||||
curSize = lastChunkSize
|
||||
}
|
||||
var reader *stream.SectionReader
|
||||
var rateLimitedRd io.Reader
|
||||
var reader io.ReadSeeker
|
||||
threadG.GoWithLifecycle(errgroup.Lifecycle{
|
||||
Before: func(ctx context.Context) error {
|
||||
if reader == nil {
|
||||
var err error
|
||||
reader, err = ss.GetSectionReader(offset, curSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rateLimitedRd = driver.NewLimitedUploadStream(ctx, reader)
|
||||
}
|
||||
return nil
|
||||
Before: func(ctx context.Context) (err error) {
|
||||
reader, err = ss.GetSectionReader(offset, curSize)
|
||||
return
|
||||
},
|
||||
Do: func(ctx context.Context) error {
|
||||
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)
|
||||
}
|
||||
reader.Seek(0, io.SeekStart)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, rateLimitedRd)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, reader))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -157,7 +148,7 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode == http.StatusForbidden {
|
||||
singleflight.AnyGroup.Do(fmt.Sprintf("Pan123.newUpload_%p", threadG), func() (any, error) {
|
||||
_, 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
|
||||
@@ -177,7 +168,7 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
|
||||
}
|
||||
return fmt.Errorf("upload s3 chunk %d failed, status code: %d, body: %s", cur, res.StatusCode, body)
|
||||
}
|
||||
progress := 10.0 + 85.0*float64(threadG.Success())/float64(chunkCount)
|
||||
progress := 100 * float64(threadG.Success()+1) / float64(chunkCount+1)
|
||||
up(progress)
|
||||
return nil
|
||||
},
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
@@ -23,9 +24,9 @@ import (
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
const (
|
||||
Api = "https://www.123pan.com/api"
|
||||
AApi = "https://www.123pan.com/a/api"
|
||||
BApi = "https://www.123pan.com/b/api"
|
||||
Api = "https://yun.123pan.com/api"
|
||||
AApi = "https://yun.123pan.com/a/api"
|
||||
BApi = "https://yun.123pan.com/b/api"
|
||||
LoginApi = "https://login.123pan.com/api"
|
||||
MainApi = BApi
|
||||
SignIn = LoginApi + "/user/sign_in"
|
||||
@@ -43,9 +44,16 @@ const (
|
||||
S3Auth = MainApi + "/file/s3_upload_object/auth"
|
||||
UploadCompleteV2 = MainApi + "/file/upload_complete/v2"
|
||||
S3Complete = MainApi + "/file/s3_complete_multipart_upload"
|
||||
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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()))
|
||||
@@ -161,8 +169,8 @@ func (d *Pan123) login() error {
|
||||
}
|
||||
res, err := base.RestyClient.R().
|
||||
SetHeaders(map[string]string{
|
||||
"origin": "https://www.123pan.com",
|
||||
"referer": "https://www.123pan.com/",
|
||||
"origin": "https://yun.123pan.com",
|
||||
"referer": "https://yun.123pan.com/",
|
||||
"user-agent": "Dart/2.19(dart:io)-openlist",
|
||||
"platform": "web",
|
||||
"app-version": "3",
|
||||
@@ -199,11 +207,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://www.123pan.com",
|
||||
"referer": "https://www.123pan.com/",
|
||||
"origin": "https://yun.123pan.com",
|
||||
"referer": "https://yun.123pan.com/",
|
||||
"authorization": "Bearer " + d.AccessToken,
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",
|
||||
"platform": "web",
|
||||
"platform": d.Platform,
|
||||
"app-version": "3",
|
||||
//"user-agent": base.UserAgent,
|
||||
})
|
||||
@@ -238,6 +246,115 @@ 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
|
||||
@@ -282,3 +399,14 @@ 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
|
||||
}
|
||||
@@ -39,6 +39,10 @@ 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)
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
type Open123 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
UID uint64
|
||||
tm *tokenManager
|
||||
}
|
||||
|
||||
func (d *Open123) Config() driver.Config {
|
||||
@@ -32,6 +34,24 @@ 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
|
||||
}
|
||||
|
||||
@@ -69,13 +89,45 @@ func (d *Open123) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
func (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
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
|
||||
}
|
||||
|
||||
link := model.Link{URL: res.Data.DownloadUrl}
|
||||
return &link, nil
|
||||
return &model.Link{URL: res.Data.DownloadUrl}, nil
|
||||
}
|
||||
|
||||
func (d *Open123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
@@ -129,6 +181,22 @@ func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
|
||||
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 {
|
||||
@@ -181,5 +249,28 @@ func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
|
||||
return nil, fmt.Errorf("upload complete timeout")
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Open123)(nil)
|
||||
var _ driver.PutResult = (*Open123)(nil)
|
||||
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)
|
||||
)
|
||||
@@ -6,16 +6,20 @@ import (
|
||||
)
|
||||
|
||||
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"`
|
||||
@@ -23,6 +27,11 @@ 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
|
||||
}
|
||||
|
||||
@@ -30,6 +39,7 @@ var config = driver.Config{
|
||||
Name: "123 Open",
|
||||
DefaultRoot: "0",
|
||||
LocalSort: true,
|
||||
PreferProxy: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -0,0 +1,129 @@
|
||||
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")
|
||||
}
|
||||
@@ -19,6 +19,7 @@ func (a *ApiInfo) Require() {
|
||||
a.token <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ApiInfo) Release() {
|
||||
if a.qps > 0 {
|
||||
time.AfterFunc(time.Second, func() {
|
||||
@@ -26,13 +27,16 @@ 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,
|
||||
@@ -54,9 +58,13 @@ 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)
|
||||
}
|
||||
|
||||
@@ -117,29 +125,32 @@ type AccessTokenResp struct {
|
||||
}
|
||||
|
||||
type RefreshTokenResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
TokenType string `json:"token_type"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type UserInfoResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
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"`
|
||||
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"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
@@ -158,6 +169,13 @@ type DownloadInfoResp struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type DirectLinkResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// 创建文件V2返回
|
||||
type UploadCreateResp struct {
|
||||
BaseResp
|
||||
@@ -178,3 +196,26 @@ type UploadCompleteResp struct {
|
||||
FileID int64 `json:"fileID"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type SHA1ReuseResp 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"`
|
||||
} `json:"data"`
|
||||
}
|
||||
@@ -67,34 +67,29 @@ func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createRes
|
||||
partNumber := partIndex + 1 // 分片号从1开始
|
||||
offset := partIndex * chunkSize
|
||||
size := min(chunkSize, size-offset)
|
||||
var reader *stream.SectionReader
|
||||
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) error {
|
||||
if reader == nil {
|
||||
var err error
|
||||
// 每个分片一个reader
|
||||
reader, err = ss.GetSectionReader(offset, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 计算当前分片的MD5
|
||||
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
|
||||
}
|
||||
rateLimitedRd = driver.NewLimitedUploadStream(ctx, reader)
|
||||
reader.Seek(0, io.SeekStart)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Do: func(ctx context.Context) error {
|
||||
// 重置分片reader位置,因为HashReader、上一次失败已经读取到分片EOF
|
||||
reader.Seek(0, io.SeekStart)
|
||||
|
||||
// 创建表单数据
|
||||
var b bytes.Buffer
|
||||
w := multipart.NewWriter(&b)
|
||||
b.Reset()
|
||||
w := multipart.NewWriter(b)
|
||||
// 添加表单字段
|
||||
err = w.WriteField("preuploadID", createResp.Data.PreuploadID)
|
||||
if err != nil {
|
||||
@@ -109,27 +104,30 @@ func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createRes
|
||||
return err
|
||||
}
|
||||
// 写入文件内容
|
||||
fw, err := w.CreateFormFile("slice", fmt.Sprintf("%s.part%d", file.GetName(), partNumber))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = utils.CopyWithBuffer(fw, rateLimitedRd)
|
||||
_, 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", &b)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadDomain+"/upload/v2/file/slice", rateLimitedRd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Add("Authorization", "Bearer "+d.AccessToken)
|
||||
req.Header.Add("Authorization", "Bearer "+token)
|
||||
req.Header.Add("Content-Type", w.FormDataContentType())
|
||||
req.Header.Add("Platform", "open_platform")
|
||||
|
||||
@@ -141,12 +139,13 @@ func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createRes
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("slice %d upload failed, status code: %d", partNumber, res.StatusCode)
|
||||
}
|
||||
var resp BaseResp
|
||||
respBody, err := io.ReadAll(res.Body)
|
||||
b.Reset()
|
||||
_, err = b.ReadFrom(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(respBody, &resp)
|
||||
var resp BaseResp
|
||||
err = json.Unmarshal(b.Bytes(), &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -154,7 +153,7 @@ func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createRes
|
||||
return fmt.Errorf("slice %d upload failed: %s", partNumber, resp.Message)
|
||||
}
|
||||
|
||||
progress := 10.0 + 85.0*float64(threadG.Success())/float64(uploadNums)
|
||||
progress := 100 * float64(threadG.Success()+1) / float64(uploadNums+1)
|
||||
up(progress)
|
||||
return nil
|
||||
},
|
||||
@@ -184,3 +183,21 @@ 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) {
|
||||
req.SetBody(base.Json{
|
||||
"parentFileID": parentFileID,
|
||||
"filename": filename,
|
||||
"sha1": strings.ToLower(sha1Hash),
|
||||
"size": size,
|
||||
"duplicate": duplicate,
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
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/go-resty/resty/v2"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
for {
|
||||
token, err := d.getAccessToken(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
"authorization": "Bearer " + token,
|
||||
"platform": "open_platform",
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
|
||||
log.Debugf("API: %s, QPS: %d, NowLen: %d", apiInfo.url, apiInfo.qps, apiInfo.NowLen())
|
||||
|
||||
apiInfo.Require()
|
||||
defer apiInfo.Release()
|
||||
res, err := req.Execute(method, apiInfo.url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := res.Body()
|
||||
|
||||
// 解析为通用响应
|
||||
var baseResp BaseResp
|
||||
if err = json.Unmarshal(body, &baseResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if baseResp.Code == 0 {
|
||||
return body, nil
|
||||
} else if baseResp.Code == 401 {
|
||||
// 强制刷新Token, 有小概率会 race condition 导致多次刷新Token,但不影响正确运行
|
||||
if _, err := d.getAccessToken(true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if baseResp.Code == 429 {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
log.Warningf("API: %s, QPS: %d, 请求太频繁,对应API提示过多请减小QPS", apiInfo.url, apiInfo.qps)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
// 待签名字符串,格式:path-timestamp-rand-uid-privateKey
|
||||
unsignedStr := fmt.Sprintf("%s-%d-%s-%d-%s", objURL.Path, ts, rand, uid, privateKey)
|
||||
md5Hash := md5.Sum([]byte(unsignedStr))
|
||||
// 生成鉴权参数,格式:timestamp-rand-uid-md5hash
|
||||
authKey := fmt.Sprintf("%d-%s-%d-%x", ts, rand, uid, md5Hash)
|
||||
|
||||
// 添加鉴权参数到URL查询参数
|
||||
v := objURL.Query()
|
||||
v.Add("auth_key", authKey)
|
||||
objURL.RawQuery = v.Encode()
|
||||
|
||||
return objURL.String(), nil
|
||||
}
|
||||
|
||||
func (d *Open123) getUserInfo(ctx context.Context) (*UserInfoResp, error) {
|
||||
var resp UserInfoResp
|
||||
|
||||
if _, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {
|
||||
req.SetContext(ctx)
|
||||
}, &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
|
||||
|
||||
_, err := d.Request(FileList, http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(
|
||||
map[string]string{
|
||||
"parentFileId": strconv.FormatInt(parentFileId, 10),
|
||||
"limit": strconv.Itoa(limit),
|
||||
"lastFileId": strconv.FormatInt(lastFileId, 10),
|
||||
"trashed": "false",
|
||||
"searchMode": "",
|
||||
"searchData": "",
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (d *Open123) getDownloadInfo(fileId int64) (*DownloadInfoResp, error) {
|
||||
var resp DownloadInfoResp
|
||||
|
||||
_, err := d.Request(DownloadInfo, 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) 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{
|
||||
"parentID": strconv.FormatInt(parentID, 10),
|
||||
"name": name,
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) move(fileID, toParentFileID int64) error {
|
||||
_, err := d.Request(Move, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"fileIDs": []int64{fileID},
|
||||
"toParentFileID": toParentFileID,
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) rename(fileId int64, fileName string) error {
|
||||
_, err := d.Request(Rename, http.MethodPut, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"fileId": fileId,
|
||||
"fileName": fileName,
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) trash(fileId int64) error {
|
||||
_, err := d.Request(Trash, http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"fileIDs": []int64{fileId},
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func (d *Pan123Share) Link(ctx context.Context, file model.Obj, args model.LinkA
|
||||
}
|
||||
|
||||
log.Debug("download url: ", u_)
|
||||
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
|
||||
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://yun.123pan.com/").Get(u_)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -19,6 +19,7 @@ var config = driver.Config{
|
||||
LocalSort: true,
|
||||
NoUpload: true,
|
||||
DefaultRoot: "0",
|
||||
PreferProxy: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -24,7 +24,7 @@ type File struct {
|
||||
}
|
||||
|
||||
func (f File) GetHash() utils.HashInfo {
|
||||
return utils.HashInfo{}
|
||||
return utils.NewHashInfo(utils.MD5, f.Etag)
|
||||
}
|
||||
|
||||
func (f File) GetPath() string {
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Api = "https://www.123pan.com/api"
|
||||
AApi = "https://www.123pan.com/a/api"
|
||||
BApi = "https://www.123pan.com/b/api"
|
||||
Api = "https://yun.123pan.com/api"
|
||||
AApi = "https://yun.123pan.com/a/api"
|
||||
BApi = "https://yun.123pan.com/b/api"
|
||||
MainApi = BApi
|
||||
FileList = MainApi + "/share/get"
|
||||
DownloadInfo = MainApi + "/share/download/info"
|
||||
@@ -58,8 +58,8 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
|
||||
}
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeaders(map[string]string{
|
||||
"origin": "https://www.123pan.com",
|
||||
"referer": "https://www.123pan.com/",
|
||||
"origin": "https://yun.123pan.com",
|
||||
"referer": "https://yun.123pan.com/",
|
||||
"authorization": "Bearer " + d.AccessToken,
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",
|
||||
"platform": "web",
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"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"
|
||||
@@ -28,6 +29,7 @@ type Yun139 struct {
|
||||
Account string
|
||||
ref *Yun139
|
||||
PersonalCloudHost string
|
||||
RootPath string
|
||||
}
|
||||
|
||||
func (d *Yun139) Config() driver.Config {
|
||||
@@ -41,7 +43,16 @@ func (d *Yun139) GetAddition() driver.Additional {
|
||||
func (d *Yun139) Init(ctx context.Context) error {
|
||||
if d.ref == nil {
|
||||
if len(d.Authorization) == 0 {
|
||||
return fmt.Errorf("authorization is empty")
|
||||
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")
|
||||
}
|
||||
}
|
||||
err := d.refreshToken()
|
||||
if err != nil {
|
||||
@@ -54,7 +65,8 @@ 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 {
|
||||
@@ -91,7 +103,22 @@ 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
|
||||
}
|
||||
@@ -278,6 +305,42 @@ 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
|
||||
}
|
||||
@@ -352,19 +415,27 @@ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) e
|
||||
var data base.Json
|
||||
var pathname string
|
||||
if srcObj.IsDir() {
|
||||
// 网页接口不支持重命名家庭云文件夹
|
||||
// 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
|
||||
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
|
||||
} else {
|
||||
data = base.Json{
|
||||
"contentID": srcObj.GetID(),
|
||||
@@ -420,6 +491,33 @@ 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
|
||||
}
|
||||
@@ -534,16 +632,15 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
if size > partSize {
|
||||
part = (size + partSize - 1) / partSize
|
||||
}
|
||||
|
||||
// 生成所有 partInfos
|
||||
partInfos := make([]PartInfo, 0, part)
|
||||
for i := int64(0); i < part; i++ {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
start := i * partSize
|
||||
byteSize := size - start
|
||||
if byteSize > partSize {
|
||||
byteSize = partSize
|
||||
}
|
||||
byteSize := min(size-start, partSize)
|
||||
partNumber := i + 1
|
||||
partInfo := PartInfo{
|
||||
PartNumber: partNumber,
|
||||
@@ -591,17 +688,20 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
// resp.Data.RapidUpload: true 支持快传,但此处直接检测是否返回分片的上传地址
|
||||
// 快传的情况下同样需要手动处理冲突
|
||||
if resp.Data.PartInfos != nil {
|
||||
// 读取前100个分片的上传地址
|
||||
uploadPartInfos := resp.Data.PartInfos
|
||||
// Progress
|
||||
p := driver.NewProgress(size, up)
|
||||
rateLimited := driver.NewLimitedUploadStream(ctx, stream)
|
||||
|
||||
// 获取后续分片的上传地址
|
||||
for i := 101; i < len(partInfos); i += 100 {
|
||||
end := i + 100
|
||||
if end > len(partInfos) {
|
||||
end = len(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))
|
||||
batchPartInfos := partInfos[i:end]
|
||||
|
||||
moredata := base.Json{
|
||||
"fileId": resp.Data.FileId,
|
||||
"uploadId": resp.Data.UploadId,
|
||||
@@ -617,44 +717,13 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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.NewRequestWithContext(ctx, http.MethodPut, uploadPartInfo.UploadUrl, r)
|
||||
err = d.uploadPersonalParts(ctx, partInfos, moreresp.Data.PartInfos, rateLimited, p)
|
||||
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
|
||||
|
||||
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",
|
||||
@@ -708,6 +777,8 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
return nil
|
||||
case MetaPersonal:
|
||||
fallthrough
|
||||
case MetaGroup:
|
||||
fallthrough
|
||||
case MetaFamily:
|
||||
// 处理冲突
|
||||
// 获取文件列表
|
||||
@@ -755,13 +826,18 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
||||
},
|
||||
}
|
||||
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
|
||||
if d.isFamily() {
|
||||
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
|
||||
}
|
||||
data = d.newJson(base.Json{
|
||||
"fileCount": 1,
|
||||
"manualRename": 2,
|
||||
"operation": 0,
|
||||
"path": path.Join(dstDir.GetPath(), dstDir.GetID()),
|
||||
"seqNo": random.String(32), //序列号不能为空
|
||||
"path": uploadPath,
|
||||
"seqNo": random.String(32), // 序列号不能为空
|
||||
"totalSize": reportSize,
|
||||
"uploadContentList": []base.Json{{
|
||||
"contentName": stream.GetName(),
|
||||
@@ -772,6 +848,7 @@ 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
|
||||
@@ -863,4 +940,24 @@ 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)
|
||||
@@ -8,9 +8,13 @@ import (
|
||||
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"`
|
||||
@@ -312,3 +312,75 @@ 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"`
|
||||
}
|
||||
+1377
File diff suppressed because it is too large
Load Diff
@@ -194,4 +194,17 @@ 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)
|
||||
@@ -0,0 +1,149 @@
|
||||
package _189
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/torrent"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
)
|
||||
|
||||
// GenerateTorrent 根据上传过程中收集的哈希信息生成包含 CAS 扩展的 torrent 文件
|
||||
func GenerateTorrent(fileName string, fileSize int64, fileMD5 string, sliceMD5s []string, sliceSize int64, pieceHashes []byte) ([]byte, error) {
|
||||
// 计算 sliceMD5
|
||||
sliceMD5 := fileMD5
|
||||
if len(sliceMD5s) > 1 {
|
||||
joined := strings.Join(sliceMD5s, "\n")
|
||||
sliceMD5 = strings.ToUpper(torrent.GetMD5Str(joined))
|
||||
}
|
||||
|
||||
t := torrent.NewTorrent(fileName, fileSize, fileMD5)
|
||||
t.Info.PieceLength = sliceSize
|
||||
t.SetPieces(pieceHashes)
|
||||
t.SetCASInfo(&torrent.CASInfo{
|
||||
FileMD5: fileMD5,
|
||||
SliceMD5: sliceMD5,
|
||||
SliceMD5s: sliceMD5s,
|
||||
SliceSize: sliceSize,
|
||||
Cloud: "189",
|
||||
})
|
||||
|
||||
return t.Encode()
|
||||
}
|
||||
|
||||
// RapidUploadFromTorrent 从 torrent 文件中提取 CAS 信息进行秒传
|
||||
func (d *Cloud189) RapidUploadFromTorrent(ctx context.Context, dstDir model.Obj, torrentData []byte) error {
|
||||
// 解析 torrent
|
||||
t, err := torrent.Decode(torrentData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析 torrent 失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查是否包含 CAS 扩展信息
|
||||
if !t.HasCASInfo() {
|
||||
return fmt.Errorf("torrent 不包含 CAS 扩展信息,无法秒传")
|
||||
}
|
||||
|
||||
cas := t.CAS
|
||||
fileName := t.Info.Name
|
||||
fileSize := t.GetTotalSize()
|
||||
|
||||
// 获取 sessionKey
|
||||
sessionKey, err := d.getSessionKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.sessionKey = sessionKey
|
||||
|
||||
// 初始化上传
|
||||
res, err := d.uploadRequest("/person/initMultiUpload", map[string]string{
|
||||
"parentFolderId": dstDir.GetID(),
|
||||
"fileName": encode(fileName),
|
||||
"fileSize": fmt.Sprint(fileSize),
|
||||
"sliceSize": fmt.Sprint(cas.SliceSize),
|
||||
"lazyCheck": "1",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化上传失败: %w", err)
|
||||
}
|
||||
|
||||
uploadFileId := utils.Json.Get(res, "data", "uploadFileId").ToString()
|
||||
|
||||
// 提交上传(使用 CAS 信息秒传)
|
||||
_, err = d.uploadRequest("/person/commitMultiUploadFile", map[string]string{
|
||||
"uploadFileId": uploadFileId,
|
||||
"fileMd5": cas.FileMD5,
|
||||
"sliceMd5": cas.SliceMD5,
|
||||
"lazyCheck": "1",
|
||||
"opertype": "3",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("秒传提交失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComputeTorrentFromReader 从 io.Reader 计算并生成 torrent 文件
|
||||
func ComputeTorrentFromReader(reader io.Reader, fileName string, fileSize int64, sliceSize int64) ([]byte, error) {
|
||||
if sliceSize <= 0 {
|
||||
sliceSize = torrent.DefaultPieceSize
|
||||
}
|
||||
|
||||
hw := torrent.NewHashWriter(sliceSize, sliceSize)
|
||||
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if n > 0 {
|
||||
hw.Write(buf[:n])
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
hw.Finish()
|
||||
|
||||
fileMD5 := hw.GetFileMD5()
|
||||
sliceMD5s := hw.GetSliceMD5s()
|
||||
pieceHashes := hw.GetPieceHashes()
|
||||
|
||||
return GenerateTorrent(fileName, fileSize, fileMD5, sliceMD5s, sliceSize, pieceHashes)
|
||||
}
|
||||
|
||||
// ComputePieceSHA1 计算单个分片的 SHA-1 哈希
|
||||
func ComputePieceSHA1(data []byte) []byte {
|
||||
h := sha1.Sum(data)
|
||||
return h[:]
|
||||
}
|
||||
|
||||
// ExtractCASFromTorrent 从 torrent 数据中提取 CAS 信息
|
||||
func ExtractCASFromTorrent(torrentData []byte) (*torrent.CASInfo, string, int64, error) {
|
||||
t, err := torrent.Decode(torrentData)
|
||||
if err != nil {
|
||||
return nil, "", 0, fmt.Errorf("解析 torrent 失败: %w", err)
|
||||
}
|
||||
|
||||
if !t.HasCASInfo() {
|
||||
return nil, "", 0, fmt.Errorf("torrent 不包含 CAS 扩展信息")
|
||||
}
|
||||
|
||||
return t.CAS, t.Info.Name, t.GetTotalSize(), nil
|
||||
}
|
||||
|
||||
// GetInfoHashHex 获取 torrent 的 info_hash(十六进制字符串)
|
||||
func GetInfoHashHex(torrentData []byte) (string, error) {
|
||||
t, err := torrent.Decode(torrentData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(t.InfoHash), nil
|
||||
}
|
||||
@@ -66,3 +66,21 @@ type DownResp struct {
|
||||
ResMessage string `json:"res_message"`
|
||||
FileDownloadUrl string `json:"downloadUrl"`
|
||||
}
|
||||
|
||||
type CapacityResp struct {
|
||||
ResCode int `json:"res_code"`
|
||||
ResMessage string `json:"res_message"`
|
||||
Account string `json:"account"`
|
||||
CloudCapacityInfo struct {
|
||||
FreeSize int64 `json:"freeSize"`
|
||||
MailUsedSize int64 `json:"mail189UsedSize"`
|
||||
TotalSize int64 `json:"totalSize"`
|
||||
UsedSize int64 `json:"usedSize"`
|
||||
} `json:"cloudCapacityInfo"`
|
||||
FamilyCapacityInfo struct {
|
||||
FreeSize int64 `json:"freeSize"`
|
||||
TotalSize int64 `json:"totalSize"`
|
||||
UsedSize int64 `json:"usedSize"`
|
||||
} `json:"familyCapacityInfo"`
|
||||
TotalSize uint64 `json:"totalSize"`
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
sha1Pkg "crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -18,6 +19,8 @@ import (
|
||||
"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/internal/stream"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
myrand "github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
|
||||
"github.com/go-resty/resty/v2"
|
||||
@@ -107,7 +110,7 @@ import (
|
||||
// res, err = d.client.R().
|
||||
// SetHeaders(map[string]string{
|
||||
// "lt": lt,
|
||||
// "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
|
||||
// "User-Agent": base.UserAgentNT,
|
||||
// "Referer": "https://open.e.189.cn/",
|
||||
// "accept": "application/json;charset=UTF-8",
|
||||
// }).SetFormData(map[string]string{
|
||||
@@ -157,7 +160,7 @@ func (d *Cloud189) request(url string, method string, callback base.ReqCallback,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//log.Debug(res.String())
|
||||
// log.Debug(res.String())
|
||||
if e.ErrorCode != "" {
|
||||
if e.ErrorCode == "InvalidSessionKey" {
|
||||
err = d.newLogin()
|
||||
@@ -186,8 +189,8 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
|
||||
"mediaType": "0",
|
||||
"folderId": fileId,
|
||||
"iconOption": "5",
|
||||
"orderBy": "lastOpTime", //account.OrderBy
|
||||
"descending": "true", //account.OrderDirection
|
||||
"orderBy": "lastOpTime", // account.OrderBy
|
||||
"descending": "true", // account.OrderDirection
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
@@ -311,48 +314,107 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
|
||||
}
|
||||
d.sessionKey = sessionKey
|
||||
const DEFAULT int64 = 10485760
|
||||
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
|
||||
fileSize := file.GetSize()
|
||||
count := int64(math.Ceil(float64(fileSize) / float64(DEFAULT)))
|
||||
|
||||
res, err := d.uploadRequest("/person/initMultiUpload", map[string]string{
|
||||
// 先计算文件完整MD5和分片MD5,用于秒传判断
|
||||
fileMd5Hex := file.GetHash().GetHash(utils.MD5)
|
||||
sliceMd5Hex := ""
|
||||
md5s := make([]string, 0)
|
||||
|
||||
if len(fileMd5Hex) < utils.MD5.Width {
|
||||
// 没有MD5,先缓存流并同时计算文件MD5和分片MD5
|
||||
fileMd5Hash := md5.New()
|
||||
sliceMd5Hash := md5.New()
|
||||
var finish int64
|
||||
cache, err := file.CacheFullAndWriter(nil, io.MultiWriter(fileMd5Hash, &sliceHashWriter{
|
||||
hash: sliceMd5Hash,
|
||||
md5s: &md5s,
|
||||
sliceSize: DEFAULT,
|
||||
finish: &finish,
|
||||
fileSize: fileSize,
|
||||
up: up,
|
||||
ctx: ctx,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 处理最后一个分片的MD5
|
||||
if finish%DEFAULT != 0 || finish == 0 {
|
||||
md5s = append(md5s, strings.ToUpper(hex.EncodeToString(sliceMd5Hash.Sum(nil))))
|
||||
}
|
||||
fileMd5Hex = hex.EncodeToString(fileMd5Hash.Sum(nil))
|
||||
|
||||
// seek回起始位置,供后续上传使用
|
||||
if _, err := cache.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 计算sliceMd5
|
||||
if fileSize > DEFAULT && len(md5s) > 0 {
|
||||
sliceMd5Hex = utils.GetMD5EncodeStr(strings.Join(md5s, "\n"))
|
||||
} else {
|
||||
sliceMd5Hex = fileMd5Hex
|
||||
}
|
||||
|
||||
// 带fileMd5调用initMultiUpload,支持秒传
|
||||
initParams := map[string]string{
|
||||
"parentFolderId": dstDir.GetID(),
|
||||
"fileName": encode(file.GetName()),
|
||||
"fileSize": strconv.FormatInt(file.GetSize(), 10),
|
||||
"fileSize": strconv.FormatInt(fileSize, 10),
|
||||
"sliceSize": strconv.FormatInt(DEFAULT, 10),
|
||||
"lazyCheck": "1",
|
||||
}, nil)
|
||||
"fileMd5": fileMd5Hex,
|
||||
"sliceMd5": sliceMd5Hex,
|
||||
}
|
||||
|
||||
res, err := d.uploadRequest("/person/initMultiUpload", initParams, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadFileId := jsoniter.Get(res, "data", "uploadFileId").ToString()
|
||||
//_, err = d.uploadRequest("/person/getUploadedPartsInfo", map[string]string{
|
||||
// "uploadFileId": uploadFileId,
|
||||
//}, nil)
|
||||
fileDataExists := jsoniter.Get(res, "data", "fileDataExists").ToInt()
|
||||
|
||||
// 秒传成功,直接提交
|
||||
if fileDataExists == 1 {
|
||||
_, err = d.uploadRequest("/person/commitMultiUploadFile", map[string]string{
|
||||
"uploadFileId": uploadFileId,
|
||||
"fileMd5": fileMd5Hex,
|
||||
"sliceMd5": sliceMd5Hex,
|
||||
"lazyCheck": "1",
|
||||
"opertype": "3",
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// 非秒传,需要上传分片
|
||||
var finish int64 = 0
|
||||
var i int64
|
||||
var byteSize int64
|
||||
md5s := make([]string, 0)
|
||||
md5Sum := md5.New()
|
||||
|
||||
// 额外计算 SHA-1 piece hash 用于生成 torrent
|
||||
pieceSHA1Hashes := make([]byte, 0, int(count)*20)
|
||||
|
||||
for i = 1; i <= count; i++ {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
byteSize = file.GetSize() - finish
|
||||
byteSize = fileSize - finish
|
||||
if DEFAULT < byteSize {
|
||||
byteSize = DEFAULT
|
||||
}
|
||||
//log.Debugf("%d,%d", byteSize, finish)
|
||||
byteData := make([]byte, byteSize)
|
||||
n, err := io.ReadFull(file, byteData)
|
||||
//log.Debug(err, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
finish += int64(n)
|
||||
md5Bytes := getMd5(byteData)
|
||||
md5Hex := hex.EncodeToString(md5Bytes)
|
||||
md5Base64 := base64.StdEncoding.EncodeToString(md5Bytes)
|
||||
md5s = append(md5s, strings.ToUpper(md5Hex))
|
||||
md5Sum.Write(byteData)
|
||||
|
||||
// 计算 SHA-1 piece hash
|
||||
sha1Hash := sha1Pkg.Sum(byteData)
|
||||
pieceSHA1Hashes = append(pieceSHA1Hashes, sha1Hash[:]...)
|
||||
var resp UploadUrlsResp
|
||||
res, err = d.uploadRequest("/person/getMultiUploadUrls", map[string]string{
|
||||
"partInfo": fmt.Sprintf("%s-%s", strconv.FormatInt(i, 10), md5Base64),
|
||||
@@ -379,19 +441,116 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
|
||||
}
|
||||
log.Debugf("%+v %+v", r, r.Request.Header)
|
||||
_ = r.Body.Close()
|
||||
up(float64(i) * 100 / float64(count))
|
||||
}
|
||||
fileMd5 := hex.EncodeToString(md5Sum.Sum(nil))
|
||||
sliceMd5 := fileMd5
|
||||
if file.GetSize() > DEFAULT {
|
||||
sliceMd5 = utils.GetMD5EncodeStr(strings.Join(md5s, "\n"))
|
||||
up(50 + float64(i)*50/float64(count))
|
||||
}
|
||||
res, err = d.uploadRequest("/person/commitMultiUploadFile", map[string]string{
|
||||
"uploadFileId": uploadFileId,
|
||||
"fileMd5": fileMd5,
|
||||
"sliceMd5": sliceMd5,
|
||||
"fileMd5": fileMd5Hex,
|
||||
"sliceMd5": sliceMd5Hex,
|
||||
"lazyCheck": "1",
|
||||
"opertype": "3",
|
||||
}, nil)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成 torrent 文件(异步,不影响上传结果)
|
||||
capturedDstDir := dstDir
|
||||
capturedFileName := file.GetName()
|
||||
capturedFileSize := fileSize
|
||||
capturedFileMd5Hex := fileMd5Hex
|
||||
capturedMd5s := md5s
|
||||
go func() {
|
||||
fileMD5Upper := strings.ToUpper(capturedFileMd5Hex)
|
||||
torrentData, err := GenerateTorrent(capturedFileName, capturedFileSize, fileMD5Upper, capturedMd5s, DEFAULT, pieceSHA1Hashes)
|
||||
if err != nil {
|
||||
log.Warnf("生成 torrent 失败: %v", err)
|
||||
return
|
||||
}
|
||||
infoHash, _ := GetInfoHashHex(torrentData)
|
||||
torrentName := capturedFileName + ".cas.torrent"
|
||||
log.Infof("已生成 torrent: %s (info_hash: %s, size: %d bytes)",
|
||||
torrentName, infoHash, len(torrentData))
|
||||
|
||||
// 将 torrent 文件上传到同一目录
|
||||
torrentFileStream := &stream.FileStream{
|
||||
Ctx: context.Background(),
|
||||
Obj: &model.Object{
|
||||
Name: torrentName,
|
||||
Size: int64(len(torrentData)),
|
||||
IsFolder: false,
|
||||
},
|
||||
Reader: bytes.NewReader(torrentData),
|
||||
Mimetype: "application/x-bittorrent",
|
||||
}
|
||||
uploadErr := d.oldUpload(capturedDstDir, torrentFileStream)
|
||||
if uploadErr != nil {
|
||||
log.Warnf("上传 torrent 文件失败: %v", uploadErr)
|
||||
} else {
|
||||
log.Infof("torrent 文件已上传: %s", torrentName)
|
||||
op.Cache.DeleteDirectory(d, capturedDstDir.GetPath())
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Cloud189) getCapacityInfo(ctx context.Context) (*CapacityResp, error) {
|
||||
var resp CapacityResp
|
||||
_, err := d.request("https://cloud.189.cn/api/portal/getUserSizeInfo.action", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetContext(ctx)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// sliceHashWriter 在写入过程中按分片大小自动切分并计算每个分片的MD5,
|
||||
// 同时支持进度回调和取消检查。
|
||||
type sliceHashWriter struct {
|
||||
hash io.Writer // 当前分片的MD5 hash
|
||||
md5s *[]string // 收集每个分片的MD5十六进制字符串
|
||||
sliceSize int64 // 分片大小
|
||||
finish *int64 // 已写入的总字节数
|
||||
fileSize int64 // 文件总大小
|
||||
up driver.UpdateProgress
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (w *sliceHashWriter) Write(p []byte) (int, error) {
|
||||
if utils.IsCanceled(w.ctx) {
|
||||
return 0, w.ctx.Err()
|
||||
}
|
||||
total := len(p)
|
||||
written := 0
|
||||
for written < total {
|
||||
// 当前分片还能写入的字节数
|
||||
sliceRemain := w.sliceSize - (*w.finish % w.sliceSize)
|
||||
toWrite := int64(total - written)
|
||||
if toWrite > sliceRemain {
|
||||
toWrite = sliceRemain
|
||||
}
|
||||
n, err := w.hash.Write(p[written : written+int(toWrite)])
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
written += n
|
||||
*w.finish += int64(n)
|
||||
|
||||
// 当前分片写满,记录MD5并重置
|
||||
if *w.finish%w.sliceSize == 0 {
|
||||
if h, ok := w.hash.(interface{ Sum([]byte) []byte }); ok {
|
||||
*w.md5s = append(*w.md5s, strings.ToUpper(hex.EncodeToString(h.Sum(nil))))
|
||||
}
|
||||
if resetter, ok := w.hash.(interface{ Reset() }); ok {
|
||||
resetter.Reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
// 报告进度(缓存阶段占50%)
|
||||
if w.fileSize > 0 && w.up != nil {
|
||||
w.up(float64(*w.finish) / float64(w.fileSize) * 50)
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package _189_tv
|
||||
|
||||
import (
|
||||
"container/ring"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -12,18 +11,20 @@ import (
|
||||
"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/cron"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type Cloud189TV struct {
|
||||
model.Storage
|
||||
Addition
|
||||
client *resty.Client
|
||||
tokenInfo *AppSessionResp
|
||||
uploadThread int
|
||||
familyTransferFolder *ring.Ring
|
||||
cleanFamilyTransferFile func()
|
||||
storageConfig driver.Config
|
||||
client *resty.Client
|
||||
tokenInfo *AppSessionResp
|
||||
uploadThread int
|
||||
storageConfig driver.Config
|
||||
|
||||
TempUuid string
|
||||
cron *cron.Cron // 新增 cron 字段
|
||||
}
|
||||
|
||||
func (y *Cloud189TV) Config() driver.Config {
|
||||
@@ -68,7 +69,7 @@ func (y *Cloud189TV) Init(ctx context.Context) (err error) {
|
||||
// 避免重复登陆
|
||||
if !y.isLogin() || y.Addition.AccessToken == "" {
|
||||
if err = y.login(); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +80,17 @@ func (y *Cloud189TV) Init(ctx context.Context) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
y.cron = cron.NewCron(time.Minute * 5)
|
||||
y.cron.Do(y.keepAlive)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (y *Cloud189TV) Drop(ctx context.Context) error {
|
||||
if y.cron != nil {
|
||||
y.cron.Stop()
|
||||
y.cron = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -236,7 +244,6 @@ func (y *Cloud189TV) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
FileName: srcObj.GetName(),
|
||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -270,5 +277,25 @@ func (y *Cloud189TV) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
||||
}
|
||||
|
||||
return y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
||||
|
||||
}
|
||||
|
||||
func (y *Cloud189TV) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
|
||||
capacityInfo, err := y.getCapacityInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var total, used int64
|
||||
if y.isFamily() {
|
||||
total = capacityInfo.FamilyCapacityInfo.TotalSize
|
||||
used = capacityInfo.FamilyCapacityInfo.UsedSize
|
||||
} else {
|
||||
total = capacityInfo.CloudCapacityInfo.TotalSize
|
||||
used = capacityInfo.CloudCapacityInfo.UsedSize
|
||||
}
|
||||
return &model.StorageDetails{
|
||||
DiskUsage: model.DiskUsage{
|
||||
TotalSpace: total,
|
||||
UsedSpace: used,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user