新增配置管理功能及多存储支持

- 添加管理员端 API,用于获取和更新完整配置。
- 添加公共端 API,用于获取非敏感配置信息。
- 增加本地存储(LocalStorage)、S3(S3Storage)、和 WebDAV(WebDAVStorage)存储类型的实现。
- 支持热更新存储配置和保存配置文件至磁盘。
- 更新 Swagger 文档以包含新接口定义。
This commit is contained in:
2026-01-14 14:15:20 +08:00
parent 1ffa16cf48
commit fe656fb298
13 changed files with 1043 additions and 6 deletions

1
.gitignore vendored
View File

@@ -43,7 +43,6 @@
*.sqlite *.sqlite
*.sqlite3 *.sqlite3
data/ data/
storage/
tmp/ tmp/
# 环境变量与配置(按需:如果你会提交示例配置,建议仅忽略真实配置文件) # 环境变量与配置(按需:如果你会提交示例配置,建议仅忽略真实配置文件)

View File

@@ -426,6 +426,80 @@ const docTemplate = `{
} }
} }
}, },
"/admin/config": {
"get": {
"security": [
{
"AdminAuth": []
}
],
"description": "获取系统的完整配置文件内容(仅管理员)",
"produces": [
"application/json"
],
"tags": [
"Admin"
],
"summary": "获取完整配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/config.Config"
}
}
}
},
"put": {
"security": [
{
"AdminAuth": []
}
],
"description": "更新系统的配置文件内容(仅管理员)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin"
],
"summary": "更新配置",
"parameters": [
{
"description": "新配置内容",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/config.Config"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/model.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/model.Response"
}
}
}
}
},
"/admin/login": { "/admin/login": {
"post": { "post": {
"description": "通过密码换取 JWT Token", "description": "通过密码换取 JWT Token",
@@ -698,6 +772,38 @@ const docTemplate = `{
} }
} }
}, },
"/api/config": {
"get": {
"description": "获取前端展示所需的非敏感配置数据",
"produces": [
"application/json"
],
"tags": [
"Public"
],
"summary": "获取公共配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/model.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/public.PublicConfig"
}
}
}
]
}
}
}
}
},
"/api/files/{file_id}/download": { "/api/files/{file_id}/download": {
"get": { "get": {
"description": "根据文件 ID 下载单个文件", "description": "根据文件 ID 下载单个文件",
@@ -831,6 +937,149 @@ const docTemplate = `{
} }
} }
}, },
"config.APITokenConfig": {
"type": "object",
"properties": {
"allowAdminAPI": {
"type": "boolean"
},
"enabled": {
"type": "boolean"
},
"maxTokens": {
"type": "integer"
}
}
},
"config.Config": {
"type": "object",
"properties": {
"apitoken": {
"$ref": "#/definitions/config.APITokenConfig"
},
"database": {
"$ref": "#/definitions/config.DatabaseConfig"
},
"security": {
"$ref": "#/definitions/config.SecurityConfig"
},
"site": {
"$ref": "#/definitions/config.SiteConfig"
},
"storage": {
"$ref": "#/definitions/config.StorageConfig"
},
"upload": {
"$ref": "#/definitions/config.UploadConfig"
}
}
},
"config.DatabaseConfig": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
},
"config.SecurityConfig": {
"type": "object",
"properties": {
"adminPasswordHash": {
"type": "string"
},
"jwtsecret": {
"type": "string"
},
"pickupCodeLength": {
"type": "integer"
},
"pickupFailLimit": {
"type": "integer"
}
}
},
"config.SiteConfig": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"config.StorageConfig": {
"type": "object",
"properties": {
"local": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
},
"s3": {
"type": "object",
"properties": {
"accessKey": {
"type": "string"
},
"bucket": {
"type": "string"
},
"endpoint": {
"type": "string"
},
"region": {
"type": "string"
},
"secretKey": {
"type": "string"
},
"useSSL": {
"type": "boolean"
}
}
},
"type": {
"type": "string"
},
"webDAV": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"root": {
"type": "string"
},
"url": {
"type": "string"
},
"username": {
"type": "string"
}
}
}
}
},
"config.UploadConfig": {
"type": "object",
"properties": {
"maxBatchFiles": {
"type": "integer"
},
"maxFileSizeMB": {
"type": "integer"
},
"maxRetentionDays": {
"type": "integer"
}
}
},
"model.APIToken": { "model.APIToken": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -979,6 +1228,25 @@ const docTemplate = `{
} }
} }
}, },
"public.PublicConfig": {
"type": "object",
"properties": {
"api_token": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"site": {
"$ref": "#/definitions/config.SiteConfig"
},
"upload": {
"$ref": "#/definitions/config.UploadConfig"
}
}
},
"public.UploadResponse": { "public.UploadResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -419,6 +419,80 @@
} }
} }
}, },
"/admin/config": {
"get": {
"security": [
{
"AdminAuth": []
}
],
"description": "获取系统的完整配置文件内容(仅管理员)",
"produces": [
"application/json"
],
"tags": [
"Admin"
],
"summary": "获取完整配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/config.Config"
}
}
}
},
"put": {
"security": [
{
"AdminAuth": []
}
],
"description": "更新系统的配置文件内容(仅管理员)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin"
],
"summary": "更新配置",
"parameters": [
{
"description": "新配置内容",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/config.Config"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/model.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/model.Response"
}
}
}
}
},
"/admin/login": { "/admin/login": {
"post": { "post": {
"description": "通过密码换取 JWT Token", "description": "通过密码换取 JWT Token",
@@ -691,6 +765,38 @@
} }
} }
}, },
"/api/config": {
"get": {
"description": "获取前端展示所需的非敏感配置数据",
"produces": [
"application/json"
],
"tags": [
"Public"
],
"summary": "获取公共配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/model.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/public.PublicConfig"
}
}
}
]
}
}
}
}
},
"/api/files/{file_id}/download": { "/api/files/{file_id}/download": {
"get": { "get": {
"description": "根据文件 ID 下载单个文件", "description": "根据文件 ID 下载单个文件",
@@ -824,6 +930,149 @@
} }
} }
}, },
"config.APITokenConfig": {
"type": "object",
"properties": {
"allowAdminAPI": {
"type": "boolean"
},
"enabled": {
"type": "boolean"
},
"maxTokens": {
"type": "integer"
}
}
},
"config.Config": {
"type": "object",
"properties": {
"apitoken": {
"$ref": "#/definitions/config.APITokenConfig"
},
"database": {
"$ref": "#/definitions/config.DatabaseConfig"
},
"security": {
"$ref": "#/definitions/config.SecurityConfig"
},
"site": {
"$ref": "#/definitions/config.SiteConfig"
},
"storage": {
"$ref": "#/definitions/config.StorageConfig"
},
"upload": {
"$ref": "#/definitions/config.UploadConfig"
}
}
},
"config.DatabaseConfig": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
},
"config.SecurityConfig": {
"type": "object",
"properties": {
"adminPasswordHash": {
"type": "string"
},
"jwtsecret": {
"type": "string"
},
"pickupCodeLength": {
"type": "integer"
},
"pickupFailLimit": {
"type": "integer"
}
}
},
"config.SiteConfig": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"config.StorageConfig": {
"type": "object",
"properties": {
"local": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
},
"s3": {
"type": "object",
"properties": {
"accessKey": {
"type": "string"
},
"bucket": {
"type": "string"
},
"endpoint": {
"type": "string"
},
"region": {
"type": "string"
},
"secretKey": {
"type": "string"
},
"useSSL": {
"type": "boolean"
}
}
},
"type": {
"type": "string"
},
"webDAV": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"root": {
"type": "string"
},
"url": {
"type": "string"
},
"username": {
"type": "string"
}
}
}
}
},
"config.UploadConfig": {
"type": "object",
"properties": {
"maxBatchFiles": {
"type": "integer"
},
"maxFileSizeMB": {
"type": "integer"
},
"maxRetentionDays": {
"type": "integer"
}
}
},
"model.APIToken": { "model.APIToken": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -972,6 +1221,25 @@
} }
} }
}, },
"public.PublicConfig": {
"type": "object",
"properties": {
"api_token": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"site": {
"$ref": "#/definitions/config.SiteConfig"
},
"upload": {
"$ref": "#/definitions/config.UploadConfig"
}
}
},
"public.UploadResponse": { "public.UploadResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -59,6 +59,98 @@ definitions:
status: status:
type: string type: string
type: object type: object
config.APITokenConfig:
properties:
allowAdminAPI:
type: boolean
enabled:
type: boolean
maxTokens:
type: integer
type: object
config.Config:
properties:
apitoken:
$ref: '#/definitions/config.APITokenConfig'
database:
$ref: '#/definitions/config.DatabaseConfig'
security:
$ref: '#/definitions/config.SecurityConfig'
site:
$ref: '#/definitions/config.SiteConfig'
storage:
$ref: '#/definitions/config.StorageConfig'
upload:
$ref: '#/definitions/config.UploadConfig'
type: object
config.DatabaseConfig:
properties:
path:
type: string
type: object
config.SecurityConfig:
properties:
adminPasswordHash:
type: string
jwtsecret:
type: string
pickupCodeLength:
type: integer
pickupFailLimit:
type: integer
type: object
config.SiteConfig:
properties:
description:
type: string
name:
type: string
type: object
config.StorageConfig:
properties:
local:
properties:
path:
type: string
type: object
s3:
properties:
accessKey:
type: string
bucket:
type: string
endpoint:
type: string
region:
type: string
secretKey:
type: string
useSSL:
type: boolean
type: object
type:
type: string
webDAV:
properties:
password:
type: string
root:
type: string
url:
type: string
username:
type: string
type: object
type: object
config.UploadConfig:
properties:
maxBatchFiles:
type: integer
maxFileSizeMB:
type: integer
maxRetentionDays:
type: integer
type: object
model.APIToken: model.APIToken:
properties: properties:
created_at: created_at:
@@ -158,6 +250,18 @@ definitions:
type: type:
type: string type: string
type: object type: object
public.PublicConfig:
properties:
api_token:
properties:
enabled:
type: boolean
type: object
site:
$ref: '#/definitions/config.SiteConfig'
upload:
$ref: '#/definitions/config.UploadConfig'
type: object
public.UploadResponse: public.UploadResponse:
properties: properties:
batch_id: batch_id:
@@ -440,6 +544,52 @@ paths:
summary: 修改批次信息 summary: 修改批次信息
tags: tags:
- Admin - Admin
/admin/config:
get:
description: 获取系统的完整配置文件内容(仅管理员)
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/config.Config'
security:
- AdminAuth: []
summary: 获取完整配置
tags:
- Admin
put:
consumes:
- application/json
description: 更新系统的配置文件内容(仅管理员)
parameters:
- description: 新配置内容
in: body
name: config
required: true
schema:
$ref: '#/definitions/config.Config'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/model.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/model.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/model.Response'
security:
- AdminAuth: []
summary: 更新配置
tags:
- Admin
/admin/login: /admin/login:
post: post:
consumes: consumes:
@@ -607,6 +757,24 @@ paths:
summary: 发送长文本 summary: 发送长文本
tags: tags:
- Public - Public
/api/config:
get:
description: 获取前端展示所需的非敏感配置数据
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/model.Response'
- properties:
data:
$ref: '#/definitions/public.PublicConfig'
type: object
summary: 获取公共配置
tags:
- Public
/api/files/{file_id}/download: /api/files/{file_id}/download:
get: get:
description: 根据文件 ID 下载单个文件 description: 根据文件 ID 下载单个文件

View File

@@ -0,0 +1,70 @@
package admin
import (
"FileRelay/internal/bootstrap"
"FileRelay/internal/config"
"FileRelay/internal/model"
"net/http"
"github.com/gin-gonic/gin"
)
type ConfigHandler struct{}
func NewConfigHandler() *ConfigHandler {
return &ConfigHandler{}
}
// GetConfig 获取当前完整配置
// @Summary 获取完整配置
// @Description 获取系统的完整配置文件内容(仅管理员)
// @Tags Admin
// @Security AdminAuth
// @Produce json
// @Success 200 {object} config.Config
// @Router /admin/config [get]
func (h *ConfigHandler) GetConfig(c *gin.Context) {
c.JSON(http.StatusOK, config.GlobalConfig)
}
// UpdateConfig 更新配置
// @Summary 更新配置
// @Description 更新系统的配置文件内容(仅管理员)
// @Tags Admin
// @Security AdminAuth
// @Accept json
// @Produce json
// @Param config body config.Config true "新配置内容"
// @Success 200 {object} model.Response
// @Failure 400 {object} model.Response
// @Failure 500 {object} model.Response
// @Router /admin/config [put]
func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
var newConfig config.Config
if err := c.ShouldBindJSON(&newConfig); err != nil {
c.JSON(http.StatusBadRequest, model.ErrorResponse(model.CodeBadRequest, err.Error()))
return
}
// 简单的校验,防止数据库路径被改空等关键错误
if newConfig.Database.Path == "" {
newConfig.Database.Path = config.GlobalConfig.Database.Path
}
// 更新内存配置
config.UpdateGlobalConfig(&newConfig)
// 重新初始化存储(热更新业务逻辑)
if err := bootstrap.ReloadStorage(); err != nil {
c.JSON(http.StatusInternalServerError, model.ErrorResponse(model.CodeInternalError, "Failed to reload storage: "+err.Error()))
return
}
// 保存到文件
if err := config.SaveConfig(); err != nil {
c.JSON(http.StatusInternalServerError, model.ErrorResponse(model.CodeInternalError, "Failed to save config: "+err.Error()))
return
}
c.JSON(http.StatusOK, model.SuccessResponse("Config updated successfully and hot-reloaded"))
}

