mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 21:13:10 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bdf16d596 | |||
| f8ebaec4f1 | |||
| fc23f4e781 | |||
| 3ea9984aea | |||
| cc11f354f1 | |||
| 8869874b76 | |||
| 523af855ba | |||
| bba3516693 | |||
| f244adf60e | |||
| ce87b45d2d | |||
| 4031b31837 | |||
| 9b34ca5bf1 | |||
| 666b1a039d | |||
| 8f9a09d359 | |||
| 394bb8f80e |
@@ -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/*
|
||||
|
||||
@@ -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/*
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 || "";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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 会移除 // ,手动加回去
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/gofakes3"
|
||||
"github.com/itsHenry35/gofakes3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user