Compare commits

..

18 Commits

Author SHA1 Message Date
f029df88e4 Add Terms of Use and Privacy Policy links to READMEs
Added links to the Terms of Use and Privacy Policy in the English, Chinese, Japanese, and Dutch README files to provide users with easy access to legal information.
2025-07-01 14:00:36 +08:00
00f825d9e2 Update documentation links in README files
Replaced plain documentation URLs with labeled links and icons in English, Chinese, Japanese, and Dutch README files for improved clarity and user experience.
2025-07-01 13:53:29 +08:00
732dcfa5b1 fix format 2025-07-01 13:40:52 +08:00
e2ad8eabb8 Revert "test large name"
This reverts commit affedc845b.
2025-07-01 13:38:28 +08:00
affedc845b test large name 2025-07-01 13:37:43 +08:00
382cd6425f Add Dutch README and update language links
Added a new Dutch translation (README_nl.md) and updated language navigation links in the English, Chinese, and Japanese README files to include Dutch.
2025-07-01 13:34:42 +08:00
e880acb71d Add AGPL-3.0 license links to README files
Updated the English, Chinese, and Japanese README files to include direct links to the AGPL-3.0 license text and the LICENSE file for clarity and easier access.
2025-07-01 13:29:51 +08:00
a0d1eadf3e Move language and links sections below logo in READMEs
Repositioned the language selection and related links sections to appear after the logo and separator in README.md, README_cn.md, and README_ja.md for improved layout consistency.
2025-07-01 13:25:52 +08:00
70a0a32b7b Revise and unify README files across languages
Updated README.md, README_cn.md, and README_ja.md to improve structure, add navigation links, clarify project purpose, and unify feature lists. Enhanced formatting, added acknowledgments to original authors, and improved legal/disclaimer sections for consistency across English, Chinese, and Japanese documentation.
2025-07-01 13:23:36 +08:00
2f32120908 Update Go Report Card badge URL in README
Changed the Go Report Card badge to reference v3 instead of v4. This ensures the badge displays the correct status for the intended version.
2025-07-01 12:56:26 +08:00
0fdfa2b365 Update README.md 2025-07-01 12:53:43 +08:00
82713611c0 Update Go Report Card badge URL in README
Changed the Go Report Card badge link to remove the '/v3' suffix, ensuring it points to the correct repository path.
2025-07-01 12:53:22 +08:00
41acb3e865 Update project description in README
Revised the introductory paragraph to emphasize OpenList's resilience and community-driven nature as a fork of AList, highlighting its commitment to defending open source against trust-based attacks.
2025-07-01 12:52:46 +08:00
77aca6609a Update README.md 2025-07-01 12:50:45 +08:00
63a597f802 Improve README badge formatting and alignment
Reformatted the badge section in the README for better readability and visual alignment. Updated the div to use 'align="center"' and placed each badge on its own line with proper indentation.
2025-07-01 12:49:57 +08:00
fcf7530dd8 Update logo size and remove migration note in README
Set explicit width and height for the logo image and removed the note about migration progress, reflecting project updates.
2025-07-01 12:46:38 +08:00
5f0645ded8 Revert README header to HTML
Replaces markdown-based center alignment and badge/image syntax with HTML tags for better visual formatting and consistency in the README header.
2025-07-01 12:45:04 +08:00
0f7ba9599d Revise README formatting and update project info
Refactored the README to use markdown badge/link syntax, improved formatting, and clarified the disclaimer section. Updated Docker Deploy status, added a Contact Us section, and reordered the Contributors section for better project transparency and communication.
2025-07-01 12:42:59 +08:00
221 changed files with 3863 additions and 4579 deletions

View File