View File

@@ -0,0 +1,41 @@
package public
import (
"FileRelay/internal/config"
"FileRelay/internal/model"
"net/http"
"github.com/gin-gonic/gin"
)
type ConfigHandler struct{}
func NewConfigHandler() *ConfigHandler {
return &ConfigHandler{}
}
// PublicConfig 公开配置结构
type PublicConfig struct {
Site config.SiteConfig `json:"site"`
Upload config.UploadConfig `json:"upload"`
APIToken struct {
Enabled bool `json:"enabled"`
} `json:"api_token"`
}
// GetPublicConfig 获取非敏感配置
// @Summary 获取公共配置
// @Description 获取前端展示所需的非敏感配置数据
// @Tags Public
// @Produce json
// @Success 200 {object} model.Response{data=PublicConfig}
// @Router /api/config [get]
func (h *ConfigHandler) GetPublicConfig(c *gin.Context) {
pub := PublicConfig{
Site: config.GlobalConfig.Site,
Upload: config.GlobalConfig.Upload,
}
pub.APIToken.Enabled = config.GlobalConfig.APIToken.Enabled
c.JSON(http.StatusOK, model.SuccessResponse(pub))
}

View File

@@ -43,13 +43,15 @@ func InitDB() {
fmt.Println("Database initialized and migrated.") fmt.Println("Database initialized and migrated.")
// 初始化存储 // 初始化存储
initStorage() if err := ReloadStorage(); err != nil {
log.Fatalf("Failed to initialize storage: %v", err)
}
// 初始化管理员 (如果不存在) // 初始化管理员 (如果不存在)
initAdmin() initAdmin()
} }
func initStorage() { func ReloadStorage() error {
storageType := config.GlobalConfig.Storage.Type storageType := config.GlobalConfig.Storage.Type
switch storageType { switch storageType {
case "local": case "local":
@@ -61,13 +63,14 @@ func initStorage() {
cfg := config.GlobalConfig.Storage.S3 cfg := config.GlobalConfig.Storage.S3
s3Storage, err := storage.NewS3Storage(context.Background(), cfg.Endpoint, cfg.Region, cfg.AccessKey, cfg.SecretKey, cfg.Bucket, cfg.UseSSL) s3Storage, err := storage.NewS3Storage(context.Background(), cfg.Endpoint, cfg.Region, cfg.AccessKey, cfg.SecretKey, cfg.Bucket, cfg.UseSSL)
if err != nil { if err != nil {
log.Fatalf("Failed to initialize S3 storage: %v", err) return err
} }
storage.GlobalStorage = s3Storage storage.GlobalStorage = s3Storage
default: default:
log.Fatalf("Unsupported storage type: %s", storageType) return fmt.Errorf("unsupported storage type: %s", storageType)
} }
fmt.Printf("Storage initialized with type: %s\n", storageType) fmt.Printf("Storage initialized with type: %s\n", storageType)
return nil
} }
func initAdmin() { func initAdmin() {

View File

@@ -2,6 +2,7 @@ package config
import ( import (
"os" "os"
"sync"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -64,9 +65,16 @@ type DatabaseConfig struct {
Path string `yaml:"path"` Path string `yaml:"path"`
} }
var GlobalConfig *Config var (
GlobalConfig *Config
ConfigPath string
configLock sync.RWMutex
)
func LoadConfig(path string) error { func LoadConfig(path string) error {
configLock.Lock()
defer configLock.Unlock()
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return err return err
@@ -78,5 +86,24 @@ func LoadConfig(path string) error {
} }
GlobalConfig = &cfg GlobalConfig = &cfg
ConfigPath = path
return nil return nil
} }
func SaveConfig() error {
configLock.RLock()
defer configLock.RUnlock()
data, err := yaml.Marshal(GlobalConfig)
if err != nil {
return err
}
return os.WriteFile(ConfigPath, data, 0644)
}
func UpdateGlobalConfig(cfg *Config) {
configLock.Lock()
defer configLock.Unlock()
GlobalConfig = cfg
}

47
internal/storage/local.go Normal file
View File

@@ -0,0 +1,47 @@
package storage
import (
"context"
"io"
"os"
"path/filepath"
)
type LocalStorage struct {
RootPath string
}
func NewLocalStorage(rootPath string) *LocalStorage {
// 确保根目录存在
if _, err := os.Stat(rootPath); os.IsNotExist(err) {
os.MkdirAll(rootPath, 0755)
}
return &LocalStorage{RootPath: rootPath}
}
func (s *LocalStorage) Save(ctx context.Context, path string, reader io.Reader) error {
fullPath := filepath.Join(s.RootPath, path)
dir := filepath.Dir(fullPath)
if _, err := os.Stat(dir); os.IsNotExist(err) {
os.MkdirAll(dir, 0755)
}
file, err := os.Create(fullPath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, reader)
return err
}
func (s *LocalStorage) Open(ctx context.Context, path string) (io.ReadCloser, error) {
fullPath := filepath.Join(s.RootPath, path)
return os.Open(fullPath)
}
func (s *LocalStorage) Delete(ctx context.Context, path string) error {
fullPath := filepath.Join(s.RootPath, path)
return os.Remove(fullPath)
}

72
internal/storage/s3.go Normal file
View File

@@ -0,0 +1,72 @@
package storage
import (
"context"
"io"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type S3Storage struct {
client *s3.Client
bucket string
}
func NewS3Storage(ctx context.Context, endpoint, region, accessKey, secretKey, bucket string, useSSL bool) (*S3Storage, error) {
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
if endpoint != "" {
return aws.Endpoint{
URL: endpoint,
SigningRegion: region,
HostnameImmutable: true,
}, nil
}
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
})
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(region),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
config.WithEndpointResolverWithOptions(customResolver),
)
if err != nil {
return nil, err
}
client := s3.NewFromConfig(cfg)
return &S3Storage{
client: client,
bucket: bucket,
}, nil
}
func (s *S3Storage) Save(ctx context.Context, path string, reader io.Reader) error {
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(path),
Body: reader,
})
return err
}
func (s *S3Storage) Open(ctx context.Context, path string) (io.ReadCloser, error) {
output, err := s.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(path),
})
if err != nil {
return nil, err
}
return output.Body, nil
}
func (s *S3Storage) Delete(ctx context.Context, path string) error {
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(path),
})
return err
}

