Compare commits

..

15 Commits

Author SHA1 Message Date
renovate[bot] 2bdf16d596 chore(deps): pin dependencies (#2736)
* chore(deps): pin dependencies

* chore: exclude docker from renovate

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Yinan Qin <elysia-best@simplelinux.cn.eu.org>
Co-authored-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2026-09-01 22:58:15 +08:00
renovate[bot] f8ebaec4f1 fix(deps): update module google.golang.org/grpc to v1.85.0-dev (#3015)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 22:58:10 +08:00
renovate[bot] fc23f4e781 fix(deps): update module github.com/shirou/gopsutil/v4 to v4.26.8 (#3014)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 22:58:05 +08:00
renovate[bot] 3ea9984aea fix(deps): update module golang.org/x/image to v0.45.0 (#2994)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 22:58:00 +08:00
renovate[bot] cc11f354f1 fix(deps): update module github.com/go-webauthn/webauthn to v0.18.0 (#2988)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2026-09-01 22:57:54 +08:00
renovate[bot] 8869874b76 fix(deps): update module github.com/bmatcuk/doublestar/v4 to v4.10.0 (#2987)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 22:57:48 +08:00
ShenLin 523af855ba fix(auth): secure SSO account binding
- Issue and verify short-lived SSO binding state and proof tokens
- Bind provider callbacks to an HttpOnly browser session cookie
- Reject invalid or already-associated SSO identities during profile updates

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com>
2026-09-01 18:45:40 +08:00
ShenLin bba3516693 fix(upload): authorize direct upload destinations
- Resolve and authorize the canonical destination from the request payload
- Reject upload capabilities that cross virtual storage mount boundaries
- Remove the unrelated File-Path middleware authorization check

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com>
2026-09-01 18:45:39 +08:00
ShenLin f244adf60e fix(meta): enforce case-insensitive access controls
- Add an invalidated metadata snapshot for case-insensitive fallback lookups
- Enforce segment-aware metadata coverage for passwords and download signatures
- Add regression tests while preserving case-sensitive write authorization

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com>
2026-09-01 18:45:39 +08:00
abcqqoo ce87b45d2d fix(cmd/start): use absolute executable path for child process (#2125)
* fix(cmd/start): use absolute executable path for child process

* fix(cmd/start): detect force-bin-dir flag variants

---------

Co-authored-by: Zoe Lee <zoelee@gmail.com>
2026-09-01 18:15:06 +08:00
renovate[bot] 4031b31837 fix(deps): update module github.com/azure/azure-sdk-for-go/sdk/azcore to v1.23.1 (#2985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 18:01:59 +08:00
renovate[bot] 9b34ca5bf1 fix(deps): update module github.com/coreos/go-oidc to v2.5.0+incompatible (#2711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 17:59:51 +08:00
renovate[bot] 666b1a039d fix(deps): update module golang.org/x/crypto to v0.55.0 (#2993)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 17:57:45 +08:00
hugcabbage 8f9a09d359 fix(drivers/guangyapan): report multipart upload progress to copy task (#2989)
multipartUploadToOSS passed the UpdateProgress callback into
stream.NewStreamSectionReader, which ignores the up argument, so the
callback was never invoked while uploading parts. As a result,
cross-storage copy tasks targeting GuangYaPan stayed at 0% and showed no
progress bar or speed in the task list.

Fix by tracking the uploaded byte count and invoking up after every
part is uploaded, keeping consistent with other drivers' upload flow.

Co-authored-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
2026-09-01 17:22:50 +08:00
renovate[bot] 394bb8f80e chore(deps): update go toolchain directive to v1.26.5 (#2733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-30 17:25:38 +08:00
34 changed files with 483 additions and 834 deletions
+7 -7
View File
@@ -18,13 +18,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Create or update ref
id: create-or-update-ref
uses: ovsds/create-or-update-ref-action@v1
uses: ovsds/create-or-update-ref-action@1157b2991820188d5a8e6f22656775d1d171f762 # v1
with:
ref: tags/beta
sha: ${{ github.sha }}
@@ -51,7 +51,7 @@ jobs:
gh release upload beta "CHANGELOG.md" --clobber
- name: Upload assets to github artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: beta changelog
path: ${{ github.workspace }}/CHANGELOG.md
@@ -108,14 +108,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version: "1.26.4"
go-version: "1.27.0"
- name: Setup web
run: bash build.sh dev web
@@ -124,7 +124,7 @@ jobs:
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Build
uses: OpenListTeam/cgo-actions@v1.2.5
uses: OpenListTeam/cgo-actions@6fcace5934c36d70503dba06e2396bb58b766130 # v1.2.5
with:
targets: ${{ matrix.target }}
flags: ${{ matrix.flags || '-ldflags=' }}
@@ -185,7 +185,7 @@ jobs:
echo "cleaned_target=$CLEANED_TARGET" >> $GITHUB_ENV
- name: Upload assets to github artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: beta builds for ${{ env.cleaned_target }}
path: ${{ github.workspace }}/build/compress/*
+4 -4
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: benjlevesque/short-sha@dbe07338b37c456ce06d23409b35a56a7815eef4 # v4.0
id: short-sha
@@ -33,7 +33,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version: "1.26.4"
go-version: "1.27.0"
- name: Setup web
run: bash build.sh dev web
@@ -42,7 +42,7 @@ jobs:
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Build
uses: OpenListTeam/cgo-actions@v1.2.5
uses: OpenListTeam/cgo-actions@6fcace5934c36d70503dba06e2396bb58b766130 # v1.2.5
with:
targets: ${{ matrix.target }}
flags: ${{ contains(matrix.target, '-musl') && '-ldflags=-linkmode external -extldflags ''-static -fpic''' || '-ldflags=' }}
@@ -69,7 +69,7 @@ jobs:
fi
- name: Upload artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openlist_${{ steps.short-sha.outputs.sha }}_${{ matrix.target }}
path: build/*
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
if: github.event_name == 'issues'
steps:
- name: Check issue for unchecked tasks and reply
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
let comment = "";
@@ -81,7 +81,7 @@ jobs:
if: github.event_name == 'pull_request_target'
steps:
- name: Check PR title for required prefix and comment
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const title = context.payload.pull_request.title || "";
+4 -4
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Prerelease
uses: irongut/EditRelease@v1.2.0
uses: irongut/EditRelease@ccf529ad26dddf9996e7dd0f24ca5da4ea507cc2 # v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
@@ -33,7 +33,7 @@ jobs:
- name: Free Disk Space (Ubuntu)
if: matrix.target-platform == ''
uses: jlumbroso/free-disk-space@main
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # main
with:
tool-cache: false
android: true
@@ -46,10 +46,10 @@ jobs:
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version: '1.26.4'
go-version: '1.27.0'
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 1
fetch-tags: true
+12 -12
View File
@@ -43,15 +43,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version: '1.26'
go-version: '1.27'
- name: Cache Musl
id: cache-musl
uses: actions/cache@v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: build/musl-libs
key: docker-musl-libs-v2
@@ -69,7 +69,7 @@ jobs:
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ env.ARTIFACT_NAME }}
overwrite: true
@@ -83,15 +83,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version: '1.26.4'
go-version: '1.27.0'
- name: Cache Musl
id: cache-musl
uses: actions/cache@v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: build/musl-libs
key: docker-musl-libs-v2
@@ -109,7 +109,7 @@ jobs:
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ env.ARTIFACT_NAME_LITE }}
overwrite: true
@@ -146,8 +146,8 @@ jobs:
tag_favor: "suffix=-aio,onlatest=true"
steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions/download-artifact@v8
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ env.ARTIFACT_NAME }}
path: 'build/'
@@ -230,8 +230,8 @@ jobs:
tag_favor: "suffix=-lite-aio,onlatest=true"
steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions/download-artifact@v8
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ env.ARTIFACT_NAME_LITE }}
path: 'build/'
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
name: Sync GitHub to Gitee
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
+5 -5
View File
@@ -32,11 +32,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version: '1.26.4'
go-version: '1.27.0'
- name: Cache Musl
id: cache-musl
@@ -58,7 +58,7 @@ jobs:
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ env.ARTIFACT_NAME }}
overwrite: true
@@ -97,8 +97,8 @@ jobs:
tag_favor: "suffix=-aio,onlatest=true"
steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions/download-artifact@v8
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ env.ARTIFACT_NAME }}
path: 'build/'
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Trigger Makefile hash update
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }}
script: |
+17 -7
View File
@@ -8,6 +8,7 @@ import (
"os/exec"
"path/filepath"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -31,14 +32,23 @@ func start() {
return
}
}
args := os.Args
args[1] = "server"
args = append(args, "--force-bin-dir")
cmd := &exec.Cmd{
Path: args[0],
Args: args,
Env: os.Environ(),
exe, err := os.Executable()
if err != nil {
log.Fatal("failed to resolve executable path: ", err)
}
childArgs := append([]string{"server"}, os.Args[2:]...)
hasForceBinDir := false
for _, arg := range childArgs {
if arg == "--force-bin-dir" || strings.HasPrefix(arg, "--force-bin-dir=") {
hasForceBinDir = true
break
}
}
if !hasForceBinDir {
childArgs = append(childArgs, "--force-bin-dir")
}
cmd := exec.Command(exe, childArgs...)
cmd.Env = os.Environ()
stdout, err := os.OpenFile(filepath.Join(filepath.Dir(pidFile), "start.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
log.Fatal(os.Getpid(), ": failed to open start log file:", err)
+5
View File
@@ -241,6 +241,7 @@ func (d *GuangYaPan) multipartUploadToOSS(ctx context.Context, bucket *oss.Bucke
}
parts := make([]oss.UploadPart, 0, partCount)
var uploaded int64
for i := 0; i < partCount; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
@@ -273,6 +274,10 @@ func (d *GuangYaPan) multipartUploadToOSS(ctx context.Context, bucket *oss.Bucke
return fmt.Errorf("failed to upload part %d: %w", i+1, err)
}
parts = append(parts, part)
uploaded += length
if total > 0 {
up(100 * float64(uploaded) / float64(total))
}
}
_, err = bucket.CompleteMultipartUpload(imur, parts)
+18 -15
View File
@@ -2,15 +2,14 @@ module github.com/OpenListTeam/OpenList/v4
go 1.25.0
toolchain go1.26.4
toolchain go1.27.0
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0
github.com/KarpelesLab/reflink v1.0.2
github.com/KirCute/zip v1.0.1
github.com/OpenListTeam/go-cache v0.1.0
github.com/OpenListTeam/gofakes3 v0.8.1
github.com/OpenListTeam/sftpd-openlist v1.0.1
github.com/OpenListTeam/tache v0.2.2
github.com/OpenListTeam/times v0.1.0
@@ -24,14 +23,14 @@ require (
github.com/avast/retry-go v3.0.0+incompatible
github.com/aws/aws-sdk-go v1.55.8
github.com/blevesearch/bleve/v2 v2.6.1
github.com/bmatcuk/doublestar/v4 v4.9.1
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/caarlos0/env/v9 v9.0.0
github.com/charmbracelet/bubbles v0.21.1
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e
github.com/cloudsoda/go-smb2 v0.0.0-20260803221621-0b399b9d036c
github.com/coreos/go-oidc v2.3.0+incompatible
github.com/coreos/go-oidc v2.5.0+incompatible
github.com/deckarep/golang-set/v2 v2.9.0
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
github.com/disintegration/imaging v1.6.2
@@ -44,7 +43,7 @@ require (
github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0
github.com/go-resty/resty/v2 v2.17.2
github.com/go-webauthn/webauthn v0.13.4
github.com/go-webauthn/webauthn v0.18.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
@@ -52,6 +51,7 @@ require (
github.com/hekmon/transmissionrpc/v3 v3.0.0
github.com/henrybear327/go-proton-api v1.0.0
github.com/ipfs/go-ipfs-api v0.7.0
github.com/itsHenry35/gofakes3 v0.0.8
github.com/jlaffaye/ftp v0.2.4
github.com/json-iterator/go v1.1.12
github.com/kdomanski/iso9660 v0.4.0
@@ -65,7 +65,7 @@ require (
github.com/pquerna/otp v1.5.0
github.com/quic-go/quic-go v0.61.0
github.com/rclone/rclone v1.75.0
github.com/shirou/gopsutil/v4 v4.26.7
github.com/shirou/gopsutil/v4 v4.26.8
github.com/sirupsen/logrus v1.10.2
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
@@ -75,9 +75,9 @@ require (
github.com/u2takey/ffmpeg-go v0.5.0
github.com/upyun/go-sdk/v3 v3.0.4
github.com/zzzhr1990/go-common-entity v0.0.0-20250202070650-1a200048f0d3
golang.org/x/crypto v0.54.0
golang.org/x/image v0.44.0
golang.org/x/net v0.57.0
golang.org/x/crypto v0.55.0
golang.org/x/image v0.45.0
golang.org/x/net v0.58.0
golang.org/x/oauth2 v0.36.0
golang.org/x/time v0.15.0
google.golang.org/appengine v1.6.8
@@ -117,6 +117,7 @@ require (
github.com/emersion/go-vcard v0.0.0-20260618161152-d854b7e0e2d3 // indirect
github.com/geoffgarside/ber v1.2.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
@@ -131,10 +132,12 @@ require (
github.com/minio/minlz v1.2.0 // indirect
github.com/minio/xxml v0.0.3 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/relvacode/iso8601 v1.7.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/stangelandcl/ppmd v0.1.1 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
@@ -219,7 +222,7 @@ require (
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-chi/chi/v5 v5.3.1 // indirect
@@ -228,12 +231,12 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.3 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-webauthn/x v0.1.23 // indirect
github.com/go-webauthn/x v0.3.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
@@ -309,10 +312,10 @@ require (
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.40.0
golang.org/x/text v0.41.0
golang.org/x/tools v0.48.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260715232425-e75dac1f907d // indirect
google.golang.org/grpc v1.84.0-dev.0.20260723093437-b6eac429d7b6
google.golang.org/grpc v1.85.0-dev
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+34 -2
View File
@@ -13,6 +13,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEB
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1/go.mod h1:iptorS+VYKFL2N6PnebpS91dubG35eAOEERnT4PJbQU=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
@@ -49,8 +51,6 @@ github.com/OpenListTeam/115-sdk-go v0.2.6 h1:ehXyStvncvn4qRBuknor3kyGZtUmHc0+stj
github.com/OpenListTeam/115-sdk-go v0.2.6/go.mod h1:cfvitk2lwe6036iNi2h+iNxwxWDifKZsSvNtrur5BqU=
github.com/OpenListTeam/go-cache v0.1.0 h1:eV2+FCP+rt+E4OCJqLUW7wGccWZNJMV0NNkh+uChbAI=
github.com/OpenListTeam/go-cache v0.1.0/go.mod h1:AHWjKhNK3LE4rorVdKyEALDHoeMnP8SjiNyfVlB+Pz4=
github.com/OpenListTeam/gofakes3 v0.8.1 h1:uihJ7Zgb4qIafFcXhcm71BzxCyGRIqBVJYg4YOUa6uY=
github.com/OpenListTeam/gofakes3 v0.8.1/go.mod h1:mS9Ywbo6aId6BrRzeYjOIOpK0QDVnMoKOIb0hpaQZ3U=
github.com/OpenListTeam/gsync v0.1.0 h1:ywzGybOvA3lW8K1BUjKZ2IUlT2FSlzPO4DOazfYXjcs=
github.com/OpenListTeam/gsync v0.1.0/go.mod h1:h/Rvv9aX/6CdW/7B8di3xK3xNV8dUg45Fehrd/ksZ9s=
github.com/OpenListTeam/reflink v0.0.0-20260701021214-78760eaeafef h1:67uGHancMF/abMrnkc8abVUWQiG73Wk5d8CKt3RzkFo=
@@ -242,6 +242,8 @@ github.com/blevesearch/zapx/v17 v17.2.3 h1:UYYJPAt5b2tVxldx5h0jmv23RMsg8/UZKFVya
github.com/blevesearch/zapx/v17 v17.2.3/go.mod h1:r7mb4QWbDQSkbAnOjCb9iCfkcrzajB4yBdJpuBIo/fE=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.5 h1:7H7BxgmeX0j6UX42lH+KXQ92WgMQJ49DoocFdfHbCng=
@@ -310,6 +312,8 @@ github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0=
github.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-oidc v2.5.0+incompatible h1:6W0vGJR3Tu0r0PwfmjOrRZSlfxeEln8dsejt3ZWIvwo=
github.com/coreos/go-oidc v2.5.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
@@ -367,6 +371,8 @@ github.com/foxxorcat/weiyun-sdk-go v0.1.4/go.mod h1:TPxzN0d2PahweUEHlOBWlwZSA+rE
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q=
github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
@@ -419,10 +425,16 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaAqMuAuQ=
github.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI=
github.com/go-webauthn/webauthn v0.18.0 h1:PC8R3PNLEmjZf++WwcQlo1Z39S9rf8ma69rlwkypZhA=
github.com/go-webauthn/webauthn v0.18.0/go.mod h1:ymzZQhx3D/PrDjznemBdQJ23gHTaSDxUchM7sH1lUCg=
github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=
github.com/go-webauthn/x v0.3.0 h1:Q2X9vbrlP0Ed+QGEzixh1hthGZlDnzVT0XH/9IIQ0kE=
github.com/go-webauthn/x v0.3.0/go.mod h1:5OkdSQdOy7taRXWqvNHggtaPffmW94ybu3rZEER4I+I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
@@ -454,6 +466,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230405160723-4a4c7d95572b h1:Qcx5LM0fSiks9uCyFZwDBUasd3lxd1RM0GYpL+Li5o4=
github.com/google/pprof v0.0.0-20230405160723-4a4c7d95572b/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
@@ -509,6 +523,8 @@ github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE=
github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo=
github.com/ipfs/go-ipfs-api v0.7.0 h1:CMBNCUl0b45coC+lQCXEVpMhwoqjiaCwUIrM+coYW2Q=
github.com/ipfs/go-ipfs-api v0.7.0/go.mod h1:AIxsTNB0+ZhkqIfTZpdZ0VR/cpX5zrXjATa3prSay3g=
github.com/itsHenry35/gofakes3 v0.0.8 h1:1AgOl04IgoUV5r/WSK7ycnvwfpgharYLfVTmnzk5miw=
github.com/itsHenry35/gofakes3 v0.0.8/go.mod h1:gQwOJ7LoH5QSpCVmjzC6oKp+MS71utLS7GHtonsvD0c=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -699,6 +715,8 @@ github.com/nwaples/rardecode/v2 v2.4.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsR
github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -779,6 +797,8 @@ github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
github.com/shirou/gopsutil/v4 v4.26.8 h1:YQMTF/1J50B5+Y0vlo1eDRf5DoR7Gk69hY+8wjYkQeo=
github.com/shirou/gopsutil/v4 v4.26.8/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
@@ -829,6 +849,8 @@ github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 h1:6Y51mutOvRGRx6K
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543/go.mod h1:jpwqYA8KUVEvSUJHkCXsnBRJCSKP1BMa81QZ6kvRpow=
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
@@ -922,6 +944,8 @@ golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
@@ -935,6 +959,8 @@ golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -958,6 +984,8 @@ golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1022,6 +1050,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
@@ -1055,6 +1085,8 @@ google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc v1.84.0-dev.0.20260723093437-b6eac429d7b6 h1:HfjjkdGIa8u9sP9EW5WCygy0kQDuTI/Tax4j//t24Fo=
google.golang.org/grpc v1.84.0-dev.0.20260723093437-b6eac429d7b6/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/grpc v1.85.0-dev h1:HxkDyKIIZPpFnroC56tQv5gNuKTmVvi0t7TzOf5zt7g=
google.golang.org/grpc v1.85.0-dev/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+6 -8
View File
@@ -83,10 +83,9 @@ type Cors struct {
}
type S3 struct {
Enable bool `json:"enable" env:"ENABLE"`
Port int `json:"port" env:"PORT"`
SSL bool `json:"ssl" env:"SSL"`
MultipartTTL string `json:"multipart_ttl" env:"MULTIPART_TTL"`
Enable bool `json:"enable" env:"ENABLE"`
Port int `json:"port" env:"PORT"`
SSL bool `json:"ssl" env:"SSL"`
}
type FTP struct {
@@ -230,10 +229,9 @@ func DefaultConfig(dataDir string) *Config {
AllowHeaders: []string{"*"},
},
S3: S3{
Enable: false,
Port: 5246,
SSL: false,
MultipartTTL: "24h",
Enable: false,
Port: 5246,
SSL: false,
},
FTP: FTP{
Enable: false,
+8
View File
@@ -13,6 +13,14 @@ func GetMetaByPath(path string) (*model.Meta, error) {
return &meta, nil
}
func GetAllMetas() ([]model.Meta, error) {
var metas []model.Meta
if err := db.Find(&metas).Error; err != nil {
return nil, errors.Wrapf(err, "failed get all metas")
}
return metas, nil
}
func GetMetaById(id uint) (*model.Meta, error) {
var u model.Meta
if err := db.First(&u, id).Error; err != nil {
+70 -11
View File
@@ -2,6 +2,8 @@ package op
import (
stdpath "path"
"strings"
"sync"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/db"
@@ -15,6 +17,9 @@ import (
)
var metaCache = cache.NewMemCache(cache.WithShards[*model.Meta](2))
var metaSnapshotMu sync.Mutex
var metaSnapshot []model.Meta
var metaSnapshotLoaded bool
// metaG maybe not needed
var metaG singleflight.Group[*model.Meta]
@@ -23,17 +28,56 @@ func GetNearestMeta(path string) (*model.Meta, error) {
return getNearestMeta(utils.FixAndCleanPath(path))
}
func getNearestMeta(path string) (*model.Meta, error) {
meta, err := GetMetaByPath(path)
if err == nil {
return meta, nil
var metas []model.Meta
loaded := false
for {
meta, err := GetMetaByPath(path)
if err == nil {
return meta, nil
}
if errors.Cause(err) != errs.MetaNotFound {
return nil, err
}
if !loaded {
metas, err = getMetaSnapshot()
if err != nil {
return nil, err
}
loaded = true
}
var matched *model.Meta
for i := range metas {
if !strings.EqualFold(utils.FixAndCleanPath(metas[i].Path), path) {
continue
}
if matched != nil {
return nil, errors.Errorf("multiple metas match %q case-insensitively", path)
}
matched = &metas[i]
}
if matched != nil {
return matched, nil
}
if path == "/" {
return nil, errs.MetaNotFound
}
path = stdpath.Dir(path)
}
if errors.Cause(err) != errs.MetaNotFound {
}
func getMetaSnapshot() ([]model.Meta, error) {
metaSnapshotMu.Lock()
defer metaSnapshotMu.Unlock()
if metaSnapshotLoaded {
return metaSnapshot, nil
}
metas, err := db.GetAllMetas()
if err != nil {
return nil, err
}
if path == "/" {
return nil, errs.MetaNotFound
}
return getNearestMeta(stdpath.Dir(path))
metaSnapshot = metas
metaSnapshotLoaded = true
return metaSnapshot, nil
}
func GetMetaByPath(path string) (*model.Meta, error) {
@@ -68,7 +112,12 @@ func DeleteMetaById(id uint) error {
return err
}
metaCache.Del(old.Path)
return db.DeleteMetaById(id)
metaSnapshotMu.Lock()
defer metaSnapshotMu.Unlock()
err = db.DeleteMetaById(id)
metaSnapshot = nil
metaSnapshotLoaded = false
return err
}
func UpdateMeta(u *model.Meta) error {
@@ -79,13 +128,23 @@ func UpdateMeta(u *model.Meta) error {
}
metaCache.Del(old.Path)
metaCache.Del(u.Path)
return db.UpdateMeta(u)
metaSnapshotMu.Lock()
defer metaSnapshotMu.Unlock()
err = db.UpdateMeta(u)
metaSnapshot = nil
metaSnapshotLoaded = false
return err
}
func CreateMeta(u *model.Meta) error {
u.Path = utils.FixAndCleanPath(u.Path)
metaCache.Del(u.Path)
return db.CreateMeta(u)
metaSnapshotMu.Lock()
defer metaSnapshotMu.Unlock()
err := db.CreateMeta(u)
metaSnapshot = nil
metaSnapshotLoaded = false
return err
}
func GetMetaById(id uint) (*model.Meta, error) {
+14
View File
@@ -30,6 +30,20 @@ func GetStorageAndActualPath(rawPath string) (storage driver.Driver, actualPath
return
}
// GetStorageVirtualMountPath returns the deterministic virtual mount path
// without advancing the balanced-storage counter.
func GetStorageVirtualMountPath(rawPath string) (string, error) {
rawPath = utils.FixAndCleanPath(rawPath)
storages := getStoragesByPath(rawPath)
if len(storages) == 0 {
if rawPath == "/" {
return "", errs.NewErr(errs.StorageNotFound, "please add a storage first")
}
return "", errs.NewErr(errs.StorageNotFound, "rawPath: %s", rawPath)
}
return utils.FixAndCleanPath(utils.GetActualMountPath(storages[0].GetStorage().MountPath)), nil
}
// urlTreeSplitLineFormPath 分割path中分割真实路径和UrlTree定义字符串
func urlTreeSplitLineFormPath(path string) (pp string, file string) {
// url.PathUnescape 会移除 // ,手动加回去
+7
View File
@@ -2,5 +2,12 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices"
],
"packageRules": [
{
"matchManagers": ["dockerfile", "docker-compose"],
"matchPackageNames": ["alpine", "ghcr.io/openlistteam/openlist-base-image", "openlistteam/openlist:latest"],
"enabled": false
}
]
}
+17 -3
View File
@@ -44,7 +44,10 @@ func CanWriteContentBypassUserPerms(meta *model.Meta, path string) bool {
if meta == nil || !meta.Write {
return false
}
return MetaCoversPath(meta.Path, path, meta.WSub)
if utils.PathEqual(meta.Path, path) {
return true
}
return utils.IsSubPath(meta.Path, path) && meta.WSub
}
func CanAccess(user *model.User, meta *model.Meta, reqPath string, password string) bool {
@@ -78,10 +81,21 @@ func CanAccess(user *model.User, meta *model.Meta, reqPath string, password stri
}
func MetaCoversPath(metaPath, reqPath string, applyToSubFolder bool) bool {
if utils.PathEqual(metaPath, reqPath) {
metaPath = utils.FixAndCleanPath(metaPath)
reqPath = utils.FixAndCleanPath(reqPath)
if strings.EqualFold(metaPath, reqPath) {
return true
}
return utils.IsSubPath(metaPath, reqPath) && applyToSubFolder
if !applyToSubFolder {
return false
}
for reqPath != "/" {
reqPath = path.Dir(reqPath)
if strings.EqualFold(metaPath, reqPath) {
return true
}
}
return false
}
// ShouldProxy TODO need optimize
+49
View File
@@ -84,6 +84,27 @@ func TestCoversPath(t *testing.T) {
applySub: true,
want: false,
},
{
name: "case-insensitive exact path match",
metaPath: "/Folder",
reqPath: "/folder",
applySub: false,
want: true,
},
{
name: "case-insensitive sub path match",
metaPath: "/Folder",
reqPath: "/folder/Subfolder",
applySub: true,
want: true,
},
{
name: "case-insensitive sibling prefix does not match",
metaPath: "/Folder",
reqPath: "/folder-name",
applySub: true,
want: false,
},
}
for _, tt := range tests {
@@ -166,6 +187,17 @@ func TestCanWriteContentIgnoringUserPerms(t *testing.T) {
want: false,
reason: "non-sub path should deny write even with WSub=true",
},
{
name: "case-insensitive match does not broaden write bypass",
meta: &model.Meta{
Path: "/Folder",
Write: true,
WSub: true,
},
path: "/folder/subfolder",
want: false,
reason: "case-insensitive matching should only enforce restrictions, not grant write bypass",
},
}
for _, tt := range tests {
@@ -579,6 +611,23 @@ func TestCanAccessWithReadPermissions(t *testing.T) {
want: false,
reason: "user not in ReadUsers list should be denied",
},
{
name: "case-insensitive exact path still requires password",
user: &model.User{
ID: 1,
Role: model.GENERAL,
Permission: 0,
},
meta: &model.Meta{
Path: "/Folder",
Password: "secret",
PSub: false,
},
reqPath: "/folder",
password: "wrong",
want: false,
reason: "changing only path casing must not bypass an exact-path password",
},
}
for _, tt := range tests {
+22 -1
View File
@@ -3,14 +3,17 @@ package handles
import (
"bytes"
"encoding/base64"
"errors"
"image/png"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
"gorm.io/gorm"
)
type LoginReq struct {
@@ -111,11 +114,29 @@ func UpdateCurrent(c *gin.Context) {
common.ErrorStrResp(c, model.GuestCannotUpdateProfile, 403)
return
}
ssoID := req.SsoID
if req.SsoID != "" && req.SsoID != user.SsoID {
claims, err := parseSSOBindingToken(c, req.SsoID, ssoBindingProofPurpose)
if err != nil {
common.ErrorStrResp(c, "invalid or expired SSO binding proof", 400)
return
}
boundUser, err := db.GetUserBySSOID(claims.SsoID)
if err == nil && boundUser.ID != user.ID {
common.ErrorStrResp(c, "SSO account is already bound to another user", 409)
return
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
common.ErrorResp(c, err, 500)
return
}
ssoID = claims.SsoID
}
user.Username = req.Username
if req.Password != "" {
user.SetPassword(req.Password)
}
user.SsoID = req.SsoID
user.SsoID = ssoID
if err := op.UpdateUser(user); err != nil {
common.ErrorResp(c, err, 500)
} else {
+35 -1
View File
@@ -8,8 +8,10 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/fs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
type FsGetDirectUploadInfoReq struct {
@@ -44,8 +46,40 @@ func FsGetDirectUploadInfo(c *gin.Context) {
common.ErrorResp(c, err, 403)
return
}
overwrite := c.GetHeader("Overwrite") != "false"
// Resolve the destination once so permission checks and the issued upload
// capability cannot target different paths.
dstPath := stdpath.Join(path, req.FileName)
path = stdpath.Dir(dstPath)
req.FileName = stdpath.Base(dstPath)
parentMeta, err := op.GetNearestMeta(path)
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
common.ErrorResp(c, err, 500, true)
return
}
if !user.CanWriteContent() && !common.CanWriteContentBypassUserPerms(parentMeta, path) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
if !common.CanWrite(user, parentMeta, path) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
// A single-segment file name can still name a nested mount point.
parentMountPath, err := op.GetStorageVirtualMountPath(path)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
targetMountPath, err := op.GetStorageVirtualMountPath(dstPath)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
if parentMountPath != targetMountPath {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
overwrite := c.GetHeader("Overwrite") != "false"
if !overwrite {
res, err := fs.Get(c.Request.Context(), dstPath, &fs.GetArgs{NoLog: true})
if err != nil && !errs.IsObjectNotFound(err) {
+119 -9
View File
@@ -1,6 +1,7 @@
package handles
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
@@ -22,12 +23,25 @@ import (
"github.com/coreos/go-oidc"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
"github.com/golang-jwt/jwt/v4"
"golang.org/x/oauth2"
"gorm.io/gorm"
)
const stateLength = 16
const stateExpire = time.Minute * 5
const ssoBindingExpire = time.Minute * 5
const ssoBindingCookie = "openlist_sso_binding"
const ssoBindingStatePurpose = "sso_binding_state"
const ssoBindingProofPurpose = "sso_binding_proof"
type ssoBindingClaims struct {
Purpose string `json:"purpose"`
Method string `json:"method"`
SessionDigest string `json:"session_digest"`
SsoID string `json:"sso_id,omitempty"`
jwt.RegisteredClaims
}
var stateCache = cache.NewMemCache[string](cache.WithShards[string](stateLength))
@@ -46,6 +60,68 @@ func verifyState(clientID, ip, state string) bool {
return ok && value == ip
}
func ssoBindingSessionDigest(session string) string {
digest := sha256.Sum256([]byte(session))
return base64.RawURLEncoding.EncodeToString(digest[:])
}
func parseSSOBindingToken(c *gin.Context, rawToken, purpose string) (*ssoBindingClaims, error) {
claims := &ssoBindingClaims{}
token, err := jwt.ParseWithClaims(rawToken, claims, func(token *jwt.Token) (interface{}, error) {
if token.Method != jwt.SigningMethodHS256 {
return nil, errors.New("invalid SSO binding token algorithm")
}
return common.SecretKey, nil
})
if err != nil || !token.Valid || claims.ExpiresAt == nil || claims.Purpose != purpose ||
claims.Method != "get_sso_id" || len(claims.SessionDigest) != 43 {
return nil, errors.New("invalid or expired SSO binding token")
}
if (purpose == ssoBindingStatePurpose && claims.SsoID != "") ||
(purpose == ssoBindingProofPurpose && claims.SsoID == "") {
return nil, errors.New("invalid SSO binding token payload")
}
session, err := c.Cookie(ssoBindingCookie)
if err != nil || ssoBindingSessionDigest(session) != claims.SessionDigest {
return nil, errors.New("invalid SSO binding session")
}
return claims, nil
}
func generateSSOBindingToken(c *gin.Context, purpose, ssoID string) (string, error) {
session, err := c.Cookie(ssoBindingCookie)
expire := ssoBindingExpire
if purpose == ssoBindingStatePurpose {
session = random.String(32)
expire = stateExpire
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(
ssoBindingCookie,
session,
int((stateExpire+ssoBindingExpire).Seconds()),
path.Join(conf.URL.Path, "/api"),
"",
strings.HasPrefix(common.GetApiUrl(c), "https://"),
true,
)
} else if err != nil {
return "", errors.New("missing SSO binding session")
}
now := time.Now()
claims := ssoBindingClaims{
Purpose: purpose,
Method: "get_sso_id",
SessionDigest: ssoBindingSessionDigest(session),
SsoID: ssoID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(expire)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
},
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(common.SecretKey)
}
func ssoRedirectUri(c *gin.Context, useCompatibility bool, method string) string {
if useCompatibility {
return common.GetApiUrl(c) + "/api/auth/" + method
@@ -74,6 +150,16 @@ func SSOLoginRedirect(c *gin.Context) {
urlValues.Add("response_type", "code")
urlValues.Add("redirect_uri", redirectUri)
urlValues.Add("client_id", clientId)
bindingState := ""
var err error
if method == "get_sso_id" {
bindingState, err = generateSSOBindingToken(c, ssoBindingStatePurpose, "")
if err != nil {
common.ErrorResp(c, err, 500)
return
}
urlValues.Add("state", bindingState)
}
switch platform {
case "Github":
rUrl = "https://github.com/login/oauth/authorize?"
@@ -94,15 +180,19 @@ func SSOLoginRedirect(c *gin.Context) {
endpoint := strings.TrimSuffix(setting.GetStr(conf.SSOEndpointName), "/")
rUrl = endpoint + "/login/oauth/authorize?"
urlValues.Add("scope", "profile")
urlValues.Add("state", endpoint)
if bindingState == "" {
urlValues.Add("state", endpoint)
}
case "OIDC":
oauth2Config, err := GetOIDCClient(c, useCompatibility, redirectUri, method)
if err != nil {
common.ErrorStrResp(c, err.Error(), 400)
return
}
state := generateState(clientId, c.ClientIP())
c.Redirect(http.StatusFound, oauth2Config.AuthCodeURL(state))
if bindingState == "" {
bindingState = generateState(clientId, c.ClientIP())
}
c.Redirect(http.StatusFound, oauth2Config.AuthCodeURL(bindingState))
return
default:
common.ErrorStrResp(c, "invalid platform", 400)
@@ -201,11 +291,15 @@ func OIDCLoginCallback(c *gin.Context) {
common.ErrorResp(c, err, 400)
return
}
if !verifyState(clientId, c.ClientIP(), c.Query("state")) {
if method == "get_sso_id" {
if _, err := parseSSOBindingToken(c, c.Query("state"), ssoBindingStatePurpose); err != nil {
common.ErrorStrResp(c, "incorrect or expired state parameter", 400)
return
}
} else if !verifyState(clientId, c.ClientIP(), c.Query("state")) {
common.ErrorStrResp(c, "incorrect or expired state parameter", 400)
return
}
oauth2Token, err := oauth2Config.Exchange(c, c.Query("code"))
if err != nil {
common.ErrorResp(c, err, 400)
@@ -235,8 +329,13 @@ func OIDCLoginCallback(c *gin.Context) {
return
}
if method == "get_sso_id" {
bindingProof, err := generateSSOBindingToken(c, ssoBindingProofPurpose, userID)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
if useCompatibility {
c.Redirect(302, common.GetApiUrl(c)+"/@manage?sso_id="+userID)
c.Redirect(302, common.GetApiUrl(c)+"/@manage?sso_id="+bindingProof)
return
}
html := fmt.Sprintf(`<!DOCTYPE html>
@@ -246,7 +345,7 @@ func OIDCLoginCallback(c *gin.Context) {
window.opener.postMessage({"sso_id": "%s"}, "*")
window.close()
</script>
</body>`, userID)
</body>`, bindingProof)
c.Data(200, "text/html; charset=utf-8", []byte(html))
return
}
@@ -352,6 +451,12 @@ func SSOLoginCallback(c *gin.Context) {
common.ErrorStrResp(c, "No code provided", 400)
return
}
if argument == "get_sso_id" {
if _, err := parseSSOBindingToken(c, c.Query("state"), ssoBindingStatePurpose); err != nil {
common.ErrorStrResp(c, "incorrect or expired state parameter", 400)
return
}
}
var resp *resty.Response
var err error
if platform == "Dingtalk" {
@@ -402,8 +507,13 @@ func SSOLoginCallback(c *gin.Context) {
return
}
if argument == "get_sso_id" {
bindingProof, err := generateSSOBindingToken(c, ssoBindingProofPurpose, userID)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
if usecompatibility {
c.Redirect(302, common.GetApiUrl(c)+"/@manage?sso_id="+userID)
c.Redirect(302, common.GetApiUrl(c)+"/@manage?sso_id="+bindingProof)
return
}
html := fmt.Sprintf(`<!DOCTYPE html>
@@ -413,7 +523,7 @@ func SSOLoginCallback(c *gin.Context) {
window.opener.postMessage({"sso_id": "%s"}, "*")
window.close()
</script>
</body>`, userID)
</body>`, bindingProof)
c.Data(200, "text/html; charset=utf-8", []byte(html))
return
}
+1 -1
View File
@@ -60,7 +60,7 @@ func needSign(meta *model.Meta, path string) bool {
if meta == nil || meta.Password == "" {
return false
}
if !meta.PSub && path != meta.Path {
if !meta.PSub && !common.MetaCoversPath(meta.Path, path, false) {
return false
}
return true
+1 -1
View File
@@ -233,7 +233,7 @@ func _fs(g *gin.RouterGroup) {
g.POST("/torrent/rapid_upload", handles.TorrentRapidUpload)
g.POST("/torrent/generate", handles.GenerateTorrentForPath)
// Direct upload (client-side upload to storage)
g.POST("/get_direct_upload_info", middlewares.FsUp, handles.FsGetDirectUploadInfo)
g.POST("/get_direct_upload_info", handles.FsGetDirectUploadInfo)
}
func _task(g *gin.RouterGroup) {
+17 -28
View File
@@ -23,7 +23,7 @@ import (
"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/gofakes3"
"github.com/itsHenry35/gofakes3"
"github.com/ncw/swift/v2"
log "github.com/sirupsen/logrus"
)
@@ -33,25 +33,19 @@ var (
timeFormat = "Mon, 2 Jan 2006 15:04:05 GMT"
)
// s3Backend implements the gofakes3.Backend interface to make an S3
// backend for gofakes3. It also implements gofakes3.MultipartBackend so that
// multipart uploads are streamed to local temp files part-by-part and
// assembled into storage on completion, instead of being buffered in memory.
// s3Backend implements the gofacess3.Backend interface to make an S3
// backend for gofakes3
type s3Backend struct {
meta *sync.Map
listDir func(context.Context, string) ([]model.Obj, error)
uploads *sync.Map // map[gofakes3.UploadID]*multipartState
}
// newBackend creates a new SimpleBucketBackend.
func newBackend() gofakes3.Backend {
b := &s3Backend{
return &s3Backend{
meta: new(sync.Map),
uploads: new(sync.Map),
listDir: getDirEntries,
}
b.startReaper()
return b
}
// ListBuckets always returns the default bucket.
@@ -239,20 +233,9 @@ func (b *s3Backend) PutObject(
meta map[string]string,
input io.Reader, size int64,
) (result gofakes3.PutObjectResult, err error) {
return result, b.putStream(ctx, bucketName, objectName, meta, input, size)
}
// putStream stores the given object into the underlying storage. It is shared
// by PutObject and the multipart-upload Complete step so both paths apply the
// same directory creation, metadata and ignore rules.
func (b *s3Backend) putStream(
ctx context.Context, bucketName, objectName string,
meta map[string]string,
input io.Reader, size int64,
) error {
bucket, err := getBucketByName(bucketName)
if err != nil {
return err
return result, err
}
bucketPath := bucket.Path
@@ -278,15 +261,15 @@ func (b *s3Backend) putStream(
log.Debugf("reqPath: %s not found and objectName contains /, need to makeDir", reqPath)
err = fs.MakeDir(ctx, reqPath)
if err != nil {
return errors.WithMessagef(err, "failed to makeDir, reqPath: %s", reqPath)
return result, errors.WithMessagef(err, "failed to makeDir, reqPath: %s", reqPath)
}
} else {
return gofakes3.KeyNotFound(objectName)
return result, gofakes3.KeyNotFound(objectName)
}
}
if isDir {
return nil
return result, nil
}
var ti time.Time
@@ -312,7 +295,7 @@ func (b *s3Backend) putStream(
}
// Check if system file should be ignored
if setting.GetBool(conf.IgnoreSystemFiles) && utils.IsSystemFile(obj.Name) {
return errs.IgnoredSystemFile
return result, errs.IgnoredSystemFile
}
stream := &stream.FileStream{
Obj: &obj,
@@ -322,12 +305,18 @@ func (b *s3Backend) putStream(
err = fs.PutDirectly(ctx, reqPath, stream)
if err != nil {
return err
return result, err
}
// if err := stream.Close(); err != nil {
// // remove file when close error occurred (FsPutErr)
// _ = fs.Remove(ctx, fp)
// return result, err
// }
b.meta.Store(fp, meta)
return nil
return result, nil
}
// DeleteMulti deletes multiple objects in a single request.
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/OpenListTeam/gofakes3"
"github.com/itsHenry35/gofakes3"
log "github.com/sirupsen/logrus"
)
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"testing"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/gofakes3"
"github.com/itsHenry35/gofakes3"
)
func TestListPageBoundsRecursiveTraversal(t *testing.T) {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"fmt"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/gofakes3"
"github.com/itsHenry35/gofakes3"
)
// logger output formatted message
-383
View File
@@ -1,383 +0,0 @@
// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3
// Package s3 implements a fake s3 server for openlist
package s3
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/gofakes3"
log "github.com/sirupsen/logrus"
)
// Compile-time assertions that s3Backend implements both the base Backend and
// the optional MultipartBackend interface from gofakes3.
var (
_ gofakes3.Backend = (*s3Backend)(nil)
_ gofakes3.MultipartBackend = (*s3Backend)(nil)
)
// multipartPart records a single uploaded part on disk.
type multipartPart struct {
path string
size int64
md5hex string // unquoted lowercase hex
updated time.Time
}
// multipartState tracks one in-progress multipart upload.
//
// Concurrency: gofakes3 does not serialize operations for the same uploadID,
// so concurrent UploadPart/Complete/Abort calls may overlap. The parts map is
// protected by mu. Each part is written to its own file inside dir, so
// concurrent UploadPart calls for different part numbers are safe without
// additional locking. lastActivity is updated under mu on create and on each
// part upload so the reaper can make a consistent expiry decision.
type multipartState struct {
bucket string
object string
meta map[string]string
dir string
created time.Time
lastActivity time.Time // updated under mu on create and each part upload
mu sync.Mutex
parts map[int]*multipartPart
}
// CreateMultipartUpload begins a new multipart upload. Parts are streamed to
// local temp files so that large uploads do not need to be buffered in memory.
//
// It implements gofakes3.MultipartBackend.
func (b *s3Backend) CreateMultipartUpload(ctx context.Context, bucket, object string, meta map[string]string) (gofakes3.UploadID, error) {
if _, err := getBucketByName(bucket); err != nil {
return "", err
}
tempDir := conf.Conf.TempDir
if tempDir == "" {
tempDir = os.TempDir()
}
dir, err := os.MkdirTemp(tempDir, "s3-multipart-*")
if err != nil {
return "", fmt.Errorf("create multipart upload dir: %w", err)
}
uploadID := gofakes3.UploadID(strings.ReplaceAll(uuid.NewString(), "-", ""))
now := time.Now()
state := &multipartState{
bucket: bucket,
object: object,
meta: meta,
dir: dir,
created: now,
lastActivity: now,
parts: map[int]*multipartPart{},
}
b.uploads.Store(uploadID, state)
log.Debugf("s3 multipart: created upload %s for %s/%s", uploadID, bucket, object)
return uploadID, nil
}
// UploadPart writes a single part to disk and returns its (quoted) MD5 etag.
//
// It implements gofakes3.MultipartBackend. The body must contain exactly
// contentLength bytes; a short read is reported as ErrIncompleteBody so a
// truncated client request is never silently stored.
func (b *s3Backend) UploadPart(ctx context.Context, bucket, object string, uploadID gofakes3.UploadID, partNumber int, contentLength int64, body io.Reader) (string, error) {
if partNumber <= 0 || partNumber > gofakes3.MaxUploadPartNumber {
return "", gofakes3.ErrInvalidPart
}
val, ok := b.uploads.Load(uploadID)
if !ok {
return "", gofakes3.ErrNoSuchUpload
}
state := val.(*multipartState)
partPath := filepath.Join(state.dir, fmt.Sprintf("part-%05d", partNumber))
f, err := os.Create(partPath)
if err != nil {
return "", fmt.Errorf("create part file: %w", err)
}
// Remove a half-written file on any failure path.
partFailed := true
defer func() {
if partFailed {
_ = f.Close()
_ = os.Remove(partPath)
}
}()
hash := md5.New()
// io.TeeReader feeds the hasher while the part is streamed to disk, so the
// etag costs no extra pass over the data.
n, err := utils.CopyWithBuffer(io.MultiWriter(f, hash), io.LimitReader(body, contentLength))
if err != nil {
return "", fmt.Errorf("write part %d: %w", partNumber, err)
}
if err := f.Close(); err != nil {
return "", fmt.Errorf("close part %d: %w", partNumber, err)
}
if n != contentLength {
// The client under-delivered (truncated/aborted request). gofakes3 does
// not validate this for streaming backends, so we must.
return "", gofakes3.ErrIncompleteBody
}
md5hex := hex.EncodeToString(hash.Sum(nil))
etag := fmt.Sprintf("%q", md5hex)
now := time.Now()
state.mu.Lock()
if old := state.parts[partNumber]; old != nil && old.path != partPath {
_ = os.Remove(old.path)
}
state.parts[partNumber] = &multipartPart{
path: partPath,
size: n,
md5hex: md5hex,
updated: now,
}
state.lastActivity = now
state.mu.Unlock()
partFailed = false
log.Debugf("s3 multipart: stored part %d for %s (%d bytes)", partNumber, uploadID, n)
return etag, nil
}
// CompleteMultipartUpload assembles the uploaded parts in ascending part-number
// order and streams the result into storage via the shared putStream path.
//
// It implements gofakes3.MultipartBackend. Part ordering and etags are
// validated against the parts actually received.
func (b *s3Backend) CompleteMultipartUpload(ctx context.Context, bucket, object string, uploadID gofakes3.UploadID, input *gofakes3.CompleteMultipartUploadRequest) (gofakes3.VersionID, string, error) {
val, ok := b.uploads.Load(uploadID)
if !ok {
return "", "", gofakes3.ErrNoSuchUpload
}
state := val.(*multipartState)
if input == nil || len(input.Parts) == 0 {
return "", "", gofakes3.ErrorMessagef(gofakes3.ErrMalformedXML, "complete multipart upload has no parts")
}
// S3 requires the parts in a CompleteMultipartUpload request to be listed
// in ascending part-number order.
for i := 1; i < len(input.Parts); i++ {
if input.Parts[i].PartNumber <= input.Parts[i-1].PartNumber {
return "", "", gofakes3.ErrInvalidPartOrder
}
}
// Validate every requested part exists with a matching etag, and collect
// them in the order requested by the client (which is sorted ascending).
state.mu.Lock()
ordered := make([]*multipartPart, 0, len(input.Parts))
var concat []byte
for _, p := range input.Parts {
stored := state.parts[p.PartNumber]
if stored == nil {
state.mu.Unlock()
return "", "", gofakes3.ErrorMessagef(gofakes3.ErrInvalidPart, "unexpected part number %d in complete request", p.PartNumber)
}
if strings.Trim(p.ETag, "\"") != stored.md5hex {
state.mu.Unlock()
return "", "", gofakes3.ErrorMessagef(gofakes3.ErrInvalidPart, "unexpected part etag for number %d in complete request", p.PartNumber)
}
ordered = append(ordered, stored)
// S3 multipart etag = hex(md5(concat(part_md5_digests)))-N
concat = append(concat, stored.md5Bytes()...)
}
// Hold the lock until the part files are opened so an abort racing with
// complete cannot delete them out from under us.
readers := make([]io.Reader, 0, len(ordered))
closers := make([]io.Closer, 0, len(ordered))
var total int64
for _, part := range ordered {
f, err := os.Open(part.path)
if err != nil {
for _, c := range closers {
_ = c.Close()
}
state.mu.Unlock()
return "", "", fmt.Errorf("open part %s: %w", part.path, err)
}
readers = append(readers, f)
closers = append(closers, f)
total += part.size
}
state.mu.Unlock()
combined := utils.NewReadCloser(io.MultiReader(readers...), func() error {
var firstErr error
for _, c := range closers {
if err := c.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
})
defer combined.Close()
err := b.putStream(ctx, bucket, object, state.meta, combined, total)
if err != nil {
// Leave the upload in place so the client may retry completion, per
// the gofakes3 MultipartBackend contract.
return "", "", err
}
// Success: drop bookkeeping and clean up part files.
b.removeUpload(uploadID)
sum := md5.Sum(concat)
etag := fmt.Sprintf("%q", fmt.Sprintf("%s-%d", hex.EncodeToString(sum[:]), len(ordered)))
log.Debugf("s3 multipart: completed upload %s -> %s/%s (%d bytes)", uploadID, bucket, object, total)
return "", etag, nil
}
// AbortMultipartUpload discards an in-progress upload and its parts.
//
// It implements gofakes3.MultipartBackend and is idempotent: aborting an
// unknown upload succeeds so retries do not fail.
func (b *s3Backend) AbortMultipartUpload(ctx context.Context, bucket, object string, uploadID gofakes3.UploadID) error {
b.removeUpload(uploadID)
return nil
}
// removeUpload deletes the upload's temp directory and drops its bookkeeping.
// Missing uploads are ignored to keep abort/complete idempotent.
func (b *s3Backend) removeUpload(uploadID gofakes3.UploadID) {
val, ok := b.uploads.LoadAndDelete(uploadID)
if !ok {
return
}
state := val.(*multipartState)
if state.dir != "" {
if err := os.RemoveAll(state.dir); err != nil {
log.Warnf("s3 multipart: failed to clean up %s: %v", state.dir, err)
}
}
}
// md5Bytes returns the raw 16-byte MD5 digest of the part.
func (p *multipartPart) md5Bytes() []byte {
b, _ := hex.DecodeString(p.md5hex)
return b
}
// Defaults for reaping abandoned multipart uploads. A client that never sends
// CompleteMultipartUpload or AbortMultipartUpload would otherwise leave part
// files on disk forever; the reaper drops uploads inactive for longer than the
// TTL.
const (
defaultMultipartTTL = 24 * time.Hour
multipartDirPrefix = "s3-multipart-"
)
// multipartTTL returns the configured max idle time for an upload before the
// reaper reclaims it. It parses conf.Conf.S3.MultipartTTL as a Go duration
// (e.g. "24h", "30m"); an empty or invalid value falls back to the default.
func multipartTTL() time.Duration {
if v := conf.Conf.S3.MultipartTTL; v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
return d
}
}
return defaultMultipartTTL
}
// reapInterval derives the reaper tick interval from the TTL: a quarter of the
// TTL, clamped to [10s, 1h].
func reapInterval(ttl time.Duration) time.Duration {
d := ttl / 4
if d < 10*time.Second {
d = 10 * time.Second
}
if d > time.Hour {
d = time.Hour
}
return d
}
// startReaper removes leftover part directories from a previous process crash
// and then launches a background goroutine that periodically reclaims uploads
// inactive for longer than the TTL. The goroutine runs for the lifetime of the
// process; NewServer is called once at startup, so there is one reaper per
// backend instance.
func (b *s3Backend) startReaper() {
b.cleanupStaleDirs(time.Now(), multipartTTL())
interval := reapInterval(multipartTTL())
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for now := range ticker.C {
b.reapExpired(now, multipartTTL())
}
}()
}
// reapExpired removes uploads whose lastActivity is older than ttl. It is safe
// to call concurrently with UploadPart/Complete/Abort: each candidate is
// re-checked under its own lock and removed atomically via removeUpload.
func (b *s3Backend) reapExpired(now time.Time, ttl time.Duration) {
b.uploads.Range(func(key, val any) bool {
state := val.(*multipartState)
state.mu.Lock()
expired := now.Sub(state.lastActivity) > ttl
state.mu.Unlock()
if !expired {
return true
}
b.removeUpload(key.(gofakes3.UploadID))
log.Infof("s3 multipart: reaped abandoned upload %s (%s/%s)", key, state.bucket, state.object)
return true
})
}
// cleanupStaleDirs removes s3-multipart-* directories under TempDir that are
// older than ttl. This reclaims part files left behind by a previous process
// crash; dirs younger than ttl are left alone so a concurrently-starting
// sibling backend instance is never disturbed.
func (b *s3Backend) cleanupStaleDirs(now time.Time, ttl time.Duration) {
tempDir := conf.Conf.TempDir
if tempDir == "" {
tempDir = os.TempDir()
}
entries, err := os.ReadDir(tempDir)
if err != nil {
return
}
cutoff := now.Add(-ttl)
for _, e := range entries {
if !e.IsDir() || !strings.HasPrefix(e.Name(), multipartDirPrefix) {
continue
}
info, err := e.Info()
if err != nil {
continue
}
if info.ModTime().After(cutoff) {
continue
}
if err := os.RemoveAll(filepath.Join(tempDir, e.Name())); err != nil {
log.Warnf("s3 multipart: failed to clean up stale dir %s: %v", e.Name(), err)
} else {
log.Infof("s3 multipart: removed stale multipart dir %s", e.Name())
}
}
}
-321
View File
@@ -1,321 +0,0 @@
package s3
import (
"bytes"
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
_ "github.com/OpenListTeam/OpenList/v4/drivers/local"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/gofakes3"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
func init() {
dataDir, err := os.MkdirTemp("", "openlist-s3-mp-*")
if err != nil {
panic(err)
}
conf.Conf = conf.DefaultConfig(dataDir)
if err := os.MkdirAll(conf.Conf.TempDir, 0o755); err != nil {
panic("mkdir temp dir: " + err.Error())
}
dB, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
panic("failed to connect database: " + err.Error())
}
db.Init(dB)
}
// s3ErrorCode extracts the gofakes3 ErrorCode from an error returned by the
// MultipartBackend methods.
func s3ErrorCode(err error) gofakes3.ErrorCode {
if err == nil {
return gofakes3.ErrNone
}
var s3err interface{ ErrorCode() gofakes3.ErrorCode }
if errors.As(err, &s3err) {
return s3err.ErrorCode()
}
return gofakes3.ErrNone
}
// setupMultipartBackend prepares a Local storage mounted at /mpbucket and an
// s3Backend with an "mp" bucket pointing at it. It returns the backend, the
// local root directory on disk, and a cleanup function.
func setupMultipartBackend(t *testing.T) (*s3Backend, string) {
t.Helper()
ctx := context.Background()
// Unique mount path and bucket per test: the in-memory sqlite is shared
// across tests in this package, so a fixed mount path would clash.
mount := "/" + sanitizeTestName(t.Name())
bucket := "mp"
localRoot, err := os.MkdirTemp("", "openlist-s3-local-*")
if err != nil {
t.Fatalf("mkdir local root: %v", err)
}
t.Cleanup(func() { _ = os.RemoveAll(localRoot) })
_, err = op.CreateStorage(ctx, model.Storage{
Driver: "Local",
MountPath: mount,
Addition: `{"root_folder_path":"` + localRoot + `","thumbnail":false}`,
})
if err != nil {
t.Fatalf("create local storage: %+v", err)
}
if err := op.SaveSettingItem(&model.SettingItem{
Key: conf.S3Buckets,
Value: `[{"name":"` + bucket + `","path":"` + mount + `"}]`,
}); err != nil {
t.Fatalf("save s3 buckets setting: %+v", err)
}
return newBackend().(*s3Backend), localRoot
}
func sanitizeTestName(name string) string {
r := strings.NewReplacer("/", "_", " ", "_")
return r.Replace(name)
}
func TestMultipartUploadEndToEnd(t *testing.T) {
ctx := context.Background()
b, localRoot := setupMultipartBackend(t)
meta := map[string]string{"Content-Type": "text/plain"}
uploadID, err := b.CreateMultipartUpload(ctx, "mp", "dir/hello.txt", meta)
if err != nil {
t.Fatalf("CreateMultipartUpload: %+v", err)
}
if uploadID == "" {
t.Fatal("empty upload id")
}
part := func(n int, body string) string {
t.Helper()
etag, err := b.UploadPart(ctx, "mp", "dir/hello.txt", uploadID, n, int64(len(body)), strings.NewReader(body))
if err != nil {
t.Fatalf("UploadPart %d: %+v", n, err)
}
return etag
}
etag1 := part(1, "Hello, ")
etag2 := part(2, "multipart ")
etag3 := part(3, "world!")
// Re-uploading the same part number overwrites it and returns a fresh etag.
if e := part(2, "multipart "); e != etag2 {
t.Fatalf("re-upload part 2 etag = %q, want %q", e, etag2)
}
// Short read (body smaller than declared Content-Length) must fail.
_, shortErr := b.UploadPart(ctx, "mp", "dir/hello.txt", uploadID, 4, 10, strings.NewReader("abc"))
shortErr = s3ErrorCode(shortErr)
if shortErr != gofakes3.ErrIncompleteBody {
t.Fatalf("short read error = %v, want IncompleteBody", shortErr)
}
// Unknown upload id.
_, uerr := b.UploadPart(ctx, "mp", "dir/hello.txt", "does-not-exist", 1, 1, strings.NewReader("x"))
if code := s3ErrorCode(uerr); code != gofakes3.ErrNoSuchUpload {
t.Fatalf("unknown upload error = %v, want NoSuchUpload", code)
}
// Out-of-range part number.
_, perr := b.UploadPart(ctx, "mp", "dir/hello.txt", uploadID, 0, 1, strings.NewReader("x"))
if code := s3ErrorCode(perr); code != gofakes3.ErrInvalidPart {
t.Fatalf("part 0 error = %v, want InvalidPart", code)
}
// Parts out of order.
_, _, err = b.CompleteMultipartUpload(ctx, "mp", "dir/hello.txt", uploadID, &gofakes3.CompleteMultipartUploadRequest{
Parts: []gofakes3.CompletedPart{
{PartNumber: 2, ETag: etag2},
{PartNumber: 1, ETag: etag1},
},
})
if code := s3ErrorCode(err); code != gofakes3.ErrInvalidPartOrder {
t.Fatalf("out-of-order complete error = %v, want InvalidPartOrder", code)
}
// Wrong etag.
_, _, err = b.CompleteMultipartUpload(ctx, "mp", "dir/hello.txt", uploadID, &gofakes3.CompleteMultipartUploadRequest{
Parts: []gofakes3.CompletedPart{
{PartNumber: 1, ETag: etag1},
{PartNumber: 2, ETag: `"deadbeef"`},
{PartNumber: 3, ETag: etag3},
},
})
if code := s3ErrorCode(err); code != gofakes3.ErrInvalidPart {
t.Fatalf("wrong etag complete error = %v, want InvalidPart", code)
}
// Missing part number.
_, _, err = b.CompleteMultipartUpload(ctx, "mp", "dir/hello.txt", uploadID, &gofakes3.CompleteMultipartUploadRequest{
Parts: []gofakes3.CompletedPart{
{PartNumber: 1, ETag: etag1},
{PartNumber: 99, ETag: etag3},
},
})
if code := s3ErrorCode(err); code != gofakes3.ErrInvalidPart {
t.Fatalf("missing part complete error = %v, want InvalidPart", code)
}
// The failed completes must leave the upload available for retry.
if _, ok := b.uploads.Load(uploadID); !ok {
t.Fatal("upload was removed after a failed complete")
}
// Successful complete.
_, etag, err := b.CompleteMultipartUpload(ctx, "mp", "dir/hello.txt", uploadID, &gofakes3.CompleteMultipartUploadRequest{
Parts: []gofakes3.CompletedPart{
{PartNumber: 1, ETag: etag1},
{PartNumber: 2, ETag: etag2},
{PartNumber: 3, ETag: etag3},
},
})
if err != nil {
t.Fatalf("CompleteMultipartUpload: %+v", err)
}
if !strings.HasSuffix(etag, `-3"`) || !strings.HasPrefix(etag, `"`) {
t.Fatalf("complete etag = %q, want a quoted \"<hex>-3\" multipart etag", etag)
}
// The object must exist on disk with the concatenated content.
got, err := os.ReadFile(filepath.Join(localRoot, "dir", "hello.txt"))
if err != nil {
t.Fatalf("read resulting file: %+v", err)
}
want := []byte("Hello, multipart world!")
if !bytes.Equal(got, want) {
t.Fatalf("resulting file content = %q, want %q", got, want)
}
// Bookkeeping and temp files must be cleaned up on success.
if _, ok := b.uploads.Load(uploadID); ok {
t.Fatal("upload still tracked after successful complete")
}
}
func TestMultipartAbort(t *testing.T) {
ctx := context.Background()
b, _ := setupMultipartBackend(t)
uploadID, err := b.CreateMultipartUpload(ctx, "mp", "abort.txt", nil)
if err != nil {
t.Fatalf("CreateMultipartUpload: %+v", err)
}
if _, err := b.UploadPart(ctx, "mp", "abort.txt", uploadID, 1, 3, strings.NewReader("abc")); err != nil {
t.Fatalf("UploadPart: %+v", err)
}
state, _ := b.uploads.Load(uploadID)
dir := state.(*multipartState).dir
if _, err := os.Stat(dir); err != nil {
t.Fatalf("temp dir missing before abort: %+v", err)
}
if err := b.AbortMultipartUpload(ctx, "mp", "abort.txt", uploadID); err != nil {
t.Fatalf("AbortMultipartUpload: %+v", err)
}
if _, ok := b.uploads.Load(uploadID); ok {
t.Fatal("upload still tracked after abort")
}
if _, err := os.Stat(dir); !os.IsNotExist(err) {
t.Fatalf("temp dir still exists after abort (err=%v)", err)
}
// Aborting an unknown upload must be idempotent.
if err := b.AbortMultipartUpload(ctx, "mp", "abort.txt", "nope"); err != nil {
t.Fatalf("abort unknown upload returned error: %+v", err)
}
}
func TestMultipartReapExpired(t *testing.T) {
ctx := context.Background()
b, _ := setupMultipartBackend(t)
// An active upload (fresh lastActivity) must be kept.
freshID, err := b.CreateMultipartUpload(ctx, "mp", "fresh.txt", nil)
if err != nil {
t.Fatalf("create fresh upload: %+v", err)
}
// An abandoned upload (stale lastActivity) must be reaped.
staleID, err := b.CreateMultipartUpload(ctx, "mp", "stale.txt", nil)
if err != nil {
t.Fatalf("create stale upload: %+v", err)
}
if _, err := b.UploadPart(ctx, "mp", "stale.txt", staleID, 1, 3, strings.NewReader("abc")); err != nil {
t.Fatalf("upload stale part: %+v", err)
}
staleState, _ := b.uploads.Load(staleID)
staleDir := staleState.(*multipartState).dir
if _, err := os.Stat(staleDir); err != nil {
t.Fatalf("stale temp dir missing: %+v", err)
}
// Force the stale upload's lastActivity well into the past.
ttl := 30 * time.Minute
now := time.Now()
staleState.(*multipartState).mu.Lock()
staleState.(*multipartState).lastActivity = now.Add(-2 * ttl)
staleState.(*multipartState).mu.Unlock()
b.reapExpired(now, ttl)
if _, ok := b.uploads.Load(staleID); ok {
t.Fatal("stale upload still tracked after reap")
}
if _, err := os.Stat(staleDir); !os.IsNotExist(err) {
t.Fatalf("stale temp dir still exists after reap (err=%v)", err)
}
if _, ok := b.uploads.Load(freshID); !ok {
t.Fatal("fresh upload was reaped, should have been kept")
}
}
func TestMultipartCleanupStaleDirs(t *testing.T) {
b, _ := setupMultipartBackend(t)
tempDir := conf.Conf.TempDir
staleDir, err := os.MkdirTemp(tempDir, multipartDirPrefix+"*")
if err != nil {
t.Fatalf("mkdir stale dir: %v", err)
}
freshDir, err := os.MkdirTemp(tempDir, multipartDirPrefix+"*")
if err != nil {
t.Fatalf("mkdir fresh dir: %v", err)
}
// Age the stale dir beyond the TTL; leave the fresh dir young.
ttl := 30 * time.Minute
now := time.Now()
past := now.Add(-2 * ttl)
if err := os.Chtimes(staleDir, past, past); err != nil {
t.Fatalf("chtimes stale dir: %v", err)
}
b.cleanupStaleDirs(now, ttl)
if _, err := os.Stat(staleDir); !os.IsNotExist(err) {
t.Fatalf("stale dir should have been removed (err=%v)", err)
}
if _, err := os.Stat(freshDir); err != nil {
t.Fatalf("fresh dir should have been kept (err=%v)", err)
}
_ = os.RemoveAll(freshDir)
}
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/OpenListTeam/gofakes3/signature"
"github.com/itsHenry35/gofakes3/signature"
)
func redirectHandler(next http.Handler, authPairs map[string]string) http.Handler {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"math/rand"
"net/http"
"github.com/OpenListTeam/gofakes3"
"github.com/itsHenry35/gofakes3"
)
// Make a new S3 Server to serve the remote
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/setting"
"github.com/OpenListTeam/gofakes3"
"github.com/itsHenry35/gofakes3"
)
type Bucket struct {