From fe656fb298995032cf9b337fd3bb18879607df8b Mon Sep 17 00:00:00 2001 From: hxuanyu <2252193204@qq.com> Date: Wed, 14 Jan 2026 14:15:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=85=8D=E7=BD=AE=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD=E5=8F=8A=E5=A4=9A=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加管理员端 API,用于获取和更新完整配置。 - 添加公共端 API,用于获取非敏感配置信息。 - 增加本地存储(LocalStorage)、S3(S3Storage)、和 WebDAV(WebDAVStorage)存储类型的实现。 - 支持热更新存储配置和保存配置文件至磁盘。 - 更新 Swagger 文档以包含新接口定义。 --- .gitignore | 1 - docs/docs.go | 268 ++++++++++++++++++++++++++++++++++ docs/swagger.json | 268 ++++++++++++++++++++++++++++++++++ docs/swagger.yaml | 168 +++++++++++++++++++++ internal/api/admin/config.go | 70 +++++++++ internal/api/public/config.go | 41 ++++++ internal/bootstrap/init.go | 11 +- internal/config/config.go | 29 +++- internal/storage/local.go | 47 ++++++ internal/storage/s3.go | 72 +++++++++ internal/storage/storage.go | 14 ++ internal/storage/webdav.go | 54 +++++++ main.go | 6 + 13 files changed, 1043 insertions(+), 6 deletions(-) create mode 100644 internal/api/admin/config.go create mode 100644 internal/api/public/config.go create mode 100644 internal/storage/local.go create mode 100644 internal/storage/s3.go create mode 100644 internal/storage/storage.go create mode 100644 internal/storage/webdav.go diff --git a/.gitignore b/.gitignore index a28a1ce..b6c5e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ *.sqlite *.sqlite3 data/ -storage/ tmp/ # 环境变量与配置(按需:如果你会提交示例配置,建议仅忽略真实配置文件) diff --git a/docs/docs.go b/docs/docs.go index 8cbf592..a5f9500 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { "post": { "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": { "get": { "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": { "type": "object", "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": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index db38483..f806bf9 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { "post": { "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": { "get": { "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": { "type": "object", "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": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 679924e..e1e334e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -59,6 +59,98 @@ definitions: status: type: string 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: properties: created_at: @@ -158,6 +250,18 @@ definitions: type: type: string 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: properties: batch_id: @@ -440,6 +544,52 @@ paths: summary: 修改批次信息 tags: - 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: post: consumes: @@ -607,6 +757,24 @@ paths: summary: 发送长文本 tags: - 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: get: description: 根据文件 ID 下载单个文件 diff --git a/internal/api/admin/config.go b/internal/api/admin/config.go new file mode 100644 index 0000000..8269d07 --- /dev/null +++ b/internal/api/admin/config.go @@ -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")) +} diff --git a/internal/api/public/config.go b/internal/api/public/config.go new file mode 100644 index 0000000..bd1ede2 --- /dev/null +++ b/internal/api/public/config.go @@ -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)) +} diff --git a/internal/bootstrap/init.go b/internal/bootstrap/init.go index 6a2160f..1d76b22 100644 --- a/internal/bootstrap/init.go +++ b/internal/bootstrap/init.go @@ -43,13 +43,15 @@ func InitDB() { fmt.Println("Database initialized and migrated.") // 初始化存储 - initStorage() + if err := ReloadStorage(); err != nil { + log.Fatalf("Failed to initialize storage: %v", err) + } // 初始化管理员 (如果不存在) initAdmin() } -func initStorage() { +func ReloadStorage() error { storageType := config.GlobalConfig.Storage.Type switch storageType { case "local": @@ -61,13 +63,14 @@ func initStorage() { cfg := config.GlobalConfig.Storage.S3 s3Storage, err := storage.NewS3Storage(context.Background(), cfg.Endpoint, cfg.Region, cfg.AccessKey, cfg.SecretKey, cfg.Bucket, cfg.UseSSL) if err != nil { - log.Fatalf("Failed to initialize S3 storage: %v", err) + return err } storage.GlobalStorage = s3Storage 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) + return nil } func initAdmin() { diff --git a/internal/config/config.go b/internal/config/config.go index a58d066..720a95a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "os" + "sync" "gopkg.in/yaml.v3" ) @@ -64,9 +65,16 @@ type DatabaseConfig struct { Path string `yaml:"path"` } -var GlobalConfig *Config +var ( + GlobalConfig *Config + ConfigPath string + configLock sync.RWMutex +) func LoadConfig(path string) error { + configLock.Lock() + defer configLock.Unlock() + data, err := os.ReadFile(path) if err != nil { return err @@ -78,5 +86,24 @@ func LoadConfig(path string) error { } GlobalConfig = &cfg + ConfigPath = path 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 +} diff --git a/internal/storage/local.go b/internal/storage/local.go new file mode 100644 index 0000000..70047d0 --- /dev/null +++ b/internal/storage/local.go @@ -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) +} diff --git a/internal/storage/s3.go b/internal/storage/s3.go new file mode 100644 index 0000000..56abb19 --- /dev/null +++ b/internal/storage/s3.go @@ -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 +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..619f224 --- /dev/null +++ b/internal/storage/storage.go @@ -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 diff --git a/internal/storage/webdav.go b/internal/storage/webdav.go new file mode 100644 index 0000000..f15f81d --- /dev/null +++ b/internal/storage/webdav.go @@ -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) +} diff --git a/main.go b/main.go index 390054f..744b6de 100644 --- a/main.go +++ b/main.go @@ -66,9 +66,11 @@ func main() { // 公共接口 uploadHandler := public.NewUploadHandler() pickupHandler := public.NewPickupHandler() + publicConfigHandler := public.NewConfigHandler() api := r.Group("/api") { + api.GET("/config", publicConfigHandler.GetPublicConfig) // 统一使用 /batches 作为资源路径 api.POST("/batches", uploadHandler.Upload) api.POST("/batches/text", uploadHandler.UploadText) @@ -85,12 +87,16 @@ func main() { authHandler := admin.NewAuthHandler() batchHandler := admin.NewBatchHandler() tokenHandler := admin.NewTokenHandler() + configHandler := admin.NewConfigHandler() r.POST("/admin/login", authHandler.Login) adm := r.Group("/admin") adm.Use(middleware.AdminAuth()) { + adm.GET("/config", configHandler.GetConfig) + adm.PUT("/config", configHandler.UpdateConfig) + adm.GET("/batches", batchHandler.ListBatches) adm.GET("/batches/:batch_id", batchHandler.GetBatch) adm.PUT("/batches/:batch_id", batchHandler.UpdateBatch)