View File

@@ -0,0 +1,14 @@
package storage
import (
"context"
"io"
)
type Storage interface {
Save(ctx context.Context, path string, reader io.Reader) error
Open(ctx context.Context, path string) (io.ReadCloser, error)
Delete(ctx context.Context, path string) error
}
var GlobalStorage Storage

View File

@@ -0,0 +1,54 @@
package storage
import (
"context"
"io"
"path/filepath"
"strings"
"github.com/studio-b12/gowebdav"
)
type WebDAVStorage struct {
client *gowebdav.Client
root string
}
func NewWebDAVStorage(url, username, password, root string) *WebDAVStorage {
client := gowebdav.NewClient(url, username, password)
return &WebDAVStorage{
client: client,
root: root,
}
}
func (s *WebDAVStorage) getFullPath(path string) string {
return filepath.ToSlash(filepath.Join(s.root, path))
}
func (s *WebDAVStorage) Save(ctx context.Context, path string, reader io.Reader) error {
fullPath := s.getFullPath(path)
// Ensure directory exists
dir := filepath.Dir(fullPath)
if dir != "." && dir != "/" {
parts := strings.Split(strings.Trim(dir, "/"), "/")
current := ""
for _, part := range parts {
current += "/" + part
_ = s.client.Mkdir(current, 0755)
}
}
return s.client.WriteStream(fullPath, reader, 0644)
}
func (s *WebDAVStorage) Open(ctx context.Context, path string) (io.ReadCloser, error) {
fullPath := s.getFullPath(path)
return s.client.ReadStream(fullPath)
}
func (s *WebDAVStorage) Delete(ctx context.Context, path string) error {
fullPath := s.getFullPath(path)
return s.client.Remove(fullPath)
}