@ -25,11 +25,11 @@ body:
- label: | - label: |
我确认我的描述清晰,语法礼貌,能帮助开发者快速定位问题,并符合社区规则。 我确认我的描述清晰,语法礼貌,能帮助开发者快速定位问题,并符合社区规则。
- label: | - label: |
我已确认阅读了[OpenList文档](https://doc.oplist.org)。 我已确认阅读了[OpenList文档](https://docs.oplist.org)。
- label: | - label: |
我已确认没有重复的问题或讨论。 我已确认没有重复的问题或讨论。
- label: | - label: |
我已确认是`OpenList`的问题,而不是其他原因(例如 [网络](https://doc.oplist.org/faq/howto#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host-1) `依赖`或`操作`)。 我已确认是`OpenList`的问题,而不是其他原因(例如 [网络](https://docs.oplist.org/zh/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host) `依赖`或`操作`)。
- label: | - label: |
我认为此问题必须由`OpenList`处理,而非第三方。 我认为此问题必须由`OpenList`处理,而非第三方。
- label: | - label: |
@ -72,7 +72,7 @@ body:
attributes: attributes:
label: 日志(可选) label: 日志(可选)
description: | 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 - type: textarea
id: reproduction id: reproduction
attributes: attributes:

View File

@ -25,11 +25,11 @@ body:
- label: | - label: |
I confirm my description is clear, polite, helps developers quickly locate the issue, and complies with community rules. I confirm my description is clear, polite, helps developers quickly locate the issue, and complies with community rules.
- label: | - label: |
I have read the [OpenList documentation](https://doc.oplist.org). I have read the [OpenList documentation](https://docs.oplist.org).
- label: | - label: |
I confirm there are no duplicate issues or discussions. I confirm there are no duplicate issues or discussions.
- label: | - label: |
I confirm this is an `OpenList` issue, not caused by other reasons (such as [network](https://doc.oplist.org/faq/howto#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host-1), dependencies, or operation). I confirm this is an `OpenList` issue, not caused by other reasons (such as [network](https://docs.oplist.org/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host), dependencies, or operation).
- label: | - label: |
I believe this issue must be handled by `OpenList` and not by a third party. I believe this issue must be handled by `OpenList` and not by a third party.
- label: | - label: |
@ -72,7 +72,7 @@ body:
attributes: attributes:
label: Logs (optional) label: Logs (optional)
description: | 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) Please copy and paste any relevant log output or screenshots. (You may mask sensitive fields)
- type: textarea - type: textarea
id: reproduction id: reproduction
attributes: attributes:

View File

@ -19,7 +19,7 @@ body:
- label: | - label: |
我确认我的描述清晰,语法礼貌,能帮助开发者快速定位问题,并符合社区规则。 我确认我的描述清晰,语法礼貌,能帮助开发者快速定位问题,并符合社区规则。
- label: | - label: |
我已确认阅读了[OpenList文档](https://doc.oplist.org)。 我已确认阅读了[OpenList文档](https://docs.oplist.org)。
- label: | - label: |
我已确认没有重复的问题或讨论。 我已确认没有重复的问题或讨论。
- label: | - label: |

View File

@ -19,7 +19,7 @@ body:
- label: | - label: |
I confirm my description is clear, polite, helps developers quickly locate the issue, and complies with community rules. I confirm my description is clear, polite, helps developers quickly locate the issue, and complies with community rules.
- label: | - label: |
I have read the [OpenList documentation](https://doc.oplist.org). I have read the [OpenList documentation](https://docs.oplist.org).
- label: | - label: |
I confirm there are no duplicate issues or discussions. I confirm there are no duplicate issues or discussions.
- label: | - label: |

21
.github/config.yml vendored Normal file
View File

@ -0,0 +1,21 @@
# Configuration for welcome - https://github.com/behaviorbot/welcome
# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
# Comment to be posted to on first time issues
newIssueWelcomeComment: >
Thanks for opening your first issue here! Be sure to follow the issue template!
# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: >
Thanks for opening this pull request! Please check out our contributing guidelines.
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request! We here at behavior bot are proud of you!
# It is recommend to include as many gifs and emojis as possible

21
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,21 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 44
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 20
# Issues with these labels will never be considered stale
exemptLabels:
- accepted
- security
- working
- pr-welcome
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue was closed due to inactive more than 52 days. You can reopen or
recreate it if you think it should continue. Thank you for your contributions again.

View File

@ -2,7 +2,7 @@ name: Beta Release builds
on: on:
push: push:
branches: ["main"] branches: [ 'main' ]
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@ -14,8 +14,12 @@ permissions:
jobs: jobs:
changelog: changelog:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Beta Release Changelog name: Beta Release Changelog
runs-on: ubuntu-latest runs-on: ${{ matrix.platform }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -61,17 +65,17 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- target: "!(*musl*|*windows-arm64*|*android*|*freebsd*)" # xgo and loongarch - target: '!(*musl*|*windows-arm64*|*android*|*freebsd*)' # xgo
hash: "md5" hash: "md5"
- target: "linux-!(arm*)-musl*" #musl-not-arm - target: 'linux-!(arm*)-musl*' #musl-not-arm
hash: "md5-linux-musl" hash: "md5-linux-musl"
- target: "linux-arm*-musl*" #musl-arm - target: 'linux-arm*-musl*' #musl-arm
hash: "md5-linux-musl-arm" hash: "md5-linux-musl-arm"
- target: "windows-arm64" #win-arm64 - target: 'windows-arm64' #win-arm64
hash: "md5-windows-arm64" hash: "md5-windows-arm64"
- target: "android-*" #android - target: 'android-*' #android
hash: "md5-android" hash: "md5-android"
- target: "freebsd-*" #freebsd - target: 'freebsd-*' #freebsd
hash: "md5-freebsd" hash: "md5-freebsd"
name: Beta Release name: Beta Release
@ -85,7 +89,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.24.5" go-version: '1.22'
- name: Setup web - name: Setup web
run: bash build.sh dev web run: bash build.sh dev web
@ -93,20 +97,19 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build - name: Build
uses: OpenListTeam/cgo-actions@v1.2.2 uses: OpenListTeam/cgo-actions@v1.1.2
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
musl-target-format: $os-$musl-$arch musl-target-format: $os-$musl-$arch
github-token: ${{ secrets.GITHUB_TOKEN }}
out-dir: build out-dir: build
output: openlist-$target$ext output: openlist-$target$ext
musl-base-url: "https://github.com/OpenListTeam/musl-compilers/releases/latest/download/" musl-base-url: "https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
x-flags: | x-flags: |
github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$built_at github.com/OpenListTeam/OpenList/internal/conf.BuiltAt=$built_at
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=OpenList github.com/OpenListTeam/OpenList/internal/conf.GitAuthor=OpenList
github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$git_commit github.com/OpenListTeam/OpenList/internal/conf.GitCommit=$git_commit
github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$tag github.com/OpenListTeam/OpenList/internal/conf.Version=$tag
github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=dev github.com/OpenListTeam/OpenList/internal/conf.WebVersion=dev
- name: Compress - name: Compress
run: | run: |
@ -114,7 +117,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# See above # See above
- name: Upload assets to beta release - name: Upload assets to beta release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:

View File

@ -1,8 +1,10 @@
name: Test Build name: Test Build
on: on:
push:
branches: [ 'main' ]
pull_request: pull_request:
branches: ["main"] branches: [ 'main' ]
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@ -13,7 +15,8 @@ jobs:
build: build:
strategy: strategy:
matrix: matrix:
target: platform: [ubuntu-latest]
target:
- darwin-amd64 - darwin-amd64
- darwin-arm64 - darwin-arm64
- windows-amd64 - windows-amd64
@ -21,9 +24,10 @@ jobs:
- linux-amd64-musl - linux-amd64-musl
- windows-arm64 - windows-arm64
- android-arm64 - android-arm64
name: Build ${{ matrix.target }} name: Build
runs-on: ubuntu-latest runs-on: ${{ matrix.platform }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -33,7 +37,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.24.5" go-version: '1.22'
- name: Setup web - name: Setup web
run: bash build.sh dev web run: bash build.sh dev web
@ -41,22 +45,20 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build - name: Build
uses: OpenListTeam/cgo-actions@v1.2.2 uses: OpenListTeam/cgo-actions@v1.1.2
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
musl-target-format: $os-$musl-$arch musl-target-format: $os-$musl-$arch
github-token: ${{ secrets.GITHUB_TOKEN }}
out-dir: build out-dir: build
x-flags: | x-flags: |
github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$built_at github.com/OpenListTeam/OpenList/internal/conf.BuiltAt=$built_at
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=OpenList github.com/OpenListTeam/OpenList/internal/conf.GitAuthor=OpenList
github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$git_commit github.com/OpenListTeam/OpenList/internal/conf.GitCommit=$git_commit
github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$tag github.com/OpenListTeam/OpenList/internal/conf.Version=$tag
github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=dev github.com/OpenListTeam/OpenList/internal/conf.WebVersion=dev
output: openlist$ext
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: openlist_${{ steps.short-sha.outputs.sha }}_${{ matrix.target }} name: openlist_${{ env.SHA }}_${{ matrix.target }}
path: build/* path: build/*

View File

@ -1,4 +1,4 @@
name: Release Automatic changelog name: Automatic changelog
on: on:
push: push:

View File

@ -1,61 +0,0 @@
name: Issue or PR Auto Reply
on:
issues:
types: [opened]
pull_request:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
auto-reply:
runs-on: ubuntu-latest
if: github.event_name == 'issues'
steps:
- name: Check issue for unchecked tasks and reply
uses: actions/github-script@v7
with:
script: |
const issueBody = context.payload.issue.body || "";
const unchecked = /- \[ \] /.test(issueBody);
let comment = "感谢您联系OpenList。我们会尽快回复您。\n";
comment += "Thanks for contacting OpenList. We will reply to you as soon as possible.\n\n";
if (unchecked) {
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";
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: comment
});
}

View File

@ -8,34 +8,24 @@ permissions:
contents: write contents: write
jobs: jobs:
# Set release to prerelease first
prerelease:
name: Set Prerelease
runs-on: ubuntu-latest
steps:
- name: Prerelease
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
prerelease: true
# Main release job for all platforms
release: release:
needs: prerelease
strategy: strategy:
matrix: matrix:
build-type: [ 'standard', 'lite' ] platform: [ ubuntu-latest ]
target-platform: [ '', 'android', 'freebsd', 'linux_musl', 'linux_musl_arm' ] go-version: [ '1.21' ]
name: Release ${{ matrix.target-platform && format('{0} ', matrix.target-platform) || '' }}${{ matrix.build-type == 'lite' && 'Lite' || '' }} name: Release
runs-on: ubuntu-latest runs-on: ${{ matrix.platform }}
steps: steps:
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
if: matrix.target-platform == ''
uses: jlumbroso/free-disk-space@main uses: jlumbroso/free-disk-space@main
with: with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true android: true
dotnet: true dotnet: true
haskell: true haskell: true
@ -43,10 +33,17 @@ jobs:
docker-images: true docker-images: true
swap-storage: true swap-storage: true
- name: Prerelease
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
prerelease: true
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.24' go-version: ${{ matrix.go-version }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -54,7 +51,6 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Install dependencies - name: Install dependencies
if: matrix.target-platform == ''
run: | run: |
sudo snap install zig --classic --beta sudo snap install zig --classic --beta
docker pull crazymax/xgo:latest docker pull crazymax/xgo:latest
@ -63,7 +59,68 @@ jobs:
- name: Build - name: Build
run: | run: |
bash build.sh release ${{ matrix.build-type == 'lite' && 'lite' || '' }} ${{ matrix.target-platform }} bash build.sh release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
prerelease: false
release-lite:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release Lite
runs-on: ${{ matrix.platform }}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Prerelease
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
prerelease: true
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
run: |
sudo snap install zig --classic --beta
docker pull crazymax/xgo:latest
go install github.com/crazy-max/xgo@latest
sudo apt install upx
- name: Build
run: |
bash build.sh release lite
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

69
.github/workflows/release_android.yml vendored Normal file
View File

@ -0,0 +1,69 @@
name: Release builds (Android)
on:
release:
types: [ published ]
permissions:
contents: write
jobs:
release_android:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release android
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
release_android_lite:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release lite android
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*

View File

@ -33,6 +33,9 @@ env:
ARTIFACT_NAME_LITE: 'binaries_docker_release_lite' ARTIFACT_NAME_LITE: 'binaries_docker_release_lite'
RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64' RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64'
IMAGE_PUSH: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} IMAGE_PUSH: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
IMAGE_IS_PROD: ${{ github.ref_type == 'tag' || github.event.inputs.as_latest == 'true' }}
IMAGE_TAGS_BETA: |
type=raw,value=beta,enable={{is_default_branch}}
permissions: permissions:
packages: write packages: write
@ -62,7 +65,14 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build go binary (beta)
if: env.IMAGE_IS_PROD != 'true'
run: bash build.sh beta docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build go binary (release) - name: Build go binary (release)
if: env.IMAGE_IS_PROD == 'true'
run: bash build.sh release docker-multiplatform run: bash build.sh release docker-multiplatform
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -78,7 +88,7 @@ jobs:
!build/musl-libs/** !build/musl-libs/**
build_binary_lite: build_binary_lite:
name: Build Binaries for Docker Release (Lite) name: Build Binaries for Docker Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -101,7 +111,14 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build go binary (beta)
if: env.IMAGE_IS_PROD != 'true'
run: bash build.sh beta lite docker-multiplatform
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build go binary (release) - name: Build go binary (release)
if: env.IMAGE_IS_PROD == 'true'
run: bash build.sh release lite docker-multiplatform run: bash build.sh release lite docker-multiplatform
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -164,7 +181,7 @@ jobs:
if: env.IMAGE_PUSH == 'true' if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ vars.DOCKERHUB_ORG_NAME_BACKUP || env.DOCKERHUB_ORG_NAME }} username: ${{ env.DOCKERHUB_ORG_NAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta - name: Docker meta
@ -175,11 +192,13 @@ jobs:
${{ env.REGISTRY }}/${{ env.GHCR_ORG_NAME }}/${{ env.IMAGE_NAME }} ${{ env.REGISTRY }}/${{ env.GHCR_ORG_NAME }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }} ${{ env.DOCKERHUB_ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}
tags: > tags: >
${{ github.event_name == 'workflow_dispatch' ${{ env.IMAGE_IS_PROD == 'true' && (
github.event_name == 'workflow_dispatch'
&& format('type=raw,value={0}', github.event.inputs.manual_tag) && format('type=raw,value={0}', github.event.inputs.manual_tag)
|| format('type=raw,value={0}', github.ref_name) }} || format('type=raw,value={0}', github.ref_name)
) || env.IMAGE_TAGS_BETA }}
flavor: | flavor: |
latest=${{ github.event_name == 'push' || github.event.inputs.as_latest == 'true' }} latest=${{ env.IMAGE_IS_PROD }}
${{ matrix.tag_favor }} ${{ matrix.tag_favor }}
- name: Build and push - name: Build and push
@ -196,7 +215,7 @@ jobs:
release_docker_lite: release_docker_lite:
needs: build_binary_lite needs: build_binary_lite
name: Release Docker image (Lite) name: Release Docker image
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
@ -242,7 +261,7 @@ jobs:
if: env.IMAGE_PUSH == 'true' if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ vars.DOCKERHUB_ORG_NAME_BACKUP || env.DOCKERHUB_ORG_NAME }} username: ${{ env.DOCKERHUB_ORG_NAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta - name: Docker meta
@ -253,11 +272,13 @@ jobs:
${{ env.REGISTRY }}/${{ env.GHCR_ORG_NAME }}/${{ env.IMAGE_NAME }} ${{ env.REGISTRY }}/${{ env.GHCR_ORG_NAME }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }} ${{ env.DOCKERHUB_ORG_NAME }}/${{ env.IMAGE_NAME_DOCKERHUB }}
tags: > tags: >
${{ github.event_name == 'workflow_dispatch' ${{ env.IMAGE_IS_PROD == 'true' && (
github.event_name == 'workflow_dispatch'
&& format('type=raw,value={0}', github.event.inputs.manual_tag) && format('type=raw,value={0}', github.event.inputs.manual_tag)
|| format('type=raw,value={0}', github.ref_name) }} || format('type=raw,value={0}', github.ref_name)
) || env.IMAGE_TAGS_BETA }}
flavor: | flavor: |
latest=${{ github.event_name == 'push' || github.event.inputs.as_latest == 'true' }} latest=${{ env.IMAGE_IS_PROD }}
${{ matrix.tag_favor }} ${{ matrix.tag_favor }}
- name: Build and push - name: Build and push
@ -270,4 +291,4 @@ jobs:
build-args: ${{ matrix.build_arg }} build-args: ${{ matrix.build_arg }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ env.RELEASE_PLATFORMS }} platforms: ${{ env.RELEASE_PLATFORMS }}

69
.github/workflows/release_freebsd.yml vendored Normal file
View File

@ -0,0 +1,69 @@
name: Release builds (Freebsd)
on:
release:
types: [ published ]
permissions:
contents: write
jobs:
release_freebsd:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release freebsd
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
release_freebsd_lite:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release lite freebsd
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*

View File

@ -0,0 +1,69 @@
name: Release builds (linux_musl)
on:
release:
types: [ published ]
permissions:
contents: write
jobs:
release_linux_musl:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release linux_musl
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
release_linux_musl_lite:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release lite linux_musl
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*

View File

@ -0,0 +1,70 @@
name: Release builds (linux_musl_arm)
on:
release:
types: [ published ]
permissions:
contents: write
jobs:
release_linux_musl_arm:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release linux_musl_arm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
release_linux_musl_arm_lite:
strategy:
matrix:
platform: [ ubuntu-latest ]
go-version: [ '1.21' ]
name: Release
runs-on: ${{ matrix.platform }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
bash build.sh release lite linux_musl_arm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: softprops/action-gh-release@v2
with:
files: build/compress/*

View File

@ -1,4 +1,4 @@
name: Beta Release (Docker) name: Docker Beta Release
on: on:
workflow_dispatch: workflow_dispatch:
@ -20,6 +20,7 @@ env:
IMAGE_NAME_DOCKERHUB: openlist IMAGE_NAME_DOCKERHUB: openlist
REGISTRY: ghcr.io REGISTRY: ghcr.io
ARTIFACT_NAME: 'binaries_docker_release' ARTIFACT_NAME: 'binaries_docker_release'
ARTIFACT_NAME_LITE: 'binaries_docker_release_lite'
RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64' RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64'
IMAGE_PUSH: ${{ github.event_name == 'push' }} IMAGE_PUSH: ${{ github.event_name == 'push' }}
IMAGE_TAGS_BETA: | IMAGE_TAGS_BETA: |
@ -28,7 +29,7 @@ env:
jobs: jobs:
build_binary: build_binary:
name: Build Binaries for Docker Release (Beta) name: Build Binaries for Docker Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -68,7 +69,7 @@ jobs:
release_docker: release_docker:
needs: build_binary needs: build_binary
name: Release Docker image (Beta) name: Release Docker image
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
packages: write packages: write
@ -116,7 +117,7 @@ jobs:
if: env.IMAGE_PUSH == 'true' if: env.IMAGE_PUSH == 'true'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ vars.DOCKERHUB_ORG_NAME_BACKUP || env.DOCKERHUB_ORG_NAME }} username: ${{ env.DOCKERHUB_ORG_NAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta - name: Docker meta

View File

@ -19,7 +19,7 @@ jobs:
uses: peter-evans/repository-dispatch@v3 uses: peter-evans/repository-dispatch@v3
with: with:
token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }} token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }}
repository: ${{ vars.HOOK_REPO || 'OpenListTeam/OpenList-OpenWRT' }} repository: ${{ vars.HOOK_REPO || 'OpenListTeam/luci-app-openlist' }}
event-type: update-hashes event-type: update-hashes
client-payload: | client-payload: |
{ {

View File

@ -17,7 +17,7 @@ WORKDIR /opt/openlist/
RUN apk update && \ RUN apk update && \
apk upgrade --no-cache && \ apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates su-exec tzdata runit; \ apk add --no-cache bash ca-certificates su-exec tzdata; \
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \ [ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \
[ "$INSTALL_ARIA2" = "true" ] && apk add --no-cache curl aria2 && \ [ "$INSTALL_ARIA2" = "true" ] && apk add --no-cache curl aria2 && \
mkdir -p /opt/aria2/.aria2 && \ mkdir -p /opt/aria2/.aria2 && \
@ -28,24 +28,9 @@ RUN apk update && \
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \ sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \ sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \ sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \
mkdir -p /opt/service/stop/aria2/log && \
echo '#!/bin/sh' > /opt/service/stop/aria2/run && \
echo 'exec 2>&1' >> /opt/service/stop/aria2/run && \
echo 'exec aria2c --enable-rpc --rpc-allow-origin-all --conf-path=/opt/aria2/.aria2/aria2.conf' >> /opt/service/stop/aria2/run && \
echo '#!/bin/sh' > /opt/service/stop/aria2/log/run && \
echo 'mkdir -p /opt/openlist/data/log/aria2 2>/dev/null' >> /opt/service/stop/aria2/log/run && \
echo 'exec svlogd /opt/openlist/data/log/aria2' >> /opt/service/stop/aria2/log/run && \
chmod +x /opt/service/stop/aria2/run /opt/service/stop/aria2/log/run && \
touch /opt/aria2/.aria2/aria2.session && \ touch /opt/aria2/.aria2/aria2.session && \
/opt/aria2/.aria2/tracker.sh ; \ /opt/aria2/.aria2/tracker.sh ; \
rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
RUN mkdir -p /opt/service/start/openlist && \
echo '#!/bin/sh' > /opt/service/start/openlist/run && \
echo 'exec 2>&1' >> /opt/service/start/openlist/run && \
echo 'cd /opt/openlist' >> /opt/service/start/openlist/run && \
echo 'exec ./openlist server --no-prefix' >> /opt/service/start/openlist/run && \
chmod +x /opt/service/start/openlist/run
COPY --chmod=755 --from=builder /app/bin/openlist ./ COPY --chmod=755 --from=builder /app/bin/openlist ./
COPY --chmod=755 entrypoint.sh /entrypoint.sh COPY --chmod=755 entrypoint.sh /entrypoint.sh

View File

@ -9,7 +9,7 @@ WORKDIR /opt/openlist/
RUN apk update && \ RUN apk update && \
apk upgrade --no-cache && \ apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates su-exec tzdata runit; \ apk add --no-cache bash ca-certificates su-exec tzdata; \
[ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \ [ "$INSTALL_FFMPEG" = "true" ] && apk add --no-cache ffmpeg; \
[ "$INSTALL_ARIA2" = "true" ] && apk add --no-cache curl aria2 && \ [ "$INSTALL_ARIA2" = "true" ] && apk add --no-cache curl aria2 && \
mkdir -p /opt/aria2/.aria2 && \ mkdir -p /opt/aria2/.aria2 && \
@ -20,24 +20,9 @@ RUN apk update && \
sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \ sed -i 's|/root/.aria2|/opt/aria2/.aria2|g' /opt/aria2/.aria2/script.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \ sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/aria2.conf && \
sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \ sed -i 's|/root|/opt/aria2|g' /opt/aria2/.aria2/script.conf && \
mkdir -p /opt/service/stop/aria2/log && \
echo '#!/bin/sh' > /opt/service/stop/aria2/run && \
echo 'exec 2>&1' >> /opt/service/stop/aria2/run && \
echo 'exec aria2c --enable-rpc --rpc-allow-origin-all --conf-path=/opt/aria2/.aria2/aria2.conf' >> /opt/service/stop/aria2/run && \
echo '#!/bin/sh' > /opt/service/stop/aria2/log/run && \
echo 'mkdir -p /opt/openlist/data/log/aria2 2>/dev/null' >> /opt/service/stop/aria2/log/run && \
echo 'exec svlogd /opt/openlist/data/log/aria2' >> /opt/service/stop/aria2/log/run && \
chmod +x /opt/service/stop/aria2/run /opt/service/stop/aria2/log/run && \
touch /opt/aria2/.aria2/aria2.session && \ touch /opt/aria2/.aria2/aria2.session && \
/opt/aria2/.aria2/tracker.sh ; \ /opt/aria2/.aria2/tracker.sh ; \
rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
RUN mkdir -p /opt/service/start/openlist && \
echo '#!/bin/sh' > /opt/service/start/openlist/run && \
echo 'exec 2>&1' >> /opt/service/start/openlist/run && \
echo 'cd /opt/openlist' >> /opt/service/start/openlist/run && \
echo 'exec ./openlist server --no-prefix' >> /opt/service/start/openlist/run && \
chmod +x /opt/service/start/openlist/run
COPY --chmod=755 /build/${TARGETPLATFORM}/openlist ./ COPY --chmod=755 /build/${TARGETPLATFORM}/openlist ./
COPY --chmod=755 entrypoint.sh /entrypoint.sh COPY --chmod=755 entrypoint.sh /entrypoint.sh

View File

@ -20,34 +20,6 @@
- [CODE OF CONDUCT](./CODE_OF_CONDUCT.md) - [CODE OF CONDUCT](./CODE_OF_CONDUCT.md)
- [LICENSE](./LICENSE) - [LICENSE](./LICENSE)
## Disclaimer
OpenList is an open-source project independently maintained by the OpenList Team, following the AGPL-3.0 license and committed to maintaining complete code openness and modification transparency.
We have noticed the emergence of some third-party projects in the community with names similar to this project, such as OpenListApp/OpenListApp, as well as some paid proprietary software using the same or similar naming. To avoid user confusion, we hereby declare:
- OpenList has no official association with any third-party derivative projects.
- All software, code, and services of this project are maintained by the OpenList Team and are freely available on GitHub.
- Project documentation and API services primarily rely on charitable resources provided by Cloudflare. There are currently no paid plans or commercial deployments, and the use of existing features does not involve any costs.
We respect the community's rights to free use and derivative development, but we also strongly urge downstream projects:
- Should not use the "OpenList" name for impersonation promotion or commercial gain;
- Must not distribute OpenList-based code in a closed-source manner or violate AGPL license terms.
To better maintain healthy ecosystem development, we recommend:
- Clearly indicate the project source and choose appropriate open-source licenses in accordance with the open-source spirit;
- If involving commercial use, please avoid using "OpenList" or any confusing naming as the project name;
- If you need to use materials located under OpenListTeam/Logo, you may modify and use them under compliance with the agreement.
Thank you for your support and understanding of the OpenList project.
## Features ## Features
- [x] Multiple storages - [x] Multiple storages
@ -106,9 +78,10 @@ Thank you for your support and understanding of the OpenList project.
## Document ## Document
- 📘 [Global Site](https://doc.oplist.org) - 📘 [Docs & Install Guide](https://docs.oplist.org)
- 📚 [Backup Site](https://doc.openlist.team) - 📚 [Backup Docs Site](https://docs.openlist.team)
- 🌏 [CN Site](https://doc.oplist.org.cn) - ⚖️ [Terms of Use](https://docs.oplist.org/terms)
- 🔒 [Privacy Policy](https://docs.oplist.org/privacy)
## Demo ## Demo

View File

@ -20,34 +20,6 @@
- [行为准则](./CODE_OF_CONDUCT.md) - [行为准则](./CODE_OF_CONDUCT.md)
- [许可证](./LICENSE) - [许可证](./LICENSE)
## 免责声明
OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3.0 许可证,致力于保持完整的代码开放性和修改透明性。
我们注意到社区中出现了一些与本项目名称相似的第三方项目,如 OpenListApp/OpenListApp以及部分采用相同或近似命名的收费专有软件。为避免用户误解现声明如下
- OpenList 与任何第三方衍生项目无官方关联。
- 本项目的全部软件、代码与服务由 OpenList 团队维护,可在 GitHub 免费获取。
- 项目文档与 API 服务均主要依托于 Cloudflare 提供的公益资源,目前无任何收费计划或商业部署,现有功能使用不涉及任何支出。
我们尊重社区的自由使用与衍生开发权利,但也强烈呼吁下游项目:
- 不应以“OpenList”名义进行冒名宣传或获取商业利益
- 不得将基于 OpenList 的代码进行闭源分发或违反 AGPL 许可证条款。
为了更好地维护生态健康发展,我们建议:
- 明确注明项目来源,并以符合开源精神的方式选择适当的开源许可证;
- 如涉及商业用途请避免使用“OpenList”或任何会产生混淆的方式作为项目名称
- 若需使用本项目位于 OpenListTeam/Logo 下的素材,可在遵守协议的前提下进行修改后使用。
感谢您对 OpenList 项目的支持与理解。
## 功能 ## 功能
- [x] 多种存储 - [x] 多种存储
@ -106,9 +78,10 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3
## 文档 ## 文档
- 🌏 [国内站点](https://doc.oplist.org.cn) - 📘 [文档与安装指南](https://docs.oplist.org)
- 📘 [海外站点](https://doc.oplist.org) - 📚 [备用文档站点](https://docs.openlist.team)
- 📚 [备用站点](https://doc.openlist.team) - ⚖️ [使用条款](https://docs.oplist.org/terms)
- 🔒 [隐私政策](https://docs.oplist.org/privacy)
## 演示 ## 演示

View File

@ -20,34 +20,6 @@
- [行動規範](./CODE_OF_CONDUCT.md) - [行動規範](./CODE_OF_CONDUCT.md)
- [ライセンス](./LICENSE) - [ライセンス](./LICENSE)
## 免責事項
OpenListは、OpenListチームが独立して維持するオープンソースプロジェクトであり、AGPL-3.0ライセンスに従い、完全なコードの開放性と変更の透明性を維持することに専念しています。
コミュニティ内で、OpenListApp/OpenListAppなど、本プロジェクトと類似した名称を持つサードパーティプロジェクトや、同一または類似した命名を採用する有料専有ソフトウェアが出現していることを確認しています。ユーザーの誤解を避けるため、以下のように宣言いたします
- OpenListは、いかなるサードパーティ派生プロジェクトとも公式な関連性はありません。
- 本プロジェクトのすべてのソフトウェア、コード、サービスはOpenListチームによって維持され、GitHubで無料で取得できます。
- プロジェクトドキュメントとAPIサービスは主にCloudflareが提供する公益リソースに依存しており、現在有料プランや商業展開はなく、既存機能の使用に費用は発生しません。
私たちはコミュニティの自由な使用と派生開発の権利を尊重しますが、下流プロジェクトに強く呼びかけます:
- 「OpenList」の名前で偽装宣伝や商業利益を得るべきではありません
- OpenListベースのコードをクローズドソースで配布したり、AGPLライセンス条項に違反してはいけません。
エコシステムの健全な発展をより良く維持するため、以下を推奨します:
- プロジェクトの出典を明確に示し、オープンソース精神に合致する適切なオープンソースライセンスを選択する;
- 商業用途が関わる場合は、「OpenList」や混乱を招く可能性のある名前をプロジェクト名として使用することを避ける
- OpenListTeam/Logo下の素材を使用する必要がある場合は、協定を遵守した上で修正して使用できます。
OpenListプロジェクトへのご支援とご理解をありがとうございます。
## 特徴 ## 特徴
- [x] 複数ストレージ - [x] 複数ストレージ
@ -106,9 +78,10 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい
## ドキュメント ## ドキュメント
- 📘 [グローバルサイト](https://doc.oplist.org) - 📘 [ドキュメント・インストールガイド](https://docs.oplist.org)
- 📚 [バックアップサイト](https://doc.openlist.team) - 📚 [バックアップドキュメントサイト](https://docs.openlist.team)
- 🌏 [CNサイト](https://doc.oplist.org.cn) - ⚖️ [利用規約](https://docs.oplist.org/terms)
- 🔒 [プライバシーポリシー](https://docs.oplist.org/privacy)
## デモ ## デモ

View File

@ -20,34 +20,6 @@
- [Gedragscode](./CODE_OF_CONDUCT.md) - [Gedragscode](./CODE_OF_CONDUCT.md)
- [Licentie](./LICENSE) - [Licentie](./LICENSE)
## Disclaimer
OpenList is een open-source project dat onafhankelijk wordt onderhouden door het OpenList Team, volgend op de AGPL-3.0 licentie en toegewijd aan het behouden van volledige code openheid en transparantie van wijzigingen.
We hebben gemerkt dat er in de gemeenschap enkele derde partij projecten zijn verschenen met namen vergelijkbaar met dit project, zoals OpenListApp/OpenListApp, evenals enkele betaalde eigendomssoftware die dezelfde of soortgelijke naamgeving gebruikt. Om verwarring bij gebruikers te voorkomen, verklaren we hierbij:
- OpenList heeft geen officiële associatie met enige derde partij afgeleide projecten.
- Alle software, code en diensten van dit project worden onderhouden door het OpenList Team en zijn gratis beschikbaar op GitHub.
- Projectdocumentatie en API diensten zijn voornamelijk afhankelijk van liefdadigheidsbronnen verstrekt door Cloudflare. Er zijn momenteel geen betaalplannen of commerciële implementaties, en het gebruik van bestaande functies brengt geen kosten met zich mee.
We respecteren de rechten van de gemeenschap voor vrij gebruik en afgeleide ontwikkeling, maar we roepen downstream projecten ook ten zeerste op:
- Mogen niet de "OpenList" naam gebruiken voor namaakpromotie of commercieel gewin;
- Mogen OpenList-gebaseerde code niet distribueren op een closed-source manier of AGPL licentievoorwaarden schenden.
Om een gezonde ecosysteemontwikkeling beter te onderhouden, bevelen we aan:
- Duidelijk de projectbron aangeven en passende open-source licenties kiezen in overeenstemming met de open-source geest;
- Bij commercieel gebruik, vermijd het gebruik van "OpenList" of enige verwarrende naamgeving als projectnaam;
- Als u materialen onder OpenListTeam/Logo moet gebruiken, kunt u deze wijzigen en gebruiken onder naleving van de overeenkomst.
Dank u voor uw ondersteuning en begrip
## Functies ## Functies
- [x] Meerdere opslagmogelijkheden - [x] Meerdere opslagmogelijkheden
@ -106,9 +78,10 @@ Dank u voor uw ondersteuning en begrip
## Documentatie ## Documentatie
- 📘 [Global Site](https://doc.oplist.org) - 📘 [Documentatie & Installatiegids](https://docs.oplist.org)
- 📚 [Backup Site](https://doc.openlist.team) - 📚 [Back-up documentatiesite](https://docs.openlist.team)
- 🌏 [CN Site](https://doc.oplist.org.cn) - ⚖️ [Gebruiksvoorwaarden](https://docs.oplist.org/terms)
- 🔒 [Privacybeleid](https://docs.oplist.org/privacy)
## Demo ## Demo

View File

@ -38,11 +38,11 @@ fi
ldflags="\ ldflags="\
-w -s \ -w -s \
-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$builtAt' \ -X 'github.com/OpenListTeam/OpenList/internal/conf.BuiltAt=$builtAt' \
-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=$gitAuthor' \ -X 'github.com/OpenListTeam/OpenList/internal/conf.GitAuthor=$gitAuthor' \
-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$gitCommit' \ -X 'github.com/OpenListTeam/OpenList/internal/conf.GitCommit=$gitCommit' \
-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$version' \ -X 'github.com/OpenListTeam/OpenList/internal/conf.Version=$version' \
-X 'github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=$webVersion' \ -X 'github.com/OpenListTeam/OpenList/internal/conf.WebVersion=$webVersion' \
" "
FetchWebDev() { FetchWebDev() {
@ -121,8 +121,8 @@ BuildDev() {
xgo -targets=windows/amd64,darwin/amd64,darwin/arm64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter . xgo -targets=windows/amd64,darwin/amd64,darwin/arm64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
mv "$appName"-* dist mv "$appName"-* dist
cd dist cd dist
# cp ./"$appName"-windows-amd64.exe ./"$appName"-windows-amd64-upx.exe cp ./"$appName"-windows-amd64.exe ./"$appName"-windows-amd64-upx.exe
# upx -9 ./"$appName"-windows-amd64-upx.exe upx -9 ./"$appName"-windows-amd64-upx.exe
find . -type f -print0 | xargs -0 md5sum >md5.txt find . -type f -print0 | xargs -0 md5sum >md5.txt
cat md5.txt cat md5.txt
} }
@ -188,9 +188,9 @@ BuildRelease() {
BuildWinArm64 ./build/"$appName"-windows-arm64.exe BuildWinArm64 ./build/"$appName"-windows-arm64.exe
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter . xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
# why? Because some target platforms seem to have issues with upx compression # why? Because some target platforms seem to have issues with upx compression
# upx -9 ./"$appName"-linux-amd64 upx -9 ./"$appName"-linux-amd64
# cp ./"$appName"-windows-amd64.exe ./"$appName"-windows-amd64-upx.exe cp ./"$appName"-windows-amd64.exe ./"$appName"-windows-amd64-upx.exe
# upx -9 ./"$appName"-windows-amd64-upx.exe upx -9 ./"$appName"-windows-amd64-upx.exe
mv "$appName"-* build mv "$appName"-* build
} }
@ -278,7 +278,6 @@ BuildReleaseFreeBSD() {
freebsd_version=$(eval "curl -fsSL --max-time 2 $githubAuthArgs \"https://api.github.com/repos/freebsd/freebsd-src/tags\"" | \ freebsd_version=$(eval "curl -fsSL --max-time 2 $githubAuthArgs \"https://api.github.com/repos/freebsd/freebsd-src/tags\"" | \
jq -r '.[].name' | \ jq -r '.[].name' | \
grep '^release/14\.' | \ grep '^release/14\.' | \
grep -v -- '-p[0-9]*$' | \
sort -V | \ sort -V | \
tail -1 | \ tail -1 | \
sed 's/release\///' | \ sed 's/release\///' | \

View File

@ -16,7 +16,7 @@ var RootCmd = &cobra.Command{
Short: "A file list program that supports multiple storage.", Short: "A file list program that supports multiple storage.",
Long: `A file list program that supports multiple storage, Long: `A file list program that supports multiple storage,
built with love by OpenListTeam. built with love by OpenListTeam.
Complete documentation is available at https://doc.oplist.org/`, Complete documentation is available at https://docs.openlist.team/`,
} }
func Execute() { func Execute() {

View File

@ -19,7 +19,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/fs" "github.com/OpenListTeam/OpenList/v4/internal/fs"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/server" "github.com/OpenListTeam/OpenList/v4/server"
"github.com/OpenListTeam/OpenList/v4/server/middlewares"
"github.com/OpenListTeam/sftpd-openlist" "github.com/OpenListTeam/sftpd-openlist"
ftpserver "github.com/fclairamb/ftpserverlib" ftpserver "github.com/fclairamb/ftpserverlib"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -48,7 +47,7 @@ the address is defined in config file`,
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
} }
r := gin.New() r := gin.New()
r.Use(middlewares.HTTPFilteredLogger(), gin.RecoveryWithWriter(log.StandardLogger().Out)) r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.Init(r) server.Init(r)
var httpHandler http.Handler = r var httpHandler http.Handler = r
if conf.Conf.Scheme.EnableH2c { if conf.Conf.Scheme.EnableH2c {
@ -103,7 +102,7 @@ the address is defined in config file`,
} }
if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable { if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
s3r := gin.New() s3r := gin.New()
s3r.Use(middlewares.S3FilteredLogger(), gin.RecoveryWithWriter(log.StandardLogger().Out)) s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.InitS3(s3r) server.InitS3(s3r)
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port) s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
utils.Log.Infof("start S3 server @ %s", s3Base) utils.Log.Infof("start S3 server @ %s", s3Base)

View File

@ -186,9 +186,7 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
preHash = strings.ToUpper(preHash) preHash = strings.ToUpper(preHash)
fullHash := stream.GetHash().GetHash(utils.SHA1) fullHash := stream.GetHash().GetHash(utils.SHA1)
if len(fullHash) != utils.SHA1.Width { if len(fullHash) != utils.SHA1.Width {
cacheFileProgress := model.UpdateProgressWithRange(up, 0, 50) _, fullHash, err = streamPkg.CacheFullInTempFileAndHash(stream, utils.SHA1)
up = model.UpdateProgressWithRange(up, 50, 100)
_, fullHash, err = streamPkg.CacheFullInTempFileAndHash(stream, cacheFileProgress, utils.SHA1)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -18,6 +18,7 @@ var config = driver.Config{
Name: "115 Cloud", Name: "115 Cloud",
DefaultRoot: "0", DefaultRoot: "0",
// OnlyProxy: true, // OnlyProxy: true,
// OnlyLocal: true,
// NoOverwriteUpload: true, // NoOverwriteUpload: true,
} }

View File

@ -8,7 +8,6 @@ import (
"strings" "strings"
"time" "time"
sdk "github.com/OpenListTeam/115-sdk-go"
"github.com/OpenListTeam/OpenList/v4/cmd/flags" "github.com/OpenListTeam/OpenList/v4/cmd/flags"
"github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
@ -17,6 +16,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range" "github.com/OpenListTeam/OpenList/v4/pkg/http_range"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
sdk "github.com/OpenListTeam/115-sdk-go"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
@ -222,9 +222,7 @@ func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
} }
sha1 := file.GetHash().GetHash(utils.SHA1) sha1 := file.GetHash().GetHash(utils.SHA1)
if len(sha1) != utils.SHA1.Width { if len(sha1) != utils.SHA1.Width {
cacheFileProgress := model.UpdateProgressWithRange(up, 0, 50) _, sha1, err = stream.CacheFullInTempFileAndHash(file, utils.SHA1)
up = model.UpdateProgressWithRange(up, 50, 100)
_, sha1, err = stream.CacheFullInTempFileAndHash(file, cacheFileProgress, utils.SHA1)
if err != nil { if err != nil {
return err return err
} }
@ -254,7 +252,6 @@ func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
return err return err
} }
if resp.Status == 2 { if resp.Status == 2 {
up(100)
return nil return nil
} }
// 2. two way verify // 2. two way verify
@ -289,7 +286,6 @@ func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
return err return err
} }
if resp.Status == 2 { if resp.Status == 2 {
up(100)
return nil return nil
} }
} }
@ -306,22 +302,6 @@ func (d *Open115) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
return nil return nil
} }
func (d *Open115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {
return d.client.AddOfflineTaskURIs(ctx, uris, dstDir.GetID())
}
func (d *Open115) DeleteOfflineTask(ctx context.Context, infoHash string, deleteFiles bool) error {
return d.client.DeleteOfflineTask(ctx, infoHash, deleteFiles)
}
func (d *Open115) OfflineList(ctx context.Context) (*sdk.OfflineTaskListResp, error) {
resp, err := d.client.OfflineTaskList(ctx, 1)
if err != nil {
return nil, err
}
return resp, nil
}
// func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) { // func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
// // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional // // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
// return nil, errs.NotImplement // return nil, errs.NotImplement

View File

@ -11,14 +11,23 @@ type Addition struct {
// define other // define other
OrderBy string `json:"order_by" type:"select" options:"file_name,file_size,user_utime,file_type"` 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"` 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)"` LimitRate float64 `json:"limit_rate,string" type:"float" default:"1" help:"limit all api request rate ([limit]r/1s)"`
AccessToken string `json:"access_token" required:"true"` AccessToken string `json:"access_token" required:"true"`
RefreshToken string `json:"refresh_token" required:"true"` RefreshToken string `json:"refresh_token" required:"true"`
} }
var config = driver.Config{ var config = driver.Config{
Name: "115 Open", Name: "115 Open",
DefaultRoot: "0", LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "0",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
} }
func init() { func init() {

View File

@ -6,12 +6,12 @@ import (
"io" "io"
"time" "time"
sdk "github.com/OpenListTeam/115-sdk-go"
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/avast/retry-go" "github.com/avast/retry-go"
sdk "github.com/OpenListTeam/115-sdk-go"
) )
func calPartSize(fileSize int64) int64 { func calPartSize(fileSize int64) int64 {
@ -121,7 +121,7 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
} else { } else {
offset += partSize offset += partSize
} }
up(float64(offset) * 100 / float64(fileSize)) up(float64(offset) / float64(fileSize))
} }
// callbackRespBytes := make([]byte, 1024) // callbackRespBytes := make([]byte, 1024)

View File

@ -19,7 +19,12 @@ type Addition struct {
var config = driver.Config{ var config = driver.Config{
Name: "115 Share", Name: "115 Share",
DefaultRoot: "0", DefaultRoot: "0",
NoUpload: true, // OnlyProxy: true,
// OnlyLocal: true,
CheckStatus: false,
Alert: "",
NoOverwriteUpload: true,
NoUpload: true,
} }
func init() { func init() {

View File

@ -188,9 +188,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, file model.FileStrea
etag := file.GetHash().GetHash(utils.MD5) etag := file.GetHash().GetHash(utils.MD5)
var err error var err error
if len(etag) < utils.MD5.Width { if len(etag) < utils.MD5.Width {
cacheFileProgress := model.UpdateProgressWithRange(up, 0, 50) _, etag, err = stream.CacheFullInTempFileAndHash(file, utils.MD5)
up = model.UpdateProgressWithRange(up, 50, 100)
_, etag, err = stream.CacheFullInTempFileAndHash(file, cacheFileProgress, utils.MD5)
if err != nil { if err != nil {
return err return err
} }

View File

@ -109,9 +109,7 @@ func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
etag := file.GetHash().GetHash(utils.MD5) etag := file.GetHash().GetHash(utils.MD5)
if len(etag) < utils.MD5.Width { if len(etag) < utils.MD5.Width {
cacheFileProgress := model.UpdateProgressWithRange(up, 0, 50) _, etag, err = stream.CacheFullInTempFileAndHash(file, utils.MD5)
up = model.UpdateProgressWithRange(up, 50, 100)
_, etag, err = stream.CacheFullInTempFileAndHash(file, cacheFileProgress, utils.MD5)
if err != nil { if err != nil {
return err return err
} }
@ -123,6 +121,7 @@ func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
if createResp.Data.Reuse { if createResp.Data.Reuse {
return nil return nil
} }
up(10)
return d.Upload(ctx, file, createResp, up) return d.Upload(ctx, file, createResp, up)
} }

View File

@ -15,10 +15,17 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "123PanShare", Name: "123PanShare",
LocalSort: true, LocalSort: true,
NoUpload: true, OnlyLocal: false,
DefaultRoot: "0", OnlyProxy: false,
NoCache: false,
NoUpload: true,
NeedMs: false,
DefaultRoot: "0",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
} }
func init() { func init() {

View File

@ -522,9 +522,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
var err error var err error
fullHash := stream.GetHash().GetHash(utils.SHA256) fullHash := stream.GetHash().GetHash(utils.SHA256)
if len(fullHash) != utils.SHA256.Width { if len(fullHash) != utils.SHA256.Width {
cacheFileProgress := model.UpdateProgressWithRange(up, 0, 50) _, fullHash, err = streamPkg.CacheFullInTempFileAndHash(stream, utils.SHA256)
up = model.UpdateProgressWithRange(up, 50, 100)
_, fullHash, err = streamPkg.CacheFullInTempFileAndHash(stream, cacheFileProgress, utils.SHA256)
if err != nil { if err != nil {
return err return err
} }

View File

@ -820,9 +820,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uplo
// 旧版本上传,家庭云不支持覆盖 // 旧版本上传,家庭云不支持覆盖
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) { func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
cacheFileProgress := model.UpdateProgressWithRange(up, 0, 50) tempFile, fileMd5, err := stream.CacheFullInTempFileAndHash(file, utils.MD5)
up = model.UpdateProgressWithRange(up, 50, 100)
tempFile, fileMd5, err := stream.CacheFullInTempFileAndHash(file, cacheFileProgress, utils.MD5)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -3,7 +3,6 @@ package alias
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
stdpath "path" stdpath "path"
"strings" "strings"
@ -12,10 +11,8 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/fs" "github.com/OpenListTeam/OpenList/v4/internal/fs"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/sign"
"github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/server/common"
) )
type Alias struct { type Alias struct {
@ -114,44 +111,20 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
return nil, errs.ObjectNotFound return nil, errs.ObjectNotFound
} }
for _, dst := range dsts { for _, dst := range dsts {
reqPath := stdpath.Join(dst, sub) link, err := d.link(ctx, dst, sub, args)
link, file, err := d.link(ctx, reqPath, args) if err == nil {
if err != nil { if !args.Redirect && len(link.URL) > 0 {
continue // 正常情况下 多并发 仅支持返回URL的驱动
} // alias套娃alias 可以让crypt、mega等驱动(不返回URL的) 支持并发
var resultLink *model.Link if d.DownloadConcurrency > 0 {
if link != nil { link.Concurrency = d.DownloadConcurrency
resultLink = &model.Link{ }
URL: link.URL, if d.DownloadPartSize > 0 {
Header: link.Header, link.PartSize = d.DownloadPartSize * utils.KB
RangeReader: link.RangeReader,
SyncClosers: utils.NewSyncClosers(link),
ContentLength: link.ContentLength,
}
if link.MFile != nil {
resultLink.RangeReader = &model.FileRangeReader{
RangeReaderIF: stream.GetRangeReaderFromMFile(file.GetSize(), link.MFile),
} }
} }
return link, nil
} else {
resultLink = &model.Link{
URL: fmt.Sprintf("%s/p%s?sign=%s",
common.GetApiUrl(ctx),
utils.EncodePath(reqPath, true),
sign.Sign(reqPath)),
}
} }
if !args.Redirect {
if d.DownloadConcurrency > 0 {
resultLink.Concurrency = d.DownloadConcurrency
}
if d.DownloadPartSize > 0 {
resultLink.PartSize = d.DownloadPartSize * utils.KB
}
}
return resultLink, nil
} }
return nil, errs.ObjectNotFound return nil, errs.ObjectNotFound
} }
@ -193,8 +166,7 @@ func (d *Alias) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
} }
if len(srcPath) == len(dstPath) { if len(srcPath) == len(dstPath) {
for i := range srcPath { for i := range srcPath {
_, e := fs.Move(ctx, *srcPath[i], *dstPath[i]) err = errors.Join(err, fs.Move(ctx, *srcPath[i], *dstPath[i]))
err = errors.Join(err, e)
} }
return err return err
} else { } else {
@ -278,13 +250,9 @@ func (d *Alias) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer,
reqPath, err := d.getReqPath(ctx, dstDir, true) reqPath, err := d.getReqPath(ctx, dstDir, true)
if err == nil { if err == nil {
if len(reqPath) == 1 { if len(reqPath) == 1 {
return fs.PutDirectly(ctx, *reqPath[0], &stream.FileStream{ return fs.PutDirectly(ctx, *reqPath[0], s)
Obj: s,
Mimetype: s.GetMimetype(),
WebPutAsTask: s.NeedStore(),
Reader: s,
})
} else { } else {
defer s.Close()
file, err := s.CacheFullInTempFile() file, err := s.CacheFullInTempFile()
if err != nil { if err != nil {
return err return err
@ -369,6 +337,14 @@ func (d *Alias) Extract(ctx context.Context, obj model.Obj, args model.ArchiveIn
for _, dst := range dsts { for _, dst := range dsts {
link, err := d.extract(ctx, dst, sub, args) link, err := d.extract(ctx, dst, sub, args)
if err == nil { if err == nil {
if !args.Redirect && len(link.URL) > 0 {
if d.DownloadConcurrency > 0 {
link.Concurrency = d.DownloadConcurrency
}
if d.DownloadPartSize > 0 {
link.PartSize = d.DownloadPartSize * utils.KB
}
}
return link, nil return link, nil
} }
} }

View File

@ -96,23 +96,37 @@ func (d *Alias) list(ctx context.Context, dst, sub string, args *fs.ListArgs) ([
}) })
} }
func (d *Alias) link(ctx context.Context, reqPath string, args model.LinkArgs) (*model.Link, model.Obj, error) { func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs) (*model.Link, error) {
reqPath := stdpath.Join(dst, sub)
// 参考 crypt 驱动
storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath) storage, reqActualPath, err := op.GetStorageAndActualPath(reqPath)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
// proxy || ftp,s3 useRawLink := len(common.GetApiUrl(ctx)) == 0 // ftps3
if !args.Redirect || len(common.GetApiUrl(ctx)) == 0 { if !useRawLink {
return op.Link(ctx, storage, reqActualPath, args) _, ok := storage.(*Alias)
useRawLink = !ok && !args.Redirect
} }
obj, err := fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true}) if useRawLink {
link, _, err := op.Link(ctx, storage, reqActualPath, args)
return link, err
}
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
if common.ShouldProxy(storage, stdpath.Base(reqPath)) { if common.ShouldProxy(storage, stdpath.Base(sub)) {
return nil, obj, nil link := &model.Link{
URL: fmt.Sprintf("%s/p%s?sign=%s",
common.GetApiUrl(ctx),
utils.EncodePath(reqPath, true),
sign.Sign(reqPath)),
}
return link, nil
} }
return op.Link(ctx, storage, reqActualPath, args) link, _, err := op.Link(ctx, storage, reqActualPath, args)
return link, err
} }
func (d *Alias) getReqPath(ctx context.Context, obj model.Obj, isParent bool) ([]*string, error) { func (d *Alias) getReqPath(ctx context.Context, obj model.Obj, isParent bool) ([]*string, error) {

View File

@ -165,7 +165,7 @@ func (d *AliDrive) Remove(ctx context.Context, obj model.Obj) error {
} }
func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error { func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error {
file := &stream.FileStream{ file := stream.FileStream{
Obj: streamer, Obj: streamer,
Reader: streamer, Reader: streamer,
Mimetype: streamer.GetMimetype(), Mimetype: streamer.GetMimetype(),
@ -209,7 +209,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
io.Closer io.Closer
}{ }{
Reader: io.MultiReader(buf, file), Reader: io.MultiReader(buf, file),
Closer: file, Closer: &file,
} }
} }
} else { } else {

View File

@ -12,7 +12,6 @@ type Addition struct {
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"` OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"` OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
UseOnlineAPI bool `json:"use_online_api" default:"true"` UseOnlineAPI bool `json:"use_online_api" default:"true"`
AlipanType string `json:"alipan_type" required:"true" type:"select" default:"default" options:"default,alipanTV"`
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/alicloud/renewapi"` APIAddress string `json:"api_url_address" default:"https://api.oplist.org/alicloud/renewapi"`
ClientID string `json:"client_id" help:"Keep it empty if you don't have one"` ClientID string `json:"client_id" help:"Keep it empty if you don't have one"`
ClientSecret string `json:"client_secret" help:"Keep it empty if you don't have one"` ClientSecret string `json:"client_secret" help:"Keep it empty if you don't have one"`
@ -25,6 +24,12 @@ type Addition struct {
var config = driver.Config{ var config = driver.Config{
Name: "AliyundriveOpen", Name: "AliyundriveOpen",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "root", DefaultRoot: "root",
NoOverwriteUpload: true, NoOverwriteUpload: true,
} }

View File

@ -194,9 +194,7 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
hash := stream.GetHash().GetHash(utils.SHA1) hash := stream.GetHash().GetHash(utils.SHA1)
if len(hash) != utils.SHA1.Width { if len(hash) != utils.SHA1.Width {
cacheFileProgress := model.UpdateProgressWithRange(up, 0, 50) _, hash, err = streamPkg.CacheFullInTempFileAndHash(stream, utils.SHA1)
up = model.UpdateProgressWithRange(up, 50, 100)
_, hash, err = streamPkg.CacheFullInTempFileAndHash(stream, cacheFileProgress, utils.SHA1)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -27,20 +27,13 @@ func (d *AliyundriveOpen) _refreshToken() (string, string, error) {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
ErrorMessage string `json:"text"` ErrorMessage string `json:"text"`
} }
// 根据AlipanType选项设置driver_txt
driverTxt := "alicloud_qr"
if d.AlipanType == "alipanTV" {
driverTxt = "alicloud_tv"
}
_, err := base.RestyClient.R(). _, err := base.RestyClient.R().
SetHeader("User-Agent", "Mozilla/5.0 (Macintosh; Apple macOS 15_5) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/138.0.0.0 Openlist/425.6.30"). SetHeader("User-Agent", "Mozilla/5.0 (Macintosh; Apple macOS 15_5) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/138.0.0.0 Openlist/425.6.30").
SetResult(&resp). SetResult(&resp).
SetQueryParams(map[string]string{ SetQueryParams(map[string]string{
"refresh_ui": d.RefreshToken, "refresh_ui": d.RefreshToken,
"server_use": "true", "server_use": "true",
"driver_txt": driverTxt, "driver_txt": "alicloud_qr",
}). }).
Get(u) Get(u)
if err != nil { if err != nil {
@ -50,7 +43,7 @@ func (d *AliyundriveOpen) _refreshToken() (string, string, error) {
if resp.ErrorMessage != "" { if resp.ErrorMessage != "" {
return "", "", fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage) return "", "", fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage)
} }
return "", "", fmt.Errorf("empty token returned from official API, a wrong refresh token may have been used") return "", "", fmt.Errorf("empty token returned from official API")
} }
return resp.RefreshToken, resp.AccessToken, nil return resp.RefreshToken, resp.AccessToken, nil
} }

View File

@ -57,12 +57,12 @@ import (
_ "github.com/OpenListTeam/OpenList/v4/drivers/seafile" _ "github.com/OpenListTeam/OpenList/v4/drivers/seafile"
_ "github.com/OpenListTeam/OpenList/v4/drivers/sftp" _ "github.com/OpenListTeam/OpenList/v4/drivers/sftp"
_ "github.com/OpenListTeam/OpenList/v4/drivers/smb" _ "github.com/OpenListTeam/OpenList/v4/drivers/smb"
_ "github.com/OpenListTeam/OpenList/v4/drivers/strm"
_ "github.com/OpenListTeam/OpenList/v4/drivers/teambition" _ "github.com/OpenListTeam/OpenList/v4/drivers/teambition"
_ "github.com/OpenListTeam/OpenList/v4/drivers/terabox" _ "github.com/OpenListTeam/OpenList/v4/drivers/terabox"
_ "github.com/OpenListTeam/OpenList/v4/drivers/thunder" _ "github.com/OpenListTeam/OpenList/v4/drivers/thunder"
_ "github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser" _ "github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser"
_ "github.com/OpenListTeam/OpenList/v4/drivers/thunderx" _ "github.com/OpenListTeam/OpenList/v4/drivers/thunderx"
_ "github.com/OpenListTeam/OpenList/v4/drivers/trainbit"
_ "github.com/OpenListTeam/OpenList/v4/drivers/url_tree" _ "github.com/OpenListTeam/OpenList/v4/drivers/url_tree"
_ "github.com/OpenListTeam/OpenList/v4/drivers/uss" _ "github.com/OpenListTeam/OpenList/v4/drivers/uss"
_ "github.com/OpenListTeam/OpenList/v4/drivers/virtual" _ "github.com/OpenListTeam/OpenList/v4/drivers/virtual"

View File

@ -55,7 +55,7 @@ func (d *BaiduNetdisk) _refreshToken() error {
if resp.ErrorMessage != "" { if resp.ErrorMessage != "" {
return fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage) return fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage)
} }
return fmt.Errorf("empty token returned from official API, a wrong refresh token may have been used") return fmt.Errorf("empty token returned from official API")
} }
d.AccessToken = resp.AccessToken d.AccessToken = resp.AccessToken
d.RefreshToken = resp.RefreshToken d.RefreshToken = resp.RefreshToken

View File

@ -32,6 +32,7 @@ func init() {
config: driver.Config{ config: driver.Config{
Name: "ChaoXingGroupDrive", Name: "ChaoXingGroupDrive",
OnlyProxy: true, OnlyProxy: true,
OnlyLocal: false,
DefaultRoot: "-1", DefaultRoot: "-1",
NoOverwriteUpload: true, NoOverwriteUpload: true,
}, },

View File

@ -20,7 +20,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/setting" "github.com/OpenListTeam/OpenList/v4/internal/setting"
"github.com/OpenListTeam/OpenList/v4/pkg/cookie" "github.com/OpenListTeam/OpenList/v4/pkg/cookie"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/avast/retry-go"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@ -241,61 +240,68 @@ func (d *Cloudreve) upRemote(ctx context.Context, stream model.FileStreamer, u U
var finish int64 = 0 var finish int64 = 0
var chunk int = 0 var chunk int = 0
DEFAULT := int64(u.ChunkSize) DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < stream.GetSize() { for finish < stream.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
left := stream.GetSize() - finish left := stream.GetSize() - finish
byteSize := min(left, DEFAULT) byteSize := min(left, DEFAULT)
err := retry.Do( utils.Log.Debugf("[Cloudreve-Remote] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
func() error { byteData := make([]byte, byteSize)
utils.Log.Debugf("[Cloudreve-Remote] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()) n, err := io.ReadFull(stream, byteData)
byteData := make([]byte, byteSize) utils.Log.Debug(err, n)
n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl+"?chunk="+strconv.Itoa(chunk),
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
if err != nil {
return err
}
req.ContentLength = byteSize
req.Header.Set("Authorization", fmt.Sprint(credential))
req.Header.Set("User-Agent", d.getUA())
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("server error: %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
var up Resp
err = json.Unmarshal(body, &up)
if err != nil {
return err
}
if up.Code != 0 {
return errors.New(up.Msg)
}
return nil
},
retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Second),
)
if err != nil { if err != nil {
return err return err
} }
finish += byteSize req, err := http.NewRequest("POST", uploadUrl+"?chunk="+strconv.Itoa(chunk),
up(float64(finish) * 100 / float64(stream.GetSize())) driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
chunk++ if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Authorization", fmt.Sprint(credential))
req.Header.Set("User-Agent", d.getUA())
err = func() error {
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return errors.New(res.Status)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
var up Resp
err = json.Unmarshal(body, &up)
if err != nil {
return err
}
if up.Code != 0 {
return errors.New(up.Msg)
}
return nil
}()
if err == nil {
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
chunk++
} else {
retryCount++
if retryCount > maxRetries {
return fmt.Errorf("upload failed after %d retries due to server errors, error: %s", maxRetries, err)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[Cloudreve-Remote] server errors while uploading, retrying after %v...", backoff)
time.Sleep(backoff)
}
} }
return nil return nil
} }
@ -304,53 +310,55 @@ func (d *Cloudreve) upOneDrive(ctx context.Context, stream model.FileStreamer, u
uploadUrl := u.UploadURLs[0] uploadUrl := u.UploadURLs[0]
var finish int64 = 0 var finish int64 = 0
DEFAULT := int64(u.ChunkSize) DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < stream.GetSize() { for finish < stream.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
left := stream.GetSize() - finish left := stream.GetSize() - finish
byteSize := min(left, DEFAULT) byteSize := min(left, DEFAULT)
err := retry.Do( utils.Log.Debugf("[Cloudreve-OneDrive] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
func() error { byteData := make([]byte, byteSize)
utils.Log.Debugf("[Cloudreve-OneDrive] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()) n, err := io.ReadFull(stream, byteData)
byteData := make([]byte, byteSize) utils.Log.Debug(err, n)
n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl,
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
if err != nil {
return err
}
req.ContentLength = byteSize
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
req.Header.Set("User-Agent", d.getUA())
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
return fmt.Errorf("server error: %d", res.StatusCode)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body)
return errors.New(string(data))
default:
return nil
}
}, retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Second),
)
if err != nil { if err != nil {
return err return err
} }
finish += byteSize req, err := http.NewRequest("PUT", uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
up(float64(finish) * 100 / float64(stream.GetSize())) if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
req.Header.Set("User-Agent", d.getUA())
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
retryCount++
if retryCount > maxRetries {
res.Body.Close()
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[Cloudreve-OneDrive] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body)
res.Body.Close()
return errors.New(string(data))
default:
res.Body.Close()
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
}
} }
// 上传成功发送回调请求 // 上传成功发送回调请求
return d.request(http.MethodPost, "/callback/onedrive/finish/"+u.SessionID, func(req *resty.Request) { return d.request(http.MethodPost, "/callback/onedrive/finish/"+u.SessionID, func(req *resty.Request) {
@ -363,54 +371,54 @@ func (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u Uploa
var chunk int = 0 var chunk int = 0
var etags []string var etags []string
DEFAULT := int64(u.ChunkSize) DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < stream.GetSize() { for finish < stream.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
left := stream.GetSize() - finish left := stream.GetSize() - finish
byteSize := min(left, DEFAULT) byteSize := min(left, DEFAULT)
err := retry.Do( utils.Log.Debugf("[Cloudreve-S3] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
func() error { byteData := make([]byte, byteSize)
utils.Log.Debugf("[Cloudreve-S3] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()) n, err := io.ReadFull(stream, byteData)
byteData := make([]byte, byteSize) utils.Log.Debug(err, n)
n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, u.UploadURLs[chunk],
driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
if err != nil {
return err
}
req.ContentLength = byteSize
req.Header.Set("User-Agent", d.getUA())
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
etag := res.Header.Get("ETag")
res.Body.Close()
switch {
case res.StatusCode != 200:
return fmt.Errorf("server error: %d", res.StatusCode)
case etag == "":
return errors.New("failed to get ETag from header")
default:
etags = append(etags, etag)
return nil
}
}, retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Second),
)
if err != nil { if err != nil {
return err return err
} }
finish += byteSize req, err := http.NewRequest("PUT", u.UploadURLs[chunk],
up(float64(finish) * 100 / float64(stream.GetSize())) driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
chunk++ if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
etag := res.Header.Get("ETag")
res.Body.Close()
switch {
case res.StatusCode != 200:
retryCount++
if retryCount > maxRetries {
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[Cloudreve-S3] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case etag == "":
return errors.New("failed to get ETag from header")
default:
retryCount = 0
etags = append(etags, etag)
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
chunk++
}
} }
// s3LikeFinishUpload // s3LikeFinishUpload
// https://github.com/cloudreve/frontend/blob/b485bf297974cbe4834d2e8e744ae7b7e5b2ad39/src/component/Uploader/core/api/index.ts#L204-L252 // https://github.com/cloudreve/frontend/blob/b485bf297974cbe4834d2e8e744ae7b7e5b2ad39/src/component/Uploader/core/api/index.ts#L204-L252
bodyBuilder := &strings.Builder{} bodyBuilder := &strings.Builder{}
@ -423,8 +431,8 @@ func (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u Uploa
)) ))
} }
bodyBuilder.WriteString("</CompleteMultipartUpload>") bodyBuilder.WriteString("</CompleteMultipartUpload>")
req, err := http.NewRequestWithContext(ctx, req, err := http.NewRequest(
http.MethodPost, "POST",
u.CompleteURL, u.CompleteURL,
strings.NewReader(bodyBuilder.String()), strings.NewReader(bodyBuilder.String()),
) )

View File

@ -26,8 +26,15 @@ type Addition struct {
var config = driver.Config{ var config = driver.Config{
Name: "Cloudreve V4", Name: "Cloudreve V4",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "cloudreve://my", DefaultRoot: "cloudreve://my",
CheckStatus: true, CheckStatus: true,
Alert: "",
NoOverwriteUpload: true, NoOverwriteUpload: true,
} }

View File

@ -47,13 +47,7 @@ type BasicConfigResp struct {
type SiteLoginConfigResp struct { type SiteLoginConfigResp struct {
LoginCaptcha bool `json:"login_captcha"` LoginCaptcha bool `json:"login_captcha"`
// RegCaptcha bool `json:"reg_captcha"` Authn bool `json:"authn"`
// ForgetCaptcha bool `json:"forget_captcha"`
// RegisterEnabled bool `json:"register_enabled"`
// TosURL string `json:"tos_url"`
// PrivacyPolicyURL string `json:"privacy_policy_url"`
// SsoDisplayName string `json:"sso_display_name"`
// OidcDisplayName string `json:"oidc_display_name"`
} }
type PrepareLoginResp struct { type PrepareLoginResp struct {

View File

@ -20,7 +20,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/setting" "github.com/OpenListTeam/OpenList/v4/internal/setting"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/avast/retry-go"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@ -96,6 +95,9 @@ func (d *CloudreveV4) login() error {
if err != nil { if err != nil {
return err return err
} }
if !siteConfig.Authn {
return errors.New("authn not support")
}
var prepareLogin PrepareLoginResp var prepareLogin PrepareLoginResp
err = d.request(http.MethodGet, "/session/prepare?email="+d.Addition.Username, nil, &prepareLogin) err = d.request(http.MethodGet, "/session/prepare?email="+d.Addition.Username, nil, &prepareLogin)
if err != nil { if err != nil {
@ -256,61 +258,68 @@ func (d *CloudreveV4) upRemote(ctx context.Context, file model.FileStreamer, u F
var finish int64 = 0 var finish int64 = 0
var chunk int = 0 var chunk int = 0
DEFAULT := int64(u.ChunkSize) DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < file.GetSize() { for finish < file.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
left := file.GetSize() - finish left := file.GetSize() - finish
byteSize := min(left, DEFAULT) byteSize := min(left, DEFAULT)
err := retry.Do( utils.Log.Debugf("[CloudreveV4-Remote] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
func() error { byteData := make([]byte, byteSize)
utils.Log.Debugf("[CloudreveV4-Remote] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize()) n, err := io.ReadFull(file, byteData)
byteData := make([]byte, byteSize) utils.Log.Debug(err, n)
n, err := io.ReadFull(file, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl+"?chunk="+strconv.Itoa(chunk),
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
if err != nil {
return err
}
req.ContentLength = byteSize
req.Header.Set("Authorization", fmt.Sprint(credential))
req.Header.Set("User-Agent", d.getUA())
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("server error: %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
var up Resp
err = json.Unmarshal(body, &up)
if err != nil {
return err
}
if up.Code != 0 {
return errors.New(up.Msg)
}
return nil
}, retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Second),
)
if err != nil { if err != nil {
return err return err
} }
finish += byteSize req, err := http.NewRequest("POST", uploadUrl+"?chunk="+strconv.Itoa(chunk),
up(float64(finish) * 100 / float64(file.GetSize())) driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
chunk++ if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Authorization", fmt.Sprint(credential))
req.Header.Set("User-Agent", d.getUA())
err = func() error {
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return errors.New(res.Status)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
var up Resp
err = json.Unmarshal(body, &up)
if err != nil {
return err
}
if up.Code != 0 {
return errors.New(up.Msg)
}
return nil
}()
if err == nil {
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(file.GetSize()))
chunk++
} else {
retryCount++
if retryCount > maxRetries {
return fmt.Errorf("upload failed after %d retries due to server errors, error: %s", maxRetries, err)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[Cloudreve-Remote] server errors while uploading, retrying after %v...", backoff)
time.Sleep(backoff)
}
} }
return nil return nil
} }
@ -319,54 +328,55 @@ func (d *CloudreveV4) upOneDrive(ctx context.Context, file model.FileStreamer, u
uploadUrl := u.UploadUrls[0] uploadUrl := u.UploadUrls[0]
var finish int64 = 0 var finish int64 = 0
DEFAULT := int64(u.ChunkSize) DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < file.GetSize() { for finish < file.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
left := file.GetSize() - finish left := file.GetSize() - finish
byteSize := min(left, DEFAULT) byteSize := min(left, DEFAULT)
err := retry.Do( utils.Log.Debugf("[CloudreveV4-OneDrive] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
func() error { byteData := make([]byte, byteSize)
utils.Log.Debugf("[CloudreveV4-OneDrive] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize()) n, err := io.ReadFull(file, byteData)
byteData := make([]byte, byteSize) utils.Log.Debug(err, n)
n, err := io.ReadFull(file, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl,
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
if err != nil {
return err
}
req.ContentLength = byteSize
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, file.GetSize()))
req.Header.Set("User-Agent", d.getUA())
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
return fmt.Errorf("server error: %d", res.StatusCode)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body)
return errors.New(string(data))
default:
return nil
}
}, retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Second),
)
if err != nil { if err != nil {
return err return err
} }
finish += byteSize req, err := http.NewRequest(http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
up(float64(finish) * 100 / float64(file.GetSize())) if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, file.GetSize()))
req.Header.Set("User-Agent", d.getUA())
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
retryCount++
if retryCount > maxRetries {
res.Body.Close()
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[CloudreveV4-OneDrive] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body)
res.Body.Close()
return errors.New(string(data))
default:
res.Body.Close()
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(file.GetSize()))
}
} }
// 上传成功发送回调请求 // 上传成功发送回调请求
return d.request(http.MethodPost, "/callback/onedrive/"+u.SessionID+"/"+u.CallbackSecret, func(req *resty.Request) { return d.request(http.MethodPost, "/callback/onedrive/"+u.SessionID+"/"+u.CallbackSecret, func(req *resty.Request) {
@ -379,54 +389,52 @@ func (d *CloudreveV4) upS3(ctx context.Context, file model.FileStreamer, u FileU
var chunk int = 0 var chunk int = 0
var etags []string var etags []string
DEFAULT := int64(u.ChunkSize) DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < file.GetSize() { for finish < file.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
left := file.GetSize() - finish left := file.GetSize() - finish
byteSize := min(left, DEFAULT) byteSize := min(left, DEFAULT)
err := retry.Do( utils.Log.Debugf("[CloudreveV4-S3] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
func() error { byteData := make([]byte, byteSize)
utils.Log.Debugf("[CloudreveV4-S3] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize()) n, err := io.ReadFull(file, byteData)
byteData := make([]byte, byteSize) utils.Log.Debug(err, n)
n, err := io.ReadFull(file, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, u.UploadUrls[chunk],
driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
if err != nil {
return err
}
req.ContentLength = byteSize
req.Header.Set("User-Agent", d.getUA())
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
etag := res.Header.Get("ETag")
res.Body.Close()
switch {
case res.StatusCode != 200:
return fmt.Errorf("server error: %d", res.StatusCode)
case etag == "":
return errors.New("failed to get ETag from header")
default:
etags = append(etags, etag)
return nil
}
},
retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Second),
)
if err != nil { if err != nil {
return err return err
} }
finish += byteSize req, err := http.NewRequest(http.MethodPut, u.UploadUrls[chunk],
up(float64(finish) * 100 / float64(file.GetSize())) driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
chunk++ if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
etag := res.Header.Get("ETag")
res.Body.Close()
switch {
case res.StatusCode != 200:
retryCount++
if retryCount > maxRetries {
return fmt.Errorf("upload failed after %d retries due to server errors", maxRetries)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("server error %d, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case etag == "":
return errors.New("failed to get ETag from header")
default:
retryCount = 0
etags = append(etags, etag)
finish += byteSize
up(float64(finish) * 100 / float64(file.GetSize()))
chunk++
}
} }
// s3LikeFinishUpload // s3LikeFinishUpload
@ -440,8 +448,8 @@ func (d *CloudreveV4) upS3(ctx context.Context, file model.FileStreamer, u FileU
)) ))
} }
bodyBuilder.WriteString("</CompleteMultipartUpload>") bodyBuilder.WriteString("</CompleteMultipartUpload>")
req, err := http.NewRequestWithContext(ctx, req, err := http.NewRequest(
http.MethodPost, "POST",
u.CompleteURL, u.CompleteURL,
strings.NewReader(bodyBuilder.String()), strings.NewReader(bodyBuilder.String()),
) )

View File

@ -1,14 +1,12 @@
package crypt package crypt
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
stdpath "path" stdpath "path"
"regexp" "regexp"
"strings" "strings"
"sync"
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/errs"
@ -112,7 +110,7 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
//return d.list(ctx, d.RemotePath, path) //return d.list(ctx, d.RemotePath, path)
//remoteFull //remoteFull
objs, err := fs.List(ctx, d.getPathForRemote(path, true), &fs.ListArgs{NoLog: true, Refresh: args.Refresh}) objs, err := fs.List(ctx, d.getPathForRemote(path, true), &fs.ListArgs{NoLog: true})
// the obj must implement the model.SetPath interface // the obj must implement the model.SetPath interface
// return objs, err // return objs, err
if err != nil { if err != nil {
@ -243,9 +241,6 @@ func (d *Crypt) Get(ctx context.Context, path string) (model.Obj, error) {
//return nil, errs.ObjectNotFound //return nil, errs.ObjectNotFound
} }
// https://github.com/rclone/rclone/blob/v1.67.0/backend/crypt/cipher.go#L37
const fileHeaderSize = 32
func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
dstDirActualPath, err := d.getActualPathForRemote(file.GetPath(), false) dstDirActualPath, err := d.getActualPathForRemote(file.GetPath(), false)
if err != nil { if err != nil {
@ -256,70 +251,61 @@ func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
return nil, err return nil, err
} }
remoteSize := remoteLink.ContentLength if remoteLink.RangeReadCloser == nil && remoteLink.MFile == nil && len(remoteLink.URL) == 0 {
if remoteSize <= 0 {
remoteSize = remoteFile.GetSize()
}
rrf, err := stream.GetRangeReaderFromLink(remoteSize, remoteLink)
if err != nil {
_ = remoteLink.Close()
return nil, fmt.Errorf("the remote storage driver need to be enhanced to support encrytion") return nil, fmt.Errorf("the remote storage driver need to be enhanced to support encrytion")
} }
remoteFileSize := remoteFile.GetSize()
mu := &sync.Mutex{} remoteClosers := utils.EmptyClosers()
var fileHeader []byte rangeReaderFunc := func(ctx context.Context, underlyingOffset, underlyingLength int64) (io.ReadCloser, error) {
rangeReaderFunc := func(ctx context.Context, offset, limit int64) (io.ReadCloser, error) { length := underlyingLength
length := limit if underlyingLength >= 0 && underlyingOffset+underlyingLength >= remoteFileSize {
if offset == 0 && limit > 0 { length = -1
mu.Lock()
if limit <= fileHeaderSize {
defer mu.Unlock()
if fileHeader != nil {
return io.NopCloser(bytes.NewReader(fileHeader[:limit])), nil
}
length = fileHeaderSize
} else if fileHeader == nil {
defer mu.Unlock()
} else {
mu.Unlock()
}
} }
rrc := remoteLink.RangeReadCloser
remoteReader, err := rrf.RangeRead(ctx, http_range.Range{Start: offset, Length: length}) if len(remoteLink.URL) > 0 {
if err != nil { var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, remoteLink)
return nil, err
}
if offset == 0 && limit > 0 {
fileHeader = make([]byte, fileHeaderSize)
n, _ := io.ReadFull(remoteReader, fileHeader)
if n != fileHeaderSize {
fileHeader = nil
return nil, fmt.Errorf("can't read data, expected=%d, got=%d", fileHeaderSize, n)
}
if limit <= fileHeaderSize {
remoteReader.Close()
return io.NopCloser(bytes.NewReader(fileHeader[:limit])), nil
} else {
remoteReader = utils.ReadCloser{
Reader: io.MultiReader(bytes.NewReader(fileHeader), remoteReader),
Closer: remoteReader,
}
}
}
return remoteReader, nil
}
return &model.Link{
RangeReader: stream.RangeReaderFunc(func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
readSeeker, err := d.cipher.DecryptDataSeek(ctx, rangeReaderFunc, httpRange.Start, httpRange.Length)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return readSeeker, nil rrc = converted
}), }
SyncClosers: utils.NewSyncClosers(remoteLink), if rrc != nil {
ContentLength: remoteSize, remoteReader, err := rrc.RangeRead(ctx, http_range.Range{Start: underlyingOffset, Length: length})
}, nil remoteClosers.AddClosers(rrc.GetClosers())
if err != nil {
return nil, err
}
return remoteReader, nil
}
if remoteLink.MFile != nil {
_, err := remoteLink.MFile.Seek(underlyingOffset, io.SeekStart)
if err != nil {
return nil, err
}
//keep reuse same MFile and close at last.
remoteClosers.Add(remoteLink.MFile)
return io.NopCloser(remoteLink.MFile), nil
}
return nil, errs.NotSupport
}
resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
readSeeker, err := d.cipher.DecryptDataSeek(ctx, rangeReaderFunc, httpRange.Start, httpRange.Length)
if err != nil {
return nil, err
}
return readSeeker, nil
}
resultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers}
resultLink := &model.Link{
RangeReadCloser: resultRangeReadCloser,
Expiration: remoteLink.Expiration,
}
return resultLink, nil
} }
func (d *Crypt) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { func (d *Crypt) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {

View File

@ -26,12 +26,17 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "Crypt", Name: "Crypt",
LocalSort: true, LocalSort: true,
OnlyProxy: true, OnlyLocal: true,
NoCache: true, OnlyProxy: true,
DefaultRoot: "/", NoCache: true,
NoLinkURL: true, NoUpload: false,
NeedMs: false,
DefaultRoot: "/",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
} }
func init() { func init() {

View File

@ -16,9 +16,17 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "Doubao", Name: "Doubao",
LocalSort: true, LocalSort: true,
DefaultRoot: "0", OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "0",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
} }
func init() { func init() {

View File

@ -14,7 +14,7 @@ import (
"math/rand" "math/rand"
"net/http" "net/http"
"net/url" "net/url"
stdpath "path" "path/filepath"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -353,7 +353,7 @@ func (d *Doubao) getUploadConfig(upConfig *UploadConfig, dataType string, file m
"ServiceId": d.UploadToken.Alice[dataType].ServiceID, "ServiceId": d.UploadToken.Alice[dataType].ServiceID,
"NeedFallback": "true", "NeedFallback": "true",
"FileSize": strconv.FormatInt(file.GetSize(), 10), "FileSize": strconv.FormatInt(file.GetSize(), 10),
"FileExtension": stdpath.Ext(file.GetName()), "FileExtension": filepath.Ext(file.GetName()),
"s": randomString(), "s": randomString(),
} }
} }
@ -524,6 +524,7 @@ func (d *Doubao) UploadByMultipart(ctx context.Context, config *UploadConfig, fi
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to cache file: %w", err) return nil, fmt.Errorf("failed to cache file: %w", err)
} }
defer tempFile.Close()
up(10.0) // 更新进度 up(10.0) // 更新进度
// 设置并行上传 // 设置并行上传
threadG, uploadCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread, threadG, uploadCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,

View File

@ -12,10 +12,17 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "DoubaoShare", Name: "DoubaoShare",
LocalSort: true, LocalSort: true,
NoUpload: true, OnlyLocal: false,
DefaultRoot: "/", OnlyProxy: false,
NoCache: false,
NoUpload: true,
NeedMs: false,
DefaultRoot: "/",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
} }
func init() { func init() {

View File

@ -18,6 +18,13 @@ type Addition struct {
var config = driver.Config{ var config = driver.Config{
Name: "Dropbox", Name: "Dropbox",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "",
NoOverwriteUpload: true, NoOverwriteUpload: true,
} }

View File

@ -39,7 +39,7 @@ func (d *Dropbox) refreshToken() error {
if resp.ErrorMessage != "" { if resp.ErrorMessage != "" {
return fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage) return fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage)
} }
return fmt.Errorf("empty token returned from official API, a wrong refresh token may have been used") return fmt.Errorf("empty token returned from official API")
} }
d.AccessToken = resp.AccessToken d.AccessToken = resp.AccessToken
d.RefreshToken = resp.RefreshToken d.RefreshToken = resp.RefreshToken

View File

@ -16,9 +16,17 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "FebBox", Name: "FebBox",
NoUpload: true, LocalSort: false,
DefaultRoot: "0", OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: true,
NeedMs: false,
DefaultRoot: "0",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
} }
func init() { func init() {

View File

@ -7,8 +7,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/jlaffaye/ftp" "github.com/jlaffaye/ftp"
) )
@ -27,7 +25,7 @@ func (d *FTP) GetAddition() driver.Additional {
} }
func (d *FTP) Init(ctx context.Context) error { func (d *FTP) Init(ctx context.Context) error {
return d._login() return d.login()
} }
func (d *FTP) Drop(ctx context.Context) error { func (d *FTP) Drop(ctx context.Context) error {
@ -66,22 +64,11 @@ func (d *FTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*m
return nil, err return nil, err
} }
remoteFile := NewFileReader(d.conn, encode(file.GetPath(), d.Encoding), file.GetSize()) r := NewFileReader(d.conn, encode(file.GetPath(), d.Encoding), file.GetSize())
if remoteFile != nil && !d.Config().OnlyLinkMFile { link := &model.Link{
return &model.Link{ MFile: r,
RangeReader: &model.FileRangeReader{
RangeReaderIF: stream.RateLimitRangeReaderFunc(stream.GetRangeReaderFromMFile(file.GetSize(), remoteFile)),
},
SyncClosers: utils.NewSyncClosers(remoteFile),
}, nil
} }
return &model.Link{ return link, nil
MFile: &stream.RateLimitFile{
File: remoteFile,
Limiter: stream.ServerDownloadLimit,
Ctx: ctx,
},
}, nil
} }
func (d *FTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { func (d *FTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {

View File

@ -31,11 +31,10 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "FTP", Name: "FTP",
LocalSort: true, LocalSort: true,
OnlyLinkMFile: true, OnlyLocal: true,
DefaultRoot: "/", DefaultRoot: "/",
NoLinkURL: true,
} }
func init() { func init() {

View File

@ -1,28 +1,18 @@
package ftp package ftp
import ( import (
"fmt"
"io" "io"
"os" "os"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/OpenListTeam/OpenList/v4/pkg/singleflight"
"github.com/jlaffaye/ftp" "github.com/jlaffaye/ftp"
) )
// do others that not defined in Driver interface // do others that not defined in Driver interface
func (d *FTP) login() error { func (d *FTP) login() error {
err, _, _ := singleflight.ErrorGroup.Do(fmt.Sprintf("FTP.login:%p", d), func() (error, error) {
return d._login(), nil
})
return err
}
func (d *FTP) _login() error {
if d.conn != nil { if d.conn != nil {
_, err := d.conn.CurrentDir() _, err := d.conn.CurrentDir()
if err == nil { if err == nil {

View File

@ -9,7 +9,6 @@ import (
"text/template" "text/template"
"time" "time"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp"
@ -97,7 +96,7 @@ func getPathCommonAncestor(a, b string) (ancestor, aChildName, bChildName, aRest
} }
func getUsername(ctx context.Context) string { func getUsername(ctx context.Context) string {
user, ok := ctx.Value(conf.UserKey).(*model.User) user, ok := ctx.Value("user").(*model.User)
if !ok { if !ok {
return "<system>" return "<system>"
} }

View File

@ -15,8 +15,17 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "GitHub Releases", Name: "GitHub Releases",
NoUpload: true, LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
} }
func init() { func init() {

View File

@ -62,7 +62,7 @@ func (d *GoogleDrive) refreshToken() error {
if resp.ErrorMessage != "" { if resp.ErrorMessage != "" {
return fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage) return fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage)
} }
return fmt.Errorf("empty token returned from official API, a wrong refresh token may have been used") return fmt.Errorf("empty token returned from official API")
} }
d.AccessToken = resp.AccessToken d.AccessToken = resp.AccessToken
d.RefreshToken = resp.RefreshToken d.RefreshToken = resp.RefreshToken

View File

@ -14,7 +14,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range" "github.com/OpenListTeam/OpenList/v4/pkg/http_range"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
@ -254,8 +253,11 @@ func (d *HalalCloud) getLink(ctx context.Context, file model.Obj, args model.Lin
chunks := getChunkSizes(result.Sizes) chunks := getChunkSizes(result.Sizes)
resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) { resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
length := httpRange.Length length := httpRange.Length
if httpRange.Length < 0 || httpRange.Start+httpRange.Length >= size { if httpRange.Length >= 0 && httpRange.Start+httpRange.Length >= size {
length = size - httpRange.Start length = -1
}
if err != nil {
return nil, fmt.Errorf("open download file failed: %w", err)
} }
oo := &openObject{ oo := &openObject{
ctx: ctx, ctx: ctx,
@ -277,9 +279,10 @@ func (d *HalalCloud) getLink(ctx context.Context, file model.Obj, args model.Lin
duration = time.Until(time.Now().Add(time.Hour)) duration = time.Until(time.Now().Add(time.Hour))
} }
resultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader}
return &model.Link{ return &model.Link{
RangeReader: stream.RateLimitRangeReaderFunc(resultRangeReader), RangeReadCloser: resultRangeReadCloser,
Expiration: &duration, Expiration: &duration,
}, nil }, nil
} }

View File

@ -18,10 +18,17 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "HalalCloud", Name: "HalalCloud",
OnlyProxy: true, LocalSort: false,
DefaultRoot: "/", OnlyLocal: true,
NoLinkURL: true, OnlyProxy: true,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "/",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
} }
func init() { func init() {

View File

@ -96,3 +96,7 @@ type SteamFile struct {
func (s *SteamFile) Read(p []byte) (n int, err error) { func (s *SteamFile) Read(p []byte) (n int, err error) {
return s.file.Read(p) return s.file.Read(p)
} }
func (s *SteamFile) Close() error {
return s.file.Close()
}

View File

@ -276,9 +276,7 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, s model.FileStreame
etag := s.GetHash().GetHash(utils.MD5) etag := s.GetHash().GetHash(utils.MD5)
var err error var err error
if len(etag) != utils.MD5.Width { if len(etag) != utils.MD5.Width {
cacheFileProgress := model.UpdateProgressWithRange(up, 0, 50) _, etag, err = stream.CacheFullInTempFileAndHash(s, utils.MD5)
up = model.UpdateProgressWithRange(up, 50, 100)
_, etag, err = stream.CacheFullInTempFileAndHash(s, cacheFileProgress, utils.MD5)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -29,8 +29,17 @@ func init() {
op.RegisterDriver(func() driver.Driver { op.RegisterDriver(func() driver.Driver {
return &ILanZou{ return &ILanZou{
config: driver.Config{ config: driver.Config{
Name: "ILanZou", Name: "ILanZou",
DefaultRoot: "0", LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "0",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
}, },
conf: Conf{ conf: Conf{
base: "https://api.ilanzou.com", base: "https://api.ilanzou.com",
@ -46,8 +55,17 @@ func init() {
op.RegisterDriver(func() driver.Driver { op.RegisterDriver(func() driver.Driver {
return &ILanZou{ return &ILanZou{
config: driver.Config{ config: driver.Config{
Name: "FeijiPan", Name: "FeijiPan",
DefaultRoot: "0", LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "0",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
}, },
conf: Conf{ conf: Conf{
base: "https://api.feijipan.com", base: "https://api.feijipan.com",

View File

@ -17,6 +17,7 @@ var config = driver.Config{
Name: "IPFS API", Name: "IPFS API",
DefaultRoot: "/", DefaultRoot: "/",
LocalSort: true, LocalSort: true,
OnlyProxy: false,
} }
func init() { func init() {

View File

@ -14,7 +14,8 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "KodBox", Name: "KodBox",
DefaultRoot: "",
} }
func init() { func init() {

View File

@ -3,7 +3,6 @@ package LenovoNasShare
import ( import (
"context" "context"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
@ -33,10 +32,6 @@ func (d *LenovoNasShare) Init(ctx context.Context) error {
if err := d.getStoken(); err != nil { if err := d.getStoken(); err != nil {
return err return err
} }
if !d.ShowRootFolder && d.RootFolderPath == "" {
list, _ := d.List(ctx, File{}, model.ListArgs{})
d.RootFolderPath = list[0].GetPath()
}
return nil return nil
} }
@ -48,26 +43,19 @@ func (d *LenovoNasShare) List(ctx context.Context, dir model.Obj, args model.Lis
d.checkStoken() // 检查stoken是否过期 d.checkStoken() // 检查stoken是否过期
files := make([]File, 0) files := make([]File, 0)
path := dir.GetPath()
if path == "" && !d.ShowRootFolder && d.RootFolderPath != "" {
path = d.RootFolderPath
}
var resp Files var resp Files
query := map[string]string{ query := map[string]string{
"code": d.ShareId, "code": d.ShareId,
"num": "5000", "num": "5000",
"stoken": d.stoken, "stoken": d.stoken,
"path": path, "path": dir.GetPath(),
} }
_, err := d.request(d.Host+"/oneproxy/api/share/v1/files", http.MethodGet, func(req *resty.Request) { _, err := d.request(d.Host+"/oneproxy/api/share/v1/files", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query) req.SetQueryParams(query)
}, &resp) }, &resp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
files = append(files, resp.Data.List...) files = append(files, resp.Data.List...)
return utils.SliceConvert(files, func(src File) (model.Obj, error) { return utils.SliceConvert(files, func(src File) (model.Obj, error) {
@ -85,10 +73,6 @@ func (d *LenovoNasShare) getStoken() error { // 获取stoken
if d.Host == "" { if d.Host == "" {
d.Host = "https://siot-share.lenovo.com.cn" d.Host = "https://siot-share.lenovo.com.cn"
} }
parts := strings.Split(d.ShareId, "/")
d.ShareId = parts[len(parts)-1]
query := map[string]string{ query := map[string]string{
"code": d.ShareId, "code": d.ShareId,
"password": d.SharePwd, "password": d.SharePwd,

View File

@ -7,16 +7,23 @@ import (
type Addition struct { type Addition struct {
driver.RootPath driver.RootPath
ShareId string `json:"share_id" required:"true" help:"The part after the last / in the shared link"` ShareId string `json:"share_id" required:"true" help:"The part after the last / in the shared link"`
SharePwd string `json:"share_pwd" required:"true" help:"The password of the shared link"` SharePwd string `json:"share_pwd" required:"true" help:"The password of the shared link"`
Host string `json:"host" required:"true" default:"https://siot-share.lenovo.com.cn" help:"You can change it to your local area network"` Host string `json:"host" required:"true" default:"https://siot-share.lenovo.com.cn" help:"You can change it to your local area network"`
ShowRootFolder bool `json:"show_root_folder" default:"true"`
} }
var config = driver.Config{ var config = driver.Config{
Name: "LenovoNasShare", Name: "LenovoNasShare",
LocalSort: true, LocalSort: true,
NoUpload: true, OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: true,
NeedMs: false,
DefaultRoot: "",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
} }
func init() { func init() {

View File

@ -19,7 +19,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/sign" "github.com/OpenListTeam/OpenList/v4/internal/sign"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/server/common" "github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/OpenListTeam/times" "github.com/OpenListTeam/times"
@ -173,6 +172,19 @@ func (d *Local) FileInfoToObj(ctx context.Context, f fs.FileInfo, reqPath string
} }
return &file return &file
} }
func (d *Local) GetMeta(ctx context.Context, path string) (model.Obj, error) {
f, err := os.Stat(path)
if err != nil {
return nil, err
}
file := d.FileInfoToObj(ctx, f, path, path)
//h := "123123"
//if s, ok := f.(model.SetHash); ok && file.GetHash() == ("","") {
// s.SetHash(h,"SHA1")
//}
return file, nil
}
func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) { func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {
path = filepath.Join(d.GetRootPath(), path) path = filepath.Join(d.GetRootPath(), path)
@ -208,7 +220,7 @@ func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {
func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
fullPath := file.GetPath() fullPath := file.GetPath()
link := &model.Link{} var link model.Link
if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" { if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" {
var buf *bytes.Buffer var buf *bytes.Buffer
var thumbPath *string var thumbPath *string
@ -228,17 +240,10 @@ func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Get thumbnail file size for Content-Length
stat, err := open.Stat()
if err != nil {
open.Close()
return nil, err
}
link.ContentLength = int64(stat.Size())
link.MFile = open link.MFile = open
} else { } else {
link.MFile = bytes.NewReader(buf.Bytes()) link.MFile = model.NewNopMFile(bytes.NewReader(buf.Bytes()))
link.ContentLength = int64(buf.Len()) //link.Header.Set("Content-Length", strconv.Itoa(buf.Len()))
} }
} else { } else {
open, err := os.Open(fullPath) open, err := os.Open(fullPath)
@ -247,14 +252,7 @@ func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
} }
link.MFile = open link.MFile = open
} }
if link.MFile != nil && !d.Config().OnlyLinkMFile { return &link, nil
link.AddIfCloser(link.MFile)
link.RangeReader = &model.FileRangeReader{
RangeReaderIF: stream.GetRangeReaderFromMFile(file.GetSize(), link.MFile),
}
link.MFile = nil
}
return link, nil
} }
func (d *Local) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { func (d *Local) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {

View File

@ -17,12 +17,11 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "Local", Name: "Local",
OnlyLinkMFile: false, OnlyLocal: true,
LocalSort: true, LocalSort: true,
NoCache: true, NoCache: true,
DefaultRoot: "/", DefaultRoot: "/",
NoLinkURL: true,
} }
func init() { func init() {

View File

@ -184,6 +184,9 @@ func (d *MediaTrack) Put(ctx context.Context, dstDir model.Obj, file model.FileS
if err != nil { if err != nil {
return err return err
} }
defer func() {
_ = tempFile.Close()
}()
uploader := s3manager.NewUploader(s) uploader := s3manager.NewUploader(s)
if file.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize { if file.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
uploader.PartSize = file.GetSize() / (s3manager.MaxUploadParts - 1) uploader.PartSize = file.GetSize() / (s3manager.MaxUploadParts - 1)

View File

@ -14,7 +14,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/t3rm1n4l/go-mega" "github.com/t3rm1n4l/go-mega"
@ -96,8 +95,8 @@ func (d *Mega) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
size := file.GetSize() size := file.GetSize()
resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) { resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
length := httpRange.Length length := httpRange.Length
if httpRange.Length < 0 || httpRange.Start+httpRange.Length >= size { if httpRange.Length >= 0 && httpRange.Start+httpRange.Length >= size {
length = size - httpRange.Start length = -1
} }
var down *mega.Download var down *mega.Download
err := utils.Retry(3, time.Second, func() (err error) { err := utils.Retry(3, time.Second, func() (err error) {
@ -115,9 +114,11 @@ func (d *Mega) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
return readers.NewLimitedReadCloser(oo, length), nil return readers.NewLimitedReadCloser(oo, length), nil
} }
return &model.Link{ resultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader}
RangeReader: stream.RateLimitRangeReaderFunc(resultRangeReader), resultLink := &model.Link{
}, nil RangeReadCloser: resultRangeReadCloser,
}
return resultLink, nil
} }
return nil, fmt.Errorf("unable to convert dir to mega n") return nil, fmt.Errorf("unable to convert dir to mega n")
} }

View File

@ -18,8 +18,7 @@ type Addition struct {
var config = driver.Config{ var config = driver.Config{
Name: "Mega_nz", Name: "Mega_nz",
LocalSort: true, LocalSort: true,
OnlyProxy: true, OnlyLocal: true,
NoLinkURL: true,
} }
func init() { func init() {

View File

@ -15,8 +15,17 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "Misskey", Name: "Misskey",
DefaultRoot: "/", LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "/",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
} }
func init() { func init() {

View File

@ -27,7 +27,8 @@ func (a *Addition) GetRootId() string {
} }
var config = driver.Config{ var config = driver.Config{
Name: "MoPan", Name: "MoPan",
// DefaultRoot: "root, / or other",
CheckStatus: true, CheckStatus: true,
Alert: "warning|This network disk may store your password in clear text. Please set your password carefully", Alert: "warning|This network disk may store your password in clear text. Please set your password carefully",
} }

View File

@ -73,7 +73,7 @@ func (d *NeteaseMusic) List(ctx context.Context, dir model.Obj, args model.ListA
func (d *NeteaseMusic) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *NeteaseMusic) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if lrc, ok := file.(*LyricObj); ok { if lrc, ok := file.(*LyricObj); ok {
if args.Type == "parsed" && !args.Redirect { if args.Type == "parsed" {
return lrc.getLyricLink(), nil return lrc.getLyricLink(), nil
} else { } else {
return lrc.getProxyLink(ctx), nil return lrc.getProxyLink(ctx), nil

View File

@ -2,6 +2,7 @@ package netease_music
import ( import (
"context" "context"
"io"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@ -10,7 +11,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/sign" "github.com/OpenListTeam/OpenList/v4/internal/sign"
"github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/pkg/http_range"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random" "github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
"github.com/OpenListTeam/OpenList/v4/server/common" "github.com/OpenListTeam/OpenList/v4/server/common"
@ -54,9 +55,16 @@ func (lrc *LyricObj) getProxyLink(ctx context.Context) *model.Link {
} }
func (lrc *LyricObj) getLyricLink() *model.Link { func (lrc *LyricObj) getLyricLink() *model.Link {
reader := strings.NewReader(lrc.lyric)
return &model.Link{ return &model.Link{
RangeReader: &model.FileRangeReader{ RangeReadCloser: &model.RangeReadCloser{
RangeReaderIF: stream.GetRangeReaderFromMFile(int64(len(lrc.lyric)), strings.NewReader(lrc.lyric)), RangeReader: func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
if httpRange.Length < 0 {
return io.NopCloser(reader), nil
}
sr := io.NewSectionReader(reader, httpRange.Start, httpRange.Length)
return io.NopCloser(sr), nil
},
}, },
} }
} }

View File

@ -16,7 +16,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/avast/retry-go"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@ -97,7 +96,7 @@ func (d *Onedrive) _refreshToken() error {
if resp.ErrorMessage != "" { if resp.ErrorMessage != "" {
return fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage) return fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage)
} }
return fmt.Errorf("empty token returned from official API, a wrong refresh token may have been used") return fmt.Errorf("empty token returned from official API")
} }
d.AccessToken = resp.AccessToken d.AccessToken = resp.AccessToken
d.RefreshToken = resp.RefreshToken d.RefreshToken = resp.RefreshToken
@ -231,7 +230,7 @@ func toAPIMetadata(stream model.FileStreamer) Metadata {
func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/createUploadSession" url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/createUploadSession"
metadata := map[string]any{"item": toAPIMetadata(stream)} metadata := map[string]interface{}{"item": toAPIMetadata(stream)}
res, err := d.Request(url, http.MethodPost, func(req *resty.Request) { res, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(metadata).SetContext(ctx) req.SetBody(metadata).SetContext(ctx)
}, nil) }, nil)
@ -241,53 +240,54 @@ func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.Fil
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString() uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
var finish int64 = 0 var finish int64 = 0
DEFAULT := d.ChunkSize * 1024 * 1024 DEFAULT := d.ChunkSize * 1024 * 1024
retryCount := 0
maxRetries := 3
for finish < stream.GetSize() { for finish < stream.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
left := stream.GetSize() - finish left := stream.GetSize() - finish
byteSize := min(left, DEFAULT) byteSize := min(left, DEFAULT)
err = retry.Do( utils.Log.Debugf("[Onedrive] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
func() error { byteData := make([]byte, byteSize)
utils.Log.Debugf("[Onedrive] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()) n, err := io.ReadFull(stream, byteData)
byteData := make([]byte, byteSize) utils.Log.Debug(err, n)
n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl,
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
if err != nil {
return err
}
req.ContentLength = byteSize
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
return fmt.Errorf("server error: %d", res.StatusCode)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body)
return errors.New(string(data))
default:
return nil
}
},
retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Second),
)
if err != nil { if err != nil {
return err return err
} }
finish += byteSize req, err := http.NewRequest("PUT", uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
up(float64(finish) * 100 / float64(stream.GetSize())) if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
retryCount++
if retryCount > maxRetries {
res.Body.Close()
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[Onedrive] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body)
res.Body.Close()
return errors.New(string(data))
default:
res.Body.Close()
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
}
} }
return nil return nil
} }

View File

@ -16,7 +16,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/avast/retry-go"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@ -41,7 +40,7 @@ var onedriveHostMap = map[string]Host{
} }
func (d *OnedriveAPP) GetMetaUrl(auth bool, path string) string { func (d *OnedriveAPP) GetMetaUrl(auth bool, path string) string {
host := onedriveHostMap[d.Region] host, _ := onedriveHostMap[d.Region]
path = utils.EncodePath(path, true) path = utils.EncodePath(path, true)
if auth { if auth {
return host.Oauth return host.Oauth
@ -155,53 +154,54 @@ func (d *OnedriveAPP) upBig(ctx context.Context, dstDir model.Obj, stream model.
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString() uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
var finish int64 = 0 var finish int64 = 0
DEFAULT := d.ChunkSize * 1024 * 1024 DEFAULT := d.ChunkSize * 1024 * 1024
retryCount := 0
maxRetries := 3
for finish < stream.GetSize() { for finish < stream.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
left := stream.GetSize() - finish left := stream.GetSize() - finish
byteSize := min(left, DEFAULT) byteSize := min(left, DEFAULT)
err = retry.Do( utils.Log.Debugf("[OnedriveAPP] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
func() error { byteData := make([]byte, byteSize)
utils.Log.Debugf("[OnedriveAPP] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()) n, err := io.ReadFull(stream, byteData)
byteData := make([]byte, byteSize) utils.Log.Debug(err, n)
n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl,
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
if err != nil {
return err
}
req.ContentLength = byteSize
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
return fmt.Errorf("server error: %d", res.StatusCode)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body)
return errors.New(string(data))
default:
return nil
}
},
retry.Attempts(3),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Second),
)
if err != nil { if err != nil {
return err return err
} }
finish += byteSize req, err := http.NewRequest("PUT", uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
up(float64(finish) * 100 / float64(stream.GetSize())) if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
retryCount++
if retryCount > maxRetries {
res.Body.Close()
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[OnedriveAPP] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body)
res.Body.Close()
return errors.New(string(data))
default:
res.Body.Close()
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
}
} }
return nil return nil
} }

View File

@ -22,6 +22,7 @@ var config = driver.Config{
OnlyProxy: true, OnlyProxy: true,
NoUpload: true, NoUpload: true,
DefaultRoot: "/", DefaultRoot: "/",
CheckStatus: false,
} }
func init() { func init() {

View File

@ -17,8 +17,9 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "PikPak", Name: "PikPak",
LocalSort: true, LocalSort: true,
DefaultRoot: "",
} }
func init() { func init() {

View File

@ -15,9 +15,10 @@ type Addition struct {
} }
var config = driver.Config{ var config = driver.Config{
Name: "PikPakShare", Name: "PikPakShare",
LocalSort: true, LocalSort: true,
NoUpload: true, NoUpload: true,
DefaultRoot: "",
} }
func init() { func init() {

View File

@ -6,10 +6,6 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"hash"
"io"
"net/http"
"github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/errs"
@ -17,6 +13,9 @@ import (
streamPkg "github.com/OpenListTeam/OpenList/v4/internal/stream" streamPkg "github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"hash"
"io"
"net/http"
) )
type QuarkOpen struct { type QuarkOpen struct {
@ -158,9 +157,7 @@ func (d *QuarkOpen) Put(ctx context.Context, dstDir model.Obj, stream model.File
} }
if len(writers) > 0 { if len(writers) > 0 {
cacheFileProgress := model.UpdateProgressWithRange(up, 0, 50) _, err := streamPkg.CacheFullInTempFileAndWriter(stream, io.MultiWriter(writers...))
up = model.UpdateProgressWithRange(up, 50, 100)
_, err := streamPkg.CacheFullInTempFileAndWriter(stream, cacheFileProgress, io.MultiWriter(writers...))
if err != nil { if err != nil {
return err return err
} }

View File

@ -28,7 +28,7 @@ func init() {
return &QuarkOpen{ return &QuarkOpen{
config: driver.Config{ config: driver.Config{
Name: "QuarkOpen", Name: "QuarkOpen",
OnlyProxy: true, OnlyLocal: true,
DefaultRoot: "0", DefaultRoot: "0",
NoOverwriteUpload: true, NoOverwriteUpload: true,
}, },

View File

@ -458,7 +458,7 @@ func (d *QuarkOpen) _refreshToken() (string, string, error) {
if resp.ErrorMessage != "" { if resp.ErrorMessage != "" {
return "", "", fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage) return "", "", fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage)
} }
return "", "", fmt.Errorf("empty token returned from official API, a wrong refresh token may have been used") return "", "", fmt.Errorf("empty token returned from official API")
} }
return resp.RefreshToken, resp.AccessToken, nil return resp.RefreshToken, resp.AccessToken, nil
} }

View File

@ -36,15 +36,6 @@ func (d *QuarkOrUC) GetAddition() driver.Additional {
func (d *QuarkOrUC) Init(ctx context.Context) error { func (d *QuarkOrUC) Init(ctx context.Context) error {
_, err := d.request("/config", http.MethodGet, nil, nil) _, err := d.request("/config", http.MethodGet, nil, nil)
if err == nil {
if d.AdditionVersion != 2 {
d.AdditionVersion = 2
if !d.UseTransCodingAddress && len(d.DownProxyURL) == 0 {
d.WebProxy = true
d.WebdavPolicy = "native_proxy"
}
}
}
return err return err
} }
@ -144,9 +135,7 @@ func (d *QuarkOrUC) Put(ctx context.Context, dstDir model.Obj, stream model.File
} }
if len(writers) > 0 { if len(writers) > 0 {
cacheFileProgress := model.UpdateProgressWithRange(up, 0, 50) _, err := streamPkg.CacheFullInTempFileAndWriter(stream, io.MultiWriter(writers...))
up = model.UpdateProgressWithRange(up, 50, 100)
_, err := streamPkg.CacheFullInTempFileAndWriter(stream, cacheFileProgress, io.MultiWriter(writers...))
if err != nil { if err != nil {
return err return err
} }

View File

@ -12,7 +12,6 @@ type Addition struct {
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"` OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
UseTransCodingAddress bool `json:"use_transcoding_address" help:"You can watch the transcoded video and support 302 redirection" required:"true" default:"false"` UseTransCodingAddress bool `json:"use_transcoding_address" help:"You can watch the transcoded video and support 302 redirection" required:"true" default:"false"`
OnlyListVideoFile bool `json:"only_list_video_file" default:"false"` OnlyListVideoFile bool `json:"only_list_video_file" default:"false"`
AdditionVersion int
} }
type Conf struct { type Conf struct {
@ -27,6 +26,7 @@ func init() {
return &QuarkOrUC{ return &QuarkOrUC{
config: driver.Config{ config: driver.Config{
Name: "Quark", Name: "Quark",
OnlyLocal: false,
DefaultRoot: "0", DefaultRoot: "0",
NoOverwriteUpload: true, NoOverwriteUpload: true,
}, },
@ -42,7 +42,7 @@ func init() {
return &QuarkOrUC{ return &QuarkOrUC{
config: driver.Config{ config: driver.Config{
Name: "UC", Name: "UC",
OnlyProxy: true, OnlyLocal: true,
DefaultRoot: "0", DefaultRoot: "0",
NoOverwriteUpload: true, NoOverwriteUpload: true,
}, },

View File

@ -149,12 +149,7 @@ func (d *QuarkOrUC) getTranscodingLink(file model.Obj) (*model.Link, error) {
return nil, err return nil, err
} }
return &model.Link{ return &model.Link{URL: resp.Data.VideoList[0].VideoInfo.URL}, nil
URL: resp.Data.VideoList[0].VideoInfo.URL,
ContentLength: resp.Data.VideoList[0].VideoInfo.Size,
Concurrency: 3,
PartSize: 10 * utils.MB,
}, nil
} }
func (d *QuarkOrUC) upPre(file model.FileStreamer, parentId string) (UpPreResp, error) { func (d *QuarkOrUC) upPre(file model.FileStreamer, parentId string) (UpPreResp, error) {

View File

@ -126,13 +126,25 @@ func (d *QuarkUCTV) List(ctx context.Context, dir model.Obj, args model.ListArgs
} }
func (d *QuarkUCTV) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { func (d *QuarkUCTV) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
f := file.(*Files) var fileLink FileLink
_, err := d.request(ctx, "/file", "GET", func(req *resty.Request) {
if d.Addition.VideoLinkMethod == "streaming" && f.Category == 1 && f.Size > 0 { req.SetQueryParams(map[string]string{
return d.getTranscodingLink(ctx, file) "method": "download",
"group_by": "source",
"fid": file.GetID(),
"resolution": "low,normal,high,super,2k,4k",
"support": "dolby_vision",
})
}, &fileLink)
if err != nil {
return nil, err
} }
return d.getDownloadLink(ctx, file) return &model.Link{
URL: fileLink.Data.DownloadURL,
Concurrency: 3,
PartSize: 10 * utils.MB,
}, nil
} }
func (d *QuarkUCTV) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { func (d *QuarkUCTV) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {

View File

@ -14,8 +14,6 @@ type Addition struct {
DeviceID string `json:"device_id" required:"false" default:""` DeviceID string `json:"device_id" required:"false" default:""`
// 登陆所用的数据 无需手动填写 // 登陆所用的数据 无需手动填写
QueryToken string `json:"query_token" required:"false" default:"" help:"don't edit'"` QueryToken string `json:"query_token" required:"false" default:"" help:"don't edit'"`
// 视频文件链接获取方式 download(可获取源视频) or streaming(获取转码后的视频)
VideoLinkMethod string `json:"link_method" required:"true" type:"select" options:"download,streaming" default:"download"`
} }
type Conf struct { type Conf struct {
@ -32,6 +30,7 @@ func init() {
return &QuarkUCTV{ return &QuarkUCTV{
config: driver.Config{ config: driver.Config{
Name: "QuarkTV", Name: "QuarkTV",
OnlyLocal: false,
DefaultRoot: "0", DefaultRoot: "0",
NoOverwriteUpload: true, NoOverwriteUpload: true,
NoUpload: true, NoUpload: true,
@ -40,8 +39,8 @@ func init() {
api: "https://open-api-drive.quark.cn", api: "https://open-api-drive.quark.cn",
clientID: "d3194e61504e493eb6222857bccfed94", clientID: "d3194e61504e493eb6222857bccfed94",
signKey: "kw2dvtd7p4t3pjl2d9ed9yc8yej8kw2d", signKey: "kw2dvtd7p4t3pjl2d9ed9yc8yej8kw2d",
appVer: "1.8.2.2", appVer: "1.5.6",
channel: "GENERAL", channel: "CP",
codeApi: "http://api.extscreen.com/quarkdrive", codeApi: "http://api.extscreen.com/quarkdrive",
}, },
} }
@ -50,6 +49,7 @@ func init() {
return &QuarkUCTV{ return &QuarkUCTV{
config: driver.Config{ config: driver.Config{
Name: "UCTV", Name: "UCTV",
OnlyLocal: false,
DefaultRoot: "0", DefaultRoot: "0",
NoOverwriteUpload: true, NoOverwriteUpload: true,
NoUpload: true, NoUpload: true,
@ -58,7 +58,7 @@ func init() {
api: "https://open-api-drive.uc.cn", api: "https://open-api-drive.uc.cn",
clientID: "5acf882d27b74502b7040b0c65519aa7", clientID: "5acf882d27b74502b7040b0c65519aa7",
signKey: "l3srvtd7p42l0d0x1u8d7yc8ye9kki4d", signKey: "l3srvtd7p42l0d0x1u8d7yc8ye9kki4d",
appVer: "1.7.2.2", appVer: "1.6.5",
channel: "UCTVOFFICIALWEB", channel: "UCTVOFFICIALWEB",
codeApi: "http://api.extscreen.com/ucdrive", codeApi: "http://api.extscreen.com/ucdrive",
}, },

View File

@ -92,32 +92,7 @@ type FilesData struct {
} `json:"data"` } `json:"data"`
} }
type StreamingFileLink struct { type FileLink struct {
CommonRsp
Data struct {
DefaultResolution string `json:"default_resolution"`
LastPlayTime int `json:"last_play_time"`
VideoInfo []struct {
Resolution string `json:"resolution"`
Accessable int `json:"accessable"`
TransStatus string `json:"trans_status"`
Duration int `json:"duration,omitempty"`
Size int64 `json:"size,omitempty"`
Format string `json:"format,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
URL string `json:"url,omitempty"`
Bitrate float64 `json:"bitrate,omitempty"`
DolbyVision struct {
Profile int `json:"profile"`
Level int `json:"level"`
} `json:"dolby_vision,omitempty"`
} `json:"video_info"`
AudioInfo []interface{} `json:"audio_info"`
} `json:"data"`
}
type DownloadFileLink struct {
CommonRsp CommonRsp
Data struct { Data struct {
Fid string `json:"fid"` Fid string `json:"fid"`

View File

@ -10,8 +10,6 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
@ -212,48 +210,3 @@ func (d *QuarkUCTV) generateReqSign(method string, pathname string, key string)
return timestamp, xPanTokenHex, reqIDHex return timestamp, xPanTokenHex, reqIDHex
} }
func (d *QuarkUCTV) getTranscodingLink(ctx context.Context, file model.Obj) (*model.Link, error) {
var fileLink StreamingFileLink
_, err := d.request(ctx, "/file", "GET", func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"method": "streaming",
"group_by": "source",
"fid": file.GetID(),
"resolution": "low,normal,high,super,2k,4k",
"support": "dolby_vision",
})
}, &fileLink)
if err != nil {
return nil, err
}
return &model.Link{
URL: fileLink.Data.VideoInfo[0].URL,
Concurrency: 3,
PartSize: 10 * utils.MB,
ContentLength: fileLink.Data.VideoInfo[0].Size,
}, nil
}
func (d *QuarkUCTV) getDownloadLink(ctx context.Context, file model.Obj) (*model.Link, error) {
var fileLink DownloadFileLink
_, err := d.request(ctx, "/file", "GET", func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"method": "download",
"group_by": "source",
"fid": file.GetID(),
"resolution": "low,normal,high,super,2k,4k",
"support": "dolby_vision",
})
}, &fileLink)
if err != nil {
return nil, err
}
return &model.Link{
URL: fileLink.Data.DownloadURL,
Concurrency: 3,
PartSize: 10 * utils.MB,
}, nil
}

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io"
"net/url" "net/url"
stdpath "path" stdpath "path"
"strings" "strings"
@ -97,9 +98,6 @@ func (d *S3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*mo
} }
req, _ := d.linkClient.GetObjectRequest(input) req, _ := d.linkClient.GetObjectRequest(input)
if req == nil {
return nil, fmt.Errorf("failed to create GetObject request")
}
var link model.Link var link model.Link
var err error var err error
if d.CustomHost != "" { if d.CustomHost != "" {
@ -109,30 +107,8 @@ func (d *S3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*mo
err = req.Build() err = req.Build()
link.URL = req.HTTPRequest.URL.String() link.URL = req.HTTPRequest.URL.String()
} }
if err != nil {
return nil, fmt.Errorf("failed to generate link URL: %w", err)
}
if d.RemoveBucket { if d.RemoveBucket {
parsedURL, parseErr := url.Parse(link.URL) link.URL = strings.Replace(link.URL, "/"+d.Bucket, "", 1)
if parseErr != nil {
log.Errorf("Failed to parse URL for bucket removal: %v, URL: %s", parseErr, link.URL)
return nil, fmt.Errorf("failed to parse URL for bucket removal: %w", parseErr)
}
path := parsedURL.Path
bucketPrefix := "/" + d.Bucket
if strings.HasPrefix(path, bucketPrefix) {
path = strings.TrimPrefix(path, bucketPrefix)
if path == "" {
path = "/"
}
parsedURL.Path = path
link.URL = parsedURL.String()
log.Debugf("Removed bucket '%s' from URL path: %s -> %s", d.Bucket, bucketPrefix, path)
} else {
log.Warnf("URL path does not contain expected bucket prefix '%s': %s", bucketPrefix, path)
}
} }
} else { } else {
if common.ShouldProxy(d, fileName) { if common.ShouldProxy(d, fileName) {
@ -157,7 +133,7 @@ func (d *S3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) e
Name: getPlaceholderName(d.Placeholder), Name: getPlaceholderName(d.Placeholder),
Modified: time.Now(), Modified: time.Now(),
}, },
Reader: bytes.NewReader([]byte{}), Reader: io.NopCloser(bytes.NewReader([]byte{})),
Mimetype: "application/octet-stream", Mimetype: "application/octet-stream",
}, func(float64) {}) }, func(float64) {})
} }

View File

@ -8,7 +8,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/pkg/sftp" "github.com/pkg/sftp"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -30,7 +29,7 @@ func (d *SFTP) GetAddition() driver.Additional {
} }
func (d *SFTP) Init(ctx context.Context) error { func (d *SFTP) Init(ctx context.Context) error {
return d._initClient() return d.initClient()
} }
func (d *SFTP) Drop(ctx context.Context) error { func (d *SFTP) Drop(ctx context.Context) error {
@ -63,21 +62,10 @@ func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
if err != nil { if err != nil {
return nil, err return nil, err
} }
if remoteFile != nil && !d.Config().OnlyLinkMFile { link := &model.Link{
return &model.Link{ MFile: remoteFile,
RangeReader: &model.FileRangeReader{
RangeReaderIF: stream.RateLimitRangeReaderFunc(stream.GetRangeReaderFromMFile(file.GetSize(), remoteFile)),
},
SyncClosers: utils.NewSyncClosers(remoteFile),
}, nil
} }
return &model.Link{ return link, nil
MFile: &stream.RateLimitFile{
File: remoteFile,
Limiter: stream.ServerDownloadLimit,
Ctx: ctx,
},
}, nil
} }
func (d *SFTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { func (d *SFTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {

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