mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 21:13:10 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f977f32dba | |||
| 004a15bc5e | |||
| 9ac2f3b7f0 | |||
| 756862d1e4 |
@@ -16,7 +16,7 @@ jobs:
|
|||||||
if: github.event_name == 'issues'
|
if: github.event_name == 'issues'
|
||||||
steps:
|
steps:
|
||||||
- name: Check issue for unchecked tasks and reply
|
- name: Check issue for unchecked tasks and reply
|
||||||
uses: actions/github-script@v9
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
let comment = "";
|
let comment = "";
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- name: Check PR title for required prefix and comment
|
- name: Check PR title for required prefix and comment
|
||||||
uses: actions/github-script@v9
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const title = context.payload.pull_request.title || "";
|
const title = context.payload.pull_request.title || "";
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 0
|
||||||
fetch-tags: true
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: matrix.target-platform == ''
|
if: matrix.target-platform == ''
|
||||||
|
|||||||
@@ -16,33 +16,20 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger Makefile hash update
|
- name: Trigger Makefile hash update
|
||||||
uses: actions/github-script@v9
|
uses: peter-evans/repository-dispatch@v3
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }}
|
token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }}
|
||||||
script: |
|
repository: ${{ vars.HOOK_REPO || 'OpenListTeam/OpenList-OpenWRT' }}
|
||||||
const repository = process.env.REPOSITORY;
|
event-type: update-hashes
|
||||||
const [owner, repo] = repository.split('/');
|
client-payload: |
|
||||||
|
{
|
||||||
await github.rest.repos.createDispatchEvent({
|
"source_repository": "${{ github.repository }}",
|
||||||
owner: owner,
|
"release_tag": "${{ inputs.tag || github.ref_name }}",
|
||||||
repo: repo,
|
"release_name": "${{ inputs.tag || github.ref_name }}",
|
||||||
event_type: 'update-hashes',
|
"release_url": "${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.tag || github.ref_name }}",
|
||||||
client_payload: {
|
"triggered_by": "${{ github.actor }}",
|
||||||
source_repository: process.env.SOURCE_REPOSITORY,
|
"trigger_reason": "${{ github.event_name }}"
|
||||||
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
|
- name: Log trigger information
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ BuildWin7() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BuildDev() {
|
BuildDev() {
|
||||||
|
rm -rf .git/
|
||||||
mkdir -p "dist"
|
mkdir -p "dist"
|
||||||
muslflags="$(GetMuslStaticLdflags)"
|
muslflags="$(GetMuslStaticLdflags)"
|
||||||
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
|
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
|
||||||
@@ -268,6 +269,7 @@ BuildDockerMultiplatform() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BuildRelease() {
|
BuildRelease() {
|
||||||
|
rm -rf .git/
|
||||||
mkdir -p "build"
|
mkdir -p "build"
|
||||||
BuildWinArm64 ./build/"$appName"-windows-arm64.exe
|
BuildWinArm64 ./build/"$appName"-windows-arm64.exe
|
||||||
BuildWin7 ./build/"$appName"-windows7
|
BuildWin7 ./build/"$appName"-windows7
|
||||||
@@ -440,6 +442,7 @@ BuildLoongGLIBC() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BuildReleaseLinuxMusl() {
|
BuildReleaseLinuxMusl() {
|
||||||
|
rm -rf .git/
|
||||||
mkdir -p "build"
|
mkdir -p "build"
|
||||||
muslflags="$(GetMuslStaticLdflags)"
|
muslflags="$(GetMuslStaticLdflags)"
|
||||||
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
|
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
|
||||||
@@ -468,6 +471,7 @@ BuildReleaseLinuxMusl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BuildReleaseLinuxMuslArm() {
|
BuildReleaseLinuxMuslArm() {
|
||||||
|
rm -rf .git/
|
||||||
mkdir -p "build"
|
mkdir -p "build"
|
||||||
muslflags="$(GetMuslStaticLdflags)"
|
muslflags="$(GetMuslStaticLdflags)"
|
||||||
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
|
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
|
||||||
@@ -498,6 +502,7 @@ BuildReleaseLinuxMuslArm() {
|
|||||||
|
|
||||||
|
|
||||||
BuildReleaseAndroid() {
|
BuildReleaseAndroid() {
|
||||||
|
rm -rf .git/
|
||||||
mkdir -p "build"
|
mkdir -p "build"
|
||||||
wget https://dl.google.com/android/repository/android-ndk-r26b-linux.zip
|
wget https://dl.google.com/android/repository/android-ndk-r26b-linux.zip
|
||||||
unzip android-ndk-r26b-linux.zip
|
unzip android-ndk-r26b-linux.zip
|
||||||
@@ -518,6 +523,7 @@ BuildReleaseAndroid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BuildReleaseFreeBSD() {
|
BuildReleaseFreeBSD() {
|
||||||
|
rm -rf .git/
|
||||||
mkdir -p "build/freebsd"
|
mkdir -p "build/freebsd"
|
||||||
|
|
||||||
# Get latest FreeBSD 14.x release version from GitHub
|
# Get latest FreeBSD 14.x release version from GitHub
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ func (d *Wps) Init(ctx context.Context) error {
|
|||||||
if !resp.IsSuccess() {
|
if !resp.IsSuccess() {
|
||||||
return fmt.Errorf("failed to check login status, status code: %d, body: %s", resp.StatusCode(), resp.String())
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ require (
|
|||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e
|
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e
|
||||||
github.com/cloudsoda/go-smb2 v0.0.0-20260701064823-d8c5600d73b8
|
github.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc
|
||||||
github.com/coreos/go-oidc v2.3.0+incompatible
|
github.com/coreos/go-oidc v2.3.0+incompatible
|
||||||
github.com/deckarep/golang-set/v2 v2.8.0
|
github.com/deckarep/golang-set/v2 v2.8.0
|
||||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||||
@@ -70,7 +70,7 @@ require (
|
|||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/t3rm1n4l/go-mega v0.0.0-20260630094356-4de2f127f34a
|
github.com/t3rm1n4l/go-mega v0.0.0-20251120131202-6845944c051c
|
||||||
github.com/tchap/go-patricia/v2 v2.3.3
|
github.com/tchap/go-patricia/v2 v2.3.3
|
||||||
github.com/u2takey/ffmpeg-go v0.5.0
|
github.com/u2takey/ffmpeg-go v0.5.0
|
||||||
github.com/upyun/go-sdk/v3 v3.0.4
|
github.com/upyun/go-sdk/v3 v3.0.4
|
||||||
@@ -132,10 +132,12 @@ require (
|
|||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/relvacode/iso8601 v1.6.0 // indirect
|
github.com/relvacode/iso8601 v1.6.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/stangelandcl/ppmd v0.1.1 // indirect
|
github.com/stangelandcl/ppmd v0.1.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // 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/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
|
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.22.5 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
|||||||
@@ -191,5 +191,4 @@ const (
|
|||||||
PathKey
|
PathKey
|
||||||
SharingIDKey
|
SharingIDKey
|
||||||
SkipHookKey
|
SkipHookKey
|
||||||
VhostPrefixKey
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,16 +14,6 @@ func GetSharingById(id string) (*model.SharingDB, error) {
|
|||||||
return &s, nil
|
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) {
|
func GetSharings(pageIndex, pageSize int) (sharings []model.SharingDB, count int64, err error) {
|
||||||
sharingDB := db.Model(&model.SharingDB{})
|
sharingDB := db.Model(&model.SharingDB{})
|
||||||
if err := sharingDB.Count(&count).Error; err != nil {
|
if err := sharingDB.Count(&count).Error; err != nil {
|
||||||
@@ -48,13 +38,6 @@ func GetSharingsByCreatorId(creator uint, pageIndex, pageSize int) (sharings []m
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CreateSharing(s *model.SharingDB) (string, error) {
|
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 == "" {
|
if s.ID == "" {
|
||||||
id := random.String(8)
|
id := random.String(8)
|
||||||
for len(id) < 12 {
|
for len(id) < 12 {
|
||||||
@@ -78,13 +61,6 @@ func CreateSharing(s *model.SharingDB) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateSharing(s *model.SharingDB) 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)
|
return errors.WithStack(db.Save(s).Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ type SharingDB struct {
|
|||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
Readme string `json:"readme" gorm:"type:text"`
|
Readme string `json:"readme" gorm:"type:text"`
|
||||||
Header string `json:"header" 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
|
Sort
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,22 +42,6 @@ func (s *Sharing) Valid() bool {
|
|||||||
return true
|
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 {
|
func (s *Sharing) Verify(pwd string) bool {
|
||||||
return s.Pwd == "" || s.Pwd == pwd
|
return s.Pwd == "" || s.Pwd == pwd
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-94
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
stdpath "path"
|
stdpath "path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/db"
|
"github.com/OpenListTeam/OpenList/v4/internal/db"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
@@ -13,7 +12,6 @@ import (
|
|||||||
"github.com/OpenListTeam/go-cache"
|
"github.com/OpenListTeam/go-cache"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeJoined(sdb []model.SharingDB) []model.Sharing {
|
func makeJoined(sdb []model.SharingDB) []model.Sharing {
|
||||||
@@ -44,11 +42,6 @@ func makeJoined(sdb []model.SharingDB) []model.Sharing {
|
|||||||
var sharingCache = cache.NewMemCache(cache.WithShards[*model.Sharing](8))
|
var sharingCache = cache.NewMemCache(cache.WithShards[*model.Sharing](8))
|
||||||
var sharingG singleflight.Group[*model.Sharing]
|
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) {
|
func GetSharingById(id string, refresh ...bool) (*model.Sharing, error) {
|
||||||
if !utils.IsBool(refresh...) {
|
if !utils.IsBool(refresh...) {
|
||||||
if sharing, ok := sharingCache.Get(id); ok {
|
if sharing, ok := sharingCache.Get(id); ok {
|
||||||
@@ -78,66 +71,6 @@ func GetSharingById(id string, refresh ...bool) (*model.Sharing, error) {
|
|||||||
return sharing, err
|
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) {
|
func GetSharings(pageIndex, pageSize int) ([]model.Sharing, int64, error) {
|
||||||
s, cnt, err := db.GetSharings(pageIndex, pageSize)
|
s, cnt, err := db.GetSharings(pageIndex, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -185,11 +118,7 @@ func CreateSharing(sharing *model.Sharing) (id string, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
id, err = db.CreateSharing(sharing.SharingDB)
|
return db.CreateSharing(sharing.SharingDB)
|
||||||
if err == nil {
|
|
||||||
invalidateDomainCache(sharing.Domain)
|
|
||||||
}
|
|
||||||
return id, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateSharing(sharing *model.Sharing, skipMarshal ...bool) (err error) {
|
func UpdateSharing(sharing *model.Sharing, skipMarshal ...bool) (err error) {
|
||||||
@@ -200,17 +129,8 @@ func UpdateSharing(sharing *model.Sharing, skipMarshal ...bool) (err error) {
|
|||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 读取旧记录以便同时失效旧 domain 缓存
|
sharingCache.Del(sharing.ID)
|
||||||
var oldDomain string
|
return db.UpdateSharing(sharing.SharingDB)
|
||||||
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 {
|
func UpdateSharingId(sharing *model.Sharing, newId string) error {
|
||||||
@@ -223,17 +143,8 @@ func UpdateSharingId(sharing *model.Sharing, newId string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DeleteSharing(sid string) error {
|
func DeleteSharing(sid string) error {
|
||||||
// 先读取 domain 用于失效缓存
|
sharingCache.Del(sid)
|
||||||
var oldDomain string
|
return db.DeleteSharingById(sid)
|
||||||
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 {
|
func DeleteSharingsByCreatorId(creatorId uint) error {
|
||||||
|
|||||||
+49
-372
File diff suppressed because it is too large
Load Diff
@@ -1,74 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package task
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrTaskNotFound = errors.New("task not found")
|
|
||||||
ErrTaskRunning = errors.New("task is running")
|
|
||||||
)
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
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,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
stdnet "net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -156,14 +155,3 @@ func ContentWithValues(ctx context.Context, keyAndValue ...any) context.Context
|
|||||||
}
|
}
|
||||||
return ctx
|
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,8 +69,6 @@ func FsListSplit(c *gin.Context) {
|
|||||||
SharingList(c, &req)
|
SharingList(c, &req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 虚拟主机路径重映射:根据 Host 头匹配虚拟主机规则,将请求路径映射到实际路径
|
|
||||||
req.Path = applyVhostPathMapping(c, req.Path)
|
|
||||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||||
if user.IsGuest() && user.Disabled {
|
if user.IsGuest() && user.Disabled {
|
||||||
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
||||||
@@ -274,11 +272,6 @@ func FsGetSplit(c *gin.Context) {
|
|||||||
SharingGet(c, &req)
|
SharingGet(c, &req)
|
||||||
return
|
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)
|
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||||
if user.IsGuest() && user.Disabled {
|
if user.IsGuest() && user.Disabled {
|
||||||
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
||||||
@@ -326,14 +319,12 @@ func FsGet(c *gin.Context, req *FsGetReq, user *model.User) {
|
|||||||
rawURL = common.GenerateDownProxyURL(storage.GetStorage(), reqPath)
|
rawURL = common.GenerateDownProxyURL(storage.GetStorage(), reqPath)
|
||||||
if rawURL == "" {
|
if rawURL == "" {
|
||||||
query := ""
|
query := ""
|
||||||
// 生成 /p/ 链接时,去掉 vhost 路径前缀,保持前端看到的路径一致
|
|
||||||
downPath := stripVhostPrefix(c, reqPath)
|
|
||||||
if isEncrypt(meta, reqPath) || setting.GetBool(conf.SignAll) {
|
if isEncrypt(meta, reqPath) || setting.GetBool(conf.SignAll) {
|
||||||
query = "?sign=" + sign.Sign(reqPath)
|
query = "?sign=" + sign.Sign(reqPath)
|
||||||
}
|
}
|
||||||
rawURL = fmt.Sprintf("%s/p%s%s",
|
rawURL = fmt.Sprintf("%s/p%s%s",
|
||||||
common.GetApiUrl(c),
|
common.GetApiUrl(c),
|
||||||
utils.EncodePath(downPath, true),
|
utils.EncodePath(reqPath, true),
|
||||||
query)
|
query)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -436,62 +427,3 @@ func FsOther(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
common.SuccessResp(c, res)
|
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,8 +413,6 @@ type UpdateSharingReq struct {
|
|||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
Readme string `json:"readme"`
|
Readme string `json:"readme"`
|
||||||
Header string `json:"header"`
|
Header string `json:"header"`
|
||||||
Domain string `json:"domain"`
|
|
||||||
WebHosting bool `json:"web_hosting"`
|
|
||||||
model.Sort
|
model.Sort
|
||||||
CreatorName string `json:"creator"`
|
CreatorName string `json:"creator"`
|
||||||
Accessed int `json:"accessed"`
|
Accessed int `json:"accessed"`
|
||||||
@@ -434,32 +432,6 @@ func validateSharingID(id string) error {
|
|||||||
return nil
|
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) {
|
func UpdateSharing(c *gin.Context) {
|
||||||
var req UpdateSharingReq
|
var req UpdateSharingReq
|
||||||
if err := c.ShouldBind(&req); err != nil {
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
@@ -502,16 +474,6 @@ func UpdateSharing(c *gin.Context) {
|
|||||||
if reqUser.IsAdmin() && req.CreatorName == "" {
|
if reqUser.IsAdmin() && req.CreatorName == "" {
|
||||||
user = s.Creator
|
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.Files = req.Files
|
||||||
s.Expires = req.Expires
|
s.Expires = req.Expires
|
||||||
s.Pwd = req.Pwd
|
s.Pwd = req.Pwd
|
||||||
@@ -522,8 +484,6 @@ func UpdateSharing(c *gin.Context) {
|
|||||||
s.Header = req.Header
|
s.Header = req.Header
|
||||||
s.Readme = req.Readme
|
s.Readme = req.Readme
|
||||||
s.Remark = req.Remark
|
s.Remark = req.Remark
|
||||||
s.Domain = normalizedDomain
|
|
||||||
s.WebHosting = req.WebHosting
|
|
||||||
s.Creator = user
|
s.Creator = user
|
||||||
if req.NewID != "" && req.NewID != req.ID {
|
if req.NewID != "" && req.NewID != req.ID {
|
||||||
if !reqUser.CanCustomizeShareID() {
|
if !reqUser.CanCustomizeShareID() {
|
||||||
@@ -590,16 +550,6 @@ func CreateSharing(c *gin.Context) {
|
|||||||
return
|
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{
|
s := &model.Sharing{
|
||||||
SharingDB: &model.SharingDB{
|
SharingDB: &model.SharingDB{
|
||||||
ID: req.ID,
|
ID: req.ID,
|
||||||
@@ -612,8 +562,6 @@ func CreateSharing(c *gin.Context) {
|
|||||||
Remark: req.Remark,
|
Remark: req.Remark,
|
||||||
Readme: req.Readme,
|
Readme: req.Readme,
|
||||||
Header: req.Header,
|
Header: req.Header,
|
||||||
Domain: normalizedDomain,
|
|
||||||
WebHosting: req.WebHosting,
|
|
||||||
},
|
},
|
||||||
Files: req.Files,
|
Files: req.Files,
|
||||||
Creator: user,
|
Creator: user,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user