View File

@@ -66,9 +66,11 @@ func main() {
// 公共接口 // 公共接口
uploadHandler := public.NewUploadHandler() uploadHandler := public.NewUploadHandler()
pickupHandler := public.NewPickupHandler() pickupHandler := public.NewPickupHandler()
publicConfigHandler := public.NewConfigHandler()
api := r.Group("/api") api := r.Group("/api")
{ {
api.GET("/config", publicConfigHandler.GetPublicConfig)
// 统一使用 /batches 作为资源路径 // 统一使用 /batches 作为资源路径
api.POST("/batches", uploadHandler.Upload) api.POST("/batches", uploadHandler.Upload)
api.POST("/batches/text", uploadHandler.UploadText) api.POST("/batches/text", uploadHandler.UploadText)
@@ -85,12 +87,16 @@ func main() {
authHandler := admin.NewAuthHandler() authHandler := admin.NewAuthHandler()
batchHandler := admin.NewBatchHandler() batchHandler := admin.NewBatchHandler()
tokenHandler := admin.NewTokenHandler() tokenHandler := admin.NewTokenHandler()
configHandler := admin.NewConfigHandler()
r.POST("/admin/login", authHandler.Login) r.POST("/admin/login", authHandler.Login)
adm := r.Group("/admin") adm := r.Group("/admin")
adm.Use(middleware.AdminAuth()) adm.Use(middleware.AdminAuth())
{ {
adm.GET("/config", configHandler.GetConfig)
adm.PUT("/config", configHandler.UpdateConfig)
adm.GET("/batches", batchHandler.ListBatches) adm.GET("/batches", batchHandler.ListBatches)
adm.GET("/batches/:batch_id", batchHandler.GetBatch) adm.GET("/batches/:batch_id", batchHandler.GetBatch)
adm.PUT("/batches/:batch_id", batchHandler.UpdateBatch) adm.PUT("/batches/:batch_id", batchHandler.UpdateBatch)