mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 13:03:09 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bfd422b4a | |||
| 744021d80d | |||
| 94eac2e91e | |||
| a92d59d35c | |||
| fcae009bd3 | |||
| f63a5e15ee | |||
| d85980d3d9 | |||
| c8b0f5ebe7 | |||
| da588ba5f2 | |||
| 4ae2d1f9a8 | |||
| 379958b5ef | |||
| 8b3ef7c658 | |||
| a5f92bddaa | |||
| ae00551b4e | |||
| bedf0a9447 | |||
| 8489950a94 | |||
| d825769779 | |||
| 166af74388 | |||
| a3b10298a1 | |||
| 0b54be27a7 | |||
| 38f761f1b1 | |||
| 8a878fa98d | |||
| 12bd5a048d |
@@ -16,7 +16,7 @@ jobs:
|
||||
if: github.event_name == 'issues'
|
||||
steps:
|
||||
- name: Check issue for unchecked tasks and reply
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
let comment = "";
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Check PR title for required prefix and comment
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const title = context.payload.pull_request.title || "";
|
||||
|
||||
@@ -51,7 +51,8 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 1
|
||||
fetch-tags: true
|
||||
|
||||
- name: Install dependencies
|
||||
if: matrix.target-platform == ''
|
||||
|
||||
@@ -16,20 +16,33 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger Makefile hash update
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }}
|
||||
repository: ${{ vars.HOOK_REPO || 'OpenListTeam/OpenList-OpenWRT' }}
|
||||
event-type: update-hashes
|
||||
client-payload: |
|
||||
{
|
||||
"source_repository": "${{ github.repository }}",
|
||||
"release_tag": "${{ inputs.tag || github.ref_name }}",
|
||||
"release_name": "${{ inputs.tag || github.ref_name }}",
|
||||
"release_url": "${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.tag || github.ref_name }}",
|
||||
"triggered_by": "${{ github.actor }}",
|
||||
"trigger_reason": "${{ github.event_name }}"
|
||||
}
|
||||
github-token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }}
|
||||
script: |
|
||||
const repository = process.env.REPOSITORY;
|
||||
const [owner, repo] = repository.split('/');
|
||||
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
event_type: 'update-hashes',
|
||||
client_payload: {
|
||||
source_repository: process.env.SOURCE_REPOSITORY,
|
||||
release_tag: process.env.RELEASE_TAG,
|
||||
release_name: process.env.RELEASE_TAG,
|
||||
release_url: process.env.RELEASE_URL,
|
||||
triggered_by: process.env.ACTOR,
|
||||
trigger_reason: process.env.EVENT_NAME
|
||||
}
|
||||
});
|
||||
env:
|
||||
REPOSITORY: ${{ vars.HOOK_REPO || 'OpenListTeam/OpenList-OpenWRT' }}
|
||||
SOURCE_REPOSITORY: ${{ github.repository }}
|
||||
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
|
||||
RELEASE_URL: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.tag || github.ref_name }}
|
||||
ACTOR: ${{ github.actor }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
|
||||
- name: Log trigger information
|
||||
run: |
|
||||
|
||||
@@ -178,7 +178,6 @@ BuildWin7() {
|
||||
}
|
||||
|
||||
BuildDev() {
|
||||
rm -rf .git/
|
||||
mkdir -p "dist"
|
||||
muslflags="$(GetMuslStaticLdflags)"
|
||||
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
|
||||
@@ -269,7 +268,6 @@ BuildDockerMultiplatform() {
|
||||
}
|
||||
|
||||
BuildRelease() {
|
||||
rm -rf .git/
|
||||
mkdir -p "build"
|
||||
BuildWinArm64 ./build/"$appName"-windows-arm64.exe
|
||||
BuildWin7 ./build/"$appName"-windows7
|
||||
@@ -442,7 +440,6 @@ BuildLoongGLIBC() {
|
||||
}
|
||||
|
||||
BuildReleaseLinuxMusl() {
|
||||
rm -rf .git/
|
||||
mkdir -p "build"
|
||||
muslflags="$(GetMuslStaticLdflags)"
|
||||
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
|
||||
@@ -471,7 +468,6 @@ BuildReleaseLinuxMusl() {
|
||||
}
|
||||
|
||||
BuildReleaseLinuxMuslArm() {
|
||||
rm -rf .git/
|
||||
mkdir -p "build"
|
||||
muslflags="$(GetMuslStaticLdflags)"
|
||||
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
|
||||
@@ -502,7 +498,6 @@ BuildReleaseLinuxMuslArm() {
|
||||
|
||||
|
||||
BuildReleaseAndroid() {
|
||||
rm -rf .git/
|
||||
mkdir -p "build"
|
||||
wget https://dl.google.com/android/repository/android-ndk-r26b-linux.zip
|
||||
unzip android-ndk-r26b-linux.zip
|
||||
@@ -523,7 +518,6 @@ BuildReleaseAndroid() {
|
||||
}
|
||||
|
||||
BuildReleaseFreeBSD() {
|
||||
rm -rf .git/
|
||||
mkdir -p "build/freebsd"
|
||||
|
||||
# Get latest FreeBSD 14.x release version from GitHub
|
||||
|
||||
@@ -46,9 +46,6 @@ func (d *Wps) Init(ctx context.Context) error {
|
||||
if !resp.IsSuccess() {
|
||||
return fmt.Errorf("failed to check login status, status code: %d, body: %s", resp.StatusCode(), resp.String())
|
||||
}
|
||||
if d.login.CompanyID == 0 {
|
||||
return fmt.Errorf("wps company id is empty, please check business account login")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ require (
|
||||
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-20250228001242-d4c70e6251cc
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260701064823-d8c5600d73b8
|
||||
github.com/coreos/go-oidc v2.3.0+incompatible
|
||||
github.com/deckarep/golang-set/v2 v2.8.0
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
@@ -70,7 +70,7 @@ require (
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20251120131202-6845944c051c
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20260630094356-4de2f127f34a
|
||||
github.com/tchap/go-patricia/v2 v2.3.3
|
||||
github.com/u2takey/ffmpeg-go v0.5.0
|
||||
github.com/upyun/go-sdk/v3 v3.0.4
|
||||
@@ -132,12 +132,10 @@ require (
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/relvacode/iso8601 v1.6.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/stangelandcl/ppmd v0.1.0 // indirect
|
||||
github.com/stangelandcl/ppmd v0.1.1 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
|
||||
@@ -33,8 +33,6 @@ github.com/OpenListTeam/go-cache v0.1.0 h1:eV2+FCP+rt+E4OCJqLUW7wGccWZNJMV0NNkh+
|
||||
github.com/OpenListTeam/go-cache v0.1.0/go.mod h1:AHWjKhNK3LE4rorVdKyEALDHoeMnP8SjiNyfVlB+Pz4=
|
||||
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-20260520031008-ed3c0dbe8009 h1:qLqJPr/FAsZTJiqy65JKKuFJP0V9pRVtSaIE0kqaQ8w=
|
||||
github.com/OpenListTeam/reflink v0.0.0-20260520031008-ed3c0dbe8009/go.mod h1:WGkTOKNjd1FsJKBw3mu4JvrPEDJyJJ+JPtxBkbPoCok=
|
||||
github.com/OpenListTeam/reflink v0.0.0-20260701021214-78760eaeafef h1:67uGHancMF/abMrnkc8abVUWQiG73Wk5d8CKt3RzkFo=
|
||||
github.com/OpenListTeam/reflink v0.0.0-20260701021214-78760eaeafef/go.mod h1:WGkTOKNjd1FsJKBw3mu4JvrPEDJyJJ+JPtxBkbPoCok=
|
||||
github.com/OpenListTeam/sftpd-openlist v1.0.1 h1:j4S3iPFOpnXCUKRPS7uCT4mF2VCl34GyqvH6lqwnkUU=
|
||||
@@ -63,8 +61,6 @@ github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiU
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
|
||||
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
||||
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
||||
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
|
||||
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
|
||||
github.com/SheltonZhu/115driver v1.3.3 h1:Bqs86D2MziYPgIOuOJF+HzG4d7GBr71ZhSCrs/U17UU=
|
||||
@@ -78,10 +74,6 @@ github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9
|
||||
github.com/andreburgaud/crypt2go v1.8.0 h1:J73vGTb1P6XL69SSuumbKs0DWn3ulbl9L92ZXBjw6pc=
|
||||
github.com/andreburgaud/crypt2go v1.8.0/go.mod h1:L5nfShQ91W78hOWhUH2tlGRPO+POAPJAF5fKOLB9SXg=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
@@ -125,8 +117,8 @@ github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+I
|
||||
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
@@ -134,8 +126,6 @@ github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
|
||||
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
@@ -180,8 +170,6 @@ github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1/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.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
|
||||
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
|
||||
github.com/bodgit/sevenzip v1.6.4 h1:iHiVJfxbrB6RF4X+snI2MpVgNBKmVfGaTqZGNlMQIU0=
|
||||
github.com/bodgit/sevenzip v1.6.4/go.mod h1:ZtNi5KNgHXeXg1G7WiF0IWSuFE2eG6lt/cTGlvuirO0=
|
||||
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||
@@ -193,45 +181,28 @@ github.com/bradenaw/juniper v0.15.3/go.mod h1:UX4FX57kVSaDp4TPqvSjkAAewmRFAfXf27
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=
|
||||
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbles v0.21.1 h1:nj0decPiixaZeL9diI4uzzQTkkz1kYY8+jgzCZXSmW0=
|
||||
github.com/charmbracelet/bubbles v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk=
|
||||
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
|
||||
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4=
|
||||
github.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764=
|
||||
@@ -247,15 +218,12 @@ github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc h1:t8YjNUCt1DimB4HCIXBztwWMhgxr5yG5/YaRl9Afdfg=
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc/go.mod h1:CgWpFCFWzzEA5hVkhAc6DZZzGd3czx+BblvOzjmg6KA=
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260701064823-d8c5600d73b8 h1:+KC2I+emnT6ccWmo7IHbtchGc/vFqAxUuovo2eUgMuM=
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260701064823-d8c5600d73b8/go.mod h1:1pQXB0vAlzRlqcY7LYKOOZMw0wKfJPFxTLsJRF2Gswo=
|
||||
github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc h1:0xCWmFKBmarCqqqLeM7jFBSw/Or81UEElFqO8MY+GDs=
|
||||
github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc/go.mod h1:uvR42Hb/t52HQd7x5/ZLzZEK8oihrFpgnodIJ1vte2E=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
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-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
@@ -309,20 +277,14 @@ 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/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
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/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
|
||||
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
||||
github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q=
|
||||
github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
@@ -351,8 +313,6 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
@@ -485,19 +445,14 @@ github.com/kdomanski/iso9660 v0.4.0/go.mod h1:OxUSupHsO9ceI8lBLPJKWBTphLemjrCQY8
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -516,8 +471,6 @@ github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFG
|
||||
github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro=
|
||||
github.com/libp2p/go-libp2p v0.27.8 h1:IX5x/4yKwyPQeVS2AXHZ3J4YATM9oHBGH1gBc23jBAI=
|
||||
github.com/libp2p/go-libp2p v0.27.8/go.mod h1:eCFFtd0s5i/EVKR7+5Ki8bM7qwkNW3TPTTSSW9sz8NE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
@@ -534,24 +487,18 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/meilisearch/meilisearch-go v0.32.0 h1:cWcycpONSH3VLTZ5npUl1O5aXPkNM0vUx6bywnYqGbE=
|
||||
github.com/meilisearch/meilisearch-go v0.32.0/go.mod h1:aNtyuwurDg/ggxQIcKqWH6G9g2ptc8GyY7PLY4zMn/g=
|
||||
github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458=
|
||||
github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU=
|
||||
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
|
||||
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
|
||||
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
|
||||
github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ=
|
||||
github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
|
||||
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
@@ -599,19 +546,13 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||
github.com/ncw/swift/v2 v2.0.4 h1:hHWVFxn5/YaTWAASmn4qyq2p6OyP/Hm3vMLzkjEqR7w=
|
||||
github.com/ncw/swift/v2 v2.0.4/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
|
||||
github.com/ncw/swift/v2 v2.0.5 h1:9o5Gsd7bInAFEqsGPcaUdsboMbqf8lnNtxqWKFT9iz8=
|
||||
github.com/ncw/swift/v2 v2.0.5/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
|
||||
github.com/nwaples/rardecode/v2 v2.1.1 h1:OJaYalXdliBUXPmC8CZGQ7oZDxzX1/5mQmgn0/GASew=
|
||||
github.com/nwaples/rardecode/v2 v2.1.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
|
||||
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
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/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
@@ -619,8 +560,6 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
|
||||
github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
|
||||
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||
github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA=
|
||||
@@ -642,12 +581,8 @@ github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQP
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
|
||||
github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rclone/rclone v1.70.3 h1:rg/WNh4DmSVZyKP2tHZ4lAaWEyMi7h/F0r7smOMA3IE=
|
||||
@@ -659,7 +594,6 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rfjakob/eme v1.1.2 h1:SxziR8msSOElPayZNFfQw4Tjx/Sbaeeh3eRvrHVMUs4=
|
||||
github.com/rfjakob/eme v1.1.2/go.mod h1:cVvpasglm/G3ngEfcfT/Wt0GwhkuO32pf/poW6Nyk1k=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -673,16 +607,12 @@ github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIG
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg=
|
||||
github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk=
|
||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
@@ -695,8 +625,8 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stangelandcl/ppmd v0.1.0 h1:0gOKtSdWyXxpRW55H/dZ3AgaJqjrtlIrAvsokFwp7ug=
|
||||
github.com/stangelandcl/ppmd v0.1.0/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
|
||||
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
|
||||
github.com/stangelandcl/ppmd v0.1.1/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -717,6 +647,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20251120131202-6845944c051c h1:dtcOwRimeiBFrlutmF6K94l0rxYFARNFMA+lSQ41C+M=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20251120131202-6845944c051c/go.mod h1:BF/l2jNyK+2h/BJZ7VLMAz6m/IWjA2F67gTjV1C/+Bo=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20260630094356-4de2f127f34a h1:VQ9CmpYeTgdFvg748N6tIOHBZn+hXjJe2xNuOJHW07E=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20260630094356-4de2f127f34a/go.mod h1:BF/l2jNyK+2h/BJZ7VLMAz6m/IWjA2F67gTjV1C/+Bo=
|
||||
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 h1:6Y51mutOvRGRx6KqyMNo//xk8B8o6zW9/RVmy1VamOs=
|
||||
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=
|
||||
@@ -731,13 +663,9 @@ github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uk
|
||||
github.com/u2takey/ffmpeg-go v0.5.0/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc=
|
||||
github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys=
|
||||
github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/unknwon/goconfig v1.0.0 h1:rS7O+CmUdli1T+oDm7fYj1MwqNWtEJfNj+FqcUHML8U=
|
||||
@@ -759,7 +687,6 @@ github.com/zzzhr1990/go-common-entity v0.0.0-20250202070650-1a200048f0d3 h1:PSRw
|
||||
github.com/zzzhr1990/go-common-entity v0.0.0-20250202070650-1a200048f0d3/go.mod h1:CKriYB8bkNgSbYUQF1khSpejKb5IsV6cR7MdaAR7Fc0=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
@@ -778,15 +705,12 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
||||
gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -799,8 +723,6 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
|
||||
@@ -813,10 +735,6 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -831,8 +749,6 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
@@ -844,8 +760,6 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -860,7 +774,6 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -870,8 +783,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
@@ -884,8 +795,6 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -900,8 +809,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -915,8 +822,6 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -974,7 +879,6 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
resty.dev/v3 v3.0.0-beta.2 h1:xu4mGAdbCLuc3kbk7eddWfWm4JfhwDtdapwss5nCjnQ=
|
||||
resty.dev/v3 v3.0.0-beta.2/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
|
||||
@@ -191,4 +191,5 @@ const (
|
||||
PathKey
|
||||
SharingIDKey
|
||||
SkipHookKey
|
||||
VhostPrefixKey
|
||||
)
|
||||
|
||||
@@ -14,6 +14,16 @@ func GetSharingById(id string) (*model.SharingDB, error) {
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// GetSharingByDomain 根据绑定的域名查询 sharing 记录(用于虚拟主机能力)。
|
||||
// 仅当 sharing.Domain 字段精确匹配时返回;调用方需自行判断 Disabled / Expires / Files 等有效性。
|
||||
func GetSharingByDomain(domain string) (*model.SharingDB, error) {
|
||||
var s model.SharingDB
|
||||
if err := db.Where("domain = ?", domain).First(&s).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "failed get sharing by domain")
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func GetSharings(pageIndex, pageSize int) (sharings []model.SharingDB, count int64, err error) {
|
||||
sharingDB := db.Model(&model.SharingDB{})
|
||||
if err := sharingDB.Count(&count).Error; err != nil {
|
||||
@@ -38,6 +48,13 @@ func GetSharingsByCreatorId(creator uint, pageIndex, pageSize int) (sharings []m
|
||||
}
|
||||
|
||||
func CreateSharing(s *model.SharingDB) (string, error) {
|
||||
// domain 非空时做唯一性提前校验
|
||||
if s.Domain != "" {
|
||||
var exist model.SharingDB
|
||||
if err := db.Where("domain = ?", s.Domain).First(&exist).Error; err == nil {
|
||||
return "", errors.New("domain already used")
|
||||
}
|
||||
}
|
||||
if s.ID == "" {
|
||||
id := random.String(8)
|
||||
for len(id) < 12 {
|
||||
@@ -61,6 +78,13 @@ func CreateSharing(s *model.SharingDB) (string, error) {
|
||||
}
|
||||
|
||||
func UpdateSharing(s *model.SharingDB) error {
|
||||
// domain 非空时校验唯一性(排除自身)
|
||||
if s.Domain != "" {
|
||||
var exist model.SharingDB
|
||||
if err := db.Where("domain = ? AND id <> ?", s.Domain, s.ID).First(&exist).Error; err == nil {
|
||||
return errors.New("domain already used")
|
||||
}
|
||||
}
|
||||
return errors.WithStack(db.Save(s).Error)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ type SharingDB struct {
|
||||
Remark string `json:"remark"`
|
||||
Readme string `json:"readme" gorm:"type:text"`
|
||||
Header string `json:"header" gorm:"type:text"`
|
||||
// Domain 绑定的域名,可为空;非空时该条记录额外作为虚拟主机参与 Host 匹配(与旧 VirtualHost.Domain 等价)。
|
||||
// 唯一性由应用层在 Create/Update 时校验,避免空字符串在 MySQL 下触发 uniqueIndex 冲突。
|
||||
Domain string `json:"domain" gorm:"index"`
|
||||
// WebHosting 仅在 Domain 非空时有效;为 true 时启用 Web 托管模式(直接响应文件内容),为 false 时仅做路径重映射。
|
||||
WebHosting bool `json:"web_hosting"`
|
||||
Sort
|
||||
}
|
||||
|
||||
@@ -42,6 +47,22 @@ func (s *Sharing) Valid() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidForVhost 虚拟主机场景的有效性检查。
|
||||
// 与 Valid() 的区别:不检查 Creator.CanShare(),因为 Web Hosting / 路径重映射
|
||||
// 是服务端功能,不依赖创建者的分享权限位。
|
||||
func (s *Sharing) ValidForVhost() bool {
|
||||
if s.Disabled {
|
||||
return false
|
||||
}
|
||||
if len(s.Files) == 0 {
|
||||
return false
|
||||
}
|
||||
if s.Expires != nil && !s.Expires.IsZero() && s.Expires.Before(time.Now()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Sharing) Verify(pwd string) bool {
|
||||
return s.Pwd == "" || s.Pwd == pwd
|
||||
}
|
||||
|
||||
+94
-5
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
stdpath "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/db"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"github.com/OpenListTeam/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func makeJoined(sdb []model.SharingDB) []model.Sharing {
|
||||
@@ -42,6 +44,11 @@ func makeJoined(sdb []model.SharingDB) []model.Sharing {
|
||||
var sharingCache = cache.NewMemCache(cache.WithShards[*model.Sharing](8))
|
||||
var sharingG singleflight.Group[*model.Sharing]
|
||||
|
||||
// domainSharingCache 按虚拟主机 domain 作为 key 缓存对应的 *model.Sharing。
|
||||
// 允许缓存为 nil 以实现"负缓存"防止穿透。
|
||||
var domainSharingCache = cache.NewMemCache(cache.WithShards[*model.Sharing](2))
|
||||
var domainSharingG singleflight.Group[*model.Sharing]
|
||||
|
||||
func GetSharingById(id string, refresh ...bool) (*model.Sharing, error) {
|
||||
if !utils.IsBool(refresh...) {
|
||||
if sharing, ok := sharingCache.Get(id); ok {
|
||||
@@ -71,6 +78,66 @@ func GetSharingById(id string, refresh ...bool) (*model.Sharing, error) {
|
||||
return sharing, err
|
||||
}
|
||||
|
||||
// GetSharingByDomain 根据 domain 获取可用的虚拟主机 sharing(带缓存)。
|
||||
// 仅当 sharing.Domain 非空、Disabled=false、Files 非空、Expires 未过期时才视为有效。
|
||||
// 如果在 DB 中未找到,会负缓存 5 分钟,避免反复穿透 DB。
|
||||
func GetSharingByDomain(domain string) (*model.Sharing, error) {
|
||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||
if domain == "" {
|
||||
return nil, errors.New("empty domain")
|
||||
}
|
||||
if s, ok := domainSharingCache.Get(domain); ok {
|
||||
if s == nil {
|
||||
log.Debugf("[Sharing] domain cache hit (nil) for %q", domain)
|
||||
return nil, errors.New("sharing not found by domain")
|
||||
}
|
||||
log.Debugf("[Sharing] domain cache hit for %q id=%s", domain, s.ID)
|
||||
if !s.ValidForVhost() {
|
||||
return nil, errors.New("sharing not valid")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
sharing, err, _ := domainSharingG.Do(domain, func() (*model.Sharing, error) {
|
||||
sdb, err := db.GetSharingByDomain(domain)
|
||||
if err != nil {
|
||||
if errors.Is(errors.Cause(err), gorm.ErrRecordNotFound) {
|
||||
log.Debugf("[Sharing] domain=%q not found in db, caching nil", domain)
|
||||
domainSharingCache.Set(domain, nil, cache.WithEx[*model.Sharing](time.Minute*5))
|
||||
return nil, errors.New("sharing not found by domain")
|
||||
}
|
||||
return nil, errors.WithMessagef(err, "failed get sharing by domain [%s]", domain)
|
||||
}
|
||||
// 虚拟主机场景不需要 creator,跳过 creator 查询以避免 CanShare 校验阻断 Web Hosting
|
||||
var files []string
|
||||
if err = utils.Json.UnmarshalFromString(sdb.FilesRaw, &files); err != nil {
|
||||
files = make([]string, 0)
|
||||
}
|
||||
s := &model.Sharing{
|
||||
SharingDB: sdb,
|
||||
Files: files,
|
||||
Creator: nil, // 虚拟主机匹配不依赖 creator 权限
|
||||
}
|
||||
domainSharingCache.Set(domain, s, cache.WithEx[*model.Sharing](time.Hour))
|
||||
return s, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sharing == nil || !sharing.ValidForVhost() {
|
||||
return nil, errors.New("sharing not valid for domain")
|
||||
}
|
||||
return sharing, nil
|
||||
}
|
||||
|
||||
// invalidateDomainCache 在创建/更新/删除记录时调用,同时传入新/旧 domain 以使两者都失效。
|
||||
func invalidateDomainCache(domains ...string) {
|
||||
for _, d := range domains {
|
||||
if d != "" {
|
||||
domainSharingCache.Del(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetSharings(pageIndex, pageSize int) ([]model.Sharing, int64, error) {
|
||||
s, cnt, err := db.GetSharings(pageIndex, pageSize)
|
||||
if err != nil {
|
||||
@@ -118,7 +185,11 @@ func CreateSharing(sharing *model.Sharing) (id string, err error) {
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
return db.CreateSharing(sharing.SharingDB)
|
||||
id, err = db.CreateSharing(sharing.SharingDB)
|
||||
if err == nil {
|
||||
invalidateDomainCache(sharing.Domain)
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
||||
func UpdateSharing(sharing *model.Sharing, skipMarshal ...bool) (err error) {
|
||||
@@ -129,8 +200,17 @@ func UpdateSharing(sharing *model.Sharing, skipMarshal ...bool) (err error) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
sharingCache.Del(sharing.ID)
|
||||
return db.UpdateSharing(sharing.SharingDB)
|
||||
// 读取旧记录以便同时失效旧 domain 缓存
|
||||
var oldDomain string
|
||||
if old, e := db.GetSharingById(sharing.ID); e == nil {
|
||||
oldDomain = old.Domain
|
||||
}
|
||||
err = db.UpdateSharing(sharing.SharingDB)
|
||||
if err == nil {
|
||||
sharingCache.Del(sharing.ID)
|
||||
invalidateDomainCache(oldDomain, sharing.Domain)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateSharingId(sharing *model.Sharing, newId string) error {
|
||||
@@ -143,8 +223,17 @@ func UpdateSharingId(sharing *model.Sharing, newId string) error {
|
||||
}
|
||||
|
||||
func DeleteSharing(sid string) error {
|
||||
sharingCache.Del(sid)
|
||||
return db.DeleteSharingById(sid)
|
||||
// 先读取 domain 用于失效缓存
|
||||
var oldDomain string
|
||||
if old, e := db.GetSharingById(sid); e == nil {
|
||||
oldDomain = old.Domain
|
||||
}
|
||||
err := db.DeleteSharingById(sid)
|
||||
if err == nil {
|
||||
sharingCache.Del(sid)
|
||||
invalidateDomainCache(oldDomain)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteSharingsByCreatorId(creatorId uint) error {
|
||||
|
||||
+372
-49
@@ -1,80 +1,365 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package generic_sync
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// MapOf is like a Go map[interface{}]interface{} but is safe for concurrent use
|
||||
// by multiple goroutines without additional locking or coordination.
|
||||
// Loads, stores, and deletes run in amortized constant time.
|
||||
//
|
||||
// The MapOf type is specialized. Most code should use a plain Go map instead,
|
||||
// with separate locking or coordination, for better type safety and to make it
|
||||
// easier to maintain other invariants along with the map content.
|
||||
//
|
||||
// The MapOf type is optimized for two common use cases: (1) when the entry for a given
|
||||
// key is only ever written once but read many times, as in caches that only grow,
|
||||
// or (2) when multiple goroutines read, write, and overwrite entries for disjoint
|
||||
// sets of keys. In these two cases, use of a MapOf may significantly reduce lock
|
||||
// contention compared to a Go map paired with a separate Mutex or RWMutex.
|
||||
//
|
||||
// The zero MapOf is empty and ready for use. A MapOf must not be copied after first use.
|
||||
type MapOf[K comparable, V any] struct {
|
||||
sync.Map
|
||||
mu sync.Mutex
|
||||
|
||||
// read contains the portion of the map's contents that are safe for
|
||||
// concurrent access (with or without mu held).
|
||||
//
|
||||
// The read field itself is always safe to load, but must only be stored with
|
||||
// mu held.
|
||||
//
|
||||
// Entries stored in read may be updated concurrently without mu, but updating
|
||||
// a previously-expunged entry requires that the entry be copied to the dirty
|
||||
// map and unexpunged with mu held.
|
||||
read atomic.Value // readOnly
|
||||
|
||||
// dirty contains the portion of the map's contents that require mu to be
|
||||
// held. To ensure that the dirty map can be promoted to the read map quickly,
|
||||
// it also includes all of the non-expunged entries in the read map.
|
||||
//
|
||||
// Expunged entries are not stored in the dirty map. An expunged entry in the
|
||||
// clean map must be unexpunged and added to the dirty map before a new value
|
||||
// can be stored to it.
|
||||
//
|
||||
// If the dirty map is nil, the next write to the map will initialize it by
|
||||
// making a shallow copy of the clean map, omitting stale entries.
|
||||
dirty map[K]*entry[V]
|
||||
|
||||
// misses counts the number of loads since the read map was last updated that
|
||||
// needed to lock mu to determine whether the key was present.
|
||||
//
|
||||
// Once enough misses have occurred to cover the cost of copying the dirty
|
||||
// map, the dirty map will be promoted to the read map (in the unamended
|
||||
// state) and the next store to the map will make a new dirty copy.
|
||||
misses int
|
||||
}
|
||||
|
||||
func (m *MapOf[K, V]) CompareAndDelete(key K, old V) (deleted bool) {
|
||||
return m.Map.CompareAndDelete(key, old)
|
||||
// readOnly is an immutable struct stored atomically in the MapOf.read field.
|
||||
type readOnly[K comparable, V any] struct {
|
||||
m map[K]*entry[V]
|
||||
amended bool // true if the dirty map contains some key not in m.
|
||||
}
|
||||
func (m *MapOf[K, V]) CompareAndSwap(key K, old V, new V) (swapped bool) {
|
||||
return m.Map.CompareAndSwap(key, old, new)
|
||||
|
||||
// expunged is an arbitrary pointer that marks entries which have been deleted
|
||||
// from the dirty map.
|
||||
var expunged = unsafe.Pointer(new(interface{}))
|
||||
|
||||
// An entry is a slot in the map corresponding to a particular key.
|
||||
type entry[V any] struct {
|
||||
// p points to the value stored for the entry.
|
||||
//
|
||||
// If p == nil, the entry has been deleted and m.dirty == nil.
|
||||
//
|
||||
// If p == expungedFor[V](), the entry has been deleted, m.dirty != nil, and
|
||||
// the entry is missing from m.dirty.
|
||||
//
|
||||
// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
|
||||
// != nil, in m.dirty[key].
|
||||
//
|
||||
// An entry can be deleted by atomic replacement with nil: when m.dirty is
|
||||
// next created, it will atomically replace nil with expunged and leave
|
||||
// m.dirty[key] unset.
|
||||
//
|
||||
// An entry's associated value can be updated by atomic replacement, provided
|
||||
// p != expunged. If p == expungedFor[V](), an entry's associated value can be updated
|
||||
// only after first setting m.dirty[key] = e so that lookups using the dirty
|
||||
// map find the entry.
|
||||
p atomic.Pointer[V]
|
||||
}
|
||||
func (m *MapOf[K, V]) Delete(key K) {
|
||||
m.Map.Delete(key)
|
||||
|
||||
func expungedFor[V any]() *V {
|
||||
return (*V)(expunged)
|
||||
}
|
||||
|
||||
func newEntry[V any](i V) *entry[V] {
|
||||
e := &entry[V]{}
|
||||
e.p.Store(&i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Load returns the value stored in the map for a key, or nil if no
|
||||
// value is present.
|
||||
// The ok result indicates whether value was found in the map.
|
||||
func (m *MapOf[K, V]) Load(key K) (value V, ok bool) {
|
||||
v, ok := m.Map.Load(key)
|
||||
if ok {
|
||||
value = v.(V)
|
||||
read, _ := m.read.Load().(readOnly[K, V])
|
||||
e, ok := read.m[key]
|
||||
if !ok && read.amended {
|
||||
m.mu.Lock()
|
||||
// Avoid reporting a spurious miss if m.dirty got promoted while we were
|
||||
// blocked on m.mu. (If further loads of the same key will not miss, it's
|
||||
// not worth copying the dirty map for this key.)
|
||||
read, _ = m.read.Load().(readOnly[K, V])
|
||||
e, ok = read.m[key]
|
||||
if !ok && read.amended {
|
||||
e, ok = m.dirty[key]
|
||||
// Regardless of whether the entry was present, record a miss: this key
|
||||
// will take the slow path until the dirty map is promoted to the read
|
||||
// map.
|
||||
m.missLocked()
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
return value, ok
|
||||
}
|
||||
func (m *MapOf[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
|
||||
v, loaded := m.Map.LoadAndDelete(key)
|
||||
if loaded {
|
||||
value = v.(V)
|
||||
if !ok {
|
||||
return value, false
|
||||
}
|
||||
return value, loaded
|
||||
}
|
||||
func (m *MapOf[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
|
||||
a, loaded := m.Map.LoadOrStore(key, value)
|
||||
if loaded {
|
||||
actual = a.(V)
|
||||
}
|
||||
return actual, loaded
|
||||
}
|
||||
func (m *MapOf[K, V]) Range(f func(key K, value V) bool) {
|
||||
m.Map.Range(func(key, value any) bool {
|
||||
return f(key.(K), value.(V))
|
||||
})
|
||||
}
|
||||
func (m *MapOf[K, V]) Store(key K, value V) {
|
||||
m.Map.Store(key, value)
|
||||
}
|
||||
func (m *MapOf[K, V]) Swap(key K, value V) (previous V, loaded bool) {
|
||||
p, loaded := m.Map.Swap(key, value)
|
||||
if loaded {
|
||||
previous = p.(V)
|
||||
}
|
||||
return previous, loaded
|
||||
return e.load()
|
||||
}
|
||||
|
||||
func (m *MapOf[K, V]) Has(key K) bool {
|
||||
_, ok := m.Map.Load(key)
|
||||
_, ok := m.Load(key)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (e *entry[V]) load() (value V, ok bool) {
|
||||
p := e.p.Load()
|
||||
if p == nil || p == expungedFor[V]() {
|
||||
return value, false
|
||||
}
|
||||
return *(*V)(p), true
|
||||
}
|
||||
|
||||
// Store sets the value for a key.
|
||||
func (m *MapOf[K, V]) Store(key K, value V) {
|
||||
read, _ := m.read.Load().(readOnly[K, V])
|
||||
if e, ok := read.m[key]; ok && e.tryStore(&value) {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
read, _ = m.read.Load().(readOnly[K, V])
|
||||
if e, ok := read.m[key]; ok {
|
||||
if e.unexpungeLocked() {
|
||||
// The entry was previously expunged, which implies that there is a
|
||||
// non-nil dirty map and this entry is not in it.
|
||||
m.dirty[key] = e
|
||||
}
|
||||
e.storeLocked(&value)
|
||||
} else if e, ok := m.dirty[key]; ok {
|
||||
e.storeLocked(&value)
|
||||
} else {
|
||||
if !read.amended {
|
||||
// We're adding the first new key to the dirty map.
|
||||
// Make sure it is allocated and mark the read-only map as incomplete.
|
||||
m.dirtyLocked()
|
||||
m.read.Store(readOnly[K, V]{m: read.m, amended: true})
|
||||
}
|
||||
m.dirty[key] = newEntry(value)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// tryStore stores a value if the entry has not been expunged.
|
||||
//
|
||||
// If the entry is expunged, tryStore returns false and leaves the entry
|
||||
// unchanged.
|
||||
func (e *entry[V]) tryStore(i *V) bool {
|
||||
for {
|
||||
p := e.p.Load()
|
||||
if p == expungedFor[V]() {
|
||||
return false
|
||||
}
|
||||
if e.p.CompareAndSwap(p, i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unexpungeLocked ensures that the entry is not marked as expunged.
|
||||
//
|
||||
// If the entry was previously expunged, it must be added to the dirty map
|
||||
// before m.mu is unlocked.
|
||||
func (e *entry[V]) unexpungeLocked() (wasExpunged bool) {
|
||||
return e.p.CompareAndSwap(expungedFor[V](), nil)
|
||||
}
|
||||
|
||||
// storeLocked unconditionally stores a value to the entry.
|
||||
//
|
||||
// The entry must be known not to be expunged.
|
||||
func (e *entry[V]) storeLocked(i *V) {
|
||||
e.p.Store(i)
|
||||
}
|
||||
|
||||
// LoadOrStore returns the existing value for the key if present.
|
||||
// Otherwise, it stores and returns the given value.
|
||||
// The loaded result is true if the value was loaded, false if stored.
|
||||
func (m *MapOf[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
|
||||
// Avoid locking if it's a clean hit.
|
||||
read, _ := m.read.Load().(readOnly[K, V])
|
||||
if e, ok := read.m[key]; ok {
|
||||
actual, loaded, ok := e.tryLoadOrStore(value)
|
||||
if ok {
|
||||
return actual, loaded
|
||||
}
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
read, _ = m.read.Load().(readOnly[K, V])
|
||||
if e, ok := read.m[key]; ok {
|
||||
if e.unexpungeLocked() {
|
||||
m.dirty[key] = e
|
||||
}
|
||||
actual, loaded, _ = e.tryLoadOrStore(value)
|
||||
} else if e, ok := m.dirty[key]; ok {
|
||||
actual, loaded, _ = e.tryLoadOrStore(value)
|
||||
m.missLocked()
|
||||
} else {
|
||||
if !read.amended {
|
||||
// We're adding the first new key to the dirty map.
|
||||
// Make sure it is allocated and mark the read-only map as incomplete.
|
||||
m.dirtyLocked()
|
||||
m.read.Store(readOnly[K, V]{m: read.m, amended: true})
|
||||
}
|
||||
m.dirty[key] = newEntry(value)
|
||||
actual, loaded = value, false
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
return actual, loaded
|
||||
}
|
||||
|
||||
// tryLoadOrStore atomically loads or stores a value if the entry is not
|
||||
// expunged.
|
||||
//
|
||||
// If the entry is expunged, tryLoadOrStore leaves the entry unchanged and
|
||||
// returns with ok==false.
|
||||
func (e *entry[V]) tryLoadOrStore(i V) (actual V, loaded, ok bool) {
|
||||
p := e.p.Load()
|
||||
if p == expungedFor[V]() {
|
||||
return actual, false, false
|
||||
}
|
||||
if p != nil {
|
||||
return *(*V)(p), true, true
|
||||
}
|
||||
|
||||
// Copy the value after the first load to make this method more amenable
|
||||
// to escape analysis: if we hit the "load" path or the entry is expunged, we
|
||||
// shouldn't bother heap-allocating.
|
||||
ic := i
|
||||
for {
|
||||
if e.p.CompareAndSwap(nil, &ic) {
|
||||
return i, false, true
|
||||
}
|
||||
p = e.p.Load()
|
||||
if p == expungedFor[V]() {
|
||||
return actual, false, false
|
||||
}
|
||||
if p != nil {
|
||||
return *(*V)(p), true, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deletes the value for a key.
|
||||
func (m *MapOf[K, V]) Delete(key K) {
|
||||
read, _ := m.read.Load().(readOnly[K, V])
|
||||
e, ok := read.m[key]
|
||||
if !ok && read.amended {
|
||||
m.mu.Lock()
|
||||
read, _ = m.read.Load().(readOnly[K, V])
|
||||
e, ok = read.m[key]
|
||||
if !ok && read.amended {
|
||||
delete(m.dirty, key)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
if ok {
|
||||
e.delete()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *entry[V]) delete() (hadValue bool) {
|
||||
for {
|
||||
p := e.p.Load()
|
||||
if p == nil || p == expungedFor[V]() {
|
||||
return false
|
||||
}
|
||||
if e.p.CompareAndSwap(p, nil) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Range calls f sequentially for each key and value present in the map.
|
||||
// If f returns false, range stops the iteration.
|
||||
//
|
||||
// Range does not necessarily correspond to any consistent snapshot of the MapOf's
|
||||
// contents: no key will be visited more than once, but if the value for any key
|
||||
// is stored or deleted concurrently, Range may reflect any mapping for that key
|
||||
// from any point during the Range call.
|
||||
//
|
||||
// Range may be O(N) with the number of elements in the map even if f returns
|
||||
// false after a constant number of calls.
|
||||
func (m *MapOf[K, V]) Range(f func(key K, value V) bool) {
|
||||
// We need to be able to iterate over all of the keys that were already
|
||||
// present at the start of the call to Range.
|
||||
// If read.amended is false, then read.m satisfies that property without
|
||||
// requiring us to hold m.mu for a long time.
|
||||
read, _ := m.read.Load().(readOnly[K, V])
|
||||
if read.amended {
|
||||
// m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
|
||||
// (assuming the caller does not break out early), so a call to Range
|
||||
// amortizes an entire copy of the map: we can promote the dirty copy
|
||||
// immediately!
|
||||
m.mu.Lock()
|
||||
read, _ = m.read.Load().(readOnly[K, V])
|
||||
if read.amended {
|
||||
read = readOnly[K, V]{m: m.dirty}
|
||||
m.read.Store(read)
|
||||
m.dirty = nil
|
||||
m.misses = 0
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
for k, e := range read.m {
|
||||
v, ok := e.load()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !f(k, v) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Values returns a slice of the values in the map.
|
||||
func (m *MapOf[K, V]) Values() []V {
|
||||
var values []V
|
||||
m.Map.Range(func(_, value any) bool {
|
||||
values = append(values, value.(V))
|
||||
m.Range(func(key K, value V) bool {
|
||||
values = append(values, value)
|
||||
return true
|
||||
})
|
||||
return values
|
||||
}
|
||||
|
||||
func (m *MapOf[K, V]) Count() int {
|
||||
count := 0
|
||||
m.Map.Range(func(_, _ any) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
return count
|
||||
return len(m.dirty)
|
||||
}
|
||||
|
||||
func (m *MapOf[K, V]) Empty() bool {
|
||||
@@ -91,5 +376,43 @@ func (m *MapOf[K, V]) ToMap() map[K]V {
|
||||
}
|
||||
|
||||
func (m *MapOf[K, V]) Clear() {
|
||||
m.Map.Clear()
|
||||
m.Range(func(key K, value V) bool {
|
||||
m.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *MapOf[K, V]) missLocked() {
|
||||
m.misses++
|
||||
if m.misses < len(m.dirty) {
|
||||
return
|
||||
}
|
||||
m.read.Store(readOnly[K, V]{m: m.dirty})
|
||||
m.dirty = nil
|
||||
m.misses = 0
|
||||
}
|
||||
|
||||
func (m *MapOf[K, V]) dirtyLocked() {
|
||||
if m.dirty != nil {
|
||||
return
|
||||
}
|
||||
|
||||
read, _ := m.read.Load().(readOnly[K, V])
|
||||
m.dirty = make(map[K]*entry[V], len(read.m))
|
||||
for k, e := range read.m {
|
||||
if !e.tryExpungeLocked() {
|
||||
m.dirty[k] = e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *entry[V]) tryExpungeLocked() (isExpunged bool) {
|
||||
p := e.p.Load()
|
||||
for p == nil {
|
||||
if e.p.CompareAndSwap(nil, expungedFor[V]()) {
|
||||
return true
|
||||
}
|
||||
p = e.p.Load()
|
||||
}
|
||||
return p == expungedFor[V]()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package generic_sync_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/generic_sync"
|
||||
)
|
||||
|
||||
func TestConcurrentRange(t *testing.T) {
|
||||
const mapSize = 1 << 10
|
||||
|
||||
m := new(generic_sync.MapOf[int64, int64])
|
||||
for n := int64(1); n <= mapSize; n++ {
|
||||
m.Store(n, int64(n))
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
defer func() {
|
||||
close(done)
|
||||
wg.Wait()
|
||||
}()
|
||||
for g := int64(runtime.GOMAXPROCS(0)); g > 0; g-- {
|
||||
r := rand.New(rand.NewSource(g))
|
||||
wg.Add(1)
|
||||
go func(g int64) {
|
||||
defer wg.Done()
|
||||
for i := int64(0); ; i++ {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
for n := int64(1); n < mapSize; n++ {
|
||||
if r.Int63n(mapSize) == 0 {
|
||||
m.Store(n, n*i*g)
|
||||
} else {
|
||||
m.Load(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(g)
|
||||
}
|
||||
|
||||
iters := 1 << 10
|
||||
if testing.Short() {
|
||||
iters = 16
|
||||
}
|
||||
for n := iters; n > 0; n-- {
|
||||
seen := make(map[int64]bool, mapSize)
|
||||
|
||||
m.Range(func(k, v int64) bool {
|
||||
if v%k != 0 {
|
||||
t.Fatalf("while Storing multiples of %v, Range saw value %v", k, v)
|
||||
}
|
||||
if seen[k] {
|
||||
t.Fatalf("Range visited key %v twice", k)
|
||||
}
|
||||
seen[k] = true
|
||||
return true
|
||||
})
|
||||
|
||||
if len(seen) != mapSize {
|
||||
t.Fatalf("Range visited %v elements of %v-element MapOf", len(seen), mapSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package task
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrTaskNotFound = errors.New("task not found")
|
||||
ErrTaskRunning = errors.New("task is running")
|
||||
)
|
||||
@@ -0,0 +1,145 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/generic_sync"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Manager[K comparable] struct {
|
||||
curID K
|
||||
workerC chan struct{}
|
||||
updateID func(*K)
|
||||
tasks generic_sync.MapOf[K, *Task[K]]
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) Submit(task *Task[K]) K {
|
||||
if tm.updateID != nil {
|
||||
tm.updateID(&tm.curID)
|
||||
task.ID = tm.curID
|
||||
}
|
||||
tm.tasks.Store(task.ID, task)
|
||||
tm.do(task)
|
||||
return task.ID
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) do(task *Task[K]) {
|
||||
go func() {
|
||||
log.Debugf("task [%s] waiting for worker", task.Name)
|
||||
select {
|
||||
case <-tm.workerC:
|
||||
log.Debugf("task [%s] starting", task.Name)
|
||||
task.run()
|
||||
log.Debugf("task [%s] ended", task.Name)
|
||||
case <-task.Ctx.Done():
|
||||
log.Debugf("task [%s] canceled", task.Name)
|
||||
return
|
||||
}
|
||||
// return worker
|
||||
tm.workerC <- struct{}{}
|
||||
}()
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) GetAll() []*Task[K] {
|
||||
return tm.tasks.Values()
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) Get(tid K) (*Task[K], bool) {
|
||||
return tm.tasks.Load(tid)
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) MustGet(tid K) *Task[K] {
|
||||
task, _ := tm.Get(tid)
|
||||
return task
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) Retry(tid K) error {
|
||||
t, ok := tm.Get(tid)
|
||||
if !ok {
|
||||
return errors.WithStack(ErrTaskNotFound)
|
||||
}
|
||||
tm.do(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) Cancel(tid K) error {
|
||||
t, ok := tm.Get(tid)
|
||||
if !ok {
|
||||
return errors.WithStack(ErrTaskNotFound)
|
||||
}
|
||||
t.Cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) Remove(tid K) error {
|
||||
t, ok := tm.Get(tid)
|
||||
if !ok {
|
||||
return errors.WithStack(ErrTaskNotFound)
|
||||
}
|
||||
if !t.Done() {
|
||||
return errors.WithStack(ErrTaskRunning)
|
||||
}
|
||||
tm.tasks.Delete(tid)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAll removes all tasks from the manager, this maybe shouldn't be used
|
||||
// because the task maybe still running.
|
||||
func (tm *Manager[K]) RemoveAll() {
|
||||
tm.tasks.Clear()
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) RemoveByStates(states ...string) {
|
||||
tasks := tm.GetAll()
|
||||
for _, task := range tasks {
|
||||
if utils.SliceContains(states, task.GetState()) {
|
||||
_ = tm.Remove(task.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) GetByStates(states ...string) []*Task[K] {
|
||||
var tasks []*Task[K]
|
||||
tm.tasks.Range(func(key K, value *Task[K]) bool {
|
||||
if utils.SliceContains(states, value.GetState()) {
|
||||
tasks = append(tasks, value)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return tasks
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) ListUndone() []*Task[K] {
|
||||
return tm.GetByStates(PENDING, RUNNING, CANCELING)
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) ListDone() []*Task[K] {
|
||||
return tm.GetByStates(SUCCEEDED, CANCELED, ERRORED)
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) ClearDone() {
|
||||
tm.RemoveByStates(SUCCEEDED, CANCELED, ERRORED)
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) ClearSucceeded() {
|
||||
tm.RemoveByStates(SUCCEEDED)
|
||||
}
|
||||
|
||||
func (tm *Manager[K]) RawTasks() *generic_sync.MapOf[K, *Task[K]] {
|
||||
return &tm.tasks
|
||||
}
|
||||
|
||||
func NewTaskManager[K comparable](maxWorker int, updateID ...func(*K)) *Manager[K] {
|
||||
tm := &Manager[K]{
|
||||
tasks: generic_sync.MapOf[K, *Task[K]]{},
|
||||
workerC: make(chan struct{}, maxWorker),
|
||||
}
|
||||
for i := 0; i < maxWorker; i++ {
|
||||
tm.workerC <- struct{}{}
|
||||
}
|
||||
if len(updateID) > 0 {
|
||||
tm.updateID = updateID[0]
|
||||
}
|
||||
return tm
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Package task manage task, such as file upload, file copy between storages, offline download, etc.
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
SUCCEEDED = "succeeded"
|
||||
CANCELING = "canceling"
|
||||
CANCELED = "canceled"
|
||||
ERRORED = "errored"
|
||||
)
|
||||
|
||||
type Func[K comparable] func(task *Task[K]) error
|
||||
type Callback[K comparable] func(task *Task[K])
|
||||
|
||||
type Task[K comparable] struct {
|
||||
ID K
|
||||
Name string
|
||||
state string // pending, running, finished, canceling, canceled, errored
|
||||
status string
|
||||
progress float64
|
||||
|
||||
Error error
|
||||
|
||||
Func Func[K]
|
||||
callback Callback[K]
|
||||
|
||||
Ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (t *Task[K]) SetStatus(status string) {
|
||||
t.status = status
|
||||
}
|
||||
|
||||
func (t *Task[K]) SetProgress(percentage float64) {
|
||||
t.progress = percentage
|
||||
}
|
||||
|
||||
func (t Task[K]) GetProgress() float64 {
|
||||
return t.progress
|
||||
}
|
||||
|
||||
func (t Task[K]) GetState() string {
|
||||
return t.state
|
||||
}
|
||||
|
||||
func (t Task[K]) GetStatus() string {
|
||||
return t.status
|
||||
}
|
||||
|
||||
func (t Task[K]) GetErrMsg() string {
|
||||
if t.Error == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Error.Error()
|
||||
}
|
||||
|
||||
func getCurrentGoroutineStack() string {
|
||||
buf := make([]byte, 1<<16)
|
||||
n := runtime.Stack(buf, false)
|
||||
return string(buf[:n])
|
||||
}
|
||||
|
||||
func (t *Task[K]) run() {
|
||||
t.state = RUNNING
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Errorf("error [%s] while run task [%s],stack trace:\n%s", err, t.Name, getCurrentGoroutineStack())
|
||||
t.Error = errors.Errorf("panic: %+v", err)
|
||||
t.state = ERRORED
|
||||
}
|
||||
}()
|
||||
t.Error = t.Func(t)
|
||||
if t.Error != nil {
|
||||
log.Errorf("error [%+v] while run task [%s]", t.Error, t.Name)
|
||||
}
|
||||
if errors.Is(t.Ctx.Err(), context.Canceled) {
|
||||
t.state = CANCELED
|
||||
} else if t.Error != nil {
|
||||
t.state = ERRORED
|
||||
} else {
|
||||
t.state = SUCCEEDED
|
||||
t.SetProgress(100)
|
||||
if t.callback != nil {
|
||||
t.callback(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Task[K]) retry() {
|
||||
t.run()
|
||||
}
|
||||
|
||||
func (t *Task[K]) Done() bool {
|
||||
return t.state == SUCCEEDED || t.state == CANCELED || t.state == ERRORED
|
||||
}
|
||||
|
||||
func (t *Task[K]) Cancel() {
|
||||
if t.state == SUCCEEDED || t.state == CANCELED {
|
||||
return
|
||||
}
|
||||
// maybe can't cancel
|
||||
t.state = CANCELING
|
||||
if t.cancel != nil {
|
||||
t.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func WithCancelCtx[K comparable](task *Task[K]) *Task[K] {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
task.Ctx = ctx
|
||||
task.cancel = cancel
|
||||
task.state = PENDING
|
||||
return task
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestTask_Manager(t *testing.T) {
|
||||
var next atomic.Uint64
|
||||
tm := NewTaskManager(3, func(id *uint64) {
|
||||
*id = next.Add(1)
|
||||
})
|
||||
id := tm.Submit(WithCancelCtx(&Task[uint64]{
|
||||
Name: "test",
|
||||
Func: func(task *Task[uint64]) error {
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
task, ok := tm.Get(id)
|
||||
if !ok {
|
||||
t.Fatal("task not found")
|
||||
}
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
if task.state != RUNNING {
|
||||
t.Errorf("task status not running: %s", task.state)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
if task.state != SUCCEEDED {
|
||||
t.Errorf("task status not finished: %s", task.state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTask_Cancel(t *testing.T) {
|
||||
var next atomic.Uint64
|
||||
tm := NewTaskManager(3, func(id *uint64) {
|
||||
*id = next.Add(1)
|
||||
})
|
||||
id := tm.Submit(WithCancelCtx(&Task[uint64]{
|
||||
Name: "test",
|
||||
Func: func(task *Task[uint64]) error {
|
||||
for {
|
||||
if utils.IsCanceled(task.Ctx) {
|
||||
return nil
|
||||
} else {
|
||||
t.Logf("task is running")
|
||||
}
|
||||
}
|
||||
},
|
||||
}))
|
||||
task, ok := tm.Get(id)
|
||||
if !ok {
|
||||
t.Fatal("task not found")
|
||||
}
|
||||
time.Sleep(time.Microsecond * 50)
|
||||
task.Cancel()
|
||||
time.Sleep(time.Millisecond)
|
||||
if task.state != CANCELED {
|
||||
t.Errorf("task status not canceled: %s", task.state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTask_Retry(t *testing.T) {
|
||||
var next atomic.Uint64
|
||||
tm := NewTaskManager(3, func(id *uint64) {
|
||||
*id = next.Add(1)
|
||||
})
|
||||
num := 0
|
||||
id := tm.Submit(WithCancelCtx(&Task[uint64]{
|
||||
Name: "test",
|
||||
Func: func(task *Task[uint64]) error {
|
||||
num++
|
||||
if num&1 == 1 {
|
||||
return errors.New("test error")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
task, ok := tm.Get(id)
|
||||
if !ok {
|
||||
t.Fatal("task not found")
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
if task.Error == nil {
|
||||
t.Error(task.state)
|
||||
t.Fatal("task error is nil, but expected error")
|
||||
} else {
|
||||
t.Logf("task error: %s", task.Error)
|
||||
}
|
||||
task.retry()
|
||||
time.Sleep(time.Millisecond)
|
||||
if task.Error != nil {
|
||||
t.Errorf("task error: %+v, but expected nil", task.Error)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
stdnet "net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -155,3 +156,14 @@ func ContentWithValues(ctx context.Context, keyAndValue ...any) context.Context
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// StripHostPort 从 Host 头中去掉端口部分,返回纯域名。
|
||||
// 支持 IPv4、IPv6([::1]:port)及无端口的裸域名/IP。
|
||||
func StripHostPort(host string) string {
|
||||
h, _, err := stdnet.SplitHostPort(host)
|
||||
if err != nil {
|
||||
// 无端口,原样返回
|
||||
return host
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ func FsListSplit(c *gin.Context) {
|
||||
SharingList(c, &req)
|
||||
return
|
||||
}
|
||||
// 虚拟主机路径重映射:根据 Host 头匹配虚拟主机规则,将请求路径映射到实际路径
|
||||
req.Path = applyVhostPathMapping(c, req.Path)
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
if user.IsGuest() && user.Disabled {
|
||||
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
||||
@@ -272,6 +274,11 @@ func FsGetSplit(c *gin.Context) {
|
||||
SharingGet(c, &req)
|
||||
return
|
||||
}
|
||||
// 虚拟主机路径重映射:根据 Host 头匹配虚拟主机规则,将请求路径映射到实际路径
|
||||
// 同时将 vhost.Path 前缀存入 context,供 FsGet 生成 /p/ 链接时去掉前缀
|
||||
var vhostPrefix string
|
||||
req.Path, vhostPrefix = applyVhostPathMappingWithPrefix(c, req.Path)
|
||||
common.GinAppendValues(c, conf.VhostPrefixKey, vhostPrefix)
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
if user.IsGuest() && user.Disabled {
|
||||
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
||||
@@ -319,12 +326,14 @@ func FsGet(c *gin.Context, req *FsGetReq, user *model.User) {
|
||||
rawURL = common.GenerateDownProxyURL(storage.GetStorage(), reqPath)
|
||||
if rawURL == "" {
|
||||
query := ""
|
||||
// 生成 /p/ 链接时,去掉 vhost 路径前缀,保持前端看到的路径一致
|
||||
downPath := stripVhostPrefix(c, reqPath)
|
||||
if isEncrypt(meta, reqPath) || setting.GetBool(conf.SignAll) {
|
||||
query = "?sign=" + sign.Sign(reqPath)
|
||||
}
|
||||
rawURL = fmt.Sprintf("%s/p%s%s",
|
||||
common.GetApiUrl(c),
|
||||
utils.EncodePath(reqPath, true),
|
||||
utils.EncodePath(downPath, true),
|
||||
query)
|
||||
}
|
||||
} else {
|
||||
@@ -427,3 +436,62 @@ func FsOther(c *gin.Context) {
|
||||
}
|
||||
common.SuccessResp(c, res)
|
||||
}
|
||||
|
||||
// applyVhostPathMapping 根据请求的 Host 头匹配虚拟主机规则,将请求路径映射到实际路径。
|
||||
func applyVhostPathMapping(c *gin.Context, reqPath string) string {
|
||||
mapped, _ := applyVhostPathMappingWithPrefix(c, reqPath)
|
||||
return mapped
|
||||
}
|
||||
|
||||
// applyVhostPathMappingWithPrefix 根据请求的 Host 头匹配 sharing 中带 Domain 的虚拟主机记录,
|
||||
// 将请求路径映射到 sharing.Files[0] 之下,同时返回该路径前缀(用于生成下载链接时去掉前缀)。
|
||||
// 例如:sharing.Files[0]="/123pan/Downloads",reqPath="/",则返回 ("/123pan/Downloads", "/123pan/Downloads")
|
||||
// 例如:sharing.Files[0]="/123pan/Downloads",reqPath="/subdir",则返回 ("/123pan/Downloads/subdir", "/123pan/Downloads")
|
||||
// 如果没有匹配的虚拟主机规则,则返回 (原始路径, "")
|
||||
func applyVhostPathMappingWithPrefix(c *gin.Context, reqPath string) (string, string) {
|
||||
rawHost := c.Request.Host
|
||||
domain := common.StripHostPort(rawHost)
|
||||
if domain == "" {
|
||||
return reqPath, ""
|
||||
}
|
||||
sharing, err := op.GetSharingByDomain(domain)
|
||||
if err != nil || sharing == nil {
|
||||
return reqPath, ""
|
||||
}
|
||||
if sharing.WebHosting {
|
||||
// Web 托管模式不做 API 路径重映射
|
||||
return reqPath, ""
|
||||
}
|
||||
if len(sharing.Files) == 0 {
|
||||
return reqPath, ""
|
||||
}
|
||||
root := sharing.Files[0]
|
||||
// Map request path into the sharing root and verify it does not escape via traversal.
|
||||
// stdpath.Join calls Clean internally, which collapses ".." segments, so we only need
|
||||
// to confirm the result still lives under root.
|
||||
mapped := stdpath.Join(root, reqPath)
|
||||
if !strings.HasPrefix(mapped, strings.TrimRight(root, "/")+"/") && mapped != root {
|
||||
utils.Log.Warnf("[VirtualHost] path traversal rejected for API remapping: domain=%q reqPath=%q", domain, reqPath)
|
||||
return reqPath, ""
|
||||
}
|
||||
utils.Log.Debugf("[VirtualHost] API path remapping: domain=%q reqPath=%q -> mappedPath=%q", domain, reqPath, mapped)
|
||||
return mapped, root
|
||||
}
|
||||
|
||||
// stripVhostPrefix 从 gin context 中取出 vhost 路径前缀,并从 path 中去掉该前缀。
|
||||
// 用于生成 /p/ 下载链接时,将真实路径还原为前端看到的路径。
|
||||
func stripVhostPrefix(c *gin.Context, path string) string {
|
||||
prefix, ok := c.Request.Context().Value(conf.VhostPrefixKey).(string)
|
||||
if !ok || prefix == "" {
|
||||
return path
|
||||
}
|
||||
if strings.HasPrefix(path, prefix+"/") {
|
||||
return path[len(prefix):]
|
||||
}
|
||||
if path == prefix {
|
||||
return "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -413,6 +413,8 @@ type UpdateSharingReq struct {
|
||||
Remark string `json:"remark"`
|
||||
Readme string `json:"readme"`
|
||||
Header string `json:"header"`
|
||||
Domain string `json:"domain"`
|
||||
WebHosting bool `json:"web_hosting"`
|
||||
model.Sort
|
||||
CreatorName string `json:"creator"`
|
||||
Accessed int `json:"accessed"`
|
||||
@@ -432,6 +434,32 @@ func validateSharingID(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validDomainRe 校验域名格式:仅允许字母、数字、连字符、点号,且不以点/连字符开头结尾。
|
||||
var validDomainRe = regexp.MustCompile(`^([a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?\.)*[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$`)
|
||||
|
||||
// normalizeDomain 对域名做归一化处理:去空白、转小写、去端口。
|
||||
// 返回归一化后的域名和可能的错误。
|
||||
func normalizeDomain(domain string) (string, error) {
|
||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||
if domain == "" {
|
||||
return "", nil
|
||||
}
|
||||
// 去掉可能误填的端口号
|
||||
if idx := strings.LastIndex(domain, ":"); idx > 0 {
|
||||
// 排除 IPv6 裸地址(含 [ 的情况)
|
||||
if !strings.Contains(domain, "[") {
|
||||
domain = domain[:idx]
|
||||
}
|
||||
}
|
||||
if len(domain) > 253 {
|
||||
return "", errors.New("domain must be at most 253 characters")
|
||||
}
|
||||
if !validDomainRe.MatchString(domain) {
|
||||
return "", errors.New("invalid domain format: only lowercase letters, numbers, hyphens and dots are allowed")
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
func UpdateSharing(c *gin.Context) {
|
||||
var req UpdateSharingReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
@@ -474,6 +502,16 @@ func UpdateSharing(c *gin.Context) {
|
||||
if reqUser.IsAdmin() && req.CreatorName == "" {
|
||||
user = s.Creator
|
||||
}
|
||||
// 域名归一化与校验
|
||||
normalizedDomain, domErr := normalizeDomain(req.Domain)
|
||||
if domErr != nil {
|
||||
common.ErrorResp(c, domErr, 400)
|
||||
return
|
||||
}
|
||||
if req.WebHosting && normalizedDomain == "" {
|
||||
common.ErrorStrResp(c, "web_hosting requires a valid domain", 400)
|
||||
return
|
||||
}
|
||||
s.Files = req.Files
|
||||
s.Expires = req.Expires
|
||||
s.Pwd = req.Pwd
|
||||
@@ -484,6 +522,8 @@ func UpdateSharing(c *gin.Context) {
|
||||
s.Header = req.Header
|
||||
s.Readme = req.Readme
|
||||
s.Remark = req.Remark
|
||||
s.Domain = normalizedDomain
|
||||
s.WebHosting = req.WebHosting
|
||||
s.Creator = user
|
||||
if req.NewID != "" && req.NewID != req.ID {
|
||||
if !reqUser.CanCustomizeShareID() {
|
||||
@@ -550,6 +590,16 @@ func CreateSharing(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 域名归一化与校验
|
||||
normalizedDomain, domErr := normalizeDomain(req.Domain)
|
||||
if domErr != nil {
|
||||
common.ErrorResp(c, domErr, 400)
|
||||
return
|
||||
}
|
||||
if req.WebHosting && normalizedDomain == "" {
|
||||
common.ErrorStrResp(c, "web_hosting requires a valid domain", 400)
|
||||
return
|
||||
}
|
||||
s := &model.Sharing{
|
||||
SharingDB: &model.SharingDB{
|
||||
ID: req.ID,
|
||||
@@ -562,6 +612,8 @@ func CreateSharing(c *gin.Context) {
|
||||
Remark: req.Remark,
|
||||
Readme: req.Readme,
|
||||
Header: req.Header,
|
||||
Domain: normalizedDomain,
|
||||
WebHosting: req.WebHosting,
|
||||
},
|
||||
Files: req.Files,
|
||||
Creator: user,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
stdpath "path"
|
||||
"strings"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
||||
@@ -17,10 +18,45 @@ import (
|
||||
|
||||
func PathParse(c *gin.Context) {
|
||||
rawPath := parsePath(c.Param("path"))
|
||||
// 虚拟主机路径重映射:根据 Host 头匹配虚拟主机规则,将请求路径映射到实际路径
|
||||
// 例如:vhost.Path="/123pan/Downloads",rawPath="/tests.html" -> "/123pan/Downloads/tests.html"
|
||||
rawPath = applyDownVhostPathMapping(c, rawPath)
|
||||
common.GinAppendValues(c, conf.PathKey, rawPath)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// applyDownVhostPathMapping 根据请求的 Host 头匹配 sharing 中带 Domain 的虚拟主机记录,
|
||||
// 将下载/预览路由的路径映射到虚拟主机配置的实际路径(取 sharing.Files[0])。
|
||||
// 仅在 sharing 有效(未禁用、未过期、Files 非空)且非 Web 托管模式时生效。
|
||||
func applyDownVhostPathMapping(c *gin.Context, reqPath string) string {
|
||||
rawHost := c.Request.Host
|
||||
domain := common.StripHostPort(rawHost)
|
||||
if domain == "" {
|
||||
return reqPath
|
||||
}
|
||||
sharing, err := op.GetSharingByDomain(domain)
|
||||
if err != nil || sharing == nil {
|
||||
return reqPath
|
||||
}
|
||||
if sharing.WebHosting {
|
||||
// Web 托管模式不做下载路径重映射
|
||||
return reqPath
|
||||
}
|
||||
if len(sharing.Files) == 0 {
|
||||
return reqPath
|
||||
}
|
||||
root := sharing.Files[0]
|
||||
// 路径重映射:将 reqPath 拼接到 root 后面,并校验不逃逸出 root
|
||||
mapped := stdpath.Join(root, reqPath)
|
||||
if !strings.HasPrefix(mapped, strings.TrimRight(root, "/")+"/") && mapped != root {
|
||||
utils.Log.Warnf("[VirtualHost] path traversal rejected for down: domain=%q reqPath=%q", domain, reqPath)
|
||||
return reqPath
|
||||
}
|
||||
utils.Log.Debugf("[VirtualHost] down path remapping: domain=%q reqPath=%q -> mappedPath=%q", domain, reqPath, mapped)
|
||||
return mapped
|
||||
}
|
||||
|
||||
|
||||
func Down(verifyFunc func(string, string) error) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
rawPath := c.Request.Context().Value(conf.PathKey).(string)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||
"github.com/OpenListTeam/OpenList/v4/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// vhostBlockedPrefixes 是虚拟主机域名下被阻止的路由前缀列表。
|
||||
// 这些路由属于管理/内部功能,不应通过 vhost 域名暴露。
|
||||
var vhostBlockedPrefixes = []string{
|
||||
"/api/admin/",
|
||||
"/dav/",
|
||||
"/s3/",
|
||||
}
|
||||
|
||||
// VhostRouteGuard 虚拟主机路由守卫中间件。
|
||||
// 当请求的 Host 头匹配到一个有效的虚拟主机 sharing 时,
|
||||
// 阻止对管理类路由(/api/admin/、/dav/、/s3/)的访问,返回 404。
|
||||
// 对于 WebHosting=true 的域名,额外阻止 /api/ 下除 /api/public/ 和 /api/fs/ 之外的路由。
|
||||
func VhostRouteGuard(c *gin.Context) {
|
||||
rawHost := c.Request.Host
|
||||
domain := common.StripHostPort(rawHost)
|
||||
if domain == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
sharing, err := op.GetSharingByDomain(domain)
|
||||
if err != nil || sharing == nil {
|
||||
// 非 vhost 域名,放行
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// 通用阻止:管理类路由
|
||||
for _, prefix := range vhostBlockedPrefixes {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// WebHosting 模式下额外限制:仅允许 /api/public/、/api/fs/ 和非 /api/ 路由
|
||||
if sharing.WebHosting && strings.HasPrefix(path, "/api/") {
|
||||
if !strings.HasPrefix(path, "/api/public/") && !strings.HasPrefix(path, "/api/fs/") {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
@@ -39,6 +39,8 @@ func Init(e *gin.Engine) {
|
||||
if conf.Conf.MaxConnections > 0 {
|
||||
g.Use(middlewares.MaxAllowed(conf.Conf.MaxConnections))
|
||||
}
|
||||
// 虚拟主机路由限制:webhost 域名下阻止管理类路由(/api/admin/、/dav/、/s3/)
|
||||
g.Use(middlewares.VhostRouteGuard)
|
||||
WebDav(g.Group("/dav"))
|
||||
S3(g.Group("/s3"))
|
||||
MCP(g)
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// Package static —— 虚拟主机 Web Hosting 模式下针对 Markdown 的服务端预览渲染。
|
||||
//
|
||||
// 设计原则:
|
||||
// 1. 不引入任何新的第三方依赖。Markdown 渲染交给浏览器端 marked.js(CDN)。
|
||||
// 2. 渲染有 size 上限(默认 5MB),防止从云端读取超大文件造成 OOM。
|
||||
// 3. Markdown 原文嵌入到 <script type="text/markdown"> 中传给前端,避免 XSS:
|
||||
// 即使内容含 <script>、</script>,也会因 type 非 JS 而不被执行;同时对 </script
|
||||
// 做 <\/ 转义防止 script 标签被切断。
|
||||
//
|
||||
// 关于 .mhtml:Chrome 在网络场景下,响应头为 multipart/related 且无 Content-Disposition: attachment
|
||||
// 时,会调用内置 MHTML 渲染器原生预览。无需服务端解析。
|
||||
package static
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
)
|
||||
|
||||
// renderMaxBytes 单个被渲染文件允许的最大字节数。
|
||||
// 超过此值视为非预期文件,回退为原始下载(不再尝试服务端解析)。
|
||||
const renderMaxBytes int64 = 5 * 1024 * 1024
|
||||
|
||||
// readLinkAll 通过 Link 把整个文件读入内存,受 maxBytes 限制。
|
||||
// 适用于 Markdown 这种小文本文件场景。
|
||||
func readLinkAll(ctx context.Context, link *model.Link, declaredSize int64, maxBytes int64) ([]byte, error) {
|
||||
size := link.ContentLength
|
||||
if size <= 0 {
|
||||
size = declaredSize
|
||||
}
|
||||
if size > 0 && size > maxBytes {
|
||||
return nil, fmt.Errorf("file too large for render: size=%d max=%d", size, maxBytes)
|
||||
}
|
||||
|
||||
rr, err := stream.GetRangeReaderFromLink(size, link)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get range reader: %w", err)
|
||||
}
|
||||
|
||||
// 当 size 未知时使用全量范围(Length=-1 由底层处理)
|
||||
rng := http_range.Range{Start: 0, Length: -1}
|
||||
if size > 0 {
|
||||
rng = http_range.Range{Start: 0, Length: size}
|
||||
}
|
||||
rc, err := rr.RangeRead(ctx, rng)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("range read: %w", err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// 用 LimitReader 兜底防止 size 申报为 0 但内容超大
|
||||
limited := io.LimitReader(rc, maxBytes+1)
|
||||
buf, err := io.ReadAll(limited)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read all: %w", err)
|
||||
}
|
||||
if int64(len(buf)) > maxBytes {
|
||||
return nil, fmt.Errorf("file exceeds max bytes during read: max=%d", maxBytes)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// markdownPreviewTpl 是 Markdown 全屏预览的 HTML 模板。
|
||||
// 使用 marked.js + highlight.js + DOMPurify 在浏览器端渲染,样式参考 GitHub Markdown CSS。
|
||||
// 占位符:
|
||||
//
|
||||
// {{TITLE}} <title> 标签内容(页面文件名,已 HTML 转义)
|
||||
// {{MD_BODY}} 原始 Markdown 文本(已对 </ 做 <\/ 转义防止 script 标签提前闭合)
|
||||
const markdownPreviewTpl = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{{TITLE}}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/github.min.css" crossorigin="anonymous">
|
||||
<style>
|
||||
html,body{margin:0;padding:0;background:#fff}
|
||||
.markdown-body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:32px 45px}
|
||||
@media (max-width:767px){.markdown-body{padding:16px}}
|
||||
.md-loading{padding:32px 45px;color:#888;font-family:system-ui,sans-serif}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body" id="md-target"><p class="md-loading">Rendering Markdown...</p></article>
|
||||
<script id="md-source" type="text/markdown">{{MD_BODY}}</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11/marked.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked-highlight@2/lib/index.umd.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/core.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/common.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
(function(){
|
||||
try{
|
||||
var src = document.getElementById('md-source').textContent || '';
|
||||
if (window.marked && window.markedHighlight && window.hljs) {
|
||||
window.marked.use(window.markedHighlight.markedHighlight({
|
||||
langPrefix: 'hljs language-',
|
||||
highlight: function(code, lang){
|
||||
try { return (lang && hljs.getLanguage(lang)) ? hljs.highlight(code,{language:lang}).value : hljs.highlightAuto(code).value; }
|
||||
catch(_) { return code; }
|
||||
}
|
||||
}));
|
||||
}
|
||||
var html = (window.marked ? window.marked.parse(src) : '').toString();
|
||||
if (window.DOMPurify) { html = window.DOMPurify.sanitize(html); }
|
||||
document.getElementById('md-target').innerHTML = html;
|
||||
}catch(e){
|
||||
document.getElementById('md-target').textContent = String(e);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// renderMarkdownPreview 把 Markdown 原文包装为完整的 HTML 预览页。
|
||||
// 注意:原文必须做 </ 转义,避免在 <script type=text/markdown> 中提前闭合。
|
||||
func renderMarkdownPreview(filename string, mdSource []byte) []byte {
|
||||
// HTML-escape title(防止文件名注入 HTML)
|
||||
title := htmlEscape(filename)
|
||||
|
||||
// 对 </ 做转义。<script type="text/markdown"> 块在 HTML 解析阶段
|
||||
// 会按"原始文本"处理,唯一会让它结束的是 </script 这种序列(不区分大小写)。
|
||||
// 替换为 <\/,浏览器仍会按原字符显示,但不会触发 script 闭合。
|
||||
body := bytes.ReplaceAll(mdSource, []byte("</"), []byte(`<\/`))
|
||||
|
||||
out := strings.NewReplacer(
|
||||
"{{TITLE}}", title,
|
||||
"{{MD_BODY}}", string(body),
|
||||
).Replace(markdownPreviewTpl)
|
||||
return []byte(out)
|
||||
}
|
||||
|
||||
// htmlEscape 简单的 HTML 转义,仅处理在文本/属性场景需要的几个字符。
|
||||
func htmlEscape(s string) string {
|
||||
r := strings.NewReplacer(
|
||||
"&", "&",
|
||||
"<", "<",
|
||||
">", ">",
|
||||
`"`, """,
|
||||
"'", "'",
|
||||
)
|
||||
return r.Replace(s)
|
||||
}
|
||||
|
||||
// writeRenderedHTML 把渲染后的 HTML 作为 200 响应写出。
|
||||
func writeRenderedHTML(w http.ResponseWriter, html []byte) {
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "text/html; charset=utf-8")
|
||||
h.Set("Content-Disposition", "inline")
|
||||
// Markdown 内容不常变化,允许浏览器缓存但每次需验证
|
||||
h.Set("Cache-Control", "no-cache, must-revalidate")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write(html); err != nil {
|
||||
utils.Log.Debugf("[VirtualHost] writeRenderedHTML: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Package static —— 虚拟主机访问码(Share Pwd)门禁。
|
||||
//
|
||||
// 当 sharing.Pwd != "" 时,所有通过域名访问 sharing 内容(无论 Web Hosting
|
||||
// 还是路径重映射模式)都必须先通过密码校验:
|
||||
// - 提取顺序(择一即可):?pwd= 查询参数 → X-Share-Pwd 请求头 → cookie share_pwd_<id>
|
||||
// - 校验通过后,会写一个 HttpOnly cookie(Path=/,SameSite=Lax),后续访问免输入;
|
||||
// - 校验未通过:GET 请求返回内嵌的密码输入页(401),POST application/x-www-form-urlencoded
|
||||
// 的 ?pwd 字段会被识别为提交(302 回原 URL)。
|
||||
//
|
||||
// 安全细节:
|
||||
// - 密码比较使用 crypto/subtle.ConstantTimeCompare 防止时序侧信道;
|
||||
// - cookie 名按 sharing.ID 隔离,避免不同分享串扰;
|
||||
// - cookie 的 Secure 属性在 HTTPS 请求下自动开启。
|
||||
package static
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// sharePwdCookieName 返回当前 sharing 对应的 cookie 名。
|
||||
// 名字按 ID 隔离避免一个浏览器同时访问多个不同分享时互相串扰。
|
||||
func sharePwdCookieName(sharingID string) string {
|
||||
// 仅保留字母数字与下划线,确保 cookie name 合法
|
||||
var b strings.Builder
|
||||
b.Grow(len("share_pwd_") + len(sharingID))
|
||||
b.WriteString("share_pwd_")
|
||||
for i := 0; i < len(sharingID); i++ {
|
||||
c := sharingID[i]
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '_', c == '-':
|
||||
b.WriteByte(c)
|
||||
default:
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// sharePwdToken 用于生成存储在 cookie 中的令牌(而非明文密码)。
|
||||
// 令牌 = HMAC-SHA256(sharingID + ":" + pwd, salt),即使 cookie 泄露也无法反推原始密码。
|
||||
const sharePwdHMACSalt = "openlist-vhost-share-pwd-v1"
|
||||
|
||||
func sharePwdToken(sharingID, pwd string) string {
|
||||
mac := hmac.New(sha256.New, []byte(sharePwdHMACSalt))
|
||||
mac.Write([]byte(sharingID + ":" + pwd))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// extractSharePwd 按优先级从请求中提取访问码(明文)。
|
||||
// 注意:不从 cookie 中提取,因为 cookie 存储的是 HMAC token 而非明文密码,
|
||||
// cookie 验证在 verifySharePwd 中单独处理。
|
||||
func extractSharePwd(c *gin.Context, sharingID string) string {
|
||||
if v := c.Query("pwd"); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := c.GetHeader("X-Share-Pwd"); v != "" {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// constantTimeEqual 时序安全字符串比较。
|
||||
func constantTimeEqual(a, b string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
// verifySharePwd 判断当前请求是否通过 sharing 访问码校验。
|
||||
// 若 sharing.Pwd 为空(未设置访问码),永远通过。
|
||||
func verifySharePwd(c *gin.Context, sharing *model.Sharing) bool {
|
||||
if sharing == nil || sharing.SharingDB == nil || sharing.Pwd == "" {
|
||||
return true
|
||||
}
|
||||
got := extractSharePwd(c, sharing.ID)
|
||||
if got == "" {
|
||||
// 尝试从 cookie 中验证令牌(非明文密码)
|
||||
if token, err := c.Cookie(sharePwdCookieName(sharing.ID)); err == nil && token != "" {
|
||||
expected := sharePwdToken(sharing.ID, sharing.Pwd)
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return constantTimeEqual(got, sharing.Pwd)
|
||||
}
|
||||
|
||||
// setSharePwdCookie 把校验通过的令牌(哈希)写入 cookie,便于后续请求免输入。
|
||||
// 不存储明文密码,避免本机 cookie 数据库泄露风险。
|
||||
func setSharePwdCookie(c *gin.Context, sharingID, pwd string) {
|
||||
secure := c.Request.TLS != nil ||
|
||||
strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https")
|
||||
token := sharePwdToken(sharingID, pwd)
|
||||
cookie := &http.Cookie{
|
||||
Name: sharePwdCookieName(sharingID),
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: secure,
|
||||
// 不设 MaxAge:作为会话 cookie,浏览器关闭后失效,降低长期暴露风险
|
||||
}
|
||||
http.SetCookie(c.Writer, cookie)
|
||||
}
|
||||
|
||||
// handleSharePwdGate 在虚拟主机请求进入 sharing 处理前完成密码门禁。
|
||||
// 返回 true 表示已放行,调用方继续后续处理;
|
||||
// 返回 false 表示当前请求已被该函数完整处理(密码页/重定向/方法不允许),调用方应直接 return。
|
||||
func handleSharePwdGate(c *gin.Context, sharing *model.Sharing) bool {
|
||||
if sharing == nil || sharing.SharingDB == nil || sharing.Pwd == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// POST application/x-www-form-urlencoded:识别为表单提交
|
||||
if c.Request.Method == http.MethodPost {
|
||||
ct := c.GetHeader("Content-Type")
|
||||
if i := strings.Index(ct, ";"); i >= 0 {
|
||||
ct = ct[:i]
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(ct), "application/x-www-form-urlencoded") {
|
||||
pwd := c.PostForm("pwd")
|
||||
if pwd != "" && constantTimeEqual(pwd, sharing.Pwd) {
|
||||
setSharePwdCookie(c, sharing.ID, pwd)
|
||||
// 提交成功:302 回当前路径(去掉 ?pwd=)
|
||||
redirect := c.Request.URL.Path
|
||||
if redirect == "" {
|
||||
redirect = "/"
|
||||
}
|
||||
c.Redirect(http.StatusFound, redirect)
|
||||
return false
|
||||
}
|
||||
// 密码错误:返回错误页
|
||||
writeSharePwdPage(c, sharing.ID, true)
|
||||
return false
|
||||
}
|
||||
// 其他 POST:405
|
||||
c.Status(http.StatusMethodNotAllowed)
|
||||
return false
|
||||
}
|
||||
|
||||
// GET / HEAD:先看是否已携带正确凭据
|
||||
if verifySharePwd(c, sharing) {
|
||||
// 若是通过 ?pwd= 提交的,顺手写入 cookie 并清掉 query 中的 pwd 重定向,
|
||||
// 否则刷新或转发链接会一直暴露密码。
|
||||
if pwd := c.Query("pwd"); pwd != "" {
|
||||
setSharePwdCookie(c, sharing.ID, pwd)
|
||||
cleaned := stripPwdQuery(c.Request.URL.RawQuery)
|
||||
target := c.Request.URL.Path
|
||||
if cleaned != "" {
|
||||
target += "?" + cleaned
|
||||
}
|
||||
c.Redirect(http.StatusFound, target)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 未通过:返回密码输入页
|
||||
writeSharePwdPage(c, sharing.ID, false)
|
||||
return false
|
||||
}
|
||||
|
||||
// stripPwdQuery 从 raw query 中剔除 pwd 参数(大小写不敏感),保留其他参数原序。
|
||||
func stripPwdQuery(raw string) string {
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(raw, "&")
|
||||
out := parts[:0]
|
||||
for _, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
key := p
|
||||
if i := strings.Index(p, "="); i >= 0 {
|
||||
key = p[:i]
|
||||
}
|
||||
if strings.EqualFold(key, "pwd") {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return strings.Join(out, "&")
|
||||
}
|
||||
|
||||
// sharePwdPageTpl 是访问码输入页的内嵌模板。
|
||||
// 占位符:
|
||||
//
|
||||
// {{ERROR_BLOCK}} 当密码错误时插入的提示块(HTML 片段,无需转义)
|
||||
const sharePwdPageTpl = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>需要访问码</title>
|
||||
<style>
|
||||
html,body{height:100%;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"PingFang SC","Microsoft YaHei",sans-serif;background:#f5f7fa;color:#222}
|
||||
.wrap{min-height:100%;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}
|
||||
.card{width:100%;max-width:380px;background:#fff;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:32px 28px}
|
||||
h1{margin:0 0 8px;font-size:20px}
|
||||
p.sub{margin:0 0 20px;color:#666;font-size:14px}
|
||||
label{display:block;margin-bottom:6px;font-size:13px;color:#444}
|
||||
input[type=password]{width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid #d0d7de;border-radius:8px;font-size:15px;outline:none;transition:border-color .15s}
|
||||
input[type=password]:focus{border-color:#1f6feb;box-shadow:0 0 0 3px rgba(31,111,235,.15)}
|
||||
button{margin-top:16px;width:100%;padding:10px 16px;background:#1f6feb;color:#fff;border:0;border-radius:8px;font-size:15px;cursor:pointer;transition:background .15s}
|
||||
button:hover{background:#1a5fd0}
|
||||
.err{margin:0 0 16px;padding:10px 12px;background:#ffeef0;border:1px solid #ffd0d4;border-radius:8px;color:#a40e26;font-size:13px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<form class="card" method="post" action="" enctype="application/x-www-form-urlencoded" autocomplete="off">
|
||||
<h1>需要访问码</h1>
|
||||
<p class="sub">该分享已设置访问码,请输入后继续。</p>
|
||||
{{ERROR_BLOCK}}
|
||||
<label for="pwd">访问码</label>
|
||||
<input id="pwd" name="pwd" type="password" autofocus required>
|
||||
<button type="submit">进入</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// writeSharePwdPage 把密码输入页写出。wrong=true 时附带"密码错误"提示块。
|
||||
// 状态码:未输入时 401(请求需要凭据),密码错误时 403(已提供但无效)。
|
||||
func writeSharePwdPage(c *gin.Context, sharingID string, wrong bool) {
|
||||
_ = sharingID // 当前模板表单 action="" 会回到当前 URL,不需要在页面里嵌入 ID
|
||||
errBlock := ""
|
||||
status := http.StatusUnauthorized
|
||||
if wrong {
|
||||
errBlock = `<p class="err">访问码错误,请重试。</p>`
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
html := strings.Replace(sharePwdPageTpl, "{{ERROR_BLOCK}}", errBlock, 1)
|
||||
|
||||
h := c.Writer.Header()
|
||||
h.Set("Content-Type", "text/html; charset=utf-8")
|
||||
h.Set("Cache-Control", "no-store")
|
||||
h.Set("X-Robots-Tag", "noindex")
|
||||
c.Status(status)
|
||||
if _, err := c.Writer.WriteString(html); err != nil {
|
||||
utils.Log.Debugf("[VirtualHost] writeSharePwdPage: %v", err)
|
||||
}
|
||||
c.Writer.Flush()
|
||||
}
|
||||
+273
-7
@@ -5,19 +5,39 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
iofs "io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
stdpath "path"
|
||||
"strings"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
||||
internalfs "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/internal/setting"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
"github.com/OpenListTeam/OpenList/v4/public"
|
||||
"github.com/OpenListTeam/OpenList/v4/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// vhostInternalUser 是虚拟主机内部使用的系统用户。
|
||||
// Web Hosting 模式下,文件访问不依赖真实用户身份,但 context 中必须有非 nil 的 User
|
||||
// 以避免下游 hook/中间件对 nil 指针解引用 panic。
|
||||
// 该用户 BasePath="/" 且 Permission 全开,实际访问范围由 handleWebHosting 的
|
||||
// HasPrefix 沙箱校验保证安全。
|
||||
var vhostInternalUser = &model.User{
|
||||
ID: 0,
|
||||
Username: "_vhost_internal",
|
||||
Role: model.GUEST,
|
||||
Permission: 0x7FFF, // 所有权限位开启(读、WebDAV 读等)
|
||||
BasePath: "/",
|
||||
Disabled: false,
|
||||
}
|
||||
|
||||
type ManifestIcon struct {
|
||||
Src string `json:"src"`
|
||||
Sizes string `json:"sizes"`
|
||||
@@ -32,12 +52,12 @@ type Manifest struct {
|
||||
Icons []ManifestIcon `json:"icons"`
|
||||
}
|
||||
|
||||
var static fs.FS
|
||||
var static iofs.FS
|
||||
|
||||
func initStatic() {
|
||||
utils.Log.Debug("Initializing static file system...")
|
||||
if conf.Conf.DistDir == "" {
|
||||
dist, err := fs.Sub(public.Public, "dist")
|
||||
dist, err := iofs.Sub(public.Public, "dist")
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("failed to read dist dir: %v", err)
|
||||
}
|
||||
@@ -76,7 +96,7 @@ func initIndex(siteConfig SiteConfig) {
|
||||
utils.Log.Debug("Reading index.html from static files system...")
|
||||
indexFile, err := static.Open("index.html")
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if errors.Is(err, iofs.ErrNotExist) {
|
||||
utils.Log.Fatalf("index.html not exist, you may forget to put dist of frontend to public/dist")
|
||||
}
|
||||
utils.Log.Fatalf("failed to read index.html: %v", err)
|
||||
@@ -192,7 +212,7 @@ func Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {
|
||||
}
|
||||
})
|
||||
for _, folder := range folders {
|
||||
sub, err := fs.Sub(static, folder)
|
||||
sub, err := iofs.Sub(static, folder)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("can't find folder: %s", folder)
|
||||
}
|
||||
@@ -210,7 +230,46 @@ func Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {
|
||||
}
|
||||
|
||||
utils.Log.Debug("Setting up catch-all route...")
|
||||
noRoute(func(c *gin.Context) {
|
||||
|
||||
// virtualHostHandler 处理虚拟主机 Web 托管,以及默认的前端 SPA 路由
|
||||
virtualHostHandler := func(c *gin.Context) {
|
||||
// 直接从 Host 头解析域名,检查是否匹配 sharing 中的虚拟主机记录
|
||||
rawHost := c.Request.Host
|
||||
domain := common.StripHostPort(rawHost)
|
||||
if domain != "" {
|
||||
sharing, err := op.GetSharingByDomain(domain)
|
||||
if err == nil && sharing != nil && len(sharing.Files) > 0 {
|
||||
utils.Log.Debugf("[VirtualHost] domain=%q matched sharing id=%s web_hosting=%v path=%q",
|
||||
domain, sharing.ID, sharing.WebHosting, c.Request.URL.Path)
|
||||
// 访问码门禁:sharing.Pwd 非空时,未通过校验的请求会被门禁函数
|
||||
// 直接处理(密码输入页 / 提交表单 / 重定向),调用方需立即返回。
|
||||
if !handleSharePwdGate(c, sharing) {
|
||||
return
|
||||
}
|
||||
if sharing.WebHosting {
|
||||
// Web 托管模式:直接返回文件内容
|
||||
// 注入 vhostInternalUser 到 context,确保下游 hook 不会因 nil user panic,
|
||||
// 同时绕过 guest Disabled 限制,作为系统级内部访问处理。
|
||||
// 实际访问范围由 handleWebHosting 的 HasPrefix 沙箱校验保证。
|
||||
common.GinAppendValues(c, conf.UserKey, vhostInternalUser)
|
||||
handleWebHosting(c, sharing)
|
||||
return
|
||||
} else {
|
||||
// 路径重映射模式(伪静态):保持地址栏不变,直接返回 SPA HTML
|
||||
// 后端 API(fs/list、fs/get、/d/、/p/)已根据 Host 头自动将路径重映射
|
||||
// 到 sharing.Files[0] 之下,前端正常请求即可看到分享目录内容。
|
||||
// 注意:此处不注入 user——浏览器加载 SPA 后发起的 /api/fs/list 是新请求,
|
||||
// 会经过 auth 中间件正常填入 guest/登录用户。
|
||||
c.Header("Content-Type", "text/html")
|
||||
c.Status(http.StatusOK)
|
||||
_, _ = c.Writer.WriteString(conf.IndexHtml)
|
||||
c.Writer.Flush()
|
||||
c.Writer.WriteHeaderNow()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Request.Method != "GET" && c.Request.Method != "POST" {
|
||||
c.Status(405)
|
||||
return
|
||||
@@ -224,5 +283,212 @@ func Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {
|
||||
}
|
||||
c.Writer.Flush()
|
||||
c.Writer.WriteHeaderNow()
|
||||
})
|
||||
}
|
||||
|
||||
// 显式注册根路径路由,确保 GET / 能被正确处理
|
||||
// gin 的 NoRoute 不会触发已注册路由前缀下的 GET /
|
||||
r.GET("/", virtualHostHandler)
|
||||
r.POST("/", virtualHostHandler)
|
||||
// NoRoute 处理其他所有未匹配路径(如 /@manage、/d/... 等 SPA 路由)
|
||||
noRoute(virtualHostHandler)
|
||||
}
|
||||
|
||||
// indexCandidates 是 Web Hosting 模式下,访问目录时按优先级查找的索引文件名列表。
|
||||
// 命中第一个存在的文件即返回;全部不存在则返回 404。
|
||||
var indexCandidates = []string{
|
||||
"index.html",
|
||||
"index.htm",
|
||||
"index.mhtml",
|
||||
"index.md",
|
||||
"default.htm",
|
||||
"default.html",
|
||||
"default.mhtml",
|
||||
"default.md",
|
||||
"README.html",
|
||||
"README.htm",
|
||||
"README.mhtml",
|
||||
"README.md",
|
||||
"readme.html",
|
||||
"readme.htm",
|
||||
"readme.mhtml",
|
||||
"readme.md",
|
||||
}
|
||||
|
||||
// handleWebHosting 处理虚拟主机(sharing)的 Web 托管请求。
|
||||
// 行为:
|
||||
// 1. 请求路径若指向某个具体文件(非目录),直接返回该文件内容;
|
||||
// 2. 请求路径指向目录或文件不存在时,按 indexCandidates 顺序查找索引文件;
|
||||
// 3. 全部未命中时返回 404。
|
||||
func handleWebHosting(c *gin.Context, sharing *model.Sharing) {
|
||||
if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodHead {
|
||||
utils.Log.Debugf("[VirtualHost] skip: method=%s not allowed for web hosting", c.Request.Method)
|
||||
c.Status(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if len(sharing.Files) == 0 {
|
||||
utils.Log.Debugf("[VirtualHost] skip: sharing has no files")
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
root := sharing.Files[0]
|
||||
|
||||
reqPath := c.Request.URL.Path
|
||||
// stdpath.Join 内部 Clean,会消除 .. 但仍可能逃出 root,故再做 HasPrefix 校验
|
||||
filePath := stdpath.Join(root, reqPath)
|
||||
if !strings.HasPrefix(filePath, strings.TrimRight(root, "/")+"/") && filePath != root {
|
||||
utils.Log.Warnf("[VirtualHost] path traversal rejected: root=%q reqPath=%q", root, reqPath)
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
utils.Log.Debugf("[VirtualHost] handleWebHosting: reqPath=%q -> filePath=%q", reqPath, filePath)
|
||||
|
||||
// 1) 直接命中文件
|
||||
if obj, err := internalfs.Get(c.Request.Context(), filePath, &internalfs.GetArgs{NoLog: true}); err == nil && !obj.IsDir() {
|
||||
utils.Log.Debugf("[VirtualHost] serving file: %q", filePath)
|
||||
serveWebHostingFile(c, filePath, obj.GetName())
|
||||
return
|
||||
}
|
||||
|
||||
// 2) 目录或文件不存在:按优先级匹配索引文件
|
||||
for _, name := range indexCandidates {
|
||||
candidate := stdpath.Join(filePath, name)
|
||||
if obj, err := internalfs.Get(c.Request.Context(), candidate, &internalfs.GetArgs{NoLog: true}); err == nil && !obj.IsDir() {
|
||||
utils.Log.Debugf("[VirtualHost] serving index candidate: %q", candidate)
|
||||
serveWebHostingFile(c, candidate, obj.GetName())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 全部未命中
|
||||
utils.Log.Debugf("[VirtualHost] no index candidate matched for reqPath=%q under root=%q", reqPath, root)
|
||||
c.Status(http.StatusNotFound)
|
||||
}
|
||||
|
||||
// serveWebHostingFile 通过代理方式直接返回文件内容
|
||||
func serveWebHostingFile(c *gin.Context, filePath, filename string) {
|
||||
link, file, err := internalfs.Link(c.Request.Context(), filePath, model.LinkArgs{
|
||||
IP: c.ClientIP(),
|
||||
Header: c.Request.Header,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Log.Errorf("web hosting: failed to get link for %s: %v", filePath, err)
|
||||
c.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer link.Close()
|
||||
|
||||
// 根据文件扩展名确定正确的 Content-Type
|
||||
ext := strings.ToLower(stdpath.Ext(filename))
|
||||
contentType := mimeTypeByExt(ext)
|
||||
|
||||
// .md 走服务端模板渲染(浏览器端 marked.js 渲染)。
|
||||
// 读失败或文件过大时回退为原始内容代理,不中断请求。
|
||||
if ext == ".md" {
|
||||
if data, rerr := readLinkAll(c.Request.Context(), link, file.GetSize(), renderMaxBytes); rerr == nil {
|
||||
html := renderMarkdownPreview(filename, data)
|
||||
writeRenderedHTML(c.Writer, html)
|
||||
return
|
||||
} else {
|
||||
utils.Log.Warnf("web hosting: markdown render fallback for %s: %v", filePath, rerr)
|
||||
}
|
||||
}
|
||||
|
||||
// 注意:不要修改 link.Header!
|
||||
// link.Header 是请求上游存储时附加的请求头(如 Referer、Authorization 等),
|
||||
// 写入 Content-Type/Content-Disposition 会污染上游请求并触发签名失败/403。
|
||||
//
|
||||
// 正确的做法是在响应阶段通过 forceContentTypeWriter 强制覆盖响应头,
|
||||
// 同时在 ProxyIgnoreHeaders 之外通过响应头 set 直接生效。
|
||||
wrapped := &forceContentTypeWriter{
|
||||
ResponseWriter: c.Writer,
|
||||
contentType: contentType,
|
||||
contentDisp: "inline",
|
||||
}
|
||||
|
||||
// 使用通用代理函数处理文件传输
|
||||
if err := common.Proxy(wrapped, c.Request, link, file); err != nil {
|
||||
utils.Log.Errorf("web hosting: proxy error for %s: %v", filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// forceContentTypeWriter 包装 http.ResponseWriter,
|
||||
// 在 WriteHeader 时强制覆盖 Content-Type 和 Content-Disposition,
|
||||
// 确保 HTML 等文件以正确类型返回而不是被浏览器下载
|
||||
type forceContentTypeWriter struct {
|
||||
http.ResponseWriter
|
||||
contentType string
|
||||
contentDisp string
|
||||
}
|
||||
|
||||
func (w *forceContentTypeWriter) WriteHeader(statusCode int) {
|
||||
// 上游可能返回非 2xx(如 OSS 签名异常的 403);这种情况不要把异常响应包装成 200,
|
||||
// 也不要给浏览器一个声称是 HTML 但内容是 OSS XML 错误的响应。
|
||||
// 直接透传上游状态码,但仍覆盖 Content-Type / Content-Disposition 防止下载/乱解析。
|
||||
h := w.ResponseWriter.Header()
|
||||
if statusCode >= 200 && statusCode < 300 {
|
||||
h.Set("Content-Type", w.contentType)
|
||||
h.Set("Content-Disposition", w.contentDisp)
|
||||
// 安全头:防止 MIME 嘲探、限制嵌入、控制引用来源
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
// HTML 类不缓存,静态资源类允许缓存
|
||||
if strings.HasPrefix(w.contentType, "text/html") {
|
||||
h.Set("Cache-Control", "no-cache, must-revalidate")
|
||||
} else {
|
||||
h.Set("Cache-Control", "public, max-age=86400")
|
||||
}
|
||||
}
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
|
||||
// mimeTypeByExt 根据文件扩展名返回 MIME 类型
|
||||
func mimeTypeByExt(ext string) string {
|
||||
switch ext {
|
||||
case ".html", ".htm":
|
||||
return "text/html; charset=utf-8"
|
||||
case ".mhtml", ".mht":
|
||||
// MHTML(Web 归档)。Chrome / Edge 看到 multipart/related 且无 attachment 时会
|
||||
// 调用内置的 MhtmlPageLoader 原生预览,无需服务端拆包。
|
||||
// 注意:boundary 参数是 MHTML 文件内嵌的,响应头 Content-Type 上可以不携带;
|
||||
// Chrome 会从响应 body 头部重新解析。
|
||||
return "multipart/related"
|
||||
case ".md":
|
||||
// .md 在 Web Hosting 场景下会在 serveWebHostingFile 提前走服务端渲染。
|
||||
// 这里仅作为回退路径(读失败 / 超大)提供合理的 Content-Type,
|
||||
// 让浏览器直接显示源文本而不是下载。
|
||||
return "text/markdown; charset=utf-8"
|
||||
case ".css":
|
||||
return "text/css; charset=utf-8"
|
||||
case ".js", ".mjs":
|
||||
return "application/javascript; charset=utf-8"
|
||||
case ".json":
|
||||
return "application/json; charset=utf-8"
|
||||
case ".xml":
|
||||
return "application/xml; charset=utf-8"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".gif":
|
||||
return "image/gif"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
case ".ico":
|
||||
return "image/x-icon"
|
||||
case ".woff":
|
||||
return "font/woff"
|
||||
case ".woff2":
|
||||
return "font/woff2"
|
||||
case ".ttf":
|
||||
return "font/ttf"
|
||||
case ".txt":
|
||||
return "text/plain; charset=utf-8"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user