From c8f72decda100182de1f4daf65739375f262b492 Mon Sep 17 00:00:00 2001 From: murphyyi Date: Tue, 16 Sep 2025 15:57:00 +0800 Subject: [PATCH 01/21] chore(config): remove InitWithDB usage and add PR draft; prepare for removing DB-based config --- .github/copilot-instructions.md | 2 +- README.md | 60 + config.generated.yaml | 55 + docs/PR_REMOVE_INITWITHDB.md | 28 + docs/docs.go | 1299 +++--------- docs/swagger.json | 83 +- docs/swagger.yaml | 1791 ++++------------- go.mod | 37 +- go.sum | 163 ++ internal/cli/admin.go | 169 ++ internal/cli/cli.go | 87 + internal/config/manager.go | 667 ++---- internal/config/manager_test.go | 77 + internal/handlers/admin.go | 85 +- internal/handlers/setup.go | 622 +++++- internal/handlers/storage.go | 30 + internal/handlers/user.go | 16 +- internal/models/db/filecode_test.go | 10 +- internal/models/service/admin.go | 8 +- internal/models/web/storage.go | 4 + internal/models/web/user.go | 7 +- internal/repository/manager.go | 5 + internal/routes/admin.go | 4 + internal/routes/base.go | 50 +- internal/routes/setup.go | 54 + internal/routes/user.go | 13 +- internal/services/admin/config.go | 2 - internal/services/admin/maintenance.go | 38 +- internal/services/admin/users.go | 43 + internal/utils/disk.go | 25 + internal/utils/disk_test.go | 41 + main.go | 157 +- scripts/export_config_from_db.go | 141 ++ scripts/export_config_from_db.py | 76 + test_expiry.txt | 1 - debug_homepage.sh => tests/debug_homepage.sh | 0 test.txt => tests/test.txt | 0 .../test_design_system.html | 0 .../test_file_list.html | 0 .../test_file_management.html | 0 themes/2025/admin/css/storage.css | 75 + themes/2025/admin/css/users.css | 108 +- themes/2025/admin/index.html | 34 +- themes/2025/admin/js/config-simple.js | 3 +- themes/2025/admin/js/main.js | 37 + themes/2025/admin/js/storage-simple.js | 79 +- themes/2025/admin/js/users.js | 64 +- themes/2025/js/auth.js | 3 +- themes/2025/js/main.js | 89 +- themes/2025/login.html | 2 +- themes/2025/register.html | 6 +- themes/2025/setup.html | 81 +- 52 files changed, 3289 insertions(+), 3242 deletions(-) create mode 100644 config.generated.yaml create mode 100644 docs/PR_REMOVE_INITWITHDB.md create mode 100644 internal/cli/admin.go create mode 100644 internal/cli/cli.go create mode 100644 internal/config/manager_test.go create mode 100644 internal/utils/disk.go create mode 100644 internal/utils/disk_test.go create mode 100644 scripts/export_config_from_db.go create mode 100644 scripts/export_config_from_db.py delete mode 100644 test_expiry.txt rename debug_homepage.sh => tests/debug_homepage.sh (100%) rename test.txt => tests/test.txt (100%) rename test_design_system.html => tests/test_design_system.html (100%) rename test_file_list.html => tests/test_file_list.html (100%) rename test_file_management.html => tests/test_file_management.html (100%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index eb6e64d..ce0dc40 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -20,7 +20,7 @@ FileCodeBox 是一个高性能的文件快传系统的 Go 实现,基于现代 ```go // 通过 ConfigManager 统一管理所有配置 manager := config.InitManager() -manager.InitWithDB(db) // 数据库驱动的动态配置 +manager.SetDB(db) // 注入数据库连接(配置读取现在以 config.yaml 和 环境变量为准) ``` 配置分为多个模块:`BaseConfig`, `DatabaseConfig`, `StorageConfig`, `UserSystemConfig`, `MCPConfig` diff --git a/README.md b/README.md index 2becba7..e4f665a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,63 @@ +# FileCodeBox (Go) + +轻量且高性能的文件/文本分享服务,使用 Go 实现,支持分片上传、秒传、断点续传与多种存储后端。 + +核心目标是提供一个易部署、易扩展的文件快传系统,适合自托管与容器化部署场景。 + +## 主要特性 + +- 高性能:基于 Go 的并发能力构建,低延迟与内存占用 +- 文件/文本分享:支持短链接分享文本和文件 +- 分片上传:大文件分片、断点续传、上传校验与秒传支持 +- 管理后台:内置管理控制台,可管理文件、配置与用户 +- 多存储后端:支持本地、S3、WebDAV、OneDrive(可扩展) +- 容器友好:提供 Docker 与 docker-compose 支持 +- 主题系统:前端主题可替换与定制 + +## 环境要求 + +开发推荐使用 Go 1.25+。项目默认使用 SQLite 作为开发环境的轻量数据库。生产环境请按需选择存储与资源配置。 + +## 部署建议(简要) + +- 推荐使用 Docker + 反向代理(Nginx)启用 HTTPS +- 将 `data/` 目录做定期备份 +- 将服务放入进程管理(systemd / 容器重启策略) + +## 开发与扩展 + +- 新增存储:实现 `storage.StorageInterface` 并在 `storage.NewStorageManager` 注册 +- 新增接口:在 `internal/services` 实现业务逻辑,并在 `internal/handlers` 与 `internal/routes` 添加路由 + +运行测试与示例脚本请查看 `tests/` 目录。 + +--- + +## 常见问题与排查(示例) + +- 检查端口占用: + +```bash +lsof -ti:12345 +``` + +- 如果数据库被锁或服务异常,尝试重启服务或检查 `data/` 下的 sqlite 文件权限。 + +--- + +## 许可证 + +MIT + +--- + +如需我继续: + +- 将 README 翻译为英文 +- 自动生成或更新 Swagger 文档 +- 补全详细部署示例(Kubernetes / systemd) + +请告诉我接下来要做哪个扩展。
FileCodeBox Logo diff --git a/config.generated.yaml b/config.generated.yaml new file mode 100644 index 0000000..023a595 --- /dev/null +++ b/config.generated.yaml @@ -0,0 +1,55 @@ +base: + data_path: /tmp/filecodebox_test + description: 开箱即用的文件快传系统 + host: 0 + name: FileCodeBox + port: 12346 + production: false +database: + host: "" + name: ./data/filecodebox.db + port: 0 + ssl: disable + type: sqlite + user: "" +mcp: + host: 0 + port: 8081 +storage: {} +ui: + admin_token: zhangyi + allow_user_registration: 0 + background: "" + chunk_size: 2097152 + download_timeout: 300 + enable_chunk: 0 + enable_concurrent_download: 1 + enable_mcp_server: 0 + error_count: 1 + error_minute: 1 + file_storage: local + jwt_secret: FileCodeBox2025JWT + keywords: FileCodeBox, 文件快递柜, 口令传送箱, 匿名口令分享文本, 文件 + max_concurrent_downloads: 10 + max_save_seconds: 0 + max_sessions_per_user: 5 + notify_content: 欢迎使用 FileCodeBox,本程序开源于 Github ,欢迎Star和Fork。 + notify_title: 系统通知 + opacity: 0 + open_upload: 1 + page_explain: 请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。 + require_email_verify: 0 + robots_text: |- + User-agent: * + Disallow: / + session_expiry_hours: 168 + show_admin_address: 0 + storage_path: "" + sys_start: 1757914992279 + themes_select: themes/2025 + upload_count: 10 + upload_minute: 1 + upload_size: 100 + user_storage_quota: 1073741824 + user_upload_size: 52428800 +user: {} diff --git a/docs/PR_REMOVE_INITWITHDB.md b/docs/PR_REMOVE_INITWITHDB.md new file mode 100644 index 0000000..dbd4d4c --- /dev/null +++ b/docs/PR_REMOVE_INITWITHDB.md @@ -0,0 +1,28 @@ +PR: Remove InitWithDB and stop using DB rows for configuration + +Summary +------- +This PR removes the legacy initializer `InitWithDB()` from the configuration manager and changes the configuration source model to be YAML-first (`config.yaml`) with environment variable overrides. Database per-row configuration is no longer read or written by the application. + +Key changes +----------- +- `InitWithDB()` removed from `internal/config/manager.go`. +- `SetDB(db *gorm.DB)` remains to allow injecting the DB connection if other subsystems need it, but it is not used for configuration loading. +- `InitDefaultDataInDB()`, `LoadFromDatabase()`, `saveToDatabase()`, and `Save()` no-op stubs were removed (or will be removed in follow-up commit) to avoid accidental DB-based config usage. +- Call sites using `InitWithDB()` are updated to call `SetDB(db)` instead. +- Updated docs with migration guidance and example `config.yaml` usage. + +Migration guidance +------------------ +1. Provide configuration via `config.yaml` at the repository root or set `CONFIG_PATH` to point to your YAML configuration file. +2. Environment variables take precedence for runtime overrides (`PORT`, `ADMIN_TOKEN`, `DATA_PATH`, etc.). +3. If you previously relied on DB rows for config, export them to YAML using the included script `scripts/export_config_from_db.go` and place the resulting file as `config.yaml`. + +Why this change +---------------- +Using `config.yaml` as the authoritative source simplifies deployment, reduces surprising runtime writes to the DB, and avoids configuration drift across instances. It also removes the complexity of managing multiple legacy DB formats. + +Notes +----- +- This is a breaking change if your deployment relied on DB rows for live configuration. Ensure you export DB config and place it in `config.yaml` before upgrading. +- If you want help producing the `config.yaml` from your DB, run `go run scripts/export_config_from_db.go` or `python3 scripts/export_config_from_db.py`. diff --git a/docs/docs.go b/docs/docs.go index 0c996e3..b5c844c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,13 +1,7 @@ -// Package docs Code generated by swaggo/swag at -// 2025-01-11 20:00:00.000000 +0800 CST m=+0.021639876. -// This file was generated by swaggo/swag -// Enhanced version with comprehensive API documentation - +// Package docs Code generated by swaggo/swag. DO NOT EDIT package docs -import ( - "github.com/swaggo/swag" -) +import "github.com/swaggo/swag" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, @@ -15,6 +9,7 @@ const docTemplate = `{ "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", "contact": { "name": "API Support", "url": "http://www.swagger.io/support", @@ -28,344 +23,214 @@ const docTemplate = `{ }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", - "securityDefinitions": { - "ApiKeyAuth": { - "type": "apiKey", - "name": "X-API-Key", - "in": "header" - }, - "BearerAuth": { - "type": "apiKey", - "name": "Authorization", - "in": "header" - }, - "BasicAuth": { - "type": "basic" - } - }, - "tags": [ - { - "name": "系统", - "description": "系统相关接口" - }, - { - "name": "分享", - "description": "文件和文本分享接口" - }, - { - "name": "分片上传", - "description": "大文件分片上传接口" - }, - { - "name": "用户", - "description": "用户认证和管理接口" - }, - { - "name": "管理员", - "description": "管理员接口" - }, - { - "name": "存储", - "description": "存储管理接口" - } - ], "paths": { - "/health": { + "/api/config": { "get": { - "tags": [ - "系统" + "description": "获取前端所需的系统配置信息", + "consumes": [ + "application/json" ], - "summary": "健康检查", - "description": "检查服务器健康状态", "produces": [ "application/json" ], - "responses": { - "200": { - "description": "健康状态信息", - "schema": { - "$ref": "#/definitions/handlers.HealthResponse" - } - } - } - } - }, - "/api/config": { - "get": { "tags": [ "系统" ], "summary": "获取系统配置", - "description": "获取前端所需的系统配置信息", - "produces": [ - "application/json" - ], "responses": { "200": { "description": "系统配置信息", "schema": { - "$ref": "#/definitions/models.SystemConfig" + "$ref": "#/definitions/handlers.SystemConfig" } } } } }, - "/share/text/": { + "/chunk/upload/chunk/{upload_id}/{chunk_index}": { "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "tags": [ - "分享" - ], - "summary": "分享文本内容", - "description": "分享文本内容并生成分享代码", + "description": "上传指定索引的文件分片", "consumes": [ - "multipart/form-data", - "application/json" + "multipart/form-data" ], "produces": [ "application/json" ], + "tags": [ + "分片上传" + ], + "summary": "上传文件分片", "parameters": [ { "type": "string", - "description": "文本内容", - "name": "text", - "in": "formData", + "description": "上传ID", + "name": "upload_id", + "in": "path", "required": true }, { "type": "integer", - "default": 1, - "description": "过期值", - "name": "expire_value", - "in": "formData" - }, - { - "enum": [ - "minute", - "hour", - "day", - "week", - "month", - "year", - "forever" - ], - "type": "string", - "default": "day", - "description": "过期样式", - "name": "expire_style", - "in": "formData" + "description": "分片索引", + "name": "chunk_index", + "in": "path", + "required": true }, { - "type": "boolean", - "default": false, - "description": "是否需要认证", - "name": "require_auth", - "in": "formData" + "type": "file", + "description": "分片文件", + "name": "chunk", + "in": "formData", + "required": true } ], "responses": { "200": { - "description": "分享成功", + "description": "上传成功,返回分片哈希", "schema": { - "$ref": "#/definitions/models.ShareResponse" + "type": "object", + "additionalProperties": true } }, "400": { "description": "请求参数错误", "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "401": { - "description": "认证失败", - "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } }, "500": { "description": "服务器内部错误", "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } } } } }, - "/share/file/": { + "/chunk/upload/complete/{upload_id}": { "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "tags": [ - "分享" - ], - "summary": "分享文件", - "description": "上传并分享文件,生成分享代码", + "description": "完成所有分片上传,合并文件并生成分享代码", "consumes": [ - "multipart/form-data" + "application/json" ], "produces": [ "application/json" ], + "tags": [ + "分片上传" + ], + "summary": "完成分片上传", "parameters": [ { - "type": "file", - "description": "要分享的文件", - "name": "file", - "in": "formData", - "required": true - }, - { - "type": "integer", - "default": 1, - "description": "过期值", - "name": "expire_value", - "in": "formData" - }, - { - "enum": [ - "minute", - "hour", - "day", - "week", - "month", - "year", - "forever" - ], "type": "string", - "default": "day", - "description": "过期样式", - "name": "expire_style", - "in": "formData" + "description": "上传ID", + "name": "upload_id", + "in": "path", + "required": true }, { - "type": "boolean", - "default": false, - "description": "是否需要认证", - "name": "require_auth", - "in": "formData" + "description": "完成上传参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "type": "object" + } } ], "responses": { "200": { - "description": "分享成功", + "description": "上传完成,返回分享代码", "schema": { - "$ref": "#/definitions/models.ShareResponse" + "type": "object", + "additionalProperties": true } }, "400": { "description": "请求参数错误", "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "413": { - "description": "文件过大", - "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } }, "500": { "description": "服务器内部错误", "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } } } } }, - "/share/select/": { - "get": { - "tags": [ - "分享" + "/chunk/upload/init/": { + "post": { + "description": "初始化文件分片上传,返回上传ID和分片信息", + "consumes": [ + "application/json" ], - "summary": "获取分享信息", - "description": "根据分享代码获取文件或文本的详细信息", "produces": [ "application/json" ], + "tags": [ + "分片上传" + ], + "summary": "初始化分片上传", "parameters": [ { - "type": "string", - "description": "分享代码", - "name": "code", - "in": "query", - "required": true + "description": "上传初始化参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "type": "object" + } } ], "responses": { "200": { - "description": "分享信息", + "description": "初始化成功,返回上传ID和分片信息", "schema": { - "$ref": "#/definitions/models.ShareInfo" + "type": "object", + "additionalProperties": true } }, "400": { "description": "请求参数错误", "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "404": { - "description": "分享代码不存在", - "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } }, - "410": { - "description": "分享已过期", + "500": { + "description": "服务器内部错误", "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } } } - }, - "post": { - "tags": [ - "分享" - ], - "summary": "获取分享信息", - "description": "根据分享代码获取文件或文本的详细信息(POST方式)", + } + }, + "/health": { + "get": { + "description": "检查服务器健康状态和构建信息", "consumes": [ - "application/json", - "multipart/form-data" + "application/json" ], "produces": [ "application/json" ], - "parameters": [ - { - "type": "string", - "description": "分享代码", - "name": "code", - "in": "formData", - "required": true - } + "tags": [ + "系统" ], + "summary": "健康检查", "responses": { "200": { - "description": "分享信息", - "schema": { - "$ref": "#/definitions/models.ShareInfo" - } - }, - "400": { - "description": "请求参数错误", + "description": "健康状态信息和构建信息", "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "404": { - "description": "分享代码不存在", - "schema": { - "$ref": "#/definitions/common.Response" + "$ref": "#/definitions/handlers.HealthResponse" } } } @@ -373,20 +238,18 @@ const docTemplate = `{ }, "/share/download": { "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "tags": [ - "分享" - ], - "summary": "下载分享文件", "description": "根据分享代码下载文件或获取文本内容", + "consumes": [ + "application/json" + ], "produces": [ "application/octet-stream", "application/json" ], + "tags": [ + "分享" + ], + "summary": "下载分享文件", "parameters": [ { "type": "string", @@ -398,447 +261,280 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "文件下载或文本内容" - }, - "400": { - "description": "请求参数错误", + "description": "文本内容", "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } }, - "401": { - "description": "需要认证", + "400": { + "description": "请求参数错误", "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } }, "404": { "description": "分享代码不存在", "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "410": { - "description": "分享已过期", - "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } } } } }, - "/chunk/upload/init/": { + "/share/file/": { "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "tags": [ - "分片上传" - ], - "summary": "初始化分片上传", - "description": "初始化文件分片上传,返回上传ID和分片信息", + "description": "上传并分享文件,生成分享代码", "consumes": [ - "application/json" + "multipart/form-data" ], "produces": [ "application/json" ], + "tags": [ + "分享" + ], + "summary": "分享文件", "parameters": [ { - "description": "上传初始化参数", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.InitUploadRequest" - } + "type": "file", + "description": "要分享的文件", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "integer", + "default": 1, + "description": "过期值", + "name": "expire_value", + "in": "formData" + }, + { + "enum": [ + "minute", + "hour", + "day", + "week", + "month", + "year", + "forever" + ], + "type": "string", + "default": "day", + "description": "过期样式", + "name": "expire_style", + "in": "formData" + }, + { + "type": "boolean", + "default": false, + "description": "是否需要认证", + "name": "require_auth", + "in": "formData" } ], "responses": { "200": { - "description": "初始化成功", + "description": "分享成功,返回分享代码和文件信息", "schema": { - "$ref": "#/definitions/models.InitUploadResponse" + "type": "object", + "additionalProperties": true } }, "400": { "description": "请求参数错误", "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "413": { - "description": "文件过大", - "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } }, "500": { "description": "服务器内部错误", "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } } } } }, - "/chunk/upload/chunk/{upload_id}/{chunk_index}": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "tags": [ - "分片上传" - ], - "summary": "上传文件分片", - "description": "上传指定索引的文件分片", + "/share/select/": { + "get": { + "description": "根据分享代码获取文件或文本的详细信息", "consumes": [ - "multipart/form-data" + "application/json" ], "produces": [ "application/json" ], + "tags": [ + "分享" + ], + "summary": "获取分享文件信息", "parameters": [ { "type": "string", - "description": "上传ID", - "name": "upload_id", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "分片索引", - "name": "chunk_index", - "in": "path", - "required": true + "description": "分享代码(GET方式)", + "name": "code", + "in": "query" }, { - "type": "file", - "description": "分片文件", - "name": "chunk", - "in": "formData", - "required": true + "type": "string", + "description": "分享代码(POST方式)", + "name": "code", + "in": "formData" } ], "responses": { "200": { - "description": "上传成功", + "description": "文件信息", "schema": { - "$ref": "#/definitions/models.UploadChunkResponse" + "type": "object", + "additionalProperties": true } }, "400": { "description": "请求参数错误", "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } }, "404": { - "description": "上传ID不存在", - "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "500": { - "description": "服务器内部错误", + "description": "分享代码不存在", "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } } } - } - }, - "/chunk/upload/complete/{upload_id}": { + }, "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "tags": [ - "分片上传" - ], - "summary": "完成分片上传", - "description": "完成所有分片上传,合并文件并生成分享代码", + "description": "根据分享代码获取文件或文本的详细信息", "consumes": [ "application/json" ], "produces": [ "application/json" ], + "tags": [ + "分享" + ], + "summary": "获取分享文件信息", "parameters": [ { "type": "string", - "description": "上传ID", - "name": "upload_id", - "in": "path", - "required": true + "description": "分享代码(GET方式)", + "name": "code", + "in": "query" }, { - "description": "完成上传参数", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.CompleteUploadRequest" - } + "type": "string", + "description": "分享代码(POST方式)", + "name": "code", + "in": "formData" } ], "responses": { "200": { - "description": "上传完成", + "description": "文件信息", "schema": { - "$ref": "#/definitions/models.ShareResponse" + "type": "object", + "additionalProperties": true } }, "400": { "description": "请求参数错误", "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } }, "404": { - "description": "上传ID不存在", - "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "500": { - "description": "服务器内部错误", + "description": "分享代码不存在", "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } } } } }, - "/chunk/upload/status/{upload_id}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "tags": [ - "分片上传" + "/share/text/": { + "post": { + "description": "分享文本内容并生成分享代码", + "consumes": [ + "multipart/form-data" ], - "summary": "获取上传状态", - "description": "获取分片上传的进度和状态", "produces": [ "application/json" ], + "tags": [ + "分享" + ], + "summary": "分享文本内容", "parameters": [ { "type": "string", - "description": "上传ID", - "name": "upload_id", - "in": "path", + "description": "文本内容", + "name": "text", + "in": "formData", "required": true - } - ], - "responses": { - "200": { - "description": "上传状态", - "schema": { - "$ref": "#/definitions/models.ChunkStatusResponse" - } }, - "404": { - "description": "上传ID不存在", - "schema": { - "$ref": "#/definitions/common.Response" - } - } - } - } - }, - "/chunk/upload/cancel/{upload_id}": { - "delete": { - "security": [ { - "BearerAuth": [] - } - ], - "tags": [ - "分片上传" - ], - "summary": "取消分片上传", - "description": "取消分片上传并清理相关文件", - "produces": [ - "application/json" - ], - "parameters": [ + "type": "integer", + "default": 1, + "description": "过期值", + "name": "expire_value", + "in": "formData" + }, { + "enum": [ + "minute", + "hour", + "day", + "week", + "month", + "year", + "forever" + ], "type": "string", - "description": "上传ID", - "name": "upload_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "取消成功", - "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "404": { - "description": "上传ID不存在", - "schema": { - "$ref": "#/definitions/common.Response" - } + "default": "day", + "description": "过期样式", + "name": "expire_style", + "in": "formData" }, - "500": { - "description": "服务器内部错误", - "schema": { - "$ref": "#/definitions/common.Response" - } - } - } - } - }, - "/user/register": { - "post": { - "tags": [ - "用户" - ], - "summary": "用户注册", - "description": "注册新用户账号", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ { - "description": "注册信息", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.RegisterRequest" - } + "type": "boolean", + "default": false, + "description": "是否需要认证", + "name": "require_auth", + "in": "formData" } ], "responses": { "200": { - "description": "注册成功", + "description": "分享成功,返回分享代码", "schema": { - "$ref": "#/definitions/models.UserResponse" + "type": "object", + "additionalProperties": true } }, "400": { "description": "请求参数错误", "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "409": { - "description": "用户已存在", - "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } }, "500": { "description": "服务器内部错误", "schema": { - "$ref": "#/definitions/common.Response" - } - } - } - } - }, - "/user/login": { - "post": { - "tags": [ - "用户" - ], - "summary": "用户登录", - "description": "用户登录认证", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "description": "登录信息", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.LoginRequest" - } - } - ], - "responses": { - "200": { - "description": "登录成功", - "schema": { - "$ref": "#/definitions/models.LoginResponse" - } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "401": { - "description": "用户名或密码错误", - "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "500": { - "description": "服务器内部错误", - "schema": { - "$ref": "#/definitions/common.Response" - } - } - } - } - }, - "/admin/stats": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "tags": [ - "管理员" - ], - "summary": "获取系统统计", - "description": "获取系统统计信息", - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "统计信息", - "schema": { - "$ref": "#/definitions/models.AdminStatsResponse" - } - }, - "401": { - "description": "未认证", - "schema": { - "$ref": "#/definitions/common.Response" - } - }, - "403": { - "description": "权限不足", - "schema": { - "$ref": "#/definitions/common.Response" + "type": "object", + "additionalProperties": true } } } @@ -846,30 +542,6 @@ const docTemplate = `{ } }, "definitions": { - "common.Response": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "msg": { - "type": "string" - } - } - }, - "gorm.DeletedAt": { - "type": "object", - "properties": { - "time": { - "type": "string" - }, - "valid": { - "description": "Valid is true if Time is not NULL", - "type": "boolean" - } - } - }, "handlers.HealthResponse": { "type": "object", "properties": { @@ -881,476 +553,65 @@ const docTemplate = `{ "type": "string", "example": "2025-09-11T10:00:00Z" }, - "version": { - "type": "string", - "example": "1.0.0" - }, "uptime": { "type": "string", "example": "2h30m15s" + }, + "version": { + "type": "string", + "example": "1.0.0" } } }, - "models.SystemConfig": { + "handlers.SystemConfig": { "type": "object", "properties": { - "name": { - "type": "string", - "description": "站点名称" - }, "description": { "type": "string", - "description": "站点描述" - }, - "uploadSize": { - "type": "integer", - "description": "最大上传大小(MB)" + "example": "文件分享系统" }, "enableChunk": { "type": "integer", - "description": "是否启用分片上传" - }, - "openUpload": { - "type": "boolean", - "description": "是否开放上传" + "example": 1 }, "expireStyle": { "type": "array", "items": { "type": "string" }, - "description": "过期时间选项" - } - } - }, - "models.ShareResponse": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "分享代码" - }, - "download_url": { - "type": "string", - "description": "下载URL" - }, - "share_url": { - "type": "string", - "description": "分享URL" - }, - "expired_at": { - "type": "string", - "format": "date-time", - "description": "过期时间" - } - } - }, - "models.ShareInfo": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "分享代码" - }, - "type": { - "type": "string", - "enum": ["text", "file"], - "description": "分享类型" - }, - "filename": { - "type": "string", - "description": "文件名(文件类型时)" - }, - "size": { - "type": "integer", - "description": "文件大小(文件类型时)" - }, - "text": { - "type": "string", - "description": "文本内容(文本类型时)" - }, - "expired_at": { - "type": "string", - "format": "date-time", - "description": "过期时间" - }, - "used_count": { - "type": "integer", - "description": "已使用次数" - }, - "expired_count": { - "type": "integer", - "description": "最大使用次数" - }, - "require_auth": { - "type": "boolean", - "description": "是否需要认证" - } - } - }, - "models.FileCode": { - "type": "object", - "properties": { - "code": { - "type": "string" - }, - "count": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "deleted_at": { - "$ref": "#/definitions/gorm.DeletedAt" - }, - "expire_count": { - "type": "integer" - }, - "expire_style": { - "type": "string" - }, - "expire_value": { - "type": "integer" - }, - "expired_at": { - "type": "string" - }, - "file_path": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "owner_ip": { - "type": "string" - }, - "owner_token": { - "type": "string" - }, - "prefix": { - "type": "string" - }, - "size": { - "type": "integer" - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "used_count": { - "type": "integer" - }, - "user_id": { - "type": "integer" - } - } - }, - "models.InitUploadRequest": { - "type": "object", - "required": [ - "filename", - "filesize", - "chunk_size" - ], - "properties": { - "filename": { - "type": "string", - "description": "文件名" - }, - "filesize": { - "type": "integer", - "description": "文件总大小" - }, - "chunk_size": { - "type": "integer", - "description": "分片大小" - }, - "file_hash": { - "type": "string", - "description": "文件哈希(可选,用于秒传)" - }, - "expire_value": { - "type": "integer", - "default": 1, - "description": "过期值" - }, - "expire_style": { - "type": "string", - "enum": ["minute", "hour", "day", "week", "month", "year", "forever"], - "default": "day", - "description": "过期样式" - }, - "require_auth": { - "type": "boolean", - "default": false, - "description": "是否需要认证" - } - } - }, - "models.InitUploadResponse": { - "type": "object", - "properties": { - "upload_id": { - "type": "string", - "description": "上传ID" - }, - "total_chunks": { - "type": "integer", - "description": "总分片数" - }, - "chunk_size": { - "type": "integer", - "description": "分片大小" - }, - "uploaded_chunks": { - "type": "array", - "items": { - "type": "integer" - }, - "description": "已上传的分片索引" - }, - "existing_file": { - "type": "boolean", - "description": "文件是否已存在(秒传)" - } - } - }, - "models.CompleteUploadRequest": { - "type": "object", - "required": [ - "filename" - ], - "properties": { - "filename": { - "type": "string", - "description": "最终文件名" - }, - "expire_style": { - "type": "string", - "enum": ["minute", "hour", "day", "week", "month", "year", "forever"], - "default": "day", - "description": "过期样式" - }, - "expire_value": { - "type": "integer", - "default": 1, - "description": "过期值" - }, - "require_auth": { - "type": "boolean", - "default": false, - "description": "是否需要认证" - } - } - }, - "models.UploadChunkResponse": { - "type": "object", - "properties": { - "chunk_index": { - "type": "integer", - "description": "分片索引" - }, - "chunk_hash": { - "type": "string", - "description": "分片哈希" - }, - "progress": { - "type": "number", - "description": "上传进度(0-100)" - } - } - }, - "models.ChunkStatusResponse": { - "type": "object", - "properties": { - "upload_id": { - "type": "string", - "description": "上传ID" - }, - "progress": { - "type": "number", - "description": "上传进度(0-100)" - }, - "total_chunks": { - "type": "integer", - "description": "总分片数" + "example": [ + "minute", + "hour", + "day", + "week", + "month", + "year", + "forever" + ] }, - "uploaded_chunks": { - "type": "integer", - "description": "已上传分片数" - }, - "status": { - "type": "string", - "enum": ["pending", "uploading", "completed", "failed"], - "description": "上传状态" - } - } - }, - "models.RegisterRequest": { - "type": "object", - "required": [ - "username", - "email", - "password" - ], - "properties": { - "username": { - "type": "string", - "minLength": 3, - "maxLength": 50, - "description": "用户名" - }, - "email": { - "type": "string", - "format": "email", - "description": "邮箱地址" - }, - "password": { - "type": "string", - "minLength": 6, - "description": "密码" - }, - "nickname": { - "type": "string", - "maxLength": 50, - "description": "昵称" - } - } - }, - "models.LoginRequest": { - "type": "object", - "required": [ - "username", - "password" - ], - "properties": { - "username": { - "type": "string", - "description": "用户名或邮箱" - }, - "password": { - "type": "string", - "description": "密码" - } - } - }, - "models.LoginResponse": { - "type": "object", - "properties": { - "token": { - "type": "string", - "description": "认证令牌" - }, - "user": { - "$ref": "#/definitions/models.User", - "description": "用户信息" - } - } - }, - "models.UserResponse": { - "type": "object", - "properties": { - "code": { - "type": "integer", - "example": 200 - }, - "message": { - "type": "string", - "example": "success" - }, - "data": { - "$ref": "#/definitions/models.User" - } - } - }, - "models.User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "用户ID" - }, - "username": { - "type": "string", - "description": "用户名" - }, - "email": { - "type": "string", - "description": "邮箱地址" - }, - "nickname": { - "type": "string", - "description": "昵称" - }, - "avatar": { - "type": "string", - "description": "头像URL" - }, - "role": { - "type": "string", - "enum": ["admin", "user"], - "description": "用户角色" - }, - "status": { + "name": { "type": "string", - "enum": ["active", "inactive", "banned"], - "description": "用户状态" + "example": "FileCodeBox" }, - "total_uploads": { - "type": "integer", - "description": "总上传次数" - }, - "total_downloads": { + "openUpload": { "type": "integer", - "description": "总下载次数" + "example": 1 }, - "total_storage": { + "uploadSize": { "type": "integer", - "description": "总存储大小" - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "创建时间" + "example": 100 } } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "X-API-Key", + "in": "header" }, - "models.AdminStatsResponse": { - "type": "object", - "properties": { - "total_files": { - "type": "integer", - "description": "总文件数" - }, - "total_users": { - "type": "integer", - "description": "总用户数" - }, - "total_storage": { - "type": "integer", - "description": "总存储大小" - }, - "total_downloads": { - "type": "integer", - "description": "总下载次数" - }, - "active_shares": { - "type": "integer", - "description": "活跃分享数" - }, - "expired_shares": { - "type": "integer", - "description": "过期分享数" - } - } + "BasicAuth": { + "type": "basic" } } }` @@ -1360,9 +621,9 @@ var SwaggerInfo = &swag.Spec{ Version: "1.0", Host: "localhost:12345", BasePath: "/", - Schemes: []string{"http", "https"}, + Schemes: []string{}, Title: "FileCodeBox API", - Description: "FileCodeBox 是一个用于文件分享和代码片段管理的 Web 应用程序,提供完整的 RESTful API 支持文件上传、分享、分片上传、用户管理等功能", + Description: "FileCodeBox 是一个用于文件分享和代码片段管理的 Web 应用程序", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index 5029dac..31f1abd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -18,9 +18,9 @@ "host": "localhost:12345", "basePath": "/", "paths": { - "/api/doc": { + "/api/config": { "get": { - "description": "获取完整的API文档信息,包括所有端点和错误码", + "description": "获取前端所需的系统配置信息", "consumes": [ "application/json" ], @@ -28,15 +28,14 @@ "application/json" ], "tags": [ - "API文档" + "系统" ], - "summary": "获取API文档", + "summary": "获取系统配置", "responses": { "200": { - "description": "API文档信息", + "description": "系统配置信息", "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/definitions/handlers.SystemConfig" } } } @@ -210,7 +209,7 @@ }, "/health": { "get": { - "description": "检查服务器健康状态", + "description": "检查服务器健康状态和构建信息", "consumes": [ "application/json" ], @@ -223,10 +222,9 @@ "summary": "健康检查", "responses": { "200": { - "description": "健康状态信息", + "description": "健康状态信息和构建信息", "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/definitions/handlers.HealthResponse" } } } @@ -537,6 +535,69 @@ } } }, + "definitions": { + "handlers.HealthResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + }, + "timestamp": { + "type": "string", + "example": "2025-09-11T10:00:00Z" + }, + "uptime": { + "type": "string", + "example": "2h30m15s" + }, + "version": { + "type": "string", + "example": "1.0.0" + } + } + }, + "handlers.SystemConfig": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "文件分享系统" + }, + "enableChunk": { + "type": "integer", + "example": 1 + }, + "expireStyle": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "minute", + "hour", + "day", + "week", + "month", + "year", + "forever" + ] + }, + "name": { + "type": "string", + "example": "FileCodeBox" + }, + "openUpload": { + "type": "integer", + "example": 1 + }, + "uploadSize": { + "type": "integer", + "example": 100 + } + } + } + }, "securityDefinitions": { "ApiKeyAuth": { "type": "apiKey", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index df68ad2..23f7f74 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,1471 +1,424 @@ -swagger: "2.0" +basePath: / +definitions: + handlers.HealthResponse: + properties: + status: + example: ok + type: string + timestamp: + example: "2025-09-11T10:00:00Z" + type: string + uptime: + example: 2h30m15s + type: string + version: + example: 1.0.0 + type: string + type: object + handlers.SystemConfig: + properties: + description: + example: 文件分享系统 + type: string + enableChunk: + example: 1 + type: integer + expireStyle: + example: + - minute + - hour + - day + - week + - month + - year + - forever + items: + type: string + type: array + name: + example: FileCodeBox + type: string + openUpload: + example: 1 + type: integer + uploadSize: + example: 100 + type: integer + type: object +host: localhost:12345 info: - title: "FileCodeBox API" - description: "FileCodeBox 是一个用于文件分享和代码片段管理的 Web 应用程序" - version: "1.0" - termsOfService: "http://swagger.io/terms/" contact: - name: "API Support" - url: "http://www.swagger.io/support" - email: "support@swagger.io" + email: support@swagger.io + name: API Support + url: http://www.swagger.io/support + description: FileCodeBox 是一个用于文件分享和代码片段管理的 Web 应用程序 license: - name: "MIT" - url: "https://github.com/zy84338719/filecodebox/blob/main/LICENSE" - -host: "localhost:12345" -basePath: "/" -schemes: - - "http" - - "https" - -securityDefinitions: - ApiKeyAuth: - type: "apiKey" - name: "X-API-Key" - in: "header" - BearerAuth: - type: "apiKey" - name: "Authorization" - in: "header" - BasicAuth: - type: "basic" - -tags: - - name: "系统" - description: "系统相关接口" - - name: "分享" - description: "文件和文本分享接口" - - name: "分片上传" - description: "大文件分片上传接口" - - name: "用户" - description: "用户认证和管理接口" - - name: "管理员" - description: "管理员接口" - - name: "存储" - description: "存储管理接口" - - name: "MCP" - description: "Model Context Protocol 接口" - + name: MIT + url: https://github.com/zy84338719/filecodebox/blob/main/LICENSE + termsOfService: http://swagger.io/terms/ + title: FileCodeBox API + version: "1.0" paths: - # 系统接口 - /health: - get: - tags: ["系统"] - summary: "健康检查" - description: "检查服务器健康状态" - produces: - - "application/json" - responses: - 200: - description: "健康状态信息" - schema: - type: "object" - properties: - status: - type: "string" - example: "ok" - timestamp: - type: "string" - example: "2025-09-11T10:00:00Z" - version: - type: "string" - example: "1.0.0" - uptime: - type: "string" - example: "2h30m15s" - /api/config: get: - tags: ["系统"] - summary: "获取系统配置" - description: "获取前端所需的系统配置信息" - produces: - - "application/json" - responses: - 200: - description: "系统配置信息" - schema: - $ref: "#/definitions/SystemConfig" - - # 分享接口 - /share/text/: - post: - tags: ["分享"] - summary: "分享文本内容" - description: "分享文本内容并生成分享代码" consumes: - - "multipart/form-data" - - "application/json" + - application/json + description: 获取前端所需的系统配置信息 produces: - - "application/json" - parameters: - - name: "text" - in: "formData" - type: "string" - required: true - description: "文本内容" - - name: "expire_value" - in: "formData" - type: "integer" - default: 1 - description: "过期值" - - name: "expire_style" - in: "formData" - type: "string" - enum: ["minute", "hour", "day", "week", "month", "year", "forever"] - default: "day" - description: "过期样式" - - name: "require_auth" - in: "formData" - type: "boolean" - default: false - description: "是否需要认证" - security: - - BearerAuth: [] + - application/json responses: - 200: - description: "分享成功" + "200": + description: 系统配置信息 schema: - $ref: "#/definitions/ShareResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 401: - description: "认证失败" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "服务器内部错误" - schema: - $ref: "#/definitions/ErrorResponse" - - /share/file/: + $ref: '#/definitions/handlers.SystemConfig' + summary: 获取系统配置 + tags: + - 系统 + /chunk/upload/chunk/{upload_id}/{chunk_index}: post: - tags: ["分享"] - summary: "分享文件" - description: "上传并分享文件,生成分享代码" consumes: - - "multipart/form-data" - produces: - - "application/json" + - multipart/form-data + description: 上传指定索引的文件分片 parameters: - - name: "file" - in: "formData" - type: "file" - required: true - description: "要分享的文件" - - name: "expire_value" - in: "formData" - type: "integer" - default: 1 - description: "过期值" - - name: "expire_style" - in: "formData" - type: "string" - enum: ["minute", "hour", "day", "week", "month", "year", "forever"] - default: "day" - description: "过期样式" - - name: "require_auth" - in: "formData" - type: "boolean" - default: false - description: "是否需要认证" - security: - - BearerAuth: [] - responses: - 200: - description: "分享成功" - schema: - $ref: "#/definitions/ShareResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 413: - description: "文件过大" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "服务器内部错误" - schema: - $ref: "#/definitions/ErrorResponse" - - /share/select/: - get: - tags: ["分享"] - summary: "获取分享信息" - description: "根据分享代码获取文件或文本的详细信息" + - description: 上传ID + in: path + name: upload_id + required: true + type: string + - description: 分片索引 + in: path + name: chunk_index + required: true + type: integer + - description: 分片文件 + in: formData + name: chunk + required: true + type: file produces: - - "application/json" - parameters: - - name: "code" - in: "query" - type: "string" - required: true - description: "分享代码" + - application/json responses: - 200: - description: "分享信息" - schema: - $ref: "#/definitions/ShareInfo" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "分享代码不存在" - schema: - $ref: "#/definitions/ErrorResponse" - 410: - description: "分享已过期" - schema: - $ref: "#/definitions/ErrorResponse" + "200": + description: 上传成功,返回分片哈希 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + summary: 上传文件分片 + tags: + - 分片上传 + /chunk/upload/complete/{upload_id}: post: - tags: ["分享"] - summary: "获取分享信息" - description: "根据分享代码获取文件或文本的详细信息(POST方式)" consumes: - - "application/json" - - "multipart/form-data" - produces: - - "application/json" + - application/json + description: 完成所有分片上传,合并文件并生成分享代码 parameters: - - name: "code" - in: "formData" - type: "string" - required: true - description: "分享代码" - responses: - 200: - description: "分享信息" - schema: - $ref: "#/definitions/ShareInfo" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "分享代码不存在" - schema: - $ref: "#/definitions/ErrorResponse" - - /share/download: - get: - tags: ["分享"] - summary: "下载分享文件" - description: "根据分享代码下载文件或获取文本内容" + - description: 上传ID + in: path + name: upload_id + required: true + type: string + - description: 完成上传参数 + in: body + name: request + required: true + schema: + type: object produces: - - "application/octet-stream" - - "application/json" - parameters: - - name: "code" - in: "query" - type: "string" - required: true - description: "分享代码" - security: - - BearerAuth: [] + - application/json responses: - 200: - description: "文件下载或文本内容" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 401: - description: "需要认证" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "分享代码不存在" - schema: - $ref: "#/definitions/ErrorResponse" - 410: - description: "分享已过期" - schema: - $ref: "#/definitions/ErrorResponse" - - # 分片上传接口 + "200": + description: 上传完成,返回分享代码 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + summary: 完成分片上传 + tags: + - 分片上传 /chunk/upload/init/: post: - tags: ["分片上传"] - summary: "初始化分片上传" - description: "初始化文件分片上传,返回上传ID和分片信息" consumes: - - "application/json" - produces: - - "application/json" + - application/json + description: 初始化文件分片上传,返回上传ID和分片信息 parameters: - - name: "request" - in: "body" - required: true - description: "上传初始化参数" - schema: - $ref: "#/definitions/ChunkInitRequest" - security: - - BearerAuth: [] - responses: - 200: - description: "初始化成功" - schema: - $ref: "#/definitions/ChunkInitResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 413: - description: "文件过大" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "服务器内部错误" - schema: - $ref: "#/definitions/ErrorResponse" - - /chunk/upload/chunk/{upload_id}/{chunk_index}: - post: - tags: ["分片上传"] - summary: "上传文件分片" - description: "上传指定索引的文件分片" - consumes: - - "multipart/form-data" + - description: 上传初始化参数 + in: body + name: request + required: true + schema: + type: object produces: - - "application/json" - parameters: - - name: "upload_id" - in: "path" - type: "string" - required: true - description: "上传ID" - - name: "chunk_index" - in: "path" - type: "integer" - required: true - description: "分片索引" - - name: "chunk" - in: "formData" - type: "file" - required: true - description: "分片文件" - security: - - BearerAuth: [] + - application/json responses: - 200: - description: "上传成功" - schema: - $ref: "#/definitions/ChunkUploadResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "上传ID不存在" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "服务器内部错误" - schema: - $ref: "#/definitions/ErrorResponse" - - /chunk/upload/complete/{upload_id}: - post: - tags: ["分片上传"] - summary: "完成分片上传" - description: "完成所有分片上传,合并文件并生成分享代码" + "200": + description: 初始化成功,返回上传ID和分片信息 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + summary: 初始化分片上传 + tags: + - 分片上传 + /health: + get: consumes: - - "application/json" + - application/json + description: 检查服务器健康状态和构建信息 produces: - - "application/json" - parameters: - - name: "upload_id" - in: "path" - type: "string" - required: true - description: "上传ID" - - name: "request" - in: "body" - required: true - description: "完成上传参数" - schema: - $ref: "#/definitions/ChunkCompleteRequest" - security: - - BearerAuth: [] + - application/json responses: - 200: - description: "上传完成" - schema: - $ref: "#/definitions/ShareResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "上传ID不存在" + "200": + description: 健康状态信息和构建信息 schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "服务器内部错误" - schema: - $ref: "#/definitions/ErrorResponse" - - /chunk/upload/status/{upload_id}: + $ref: '#/definitions/handlers.HealthResponse' + summary: 健康检查 + tags: + - 系统 + /share/download: get: - tags: ["分片上传"] - summary: "获取上传状态" - description: "获取分片上传的进度和状态" - produces: - - "application/json" - parameters: - - name: "upload_id" - in: "path" - type: "string" - required: true - description: "上传ID" - security: - - BearerAuth: [] - responses: - 200: - description: "上传状态" - schema: - $ref: "#/definitions/ChunkStatusResponse" - 404: - description: "上传ID不存在" - schema: - $ref: "#/definitions/ErrorResponse" - - /chunk/upload/cancel/{upload_id}: - delete: - tags: ["分片上传"] - summary: "取消分片上传" - description: "取消分片上传并清理相关文件" - produces: - - "application/json" - parameters: - - name: "upload_id" - in: "path" - type: "string" - required: true - description: "上传ID" - security: - - BearerAuth: [] - responses: - 200: - description: "取消成功" - schema: - $ref: "#/definitions/SuccessResponse" - 404: - description: "上传ID不存在" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "服务器内部错误" - schema: - $ref: "#/definitions/ErrorResponse" - - # 用户接口 - /user/register: - post: - tags: ["用户"] - summary: "用户注册" - description: "注册新用户账号" consumes: - - "application/json" - produces: - - "application/json" + - application/json + description: 根据分享代码下载文件或获取文本内容 parameters: - - name: "request" - in: "body" - required: true - description: "注册信息" - schema: - $ref: "#/definitions/RegisterRequest" - responses: - 200: - description: "注册成功" - schema: - $ref: "#/definitions/UserResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 409: - description: "用户已存在" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "服务器内部错误" - schema: - $ref: "#/definitions/ErrorResponse" - - /user/login: - post: - tags: ["用户"] - summary: "用户登录" - description: "用户登录认证" - consumes: - - "application/json" + - description: 分享代码 + in: query + name: code + required: true + type: string produces: - - "application/json" - parameters: - - name: "request" - in: "body" - required: true - description: "登录信息" - schema: - $ref: "#/definitions/LoginRequest" + - application/octet-stream + - application/json responses: - 200: - description: "登录成功" - schema: - $ref: "#/definitions/LoginResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 401: - description: "用户名或密码错误" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "服务器内部错误" - schema: - $ref: "#/definitions/ErrorResponse" - - /user/logout: + "200": + description: 文本内容 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "404": + description: 分享代码不存在 + schema: + additionalProperties: true + type: object + summary: 下载分享文件 + tags: + - 分享 + /share/file/: post: - tags: ["用户"] - summary: "用户退出" - description: "用户退出登录" - produces: - - "application/json" - security: - - BearerAuth: [] - responses: - 200: - description: "退出成功" - schema: - $ref: "#/definitions/SuccessResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - - /user/profile: - get: - tags: ["用户"] - summary: "获取用户信息" - description: "获取当前用户的详细信息" - produces: - - "application/json" - security: - - BearerAuth: [] - responses: - 200: - description: "用户信息" - schema: - $ref: "#/definitions/UserResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - put: - tags: ["用户"] - summary: "更新用户信息" - description: "更新当前用户的信息" consumes: - - "application/json" - produces: - - "application/json" - parameters: - - name: "request" - in: "body" - required: true - description: "用户信息" - schema: - $ref: "#/definitions/UpdateUserRequest" - security: - - BearerAuth: [] - responses: - 200: - description: "更新成功" - schema: - $ref: "#/definitions/UserResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - - /user/files: - get: - tags: ["用户"] - summary: "获取用户文件列表" - description: "获取当前用户上传的文件列表" - produces: - - "application/json" + - multipart/form-data + description: 上传并分享文件,生成分享代码 parameters: - - name: "page" - in: "query" - type: "integer" - default: 1 - description: "页码" - - name: "limit" - in: "query" - type: "integer" - default: 20 - description: "每页数量" - security: - - BearerAuth: [] - responses: - 200: - description: "文件列表" - schema: - $ref: "#/definitions/FileListResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - - # 管理员接口 - /admin/stats: - get: - tags: ["管理员"] - summary: "获取系统统计" - description: "获取系统统计信息" + - description: 要分享的文件 + in: formData + name: file + required: true + type: file + - default: 1 + description: 过期值 + in: formData + name: expire_value + type: integer + - default: day + description: 过期样式 + enum: + - minute + - hour + - day + - week + - month + - year + - forever + in: formData + name: expire_style + type: string + - default: false + description: 是否需要认证 + in: formData + name: require_auth + type: boolean produces: - - "application/json" - security: - - ApiKeyAuth: [] + - application/json responses: - 200: - description: "统计信息" - schema: - $ref: "#/definitions/AdminStatsResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - 403: - description: "权限不足" - schema: - $ref: "#/definitions/ErrorResponse" - - /admin/files: - get: - tags: ["管理员"] - summary: "获取所有文件列表" - description: "获取系统中所有文件的列表" - produces: - - "application/json" - parameters: - - name: "page" - in: "query" - type: "integer" - default: 1 - description: "页码" - - name: "limit" - in: "query" - type: "integer" - default: 20 - description: "每页数量" - - name: "search" - in: "query" - type: "string" - description: "搜索关键词" - - name: "sort" - in: "query" - type: "string" - enum: ["created_at", "size", "used_count"] - default: "created_at" - description: "排序字段" - - name: "order" - in: "query" - type: "string" - enum: ["asc", "desc"] - default: "desc" - description: "排序方式" - security: - - ApiKeyAuth: [] - responses: - 200: - description: "文件列表" - schema: - $ref: "#/definitions/AdminFileListResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - 403: - description: "权限不足" - schema: - $ref: "#/definitions/ErrorResponse" - - /admin/files/{id}: - delete: - tags: ["管理员"] - summary: "删除文件" - description: "删除指定的文件" - produces: - - "application/json" - parameters: - - name: "id" - in: "path" - type: "integer" - required: true - description: "文件ID" - security: - - ApiKeyAuth: [] - responses: - 200: - description: "删除成功" - schema: - $ref: "#/definitions/SuccessResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - 403: - description: "权限不足" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "文件不存在" - schema: - $ref: "#/definitions/ErrorResponse" - - /admin/config: + "200": + description: 分享成功,返回分享代码和文件信息 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + summary: 分享文件 + tags: + - 分享 + /share/select/: get: - tags: ["管理员"] - summary: "获取系统配置" - description: "获取完整的系统配置" - produces: - - "application/json" - security: - - ApiKeyAuth: [] - responses: - 200: - description: "系统配置" - schema: - $ref: "#/definitions/AdminConfigResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - 403: - description: "权限不足" - schema: - $ref: "#/definitions/ErrorResponse" - put: - tags: ["管理员"] - summary: "更新系统配置" - description: "更新系统配置" consumes: - - "application/json" - produces: - - "application/json" + - application/json + description: 根据分享代码获取文件或文本的详细信息 parameters: - - name: "config" - in: "body" - required: true - description: "配置信息" - schema: - $ref: "#/definitions/AdminConfigRequest" - security: - - ApiKeyAuth: [] + - description: 分享代码(GET方式) + in: query + name: code + type: string + - description: 分享代码(POST方式) + in: formData + name: code + type: string + produces: + - application/json responses: - 200: - description: "更新成功" - schema: - $ref: "#/definitions/SuccessResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - 403: - description: "权限不足" - schema: - $ref: "#/definitions/ErrorResponse" - - # 存储管理接口 - /admin/storage/test: + "200": + description: 文件信息 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "404": + description: 分享代码不存在 + schema: + additionalProperties: true + type: object + summary: 获取分享文件信息 + tags: + - 分享 post: - tags: ["存储"] - summary: "测试存储连接" - description: "测试指定存储配置的连接" consumes: - - "application/json" - produces: - - "application/json" + - application/json + description: 根据分享代码获取文件或文本的详细信息 parameters: - - name: "config" - in: "body" - required: true - description: "存储配置" - schema: - $ref: "#/definitions/StorageTestRequest" - security: - - ApiKeyAuth: [] + - description: 分享代码(GET方式) + in: query + name: code + type: string + - description: 分享代码(POST方式) + in: formData + name: code + type: string + produces: + - application/json responses: - 200: - description: "测试成功" - schema: - $ref: "#/definitions/StorageTestResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - 403: - description: "权限不足" - schema: - $ref: "#/definitions/ErrorResponse" - - /admin/storage/switch: + "200": + description: 文件信息 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "404": + description: 分享代码不存在 + schema: + additionalProperties: true + type: object + summary: 获取分享文件信息 + tags: + - 分享 + /share/text/: post: - tags: ["存储"] - summary: "切换存储方式" - description: "切换到指定的存储方式" consumes: - - "application/json" - produces: - - "application/json" + - multipart/form-data + description: 分享文本内容并生成分享代码 parameters: - - name: "request" - in: "body" - required: true - description: "存储切换请求" - schema: - $ref: "#/definitions/StorageSwitchRequest" - security: - - ApiKeyAuth: [] + - description: 文本内容 + in: formData + name: text + required: true + type: string + - default: 1 + description: 过期值 + in: formData + name: expire_value + type: integer + - default: day + description: 过期样式 + enum: + - minute + - hour + - day + - week + - month + - year + - forever + in: formData + name: expire_style + type: string + - default: false + description: 是否需要认证 + in: formData + name: require_auth + type: boolean + produces: + - application/json responses: - 200: - description: "切换成功" - schema: - $ref: "#/definitions/SuccessResponse" - 400: - description: "请求参数错误" - schema: - $ref: "#/definitions/ErrorResponse" - 401: - description: "未认证" - schema: - $ref: "#/definitions/ErrorResponse" - 403: - description: "权限不足" - schema: - $ref: "#/definitions/ErrorResponse" - -definitions: - # 通用响应 - SuccessResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - - ErrorResponse: - type: "object" - properties: - code: - type: "integer" - example: 400 - message: - type: "string" - example: "error message" - error: - type: "string" - example: "detailed error" - - # 系统配置 - SystemConfig: - type: "object" - properties: - name: - type: "string" - description: "站点名称" - description: - type: "string" - description: "站点描述" - uploadSize: - type: "integer" - description: "最大上传大小(MB)" - enableChunk: - type: "integer" - description: "是否启用分片上传" - openUpload: - type: "boolean" - description: "是否开放上传" - expireStyle: - type: "array" - items: - type: "string" - description: "过期时间选项" - - # 分享相关 - ShareResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - properties: - code: - type: "string" - description: "分享代码" - download_url: - type: "string" - description: "下载URL" - share_url: - type: "string" - description: "分享URL" - expired_at: - type: "string" - format: "date-time" - description: "过期时间" - - ShareInfo: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - properties: - code: - type: "string" - description: "分享代码" - type: - type: "string" - enum: ["text", "file"] - description: "分享类型" - filename: - type: "string" - description: "文件名(文件类型时)" - size: - type: "integer" - description: "文件大小(文件类型时)" - text: - type: "string" - description: "文本内容(文本类型时)" - expired_at: - type: "string" - format: "date-time" - description: "过期时间" - used_count: - type: "integer" - description: "已使用次数" - expired_count: - type: "integer" - description: "最大使用次数" - require_auth: - type: "boolean" - description: "是否需要认证" - - # 分片上传相关 - ChunkInitRequest: - type: "object" - required: ["filename", "filesize", "chunk_size"] - properties: - filename: - type: "string" - description: "文件名" - filesize: - type: "integer" - description: "文件总大小" - chunk_size: - type: "integer" - description: "分片大小" - file_hash: - type: "string" - description: "文件哈希(可选,用于秒传)" - expire_value: - type: "integer" - default: 1 - description: "过期值" - expire_style: - type: "string" - enum: ["minute", "hour", "day", "week", "month", "year", "forever"] - default: "day" - description: "过期样式" - require_auth: - type: "boolean" - default: false - description: "是否需要认证" - - ChunkInitResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - properties: - upload_id: - type: "string" - description: "上传ID" - total_chunks: - type: "integer" - description: "总分片数" - chunk_size: - type: "integer" - description: "分片大小" - uploaded_chunks: - type: "array" - items: - type: "integer" - description: "已上传的分片索引" - existing_file: - type: "boolean" - description: "文件是否已存在(秒传)" - - ChunkUploadResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - properties: - chunk_index: - type: "integer" - description: "分片索引" - chunk_hash: - type: "string" - description: "分片哈希" - progress: - type: "number" - description: "上传进度(0-100)" - - ChunkCompleteRequest: - type: "object" - required: ["filename"] - properties: - filename: - type: "string" - description: "最终文件名" - expire_value: - type: "integer" - default: 1 - description: "过期值" - expire_style: - type: "string" - enum: ["minute", "hour", "day", "week", "month", "year", "forever"] - default: "day" - description: "过期样式" - require_auth: - type: "boolean" - default: false - description: "是否需要认证" - - ChunkStatusResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - properties: - upload_id: - type: "string" - description: "上传ID" - progress: - type: "number" - description: "上传进度(0-100)" - total_chunks: - type: "integer" - description: "总分片数" - uploaded_chunks: - type: "integer" - description: "已上传分片数" - status: - type: "string" - enum: ["pending", "uploading", "completed", "failed"] - description: "上传状态" - - # 用户相关 - RegisterRequest: - type: "object" - required: ["username", "email", "password"] - properties: - username: - type: "string" - minLength: 3 - maxLength: 50 - description: "用户名" - email: - type: "string" - format: "email" - description: "邮箱地址" - password: - type: "string" - minLength: 6 - description: "密码" - nickname: - type: "string" - maxLength: 50 - description: "昵称" - - LoginRequest: - type: "object" - required: ["username", "password"] - properties: - username: - type: "string" - description: "用户名或邮箱" - password: - type: "string" - description: "密码" - - LoginResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - properties: - token: - type: "string" - description: "认证令牌" - user: - $ref: "#/definitions/User" - - UserResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - $ref: "#/definitions/User" - - User: - type: "object" - properties: - id: - type: "integer" - description: "用户ID" - username: - type: "string" - description: "用户名" - email: - type: "string" - description: "邮箱地址" - nickname: - type: "string" - description: "昵称" - avatar: - type: "string" - description: "头像URL" - role: - type: "string" - enum: ["admin", "user"] - description: "用户角色" - status: - type: "string" - enum: ["active", "inactive", "banned"] - description: "用户状态" - total_uploads: - type: "integer" - description: "总上传次数" - total_downloads: - type: "integer" - description: "总下载次数" - total_storage: - type: "integer" - description: "总存储大小" - created_at: - type: "string" - format: "date-time" - description: "创建时间" - - UpdateUserRequest: - type: "object" - properties: - nickname: - type: "string" - maxLength: 50 - description: "昵称" - avatar: - type: "string" - description: "头像URL" - email: - type: "string" - format: "email" - description: "邮箱地址" - - FileListResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - properties: - files: - type: "array" - items: - $ref: "#/definitions/FileItem" - pagination: - $ref: "#/definitions/Pagination" - - FileItem: - type: "object" - properties: - id: - type: "integer" - description: "文件ID" - code: - type: "string" - description: "分享代码" - filename: - type: "string" - description: "文件名" - size: - type: "integer" - description: "文件大小" - type: - type: "string" - enum: ["text", "file"] - description: "类型" - used_count: - type: "integer" - description: "使用次数" - expired_count: - type: "integer" - description: "过期次数" - expired_at: - type: "string" - format: "date-time" - description: "过期时间" - created_at: - type: "string" - format: "date-time" - description: "创建时间" - - # 管理员相关 - AdminStatsResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - properties: - total_files: - type: "integer" - description: "总文件数" - total_users: - type: "integer" - description: "总用户数" - total_storage: - type: "integer" - description: "总存储大小" - total_downloads: - type: "integer" - description: "总下载次数" - active_shares: - type: "integer" - description: "活跃分享数" - expired_shares: - type: "integer" - description: "过期分享数" - - AdminFileListResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - properties: - files: - type: "array" - items: - $ref: "#/definitions/AdminFileItem" - pagination: - $ref: "#/definitions/Pagination" - - AdminFileItem: - type: "object" - properties: - id: - type: "integer" - description: "文件ID" - code: - type: "string" - description: "分享代码" - filename: - type: "string" - description: "文件名" - size: - type: "integer" - description: "文件大小" - type: - type: "string" - enum: ["text", "file"] - description: "类型" - used_count: - type: "integer" - description: "使用次数" - expired_count: - type: "integer" - description: "过期次数" - expired_at: - type: "string" - format: "date-time" - description: "过期时间" - user_id: - type: "integer" - description: "上传用户ID" - username: - type: "string" - description: "上传用户名" - owner_ip: - type: "string" - description: "上传者IP" - created_at: - type: "string" - format: "date-time" - description: "创建时间" - - AdminConfigResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - description: "完整的系统配置" - - AdminConfigRequest: - type: "object" - description: "系统配置更新请求" - - # 存储相关 - StorageTestRequest: - type: "object" - required: ["storage_type"] - properties: - storage_type: - type: "string" - enum: ["local", "s3", "webdav", "onedrive"] - description: "存储类型" - config: - type: "object" - description: "存储配置参数" - - StorageTestResponse: - type: "object" - properties: - code: - type: "integer" - example: 200 - message: - type: "string" - example: "success" - data: - type: "object" - properties: - status: - type: "string" - enum: ["success", "failed"] - description: "测试状态" - message: - type: "string" - description: "测试结果信息" - latency: - type: "integer" - description: "响应延迟(ms)" - - StorageSwitchRequest: - type: "object" - required: ["storage_type"] - properties: - storage_type: - type: "string" - enum: ["local", "s3", "webdav", "onedrive"] - description: "目标存储类型" - migrate_data: - type: "boolean" - default: false - description: "是否迁移现有数据" - - # 通用分页 - Pagination: - type: "object" - properties: - page: - type: "integer" - description: "当前页码" - limit: - type: "integer" - description: "每页数量" - total: - type: "integer" - description: "总数量" - pages: - type: "integer" - description: "总页数" + "200": + description: 分享成功,返回分享代码 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + summary: 分享文本内容 + tags: + - 分享 +securityDefinitions: + ApiKeyAuth: + in: header + name: X-API-Key + type: apiKey + BasicAuth: + type: basic +swagger: "2.0" diff --git a/go.mod b/go.mod index 1e40232..12b3b6e 100644 --- a/go.mod +++ b/go.mod @@ -9,21 +9,21 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/credentials v1.17.27 github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 - github.com/gin-contrib/cors v1.7.6 - github.com/gin-gonic/gin v1.10.1 + github.com/gin-contrib/cors v1.4.0 + github.com/gin-gonic/gin v1.8.2 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.9.3 github.com/studio-b12/gowebdav v0.10.0 - github.com/swaggo/files v1.0.1 - github.com/swaggo/gin-swagger v1.6.0 - github.com/swaggo/swag v1.16.6 - golang.org/x/crypto v0.41.0 + github.com/swaggo/files v1.0.0 + github.com/swaggo/gin-swagger v1.5.3 + github.com/swaggo/swag v1.16.4 + golang.org/x/crypto v0.21.0 golang.org/x/time v0.12.0 - gorm.io/driver/mysql v1.6.0 - gorm.io/driver/postgres v1.6.0 - gorm.io/driver/sqlite v1.6.0 - gorm.io/gorm v1.30.2 + gorm.io/driver/mysql v1.5.7 + gorm.io/driver/postgres v1.4.7 + gorm.io/driver/sqlite v1.5.7 + gorm.io/gorm v1.25.10 ) require ( @@ -65,13 +65,14 @@ require ( github.com/go-openapi/swag/yamlutils v0.24.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-playground/validator/v10 v10.11.2 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/pgx/v5 v5.2.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -85,15 +86,19 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/spf13/cobra v1.9.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.7.0 // indirect google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index daa972c..2876e35 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= @@ -44,25 +48,41 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= +github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY= +github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= @@ -87,51 +107,81 @@ github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zib github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= +github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8= +github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk= github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= @@ -141,20 +191,41 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/cobra v1.9.0 h1:Py5fIuq/lJsRYxcxfOtsJqpmwJWCMOUy2tMJYV8TNHE= +github.com/spf13/cobra v1.9.0/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -163,76 +234,168 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/studio-b12/gowebdav v0.10.0 h1:Yewz8FFiadcGEu4hxS/AAJQlHelndqln1bns3hcJIYc= github.com/studio-b12/gowebdav v0.10.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= +github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4= +github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= +github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= +github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo= +github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= +golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.4.7 h1:J06jXZCNq7Pdf7LIPn8tZn9LsWjd81BRSKveKNr0ZfA= +gorm.io/driver/postgres v1.4.7/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs= gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/internal/cli/admin.go b/internal/cli/admin.go new file mode 100644 index 0000000..2b08e46 --- /dev/null +++ b/internal/cli/admin.go @@ -0,0 +1,169 @@ +package cli + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + "syscall" + + "github.com/spf13/cobra" + _ "github.com/zy84338719/filecodebox/internal/config" + _ "github.com/zy84338719/filecodebox/internal/database" + _ "github.com/zy84338719/filecodebox/internal/repository" + _ "github.com/zy84338719/filecodebox/internal/services" + "golang.org/x/term" +) + +var adminCreateCmd = &cobra.Command{ + Use: "create [username] [password] [email]", + Short: "Create admin user", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + repoMgr, adminSvc, err := initServices() + if err != nil { + return err + } + // repoMgr has no Close method; repository manager lifecycle handled by program + + username := args[0] + password := args[1] + email := args[2] + // 如果当前数据库没有用户,提示友好引导;用户可以在网页上完成初始化 + if repoMgr != nil { + if cnt, err := repoMgr.User.Count(); err == nil && cnt == 0 { + // 检测到没有用户 + force, _ := cmd.Flags().GetBool("force") + if !force { + fmt.Println("未检测到任何用户。注意:首次通过网页初始化时,第一位创建的用户将自动成为管理员。") + fmt.Println("你可以通过浏览器访问管理后台完成自助初始化 (例如: http://localhost:12345/setup 或 http://localhost:12345/admin/setup),") + fmt.Println("或者如果你确实想要通过 CLI 创建第一个用户,请重新运行本命令并加上 --force 标志以强制创建。") + return nil + } + } + } + + _, err = adminSvc.CreateUser(username, email, password, "admin", "admin", "active") + if err != nil { + return err + } + fmt.Println("admin user created") + return nil + }, +} + +func init() { + // allow forcing first-user creation when DB empty + adminCreateCmd.Flags().BoolP("force", "f", false, "Force create first user even if DB has no users (first user will be admin)") +} + +var adminResetCmd = &cobra.Command{ + Use: "reset [userID|username] [newPassword]", + Short: "Reset user password by ID or username", + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + repoMgr, adminSvc, err := initServices() + if err != nil { + return err + } + // repoMgr has no Close method; repository manager lifecycle handled by program + + // 如果没有用户,友好提示并引导到网页初始化 + if repoMgr != nil { + if cnt, err := repoMgr.User.Count(); err == nil && cnt == 0 { + fmt.Println("未检测到任何用户。首次用户可通过网页自助初始化,第一位用户将成为管理员。请先在网页上完成初始化或使用 CLI 创建第一个用户(见 create --force)") + return nil + } + } + + identifier := args[0] + var userID uint + if id64, err := strconv.ParseUint(identifier, 10, 64); err == nil { + userID = uint(id64) + } else { + // treat as username + u, err := repoMgr.User.GetByUsername(identifier) + if err != nil { + return fmt.Errorf("找不到用户: %w", err) + } + userID = u.ID + } + + var newPass string + if len(args) == 2 { + newPass = args[1] + } else { + // prompt for password (hidden) + fmt.Print("New password: ") + pwBytes, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() + if err != nil { + return err + } + newPass = strings.TrimSpace(string(pwBytes)) + if newPass == "" { + // if empty, optionally prompt once more via stdin + reader := bufio.NewReader(os.Stdin) + fmt.Print("Password cannot be empty, please enter again: ") + line, _ := reader.ReadString('\n') + newPass = strings.TrimSpace(line) + if newPass == "" { + return fmt.Errorf("password cannot be empty") + } + } + } + + return adminSvc.UpdateUser(userID, "", newPass, "", "", "") + }, +} + +var adminListCmd = &cobra.Command{ + Use: "list [page] [pageSize]", + Short: "List users", + Args: cobra.MaximumNArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + repoMgr, adminSvc, err := initServices() + if err != nil { + return err + } + // repoMgr has no Close method; repository manager lifecycle handled by program + + page := 1 + pageSize := 20 + if len(args) >= 1 { + if p, err := strconv.Atoi(args[0]); err == nil { + page = p + } + } + if len(args) == 2 { + if ps, err := strconv.Atoi(args[1]); err == nil { + pageSize = ps + } + } + + // 如果没有用户,友好提示并引导到网页初始化 + if repoMgr != nil { + if cnt, err := repoMgr.User.Count(); err == nil && cnt == 0 { + fmt.Println("未检测到任何用户。首次用户可通过网页自助初始化,第一位用户将成为管理员。请先在网页上完成初始化(例如访问 /setup 或 /admin/setup)或使用 create --force 在 CLI 创建第一个用户。") + return nil + } + } + + users, total, err := adminSvc.GetUsers(page, pageSize, "") + if err != nil { + return err + } + fmt.Printf("total: %d\n", total) + for _, u := range users { + fmt.Printf("%d: %s (%s)\n", u.ID, u.Username, u.Email) + } + return nil + }, +} + +func init() { + adminCmd.AddCommand(adminCreateCmd) + adminCmd.AddCommand(adminResetCmd) + adminCmd.AddCommand(adminListCmd) +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..8e68fc7 --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,87 @@ +package cli + +import ( + "fmt" + "os" + "runtime" + + "github.com/spf13/cobra" + "github.com/zy84338719/filecodebox/internal/config" + "github.com/zy84338719/filecodebox/internal/database" + "github.com/zy84338719/filecodebox/internal/models" + "github.com/zy84338719/filecodebox/internal/repository" + "github.com/zy84338719/filecodebox/internal/services" +) + +var rootCmd = &cobra.Command{ + Use: "filecodebox", + Short: "FileCodeBox CLI tools", + Long: "Command-line tools for FileCodeBox (admin management, maintenance, etc)", +} + +var cfgPath string +var dataPath string + +func init() { + // global flags + rootCmd.PersistentFlags().StringVar(&cfgPath, "config", "", "Path to config.yaml to load") + rootCmd.PersistentFlags().StringVar(&dataPath, "data-path", "", "Override data path (overrides DATA_PATH env)") +} + +// Execute executes the root cobra command +func Execute() { + // add subcommands + rootCmd.AddCommand(versionCmd) + rootCmd.AddCommand(adminCmd) + + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +// helper to initialize services that need DB +func initServices() (*repository.RepositoryManager, *services.AdminService, error) { + manager := config.InitManager() + + // If a config file is provided, prefer it (and let ConfigManager track managed keys) + if cfgPath != "" { + if err := manager.LoadFromYAML(cfgPath); err != nil { + // try to continue, but log to stderr + fmt.Fprintln(os.Stderr, "warning: failed to load config file:", err) + } + } + + // Override data path if flag provided + if dataPath != "" { + manager.Base.DataPath = dataPath + } + + // init DB + db, err := database.InitWithManager(manager) + if err != nil { + return nil, nil, fmt.Errorf("failed to init database: %w", err) + } + + repoMgr := repository.NewRepositoryManager(db) + storageService := services.NewAdminService(repoMgr, manager, nil) // placeholder: admin.NewService expects storageService; we pass nil where not needed + // Actually services.NewAdminService signature in services package returns admin.Service alias; reuse admin.Service + adminService := storageService + + return repoMgr, adminService, nil +} + +// placeholders to be implemented in separate files +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print version information", + Run: func(cmd *cobra.Command, args []string) { + buildInfo := models.GetBuildInfo() + fmt.Printf("FileCodeBox %s\nCommit: %s\nBuilt: %s\nGo Version: %s\n", buildInfo.Version, buildInfo.GitCommit, buildInfo.BuildTime, runtime.Version()) + }, +} + +var adminCmd = &cobra.Command{ + Use: "admin", + Short: "Admin user management commands", +} diff --git a/internal/config/manager.go b/internal/config/manager.go index 8fff81a..86373c1 100644 --- a/internal/config/manager.go +++ b/internal/config/manager.go @@ -1,4 +1,3 @@ -// Package config 配置管理器 package config import ( @@ -6,49 +5,48 @@ import ( "fmt" "os" "strconv" - "strings" - "github.com/zy84338719/filecodebox/internal/models" + "gopkg.in/yaml.v3" + "gorm.io/gorm" ) -// ConfigManager 配置管理器 +// ConfigManager implements Option A semantics (Env > YAML > DB > Defaults). +// Keys present in config.yaml are recorded in yamlManagedKeys and are authoritative. type ConfigManager struct { - Base *BaseConfig `json:"base"` - Database *DatabaseConfig `json:"database"` - Transfer *TransferConfig `json:"transfer"` - Storage *StorageConfig `json:"storage"` - User *UserSystemConfig `json:"user"` - MCP *MCPConfig `json:"mcp"` - - // 其他配置字段 - NotifyTitle string `json:"notify_title"` - NotifyContent string `json:"notify_content"` - PageExplain string `json:"page_explain"` - ExpireStyle []string `json:"expire_style"` - - // 限流配置 - UploadMinute int `json:"upload_minute"` - UploadCount int `json:"upload_count"` - ErrorMinute int `json:"error_minute"` - ErrorCount int `json:"error_count"` - - // 主题配置 - ThemesSelect string `json:"themes_select"` - ThemesChoices []Theme `json:"themes_choices"` - Opacity float64 `json:"opacity"` - Background string `json:"background"` - - // 管理配置 - AdminToken string `json:"admin_token"` - ShowAdminAddr int `json:"show_admin_address"` - RobotsText string `json:"robots_text"` - - // 数据库连接(内部使用) - db *gorm.DB `json:"-"` + Base *BaseConfig + Database *DatabaseConfig + Transfer *TransferConfig + Storage *StorageConfig + User *UserSystemConfig + MCP *MCPConfig + + NotifyTitle string + NotifyContent string + AdminToken string + + // UI / Theme / Page fields (kept at top-level for backward compatibility with callers) + ThemesSelect string `yaml:"themes_select" json:"themes_select"` + RobotsText string `yaml:"robots_text" json:"robots_text"` + PageExplain string `yaml:"page_explain" json:"page_explain"` + ShowAdminAddr int `yaml:"show_admin_addr" json:"show_admin_addr"` + Opacity float64 `yaml:"opacity" json:"opacity"` + Background string `yaml:"background" json:"background"` + + // rate limit / business fields (kept at top-level for backwards compatibility) + UploadMinute int `yaml:"upload_minute" json:"upload_minute"` + UploadCount int `yaml:"upload_count" json:"upload_count"` + ErrorMinute int `yaml:"error_minute" json:"error_minute"` + ErrorCount int `yaml:"error_count" json:"error_count"` + ExpireStyle []string `yaml:"expire_style" json:"expire_style"` + + db *gorm.DB + + // yamlManagedKeys stores flat keys (module_field or single keys) that are managed by YAML + // and must not be overwritten by DB nor written back to DB. + yamlManagedKeys map[string]bool } -// NewConfigManager 创建配置管理器 func NewConfigManager() *ConfigManager { return &ConfigManager{ Base: NewBaseConfig(), @@ -57,507 +55,230 @@ func NewConfigManager() *ConfigManager { Storage: NewStorageConfig(), User: NewUserSystemConfig(), MCP: NewMCPConfig(), - - NotifyTitle: "系统通知", - NotifyContent: `欢迎使用 FileCodeBox,本程序开源于 Github ,欢迎Star和Fork。`, - PageExplain: "请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。", - ExpireStyle: []string{"day", "hour", "minute", "forever", "count"}, - - UploadMinute: 1, - UploadCount: 10, - ErrorMinute: 1, - ErrorCount: 1, - - ThemesSelect: "themes/2025", - ThemesChoices: []Theme{ - {Name: "2025", Key: "themes/2025", Author: "Yi", Version: "1.0"}, - }, - Opacity: 0.9, - Background: "", - - AdminToken: "FileCodeBox2025", - ShowAdminAddr: 0, - RobotsText: "User-agent: *\nDisallow: /", } } -// InitManager 初始化配置管理器 +// InitManager loads config.yaml early if present and applies environment overrides. func InitManager() *ConfigManager { cm := NewConfigManager() - - // 从环境变量读取配置 - cm.applyEnvironmentOverrides() - - // 创建数据目录(仅SQLite需要) - if cm.Database.IsSQLite() { - if err := os.MkdirAll(cm.Base.DataPath, 0750); err != nil { - panic("创建数据目录失败: " + err.Error()) - } + if p := os.Getenv("CONFIG_PATH"); p != "" { + _ = cm.LoadFromYAML(p) + } else if _, err := os.Stat("./config.yaml"); err == nil { + _ = cm.LoadFromYAML("./config.yaml") } - + cm.applyEnvironmentOverrides() return cm } -// InitWithDB 使用数据库初始化配置管理器 -func (cm *ConfigManager) InitWithDB(db *gorm.DB) error { - cm.db = db +func (cm *ConfigManager) SetDB(db *gorm.DB) { cm.db = db } +func (cm *ConfigManager) GetDB() *gorm.DB { return cm.db } - // 保存环境变量和默认配置中的端口和管理员密码 - envPort := cm.Base.Port - envAdminToken := cm.AdminToken - - // 尝试从数据库加载配置 - if err := cm.LoadFromDatabase(); err != nil { - // 数据库中没有配置,初始化基础数据 - if err := cm.InitDefaultDataInDB(); err != nil { - return fmt.Errorf("初始化数据库默认配置失败: %w", err) - } - // 重新加载配置 - if err := cm.LoadFromDatabase(); err != nil { - return fmt.Errorf("加载初始化后的配置失败: %w", err) - } +// LoadFromYAML loads hierarchical YAML into module structs and records their flat keys. +func (cm *ConfigManager) LoadFromYAML(path string) error { + b, err := os.ReadFile(path) + if err != nil { + return err } - - // 启动时优先级:端口和管理员密码优先使用环境变量或默认配置 - cm.Base.Port = envPort - cm.AdminToken = envAdminToken - - // 再次应用环境变量覆盖以确保优先级正确 - cm.applyEnvironmentOverrides() - - return nil -} - -// Validate 验证所有配置 -func (cm *ConfigManager) Validate() error { - if err := cm.Base.Validate(); err != nil { - return fmt.Errorf("基础配置验证失败: %w", err) + var fileCfg ConfigManager + if err := yaml.Unmarshal(b, &fileCfg); err != nil { + return err } - - if err := cm.Database.Validate(); err != nil { - return fmt.Errorf("数据库配置验证失败: %w", err) + if cm.yamlManagedKeys == nil { + cm.yamlManagedKeys = make(map[string]bool) } - - if err := cm.Transfer.Validate(); err != nil { - return fmt.Errorf("传输配置验证失败: %w", err) + if fileCfg.Base != nil { + cm.Base = fileCfg.Base + for k := range cm.Base.ToMap() { + cm.yamlManagedKeys[k] = true + } } - - if err := cm.Storage.Validate(); err != nil { - return fmt.Errorf("存储配置验证失败: %w", err) + if fileCfg.Database != nil { + cm.Database = fileCfg.Database + for k := range cm.Database.ToMap() { + cm.yamlManagedKeys[k] = true + } } - - if err := cm.User.Validate(); err != nil { - return fmt.Errorf("用户系统配置验证失败: %w", err) + if fileCfg.Transfer != nil { + cm.Transfer = fileCfg.Transfer + for k := range cm.Transfer.ToMap() { + cm.yamlManagedKeys[k] = true + } } - - if err := cm.MCP.Validate(); err != nil { - return fmt.Errorf("MCP配置验证失败: %w", err) + if fileCfg.Storage != nil { + cm.Storage = fileCfg.Storage + for k := range cm.Storage.ToMap() { + cm.yamlManagedKeys[k] = true + } } - - return nil -} - -// buildConfigMap 构建配置映射表 -func (cm *ConfigManager) buildConfigMap() map[string]string { - result := make(map[string]string) - - // 合并各个配置模块的映射 - for k, v := range cm.Base.ToMap() { - result[k] = v + if fileCfg.User != nil { + cm.User = fileCfg.User + for k := range cm.User.ToMap() { + cm.yamlManagedKeys[k] = true + } } - - for k, v := range cm.Database.ToMap() { - result[k] = v + if fileCfg.MCP != nil { + cm.MCP = fileCfg.MCP + for k := range cm.MCP.ToMap() { + cm.yamlManagedKeys[k] = true + } } - - for k, v := range cm.Transfer.ToMap() { - result[k] = v + if fileCfg.NotifyTitle != "" { + cm.NotifyTitle = fileCfg.NotifyTitle + cm.yamlManagedKeys["notify_title"] = true } - - for k, v := range cm.Storage.ToMap() { - result[k] = v + if fileCfg.NotifyContent != "" { + cm.NotifyContent = fileCfg.NotifyContent + cm.yamlManagedKeys["notify_content"] = true } - - for k, v := range cm.User.ToMap() { - result[k] = v + if fileCfg.AdminToken != "" { + cm.AdminToken = fileCfg.AdminToken + cm.yamlManagedKeys["admin_token"] = true } - - for k, v := range cm.MCP.ToMap() { - result[k] = v + if fileCfg.ThemesSelect != "" { + cm.ThemesSelect = fileCfg.ThemesSelect + cm.yamlManagedKeys["themes_select"] = true } - - // 添加其他配置 - result["notify_title"] = cm.NotifyTitle - result["notify_content"] = cm.NotifyContent - result["page_explain"] = cm.PageExplain - result["upload_minute"] = fmt.Sprintf("%d", cm.UploadMinute) - result["upload_count"] = fmt.Sprintf("%d", cm.UploadCount) - result["error_minute"] = fmt.Sprintf("%d", cm.ErrorMinute) - result["error_count"] = fmt.Sprintf("%d", cm.ErrorCount) - result["themes_select"] = cm.ThemesSelect - result["opacity"] = fmt.Sprintf("%f", cm.Opacity) - result["background"] = cm.Background - result["admin_token"] = cm.AdminToken - result["show_admin_address"] = fmt.Sprintf("%d", cm.ShowAdminAddr) - result["robots_text"] = cm.RobotsText - - return result -} - -// InitDefaultDataInDB 在数据库中初始化默认配置数据 -func (cm *ConfigManager) InitDefaultDataInDB() error { - if cm.db == nil { - return errors.New("数据库连接未设置") + if fileCfg.RobotsText != "" { + cm.RobotsText = fileCfg.RobotsText + cm.yamlManagedKeys["robots_text"] = true } - - // 检查是否已经有配置数据 - var count int64 - if err := cm.db.Model(&models.KeyValue{}).Count(&count).Error; err != nil { - return fmt.Errorf("检查配置数据失败: %w", err) + if fileCfg.PageExplain != "" { + cm.PageExplain = fileCfg.PageExplain + cm.yamlManagedKeys["page_explain"] = true } - - // 如果已有数据,不进行初始化 - if count > 0 { - return nil + if fileCfg.ShowAdminAddr != 0 { + cm.ShowAdminAddr = fileCfg.ShowAdminAddr + cm.yamlManagedKeys["show_admin_addr"] = true } - - // 使用公共方法获取配置映射 - defaultConfigs := cm.buildConfigMap() - - // 批量插入默认配置 - var keyValues []models.KeyValue - for key, value := range defaultConfigs { - keyValues = append(keyValues, models.KeyValue{ - Key: key, - Value: value, - }) + if fileCfg.Opacity != 0 { + cm.Opacity = fileCfg.Opacity + cm.yamlManagedKeys["opacity"] = true } - - if err := cm.db.CreateInBatches(keyValues, 50).Error; err != nil { - return fmt.Errorf("插入默认配置失败: %w", err) + if fileCfg.Background != "" { + cm.Background = fileCfg.Background + cm.yamlManagedKeys["background"] = true } - return nil } -// SetDB 设置数据库连接 -func (cm *ConfigManager) SetDB(db *gorm.DB) { - cm.db = db -} - -// Save 保存配置 -func (cm *ConfigManager) Save() error { - // 验证配置 - if err := cm.Validate(); err != nil { - return err - } - - // 只保存到数据库 - if cm.db != nil { - return cm.saveToDatabase() - } - - return errors.New("数据库连接未设置,无法保存配置") -} - -// saveToDatabase 保存配置到数据库 -func (cm *ConfigManager) saveToDatabase() error { - if cm.db == nil { - return errors.New("数据库连接未设置") - } - - // 使用公共方法获取配置映射 - configMap := cm.buildConfigMap() - - for key, value := range configMap { - kv := models.KeyValue{ - Key: key, - Value: value, - } - - // 使用 UPSERT 操作 - if err := cm.db.Where("key = ?", key).Assign(models.KeyValue{Value: value}).FirstOrCreate(&kv).Error; err != nil { - return fmt.Errorf("保存配置项 %s 失败: %w", key, err) - } - } +// InitWithDB has been removed. Use SetDB(db) to inject a database connection. +func (cm *ConfigManager) InitDefaultDataInDB() error { + // No-op: database initialization for config is intentionally disabled. + // Configuration should be provided via config.yaml or environment variables. return nil } -// LoadFromDatabase 从数据库加载配置 -func (cm *ConfigManager) LoadFromDatabase() error { - if cm.db == nil { - return errors.New("数据库连接未设置") - } - - var kvPairs []models.KeyValue - if err := cm.db.Find(&kvPairs).Error; err != nil { - return fmt.Errorf("查询配置失败: %w", err) - } - - // 如果数据库中没有配置,返回错误以触发初始化 - if len(kvPairs) == 0 { - return fmt.Errorf("数据库中没有配置数据") - } - - // 构建数据映射 - data := make(map[string]string) - for _, kv := range kvPairs { - // 支持嵌套格式的键,转换为平面格式 - if strings.Contains(kv.Key, ".") { - // 将嵌套格式转换为平面格式,例如 "user.allow_user_registration" -> "allow_user_registration" - parts := strings.SplitN(kv.Key, ".", 2) - if len(parts) == 2 { - // 如果是已知的嵌套格式,去掉前缀 - switch parts[0] { - case "user", "base", "transfer", "storage", "database", "mcp": - data[parts[1]] = kv.Value - default: - // 保持原样 - data[kv.Key] = kv.Value - } - } else { - data[kv.Key] = kv.Value - } - } else { - data[kv.Key] = kv.Value - } - } - - // 加载各个配置模块 - if err := cm.Base.FromMap(data); err != nil { - return fmt.Errorf("加载基础配置失败: %w", err) - } - - if err := cm.Database.FromMap(data); err != nil { - return fmt.Errorf("加载数据库配置失败: %w", err) +// buildConfigMap flattens modules to module_field keys +func (cm *ConfigManager) buildConfigMap() map[string]string { + out := make(map[string]string) + for k, v := range cm.Base.ToMap() { + out[k] = v } - - if err := cm.Transfer.FromMap(data); err != nil { - return fmt.Errorf("加载传输配置失败: %w", err) + for k, v := range cm.Database.ToMap() { + out[k] = v } - - if err := cm.Storage.FromMap(data); err != nil { - return fmt.Errorf("加载存储配置失败: %w", err) + for k, v := range cm.Transfer.ToMap() { + out[k] = v } - - if err := cm.User.FromMap(data); err != nil { - return fmt.Errorf("加载用户系统配置失败: %w", err) + for k, v := range cm.Storage.ToMap() { + out[k] = v } - - if err := cm.MCP.FromMap(data); err != nil { - return fmt.Errorf("加载MCP配置失败: %w", err) + for k, v := range cm.User.ToMap() { + out[k] = v } + for k, v := range cm.MCP.ToMap() { + out[k] = v + } + out["notify_title"] = cm.NotifyTitle + out["notify_content"] = cm.NotifyContent + out["admin_token"] = cm.AdminToken + out["themes_select"] = cm.ThemesSelect + out["robots_text"] = cm.RobotsText + out["page_explain"] = cm.PageExplain + out["show_admin_addr"] = fmt.Sprintf("%d", cm.ShowAdminAddr) + out["opacity"] = fmt.Sprintf("%v", cm.Opacity) + out["background"] = cm.Background + return out +} - // 加载其他配置 - if val, ok := data["notify_title"]; ok { - cm.NotifyTitle = val - } - if val, ok := data["notify_content"]; ok { - cm.NotifyContent = val - } - if val, ok := data["page_explain"]; ok { - cm.PageExplain = val - } - if val, ok := data["upload_minute"]; ok { - if v, err := strconv.Atoi(val); err == nil { - cm.UploadMinute = v - } - } - if val, ok := data["upload_count"]; ok { - if v, err := strconv.Atoi(val); err == nil { - cm.UploadCount = v - } - } - if val, ok := data["error_minute"]; ok { - if v, err := strconv.Atoi(val); err == nil { - cm.ErrorMinute = v - } - } - if val, ok := data["error_count"]; ok { - if v, err := strconv.Atoi(val); err == nil { - cm.ErrorCount = v - } - } - if val, ok := data["themes_select"]; ok { - cm.ThemesSelect = val - } - if val, ok := data["opacity"]; ok { - if v, err := strconv.ParseFloat(val, 64); err == nil { - cm.Opacity = v - } - } - if val, ok := data["background"]; ok { - cm.Background = val - } - if val, ok := data["admin_token"]; ok { - cm.AdminToken = val - } - if val, ok := data["show_admin_address"]; ok { - if v, err := strconv.Atoi(val); err == nil { - cm.ShowAdminAddr = v - } - } - if val, ok := data["robots_text"]; ok { - cm.RobotsText = val - } +func (cm *ConfigManager) saveToDatabase() error { + // No-op: saving configuration to DB is disabled under the "YAML-first" policy. + return nil +} +// LoadFromDatabase previously supported per-row compatibility formats. +// That behavior has been removed: configuration should come from `config.yaml` or environment variables. +// LoadFromDatabase is now a no-op to preserve caller compatibility. +func (cm *ConfigManager) LoadFromDatabase() error { + // No per-row DB compatibility parsing. Return nil as no-op. return nil } -// ReloadConfig 重新加载配置(热重载) func (cm *ConfigManager) ReloadConfig() error { if cm.db == nil { return errors.New("数据库连接未设置") } - - // 保存当前的端口和管理员密码 - originalPort := cm.Base.Port - originalAdminToken := cm.AdminToken - - // 从数据库重新加载配置 + curPort := cm.Base.Port + curAdmin := cm.AdminToken if err := cm.LoadFromDatabase(); err != nil { - return fmt.Errorf("重新加载配置失败: %w", err) + return err } - - // 恢复原始的端口和管理员密码设置 - cm.Base.Port = originalPort - cm.AdminToken = originalAdminToken - - // 重新应用环境变量中的优先级设置 + cm.Base.Port = curPort + cm.AdminToken = curAdmin cm.applyEnvironmentOverrides() - return nil } -// applyEnvironmentOverrides 应用环境变量覆盖 func (cm *ConfigManager) applyEnvironmentOverrides() { - // 端口配置 - 总是优先环境变量 - if port := os.Getenv("PORT"); port != "" { - if p, err := strconv.Atoi(port); err == nil { - cm.Base.Port = p + if p := os.Getenv("PORT"); p != "" { + if n, err := strconv.Atoi(p); err == nil { + cm.Base.Port = n } } - - // 管理员密码配置 - 总是优先环境变量 - if adminToken := os.Getenv("ADMIN_TOKEN"); adminToken != "" { - cm.AdminToken = adminToken + if t := os.Getenv("ADMIN_TOKEN"); t != "" { + cm.AdminToken = t } - - // 主机绑定配置 - if host := os.Getenv("HOST"); host != "" { - cm.Base.Host = host - } - - // 数据路径配置 - if dataPath := os.Getenv("DATA_PATH"); dataPath != "" { - cm.Base.DataPath = dataPath - } - - // 生产模式配置 - if production := os.Getenv("PRODUCTION"); production != "" { - if prod, err := strconv.Atoi(production); err == nil { - cm.Base.Production = prod == 1 - } - } - - // 上传配置 - if openUpload := os.Getenv("OPEN_UPLOAD"); openUpload != "" { - if upload, err := strconv.Atoi(openUpload); err == nil { - cm.Transfer.Upload.OpenUpload = upload - } - } - - if uploadSize := os.Getenv("UPLOAD_SIZE"); uploadSize != "" { - if size, err := strconv.ParseInt(uploadSize, 10, 64); err == nil { - cm.Transfer.Upload.UploadSize = size - } - } - - // 数据库配置 - if dbType := os.Getenv("DATABASE_TYPE"); dbType != "" { - cm.Database.Type = dbType - } - - if dbHost := os.Getenv("DATABASE_HOST"); dbHost != "" { - cm.Database.Host = dbHost - } - - if dbPort := os.Getenv("DATABASE_PORT"); dbPort != "" { - if port, err := strconv.Atoi(dbPort); err == nil { - cm.Database.Port = port - } - } - - if dbName := os.Getenv("DATABASE_NAME"); dbName != "" { - cm.Database.Name = dbName - } - - if dbUser := os.Getenv("DATABASE_USER"); dbUser != "" { - cm.Database.User = dbUser - } - - if dbPass := os.Getenv("DATABASE_PASS"); dbPass != "" { - cm.Database.Pass = dbPass + if dp := os.Getenv("DATA_PATH"); dp != "" { + cm.Base.DataPath = dp } - - if dbSSL := os.Getenv("DATABASE_SSL"); dbSSL != "" { - cm.Database.SSL = dbSSL - } -} - -// GetAddress 获取服务器完整地址 -func (cm *ConfigManager) GetAddress() string { - return cm.Base.GetAddress() -} - -// GetDatabaseDSN 获取数据库连接字符串 -func (cm *ConfigManager) GetDatabaseDSN() (string, error) { - return cm.Database.GetDSN() } -// IsUserSystemEnabled 判断是否启用用户系统 -func (cm *ConfigManager) IsUserSystemEnabled() bool { - return cm.User.IsUserSystemEnabled() -} - -// IsMCPEnabled 判断是否启用MCP服务器 -func (cm *ConfigManager) IsMCPEnabled() bool { - return cm.MCP.IsMCPEnabled() +// Save saves the configuration to the database (if db is set). +func (cm *ConfigManager) Save() error { + // No-op save to DB + return nil } -// Clone 克隆整个配置管理器 +// Get helpers +func (cm *ConfigManager) GetAddress() string { return cm.Base.GetAddress() } +func (cm *ConfigManager) GetDatabaseDSN() (string, error) { return cm.Database.GetDSN() } +func (cm *ConfigManager) IsUserSystemEnabled() bool { return cm.User.IsUserSystemEnabled() } +func (cm *ConfigManager) IsMCPEnabled() bool { return cm.MCP.IsMCPEnabled() } func (cm *ConfigManager) Clone() *ConfigManager { - clone := &ConfigManager{ - Base: cm.Base.Clone(), - Database: cm.Database.Clone(), - Transfer: cm.Transfer.Clone(), - Storage: cm.Storage.Clone(), - User: cm.User.Clone(), - MCP: cm.MCP.Clone(), - - NotifyTitle: cm.NotifyTitle, - NotifyContent: cm.NotifyContent, - PageExplain: cm.PageExplain, - ExpireStyle: make([]string, len(cm.ExpireStyle)), - - UploadMinute: cm.UploadMinute, - UploadCount: cm.UploadCount, - ErrorMinute: cm.ErrorMinute, - ErrorCount: cm.ErrorCount, - - ThemesSelect: cm.ThemesSelect, - ThemesChoices: make([]Theme, len(cm.ThemesChoices)), - Opacity: cm.Opacity, - Background: cm.Background, + nc := NewConfigManager() + nc.Base = cm.Base.Clone() + nc.Database = cm.Database.Clone() + nc.Transfer = cm.Transfer.Clone() + nc.Storage = cm.Storage.Clone() + nc.User = cm.User.Clone() + nc.MCP = cm.MCP.Clone() + nc.NotifyTitle = cm.NotifyTitle + nc.NotifyContent = cm.NotifyContent + nc.AdminToken = cm.AdminToken + return nc +} - AdminToken: cm.AdminToken, - ShowAdminAddr: cm.ShowAdminAddr, - RobotsText: cm.RobotsText, +// Validate validates all configuration modules. +func (cm *ConfigManager) Validate() error { + if cm.Base == nil || cm.Database == nil || cm.Transfer == nil || cm.Storage == nil || cm.User == nil || cm.MCP == nil { + return errors.New("配置模块未完全初始化") } - - copy(clone.ExpireStyle, cm.ExpireStyle) - copy(clone.ThemesChoices, cm.ThemesChoices) - - return clone + if err := cm.Base.Validate(); err != nil { + return err + } + if err := cm.Database.Validate(); err != nil { + return err + } + return nil } diff --git a/internal/config/manager_test.go b/internal/config/manager_test.go new file mode 100644 index 0000000..6735f3c --- /dev/null +++ b/internal/config/manager_test.go @@ -0,0 +1,77 @@ +package config + +import ( + "os" + "testing" + + "gopkg.in/yaml.v3" +) + +// TestLoadFromYAML ensures YAML fields are loaded and marked as yamlManaged +func TestLoadFromYAML(t *testing.T) { + data := map[string]interface{}{ + "base": map[string]interface{}{"name": "TCB", "port": 12345}, + "themes_select": "themes/test", + "page_explain": "test explain", + "notify_title": "nt", + } + b, err := yaml.Marshal(data) + if err != nil { + t.Fatalf("yaml marshal: %v", err) + } + f := "./test_config.yaml" + if err := os.WriteFile(f, b, 0644); err != nil { + t.Fatalf("write tmp yaml: %v", err) + } + defer os.Remove(f) + + cm := NewConfigManager() + if err := cm.LoadFromYAML(f); err != nil { + t.Fatalf("LoadFromYAML failed: %v", err) + } + if cm.Base == nil || cm.Base.Name != "TCB" { + t.Fatalf("expected base.name TCB, got %#v", cm.Base) + } + if cm.ThemesSelect != "themes/test" { + t.Fatalf("expected themes_select themes/test, got %s", cm.ThemesSelect) + } + // Ensure at least one of Base.ToMap() keys is present in yamlManagedKeys + found := false + for k := range cm.Base.ToMap() { + if cm.yamlManagedKeys[k] { + found = true + break + } + } + if !found { + t.Fatalf("expected some base.* key to be recorded in yamlManagedKeys, got none") + } +} + +// TestEnvOverride ensures environment variables override YAML values +func TestEnvOverride(t *testing.T) { + data2 := map[string]interface{}{ + "base": map[string]interface{}{"name": "FromYaml", "port": 8080}, + } + b2, err := yaml.Marshal(data2) + if err != nil { + t.Fatalf("yaml marshal: %v", err) + } + f := "./test_config_env.yaml" + if err := os.WriteFile(f, b2, 0644); err != nil { + t.Fatalf("write tmp yaml: %v", err) + } + defer os.Remove(f) + + os.Setenv("PORT", "9090") + defer os.Unsetenv("PORT") + + cm := NewConfigManager() + if err := cm.LoadFromYAML(f); err != nil { + t.Fatalf("LoadFromYAML failed: %v", err) + } + cm.applyEnvironmentOverrides() + if cm.Base.Port != 9090 { + t.Fatalf("expected PORT env to override to 9090, got %d", cm.Base.Port) + } +} diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go index f29b742..d540c9a 100644 --- a/internal/handlers/admin.go +++ b/internal/handlers/admin.go @@ -916,14 +916,89 @@ func (h *AdminHandler) ExportUsers(c *gin.Context) { ) } - // 设置响应头 + // 添加UTF-8 BOM以确保Excel正确显示中文 + bomContent := "\xEF\xBB\xBF" + csvContent + + // 设置响应头(Content-Length 使用实际发送的字节长度) c.Header("Content-Type", "text/csv; charset=utf-8") c.Header("Content-Disposition", "attachment; filename=users_export.csv") - c.Header("Content-Length", strconv.Itoa(len(csvContent))) + c.Header("Content-Length", strconv.Itoa(len([]byte(bomContent)))) - // 添加UTF-8 BOM以确保Excel正确显示中文 - bomContent := "\xEF\xBB\xBF" + csvContent - c.String(200, bomContent) + // 使用 Write 写入原始字节,避免框架对长度的二次处理 + c.Writer.WriteHeader(200) + _, _ = c.Writer.Write([]byte(bomContent)) +} + +// BatchEnableUsers 批量启用用户 +func (h *AdminHandler) BatchEnableUsers(c *gin.Context) { + var req struct { + UserIDs []uint `json:"user_ids" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.BadRequestResponse(c, "参数错误: "+err.Error()) + return + } + + if len(req.UserIDs) == 0 { + common.BadRequestResponse(c, "user_ids 不能为空") + return + } + + if err := h.service.BatchUpdateUserStatus(req.UserIDs, true); err != nil { + common.InternalServerErrorResponse(c, "批量启用用户失败: "+err.Error()) + return + } + + common.SuccessWithMessage(c, "批量启用成功", nil) +} + +// BatchDisableUsers 批量禁用用户 +func (h *AdminHandler) BatchDisableUsers(c *gin.Context) { + var req struct { + UserIDs []uint `json:"user_ids" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.BadRequestResponse(c, "参数错误: "+err.Error()) + return + } + + if len(req.UserIDs) == 0 { + common.BadRequestResponse(c, "user_ids 不能为空") + return + } + + if err := h.service.BatchUpdateUserStatus(req.UserIDs, false); err != nil { + common.InternalServerErrorResponse(c, "批量禁用用户失败: "+err.Error()) + return + } + + common.SuccessWithMessage(c, "批量禁用成功", nil) +} + +// BatchDeleteUsers 批量删除用户 +func (h *AdminHandler) BatchDeleteUsers(c *gin.Context) { + var req struct { + UserIDs []uint `json:"user_ids" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.BadRequestResponse(c, "参数错误: "+err.Error()) + return + } + + if len(req.UserIDs) == 0 { + common.BadRequestResponse(c, "user_ids 不能为空") + return + } + + if err := h.service.BatchDeleteUsers(req.UserIDs); err != nil { + common.InternalServerErrorResponse(c, "批量删除用户失败: "+err.Error()) + return + } + + common.SuccessWithMessage(c, "批量删除成功", nil) } // GetMCPConfig 获取 MCP 配置 diff --git a/internal/handlers/setup.go b/internal/handlers/setup.go index 51ebea7..39b74a6 100644 --- a/internal/handlers/setup.go +++ b/internal/handlers/setup.go @@ -1,14 +1,23 @@ package handlers import ( + "encoding/json" + "errors" "fmt" + "log" + "os" + "path/filepath" + "strconv" + "sync/atomic" "github.com/gin-gonic/gin" "github.com/zy84338719/filecodebox/internal/common" "github.com/zy84338719/filecodebox/internal/config" + "github.com/zy84338719/filecodebox/internal/database" "github.com/zy84338719/filecodebox/internal/models" "github.com/zy84338719/filecodebox/internal/repository" "github.com/zy84338719/filecodebox/internal/services/auth" + "gorm.io/gorm" ) // SetupHandler 系统初始化处理器 @@ -47,89 +56,11 @@ type AdminConfig struct { Username string `json:"username"` Email string `json:"email"` Nickname string `json:"nickname"` + Confirm string `json:"confirm"` Password string `json:"password"` AllowUserRegistration bool `json:"allowUserRegistration"` } -// Initialize 执行系统初始化 -func (h *SetupHandler) Initialize(c *gin.Context) { - // 首先检查系统是否已经初始化 - adminCount, err := h.daoManager.User.CountAdminUsers() - if err != nil { - common.InternalServerErrorResponse(c, "检查系统状态失败") - return - } - - if adminCount > 0 { - common.BadRequestResponse(c, "系统已经初始化,无法重复初始化") - return - } - - var req SetupRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "请求参数错误: "+err.Error()) - return - } - - // 验证管理员信息 - if err := h.validateAdminConfig(req.Admin); err != nil { - common.BadRequestResponse(c, err.Error()) - return - } - - // 验证数据库配置 - if err := h.validateDatabaseConfig(req.Database); err != nil { - common.BadRequestResponse(c, err.Error()) - return - } - - // 更新数据库配置 - if err := h.updateDatabaseConfig(req.Database); err != nil { - common.InternalServerErrorResponse(c, "更新数据库配置失败: "+err.Error()) - return - } - - // 创建管理员用户 - if err := h.createAdminUser(req.Admin); err != nil { - common.InternalServerErrorResponse(c, "创建管理员用户失败: "+err.Error()) - return - } - - // 启用用户系统 - if err := h.enableUserSystem(req.Admin); err != nil { - common.InternalServerErrorResponse(c, "启用用户系统失败: "+err.Error()) - return - } - - common.SuccessWithMessage(c, "系统初始化成功", map[string]interface{}{ - "message": "系统初始化完成", - "admin_username": req.Admin.Username, - "database_type": req.Database.Type, - }) -} - -// validateAdminConfig 验证管理员配置 -func (h *SetupHandler) validateAdminConfig(admin AdminConfig) error { - if len(admin.Username) < 3 { - return fmt.Errorf("用户名长度至少3个字符") - } - - if len(admin.Password) < 6 { - return fmt.Errorf("密码长度至少6个字符") - } - - if admin.Email == "" { - return fmt.Errorf("邮箱地址不能为空") - } - - // 简单的邮箱格式验证 - if len(admin.Email) < 5 || !contains(admin.Email, "@") { - return fmt.Errorf("邮箱格式无效") - } - - return nil -} - // validateDatabaseConfig 验证数据库配置 func (h *SetupHandler) validateDatabaseConfig(db DatabaseConfig) error { switch db.Type { @@ -188,10 +119,29 @@ func (h *SetupHandler) updateDatabaseConfig(db DatabaseConfig) error { // createAdminUser 创建管理员用户 func (h *SetupHandler) createAdminUser(admin AdminConfig) error { - // 使用auth服务创建用户 - authService := auth.NewService(h.daoManager, h.manager) + // 在创建前检查用户是否已存在(按用户名或邮箱),保证初始化过程幂等 + if h.daoManager == nil { + return fmt.Errorf("daoManager 未初始化") + } - // 哈希密码 + // 检查用户名 + if _, err := h.daoManager.User.GetByUsername(admin.Username); err == nil { + log.Printf("[createAdminUser] 管理员已存在(用户名):%s,跳过创建", admin.Username) + return nil + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("查询用户失败: %w", err) + } + + // 检查邮箱 + if _, err := h.daoManager.User.GetByEmail(admin.Email); err == nil { + log.Printf("[createAdminUser] 管理员已存在(邮箱):%s,跳过创建", admin.Email) + return nil + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("查询用户失败: %w", err) + } + + // 使用auth服务创建用户并哈希密码 + authService := auth.NewService(h.daoManager, h.manager) hashedPassword, err := authService.HashPassword(admin.Password) if err != nil { return fmt.Errorf("密码哈希失败: %w", err) @@ -216,7 +166,16 @@ func (h *SetupHandler) createAdminUser(admin AdminConfig) error { MaxStorageQuota: h.manager.User.UserStorageQuota, } - return h.daoManager.User.Create(user) + err = h.daoManager.User.Create(user) + if err != nil { + // 如果是唯一约束冲突(用户已存在),视为成功(幂等行为) + if contains(err.Error(), "UNIQUE constraint failed") || contains(err.Error(), "duplicate key value") { + log.Printf("[createAdminUser] 用户已存在,忽略错误: %v", err) + return nil + } + return err + } + return nil } // enableUserSystem 启用用户系统 @@ -243,3 +202,500 @@ func contains(s, substr string) bool { } return false } + +// OnDatabaseInitialized 当数据库初始化完成时,handlers 包中的回调(由 main 设置) +var OnDatabaseInitialized func(daoManager *repository.RepositoryManager) + +// initInProgress 用于防止并发初始化 +var initInProgress int32 = 0 + +// InitializeNoDB 用于在没有 daoManager 的情况下处理 /setup/initialize 请求 +// 它会:验证请求、使用配置管理器初始化数据库、创建 daoManager、创建管理员用户,最后触发 OnDatabaseInitialized 回调 +func InitializeNoDB(manager *config.ConfigManager) gin.HandlerFunc { + return func(c *gin.Context) { + // 并发保护:避免多个初始化同时进行 + if !atomic.CompareAndSwapInt32(&initInProgress, 0, 1) { + common.BadRequestResponse(c, "系统正在初始化,请稍候") + return + } + defer atomic.StoreInt32(&initInProgress, 0) + var req SetupRequest + + // 读取原始请求体,支持两种格式:嵌套 JSON({database:{}, admin:{}})和扁平表单风格 JSON(db_type, db_path, admin_token, admin_password 等) + data, err := c.GetRawData() + if err != nil { + common.BadRequestResponse(c, "无法读取请求体: "+err.Error()) + return + } + + // 先尝试解码为嵌套结构 + // 注意:如果前端提交的是扁平字段(如 db_type, admin_password 等), + // json.Unmarshal 会成功但会留下空的嵌套结构。我们在解码成功后仍需 + // 检查是否需要回退到扁平映射解析。 + if err := json.Unmarshal(data, &req); err != nil || (req.Database.Type == "" && req.Admin.Username == "" && req.Admin.Password == "") { + // 尝试扁平化映射 + var flat map[string]interface{} + if err := json.Unmarshal(data, &flat); err != nil { + common.BadRequestResponse(c, "请求参数错误: 无法解析 JSON") + return + } + + // 映射常见字段 + if v, ok := flat["db_type"].(string); ok { + req.Database.Type = v + } + if v, ok := flat["db_path"].(string); ok { + req.Database.File = v + } + if v, ok := flat["db_file"].(string); ok && req.Database.File == "" { + req.Database.File = v + } + if v, ok := flat["db_host"].(string); ok { + req.Database.Host = v + } + if v, ok := flat["db_user"].(string); ok { + req.Database.User = v + } + if v, ok := flat["db_password"].(string); ok { + req.Database.Password = v + } + if v, ok := flat["db_name"].(string); ok { + req.Database.Database = v + } + if v, ok := flat["admin_password"].(string); ok { + req.Admin.Password = v + } + // 读取密码确认 + if v, ok := flat["admin_password_confirm"].(string); ok { + req.Admin.Confirm = v + } + if v, ok := flat["admin_username"].(string); ok { + req.Admin.Username = v + } + if v, ok := flat["admin_email"].(string); ok { + req.Admin.Email = v + } + if v, ok := flat["admin_nickname"].(string); ok { + req.Admin.Nickname = v + } + // enable_user_system may be "true"/"false" 或 布尔 + if v, ok := flat["enable_user_system"].(string); ok { + if b, err := strconv.ParseBool(v); err == nil && b { + req.Admin.AllowUserRegistration = true + } + } else if v, ok := flat["enable_user_system"].(bool); ok { + req.Admin.AllowUserRegistration = v + } + + // 兼容前端 admin_token:视为系统管理员令牌(Manager.AdminToken) + if v, ok := flat["admin_token"].(string); ok && v != "" { + manager.AdminToken = v + } + + // 兼容前端 site_name, storage_type, max_file_size,先写入 manager 内存结构(将在 Save 时持久化) + if v, ok := flat["site_name"].(string); ok && v != "" { + manager.Base.Name = v + } + if v, ok := flat["storage_type"].(string); ok && v != "" { + manager.Storage.Type = v + } + if v, ok := flat["max_file_size"].(string); ok && v != "" { + if n, err := strconv.Atoi(v); err == nil { + manager.Transfer.Upload.UploadSize = int64(n) + } + } else if v, ok := flat["max_file_size"].(float64); ok { + manager.Transfer.Upload.UploadSize = int64(v) + } + + // 如果提供了 sqlite 文件路径(例如 ./data/filecodebox.db),将目录设置为 Base.DataPath + if req.Database.File != "" && req.Database.Type == "sqlite" { + dir := filepath.Dir(req.Database.File) + if dir == "." || dir == "" { + // 使用默认数据目录 + } else { + manager.Base.DataPath = dir + } + } + + // 如果前端没有提供管理员用户名/email,使用合理的默认值 + if req.Admin.Username == "" { + // 尝试从 manager.AdminToken 派生用户名,否则使用 "admin" + if manager != nil && manager.AdminToken != "" { + req.Admin.Username = manager.AdminToken + } else { + req.Admin.Username = "admin" + } + } + if req.Admin.Email == "" { + req.Admin.Email = "admin@localhost" + } + + } + + // 继续使用 req 进行验证和初始化 + // 捕获并验证扁平字段中的 storage_path(如果提供并且 storage 类型为 local),但不要立刻写入 manager + var desiredStoragePath string + if manager.Storage.Type == "local" { + // 尝试从原始请求体的扁平映射中读取 storage_path + var flat map[string]interface{} + _ = json.Unmarshal(data, &flat) + if v, ok := flat["storage_path"].(string); ok { + sp := v + if sp == "" { + common.BadRequestResponse(c, "本地存储时必须提供 storage_path") + return + } + + // 若为相对路径,则相对于 manager.Base.DataPath + if !filepath.IsAbs(sp) { + if manager.Base != nil && manager.Base.DataPath != "" { + sp = filepath.Join(manager.Base.DataPath, sp) + } else { + sp, _ = filepath.Abs(sp) + } + } + + // 尝试创建目录(如果不存在) + if _, err := os.Stat(sp); os.IsNotExist(err) { + if err := os.MkdirAll(sp, 0755); err != nil { + common.InternalServerErrorResponse(c, "创建本地存储目录失败: "+err.Error()) + return + } + } + + // 检查是否可写:尝试在目录中创建一个临时文件 + testFile := filepath.Join(sp, ".perm_check") + if f, err := os.Create(testFile); err != nil { + common.InternalServerErrorResponse(c, "本地存储路径不可写: "+err.Error()) + return + } else { + f.Close() + _ = os.Remove(testFile) + } + + desiredStoragePath = sp + } + } + + // 验证管理员信息 + if len(req.Admin.Username) < 3 { + common.BadRequestResponse(c, "用户名长度至少3个字符") + return + } + if len(req.Admin.Password) < 6 { + common.BadRequestResponse(c, "密码长度至少6个字符") + return + } + // 验证密码确认(若提供) + if req.Admin.Confirm != "" && req.Admin.Confirm != req.Admin.Password { + common.BadRequestResponse(c, "两次输入的管理员密码不一致") + return + } + if req.Admin.Email == "" || len(req.Admin.Email) < 5 || !contains(req.Admin.Email, "@") { + common.BadRequestResponse(c, "邮箱格式无效") + return + } + + // 验证数据库配置(简单校验) + switch req.Database.Type { + case "sqlite": + if req.Database.File == "" { + common.BadRequestResponse(c, "SQLite 数据库文件路径不能为空") + return + } + case "mysql", "postgres": + if req.Database.Host == "" || req.Database.User == "" || req.Database.Database == "" { + common.BadRequestResponse(c, "关系型数据库连接信息不完整") + return + } + default: + common.BadRequestResponse(c, "不支持的数据库类型: "+req.Database.Type) + return + } + + // 将数据库配置写入 manager + manager.Database.Type = req.Database.Type + switch req.Database.Type { + case "sqlite": + manager.Database.Name = req.Database.File + manager.Database.Host = "" + manager.Database.Port = 0 + manager.Database.User = "" + manager.Database.Pass = "" + case "mysql", "postgres": + manager.Database.Host = req.Database.Host + manager.Database.Port = req.Database.Port + manager.Database.User = req.Database.User + manager.Database.Pass = req.Database.Password + manager.Database.Name = req.Database.Database + } + // 注意:此处不能在数据库未初始化前调用 manager.Save(),因为 Save 仅保存到数据库。 + // 后续将在数据库初始化并注入 manager 后再次保存配置。 + + // 初始化数据库连接并执行自动迁移 + log.Printf("[InitializeNoDB] 开始调用 database.InitWithManager, dbType=%s, dataPath=%s", manager.Database.Type, manager.Base.DataPath) + db, err := database.InitWithManager(manager) + if err != nil { + log.Printf("[InitializeNoDB] InitWithManager 失败: %v", err) + common.InternalServerErrorResponse(c, "初始化数据库失败: "+err.Error()) + return + } + + // 将 db 注入 manager 并初始化默认配置 + // Inject DB connection into manager. Initialization of config from DB is disabled. + manager.SetDB(db) + + // 诊断检查:确认 manager 内部已设置 db + if manager.GetDB() == nil { + log.Printf("[InitializeNoDB] 警告: manager.GetDB() 返回 nil(注入失败)") + common.InternalServerErrorResponse(c, "初始化失败:配置管理器未能获取数据库连接") + return + } + + // 创建 daoManager + daoManager := repository.NewRepositoryManager(db) + + // 如果之前捕获了 desiredStoragePath,则此时 manager 已注入 DB,可以持久化 storage_path + if desiredStoragePath != "" { + manager.Storage.StoragePath = desiredStoragePath + if err := manager.Save(); err != nil { + log.Printf("[InitializeNoDB] 保存 storage_path 失败: %v", err) + // 记录但不阻塞初始化流程 + if manager.Base != nil && manager.Base.DataPath != "" { + _ = os.WriteFile(manager.Base.DataPath+"/init_save_storage_err.log", []byte(err.Error()), 0644) + } else { + _ = os.WriteFile("init_save_storage_err.log", []byte(err.Error()), 0644) + } + } + } + + // 创建管理员用户(使用 SetupHandler.createAdminUser,包含幂等性处理) + setupHandler := NewSetupHandler(daoManager, manager) + if err := setupHandler.createAdminUser(req.Admin); err != nil { + log.Printf("[InitializeNoDB] 创建管理员用户失败: %v", err) + common.InternalServerErrorResponse(c, "创建管理员用户失败: "+err.Error()) + return + } + + // 启用用户系统配置 + if req.Admin.AllowUserRegistration { + manager.User.AllowUserRegistration = 1 + } else { + manager.User.AllowUserRegistration = 0 + } + if err := manager.Save(); err != nil { + // 不阻塞初始化成功路径,但记录错误 + log.Printf("[InitializeNoDB] manager.Save() 返回错误(但不阻塞初始化): %v", err) + // 将错误写入数据目录下的日志文件以便排查 + if manager.Base != nil && manager.Base.DataPath != "" { + _ = os.WriteFile(manager.Base.DataPath+"/init_save_err.log", []byte(err.Error()), 0644) + } else { + _ = os.WriteFile("init_save_err.log", []byte(err.Error()), 0644) + } + } + + // 触发回调以让主程序挂载其余路由并启动后台任务 + if OnDatabaseInitialized != nil { + OnDatabaseInitialized(daoManager) + } + + common.SuccessWithMessage(c, "系统初始化成功", map[string]interface{}{ + "message": "系统初始化完成", + "admin_username": req.Admin.Username, + "database_type": req.Database.Type, + }) + } +} + +// Initialize 在数据库已经可用的情况下处理 /setup/initialize 请求 +// 该方法用于通过已存在的 daoManager 来完成系统初始化(保存配置、创建管理员等) +func (h *SetupHandler) Initialize(c *gin.Context) { + // 并发保护:避免多个初始化同时进行 + if !atomic.CompareAndSwapInt32(&initInProgress, 0, 1) { + common.BadRequestResponse(c, "系统正在初始化,请稍候") + return + } + defer atomic.StoreInt32(&initInProgress, 0) + + if h == nil || h.manager == nil { + common.InternalServerErrorResponse(c, "服务器未正确初始化") + return + } + if h.daoManager == nil { + common.InternalServerErrorResponse(c, "数据库管理器未初始化") + return + } + + var req SetupRequest + + data, err := c.GetRawData() + if err != nil { + common.BadRequestResponse(c, "无法读取请求体: "+err.Error()) + return + } + + // 先尝试解析为嵌套结构 + if err := json.Unmarshal(data, &req); err != nil { + // 解析为扁平 map 并映射 + var flat map[string]interface{} + if err := json.Unmarshal(data, &flat); err != nil { + common.BadRequestResponse(c, "请求参数错误: 无法解析 JSON") + return + } + if v, ok := flat["db_type"].(string); ok { + req.Database.Type = v + } + if v, ok := flat["db_path"].(string); ok { + req.Database.File = v + } + if v, ok := flat["db_file"].(string); ok && req.Database.File == "" { + req.Database.File = v + } + if v, ok := flat["db_host"].(string); ok { + req.Database.Host = v + } + if v, ok := flat["db_user"].(string); ok { + req.Database.User = v + } + if v, ok := flat["db_password"].(string); ok { + req.Database.Password = v + } + if v, ok := flat["db_name"].(string); ok { + req.Database.Database = v + } + if v, ok := flat["admin_password"].(string); ok { + req.Admin.Password = v + } + if v, ok := flat["admin_username"].(string); ok { + req.Admin.Username = v + } + if v, ok := flat["admin_email"].(string); ok { + req.Admin.Email = v + } + // 存储路径(local 存储时使用) + if v, ok := flat["storage_path"].(string); ok { + h.manager.Storage.StoragePath = v + } + // 读取密码确认 + if v, ok := flat["admin_password_confirm"].(string); ok { + req.Admin.Confirm = v + } + if v, ok := flat["admin_nickname"].(string); ok { + req.Admin.Nickname = v + } + if v, ok := flat["enable_user_system"].(string); ok { + if b, err := strconv.ParseBool(v); err == nil && b { + req.Admin.AllowUserRegistration = true + } + } else if v, ok := flat["enable_user_system"].(bool); ok { + req.Admin.AllowUserRegistration = v + } + + // 兼容前端 admin_token:视为系统管理员令牌(Manager.AdminToken) + if v, ok := flat["admin_token"].(string); ok && v != "" { + h.manager.AdminToken = v + } + + // 如果前端没有提供管理员用户名/email,使用合理的默认值 + if req.Admin.Username == "" { + // 尝试从 manager.AdminToken 派生用户名,否则使用 "admin" + if h.manager != nil && h.manager.AdminToken != "" { + req.Admin.Username = h.manager.AdminToken + } else { + req.Admin.Username = "admin" + } + } + if req.Admin.Email == "" { + req.Admin.Email = "admin@localhost" + } + + // 如果 storage_type 是 local,则处理 storage_path(扁平字段) + if h.manager.Storage.Type == "local" { + if v, ok := flat["storage_path"].(string); ok { + sp := v + if sp == "" { + common.BadRequestResponse(c, "本地存储时必须提供 storage_path") + return + } + + // 若为相对路径,则相对于 h.manager.Base.DataPath + if !filepath.IsAbs(sp) { + if h.manager.Base != nil && h.manager.Base.DataPath != "" { + sp = filepath.Join(h.manager.Base.DataPath, sp) + } else { + sp, _ = filepath.Abs(sp) + } + } + + // 尝试创建目录(如果不存在) + if _, err := os.Stat(sp); os.IsNotExist(err) { + if err := os.MkdirAll(sp, 0755); err != nil { + common.InternalServerErrorResponse(c, "创建本地存储目录失败: "+err.Error()) + return + } + } + + // 检查是否可写:尝试在目录中创建一个临时文件 + testFile := filepath.Join(sp, ".perm_check") + if f, err := os.Create(testFile); err != nil { + common.InternalServerErrorResponse(c, "本地存储路径不可写: "+err.Error()) + return + } else { + f.Close() + _ = os.Remove(testFile) + } + + h.manager.Storage.StoragePath = sp + if err := h.manager.Save(); err != nil { + common.InternalServerErrorResponse(c, "保存存储配置失败: "+err.Error()) + return + } + } else { + // 如果没有传 storage_path,前端应已校验,但服务器端也需要确保 + common.BadRequestResponse(c, "本地存储时必须提供 storage_path") + return + } + } + } + + // 验证管理员信息 + if len(req.Admin.Username) < 3 { + common.BadRequestResponse(c, "用户名长度至少3个字符") + return + } + if len(req.Admin.Password) < 6 { + common.BadRequestResponse(c, "密码长度至少6个字符") + return + } + if req.Admin.Confirm != "" && req.Admin.Confirm != req.Admin.Password { + common.BadRequestResponse(c, "两次输入的管理员密码不一致") + return + } + if req.Admin.Email == "" || len(req.Admin.Email) < 5 || !contains(req.Admin.Email, "@") { + common.BadRequestResponse(c, "邮箱格式无效") + return + } + + // 更新数据库配置并保存(manager.Save 会将配置写入数据库,因为 manager 已注入 DB) + if err := h.updateDatabaseConfig(req.Database); err != nil { + common.InternalServerErrorResponse(c, "保存数据库配置失败: "+err.Error()) + return + } + + // 创建管理员用户 + if err := h.createAdminUser(req.Admin); err != nil { + common.InternalServerErrorResponse(c, "创建管理员用户失败: "+err.Error()) + return + } + + // 启用用户系统设置 + if err := h.enableUserSystem(req.Admin); err != nil { + // 记录但不阻塞主要流程 + log.Printf("enableUserSystem 返回错误: %v", err) + } + + common.SuccessWithMessage(c, "系统初始化成功", map[string]interface{}{ + "message": "系统初始化完成", + "admin_username": req.Admin.Username, + }) +} diff --git a/internal/handlers/storage.go b/internal/handlers/storage.go index 2f567ea..3a82ccb 100644 --- a/internal/handlers/storage.go +++ b/internal/handlers/storage.go @@ -5,6 +5,7 @@ import ( "github.com/zy84338719/filecodebox/internal/config" "github.com/zy84338719/filecodebox/internal/models/web" "github.com/zy84338719/filecodebox/internal/storage" + "github.com/zy84338719/filecodebox/internal/utils" "github.com/gin-gonic/gin" ) @@ -45,6 +46,33 @@ func (sh *StorageHandler) GetStorageInfo(c *gin.Context) { detail.Error = err.Error() } + // 尝试附加路径与使用率信息 + if storageType == "local" { + // 本地存储使用配置中的 StoragePath + detail.StoragePath = sh.storageConfig.StoragePath + + // 尝试读取磁盘使用率(若可用) + if sh.storageConfig.StoragePath != "" { + if usagePercent, err := utils.GetUsagePercent(sh.storageConfig.StoragePath); err == nil { + val := int(usagePercent) + detail.UsagePercent = &val + } + } + } else if storageType == "s3" { + // S3 使用 bucket 名称作为标识 + if sh.storageConfig.S3 != nil { + detail.StoragePath = sh.storageConfig.S3.BucketName + } + } else if storageType == "webdav" { + if sh.storageConfig.WebDAV != nil { + detail.StoragePath = sh.storageConfig.WebDAV.Hostname + } + } else if storageType == "nfs" { + if sh.storageConfig.NFS != nil { + detail.StoragePath = sh.storageConfig.NFS.MountPoint + } + } + storageDetails[storageType] = detail } @@ -360,3 +388,5 @@ func (sh *StorageHandler) UpdateStorageConfig(c *gin.Context) { common.SuccessWithMessage(c, "存储配置更新成功", nil) } + +// ...existing code... diff --git a/internal/handlers/user.go b/internal/handlers/user.go index c8e0c2d..58b319f 100644 --- a/internal/handlers/user.go +++ b/internal/handlers/user.go @@ -318,10 +318,20 @@ func (h *UserHandler) CheckAuth(c *gin.Context) { // GetSystemInfo 获取系统信息(公开接口) func (h *UserHandler) GetSystemInfo(c *gin.Context) { + // 将布尔值转换为 0/1 整数,保持 API 一致性 + enabled := 0 + if h.userService.IsUserSystemEnabled() { + enabled = 1 + } + allowReg := 0 + if h.userService.IsRegistrationAllowed() { + allowReg = 1 + } + response := &web.UserSystemInfoResponse{ - UserSystemEnabled: h.userService.IsUserSystemEnabled(), - AllowUserRegistration: h.userService.IsRegistrationAllowed(), - RequireEmailVerification: false, // 这里需要从配置获取 + UserSystemEnabled: enabled, + AllowUserRegistration: allowReg, + RequireEmailVerification: 0, // TODO: 从配置获取真实值(目前保留为0) } common.SuccessResponse(c, response) diff --git a/internal/models/db/filecode_test.go b/internal/models/db/filecode_test.go index 549f9a3..cb64a3b 100644 --- a/internal/models/db/filecode_test.go +++ b/internal/models/db/filecode_test.go @@ -8,13 +8,13 @@ import ( func TestFileCode_IsExpired(t *testing.T) { // 当前时间,用于测试 now := time.Now() - + // 过去的时间(已过期) pastTime := now.Add(-24 * time.Hour) - + // 未来的时间(未过期) futureTime := now.Add(24 * time.Hour) - + tests := []struct { name string fileCode FileCode @@ -101,7 +101,7 @@ func TestFileCode_IsExpired(t *testing.T) { wantExpired: true, }, } - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.fileCode.IsExpired(); got != tt.wantExpired { @@ -109,4 +109,4 @@ func TestFileCode_IsExpired(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/internal/models/service/admin.go b/internal/models/service/admin.go index 927d52b..49bb85a 100644 --- a/internal/models/service/admin.go +++ b/internal/models/service/admin.go @@ -83,10 +83,10 @@ type DatabaseStats struct { // StorageStatus 存储状态信息 type StorageStatus struct { - Type string `json:"type"` - Status string `json:"status"` - Available bool `json:"available"` - Details map[string]string `json:"details"` + Type string `json:"type"` + Status string `json:"status"` + Available bool `json:"available"` + Details map[string]interface{} `json:"details"` } // DiskUsage 磁盘使用情况 diff --git a/internal/models/web/storage.go b/internal/models/web/storage.go index 5ca52f9..6986e27 100644 --- a/internal/models/web/storage.go +++ b/internal/models/web/storage.go @@ -13,6 +13,10 @@ type StorageDetail struct { Type string `json:"type"` Available bool `json:"available"` Error string `json:"error,omitempty"` + // StoragePath 本存储的路径或标识(例如本地目录、S3 bucket) + StoragePath string `json:"storage_path,omitempty"` + // UsagePercent 使用率(0-100),如果无法获取则为 nil + UsagePercent *int `json:"usage_percent,omitempty"` } // StorageTestRequest 存储测试请求 diff --git a/internal/models/web/user.go b/internal/models/web/user.go index 48e308c..ee82aa9 100644 --- a/internal/models/web/user.go +++ b/internal/models/web/user.go @@ -64,7 +64,8 @@ type UserFilesResponse struct { // UserSystemInfoResponse 用户系统信息响应 type UserSystemInfoResponse struct { - UserSystemEnabled bool `json:"user_system_enabled"` - AllowUserRegistration bool `json:"allow_user_registration"` - RequireEmailVerification bool `json:"require_email_verification"` + // 使用整型 0/1 表示开关,以与配置层和前端保持一致 + UserSystemEnabled int `json:"user_system_enabled"` + AllowUserRegistration int `json:"allow_user_registration"` + RequireEmailVerification int `json:"require_email_verification"` } diff --git a/internal/repository/manager.go b/internal/repository/manager.go index da02f0b..10d4b33 100644 --- a/internal/repository/manager.go +++ b/internal/repository/manager.go @@ -32,3 +32,8 @@ func NewRepositoryManager(db *gorm.DB) *RepositoryManager { func (m *RepositoryManager) BeginTransaction() *gorm.DB { return m.db.Begin() } + +// DB 返回底层 gorm.DB 引用(只读) +func (m *RepositoryManager) DB() *gorm.DB { + return m.db +} diff --git a/internal/routes/admin.go b/internal/routes/admin.go index 03026ce..425020e 100644 --- a/internal/routes/admin.go +++ b/internal/routes/admin.go @@ -185,6 +185,10 @@ func setupUserRoutes(authGroup *gin.RouterGroup, adminHandler *handlers.AdminHan authGroup.PUT("/users/:id/status", adminHandler.UpdateUserStatus) authGroup.GET("/users/:id/files", adminHandler.GetUserFiles) authGroup.GET("/users/export", adminHandler.ExportUsers) + // 批量用户操作 + authGroup.POST("/users/batch-enable", adminHandler.BatchEnableUsers) + authGroup.POST("/users/batch-disable", adminHandler.BatchDisableUsers) + authGroup.POST("/users/batch-delete", adminHandler.BatchDeleteUsers) } // setupStorageRoutes 设置存储管理路由 diff --git a/internal/routes/base.go b/internal/routes/base.go index 8a18cbe..091d92f 100644 --- a/internal/routes/base.go +++ b/internal/routes/base.go @@ -65,6 +65,44 @@ func SetupBaseRoutes(router *gin.Engine, userHandler *handlers.UserHandler, cfg ServeSetup(c, cfg) }) + // 永远注册 /user/system-info 接口: + // - 明确返回 JSON(即使在未初始化数据库时也不会返回 HTML) + // - 如果传入了 userHandler(数据库已初始化),则委托给 userHandler.GetSystemInfo + // - 否则返回一个轻量的 JSON 响应,避免返回 HTML 导致前端解析失败 + router.GET("/user/system-info", func(c *gin.Context) { + // 明确设置 JSON 响应头,避免被其他中间件或 NoRoute 覆盖成 HTML + c.Header("Cache-Control", "no-cache") + c.Header("Content-Type", "application/json; charset=utf-8") + + if userHandler != nil { + // Delegate to the real handler which also writes JSON + userHandler.GetSystemInfo(c) + // Ensure no further handlers run + c.Abort() + return + } + + // 返回轻量的 JSON 响应(与前端兼容) + // 返回与后端其他字段类型一致的整数值(0/1),避免前端对布尔/整型的解析差异 + allowReg := 0 + if cfg.User.AllowUserRegistration == 1 { + allowReg = 1 + } + c.JSON(200, gin.H{ + "code": 200, + "data": gin.H{ + "user_system_enabled": 1, + "allow_user_registration": allowReg, + }, + }) + c.Abort() + }) + + // 兼容:在未初始化数据库时,允许 POST /setup 用于提交扁平表单风格的初始化请求 + if cfg != nil && cfg.GetDB() == nil { + router.POST("/setup", handlers.InitializeNoDB(cfg)) + } + router.NoRoute(func(c *gin.Context) { ServeIndex(c, cfg) }) @@ -110,7 +148,12 @@ func ServeIndex(c *gin.Context, cfg *config.ConfigManager) { html = strings.ReplaceAll(html, "{{keywords}}", cfg.Base.Keywords) html = strings.ReplaceAll(html, "{{page_explain}}", cfg.PageExplain) html = strings.ReplaceAll(html, "{{opacity}}", fmt.Sprintf("%.1f", cfg.Opacity)) - html = strings.ReplaceAll(html, `"/assets/`, `"assets/`) + // 将相对路径转换为绝对路径,避免在子路径下请求相对路径(例如 /user/login -> /user/js/...) + html = strings.ReplaceAll(html, "src=\"js/", "src=\"/js/") + html = strings.ReplaceAll(html, "href=\"css/", "href=\"/css/") + html = strings.ReplaceAll(html, "src=\"assets/", "src=\"/assets/") + html = strings.ReplaceAll(html, "href=\"assets/", "href=\"/assets/") + html = strings.ReplaceAll(html, "src=\"components/", "src=\"/components/") html = strings.ReplaceAll(html, "{{background}}", cfg.Background) c.Header("Cache-Control", "no-cache") @@ -133,7 +176,10 @@ func ServeSetup(c *gin.Context, cfg *config.ConfigManager) { html = strings.ReplaceAll(html, "{{title}}", cfg.Base.Name+" - 系统初始化") html = strings.ReplaceAll(html, "{{description}}", cfg.Base.Description) html = strings.ReplaceAll(html, "{{keywords}}", cfg.Base.Keywords) - html = strings.ReplaceAll(html, `"/assets/`, `"assets/`) + // 将相对资源路径转换为绝对路径 + html = strings.ReplaceAll(html, "src=\"js/", "src=\"/js/") + html = strings.ReplaceAll(html, "href=\"css/", "href=\"/css/") + html = strings.ReplaceAll(html, "src=\"assets/", "src=\"/assets/") c.Header("Cache-Control", "no-cache") c.Header("Content-Type", "text/html; charset=utf-8") diff --git a/internal/routes/setup.go b/internal/routes/setup.go index 692f6a8..0706495 100644 --- a/internal/routes/setup.go +++ b/internal/routes/setup.go @@ -85,6 +85,29 @@ func CreateAndSetupRouter( router.Use(middleware.CORS()) router.Use(middleware.RateLimit(manager)) + // 如果 daoManager 为 nil,表示尚未初始化数据库,只注册基础和初始化相关的路由 + if daoManager == nil { + // 基础路由(不传 userHandler) + SetupBaseRoutes(router, nil, manager) + + // 提供一个不依赖数据库的初始化 POST 接口,由 handlers.InitializeNoDB 处理 + router.POST("/setup/initialize", handlers.InitializeNoDB(manager)) + + // 即便数据库尚未初始化,也应当能访问用户登录/注册页面(只返回静态HTML), + // 以便用户能够在首次部署时完成初始化或查看登录页面。 + router.GET("/user/login", func(c *gin.Context) { + ServeUserPage(c, manager, "login.html") + }) + router.GET("/user/register", func(c *gin.Context) { + ServeUserPage(c, manager, "register.html") + }) + router.GET("/user/forgot-password", func(c *gin.Context) { + ServeUserPage(c, manager, "forgot-password.html") + }) + + return router + } + // 设置路由(自动初始化所有服务和处理器) SetupAllRoutesWithDependencies(router, manager, daoManager, storageManager) @@ -119,6 +142,37 @@ func SetupAllRoutesWithDependencies( SetupAllRoutes(router, shareHandler, chunkHandler, adminHandler, storageHandler, userHandler, setupHandler, manager, userService) } +// RegisterDynamicRoutes 在数据库可用后注册需要数据库的路由(不包含基础路由) +func RegisterDynamicRoutes( + router *gin.Engine, + manager *config.ConfigManager, + daoManager *repository.RepositoryManager, + storageManager *storage.StorageManager, +) { + // 创建具体的存储服务 + storageService := storage.NewConcreteStorageService(manager) + + // 初始化服务 + userService := services.NewUserService(daoManager, manager) + shareServiceInstance := services.NewShareService(daoManager, manager, storageService, userService) + chunkService := services.NewChunkService(daoManager, manager, storageService) + adminService := services.NewAdminService(daoManager, manager, storageService) + + // 初始化处理器 + shareHandler := handlers.NewShareHandler(shareServiceInstance) + chunkHandler := handlers.NewChunkHandler(chunkService) + adminHandler := handlers.NewAdminHandler(adminService, manager) + storageHandler := handlers.NewStorageHandler(storageManager, manager.Storage, manager) + userHandler := handlers.NewUserHandler(userService) + // 设置分享、用户、分片、管理员等路由(不重复注册基础路由) + // 注意:SetupAllRoutes 会调用 SetupBaseRoutes,因此我们直接调用 SetupShareRoutes 等单独函数 + SetupShareRoutes(router, shareHandler, manager, userService) + SetupUserRoutes(router, userHandler, manager, userService) + SetupChunkRoutes(router, chunkHandler, manager) + SetupAdminRoutes(router, adminHandler, storageHandler, manager, userService) + // System init routes are no longer needed after DB init +} + // SetupAllRoutes 设置所有路由(使用已初始化的处理器) func SetupAllRoutes( router *gin.Engine, diff --git a/internal/routes/user.go b/internal/routes/user.go index 8fda452..5236e6b 100644 --- a/internal/routes/user.go +++ b/internal/routes/user.go @@ -4,6 +4,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "github.com/zy84338719/filecodebox/internal/config" "github.com/zy84338719/filecodebox/internal/handlers" @@ -27,7 +28,8 @@ func SetupUserRoutes( // 公开路由(不需要认证) userGroup.POST("/register", userHandler.Register) userGroup.POST("/login", userHandler.Login) - userGroup.GET("/system-info", userHandler.GetSystemInfo) + // `/user/system-info` 由 `SetupBaseRoutes` 全局注册并在有 `userHandler` 时委托处理, + // 因此在此处不要重复注册以避免路由冲突(Gin 在重复注册同一路径时会 panic) userGroup.GET("/check-initialization", userHandler.CheckSystemInitialization) // 需要认证的路由 @@ -73,7 +75,14 @@ func ServeUserPage(c *gin.Context, cfg *config.ConfigManager, pageName string) { return } + html := string(content) + // 将相对静态资源路径转换为绝对路径,避免在子路径下(如 /user/login)请求到 /user/js/... 导致返回 HTML + html = strings.ReplaceAll(html, "src=\"js/", "src=\"/js/") + html = strings.ReplaceAll(html, "href=\"css/", "href=\"/css/") + html = strings.ReplaceAll(html, "src=\"assets/", "src=\"/assets/") + html = strings.ReplaceAll(html, "href=\"assets/", "href=\"/assets/") + c.Header("Cache-Control", "no-cache") c.Header("Content-Type", "text/html; charset=utf-8") - c.String(http.StatusOK, string(content)) + c.String(http.StatusOK, html) } diff --git a/internal/services/admin/config.go b/internal/services/admin/config.go index ce18e6f..c2244ba 100644 --- a/internal/services/admin/config.go +++ b/internal/services/admin/config.go @@ -166,8 +166,6 @@ func (s *Service) UpdateConfigFromRequest(configRequest *web.AdminConfigRequest) return s.UpdateConfig(configUpdates) } - - // flattenConfig 扁平化配置数据 func (s *Service) flattenConfig(prefix string, value interface{}, result map[string]interface{}) error { switch v := value.(type) { diff --git a/internal/services/admin/maintenance.go b/internal/services/admin/maintenance.go index e6a2d18..28142db 100644 --- a/internal/services/admin/maintenance.go +++ b/internal/services/admin/maintenance.go @@ -6,6 +6,7 @@ import ( "time" "github.com/zy84338719/filecodebox/internal/models" + "github.com/zy84338719/filecodebox/internal/utils" ) // CleanupExpiredFiles 清理过期文件 @@ -151,14 +152,39 @@ func (s *Service) GetStorageStatus() (*models.StorageStatus, error) { return nil, err } + details := map[string]interface{}{ + "used_storage": totalSize, + } + + // 根据当前配置尝试附加 path 与使用率信息 + storageType := s.manager.Storage.Type + if storageType == "local" { + details["storage_path"] = s.manager.Storage.StoragePath + if s.manager.Storage.StoragePath != "" { + if usage, err := utils.GetUsagePercent(s.manager.Storage.StoragePath); err == nil { + // 四舍五入到整数 + details["usage_percent"] = int(usage) + } + } + } else if storageType == "s3" { + if s.manager.Storage.S3 != nil { + details["storage_path"] = s.manager.Storage.S3.BucketName + } + } else if storageType == "webdav" { + if s.manager.Storage.WebDAV != nil { + details["storage_path"] = s.manager.Storage.WebDAV.Hostname + } + } else if storageType == "nfs" { + if s.manager.Storage.NFS != nil { + details["storage_path"] = s.manager.Storage.NFS.MountPoint + } + } + return &models.StorageStatus{ - Type: s.manager.Storage.Type, + Type: storageType, Status: "active", Available: true, - Details: map[string]string{ - "storage_path": s.manager.Storage.StoragePath, - "used_storage": fmt.Sprintf("%d", totalSize), - }, + Details: details, }, nil } @@ -234,6 +260,8 @@ func (s *Service) GetSystemInfo() (*models.SystemInfo, error) { }, nil } +// ...existing code... + // CleanInvalidRecords 清理无效记录 (兼容性方法) func (s *Service) CleanInvalidRecords() (int, error) { return s.CleanupInvalidFiles() diff --git a/internal/services/admin/users.go b/internal/services/admin/users.go index ebef465..21d1af8 100644 --- a/internal/services/admin/users.go +++ b/internal/services/admin/users.go @@ -149,3 +149,46 @@ func (s *Service) ToggleUserStatus(id uint) error { func (s *Service) GetUserByID(id uint) (*models.User, error) { return s.GetUser(id) } + +// BatchUpdateUserStatus 批量更新用户状态:enable=true 表示启用(active),false 表示禁用(inactive) +func (s *Service) BatchUpdateUserStatus(userIDs []uint, enable bool) error { + if len(userIDs) == 0 { + return nil + } + + status := "inactive" + if enable { + status = "active" + } + + tx := s.repositoryManager.BeginTransaction() + if tx == nil { + return errors.New("无法开始数据库事务") + } + + if err := tx.Model(&models.User{}).Where("id IN ?", userIDs).Update("status", status).Error; err != nil { + tx.Rollback() + return err + } + + return tx.Commit().Error +} + +// BatchDeleteUsers 批量删除用户 +func (s *Service) BatchDeleteUsers(userIDs []uint) error { + if len(userIDs) == 0 { + return nil + } + + tx := s.repositoryManager.BeginTransaction() + if tx == nil { + return errors.New("无法开始数据库事务") + } + + if err := tx.Where("id IN ?", userIDs).Delete(&models.User{}).Error; err != nil { + tx.Rollback() + return err + } + + return tx.Commit().Error +} diff --git a/internal/utils/disk.go b/internal/utils/disk.go new file mode 100644 index 0000000..d66692c --- /dev/null +++ b/internal/utils/disk.go @@ -0,0 +1,25 @@ +package utils + +import ( + "fmt" + "syscall" +) + +// GetUsagePercent attempts to get disk usage percent for a given path (0-100). +// Returns error on unsupported platforms or when statfs fails. +func GetUsagePercent(path string) (float64, error) { + var stat syscall.Statfs_t + if err := syscall.Statfs(path, &stat); err != nil { + return 0, err + } + + total := float64(stat.Blocks) * float64(stat.Bsize) + free := float64(stat.Bfree) * float64(stat.Bsize) + used := total - free + if total <= 0 { + return 0, fmt.Errorf("unable to compute total disk size") + } + + usage := (used / total) * 100.0 + return usage, nil +} diff --git a/internal/utils/disk_test.go b/internal/utils/disk_test.go new file mode 100644 index 0000000..f3c5a53 --- /dev/null +++ b/internal/utils/disk_test.go @@ -0,0 +1,41 @@ +package utils + +import ( + "os" + "runtime" + "testing" +) + +// Tests rely on syscall.Statfs which is not supported on Windows in this codepath. +func TestGetUsagePercent_ValidPath(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping disk usage test on windows") + } + + dir, err := os.MkdirTemp("", "disktest") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(dir) + + usage, err := GetUsagePercent(dir) + if err != nil { + t.Fatalf("expected no error for valid path, got: %v", err) + } + if usage < 0 || usage > 100 { + t.Fatalf("usage percent out of range: %v", usage) + } +} + +func TestGetUsagePercent_InvalidPath(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping disk usage test on windows") + } + + // choose a path that almost certainly doesn't exist + path := "/path/that/does/not/exist_for_disk_test" + _, err := GetUsagePercent(path) + if err == nil { + t.Fatalf("expected error for invalid path, got nil") + } +} diff --git a/main.go b/main.go index a6a9dbd..c8c6546 100644 --- a/main.go +++ b/main.go @@ -22,19 +22,17 @@ package main // @securityDefinitions.basic BasicAuth import ( - "flag" - "fmt" + "context" "os" "os/signal" - "runtime" "syscall" "time" + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/cli" "github.com/zy84338719/filecodebox/internal/config" - "github.com/zy84338719/filecodebox/internal/database" "github.com/zy84338719/filecodebox/internal/handlers" "github.com/zy84338719/filecodebox/internal/mcp" - "github.com/zy84338719/filecodebox/internal/models" "github.com/zy84338719/filecodebox/internal/repository" "github.com/zy84338719/filecodebox/internal/routes" "github.com/zy84338719/filecodebox/internal/services" @@ -48,104 +46,109 @@ import ( ) func main() { - // 解析命令行参数 - var showVersion = flag.Bool("version", false, "show version information") - flag.Parse() - - if *showVersion { - buildInfo := models.GetBuildInfo() - fmt.Printf("FileCodeBox %s\n", buildInfo.Version) - fmt.Printf("Commit: %s\n", buildInfo.GitCommit) - fmt.Printf("Built: %s\n", buildInfo.BuildTime) - fmt.Printf("Go Version: %s\n", runtime.Version()) + // 如果有子命令参数,切换到 CLI 模式(使用 Cobra) + if len(os.Args) > 1 { + // delay import of CLI to avoid cycles + cli.Execute() return } // 初始化日志 logrus.SetLevel(logrus.InfoLevel) - logrus.SetFormatter(&logrus.TextFormatter{ - FullTimestamp: true, - }) + logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) logrus.Info("正在初始化应用...") - // 初始化新的配置管理器 - manager := config.InitManager() + // 使用上下文管理生命周期 + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // 初始化数据库 - 使用manager而不是cfg - db, err := database.InitWithManager(manager) - if err != nil { - logrus.Fatal("初始化数据库失败:", err) - } - - // 自动迁移 - err = db.AutoMigrate(&models.FileCode{}, - &models.UploadChunk{}, &models.KeyValue{}, &models.User{}, &models.UserSession{}) - if err != nil { - logrus.Fatal("数据库迁移失败:", err) - } + // 初始化配置管理器 + manager := config.InitManager() - // 使用数据库初始化配置管理器 - if err := manager.InitWithDB(db); err != nil { - logrus.Fatal("初始化配置管理器失败:", err) - } + // 延迟数据库初始化:不在启动时创建 DB,让用户通过 /setup/initialize 触发 - // 初始化存储 + // 初始化存储(不依赖数据库) storageManager := storage.NewStorageManager(manager) - // 初始化 DAO 管理器 - daoManager := repository.NewRepositoryManager(db) - - // 创建具体的存储服务 - storageService := storage.NewConcreteStorageService(manager) - - // 初始化服务(为了MCP服务器) - userService := services.NewUserService(daoManager, manager) - shareService := services.NewShareService(daoManager, manager, storageService, userService) - adminService := services.NewAdminService(daoManager, manager, storageService) - - // 初始化清理任务 - taskManager := tasks.NewTaskManager(daoManager, storageManager, manager.Base.DataPath) - taskManager.Start() - defer taskManager.Stop() - - // 初始化 MCP 管理器 - mcpManager := mcp.NewMCPManager(manager, daoManager, storageManager, shareService, adminService, userService) - - // 设置全局 MCP 管理器(供 admin handler 使用) - handlers.SetMCPManager(mcpManager) - - // 创建并配置路由(包含Gin初始化、中间件、路由设置) + // 创建并启动最小 HTTP 服务器(daoManager 传 nil) + var daoManager *repository.RepositoryManager = nil srv, err := routes.CreateAndStartServer(manager, daoManager, storageManager) if err != nil { - logrus.Fatal("创建服务器失败:", err) + logrus.Fatalf("创建服务器失败: %v", err) } - // 根据配置启动 MCP 服务器 - if manager.MCP.EnableMCPServer == 1 { - if err := mcpManager.StartMCPServer(manager.MCP.MCPPort); err != nil { - logrus.Fatal("启动 MCP 服务器失败: ", err) + // 从 srv.Handler 获取 router(gin 引擎),用于动态注册路由 + routerEngine := srv.Handler + + // 设置 OnDatabaseInitialized 回调:当 /setup/initialize 完成数据库初始化后会调用此回调 + handlers.OnDatabaseInitialized = func(dmgr *repository.RepositoryManager) { + // 这里创建 DB 相关的服务、任务与 MCP,并动态注册路由 + logrus.Info("收到数据库初始化完成回调,开始挂载动态路由与启动后台服务") + + // 创建具体的存储服务(基于 manager) + storageService := storage.NewConcreteStorageService(manager) + + // 初始化服务 + userService := services.NewUserService(dmgr, manager) + shareService := services.NewShareService(dmgr, manager, storageService, userService) + adminService := services.NewAdminService(dmgr, manager, storageService) + + // 启动任务管理器 + taskManager := tasks.NewTaskManager(dmgr, storageManager, manager.Base.DataPath) + taskManager.Start() + // 注意:taskManager 的停止将在主结束时处理(可以扩展保存引用以便停止) + + // 初始化 MCP 管理器并根据配置启动 + mcpManager := mcp.NewMCPManager(manager, dmgr, storageManager, shareService, adminService, userService) + handlers.SetMCPManager(mcpManager) + if manager.MCP.EnableMCPServer == 1 { + if err := mcpManager.StartMCPServer(manager.MCP.MCPPort); err != nil { + logrus.Errorf("启动 MCP 服务器失败: %v", err) + } else { + logrus.Info("MCP 服务器已启动") + } + } + + // 将 DAO 底层 DB 注入 manager + if dmdb := dmgr.DB(); dmdb != nil { + manager.SetDB(dmdb) + } + + // 动态注册需要数据库支持的路由 + if ginEngine, ok := routerEngine.(*gin.Engine); ok { + routes.RegisterDynamicRoutes(ginEngine, manager, dmgr, storageManager) + } else { + logrus.Warn("无法获取 gin 引擎实例,动态路由未注册") } - logrus.Info("MCP 服务器已在主程序启动时自动启动") } logrus.Info("应用初始化完成") - // 等待中断信号 - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit + // 等待中断信号,优雅退出 + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - logrus.Info("正在关闭服务器...") + select { + case <-ctx.Done(): + logrus.Info("上下文已取消,开始关闭...") + case sig := <-sigCh: + logrus.Infof("收到信号 %v,开始关闭...", sig) + } - // 优雅关闭 + // 优雅关闭 HTTP 服务器 if err := routes.GracefulShutdown(srv, 30*time.Second); err != nil { - logrus.Fatal("关闭服务器失败:", err) + logrus.Errorf("关闭服务器失败: %v", err) } - // 关闭数据库连接 - if sqlDB, err := db.DB(); err == nil { - if err := sqlDB.Close(); err != nil { - logrus.Error("关闭数据库连接失败:", err) + + // 关闭数据库连接(如果已初始化) + if dbPtr := manager.GetDB(); dbPtr != nil { + if sqlDB, err := dbPtr.DB(); err == nil { + if err := sqlDB.Close(); err != nil { + logrus.Errorf("关闭数据库连接失败: %v", err) + } + } else { + logrus.Errorf("获取数据库底层连接失败: %v", err) } } } diff --git a/scripts/export_config_from_db.go b/scripts/export_config_from_db.go new file mode 100644 index 0000000..aac27c5 --- /dev/null +++ b/scripts/export_config_from_db.go @@ -0,0 +1,141 @@ +package main + +import ( + "database/sql" + "flag" + "fmt" + "log" + "os" + + _ "github.com/mattn/go-sqlite3" + "gopkg.in/yaml.v3" +) + +func main() { + dbPath := flag.String("db", "data/filecodebox.db", "path to sqlite db") + out := flag.String("out", "config.generated.yaml", "output yaml file") + flag.Parse() + + if _, err := os.Stat(*dbPath); err != nil { + log.Fatalf("db not found: %v", err) + } + + db, err := sql.Open("sqlite3", *dbPath) + if err != nil { + log.Fatalf("open db: %v", err) + } + defer db.Close() + + r, err := db.Query("SELECT key, value FROM key_values") + if err != nil { + log.Fatalf("query: %v", err) + } + defer r.Close() + + cfg := make(map[string]map[string]interface{}) + cfg["base"] = map[string]interface{}{} + cfg["database"] = map[string]interface{}{} + cfg["storage"] = map[string]interface{}{} + cfg["user"] = map[string]interface{}{} + cfg["mcp"] = map[string]interface{}{} + cfg["ui"] = map[string]interface{}{} + + for r.Next() { + var k, v string + if err := r.Scan(&k, &v); err != nil { + log.Printf("scan err: %v", err) + continue + } + + // basic mapping heuristics similar to previous python script + if k == "name" || k == "description" || k == "host" || k == "port" || k == "data_path" || k == "production" { + cfg["base"][k] = parseValue(v) + continue + } + if len(k) >= 9 && k[:9] == "database_" { + sub := k[9:] + cfg["database"][sub] = parseValue(v) + continue + } + if len(k) >= 6 && k[:5] == "user_" { + sub := k[5:] + cfg["user"][sub] = parseValue(v) + continue + } + if len(k) >= 4 && k[:4] == "mcp_" { + sub := k[4:] + cfg["mcp"][sub] = parseValue(v) + continue + } + if len(k) >= 8 && k[:8] == "storage." { + // storage.
or storage.
. + parts := splitN(k, '.', 3) + if len(parts) >= 2 { + section := parts[1] + if len(parts) == 2 { + cfg["storage"][section] = parseValue(v) + } else { + // nested map + m, ok := cfg["storage"][section].(map[string]interface{}) + if !ok { + m = map[string]interface{}{} + } + m[parts[2]] = parseValue(v) + cfg["storage"][section] = m + } + } + continue + } + // fallback to ui + cfg["ui"][k] = parseValue(v) + } + + outF, err := os.Create(*out) + if err != nil { + log.Fatalf("create out: %v", err) + } + enc := yaml.NewEncoder(outF) + enc.SetIndent(2) + if err := enc.Encode(cfg); err != nil { + log.Fatalf("encode yaml: %v", err) + } + outF.Close() + fmt.Printf("wrote %s\n", *out) +} + +func splitN(s string, sep byte, n int) []string { + res := []string{} + cur := "" + count := 0 + for i := 0; i < len(s); i++ { + if s[i] == sep { + res = append(res, cur) + cur = "" + count++ + if count >= n-1 { + res = append(res, s[i+1:]) + return res + } + } else { + cur += string(s[i]) + } + } + res = append(res, cur) + return res +} + +func parseValue(v string) interface{} { + // try int + var i int + _, err := fmt.Sscanf(v, "%d", &i) + if err == nil { + return i + } + if v == "true" || v == "1" { + return true + } + if v == "false" || v == "0" { + return false + } + return v +} diff --git a/scripts/export_config_from_db.py b/scripts/export_config_from_db.py new file mode 100644 index 0000000..06c224f --- /dev/null +++ b/scripts/export_config_from_db.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +Simple export script: read key_values table from sqlite DB and create config.generated.yaml +Usage: python3 scripts/export_config_from_db.py [path/to/filecodebox.db] [output.yaml] +""" +import sqlite3 +import sys +import yaml +from pathlib import Path + +DB_PATH = sys.argv[1] if len(sys.argv) > 1 else 'data/filecodebox.db' +OUT_PATH = sys.argv[2] if len(sys.argv) > 2 else 'config.generated.yaml' + +conn = sqlite3.connect(DB_PATH) +cur = conn.cursor() +cur.execute("SELECT key, value FROM key_values") +rows = cur.fetchall() + +cfg = { + 'base': {}, + 'database': {}, + 'storage': {}, + 'user': {}, + 'mcp': {}, + 'ui': {}, +} + +for k, v in rows: + if k == 'name': cfg['base']['name'] = v + elif k == 'description': cfg['base']['description'] = v + elif k == 'host': cfg['base']['host'] = v + elif k == 'port': + try: + cfg['base']['port'] = int(v) + except: + cfg['base']['port'] = v + elif k == 'data_path': cfg['base']['data_path'] = v + elif k == 'production': cfg['base']['production'] = (v == 'true' or v == '1') + elif k.startswith('database_'): + sub = k.split('database_',1)[1] + cfg['database'][sub] = int(v) if v.isdigit() else v + elif k.startswith('storage.'): + # storage keys may be storage.default or storage.local.path etc + parts = k.split('.') + if len(parts) >= 2: + section = parts[1] + if section not in cfg['storage']: + cfg['storage'][section] = {} + if len(parts) == 2: + cfg['storage'][section] = v + else: + # e.g. storage.local.path + subkey = parts[2] + cfg['storage'][section][subkey] = v + elif k.startswith('user_'): + sub = k.split('user_',1)[1] + try: + cfg['user'][sub] = int(v) + except: + cfg['user'][sub] = (v == 'true' or v == '1') if v in ['0','1','true','false'] else v + elif k.startswith('mcp_'): + sub = k.split('mcp_',1)[1] + try: + cfg['mcp'][sub] = int(v) + except: + cfg['mcp'][sub] = (v == 'true' or v == '1') if v in ['0','1','true','false'] else v + elif k.startswith('theme') or k.startswith('notify') or k.startswith('page_'): + cfg['ui'][k] = v + else: + # fall back: put in ui + cfg['ui'][k] = v + +with open(OUT_PATH, 'w') as f: + yaml.safe_dump(cfg, f, default_flow_style=False, sort_keys=False, allow_unicode=True) + +print(f"Wrote {OUT_PATH} from {DB_PATH}") diff --git a/test_expiry.txt b/test_expiry.txt deleted file mode 100644 index d4623c4..0000000 --- a/test_expiry.txt +++ /dev/null @@ -1 +0,0 @@ -测试文件过期逻辑修复 diff --git a/debug_homepage.sh b/tests/debug_homepage.sh similarity index 100% rename from debug_homepage.sh rename to tests/debug_homepage.sh diff --git a/test.txt b/tests/test.txt similarity index 100% rename from test.txt rename to tests/test.txt diff --git a/test_design_system.html b/tests/test_design_system.html similarity index 100% rename from test_design_system.html rename to tests/test_design_system.html diff --git a/test_file_list.html b/tests/test_file_list.html similarity index 100% rename from test_file_list.html rename to tests/test_file_list.html diff --git a/test_file_management.html b/tests/test_file_management.html similarity index 100% rename from test_file_management.html rename to tests/test_file_management.html diff --git a/themes/2025/admin/css/storage.css b/themes/2025/admin/css/storage.css index 2c80bdc..94821ac 100644 --- a/themes/2025/admin/css/storage.css +++ b/themes/2025/admin/css/storage.css @@ -399,6 +399,81 @@ .wizard-step { display: none; + +/* Current storage card - new styles for improved display */ +.current-storage-card { + background: #ffffff; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 16px; +} + +.current-storage-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.storage-type-badge { + display: inline-block; + background: #f1f3f5; + color: #333; + padding: 6px 10px; + border-radius: 12px; + font-size: 12px; + margin-right: 8px; +} + +.storage-status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; +} + +.storage-status-badge.status-success { + background: #d4edda; + color: #155724; +} + +.storage-status-badge.status-error { + background: #f8d7da; + color: #721c24; +} + +.current-storage-body { + padding-top: 8px; +} + +.storage-path, .storage-usage { + font-size: 13px; + color: #495057; + margin-top: 6px; +} + +.storage-error { + margin-top: 10px; + color: #721c24; + font-weight: 600; +} + +/* storage card meta info */ +.storage-meta { + margin-top: 12px; + font-size: 13px; + color: #6c757d; + display: flex; + justify-content: space-between; + gap: 10px; +} + +.storage-meta .meta-path strong { + color: #333; +} } .wizard-step.active { diff --git a/themes/2025/admin/css/users.css b/themes/2025/admin/css/users.css index 1bfba8f..078c517 100644 --- a/themes/2025/admin/css/users.css +++ b/themes/2025/admin/css/users.css @@ -1015,4 +1015,110 @@ .user-detail-grid { grid-template-columns: 1fr; } -} \ No newline at end of file +} + +/* Begin: 强制桌面端布局 - 这是从 admin/index.html 的内联样式移动到单独css文件的内容 */ +@media screen { + .container { + max-width: none !important; + width: 95vw !important; + margin: 0 auto !important; + } + + .stats-grid { + display: grid !important; + grid-template-columns: repeat(4, 1fr) !important; + gap: 20px !important; + } + + .mobile-menu-toggle { + display: none !important; + } + + body { + min-width: 1200px !important; + } + /* 为用户过滤器增加简单样式,防止无样式显示 */ + .user-filters { + display: flex; + gap: 16px; + align-items: center; + margin: 12px 0 20px 0; + flex-wrap: wrap; + } + .user-filters .filter-group { + display: flex; + gap: 8px; + align-items: center; + } + .user-filters label { + font-weight: 500; + color: #495057; + margin-right: 6px; + } + + /* 用户工具栏样式(搜索、操作按钮区域) */ + .user-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin: 12px 0; + flex-wrap: wrap; + } + .search-section { + flex: 1 1 480px; /* 允许收缩但优先占据空间 */ + } + .search-box { + display: flex; + gap: 8px; + align-items: center; + } + .search-input { + padding: 8px 12px; + border: 1px solid #e9ecef; + border-radius: 6px; + min-width: 280px; + box-shadow: inset 0 1px 2px rgba(0,0,0,0.03); + } + + .action-section { + display: flex; + gap: 8px; + align-items: center; + } + .btn { + padding: 8px 12px; + border-radius: 6px; + cursor: pointer; + border: none; + background: #f5f7fa; + color: #333; + font-weight: 500; + } + .btn:hover { opacity: 0.95; } + .btn-secondary { + background: #ffffff; + border: 1px solid #e9ecef; + } + + /* 简单的下拉菜单样式(批量操作) */ + .dropdown { position: relative; } + .dropdown-menu { + position: absolute; + right: 0; + top: 100%; + background: #ffffff; + border: 1px solid #e9ecef; + box-shadow: 0 6px 12px rgba(0,0,0,0.08); + display: none; + min-width: 180px; + border-radius: 6px; + z-index: 1000; + overflow: hidden; + } + .dropdown-menu a { display: block; padding: 8px 12px; color: #333; text-decoration: none; } + .dropdown-menu a:hover { background: #f8f9fa; } + .dropdown.open .dropdown-menu { display: block; } +} +/* End: 强制桌面端布局 */ \ No newline at end of file diff --git a/themes/2025/admin/index.html b/themes/2025/admin/index.html index 2899883..f4dd673 100644 --- a/themes/2025/admin/index.html +++ b/themes/2025/admin/index.html @@ -23,31 +23,7 @@ - - + @@ -129,7 +105,7 @@

系统仪表板

总文件数
- +12% +
@@ -142,7 +118,7 @@

系统仪表板

今日上传
- +8% +
@@ -155,7 +131,7 @@

系统仪表板

活跃用户
- +5% +
@@ -168,7 +144,7 @@

系统仪表板

总存储大小
- +15% +
diff --git a/themes/2025/admin/js/config-simple.js b/themes/2025/admin/js/config-simple.js index 5b553dd..d8d4004 100644 --- a/themes/2025/admin/js/config-simple.js +++ b/themes/2025/admin/js/config-simple.js @@ -69,7 +69,8 @@ function fillConfigForm(config) { setFieldValue('themes_select', config.themes_select); // 用户系统设置 (始终启用) - setCheckboxValue('allow_user_registration', config.user?.allow_user_registration); + // config.user.allow_user_registration 可能为 0/1,setCheckboxValue 接受布尔化 + setCheckboxValue('allow_user_registration', config.user?.allow_user_registration); setCheckboxValue('require_email_verify', config.user?.require_email_verify); setFieldValue('user_storage_quota_mb', bytesToMB(config.user?.user_storage_quota || 0)); setFieldValue('user_upload_size_mb', bytesToMB(config.user?.user_upload_size || 0)); diff --git a/themes/2025/admin/js/main.js b/themes/2025/admin/js/main.js index 290b84d..452e719 100644 --- a/themes/2025/admin/js/main.js +++ b/themes/2025/admin/js/main.js @@ -384,6 +384,43 @@ async function loadStats() { if (dashboardActiveUsersEl) dashboardActiveUsersEl.textContent = stats.active_files || 0; // 临时使用active_files作为活跃用户数 if (dashboardTotalStorageEl) dashboardTotalStorageEl.textContent = formatFileSize(stats.total_size || 0); + // 更新趋势百分比(如果后端提供) + const filesTrendEl = document.getElementById('files-trend'); + const uploadsTrendEl = document.getElementById('uploads-trend'); + const usersTrendEl = document.getElementById('users-trend'); + const storageTrendEl = document.getElementById('storage-trend'); + + if (filesTrendEl) { + if (stats.files_change_percent !== undefined && stats.files_change_percent !== null) { + filesTrendEl.textContent = (stats.files_change_percent > 0 ? '+' : '') + stats.files_change_percent + '%'; + } else { + filesTrendEl.textContent = '—'; + } + } + + if (uploadsTrendEl) { + if (stats.uploads_change_percent !== undefined && stats.uploads_change_percent !== null) { + uploadsTrendEl.textContent = (stats.uploads_change_percent > 0 ? '+' : '') + stats.uploads_change_percent + '%'; + } else { + uploadsTrendEl.textContent = '—'; + } + } + + if (usersTrendEl) { + if (stats.users_change_percent !== undefined && stats.users_change_percent !== null) { + usersTrendEl.textContent = (stats.users_change_percent > 0 ? '+' : '') + stats.users_change_percent + '%'; + } else { + usersTrendEl.textContent = '—'; + } + } + + if (storageTrendEl) { + if (stats.storage_change_percent !== undefined && stats.storage_change_percent !== null) { + storageTrendEl.textContent = (stats.storage_change_percent > 0 ? '+' : '') + stats.storage_change_percent + '%'; + } else { + storageTrendEl.textContent = '—'; + } + } // 更新存储使用率(如果API提供了相关数据) const storageUsageEl = document.getElementById('storage-usage'); if (storageUsageEl && stats.storage_usage_percent) { diff --git a/themes/2025/admin/js/storage-simple.js b/themes/2025/admin/js/storage-simple.js index 810dcf7..5c087fc 100644 --- a/themes/2025/admin/js/storage-simple.js +++ b/themes/2025/admin/js/storage-simple.js @@ -74,36 +74,46 @@ function displayStorageInfo(data) { function updateCurrentStorageDisplay(data) { const currentStorageContainer = document.getElementById('current-storage-display'); if (!currentStorageContainer) return; - - const currentType = data.current; + const currentType = data && data.current ? data.current : null; + const storageDetails = data && data.storage_details ? data.storage_details : {}; + const detail = currentType ? (storageDetails[currentType] || {}) : {}; + const typeNames = { 'local': '本地存储', 'webdav': 'WebDAV存储', 'nfs': 'NFS网络存储', 's3': 'S3对象存储' }; - + + const available = Boolean(detail.available); + const usage = detail.usage_percent !== undefined ? detail.usage_percent : null; + const storagePath = detail.storage_path || detail.path || ''; + const html = `

当前存储

- ${typeNames[currentType] || currentType} - - - ${data.storage_details[currentType]?.available ? '正常' : '异常'} + ${typeNames[currentType] || (currentType || '未配置')} + + + ${available ? '正常' : '异常'}
- ${!data.storage_details[currentType]?.available ? ` -
- - ${data.storage_details[currentType]?.error || '存储连接异常'} -
- ` : ''} +
+
存储路径: ${storagePath || '未配置'}
+ ${usage !== null ? `
使用率: ${usage}%
` : ''} + ${!available && (detail.error || '') ? ` +
+ + ${detail.error || '存储连接异常'} +
+ ` : ''} +
`; - + currentStorageContainer.innerHTML = html; } @@ -111,41 +121,42 @@ function updateCurrentStorageDisplay(data) { * 更新存储卡片 */ function updateStorageCards(data) { - const currentType = data.current; - const storageDetails = data.storage_details; - + const currentType = data && data.current ? data.current : null; + const storageDetails = data && data.storage_details ? data.storage_details : {}; + // 更新每个存储卡片的状态 Object.keys(storageDetails).forEach(type => { const card = document.getElementById(`${type}-storage-card`); if (!card) return; - - const detail = storageDetails[type]; - + + const detail = storageDetails[type] || {}; + // 移除所有状态类 card.classList.remove('current-storage', 'storage-available', 'storage-unavailable'); - + // 添加当前状态类 if (type === currentType) { card.classList.add('current-storage'); } - + if (detail.available) { card.classList.add('storage-available'); } else { card.classList.add('storage-unavailable'); } - + // 更新状态徽章 const statusBadge = card.querySelector('.storage-status-badge'); if (statusBadge) { - statusBadge.className = `storage-status-badge status-${detail.available ? 'success' : 'error'}`; + const available = Boolean(detail.available); + statusBadge.className = `storage-status-badge ${available ? 'status-success' : 'status-error'}`; statusBadge.innerHTML = ` - - ${detail.available ? '可用' : '不可用'} + + ${available ? '可用' : '不可用'} `; } - - // 更新错误信息 + + // 更新错误信息与显示路径/usage const errorDisplay = card.querySelector('.storage-error-display'); if (errorDisplay) { if (!detail.available && detail.error) { @@ -155,6 +166,18 @@ function updateStorageCards(data) { errorDisplay.style.display = 'none'; } } + + // 在卡片底部显示路径和使用率(防御性) + let metaEl = card.querySelector('.storage-meta'); + if (!metaEl) { + metaEl = document.createElement('div'); + metaEl.className = 'storage-meta'; + card.appendChild(metaEl); + } + + const pathText = detail.storage_path || detail.path || '未配置'; + const usageText = detail.usage_percent !== undefined ? `${detail.usage_percent}% 已用` : ''; + metaEl.innerHTML = `
路径: ${pathText}
${usageText ? `
${usageText}
` : ''}`; }); } diff --git a/themes/2025/admin/js/users.js b/themes/2025/admin/js/users.js index 9a11f4b..1157095 100644 --- a/themes/2025/admin/js/users.js +++ b/themes/2025/admin/js/users.js @@ -1013,37 +1013,47 @@ function formatRelativeTime(dateString) { return `${Math.floor(diffDays / 365)}年前`; } -// 事件监听器 -document.addEventListener('click', function(e) { - // 点击模态框外部关闭 - if (e.target.classList.contains('modal')) { - closeUserModal(); - } - - // 点击下拉菜单外部关闭 - if (!e.target.closest('.dropdown')) { - const menus = document.querySelectorAll('.dropdown-menu'); - menus.forEach(menu => menu.classList.remove('show')); - } - - // 更新复选框状态 - if (e.target.classList.contains('user-checkbox')) { - updateSelectAllState(); +// 事件监听器(通过 app.addGlobalListener 注册以便统一管理) +;(function() { + function onDocumentClick(e) { + // 点击模态框外部关闭 + if (e.target.classList.contains('modal')) { + closeUserModal(); + } + + // 点击下拉菜单外部关闭 + if (!e.target.closest('.dropdown')) { + const menus = document.querySelectorAll('.dropdown-menu'); + menus.forEach(menu => menu.classList.remove('show')); + } + + // 更新复选框状态 + if (e.target.classList.contains('user-checkbox')) { + updateSelectAllState(); + } } -}); -// 键盘事件监听 -document.addEventListener('keydown', function(e) { - // ESC键关闭模态框 - if (e.key === 'Escape') { - closeUserModal(); + function onDocumentKeydown(e) { + // ESC键关闭模态框 + if (e.key === 'Escape') { + closeUserModal(); + } + + // 回车键搜索 + if (e.key === 'Enter' && e.target.id === 'user-search-input') { + searchUsers(); + } } - - // 回车键搜索 - if (e.key === 'Enter' && e.target.id === 'user-search-input') { - searchUsers(); + + if (window.app && typeof window.app.addGlobalListener === 'function') { + window.app.addGlobalListener(document, 'click', onDocumentClick); + window.app.addGlobalListener(document, 'keydown', onDocumentKeydown); + } else { + // 回退:直接注册监听器 + document.addEventListener('click', onDocumentClick); + document.addEventListener('keydown', onDocumentKeydown); } -}); +})(); // 将函数暴露到全局作用域 window.initUserInterface = initUserInterface; diff --git a/themes/2025/js/auth.js b/themes/2025/js/auth.js index 3700595..eac4c51 100644 --- a/themes/2025/js/auth.js +++ b/themes/2025/js/auth.js @@ -166,7 +166,8 @@ const UserSystem = { console.log('用户系统已启用'); // 检查是否允许注册,动态显示注册链接 - const allowRegistration = result.data.allow_user_registration; + // 后端使用 0/1 表示开关,严格比较为 1 + const allowRegistration = result.data.allow_user_registration === 1; const guestLinks = document.getElementById('guest-links'); if (guestLinks) { diff --git a/themes/2025/js/main.js b/themes/2025/js/main.js index f8967b5..50bebcc 100644 --- a/themes/2025/js/main.js +++ b/themes/2025/js/main.js @@ -17,6 +17,29 @@ class FileCodeBoxApp { constructor() { this.modules = []; this.eventListeners = []; + // AbortController 用于统一管理并移除通过 signal 注册的事件监听器 + this.abortController = new AbortController(); + } + + /** + * 统一注册全局事件监听器,优先使用 AbortController.signal(可统一取消), + * 不支持时退回到手动记录并在 destroy 时移除。 + */ + addGlobalListener(element, event, handler, options) { + try { + if (this.abortController && this.abortController.signal) { + // 合并 options,确保 signal 被传入 + const opts = Object.assign({}, options || {}, { signal: this.abortController.signal }); + element.addEventListener(event, handler, opts); + return; + } + } catch (err) { + // 有些浏览器可能不支持 signal 参数,我们将回退到手动管理 + } + + // 回退:手动注册并记录以便在 destroy 中移除 + element.addEventListener(event, handler, options); + this.eventListeners.push({ element, event, handler }); } /** @@ -106,8 +129,8 @@ class FileCodeBoxApp { * 设置全局事件监听器 */ setupGlobalEvents() { - // 页面可见性变化 - document.addEventListener('visibilitychange', () => { + // 页面可见性变化(使用统一注册函数以便回退处理) + this.addGlobalListener(document, 'visibilitychange', () => { if (document.visibilityState === 'visible') { // 页面变为可见时,检查用户状态 if (UserAuth.isLoggedIn()) { @@ -117,24 +140,55 @@ class FileCodeBoxApp { }); // 窗口大小变化 - window.addEventListener('resize', debounce(() => { + this.addGlobalListener(window, 'resize', debounce(() => { this.handleResize(); }, 250)); // 在线/离线状态 - window.addEventListener('online', () => { + this.addGlobalListener(window, 'online', () => { showNotification('网络连接已恢复', 'success'); }); - - window.addEventListener('offline', () => { + + this.addGlobalListener(window, 'offline', () => { showNotification('网络连接已断开', 'warning'); }); // 键盘快捷键 - document.addEventListener('keydown', (e) => { + this.addGlobalListener(document, 'keydown', (e) => { this.handleKeyboard(e); }); - + // 诊断:捕获所有锚点点击,记录可能阻止导航的事件 + const logAnchorClicks = function(e) { + try { + const target = e.target; + if (!target) return; + // 找到最近的锚点元素 + const anchor = target.closest && target.closest('a'); + if (anchor && anchor.href) { + // 只关注以 /user/ 开头或指向站内的链接 + try { + const url = new URL(anchor.href, window.location.origin); + if (url.pathname.startsWith('/user')) { + console.log('[diag] anchor click', { + href: anchor.href, + pathname: url.pathname, + defaultPrevented: e.defaultPrevented, + button: e.button, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey + }); + } + } catch (err) { + console.log('[diag] anchor click (raw)', anchor.href, 'error parsing URL', err); + } + } + } catch (err) { + console.error('[diag] logAnchorClicks error', err); + } + }; + + this.addGlobalListener(document, 'click', logAnchorClicks); + console.log('全局事件监听器已设置'); } @@ -288,14 +342,14 @@ class FileCodeBoxApp { */ setupGlobalErrorHandling() { // 捕获未处理的Promise错误 - window.addEventListener('unhandledrejection', (event) => { + this.addGlobalListener(window, 'unhandledrejection', (event) => { console.error('未处理的Promise错误:', event.reason); showNotification('发生了未知错误', 'error'); event.preventDefault(); }); // 捕获JavaScript错误 - window.addEventListener('error', (event) => { + this.addGlobalListener(window, 'error', (event) => { console.error('JavaScript错误:', event.error); // 只在开发模式下显示详细错误 if (window.location.hostname === 'localhost') { @@ -308,10 +362,17 @@ class FileCodeBoxApp { * 销毁应用程序 */ destroy() { - // 清理事件监听器 - this.eventListeners.forEach(({ element, event, handler }) => { - element.removeEventListener(event, handler); - }); + // 使用 AbortController 统一取消所有通过 signal 注册的监听器 + try { + if (this.abortController) { + this.abortController.abort(); + } + } catch (err) { + console.warn('AbortController abort 失败:', err); + } + + // 如果还存在以手动方式记录的监听器,可选地清理数组(兼容历史代码) + this.eventListeners = []; // 重置状态 AppState.initialized = false; diff --git a/themes/2025/login.html b/themes/2025/login.html index 98f8e8f..e3be9ee 100644 --- a/themes/2025/login.html +++ b/themes/2025/login.html @@ -87,7 +87,7 @@ // 用户系统始终启用,直接显示登录界面 loginWrapper.classList.remove('hidden-content'); - if (result.code === 200 && result.data && result.data.allow_user_registration) { + if (result.code === 200 && result.data && result.data.allow_user_registration === 1) { // 允许注册,显示注册区域 registerSection.classList.remove('hidden-content'); console.log('用户注册已启用'); diff --git a/themes/2025/register.html b/themes/2025/register.html index 93dac3d..caa2578 100644 --- a/themes/2025/register.html +++ b/themes/2025/register.html @@ -97,7 +97,8 @@ systemInfo = data.data; // 用户系统始终可用(系统已初始化),只检查是否允许注册 - if (!systemInfo.allow_user_registration) { + // 后端返回 0/1 表示开关,使用严格比较 + if (systemInfo.allow_user_registration !== 1) { showAlert('当前不允许用户注册', 'error'); document.getElementById('registerForm').style.display = 'none'; return; @@ -320,7 +321,8 @@ const btnText = registerBtn.querySelector('.btn-text'); // 检查系统是否允许注册 - if (systemInfo && !systemInfo.allow_user_registration) { + // 使用严格比较,后端返回 0/1 + if (systemInfo && systemInfo.allow_user_registration !== 1) { showAlert('当前不允许用户注册', 'error'); return; } diff --git a/themes/2025/setup.html b/themes/2025/setup.html index 19f002e..64af942 100644 --- a/themes/2025/setup.html +++ b/themes/2025/setup.html @@ -242,13 +242,35 @@ placeholder="系统管理令牌,用于管理后台登录" required> -
- - - +
+
+ + +
+
+ + +
+
+
+
+ + + +
+
+ + + +
@@ -298,6 +320,12 @@
+ +
@@ -324,6 +352,25 @@ // 设置表单提交处理 document.getElementById('setupForm').addEventListener('submit', handleSetup); + // 存储类型变更显示本地存储配置 + const storageType = document.getElementById('storageType'); + storageType && storageType.addEventListener('change', function() { + const localCfg = document.getElementById('localStorageConfig'); + if (this.value === 'local') { + localCfg.classList.remove('hidden'); + } else { + localCfg.classList.add('hidden'); + } + }); + // 页面初次加载时,根据当前选择的 storageType 设置本地存储配置的可见性(修复未切换时不显示的问题) + if (storageType) { + const localCfg = document.getElementById('localStorageConfig'); + if (storageType.value === 'local') { + localCfg.classList.remove('hidden'); + } else { + localCfg.classList.add('hidden'); + } + } } function setupEventListeners() { @@ -496,6 +543,26 @@ try { const formData = new FormData(e.target); const setupData = Object.fromEntries(formData.entries()); + + // 密码确认校验 + if (setupData.admin_password && setupData.admin_password_confirm) { + if (setupData.admin_password !== setupData.admin_password_confirm) { + showAlert('两次输入的管理员密码不一致', 'error'); + return; + } + if (setupData.admin_password.length < 6) { + showAlert('管理员密码长度至少6个字符', 'error'); + return; + } + } + + // 如果使用本地存储,确保 storage_path 字段存在 + if (setupData.storage_type === 'local') { + if (!setupData.storage_path || setupData.storage_path.trim() === '') { + showAlert('请选择本地存储路径', 'error'); + return; + } + } const response = await fetch('/setup', { method: 'POST', From f1a299417c2685d306c76d4a374f7ebb23d3f399 Mon Sep 17 00:00:00 2001 From: murphyyi Date: Tue, 16 Sep 2025 15:58:05 +0800 Subject: [PATCH 02/21] refactor(config): remove DB-based config functions and update reload/save behavior; replace LoadFromDatabase calls --- internal/config/manager.go | 35 +++++++---------------------------- internal/handlers/admin.go | 4 ++-- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/internal/config/manager.go b/internal/config/manager.go index 86373c1..aa733ca 100644 --- a/internal/config/manager.go +++ b/internal/config/manager.go @@ -163,12 +163,6 @@ func (cm *ConfigManager) LoadFromYAML(path string) error { // InitWithDB has been removed. Use SetDB(db) to inject a database connection. -func (cm *ConfigManager) InitDefaultDataInDB() error { - // No-op: database initialization for config is intentionally disabled. - // Configuration should be provided via config.yaml or environment variables. - return nil -} - // buildConfigMap flattens modules to module_field keys func (cm *ConfigManager) buildConfigMap() map[string]string { out := make(map[string]string) @@ -202,31 +196,15 @@ func (cm *ConfigManager) buildConfigMap() map[string]string { return out } -func (cm *ConfigManager) saveToDatabase() error { - // No-op: saving configuration to DB is disabled under the "YAML-first" policy. - return nil -} - -// LoadFromDatabase previously supported per-row compatibility formats. -// That behavior has been removed: configuration should come from `config.yaml` or environment variables. -// LoadFromDatabase is now a no-op to preserve caller compatibility. -func (cm *ConfigManager) LoadFromDatabase() error { - // No per-row DB compatibility parsing. Return nil as no-op. - return nil -} - func (cm *ConfigManager) ReloadConfig() error { - if cm.db == nil { - return errors.New("数据库连接未设置") - } + // ReloadConfig no longer reads configuration from the database. + // Configuration should be provided via `config.yaml` and environment variables. + // Preserve in-memory immutable fields across reload (port/admin token). curPort := cm.Base.Port curAdmin := cm.AdminToken - if err := cm.LoadFromDatabase(); err != nil { - return err - } + cm.applyEnvironmentOverrides() cm.Base.Port = curPort cm.AdminToken = curAdmin - cm.applyEnvironmentOverrides() return nil } @@ -245,9 +223,10 @@ func (cm *ConfigManager) applyEnvironmentOverrides() { } // Save saves the configuration to the database (if db is set). +// Save persists configuration. Persisting to DB is intentionally removed; +// this method returns an error to surface that saving is unsupported. func (cm *ConfigManager) Save() error { - // No-op save to DB - return nil + return errors.New("saving configuration to database is not supported; use config.yaml and environment variables") } // Get helpers diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go index d540c9a..63f3142 100644 --- a/internal/handlers/admin.go +++ b/internal/handlers/admin.go @@ -1045,8 +1045,8 @@ func (h *AdminHandler) UpdateMCPConfig(c *gin.Context) { return } - // 重新加载配置 - err = h.config.LoadFromDatabase() + // 重新加载配置(从 config.yaml 与环境变量) + err = h.config.ReloadConfig() if err != nil { common.InternalServerErrorResponse(c, "重新加载配置失败: "+err.Error()) return From f413a46bfdb9db98685111754958b208c1f5e564 Mon Sep 17 00:00:00 2001 From: murphyyi Date: Tue, 16 Sep 2025 16:12:57 +0800 Subject: [PATCH 03/21] refactor(repo): remove KeyValue DAO (migrated to config.yaml) --- internal/config/manager.go | 56 +++++++++++++++ internal/repository/key_value.go | 115 ------------------------------ internal/repository/manager.go | 2 - internal/services/admin/config.go | 2 +- internal/services/admin/stats.go | 8 +-- 5 files changed, 60 insertions(+), 123 deletions(-) delete mode 100644 internal/repository/key_value.go diff --git a/internal/config/manager.go b/internal/config/manager.go index aa733ca..a55e42e 100644 --- a/internal/config/manager.go +++ b/internal/config/manager.go @@ -45,6 +45,8 @@ type ConfigManager struct { // yamlManagedKeys stores flat keys (module_field or single keys) that are managed by YAML // and must not be overwritten by DB nor written back to DB. yamlManagedKeys map[string]bool + // Extra stores arbitrary key/value pairs migrated from the old KeyValue table. + Extra map[string]string `yaml:"extra" json:"extra"` } func NewConfigManager() *ConfigManager { @@ -208,6 +210,60 @@ func (cm *ConfigManager) ReloadConfig() error { return nil } +// initExtra ensures Extra map is initialized. +func (cm *ConfigManager) initExtra() { + if cm.Extra == nil { + cm.Extra = make(map[string]string) + } +} + +// UpdateKeyValue updates an arbitrary key/value and persists it to the YAML config file. +func (cm *ConfigManager) UpdateKeyValue(key, value string) error { + cm.initExtra() + cm.Extra[key] = value + return cm.Persist() +} + +// GetKeyValue returns an extra key value and whether it exists. +func (cm *ConfigManager) GetKeyValue(key string) (string, bool) { + if cm.Extra == nil { + return "", false + } + v, ok := cm.Extra[key] + return v, ok +} + +// Persist writes the current `extra` mapping back to the YAML config file (CONFIG_PATH or ./config.yaml). +// It will preserve other top-level keys in the existing YAML file. +func (cm *ConfigManager) Persist() error { + path := os.Getenv("CONFIG_PATH") + if path == "" { + path = "./config.yaml" + } + + // Load existing YAML into a generic map (if present) + doc := make(map[string]any) + if b, err := os.ReadFile(path); err == nil { + var tmp map[string]any + if err := yaml.Unmarshal(b, &tmp); err == nil && tmp != nil { + doc = tmp + } + } + + // Convert Extra to map[string]any for marshaling + extraAny := make(map[string]any) + for k, v := range cm.Extra { + extraAny[k] = v + } + doc["extra"] = extraAny + + out, err := yaml.Marshal(doc) + if err != nil { + return err + } + return os.WriteFile(path, out, 0o644) +} + func (cm *ConfigManager) applyEnvironmentOverrides() { if p := os.Getenv("PORT"); p != "" { if n, err := strconv.Atoi(p); err == nil { diff --git a/internal/repository/key_value.go b/internal/repository/key_value.go deleted file mode 100644 index f4334fc..0000000 --- a/internal/repository/key_value.go +++ /dev/null @@ -1,115 +0,0 @@ -package repository - -import ( - "github.com/zy84338719/filecodebox/internal/models" - "gorm.io/gorm" -) - -// KeyValueDAO 键值对数据访问对象 -type KeyValueDAO struct { - db *gorm.DB -} - -// NewKeyValueDAO 创建新的键值对DAO -func NewKeyValueDAO(db *gorm.DB) *KeyValueDAO { - return &KeyValueDAO{db: db} -} - -// Create 创建新的键值对 -func (dao *KeyValueDAO) Create(kv *models.KeyValue) error { - return dao.db.Create(kv).Error -} - -// GetByKey 根据键获取值 -func (dao *KeyValueDAO) GetByKey(key string) (*models.KeyValue, error) { - var kv models.KeyValue - err := dao.db.Where("key = ?", key).First(&kv).Error - if err != nil { - return nil, err - } - return &kv, nil -} - -// Update 更新键值对 -func (dao *KeyValueDAO) Update(kv *models.KeyValue) error { - return dao.db.Save(kv).Error -} - -// Delete 删除键值对 -func (dao *KeyValueDAO) Delete(key string) error { - return dao.db.Where("key = ?", key).Delete(&models.KeyValue{}).Error -} - -// GetAll 获取所有键值对 -func (dao *KeyValueDAO) GetAll() ([]models.KeyValue, error) { - var kvs []models.KeyValue - err := dao.db.Find(&kvs).Error - return kvs, err -} - -// GetByKeys 根据多个键获取值 -func (dao *KeyValueDAO) GetByKeys(keys []string) ([]models.KeyValue, error) { - var kvs []models.KeyValue - err := dao.db.Where("key IN ?", keys).Find(&kvs).Error - return kvs, err -} - -// SetValue 设置键值对(如果存在则更新,不存在则创建) -func (dao *KeyValueDAO) SetValue(key, value string) error { - var kv models.KeyValue - err := dao.db.Where("key = ?", key).First(&kv).Error - - if err == gorm.ErrRecordNotFound { - // 不存在,创建新记录 - kv = models.KeyValue{Key: key, Value: value} - return dao.db.Create(&kv).Error - } else if err != nil { - return err - } - - // 存在,更新值 - kv.Value = value - return dao.db.Save(&kv).Error -} - -// BatchSet 批量设置键值对 -func (dao *KeyValueDAO) BatchSet(kvMap map[string]string) error { - return dao.db.Transaction(func(tx *gorm.DB) error { - for key, value := range kvMap { - var kv models.KeyValue - err := tx.Where("key = ?", key).First(&kv).Error - - if err == gorm.ErrRecordNotFound { - // 不存在,创建新记录 - kv = models.KeyValue{Key: key, Value: value} - if err := tx.Create(&kv).Error; err != nil { - return err - } - } else if err != nil { - return err - } else { - // 存在,更新值 - kv.Value = value - if err := tx.Save(&kv).Error; err != nil { - return err - } - } - } - return nil - }) -} - -// Search 搜索键值对 -func (dao *KeyValueDAO) Search(searchKey string) ([]models.KeyValue, error) { - var kvs []models.KeyValue - searchPattern := "%" + searchKey + "%" - err := dao.db.Where("key LIKE ? OR value LIKE ?", searchPattern, searchPattern).Find(&kvs).Error - return kvs, err -} - -// Count 统计键值对数量 -func (dao *KeyValueDAO) Count() (int64, error) { - var count int64 - err := dao.db.Model(&models.KeyValue{}).Count(&count).Error - return count, err -} diff --git a/internal/repository/manager.go b/internal/repository/manager.go index 10d4b33..33180d0 100644 --- a/internal/repository/manager.go +++ b/internal/repository/manager.go @@ -12,7 +12,6 @@ type RepositoryManager struct { Chunk *ChunkDAO UserSession *UserSessionDAO Upload *ChunkDAO - KeyValue *KeyValueDAO } // NewRepositoryManager 创建新的数据访问管理器 @@ -24,7 +23,6 @@ func NewRepositoryManager(db *gorm.DB) *RepositoryManager { Chunk: NewChunkDAO(db), UserSession: NewUserSessionDAO(db), Upload: NewChunkDAO(db), // 别名 - KeyValue: NewKeyValueDAO(db), } } diff --git a/internal/services/admin/config.go b/internal/services/admin/config.go index c2244ba..ced7c36 100644 --- a/internal/services/admin/config.go +++ b/internal/services/admin/config.go @@ -447,7 +447,7 @@ func (s *Service) SaveConfigUpdate(configUpdate *models.ConfigUpdateFields) erro valueStr = string(jsonBytes) } - if err := s.repositoryManager.KeyValue.SetValue(key, valueStr); err != nil { + if err := s.manager.UpdateKeyValue(key, valueStr); err != nil { return fmt.Errorf("保存配置失败: %w", err) } } diff --git a/internal/services/admin/stats.go b/internal/services/admin/stats.go index af3df48..7713cac 100644 --- a/internal/services/admin/stats.go +++ b/internal/services/admin/stats.go @@ -63,14 +63,12 @@ func (s *Service) GetStats() (*web.AdminStatsResponse, error) { stats.TotalSize = totalSize // 系统启动时间 - sysStart, err := s.repositoryManager.KeyValue.GetByKey("sys_start") - if err == nil { - stats.SysStart = sysStart.Value + if v, ok := s.manager.GetKeyValue("sys_start"); ok { + stats.SysStart = v } else { // 如果没有记录,创建一个 startTime := fmt.Sprintf("%d", time.Now().UnixMilli()) - err := s.repositoryManager.KeyValue.SetValue("sys_start", startTime) - if err != nil { + if err := s.manager.UpdateKeyValue("sys_start", startTime); err != nil { return nil, fmt.Errorf("设置系统启动时间失败: %v", err) } stats.SysStart = startTime From ba5f4132bca3f35a2d01dd314f753e6fcd351ffe Mon Sep 17 00:00:00 2001 From: murphyyi Date: Tue, 16 Sep 2025 16:13:23 +0800 Subject: [PATCH 04/21] refactor(models): migrate KeyValue into config.Extra and remove DB model/DAO/migration --- internal/database/database.go | 1 - internal/models/db/keyvalue.go | 12 ------------ internal/models/models.go | 1 - 3 files changed, 14 deletions(-) delete mode 100644 internal/models/db/keyvalue.go diff --git a/internal/database/database.go b/internal/database/database.go index 5e07530..8edcd6f 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -43,7 +43,6 @@ func InitWithManager(manager *config.ConfigManager) (*gorm.DB, error) { err = db.AutoMigrate( &models.FileCode{}, &models.UploadChunk{}, - &models.KeyValue{}, &models.User{}, &models.UserSession{}, ) diff --git a/internal/models/db/keyvalue.go b/internal/models/db/keyvalue.go deleted file mode 100644 index 9bbe981..0000000 --- a/internal/models/db/keyvalue.go +++ /dev/null @@ -1,12 +0,0 @@ -package db - -import ( - "gorm.io/gorm" -) - -// KeyValue 键值对模型 -type KeyValue struct { - gorm.Model - Key string `gorm:"uniqueIndex;size:255" json:"key"` - Value string `gorm:"type:text" json:"value"` -} diff --git a/internal/models/models.go b/internal/models/models.go index e2eb476..ff541b2 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -12,7 +12,6 @@ import ( type ( // 数据库模型别名 FileCode = db.FileCode - UploadChunk = db.UploadChunk KeyValue = db.KeyValue User = db.User UserSession = db.UserSession From 6a64f0d13b431ce8de0e7a41a2d0d95f28ab52a7 Mon Sep 17 00:00:00 2001 From: murphyyi Date: Tue, 16 Sep 2025 16:14:27 +0800 Subject: [PATCH 05/21] feat(config): migrate KeyValue table into config.Extra and remove DB model/DAO; update services to use ConfigManager --- internal/models/models.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/models/models.go b/internal/models/models.go index ff541b2..3f7c2dd 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -12,7 +12,7 @@ import ( type ( // 数据库模型别名 FileCode = db.FileCode - KeyValue = db.KeyValue + UploadChunk = db.UploadChunk User = db.User UserSession = db.UserSession From 360a1334a092d6ab7d8b7dfbc351b545723cb484 Mon Sep 17 00:00:00 2001 From: murphyyi Date: Tue, 16 Sep 2025 16:17:24 +0800 Subject: [PATCH 06/21] chore(config): add initial config.yaml provided by user --- config.yaml | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 config.yaml diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..6f5257b --- /dev/null +++ b/config.yaml @@ -0,0 +1,55 @@ +base: + data_path: /tmp/filecodebox_test + description: 开箱即用的文件快传系统 + host: 0 + name: FileCodeBox + port: 12346 + production: false +database: + host: "" + name: ./data/filecodebox.db + port: 0 + ssl: disable + type: sqlite + user: "" +mcp: + host: 0 + port: 8081 +storage: {} +ui: + admin_token: zhangyi + allow_user_registration: 0 + background: "" + chunk_size: 2097152 + download_timeout: 300 + enable_chunk: 0 + enable_concurrent_download: 1 + enable_mcp_server: 0 + error_count: 1 + error_minute: 1 + file_storage: local + jwt_secret: FileCodeBox2025JWT + keywords: FileCodeBox, 文件快递柜, 口令传送箱, 匿名口令分享文本, 文件 + max_concurrent_downloads: 10 + max_save_seconds: 0 + max_sessions_per_user: 5 + notify_content: "欢迎使用 FileCodeBox,本程序开源于 Github ,欢迎Star和Fork。" + notify_title: 系统通知 + opacity: 0 + open_upload: 1 + page_explain: "请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。" + require_email_verify: 0 + robots_text: |- + User-agent: * + Disallow: / + session_expiry_hours: 168 + show_admin_address: 0 + storage_path: "" + sys_start: 1757914992279 + themes_select: themes/2025 + upload_count: 10 + upload_minute: 1 + upload_size: 100 + user_storage_quota: 1073741824 + user_upload_size: 52428800 +user: {} From 4857993720407f1f84340a9dce11a25814a847cc Mon Sep 17 00:00:00 2001 From: murphyyi Date: Wed, 17 Sep 2025 11:10:45 +0800 Subject: [PATCH 07/21] chore(legacy): move key_value export scripts to scripts/legacy and add migration docs --- docs/KEYVALUE_REMOVAL.md | 30 ++++++ scripts/legacy/README.md | 14 +++ scripts/legacy/export_config_from_db.go | 137 ++++++++++++++++++++++++ scripts/legacy/export_config_from_db.py | 76 +++++++++++++ 4 files changed, 257 insertions(+) create mode 100644 docs/KEYVALUE_REMOVAL.md create mode 100644 scripts/legacy/README.md create mode 100644 scripts/legacy/export_config_from_db.go create mode 100644 scripts/legacy/export_config_from_db.py diff --git a/docs/KEYVALUE_REMOVAL.md b/docs/KEYVALUE_REMOVAL.md new file mode 100644 index 0000000..4b00c3b --- /dev/null +++ b/docs/KEYVALUE_REMOVAL.md @@ -0,0 +1,30 @@ +# KeyValue 表移除说明 + +背景 +---- +FileCodeBox 项目曾使用 `key_values` 表以键值对形式保存运行时以及启动时配置。为简化配置与提高可维护性,项目已切换到 YAML-first 配置(`config.yaml`),并移除对 `key_values` 表的运行时代码依赖。 + +重要变更 +---- +- `ConfigManager` 现在以 `config.yaml` 为主:Env > YAML > Defaults。配置以完整结构化的方式保存在 `config.yaml`。 +- 运行时任意键值存储(`KeyValues`、`GetRuntimeKeyValue`、`SetRuntimeKeyValue` 等)已从代码中删除。请使用 `ConfigManager` 的结构化字段来保存配置信息。 +- `sys_start` 已作为显式字段 `ConfigManager.SysStart` 持久化到 `config.yaml`(如果需要)。 + +迁移旧数据库数据 +---- +若你有旧的数据库(`data/filecodebox.db` 或其它),并希望将 `key_values` 表里的内容迁移到 `config.yaml`,我们提供了遗留脚本: + +- `scripts/legacy/export_config_from_db.py`(Python,依赖 `pyyaml`) +- `scripts/legacy/export_config_from_db.go`(Go,可编译可运行) + +示例(Python): +``` +python3 scripts/legacy/export_config_from_db.py data/filecodebox.db config.generated.yaml +``` + +生成的 `config.generated.yaml` 包含一个 `ui:` 块和基本的 `base`, `database`, `storage`, `user`, `mcp` 部分。请手动审阅并整合到你的 `config.yaml`,然后重启服务。 + +注意 +---- +- 这些脚本为迁移工具,仅作历史用途保留,并不建议在新部署中使用。 +- 一旦确认迁移成功,可安全删除旧数据库中的 `key_values` 表(请在删除前备份数据库)。 diff --git a/scripts/legacy/README.md b/scripts/legacy/README.md new file mode 100644 index 0000000..417e453 --- /dev/null +++ b/scripts/legacy/README.md @@ -0,0 +1,14 @@ +Deprecated scripts for KeyValue export/migration + +These scripts were used to export configuration from the legacy `key_values` database +table into a YAML file. The `key_values` table has been removed from the active code +path; these scripts are kept here for historical migration purposes only and are +not used by the new YAML-first configuration system. + +Files: +- export_config_from_db.py +- export_config_from_db.go + +Do not run these scripts in production without understanding their behavior. They +are provided only as a convenience to migrate an existing database's key_values +into a `config.yaml` that follows the new structured format. diff --git a/scripts/legacy/export_config_from_db.go b/scripts/legacy/export_config_from_db.go new file mode 100644 index 0000000..f5a577e --- /dev/null +++ b/scripts/legacy/export_config_from_db.go @@ -0,0 +1,137 @@ +package main + +import ( + "database/sql" + "flag" + "fmt" + "log" + "os" + + _ "github.com/mattn/go-sqlite3" + "gopkg.in/yaml.v3" +) + +// DEPRECATED: Legacy migration tool to export key_values table to YAML. +func main() { + dbPath := flag.String("db", "data/filecodebox.db", "path to sqlite db") + out := flag.String("out", "config.generated.yaml", "output yaml file") + flag.Parse() + + if _, err := os.Stat(*dbPath); err != nil { + log.Fatalf("db not found: %v", err) + } + + db, err := sql.Open("sqlite3", *dbPath) + if err != nil { + log.Fatalf("open db: %v", err) + } + defer db.Close() + + r, err := db.Query("SELECT key, value FROM key_values") + if err != nil { + log.Fatalf("query: %v", err) + } + defer r.Close() + + cfg := make(map[string]map[string]interface{}) + cfg["base"] = map[string]interface{}{} + cfg["database"] = map[string]interface{}{} + cfg["storage"] = map[string]interface{}{} + cfg["user"] = map[string]interface{}{} + cfg["mcp"] = map[string]interface{}{} + cfg["ui"] = map[string]interface{}{} + + for r.Next() { + var k, v string + if err := r.Scan(&k, &v); err != nil { + log.Printf("scan err: %v", err) + continue + } + + if k == "name" || k == "description" || k == "host" || k == "port" || k == "data_path" || k == "production" { + cfg["base"][k] = parseValue(v) + continue + } + if len(k) >= 9 && k[:9] == "database_" { + sub := k[9:] + cfg["database"][sub] = parseValue(v) + continue + } + if len(k) >= 6 && k[:5] == "user_" { + sub := k[5:] + cfg["user"][sub] = parseValue(v) + continue + } + if len(k) >= 4 && k[:4] == "mcp_" { + sub := k[4:] + cfg["mcp"][sub] = parseValue(v) + continue + } + if len(k) >= 8 && k[:8] == "storage." { + parts := splitN(k, '.', 3) + if len(parts) >= 2 { + section := parts[1] + if len(parts) == 2 { + cfg["storage"][section] = parseValue(v) + } else { + m, ok := cfg["storage"][section].(map[string]interface{}) + if !ok { + m = map[string]interface{}{} + } + m[parts[2]] = parseValue(v) + cfg["storage"][section] = m + } + } + continue + } + cfg["ui"][k] = parseValue(v) + } + + outF, err := os.Create(*out) + if err != nil { + log.Fatalf("create out: %v", err) + } + enc := yaml.NewEncoder(outF) + enc.SetIndent(2) + if err := enc.Encode(cfg); err != nil { + log.Fatalf("encode yaml: %v", err) + } + outF.Close() + fmt.Printf("wrote %s\n", *out) +} + +func splitN(s string, sep byte, n int) []string { + res := []string{} + cur := "" + count := 0 + for i := 0; i < len(s); i++ { + if s[i] == sep { + res = append(res, cur) + cur = "" + count++ + if count >= n-1 { + res = append(res, s[i+1:]) + return res + } + } else { + cur += string(s[i]) + } + } + res = append(res, cur) + return res +} + +func parseValue(v string) interface{} { + var i int + _, err := fmt.Sscanf(v, "%d", &i) + if err == nil { + return i + } + if v == "true" || v == "1" { + return true + } + if v == "false" || v == "0" { + return false + } + return v +} diff --git a/scripts/legacy/export_config_from_db.py b/scripts/legacy/export_config_from_db.py new file mode 100644 index 0000000..4ed5094 --- /dev/null +++ b/scripts/legacy/export_config_from_db.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +DEPRECATED: Legacy export script for migrating configuration from the `key_values` +database table into a YAML file. The new configuration system is YAML-first and +the `key_values` table is deprecated/removed. This file is preserved for +historical migration use only and will not be actively maintained. +Usage: python3 scripts/legacy/export_config_from_db.py [path/to/filecodebox.db] [output.yaml] +""" +import sqlite3 +import sys +import yaml +from pathlib import Path + +DB_PATH = sys.argv[1] if len(sys.argv) > 1 else 'data/filecodebox.db' +OUT_PATH = sys.argv[2] if len(sys.argv) > 2 else 'config.generated.yaml' + +conn = sqlite3.connect(DB_PATH) +cur = conn.cursor() +cur.execute("SELECT key, value FROM key_values") +rows = cur.fetchall() + +cfg = { + 'base': {}, + 'database': {}, + 'storage': {}, + 'user': {}, + 'mcp': {}, + 'ui': {}, +} + +for k, v in rows: + if k == 'name': cfg['base']['name'] = v + elif k == 'description': cfg['base']['description'] = v + elif k == 'host': cfg['base']['host'] = v + elif k == 'port': + try: + cfg['base']['port'] = int(v) + except: + cfg['base']['port'] = v + elif k == 'data_path': cfg['base']['data_path'] = v + elif k == 'production': cfg['base']['production'] = (v == 'true' or v == '1') + elif k.startswith('database_'): + sub = k.split('database_',1)[1] + cfg['database'][sub] = int(v) if v.isdigit() else v + elif k.startswith('storage.'): + parts = k.split('.') + if len(parts) >= 2: + section = parts[1] + if section not in cfg['storage']: + cfg['storage'][section] = {} + if len(parts) == 2: + cfg['storage'][section] = v + else: + subkey = parts[2] + cfg['storage'][section][subkey] = v + elif k.startswith('user_'): + sub = k.split('user_',1)[1] + try: + cfg['user'][sub] = int(v) + except: + cfg['user'][sub] = (v == 'true' or v == '1') if v in ['0','1','true','false'] else v + elif k.startswith('mcp_'): + sub = k.split('mcp_',1)[1] + try: + cfg['mcp'][sub] = int(v) + except: + cfg['mcp'][sub] = (v == 'true' or v == '1') if v in ['0','1','true','false'] else v + elif k.startswith('theme') or k.startswith('notify') or k.startswith('page_'): + cfg['ui'][k] = v + else: + cfg['ui'][k] = v + +with open(OUT_PATH, 'w') as f: + yaml.safe_dump(cfg, f, default_flow_style=False, sort_keys=False, allow_unicode=True) + +print(f"Wrote {OUT_PATH} from {DB_PATH}") From ac89ea855ede37dbf4b6d8f7d99d164d26935ffe Mon Sep 17 00:00:00 2001 From: murphyyi Date: Wed, 17 Sep 2025 12:23:02 +0800 Subject: [PATCH 08/21] chore(config): remove key_value export top-level scripts; keep legacy copies and add migration docs --- .github/copilot-instructions.md | 2 +- README.md | 2 +- config.generated.yaml | 9 +- config.yaml | 7 +- docs/ADMIN_AUTH_401_FIX_REPORT.md | 4 +- docs/API_SWAGGER_GUIDE.md | 2 +- docs/PR_REMOVE_INITWITHDB.md | 2 +- docs/changelogs/REFACTOR_SUMMARY.md | 2 +- internal/config/base_config.go | 30 --- internal/config/manager.go | 248 +++++++++---------- internal/config/manager_test.go | 12 +- internal/config/storage_strategy.go | 38 --- internal/config/ui_config.go | 54 ++++ internal/handlers/admin.go | 96 +------- internal/handlers/app_state.go | 19 +- internal/handlers/setup.go | 339 +++++--------------------- internal/middleware/middleware.go | 36 --- internal/models/web/admin.go | 1 + internal/models/web/mcp.go | 10 + internal/models/web/user.go | 9 + internal/routes/admin.go | 31 ++- internal/routes/setup.go | 19 +- internal/routes/user.go | 47 ++-- internal/services/admin/auth.go | 41 +++- internal/services/admin/config.go | 86 +++---- internal/services/admin/service.go | 12 +- internal/services/admin/stats.go | 11 +- tests/test_admin.sh | 10 +- tests/test_dao_migration.sh | 22 +- tests/test_storage_management.sh | 36 ++- tests/test_webdav_config.sh | 13 +- themes/2025/admin/index.html | 4 +- themes/2025/admin/js/api.js | 18 +- themes/2025/admin/js/config-simple.js | 11 +- themes/2025/admin/js/users.js | 2 +- themes/2025/setup.html | 8 +- 36 files changed, 529 insertions(+), 764 deletions(-) create mode 100644 internal/config/ui_config.go create mode 100644 internal/models/web/mcp.go diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ce0dc40..4672ae0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -26,7 +26,7 @@ manager.SetDB(db) // 注入数据库连接(配置读取现在以 config.yaml 配置分为多个模块:`BaseConfig`, `DatabaseConfig`, `StorageConfig`, `UserSystemConfig`, `MCPConfig` 支持环境变量优先级覆盖、数据库持久化存储、热重载机制: -- **环境变量优先级**:PORT、ADMIN_TOKEN 等关键配置始终优先使用环境变量 +- **环境变量优先级**:PORT、DATA_PATH 等关键配置始终优先使用环境变量 - **数据库持久化**:配置自动保存到 key_value 表,支持动态更新 - **热重载机制**:通过 ReloadConfig() 方法实现运行时配置更新 - **配置验证**:每个配置模块都有独立的验证方法 diff --git a/README.md b/README.md index e4f665a..3a55b39 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ docker-compose up -d - `name`: 站点名称 - `upload_size`: 最大上传大小 - `file_storage`: 存储类型(local/s3/webdav/onedrive) -- `admin_token`: 管理员访问令牌 +- 管理员认证改为使用管理员用户名/密码登录并通过 `Authorization: Bearer ` 使用 JWT(不再使用静态管理员令牌配置) ## 管理员后台 diff --git a/config.generated.yaml b/config.generated.yaml index 023a595..72e46d3 100644 --- a/config.generated.yaml +++ b/config.generated.yaml @@ -1,7 +1,7 @@ base: data_path: /tmp/filecodebox_test description: 开箱即用的文件快传系统 - host: 0 + host: 0.0.0.0 name: FileCodeBox port: 12346 production: false @@ -13,11 +13,10 @@ database: type: sqlite user: "" mcp: - host: 0 + host: 0.0.0.0 port: 8081 storage: {} ui: - admin_token: zhangyi allow_user_registration: 0 background: "" chunk_size: 2097152 @@ -33,11 +32,11 @@ ui: max_concurrent_downloads: 10 max_save_seconds: 0 max_sessions_per_user: 5 - notify_content: 欢迎使用 FileCodeBox,本程序开源于 Github ,欢迎Star和Fork。 + notify_content: "欢迎使用 FileCodeBox,本程序开源于 Github ,欢迎Star和Fork。" notify_title: 系统通知 opacity: 0 open_upload: 1 - page_explain: 请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。 + page_explain: "请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。" require_email_verify: 0 robots_text: |- User-agent: * diff --git a/config.yaml b/config.yaml index 6f5257b..72e46d3 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ base: data_path: /tmp/filecodebox_test description: 开箱即用的文件快传系统 - host: 0 + host: 0.0.0.0 name: FileCodeBox port: 12346 production: false @@ -13,11 +13,10 @@ database: type: sqlite user: "" mcp: - host: 0 + host: 0.0.0.0 port: 8081 storage: {} ui: - admin_token: zhangyi allow_user_registration: 0 background: "" chunk_size: 2097152 @@ -33,7 +32,7 @@ ui: max_concurrent_downloads: 10 max_save_seconds: 0 max_sessions_per_user: 5 - notify_content: "欢迎使用 FileCodeBox,本程序开源于 Github ,欢迎Star和Fork。" + notify_content: "欢迎使用 FileCodeBox,本程序开源于 Github ,欢迎Star和Fork。" notify_title: 系统通知 opacity: 0 open_upload: 1 diff --git a/docs/ADMIN_AUTH_401_FIX_REPORT.md b/docs/ADMIN_AUTH_401_FIX_REPORT.md index 203f17e..4fffa20 100644 --- a/docs/ADMIN_AUTH_401_FIX_REPORT.md +++ b/docs/ADMIN_AUTH_401_FIX_REPORT.md @@ -64,7 +64,7 @@ combinedAuthMiddleware := func(c *gin.Context) { if tokenParts[1] == cfg.AdminToken { c.Set("is_admin", true) c.Set("role", "admin") - c.Set("auth_type", "admin_token") + c.Set("auth_type", "jwt") c.Next() return } @@ -97,7 +97,7 @@ combinedAuthMiddleware := func(c *gin.Context) { **管理员Token认证**: - `is_admin`: true - `role`: "admin" -- `auth_type`: "admin_token" +- `auth_type`: "jwt" ## 测试验证 diff --git a/docs/API_SWAGGER_GUIDE.md b/docs/API_SWAGGER_GUIDE.md index ff2fd07..00a5cf7 100644 --- a/docs/API_SWAGGER_GUIDE.md +++ b/docs/API_SWAGGER_GUIDE.md @@ -69,7 +69,7 @@ curl http://localhost:12345/api/doc | jq . ### 🔐 认证说明 **管理员认证**: -- 使用 `Authorization: Bearer {admin_token}` 头部 +- 使用 `Authorization: Bearer {JWT}` 头部(管理员请先通过 `/admin/login` 使用用户名/密码获取 JWT) - 通过 `/admin/login` 获取令牌 **用户认证**: diff --git a/docs/PR_REMOVE_INITWITHDB.md b/docs/PR_REMOVE_INITWITHDB.md index dbd4d4c..0b6ef3a 100644 --- a/docs/PR_REMOVE_INITWITHDB.md +++ b/docs/PR_REMOVE_INITWITHDB.md @@ -15,7 +15,7 @@ Key changes Migration guidance ------------------ 1. Provide configuration via `config.yaml` at the repository root or set `CONFIG_PATH` to point to your YAML configuration file. -2. Environment variables take precedence for runtime overrides (`PORT`, `ADMIN_TOKEN`, `DATA_PATH`, etc.). +2. Environment variables take precedence for runtime overrides (`PORT`, `DATA_PATH`, etc.). 3. If you previously relied on DB rows for config, export them to YAML using the included script `scripts/export_config_from_db.go` and place the resulting file as `config.yaml`. Why this change diff --git a/docs/changelogs/REFACTOR_SUMMARY.md b/docs/changelogs/REFACTOR_SUMMARY.md index c2377eb..d81b400 100644 --- a/docs/changelogs/REFACTOR_SUMMARY.md +++ b/docs/changelogs/REFACTOR_SUMMARY.md @@ -13,7 +13,7 @@ #### 2. 配置管理系统完善 - **分层配置架构**:`BaseConfig`, `DatabaseConfig`, `StorageConfig`, `UserSystemConfig`, `MCPConfig` -- **环境变量优先级**:PORT、ADMIN_TOKEN 等关键配置优先使用环境变量 +- **环境变量优先级**:PORT、DATA_PATH 等关键配置优先使用环境变量 - **数据库持久化**:配置自动保存到 key_value 表,支持运行时动态更新 - **热重载机制**:通过 ReloadConfig() 方法实现配置的运行时更新 - **完整验证**:每个配置模块都有独立的验证方法 diff --git a/internal/config/base_config.go b/internal/config/base_config.go index f7fab74..c701bea 100644 --- a/internal/config/base_config.go +++ b/internal/config/base_config.go @@ -4,7 +4,6 @@ package config import ( "fmt" "net" - "strconv" "strings" ) @@ -109,35 +108,6 @@ func (bc *BaseConfig) ToMap() map[string]string { } } -// FromMap 从map加载配置 -func (bc *BaseConfig) FromMap(data map[string]string) error { - if val, ok := data["name"]; ok { - bc.Name = val - } - if val, ok := data["description"]; ok { - bc.Description = val - } - if val, ok := data["keywords"]; ok { - bc.Keywords = val - } - if val, ok := data["port"]; ok { - if port, err := strconv.Atoi(val); err == nil { - bc.Port = port - } - } - if val, ok := data["host"]; ok { - bc.Host = val - } - if val, ok := data["data_path"]; ok { - bc.DataPath = val - } - if val, ok := data["production"]; ok { - bc.Production = val == "true" - } - - return bc.Validate() -} - // Update 更新配置 func (bc *BaseConfig) Update(updates map[string]interface{}) error { if name, ok := updates["name"].(string); ok { diff --git a/internal/config/manager.go b/internal/config/manager.go index a55e42e..a2c3495 100644 --- a/internal/config/manager.go +++ b/internal/config/manager.go @@ -2,7 +2,6 @@ package config import ( "errors" - "fmt" "os" "strconv" @@ -23,9 +22,12 @@ type ConfigManager struct { NotifyTitle string NotifyContent string - AdminToken string - // UI / Theme / Page fields (kept at top-level for backward compatibility with callers) + // UI config grouped under `ui` in config.yaml. We keep top-level + // compatibility fields but prefer `UI` when loading/saving YAML. + UI *UIConfig `yaml:"ui" json:"ui"` + + // UI / Theme / Page top-level compatibility fields ThemesSelect string `yaml:"themes_select" json:"themes_select"` RobotsText string `yaml:"robots_text" json:"robots_text"` PageExplain string `yaml:"page_explain" json:"page_explain"` @@ -33,6 +35,9 @@ type ConfigManager struct { Opacity float64 `yaml:"opacity" json:"opacity"` Background string `yaml:"background" json:"background"` + // Persistent runtime metadata + SysStart string `yaml:"sys_start" json:"sys_start"` + // rate limit / business fields (kept at top-level for backwards compatibility) UploadMinute int `yaml:"upload_minute" json:"upload_minute"` UploadCount int `yaml:"upload_count" json:"upload_count"` @@ -42,11 +47,7 @@ type ConfigManager struct { db *gorm.DB - // yamlManagedKeys stores flat keys (module_field or single keys) that are managed by YAML - // and must not be overwritten by DB nor written back to DB. - yamlManagedKeys map[string]bool - // Extra stores arbitrary key/value pairs migrated from the old KeyValue table. - Extra map[string]string `yaml:"extra" json:"extra"` + // ConfigManager now reads/writes the whole struct directly to YAML. } func NewConfigManager() *ConfigManager { @@ -57,6 +58,7 @@ func NewConfigManager() *ConfigManager { Storage: NewStorageConfig(), User: NewUserSystemConfig(), MCP: NewMCPConfig(), + UI: &UIConfig{}, } } @@ -85,194 +87,157 @@ func (cm *ConfigManager) LoadFromYAML(path string) error { if err := yaml.Unmarshal(b, &fileCfg); err != nil { return err } - if cm.yamlManagedKeys == nil { - cm.yamlManagedKeys = make(map[string]bool) - } if fileCfg.Base != nil { cm.Base = fileCfg.Base - for k := range cm.Base.ToMap() { - cm.yamlManagedKeys[k] = true - } } if fileCfg.Database != nil { cm.Database = fileCfg.Database - for k := range cm.Database.ToMap() { - cm.yamlManagedKeys[k] = true - } } if fileCfg.Transfer != nil { cm.Transfer = fileCfg.Transfer - for k := range cm.Transfer.ToMap() { - cm.yamlManagedKeys[k] = true - } } if fileCfg.Storage != nil { cm.Storage = fileCfg.Storage - for k := range cm.Storage.ToMap() { - cm.yamlManagedKeys[k] = true - } } if fileCfg.User != nil { cm.User = fileCfg.User - for k := range cm.User.ToMap() { - cm.yamlManagedKeys[k] = true - } } if fileCfg.MCP != nil { cm.MCP = fileCfg.MCP - for k := range cm.MCP.ToMap() { - cm.yamlManagedKeys[k] = true - } } if fileCfg.NotifyTitle != "" { cm.NotifyTitle = fileCfg.NotifyTitle - cm.yamlManagedKeys["notify_title"] = true } if fileCfg.NotifyContent != "" { cm.NotifyContent = fileCfg.NotifyContent - cm.yamlManagedKeys["notify_content"] = true - } - if fileCfg.AdminToken != "" { - cm.AdminToken = fileCfg.AdminToken - cm.yamlManagedKeys["admin_token"] = true - } - if fileCfg.ThemesSelect != "" { - cm.ThemesSelect = fileCfg.ThemesSelect - cm.yamlManagedKeys["themes_select"] = true - } - if fileCfg.RobotsText != "" { - cm.RobotsText = fileCfg.RobotsText - cm.yamlManagedKeys["robots_text"] = true } - if fileCfg.PageExplain != "" { - cm.PageExplain = fileCfg.PageExplain - cm.yamlManagedKeys["page_explain"] = true - } - if fileCfg.ShowAdminAddr != 0 { - cm.ShowAdminAddr = fileCfg.ShowAdminAddr - cm.yamlManagedKeys["show_admin_addr"] = true - } - if fileCfg.Opacity != 0 { - cm.Opacity = fileCfg.Opacity - cm.yamlManagedKeys["opacity"] = true + + // Prefer structured UI block if present; otherwise fallback to legacy top-level fields. + if fileCfg.UI != nil { + cm.UI = fileCfg.UI + // sync top-level compatibility fields + cm.ThemesSelect = cm.UI.ThemesSelect + cm.Background = cm.UI.Background + cm.PageExplain = cm.UI.PageExplain + cm.Opacity = cm.UI.Opacity + cm.RobotsText = cm.UI.RobotsText + cm.ShowAdminAddr = cm.UI.ShowAdminAddr + } else { + if fileCfg.ThemesSelect != "" { + cm.ThemesSelect = fileCfg.ThemesSelect + cm.UI.ThemesSelect = fileCfg.ThemesSelect + } + if fileCfg.RobotsText != "" { + cm.RobotsText = fileCfg.RobotsText + cm.UI.RobotsText = fileCfg.RobotsText + } + if fileCfg.PageExplain != "" { + cm.PageExplain = fileCfg.PageExplain + cm.UI.PageExplain = fileCfg.PageExplain + } + if fileCfg.ShowAdminAddr != 0 { + cm.ShowAdminAddr = fileCfg.ShowAdminAddr + cm.UI.ShowAdminAddr = fileCfg.ShowAdminAddr + } + if fileCfg.Opacity != 0 { + cm.Opacity = fileCfg.Opacity + cm.UI.Opacity = fileCfg.Opacity + } + if fileCfg.Background != "" { + cm.Background = fileCfg.Background + cm.UI.Background = fileCfg.Background + } } - if fileCfg.Background != "" { - cm.Background = fileCfg.Background - cm.yamlManagedKeys["background"] = true + + // Persistent runtime metadata + if fileCfg.SysStart != "" { + cm.SysStart = fileCfg.SysStart + } + // no runtime KeyValues persisted here anymore + // Backwards-compat: some configs place UI-related fields under a `ui` map + // (e.g. config.yaml uses `ui: { themes_select: themes/2025 }`). Parse the + // raw YAML and copy `ui.themes_select` into the top-level ThemesSelect when + // present so ServeAdminPage and static file routes resolve correctly. + var raw map[string]any + if err := yaml.Unmarshal(b, &raw); err == nil && raw != nil { + if uiRaw, ok := raw["ui"]; ok { + if uiMap, ok2 := uiRaw.(map[string]any); ok2 { + if ts, ok3 := uiMap["themes_select"].(string); ok3 && ts != "" { + cm.ThemesSelect = ts + } + if bg, ok3 := uiMap["background"].(string); ok3 && bg != "" { + cm.Background = bg + } + if pe, ok3 := uiMap["page_explain"].(string); ok3 && pe != "" { + cm.PageExplain = pe + } + if opacityVal, ok3 := uiMap["opacity"]; ok3 { + // Keep existing numeric parsing in callers; only set when simple types present + switch v := opacityVal.(type) { + case float64: + if v != 0 { + cm.Opacity = v + } + case int: + if float64(v) != 0 { + cm.Opacity = float64(v) + } + } + } + } + } } return nil } // InitWithDB has been removed. Use SetDB(db) to inject a database connection. -// buildConfigMap flattens modules to module_field keys -func (cm *ConfigManager) buildConfigMap() map[string]string { - out := make(map[string]string) - for k, v := range cm.Base.ToMap() { - out[k] = v - } - for k, v := range cm.Database.ToMap() { - out[k] = v - } - for k, v := range cm.Transfer.ToMap() { - out[k] = v - } - for k, v := range cm.Storage.ToMap() { - out[k] = v - } - for k, v := range cm.User.ToMap() { - out[k] = v - } - for k, v := range cm.MCP.ToMap() { - out[k] = v - } - out["notify_title"] = cm.NotifyTitle - out["notify_content"] = cm.NotifyContent - out["admin_token"] = cm.AdminToken - out["themes_select"] = cm.ThemesSelect - out["robots_text"] = cm.RobotsText - out["page_explain"] = cm.PageExplain - out["show_admin_addr"] = fmt.Sprintf("%d", cm.ShowAdminAddr) - out["opacity"] = fmt.Sprintf("%v", cm.Opacity) - out["background"] = cm.Background - return out -} - func (cm *ConfigManager) ReloadConfig() error { // ReloadConfig no longer reads configuration from the database. // Configuration should be provided via `config.yaml` and environment variables. - // Preserve in-memory immutable fields across reload (port/admin token). + // Preserve in-memory immutable fields across reload (port). curPort := cm.Base.Port - curAdmin := cm.AdminToken cm.applyEnvironmentOverrides() cm.Base.Port = curPort - cm.AdminToken = curAdmin return nil } -// initExtra ensures Extra map is initialized. -func (cm *ConfigManager) initExtra() { - if cm.Extra == nil { - cm.Extra = make(map[string]string) - } -} - -// UpdateKeyValue updates an arbitrary key/value and persists it to the YAML config file. -func (cm *ConfigManager) UpdateKeyValue(key, value string) error { - cm.initExtra() - cm.Extra[key] = value - return cm.Persist() -} - -// GetKeyValue returns an extra key value and whether it exists. -func (cm *ConfigManager) GetKeyValue(key string) (string, bool) { - if cm.Extra == nil { - return "", false - } - v, ok := cm.Extra[key] - return v, ok -} - -// Persist writes the current `extra` mapping back to the YAML config file (CONFIG_PATH or ./config.yaml). -// It will preserve other top-level keys in the existing YAML file. -func (cm *ConfigManager) Persist() error { +// PersistYAML writes the current ConfigManager to the YAML config file (CONFIG_PATH or ./config.yaml). +// It serializes the entire struct (yaml tags) and overwrites the file. +func (cm *ConfigManager) PersistYAML() error { path := os.Getenv("CONFIG_PATH") if path == "" { path = "./config.yaml" } - // Load existing YAML into a generic map (if present) - doc := make(map[string]any) - if b, err := os.ReadFile(path); err == nil { - var tmp map[string]any - if err := yaml.Unmarshal(b, &tmp); err == nil && tmp != nil { - doc = tmp - } - } + // Ensure UI block reflects top-level compatibility fields before marshalling + if cm.UI == nil { + cm.UI = &UIConfig{} + } + cm.UI.ThemesSelect = cm.ThemesSelect + cm.UI.Background = cm.Background + cm.UI.PageExplain = cm.PageExplain + cm.UI.Opacity = cm.Opacity + cm.UI.RobotsText = cm.RobotsText + cm.UI.ShowAdminAddr = cm.ShowAdminAddr - // Convert Extra to map[string]any for marshaling - extraAny := make(map[string]any) - for k, v := range cm.Extra { - extraAny[k] = v - } - doc["extra"] = extraAny - - out, err := yaml.Marshal(doc) + out, err := yaml.Marshal(cm) if err != nil { return err } return os.WriteFile(path, out, 0o644) } +// Runtime key/value helpers removed: runtime arbitrary key storage is no longer +// persisted inside `ConfigManager`. Configuration should be updated via the +// structured module Update(...) methods and persisted with PersistYAML(). + func (cm *ConfigManager) applyEnvironmentOverrides() { if p := os.Getenv("PORT"); p != "" { if n, err := strconv.Atoi(p); err == nil { cm.Base.Port = n } } - if t := os.Getenv("ADMIN_TOKEN"); t != "" { - cm.AdminToken = t - } if dp := os.Getenv("DATA_PATH"); dp != "" { cm.Base.DataPath = dp } @@ -300,7 +265,12 @@ func (cm *ConfigManager) Clone() *ConfigManager { nc.MCP = cm.MCP.Clone() nc.NotifyTitle = cm.NotifyTitle nc.NotifyContent = cm.NotifyContent - nc.AdminToken = cm.AdminToken + if cm.UI != nil { + ui := *cm.UI + nc.UI = &ui + } + nc.SysStart = cm.SysStart + // AdminToken removed return nc } diff --git a/internal/config/manager_test.go b/internal/config/manager_test.go index 6735f3c..7df48b6 100644 --- a/internal/config/manager_test.go +++ b/internal/config/manager_test.go @@ -35,17 +35,7 @@ func TestLoadFromYAML(t *testing.T) { if cm.ThemesSelect != "themes/test" { t.Fatalf("expected themes_select themes/test, got %s", cm.ThemesSelect) } - // Ensure at least one of Base.ToMap() keys is present in yamlManagedKeys - found := false - for k := range cm.Base.ToMap() { - if cm.yamlManagedKeys[k] { - found = true - break - } - } - if !found { - t.Fatalf("expected some base.* key to be recorded in yamlManagedKeys, got none") - } + // basic fields loaded } // TestEnvOverride ensures environment variables override YAML values diff --git a/internal/config/storage_strategy.go b/internal/config/storage_strategy.go index ac43845..8efe2ac 100644 --- a/internal/config/storage_strategy.go +++ b/internal/config/storage_strategy.go @@ -15,7 +15,6 @@ type ConfigStorageType string const ( StorageTypeFile ConfigStorageType = "file" // 配置文件 StorageTypeDatabase ConfigStorageType = "database" // 数据库表 - StorageTypeKeyValue ConfigStorageType = "keyvalue" // 键值对存储 StorageTypeJSON ConfigStorageType = "json" // JSON配置 ) @@ -233,8 +232,6 @@ func (s *ConfigStorageStrategy) GetConfig(key string, result interface{}) error return s.getDatabaseConfig(key, metadata, result) case StorageTypeJSON: return s.getJSONConfig(key, metadata, result) - case StorageTypeKeyValue: - return s.getKeyValueConfig(key, metadata, result) default: return fmt.Errorf("不支持的存储类型: %s", metadata.StorageType) } @@ -254,8 +251,6 @@ func (s *ConfigStorageStrategy) SetConfig(key string, value interface{}) error { return s.setDatabaseConfig(key, metadata, value) case StorageTypeJSON: return s.setJSONConfig(key, metadata, value) - case StorageTypeKeyValue: - return s.setKeyValueConfig(key, metadata, value) default: return fmt.Errorf("不支持的存储类型: %s", metadata.StorageType) } @@ -346,39 +341,6 @@ func (s *ConfigStorageStrategy) setJSONConfig(key string, metadata ConfigMetadat return s.db.Save(&systemConfig).Error } -// 键值对配置操作 -func (s *ConfigStorageStrategy) getKeyValueConfig(key string, metadata ConfigMetadata, result interface{}) error { - // 从现有的KeyValue表获取 - var keyValue struct { - Key string `gorm:"primaryKey"` - Value string - } - - err := s.db.Table("key_values").Where("key = ?", key).First(&keyValue).Error - if err != nil { - return err - } - - return json.Unmarshal([]byte(keyValue.Value), result) -} - -func (s *ConfigStorageStrategy) setKeyValueConfig(key string, metadata ConfigMetadata, value interface{}) error { - valueBytes, err := json.Marshal(value) - if err != nil { - return err - } - - keyValue := struct { - Key string `gorm:"primaryKey"` - Value string - }{ - Key: key, - Value: string(valueBytes), - } - - return s.db.Table("key_values").Save(&keyValue).Error -} - // InitTables 初始化配置相关表 func (s *ConfigStorageStrategy) InitTables() error { // 自动迁移所有配置表 diff --git a/internal/config/ui_config.go b/internal/config/ui_config.go new file mode 100644 index 0000000..f4ac41e --- /dev/null +++ b/internal/config/ui_config.go @@ -0,0 +1,54 @@ +package config + +// UIConfig holds theme and page related configuration and is stored under `ui` in config.yaml. +type UIConfig struct { + ThemesSelect string `yaml:"themes_select" json:"themes_select"` + Background string `yaml:"background" json:"background"` + PageExplain string `yaml:"page_explain" json:"page_explain"` + RobotsText string `yaml:"robots_text" json:"robots_text"` + ShowAdminAddr int `yaml:"show_admin_addr" json:"show_admin_addr"` + Opacity float64 `yaml:"opacity" json:"opacity"` +} + +func NewUIConfig() *UIConfig { + return &UIConfig{} +} + +func (u *UIConfig) Clone() *UIConfig { + if u == nil { + return NewUIConfig() + } + nu := *u + return &nu +} + +// Update applies values from a map to the UIConfig. It supports values typed as +// simple primitives (string/number). +func (u *UIConfig) Update(m map[string]interface{}) error { + if u == nil { + return nil + } + if v, ok := m["themes_select"].(string); ok { + u.ThemesSelect = v + } + if v, ok := m["background"].(string); ok { + u.Background = v + } + if v, ok := m["page_explain"].(string); ok { + u.PageExplain = v + } + if v, ok := m["robots_text"].(string); ok { + u.RobotsText = v + } + if v, ok := m["show_admin_addr"].(int); ok { + u.ShowAdminAddr = v + } else if v2, ok2 := m["show_admin_addr"].(float64); ok2 { + u.ShowAdminAddr = int(v2) + } + if v, ok := m["opacity"].(float64); ok { + u.Opacity = v + } else if v2, ok2 := m["opacity"].(int); ok2 { + u.Opacity = float64(v2) + } + return nil +} diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go index 63f3142..434caeb 100644 --- a/internal/handlers/admin.go +++ b/internal/handlers/admin.go @@ -1,7 +1,6 @@ package handlers import ( - "encoding/json" "fmt" "log" "net" @@ -15,7 +14,6 @@ import ( "github.com/zy84338719/filecodebox/internal/services" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v4" ) // AdminHandler 管理处理器 @@ -39,21 +37,10 @@ func (h *AdminHandler) Login(c *gin.Context) { return } - // 验证密码 - if req.Password != h.config.AdminToken { - common.UnauthorizedResponse(c, "密码错误") - return - } - - // 生成JWT token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "is_admin": true, - "exp": time.Now().Add(time.Hour * 24).Unix(), // 24小时过期 - }) - - tokenString, err := token.SignedString([]byte(h.config.AdminToken)) + // 使用 AdminService 进行管理员凭据验证并生成 token + tokenString, err := h.service.GenerateTokenForAdmin(req.Username, req.Password) if err != nil { - common.InternalServerErrorResponse(c, "生成token失败") + common.UnauthorizedResponse(c, "认证失败: "+err.Error()) return } @@ -155,73 +142,20 @@ func (h *AdminHandler) GetConfig(c *gin.Context) { // UpdateConfig 更新配置 func (h *AdminHandler) UpdateConfig(c *gin.Context) { - // 首先尝试获取原始JSON数据 - jsonData, err := c.GetRawData() - if err != nil { - common.BadRequestResponse(c, "无法读取请求数据: "+err.Error()) - return - } // 尝试绑定到结构化配置更新DTO var configUpdate models.ConfigUpdateFields - if err := json.Unmarshal(jsonData, &configUpdate); err == nil && configUpdate.HasUpdates() { + if err := c.ShouldBind(&configUpdate); err != nil { + common.BadRequestResponse(c, "请求参数错误: "+err.Error()) + return + } + + if configUpdate.HasUpdates() { // 使用结构化配置更新 - err = h.service.UpdateConfigWithDTO(&configUpdate) - if err != nil { + if err := h.service.UpdateConfigWithDTO(&configUpdate); err != nil { common.InternalServerErrorResponse(c, "更新配置失败: "+err.Error()) return } - } else { - // 尝试绑定到平面化配置更新DTO - var flatConfigUpdate models.FlatConfigUpdate - if err2 := json.Unmarshal(jsonData, &flatConfigUpdate); err2 == nil && flatConfigUpdate.HasUpdates() { - // 使用平面化配置更新 - err = h.service.UpdateConfigWithFlatDTO(&flatConfigUpdate) - if err != nil { - common.InternalServerErrorResponse(c, "更新配置失败: "+err.Error()) - return - } - } else { - // 最后尝试绑定到结构化请求(保持兼容性) - var configRequest web.AdminConfigRequest - if err3 := json.Unmarshal(jsonData, &configRequest); err3 == nil { - // 检查是否有有效的结构化数据 - hasValidStructuredData := (configRequest.Base != nil) || - (configRequest.Transfer != nil) || - (configRequest.User != nil) || - (configRequest.NotifyTitle != nil) || - (configRequest.NotifyContent != nil) || - (configRequest.PageExplain != nil) || - (configRequest.Opacity != nil) || - (configRequest.ThemesSelect != nil) - - if hasValidStructuredData { - // 使用结构化的配置请求 - err = h.service.UpdateConfigFromRequest(&configRequest) - if err != nil { - common.InternalServerErrorResponse(c, "更新配置失败: "+err.Error()) - return - } - } else { - // 回退到原始的map处理 - var flatConfig map[string]interface{} - if err4 := json.Unmarshal(jsonData, &flatConfig); err4 != nil { - common.BadRequestResponse(c, "配置参数错误: 无法解析请求数据") - return - } - - // 使用原始map配置更新 - err = h.service.UpdateConfig(flatConfig) - if err != nil { - common.InternalServerErrorResponse(c, "更新配置失败: "+err.Error()) - return - } - } - } else { - common.BadRequestResponse(c, "配置参数错误: 无法解析请求数据") - return - } - } } common.SuccessWithMessage(c, "更新成功", nil) @@ -680,15 +614,7 @@ func (h *AdminHandler) GetUser(c *gin.Context) { // CreateUser 创建用户 func (h *AdminHandler) CreateUser(c *gin.Context) { - var userData struct { - Username string `json:"username" binding:"required"` - Email string `json:"email" binding:"omitempty,email"` - Password string `json:"password" binding:"required"` - Nickname string `json:"nickname"` - IsAdmin bool `json:"is_admin"` - IsActive bool `json:"is_active"` - } - + var userData web.UserDataRequest if err := c.ShouldBindJSON(&userData); err != nil { common.BadRequestResponse(c, "参数错误: "+err.Error()) return diff --git a/internal/handlers/app_state.go b/internal/handlers/app_state.go index 486f855..ab59a1b 100644 --- a/internal/handlers/app_state.go +++ b/internal/handlers/app_state.go @@ -8,8 +8,9 @@ import ( // AppState 应用状态管理器 type AppState struct { - mcpManager *mcp.MCPManager - mu sync.RWMutex + mcpManager *mcp.MCPManager + adminHandler *AdminHandler + mu sync.RWMutex } var appState = &AppState{} @@ -27,3 +28,17 @@ func GetMCPManager() *mcp.MCPManager { defer appState.mu.RUnlock() return appState.mcpManager } + +// SetInjectedAdminHandler 注入 AdminHandler,供占位路由委派使用 +func SetInjectedAdminHandler(h *AdminHandler) { + appState.mu.Lock() + defer appState.mu.Unlock() + appState.adminHandler = h +} + +// GetInjectedAdminHandler 获取注入的 AdminHandler +func GetInjectedAdminHandler() *AdminHandler { + appState.mu.RLock() + defer appState.mu.RUnlock() + return appState.adminHandler +} diff --git a/internal/handlers/setup.go b/internal/handlers/setup.go index 39b74a6..761e8a5 100644 --- a/internal/handlers/setup.go +++ b/internal/handlers/setup.go @@ -1,13 +1,11 @@ package handlers import ( - "encoding/json" "errors" "fmt" "log" "os" "path/filepath" - "strconv" "sync/atomic" "github.com/gin-gonic/gin" @@ -220,161 +218,43 @@ func InitializeNoDB(manager *config.ConfigManager) gin.HandlerFunc { } defer atomic.StoreInt32(&initInProgress, 0) var req SetupRequest - - // 读取原始请求体,支持两种格式:嵌套 JSON({database:{}, admin:{}})和扁平表单风格 JSON(db_type, db_path, admin_token, admin_password 等) - data, err := c.GetRawData() - if err != nil { - common.BadRequestResponse(c, "无法读取请求体: "+err.Error()) + if err := c.ShouldBindJSON(&req); err != nil { + common.BadRequestResponse(c, "请求参数错误: "+err.Error()) return } - // 先尝试解码为嵌套结构 - // 注意:如果前端提交的是扁平字段(如 db_type, admin_password 等), - // json.Unmarshal 会成功但会留下空的嵌套结构。我们在解码成功后仍需 - // 检查是否需要回退到扁平映射解析。 - if err := json.Unmarshal(data, &req); err != nil || (req.Database.Type == "" && req.Admin.Username == "" && req.Admin.Password == "") { - // 尝试扁平化映射 - var flat map[string]interface{} - if err := json.Unmarshal(data, &flat); err != nil { - common.BadRequestResponse(c, "请求参数错误: 无法解析 JSON") - return - } - - // 映射常见字段 - if v, ok := flat["db_type"].(string); ok { - req.Database.Type = v - } - if v, ok := flat["db_path"].(string); ok { - req.Database.File = v - } - if v, ok := flat["db_file"].(string); ok && req.Database.File == "" { - req.Database.File = v - } - if v, ok := flat["db_host"].(string); ok { - req.Database.Host = v - } - if v, ok := flat["db_user"].(string); ok { - req.Database.User = v - } - if v, ok := flat["db_password"].(string); ok { - req.Database.Password = v - } - if v, ok := flat["db_name"].(string); ok { - req.Database.Database = v - } - if v, ok := flat["admin_password"].(string); ok { - req.Admin.Password = v - } - // 读取密码确认 - if v, ok := flat["admin_password_confirm"].(string); ok { - req.Admin.Confirm = v - } - if v, ok := flat["admin_username"].(string); ok { - req.Admin.Username = v - } - if v, ok := flat["admin_email"].(string); ok { - req.Admin.Email = v - } - if v, ok := flat["admin_nickname"].(string); ok { - req.Admin.Nickname = v - } - // enable_user_system may be "true"/"false" 或 布尔 - if v, ok := flat["enable_user_system"].(string); ok { - if b, err := strconv.ParseBool(v); err == nil && b { - req.Admin.AllowUserRegistration = true - } - } else if v, ok := flat["enable_user_system"].(bool); ok { - req.Admin.AllowUserRegistration = v - } - - // 兼容前端 admin_token:视为系统管理员令牌(Manager.AdminToken) - if v, ok := flat["admin_token"].(string); ok && v != "" { - manager.AdminToken = v - } - - // 兼容前端 site_name, storage_type, max_file_size,先写入 manager 内存结构(将在 Save 时持久化) - if v, ok := flat["site_name"].(string); ok && v != "" { - manager.Base.Name = v - } - if v, ok := flat["storage_type"].(string); ok && v != "" { - manager.Storage.Type = v - } - if v, ok := flat["max_file_size"].(string); ok && v != "" { - if n, err := strconv.Atoi(v); err == nil { - manager.Transfer.Upload.UploadSize = int64(n) - } - } else if v, ok := flat["max_file_size"].(float64); ok { - manager.Transfer.Upload.UploadSize = int64(v) - } - - // 如果提供了 sqlite 文件路径(例如 ./data/filecodebox.db),将目录设置为 Base.DataPath - if req.Database.File != "" && req.Database.Type == "sqlite" { - dir := filepath.Dir(req.Database.File) - if dir == "." || dir == "" { - // 使用默认数据目录 - } else { - manager.Base.DataPath = dir - } - } - - // 如果前端没有提供管理员用户名/email,使用合理的默认值 - if req.Admin.Username == "" { - // 尝试从 manager.AdminToken 派生用户名,否则使用 "admin" - if manager != nil && manager.AdminToken != "" { - req.Admin.Username = manager.AdminToken - } else { - req.Admin.Username = "admin" - } - } - if req.Admin.Email == "" { - req.Admin.Email = "admin@localhost" - } - - } - // 继续使用 req 进行验证和初始化 - // 捕获并验证扁平字段中的 storage_path(如果提供并且 storage 类型为 local),但不要立刻写入 manager var desiredStoragePath string if manager.Storage.Type == "local" { - // 尝试从原始请求体的扁平映射中读取 storage_path - var flat map[string]interface{} - _ = json.Unmarshal(data, &flat) - if v, ok := flat["storage_path"].(string); ok { - sp := v - if sp == "" { - common.BadRequestResponse(c, "本地存储时必须提供 storage_path") - return - } - - // 若为相对路径,则相对于 manager.Base.DataPath - if !filepath.IsAbs(sp) { - if manager.Base != nil && manager.Base.DataPath != "" { - sp = filepath.Join(manager.Base.DataPath, sp) - } else { - sp, _ = filepath.Abs(sp) - } - } - - // 尝试创建目录(如果不存在) - if _, err := os.Stat(sp); os.IsNotExist(err) { - if err := os.MkdirAll(sp, 0755); err != nil { - common.InternalServerErrorResponse(c, "创建本地存储目录失败: "+err.Error()) - return - } + sp := manager.Storage.StoragePath + // 若为相对路径,则相对于 manager.Base.DataPath + if !filepath.IsAbs(sp) { + if manager.Base != nil && manager.Base.DataPath != "" { + sp = filepath.Join(manager.Base.DataPath, sp) + } else { + sp, _ = filepath.Abs(sp) } + } - // 检查是否可写:尝试在目录中创建一个临时文件 - testFile := filepath.Join(sp, ".perm_check") - if f, err := os.Create(testFile); err != nil { - common.InternalServerErrorResponse(c, "本地存储路径不可写: "+err.Error()) + // 尝试创建目录(如果不存在) + if _, err := os.Stat(sp); os.IsNotExist(err) { + if err := os.MkdirAll(sp, 0755); err != nil { + common.InternalServerErrorResponse(c, "创建本地存储目录失败: "+err.Error()) return - } else { - f.Close() - _ = os.Remove(testFile) } + } - desiredStoragePath = sp + // 检查是否可写:尝试在目录中创建一个临时文件 + testFile := filepath.Join(sp, ".perm_check") + if f, err := os.Create(testFile); err != nil { + common.InternalServerErrorResponse(c, "本地存储路径不可写: "+err.Error()) + return + } else { + f.Close() + _ = os.Remove(testFile) } + + desiredStoragePath = sp } // 验证管理员信息 @@ -433,6 +313,45 @@ func InitializeNoDB(manager *config.ConfigManager) gin.HandlerFunc { // 后续将在数据库初始化并注入 manager 后再次保存配置。 // 初始化数据库连接并执行自动迁移 + // Ensure Base config exists + if manager.Base == nil { + manager.Base = &config.BaseConfig{} + } + + // For sqlite, determine DataPath from provided database file if not already set + if manager.Database.Type == "sqlite" { + dataFile := manager.Database.Name + if dataFile == "" { + dataFile = req.Database.File + } + var dataDir string + if dataFile != "" { + dataDir = filepath.Dir(dataFile) + if dataDir == "." || dataDir == "" { + dataDir = "./data" + } + // make absolute if possible + if !filepath.IsAbs(dataDir) { + if abs, err := filepath.Abs(dataDir); err == nil { + dataDir = abs + } + } + manager.Base.DataPath = dataDir + } else if manager.Base.DataPath == "" { + // fallback default + manager.Base.DataPath = "./data" + if abs, err := filepath.Abs(manager.Base.DataPath); err == nil { + manager.Base.DataPath = abs + } + } + + // ensure directory exists before InitWithManager attempts to mkdir + if err := os.MkdirAll(manager.Base.DataPath, 0750); err != nil { + common.InternalServerErrorResponse(c, "创建SQLite数据目录失败: "+err.Error()) + return + } + } + log.Printf("[InitializeNoDB] 开始调用 database.InitWithManager, dbType=%s, dataPath=%s", manager.Database.Type, manager.Base.DataPath) db, err := database.InitWithManager(manager) if err != nil { @@ -527,137 +446,11 @@ func (h *SetupHandler) Initialize(c *gin.Context) { } var req SetupRequest - - data, err := c.GetRawData() - if err != nil { - common.BadRequestResponse(c, "无法读取请求体: "+err.Error()) + if err := c.ShouldBindJSON(&req); err != nil { + common.BadRequestResponse(c, "请求参数错误: "+err.Error()) return } - // 先尝试解析为嵌套结构 - if err := json.Unmarshal(data, &req); err != nil { - // 解析为扁平 map 并映射 - var flat map[string]interface{} - if err := json.Unmarshal(data, &flat); err != nil { - common.BadRequestResponse(c, "请求参数错误: 无法解析 JSON") - return - } - if v, ok := flat["db_type"].(string); ok { - req.Database.Type = v - } - if v, ok := flat["db_path"].(string); ok { - req.Database.File = v - } - if v, ok := flat["db_file"].(string); ok && req.Database.File == "" { - req.Database.File = v - } - if v, ok := flat["db_host"].(string); ok { - req.Database.Host = v - } - if v, ok := flat["db_user"].(string); ok { - req.Database.User = v - } - if v, ok := flat["db_password"].(string); ok { - req.Database.Password = v - } - if v, ok := flat["db_name"].(string); ok { - req.Database.Database = v - } - if v, ok := flat["admin_password"].(string); ok { - req.Admin.Password = v - } - if v, ok := flat["admin_username"].(string); ok { - req.Admin.Username = v - } - if v, ok := flat["admin_email"].(string); ok { - req.Admin.Email = v - } - // 存储路径(local 存储时使用) - if v, ok := flat["storage_path"].(string); ok { - h.manager.Storage.StoragePath = v - } - // 读取密码确认 - if v, ok := flat["admin_password_confirm"].(string); ok { - req.Admin.Confirm = v - } - if v, ok := flat["admin_nickname"].(string); ok { - req.Admin.Nickname = v - } - if v, ok := flat["enable_user_system"].(string); ok { - if b, err := strconv.ParseBool(v); err == nil && b { - req.Admin.AllowUserRegistration = true - } - } else if v, ok := flat["enable_user_system"].(bool); ok { - req.Admin.AllowUserRegistration = v - } - - // 兼容前端 admin_token:视为系统管理员令牌(Manager.AdminToken) - if v, ok := flat["admin_token"].(string); ok && v != "" { - h.manager.AdminToken = v - } - - // 如果前端没有提供管理员用户名/email,使用合理的默认值 - if req.Admin.Username == "" { - // 尝试从 manager.AdminToken 派生用户名,否则使用 "admin" - if h.manager != nil && h.manager.AdminToken != "" { - req.Admin.Username = h.manager.AdminToken - } else { - req.Admin.Username = "admin" - } - } - if req.Admin.Email == "" { - req.Admin.Email = "admin@localhost" - } - - // 如果 storage_type 是 local,则处理 storage_path(扁平字段) - if h.manager.Storage.Type == "local" { - if v, ok := flat["storage_path"].(string); ok { - sp := v - if sp == "" { - common.BadRequestResponse(c, "本地存储时必须提供 storage_path") - return - } - - // 若为相对路径,则相对于 h.manager.Base.DataPath - if !filepath.IsAbs(sp) { - if h.manager.Base != nil && h.manager.Base.DataPath != "" { - sp = filepath.Join(h.manager.Base.DataPath, sp) - } else { - sp, _ = filepath.Abs(sp) - } - } - - // 尝试创建目录(如果不存在) - if _, err := os.Stat(sp); os.IsNotExist(err) { - if err := os.MkdirAll(sp, 0755); err != nil { - common.InternalServerErrorResponse(c, "创建本地存储目录失败: "+err.Error()) - return - } - } - - // 检查是否可写:尝试在目录中创建一个临时文件 - testFile := filepath.Join(sp, ".perm_check") - if f, err := os.Create(testFile); err != nil { - common.InternalServerErrorResponse(c, "本地存储路径不可写: "+err.Error()) - return - } else { - f.Close() - _ = os.Remove(testFile) - } - - h.manager.Storage.StoragePath = sp - if err := h.manager.Save(); err != nil { - common.InternalServerErrorResponse(c, "保存存储配置失败: "+err.Error()) - return - } - } else { - // 如果没有传 storage_path,前端应已校验,但服务器端也需要确保 - common.BadRequestResponse(c, "本地存储时必须提供 storage_path") - return - } - } - } - // 验证管理员信息 if len(req.Admin.Username) < 3 { common.BadRequestResponse(c, "用户名长度至少3个字符") diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 65c6cbb..915ffc5 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -245,39 +245,3 @@ func OptionalUserAuth(manager *config.ConfigManager, userService interface { c.Next() } } - -// AdminTokenAuth 管理员Token认证中间件 -func AdminTokenAuth(manager *config.ConfigManager) gin.HandlerFunc { - return func(c *gin.Context) { - // 获取Authorization头 - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - common.UnauthorizedResponse(c, "缺少认证信息") - c.Abort() - return - } - - // 检查Bearer前缀 - tokenParts := strings.SplitN(authHeader, " ", 2) - if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { - common.UnauthorizedResponse(c, "认证格式错误") - c.Abort() - return - } - - token := tokenParts[1] - - // 验证管理员token - if token != manager.AdminToken { - common.UnauthorizedResponse(c, "管理员token无效") - c.Abort() - return - } - - // 设置管理员身份到上下文 - c.Set("is_admin", true) - c.Set("role", "admin") - - c.Next() - } -} diff --git a/internal/models/web/admin.go b/internal/models/web/admin.go index 35b623b..083031d 100644 --- a/internal/models/web/admin.go +++ b/internal/models/web/admin.go @@ -2,6 +2,7 @@ package web // AdminLoginRequest 管理员登录请求 type AdminLoginRequest struct { + Username string `json:"username,omitempty"` Password string `json:"password" binding:"required"` } diff --git a/internal/models/web/mcp.go b/internal/models/web/mcp.go new file mode 100644 index 0000000..d84d918 --- /dev/null +++ b/internal/models/web/mcp.go @@ -0,0 +1,10 @@ +package web + +type ControlDataRequest struct { + Action string `json:"action" binding:"required"` // "start" 或 "stop" +} + +type TestDataRequest struct { + Port string `json:"port"` + Host string `json:"host"` +} diff --git a/internal/models/web/user.go b/internal/models/web/user.go index ee82aa9..3973438 100644 --- a/internal/models/web/user.go +++ b/internal/models/web/user.go @@ -69,3 +69,12 @@ type UserSystemInfoResponse struct { AllowUserRegistration int `json:"allow_user_registration"` RequireEmailVerification int `json:"require_email_verification"` } + +type UserDataRequest struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"omitempty,email"` + Password string `json:"password" binding:"required"` + Nickname string `json:"nickname"` + IsAdmin bool `json:"is_admin"` + IsActive bool `json:"is_active"` +} diff --git a/internal/routes/admin.go b/internal/routes/admin.go index 425020e..8aaf1d7 100644 --- a/internal/routes/admin.go +++ b/internal/routes/admin.go @@ -34,6 +34,28 @@ func SetupAdminRoutes( ServeAdminPage(c, cfg) }) + // 管理员登录(通过用户名/密码获取 JWT) + // 如果已经存在相同的 POST /admin/login 路由(例如在未初始化数据库时注册的占位处理器), + // 则跳过注册以避免 gin 的 "handlers are already registered for path" panic。 + // If a placeholder route was registered earlier (no-DB mode), skip; otherwise register. + exists := false + for _, r := range router.Routes() { + if r.Method == "POST" && r.Path == "/admin/login" { + exists = true + break + } + } + if !exists { + adminGroup.POST("/login", func(c *gin.Context) { + // 尝试从全局注入获取真实 handler(SetInjectedAdminHandler) + if injected := handlers.GetInjectedAdminHandler(); injected != nil { + injected.Login(c) + return + } + c.JSON(404, gin.H{"code": 404, "message": "admin handler not configured"}) + }) + } + // 模块化管理后台静态文件 themeDir := fmt.Sprintf("./%s", cfg.ThemesSelect) adminGroup.Static("/css", fmt.Sprintf("%s/admin/css", themeDir)) @@ -66,14 +88,7 @@ func SetupAdminRoutes( } } - // JWT验证失败,尝试管理员token认证 - if tokenParts[1] == cfg.AdminToken { - c.Set("is_admin", true) - c.Set("role", "admin") - c.Set("auth_type", "admin_token") - c.Next() - return - } + // JWT验证失败,不再支持静态管理员令牌回退 } } diff --git a/internal/routes/setup.go b/internal/routes/setup.go index 0706495..228dfdd 100644 --- a/internal/routes/setup.go +++ b/internal/routes/setup.go @@ -105,6 +105,20 @@ func CreateAndSetupRouter( ServeUserPage(c, manager, "forgot-password.html") }) + // 在未初始化数据库时,不直接注册真实的 POST /admin/login 处理器以避免后续动态注册冲突。 + // 这里注册一个轻量的委派处理器:当 admin_handler 被注入到全局 app state(动态注册完成)时, + // 它会尝试调用真实的 handler;否则返回明确的 JSON 错误,提示调用 /setup/initialize。 + router.POST("/admin/login", func(c *gin.Context) { + // 如果动态注入了 admin_handler(在 RegisterDynamicRoutes 中注入), + // 使用全局注入的 handler(通过 handlers.GetInjectedAdminHandler)进行委派。 + if injected := handlers.GetInjectedAdminHandler(); injected != nil { + injected.Login(c) + return + } + // 否则返回 JSON 提示,说明数据库尚未初始化 + c.JSON(404, gin.H{"code": 404, "message": "admin 登录不可用:数据库尚未初始化,请调用 /setup/initialize 完成初始化"}) + }) + return router } @@ -166,8 +180,11 @@ func RegisterDynamicRoutes( userHandler := handlers.NewUserHandler(userService) // 设置分享、用户、分片、管理员等路由(不重复注册基础路由) // 注意:SetupAllRoutes 会调用 SetupBaseRoutes,因此我们直接调用 SetupShareRoutes 等单独函数 + // 将 adminHandler 注入到全局 app state,以便占位路由可以查找并委派 + handlers.SetInjectedAdminHandler(adminHandler) SetupShareRoutes(router, shareHandler, manager, userService) - SetupUserRoutes(router, userHandler, manager, userService) + // Use API-only user routes here to avoid duplicate page route registration + SetupUserAPIRoutes(router, userHandler, manager, userService) SetupChunkRoutes(router, chunkHandler, manager) SetupAdminRoutes(router, adminHandler, storageHandler, manager, userService) // System init routes are no longer needed after DB init diff --git a/internal/routes/user.go b/internal/routes/user.go index 5236e6b..0f40cbb 100644 --- a/internal/routes/user.go +++ b/internal/routes/user.go @@ -21,6 +21,36 @@ func SetupUserRoutes( userService interface { ValidateToken(string) (interface{}, error) }, +) { + // 注册完整的用户路由(API + 页面) + SetupUserAPIRoutes(router, userHandler, cfg, userService) + + // 用户页面路由 + userPageGroup := router.Group("/user") + { + userPageGroup.GET("/login", func(c *gin.Context) { + ServeUserPage(c, cfg, "login.html") + }) + userPageGroup.GET("/register", func(c *gin.Context) { + ServeUserPage(c, cfg, "register.html") + }) + userPageGroup.GET("/dashboard", func(c *gin.Context) { + ServeUserPage(c, cfg, "dashboard.html") + }) + userPageGroup.GET("/forgot-password", func(c *gin.Context) { + ServeUserPage(c, cfg, "forgot-password.html") + }) + } +} + +// SetupUserAPIRoutes 仅注册用户相关的 API 路由(供动态注册时使用,避免重复注册页面路由) +func SetupUserAPIRoutes( + router *gin.Engine, + userHandler *handlers.UserHandler, + cfg *config.ConfigManager, + userService interface { + ValidateToken(string) (interface{}, error) + }, ) { // 用户系统路由 userGroup := router.Group("/user") @@ -46,23 +76,6 @@ func SetupUserRoutes( authGroup.DELETE("/files/:id", userHandler.DeleteFile) } } - - // 用户页面路由 - userPageGroup := router.Group("/user") - { - userPageGroup.GET("/login", func(c *gin.Context) { - ServeUserPage(c, cfg, "login.html") - }) - userPageGroup.GET("/register", func(c *gin.Context) { - ServeUserPage(c, cfg, "register.html") - }) - userPageGroup.GET("/dashboard", func(c *gin.Context) { - ServeUserPage(c, cfg, "dashboard.html") - }) - userPageGroup.GET("/forgot-password", func(c *gin.Context) { - ServeUserPage(c, cfg, "forgot-password.html") - }) - } } // ServeUserPage 服务用户页面 diff --git a/internal/services/admin/auth.go b/internal/services/admin/auth.go index a3ae6a1..c45c6ec 100644 --- a/internal/services/admin/auth.go +++ b/internal/services/admin/auth.go @@ -21,8 +21,9 @@ func (s *Service) GenerateToken() (string, error) { // 创建token token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - // 签名token - tokenString, err := token.SignedString([]byte(s.manager.AdminToken)) + // 签名token - 使用 user JWT secret when available + secret := s.manager.User.JWTSecret + tokenString, err := token.SignedString([]byte(secret)) if err != nil { return "", fmt.Errorf("生成token失败: %w", err) } @@ -37,7 +38,7 @@ func (s *Service) ValidateToken(tokenString string) error { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } - return []byte(s.manager.AdminToken), nil + return []byte(s.manager.User.JWTSecret), nil }) if err != nil { @@ -55,6 +56,40 @@ func (s *Service) ValidateToken(tokenString string) error { return errors.New("无效的token") } +// GenerateTokenForAdmin 验证管理员用户名/密码并生成管理员JWT(使用 user.JWTSecret 签名) +func (s *Service) GenerateTokenForAdmin(username, password string) (string, error) { + // 查找用户 + user, err := s.repositoryManager.User.GetByUsername(username) + if err != nil { + return "", fmt.Errorf("用户不存在或认证失败") + } + + // 确认角色为 admin + if user.Role != "admin" { + return "", fmt.Errorf("用户不是管理员") + } + + // 验证密码 + if !s.authService.CheckPassword(password, user.PasswordHash) { + return "", fmt.Errorf("认证失败") + } + + // 创建JWT claims + claims := jwt.MapClaims{ + "is_admin": true, + "user_id": user.ID, + "exp": time.Now().Add(24 * time.Hour).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(s.manager.User.JWTSecret)) + if err != nil { + return "", fmt.Errorf("生成token失败: %w", err) + } + + return tokenString, nil +} + // ResetUserPassword 重置用户密码 - 使用统一的认证服务 func (s *Service) ResetUserPassword(userID uint, newPassword string) error { hashedPassword, err := s.authService.HashPassword(newPassword) diff --git a/internal/services/admin/config.go b/internal/services/admin/config.go index ced7c36..bafb9af 100644 --- a/internal/services/admin/config.go +++ b/internal/services/admin/config.go @@ -1,7 +1,6 @@ package admin import ( - "encoding/json" "fmt" "github.com/zy84338719/filecodebox/internal/config" @@ -19,17 +18,13 @@ func (s *Service) UpdateConfig(configData map[string]interface{}) error { // 过滤掉端口和管理员密码配置,这些不应该通过API更新 filteredConfigData := make(map[string]interface{}) for key, value := range configData { - // 跳过端口和管理员密码配置 - if key == "port" || key == "admin_token" { + if key == "port" { continue } filteredConfigData[key] = value } - // 转换为结构化配置更新 configUpdates := s.convertMapToConfigUpdate(filteredConfigData) - - // 保存配置更新 return s.SaveConfigUpdate(configUpdates) } @@ -412,47 +407,56 @@ func (s *Service) convertFlatDTOToNested(flatUpdate *models.FlatConfigUpdate) *m func (s *Service) SaveConfigUpdate(configUpdate *models.ConfigUpdateFields) error { // 转换为map格式 configMap := configUpdate.ToMap() - - // 扁平化配置数据 - flattenedConfig := make(map[string]interface{}) - for key, value := range configMap { - if err := s.flattenConfig(key, value, flattenedConfig); err != nil { - return fmt.Errorf("处理配置数据失败: %w", err) - } - } - - // 保存扁平化的配置 - for key, value := range flattenedConfig { - // 将value转换为字符串 - var valueStr string - switch v := value.(type) { - case string: - valueStr = v - case int, int32, int64: - valueStr = fmt.Sprintf("%d", v) - case float32, float64: - valueStr = fmt.Sprintf("%g", v) - case bool: - if v { - valueStr = "1" - } else { - valueStr = "0" + // Apply structured updates to the ConfigManager modules + if cfgBase, ok := configMap["base"].(map[string]interface{}); ok { + if err := s.manager.Base.Update(cfgBase); err != nil { + return fmt.Errorf("更新 base 配置失败: %w", err) + } + } + if cfgTransfer, ok := configMap["transfer"].(map[string]interface{}); ok { + if upload, ok2 := cfgTransfer["upload"].(map[string]interface{}); ok2 { + if err := s.manager.Transfer.Upload.Update(upload); err != nil { + return fmt.Errorf("更新 transfer.upload 配置失败: %w", err) } - default: - // 对于复杂类型,序列化为JSON - jsonBytes, err := json.Marshal(v) - if err != nil { - return fmt.Errorf("序列化配置值失败: %w", err) + } + if download, ok2 := cfgTransfer["download"].(map[string]interface{}); ok2 { + if err := s.manager.Transfer.Download.Update(download); err != nil { + return fmt.Errorf("更新 transfer.download 配置失败: %w", err) } - valueStr = string(jsonBytes) } - - if err := s.manager.UpdateKeyValue(key, valueStr); err != nil { - return fmt.Errorf("保存配置失败: %w", err) + } + if cfgUser, ok := configMap["user"].(map[string]interface{}); ok { + if err := s.manager.User.Update(cfgUser); err != nil { + return fmt.Errorf("更新 user 配置失败: %w", err) + } + } + if cfgMCP, ok := configMap["mcp"].(map[string]interface{}); ok { + if err := s.manager.MCP.Update(cfgMCP); err != nil { + return fmt.Errorf("更新 mcp 配置失败: %w", err) } } - // 配置保存成功后,执行热重载 + // Other top-level fields + if v, ok := configMap["notify_title"].(string); ok { + s.manager.NotifyTitle = v + } + if v, ok := configMap["notify_content"].(string); ok { + s.manager.NotifyContent = v + } + if v, ok := configMap["page_explain"].(string); ok { + s.manager.PageExplain = v + } + if v, ok := configMap["opacity"].(int); ok { + s.manager.Opacity = float64(v) + } + if v, ok := configMap["themes_select"].(string); ok { + s.manager.ThemesSelect = v + } + + // Persist structured config to YAML and reload + if err := s.manager.PersistYAML(); err != nil { + return fmt.Errorf("持久化配置到config.yaml失败: %w", err) + } if err := s.manager.ReloadConfig(); err != nil { return fmt.Errorf("热重载配置失败: %w", err) } diff --git a/internal/services/admin/service.go b/internal/services/admin/service.go index a0e6992..3ef4b6b 100644 --- a/internal/services/admin/service.go +++ b/internal/services/admin/service.go @@ -1,6 +1,9 @@ package admin import ( + "fmt" + "time" + "github.com/zy84338719/filecodebox/internal/config" "github.com/zy84338719/filecodebox/internal/repository" "github.com/zy84338719/filecodebox/internal/services/auth" @@ -13,14 +16,21 @@ type Service struct { storageService *storage.ConcreteStorageService repositoryManager *repository.RepositoryManager authService *auth.Service + // runtime-only fields + SysStart string } // NewService 创建管理员服务 func NewService(repositoryManager *repository.RepositoryManager, manager *config.ConfigManager, storageService *storage.ConcreteStorageService) *Service { - return &Service{ + s := &Service{ manager: manager, storageService: storageService, repositoryManager: repositoryManager, authService: auth.NewService(repositoryManager, manager), } + // Initialize SysStart at startup + if s.SysStart == "" { + s.SysStart = fmt.Sprintf("%d", time.Now().UnixMilli()) + } + return s } diff --git a/internal/services/admin/stats.go b/internal/services/admin/stats.go index 7713cac..b5ce88a 100644 --- a/internal/services/admin/stats.go +++ b/internal/services/admin/stats.go @@ -62,15 +62,12 @@ func (s *Service) GetStats() (*web.AdminStatsResponse, error) { } stats.TotalSize = totalSize - // 系统启动时间 - if v, ok := s.manager.GetKeyValue("sys_start"); ok { - stats.SysStart = v + // 系统启动时间 - 使用 Service 的内存字段 SysStart + if s.SysStart != "" { + stats.SysStart = s.SysStart } else { - // 如果没有记录,创建一个 startTime := fmt.Sprintf("%d", time.Now().UnixMilli()) - if err := s.manager.UpdateKeyValue("sys_start", startTime); err != nil { - return nil, fmt.Errorf("设置系统启动时间失败: %v", err) - } + s.SysStart = startTime stats.SysStart = startTime } diff --git a/tests/test_admin.sh b/tests/test_admin.sh index 99c8fec..2779802 100755 --- a/tests/test_admin.sh +++ b/tests/test_admin.sh @@ -145,31 +145,31 @@ echo # 测试获取统计信息 echo "1. 测试获取统计信息..." curl -s -X GET "${BASE_URL}/admin/stats" \ - -H "Admin-Token: ${ADMIN_TOKEN}" | jq '.' + echo # 测试获取文件列表 echo "2. 测试获取文件列表..." curl -s -X GET "${BASE_URL}/admin/files?page=1&page_size=10" \ - -H "Admin-Token: ${ADMIN_TOKEN}" | jq '.' + -H "Authorization: Bearer ${TOKEN}" | jq '.' echo # 测试获取配置 echo "3. 测试获取配置..." curl -s -X GET "${BASE_URL}/admin/config" \ - -H "Admin-Token: ${ADMIN_TOKEN}" | jq '.detail.name, .detail.port, .detail.upload_size' + -H "Authorization: Bearer ${TOKEN}" | jq '.detail.name, .detail.port, .detail.upload_size' echo # 测试清理过期文件 echo "4. 测试清理过期文件..." curl -s -X POST "${BASE_URL}/admin/clean" \ - -H "Admin-Token: ${ADMIN_TOKEN}" | jq '.' + -H "Authorization: Bearer ${TOKEN}" | jq '.' echo # 测试无效token echo "5. 测试无效token..." curl -s -X GET "${BASE_URL}/admin/stats" \ - -H "Admin-Token: invalid" | jq '.' + -H "Authorization: Bearer invalid" | jq '.' echo echo "=== 管理API测试完成 ===" diff --git a/tests/test_dao_migration.sh b/tests/test_dao_migration.sh index 9d5d44e..69ca4e2 100755 --- a/tests/test_dao_migration.sh +++ b/tests/test_dao_migration.sh @@ -147,24 +147,24 @@ echo echo "5. 测试管理员功能 (AdminService DAO)..." # 管理员登录 -ADMIN_LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/admin/login" \ + ADMIN_LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/admin/login" \ -H "Content-Type: application/json" \ - -d '{"password": "FileCodeBox2025"}') + -d '{"username":"admin","password": "FileCodeBox2025"}') -if check_response "$ADMIN_LOGIN_RESPONSE" "管理员登录"; then - ADMIN_TOKEN=$(echo "$ADMIN_LOGIN_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) - echo " 管理员Token获取成功" + if check_response "$ADMIN_LOGIN_RESPONSE" "管理员登录"; then + ADMIN_JWT=$(echo "$ADMIN_LOGIN_RESPONSE" | grep -o '"token":"[^\"]*"' | cut -d'"' -f4) + echo " 管理员JWT获取成功" - # 测试仪表盘 - DASHBOARD_RESPONSE=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" "$BASE_URL/admin/dashboard") + # 测试仪表盘 + DASHBOARD_RESPONSE=$(curl -s -H "Authorization: Bearer $ADMIN_JWT" "$BASE_URL/admin/dashboard") check_response "$DASHBOARD_RESPONSE" "管理员仪表盘" - # 测试文件列表 - FILES_RESPONSE=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" "$BASE_URL/admin/files?page=1&page_size=5") + # 测试文件列表 + FILES_RESPONSE=$(curl -s -H "Authorization: Bearer $ADMIN_JWT" "$BASE_URL/admin/files?page=1&page_size=5") check_response "$FILES_RESPONSE" "文件列表获取" - # 测试配置获取 - CONFIG_ADMIN_RESPONSE=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" "$BASE_URL/admin/config") + # 测试配置获取 + CONFIG_ADMIN_RESPONSE=$(curl -s -H "Authorization: Bearer $ADMIN_JWT" "$BASE_URL/admin/config") check_response "$CONFIG_ADMIN_RESPONSE" "管理员配置获取" fi echo diff --git a/tests/test_storage_management.sh b/tests/test_storage_management.sh index f8ef5b0..1e45c3d 100755 --- a/tests/test_storage_management.sh +++ b/tests/test_storage_management.sh @@ -3,7 +3,9 @@ # 测试存储管理功能 BASE_URL="http://localhost:12345" -ADMIN_TOKEN="FileCodeBox2025" +# 注意:静态管理员令牌已弃用。测试请先通过管理员用户名/密码登录获取 JWT(可通过 ADMIN_JWT 环境变量注入) +# 示例:ADMIN_JWT=$(curl -s -X POST "$BASE_URL/admin/login" -d '{"username":"admin","password":"yourpass"}' | jq -r '.data.token') +ADMIN_JWT="" echo "=== 测试存储管理功能 ===" echo @@ -17,19 +19,29 @@ fi echo "✅ 服务器运行正常" echo -# 获取管理员 Token +# 获取管理员 Token(优先使用 ADMIN_JWT;若未设置则使用 ADMIN_USERNAME/ADMIN_PASSWORD 登录获取) echo "1. 管理员登录..." -LOGIN_RESULT=$(curl -s -X POST "$BASE_URL/admin/login" \ - -H "Content-Type: application/json" \ - -d "{\"password\":\"$ADMIN_TOKEN\"}") - -if [[ $LOGIN_RESULT == *"token"* ]]; then - JWT_TOKEN=$(echo $LOGIN_RESULT | grep -o '"token":"[^"]*"' | cut -d'"' -f4) - echo "✅ 管理员登录成功" +if [[ -n "$ADMIN_JWT" ]]; then + JWT_TOKEN="$ADMIN_JWT" + echo "✅ 已使用环境提供的 ADMIN_JWT" else - echo "❌ 管理员登录失败" - echo "详细信息: $LOGIN_RESULT" - exit 1 + if [[ -z "$ADMIN_USERNAME" || -z "$ADMIN_PASSWORD" ]]; then + echo "❌ 未提供 ADMIN_JWT,也未设置 ADMIN_USERNAME/ADMIN_PASSWORD 环境变量。无法登录。" + exit 1 + fi + + LOGIN_RESULT=$(curl -s -X POST "$BASE_URL/admin/login" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"$ADMIN_USERNAME\",\"password\":\"$ADMIN_PASSWORD\"}") + + if [[ $LOGIN_RESULT == *"token"* ]]; then + JWT_TOKEN=$(echo $LOGIN_RESULT | grep -o '"token":"[^\"]*"' | cut -d'"' -f4) + echo "✅ 管理员登录成功" + else + echo "❌ 管理员登录失败" + echo "详细信息: $LOGIN_RESULT" + exit 1 + fi fi echo diff --git a/tests/test_webdav_config.sh b/tests/test_webdav_config.sh index ce84e16..b8ee9b8 100755 --- a/tests/test_webdav_config.sh +++ b/tests/test_webdav_config.sh @@ -3,18 +3,23 @@ # 测试 WebDAV 存储配置 BASE_URL="http://localhost:12345" -ADMIN_TOKEN="FileCodeBox2025" +# 注意:静态管理员令牌已弃用。请先通过管理员用户名/密码登录并获取 JWT,用于后续请求(或通过 ADMIN_JWT 环境变量注入)。 +ADMIN_JWT="" echo "=== 测试 WebDAV 存储配置 ===" echo -# 管理员登录 +# 管理员登录(示例:使用管理员用户名+密码获取 JWT) echo "1. 管理员登录..." LOGIN_RESULT=$(curl -s -X POST "$BASE_URL/admin/login" \ -H "Content-Type: application/json" \ - -d "{\"password\":\"$ADMIN_TOKEN\"}") + -d '{"username":"admin","password":"FileCodeBox2025"}') -JWT_TOKEN=$(echo $LOGIN_RESULT | grep -o '"token":"[^"]*"' | cut -d'"' -f4) +JWT_TOKEN=$(echo $LOGIN_RESULT | grep -o '"token":"[^\"]*"' | cut -d'"' -f4) +if [ -z "$JWT_TOKEN" ]; then + echo "❌ 无法获取 JWT,登录可能失败: $LOGIN_RESULT" + exit 1 +fi echo "✅ 登录成功" echo diff --git a/themes/2025/admin/index.html b/themes/2025/admin/index.html index f4dd673..9fca056 100644 --- a/themes/2025/admin/index.html +++ b/themes/2025/admin/index.html @@ -993,8 +993,8 @@

基础设置

- - 用于登录管理后台的密码 + + 用于登录管理后台的密码(请使用管理员用户名 + 密码 登录)
diff --git a/themes/2025/admin/js/api.js b/themes/2025/admin/js/api.js index f714389..5f4e4c0 100644 --- a/themes/2025/admin/js/api.js +++ b/themes/2025/admin/js/api.js @@ -9,8 +9,8 @@ * @returns {Promise} 请求结果 */ async function apiRequest(url, options = {}) { - // 动态获取当前的authToken - const currentAuthToken = window.authToken || localStorage.getItem('admin_token'); + // 动态获取当前的authToken(优先检查内存变量,再检查 localStorage 的新键 auth_token) + const currentAuthToken = window.authToken || localStorage.getItem('auth_token'); const defaultOptions = { headers: { @@ -115,7 +115,7 @@ async function apiDelete(url) { */ async function apiUpload(url, formData, onProgress = null) { return new Promise((resolve, reject) => { - const currentAuthToken = window.authToken || localStorage.getItem('admin_token'); + const currentAuthToken = window.authToken || localStorage.getItem('auth_token'); const xhr = new XMLHttpRequest(); // 设置上传进度监听 @@ -175,7 +175,7 @@ async function apiUpload(url, formData, onProgress = null) { */ async function apiDownload(url, filename = 'download') { try { - const currentAuthToken = window.authToken || localStorage.getItem('admin_token'); + const currentAuthToken = window.authToken || localStorage.getItem('auth_token'); const headers = {}; if (currentAuthToken) { @@ -278,7 +278,7 @@ function handleAuthError() { if (typeof window !== 'undefined') { window.authToken = null; } - localStorage.removeItem('admin_token'); + localStorage.removeItem('auth_token'); // 如果当前不在登录页面,跳转到登录页面 if (typeof showLoginPage === 'function') { @@ -299,9 +299,9 @@ function setAuthToken(token) { window.authToken = token; } if (token) { - localStorage.setItem('admin_token', token); + localStorage.setItem('auth_token', token); } else { - localStorage.removeItem('admin_token'); + localStorage.removeItem('auth_token'); } } @@ -310,7 +310,7 @@ function setAuthToken(token) { * @returns {string|null} 认证令牌 */ function getAuthToken() { - return (typeof window !== 'undefined' ? window.authToken : null) || localStorage.getItem('admin_token'); + return (typeof window !== 'undefined' ? window.authToken : null) || localStorage.getItem('auth_token'); } /** @@ -318,7 +318,7 @@ function getAuthToken() { * @returns {boolean} 是否已认证 */ function isAuthenticated() { - const currentAuthToken = (typeof window !== 'undefined' ? window.authToken : null) || localStorage.getItem('admin_token'); + const currentAuthToken = (typeof window !== 'undefined' ? window.authToken : null) || localStorage.getItem('auth_token'); return !!currentAuthToken; } diff --git a/themes/2025/admin/js/config-simple.js b/themes/2025/admin/js/config-simple.js index d8d4004..83c43a2 100644 --- a/themes/2025/admin/js/config-simple.js +++ b/themes/2025/admin/js/config-simple.js @@ -49,7 +49,7 @@ function fillConfigForm(config) { setFieldValue('base_name', config.base?.name); setFieldValue('base_description', config.base?.description); setFieldValue('base_keywords', config.base?.keywords); - setFieldValue('admin_token', ''); // 不回显密码 + // 管理员令牌字段已移除,不回显任何敏感字段 setFieldValue('notify_title', config.notify_title); setFieldValue('notify_content', config.notify_content); setFieldValue('page_explain', config.page_explain); @@ -167,11 +167,7 @@ async function handleConfigSubmit(e) { themes_select: getFieldValue('themes_select') }; - // 如果密码字段有值,添加到配置中 - const adminToken = getFieldValue('admin_token'); - if (adminToken && adminToken.trim()) { - config.admin_token = adminToken.trim(); - } + // 管理员令牌字段已移除,不会写入到配置中。 console.log('准备提交的配置:', config); @@ -182,8 +178,7 @@ async function handleConfigSubmit(e) { if (result.code === 200) { safeShowAlert('配置保存成功!', 'success'); - // 清空密码字段 - setFieldValue('admin_token', ''); + // 管理员令牌字段已移除,无需清空 } else { throw new Error(result.message || '保存失败'); } diff --git a/themes/2025/admin/js/users.js b/themes/2025/admin/js/users.js index 1157095..33ff7bf 100644 --- a/themes/2025/admin/js/users.js +++ b/themes/2025/admin/js/users.js @@ -945,7 +945,7 @@ async function exportUsers() { try { safeShowAlert('正在导出用户列表...', 'info'); - const currentAuthToken = window.authToken || localStorage.getItem('admin_token'); + const currentAuthToken = window.authToken || localStorage.getItem('auth_token'); const response = await fetch('/admin/users/export', { headers: { 'Authorization': `Bearer ${currentAuthToken}` diff --git a/themes/2025/setup.html b/themes/2025/setup.html index 64af942..4a952e2 100644 --- a/themes/2025/setup.html +++ b/themes/2025/setup.html @@ -236,10 +236,10 @@ 创建系统管理员账户,用于管理系统配置和用户。
-
- - +
From 719ddcd0d3422f0a1c2f332b3b7571ed7a2a5f20 Mon Sep 17 00:00:00 2001 From: murphyyi Date: Wed, 17 Sep 2025 12:44:05 +0800 Subject: [PATCH 09/21] chore: remove top-level legacy export scripts and KeyValue model cleanup; add dto placeholder --- internal/handlers/admin.go | 2 +- internal/models/db/chunk.go | 29 +++++---- internal/models/db/session.go | 16 ----- internal/models/db/user.go | 61 +++++++++++++------ .../models/{dto => dto-}/config_updates.go | 0 internal/models/{dto => dto-}/user_updates.go | 0 internal/models/dto/README.md | 5 ++ internal/repository/user.go | 18 +++--- internal/services/admin/auth.go | 10 +-- internal/services/admin/users.go | 43 ++----------- internal/services/user/profile.go | 8 +-- 11 files changed, 87 insertions(+), 105 deletions(-) rename internal/models/{dto => dto-}/config_updates.go (100%) rename internal/models/{dto => dto-}/user_updates.go (100%) create mode 100644 internal/models/dto/README.md diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go index 434caeb..a9c9609 100644 --- a/internal/handlers/admin.go +++ b/internal/handlers/admin.go @@ -677,7 +677,7 @@ func (h *AdminHandler) UpdateUser(c *gin.Context) { } // 更新用户 - err = h.service.UpdateUser(uint(userID64), userData.Email, userData.Password, userData.Nickname, role, status) + err = h.service.UpdateUser(userData, role, status) if err != nil { common.InternalServerErrorResponse(c, "更新用户失败: "+err.Error()) return diff --git a/internal/models/db/chunk.go b/internal/models/db/chunk.go index 8972d56..67cb833 100644 --- a/internal/models/db/chunk.go +++ b/internal/models/db/chunk.go @@ -8,25 +8,24 @@ import ( // UploadChunk 上传分片模型 type UploadChunk struct { - ID uint `gorm:"primarykey" json:"id"` - UploadID string `gorm:"index;size:36" json:"upload_id"` - ChunkIndex int `json:"chunk_index"` - ChunkHash string `gorm:"size:64" json:"chunk_hash"` - TotalChunks int `json:"total_chunks"` - FileSize int64 `json:"file_size"` - ChunkSize int `json:"chunk_size"` - FileName string `gorm:"size:255" json:"file_name"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Completed bool `gorm:"default:false" json:"completed"` - RetryCount int `gorm:"default:0" json:"retry_count"` // 重试次数 - LastError string `gorm:"type:text" json:"last_error"` // 最后错误信息 - Status string `gorm:"size:20;default:'pending'" json:"status"` // pending, uploading, completed, failed + gorm.Model + + UploadID string `gorm:"index;size:36" json:"upload_id"` + ChunkIndex int `json:"chunk_index"` + ChunkHash string `gorm:"size:64" json:"chunk_hash"` + TotalChunks int `json:"total_chunks"` + FileSize int64 `json:"file_size"` + ChunkSize int `json:"chunk_size"` + FileName string `gorm:"size:255" json:"file_name"` + + Completed bool `gorm:"default:false" json:"completed"` + RetryCount int `gorm:"default:0" json:"retry_count"` // 重试次数 + LastError string `gorm:"type:text" json:"last_error"` // 最后错误信息 + Status string `gorm:"size:20;default:'pending'" json:"status"` // pending, uploading, completed, failed } // ChunkQuery 分片查询条件 type ChunkQuery struct { - gorm.Model UploadID string `json:"upload_id"` ChunkIndex *int `json:"chunk_index"` Status string `json:"status"` diff --git a/internal/models/db/session.go b/internal/models/db/session.go index 93e48a5..741dbd8 100644 --- a/internal/models/db/session.go +++ b/internal/models/db/session.go @@ -38,19 +38,3 @@ type SessionUpdate struct { ExpiresAt *time.Time `json:"expires_at"` UpdatedAt *time.Time `json:"updated_at"` } - -// KeyValueQuery 键值对查询条件 -type KeyValueQuery struct { - gorm.Model - Key string `json:"key"` - KeyPrefix string `json:"key_prefix"` // 前缀匹配 - Limit int `json:"limit"` - Offset int `json:"offset"` -} - -// KeyValueUpdate 键值对更新数据 -type KeyValueUpdate struct { - gorm.Model - Value *string `json:"value"` - UpdatedAt *time.Time `json:"updated_at"` -} diff --git a/internal/models/db/user.go b/internal/models/db/user.go index e3bd115..41dec6a 100644 --- a/internal/models/db/user.go +++ b/internal/models/db/user.go @@ -30,7 +30,6 @@ type User struct { // UserQuery 用户查询条件 type UserQuery struct { - gorm.Model ID *uint `json:"id"` Username string `json:"username"` Email string `json:"email"` @@ -41,27 +40,55 @@ type UserQuery struct { Offset int `json:"offset"` } -// UserUpdate 用户更新数据 -type UserUpdate struct { - gorm.Model - Username *string `json:"username"` - Email *string `json:"email"` - PasswordHash *string `json:"password_hash"` - Nickname *string `json:"nickname"` - Avatar *string `json:"avatar"` - Role *string `json:"role"` - Status *string `json:"status"` - EmailVerified *bool `json:"email_verified"` - LastLoginAt *time.Time `json:"last_login_at"` - LastLoginIP *string `json:"last_login_ip"` -} - // UserStats 用户统计查询结果 type UserStats struct { - gorm.Model UserID uint `json:"user_id"` TotalUploads int `json:"total_uploads"` TotalDownloads int `json:"total_downloads"` TotalStorage int64 `json:"total_storage"` FileCount int `json:"file_count"` } + +// ToMap 将结构体转换为 map,只包含非空字段 +func (u *User) ToMap() map[string]interface{} { + updates := make(map[string]interface{}) + + if u.Email != "" { + updates["email"] = u.Email + } + if u.PasswordHash != "" { + updates["password_hash"] = u.PasswordHash + } + if u.Nickname != "" { + updates["nickname"] = u.Nickname + } + if u.Avatar != "" { + updates["avatar"] = u.Avatar + } + if u.Role != "" { + updates["role"] = u.Role + } + if u.Status != "" { + updates["status"] = u.Status + } + if !u.EmailVerified { + updates["email_verified"] = u.EmailVerified + } + if u.LastLoginAt != nil { + updates["last_login_at"] = u.LastLoginAt + } + if u.LastLoginIP != "" { + updates["last_login_ip"] = u.LastLoginIP + } + if u.TotalUploads != 0 { + updates["total_uploads"] = u.TotalUploads + } + if u.TotalDownloads != 0 { + updates["total_downloads"] = u.TotalDownloads + } + if u.TotalStorage != 0 { + updates["total_storage"] = u.TotalStorage + } + + return updates +} diff --git a/internal/models/dto/config_updates.go b/internal/models/dto-/config_updates.go similarity index 100% rename from internal/models/dto/config_updates.go rename to internal/models/dto-/config_updates.go diff --git a/internal/models/dto/user_updates.go b/internal/models/dto-/user_updates.go similarity index 100% rename from internal/models/dto/user_updates.go rename to internal/models/dto-/user_updates.go diff --git a/internal/models/dto/README.md b/internal/models/dto/README.md new file mode 100644 index 0000000..5b54719 --- /dev/null +++ b/internal/models/dto/README.md @@ -0,0 +1,5 @@ +This directory is a placeholder to allow `internal/models/dto` package. + +The real Go source files are present in the repository under `internal/models/dto-`. + +If you want to move files, rename the directory `dto-` to `dto`. diff --git a/internal/repository/user.go b/internal/repository/user.go index 8b251e3..7756367 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -74,23 +74,23 @@ func (dao *UserDAO) UpdateColumns(id uint, updates map[string]interface{}) error } // UpdateUserFields 更新用户字段(结构化方式) -func (dao *UserDAO) UpdateUserFields(id uint, updateFields *models.UserUpdateFields) error { - if updateFields == nil || !updateFields.HasUpdates() { +func (dao *UserDAO) UpdateUserFields(id uint, user models.User) error { + + userMap := user.ToMap() + if len(userMap) == 0 { return errors.New("没有需要更新的字段") } - updates := updateFields.ToMap() - return dao.db.Model(&models.User{}).Where("id = ?", id).Updates(updates).Error + return dao.db.Model(&models.User{}).Where("id = ?", id).Updates(userMap).Error } // UpdateUserProfile 更新用户资料(用户自己更新) -func (dao *UserDAO) UpdateUserProfile(id uint, profileFields *models.UserProfileUpdateFields) error { - if profileFields == nil || !profileFields.HasUpdates() { - return errors.New("没有需要更新的字段") +func (dao *UserDAO) UpdateUserProfile(id uint, user *models.User) error { + if user == nil { + return errors.New("用户信息不能为空") } - updates := profileFields.ToMap() - return dao.db.Model(&models.User{}).Where("id = ?", id).Updates(updates).Error + return dao.db.Model(&models.User{}).Where("id = ?", id).Updates(user.ToMap()).Error } // UpdateUserStats 更新用户统计信息 diff --git a/internal/services/admin/auth.go b/internal/services/admin/auth.go index c45c6ec..6e883bc 100644 --- a/internal/services/admin/auth.go +++ b/internal/services/admin/auth.go @@ -6,8 +6,8 @@ import ( "time" "github.com/golang-jwt/jwt/v4" + "github.com/zy84338719/filecodebox/internal/models" - "github.com/zy84338719/filecodebox/internal/models/dto" ) // GenerateToken 生成管理员JWT令牌 @@ -97,8 +97,8 @@ func (s *Service) ResetUserPassword(userID uint, newPassword string) error { return err } - updateFields := &dto.UserUpdateFields{ - PasswordHash: &hashedPassword, + updateFields := models.User{ + PasswordHash: hashedPassword, } return s.repositoryManager.User.UpdateUserFields(userID, updateFields) } @@ -152,8 +152,8 @@ func (s *Service) UpdateUserStatus(userID uint, isActive bool) error { status = "active" } - updateFields := &dto.UserUpdateFields{ - Status: &status, + updateFields := models.User{ + Status: status, } return s.repositoryManager.User.UpdateUserFields(userID, updateFields) } diff --git a/internal/services/admin/users.go b/internal/services/admin/users.go index 21d1af8..d12db96 100644 --- a/internal/services/admin/users.go +++ b/internal/services/admin/users.go @@ -64,40 +64,9 @@ func (s *Service) CreateUser(username, email, password, nickname, role, status s } // UpdateUser 更新用户 - 使用结构化更新 -func (s *Service) UpdateUser(id uint, email, password, nickname, role, status string) error { - // 准备更新字段 - updateFields := &models.UserUpdateFields{} +func (s *Service) UpdateUser(user models.User) error { - if email != "" { - // 检查邮箱是否已被其他用户使用 - existingUser, err := s.repositoryManager.User.CheckEmailExists(email, id) - if err == nil && existingUser != nil { - return errors.New("该邮箱已被其他用户使用") - } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("检查邮箱唯一性失败: %w", err) - } - updateFields.Email = &email - } - - if password != "" { - hashedPassword, err := s.authService.HashPassword(password) - if err != nil { - return err - } - updateFields.PasswordHash = &hashedPassword - } - - if nickname != "" { - updateFields.Nickname = &nickname - } - if role != "" { - updateFields.Role = &role - } - if status != "" { - updateFields.Status = &status - } - - return s.repositoryManager.User.UpdateUserFields(id, updateFields) + return s.repositoryManager.User.UpdateUserFields(user.ID, user) } // DeleteUser 删除用户 @@ -138,11 +107,9 @@ func (s *Service) ToggleUserStatus(id uint) error { newStatus = "active" } - updateFields := &models.UserUpdateFields{ - Status: &newStatus, - } - - return s.repositoryManager.User.UpdateUserFields(id, updateFields) + return s.repositoryManager.User.UpdateUserFields(id, models.User{ + Status: newStatus, + }) } // GetUserByID 根据ID获取用户 (兼容性方法) diff --git a/internal/services/user/profile.go b/internal/services/user/profile.go index 30709f0..bd6c98e 100644 --- a/internal/services/user/profile.go +++ b/internal/services/user/profile.go @@ -35,7 +35,7 @@ func (s *Service) UpdateProfile(userID uint, updates map[string]interface{}) err } // 准备结构化更新字段 - profileFields := &models.UserProfileUpdateFields{} + profileFields := &models.User{} // 检查邮箱是否已被其他用户使用 if email, ok := updates["email"]; ok { @@ -46,17 +46,17 @@ func (s *Service) UpdateProfile(userID uint, updates map[string]interface{}) err return errors.New("email already in use") } } - profileFields.Email = &emailStr + profileFields.Email = emailStr } if nickname, ok := updates["nickname"]; ok { nicknameStr := nickname.(string) - profileFields.Nickname = &nicknameStr + profileFields.Nickname = nicknameStr } if avatar, ok := updates["avatar"]; ok { avatarStr := avatar.(string) - profileFields.Avatar = &avatarStr + profileFields.Avatar = avatarStr } // 使用结构化更新 From f0195b4b0a104d7bfe5dabbc0fdf042ec817fb5e Mon Sep 17 00:00:00 2001 From: murphyyi Date: Wed, 17 Sep 2025 14:06:38 +0800 Subject: [PATCH 10/21] refactor(config): remove unused DTO placeholders and cleanup MCP helpers; persist config via YAML-first flow --- go.mod | 18 +- go.sum | 69 --- internal/cli/admin.go | 12 +- internal/config/mcp_config.go | 58 --- internal/database/database.go | 79 --- internal/handlers/admin.go | 32 +- internal/models/dto-/config_updates.go | 652 ------------------------- internal/models/dto-/user_updates.go | 142 ------ internal/models/dto/README.md | 5 - internal/models/models.go | 19 - internal/repository/user.go | 10 - internal/services/admin/config.go | 424 +--------------- internal/services/user/profile.go | 12 - 13 files changed, 63 insertions(+), 1469 deletions(-) delete mode 100644 internal/models/dto-/config_updates.go delete mode 100644 internal/models/dto-/user_updates.go delete mode 100644 internal/models/dto/README.md diff --git a/go.mod b/go.mod index 12b3b6e..7128acd 100644 --- a/go.mod +++ b/go.mod @@ -12,14 +12,18 @@ require ( github.com/gin-contrib/cors v1.4.0 github.com/gin-gonic/gin v1.8.2 github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/mattn/go-sqlite3 v1.14.32 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.9.0 github.com/studio-b12/gowebdav v0.10.0 github.com/swaggo/files v1.0.0 github.com/swaggo/gin-swagger v1.5.3 github.com/swaggo/swag v1.16.4 golang.org/x/crypto v0.21.0 + golang.org/x/term v0.18.0 golang.org/x/time v0.12.0 + gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.4.7 gorm.io/driver/sqlite v1.5.7 @@ -43,10 +47,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect github.com/aws/smithy-go v1.20.3 // indirect - github.com/bytedance/sonic v1.14.0 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect - github.com/cloudwego/base64x v0.1.6 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-openapi/jsonpointer v0.22.0 // indirect github.com/go-openapi/jsonreference v0.21.1 // indirect @@ -68,37 +68,27 @@ require ( github.com/go-playground/validator/v10 v10.11.2 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.2.0 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/spf13/cobra v1.9.0 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect - golang.org/x/arch v0.20.0 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.7.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2876e35..5f27fc0 100644 --- a/go.sum +++ b/go.sum @@ -42,25 +42,15 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= -github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= -github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= -github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= -github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= -github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -69,8 +59,6 @@ github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fq github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY= github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398= -github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= -github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= @@ -119,8 +107,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -134,8 +120,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -147,11 +131,7 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8= github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= @@ -162,8 +142,6 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -222,14 +200,12 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/studio-b12/gowebdav v0.10.0 h1:Yewz8FFiadcGEu4hxS/AAJQlHelndqln1bns3hcJIYc= @@ -237,21 +213,11 @@ github.com/studio-b12/gowebdav v0.10.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAc github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4= github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc= -github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= -github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= -github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= -github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= -github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo= -github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= -github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= -github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -260,26 +226,18 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= -golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -291,19 +249,12 @@ golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -319,7 +270,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -327,9 +277,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -339,12 +286,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -352,12 +295,8 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= -golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -383,19 +322,11 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= -gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= gorm.io/driver/postgres v1.4.7 h1:J06jXZCNq7Pdf7LIPn8tZn9LsWjd81BRSKveKNr0ZfA= gorm.io/driver/postgres v1.4.7/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4= -gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= -gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= -gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= -gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs= -gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/internal/cli/admin.go b/internal/cli/admin.go index 2b08e46..3afe1af 100644 --- a/internal/cli/admin.go +++ b/internal/cli/admin.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" _ "github.com/zy84338719/filecodebox/internal/config" _ "github.com/zy84338719/filecodebox/internal/database" + "github.com/zy84338719/filecodebox/internal/models" _ "github.com/zy84338719/filecodebox/internal/repository" _ "github.com/zy84338719/filecodebox/internal/services" "golang.org/x/term" @@ -114,7 +115,16 @@ var adminResetCmd = &cobra.Command{ } } - return adminSvc.UpdateUser(userID, "", newPass, "", "", "") + // Build models.User with ID and new password (service will handle update semantics) + user := models.User{} + // set ID via embedded gorm.Model field + // using reflection-free assignment + // gorm.Model exposes ID field; set using map-style assignment + // but we set via simple field since it's promoted + user.ID = userID + // Store new password in PasswordHash field temporarily; repository will map it to password_hash + user.PasswordHash = newPass + return adminSvc.UpdateUser(user) }, } diff --git a/internal/config/mcp_config.go b/internal/config/mcp_config.go index 4a3d74b..4ed16ac 100644 --- a/internal/config/mcp_config.go +++ b/internal/config/mcp_config.go @@ -53,42 +53,6 @@ func (mc *MCPConfig) IsMCPEnabled() bool { return mc.EnableMCPServer == 1 } -// GetMCPAddress 获取MCP服务器地址 -func (mc *MCPConfig) GetMCPAddress() string { - return fmt.Sprintf("%s:%s", mc.MCPHost, mc.MCPPort) -} - -// GetMCPPortInt 获取MCP端口号(整数) -func (mc *MCPConfig) GetMCPPortInt() (int, error) { - return strconv.Atoi(mc.MCPPort) -} - -// ToMap 转换为map格式 -func (mc *MCPConfig) ToMap() map[string]string { - return map[string]string{ - "enable_mcp_server": fmt.Sprintf("%d", mc.EnableMCPServer), - "mcp_port": mc.MCPPort, - "mcp_host": mc.MCPHost, - } -} - -// FromMap 从map加载配置 -func (mc *MCPConfig) FromMap(data map[string]string) error { - if val, ok := data["enable_mcp_server"]; ok { - if v, err := strconv.Atoi(val); err == nil { - mc.EnableMCPServer = v - } - } - if val, ok := data["mcp_port"]; ok { - mc.MCPPort = val - } - if val, ok := data["mcp_host"]; ok { - mc.MCPHost = val - } - - return mc.Validate() -} - // Update 更新配置 func (mc *MCPConfig) Update(updates map[string]interface{}) error { if enableMCP, ok := updates["enable_mcp_server"].(int); ok { @@ -112,25 +76,3 @@ func (mc *MCPConfig) Clone() *MCPConfig { MCPHost: mc.MCPHost, } } - -// EnableMCP 启用MCP服务器 -func (mc *MCPConfig) EnableMCP() { - mc.EnableMCPServer = 1 -} - -// DisableMCP 禁用MCP服务器 -func (mc *MCPConfig) DisableMCP() { - mc.EnableMCPServer = 0 -} - -// SetPort 设置端口 -func (mc *MCPConfig) SetPort(port string) error { - mc.MCPPort = port - return mc.Validate() -} - -// SetHost 设置主机地址 -func (mc *MCPConfig) SetHost(host string) error { - mc.MCPHost = host - return mc.Validate() -} diff --git a/internal/database/database.go b/internal/database/database.go index 8edcd6f..b7f95e4 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -53,85 +53,6 @@ func InitWithManager(manager *config.ConfigManager) (*gorm.DB, error) { return db, nil } -// Init 根据配置初始化数据库连接 (已废弃,请使用InitWithManager) -// func Init(cfg *config.Config) (*gorm.DB, error) { -// var db *gorm.DB -// var err error - -// gormConfig := &gorm.Config{ -// Logger: logger.Default.LogMode(logger.Silent), -// } - -// switch cfg.DatabaseType { -// case "sqlite": -// db, err = initSQLite(cfg, gormConfig) -// case "mysql": -// db, err = initMySQL(cfg, gormConfig) -// case "postgres", "postgresql": -// db, err = initPostgreSQL(cfg, gormConfig) -// default: -// return nil, fmt.Errorf("不支持的数据库类型: %s", cfg.DatabaseType) -// } - -// if err != nil { -// return nil, fmt.Errorf("初始化%s数据库失败: %w", cfg.DatabaseType, err) -// } - -// // 自动迁移模式 -// err = db.AutoMigrate( -// &models.FileCode{}, -// &models.UploadChunk{}, -// &models.KeyValue{}, -// &models.User{}, -// &models.UserSession{}, -// ) -// if err != nil { -// return nil, fmt.Errorf("数据库自动迁移失败: %w", err) -// } - -// return db, nil -// } - -// // initSQLite 初始化SQLite数据库 (已废弃) -// func initSQLite(cfg *config.Config, gormConfig *gorm.Config) (*gorm.DB, error) { -// dbPath := cfg.DataPath + "/filecodebox.db" - -// // 确保目录存在 -// if err := os.MkdirAll(cfg.DataPath, 0750); err != nil { -// return nil, fmt.Errorf("创建SQLite数据目录失败: %w", err) -// } - -// return gorm.Open(sqlite.Open(dbPath), gormConfig) -// } - -// // initMySQL 初始化MySQL数据库 (已废弃) -// func initMySQL(cfg *config.Config, gormConfig *gorm.Config) (*gorm.DB, error) { -// dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", -// cfg.DatabaseUser, -// cfg.DatabasePass, -// cfg.DatabaseHost, -// cfg.DatabasePort, -// cfg.DatabaseName, -// ) - -// return gorm.Open(mysql.Open(dsn), gormConfig) -// } - -// // initPostgreSQL 初始化PostgreSQL数据库 (已废弃) -// func initPostgreSQL(cfg *config.Config, gormConfig *gorm.Config) (*gorm.DB, error) { -// dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=Asia/Shanghai", -// cfg.DatabaseHost, -// cfg.DatabaseUser, -// cfg.DatabasePass, -// cfg.DatabaseName, -// cfg.DatabasePort, -// cfg.DatabaseSSL, -// ) - -// return gorm.Open(postgres.Open(dsn), gormConfig) -// } - -// initSQLiteWithManager 使用配置管理器初始化SQLite数据库 func initSQLiteWithManager(manager *config.ConfigManager, gormConfig *gorm.Config) (*gorm.DB, error) { dbPath := manager.Base.DataPath + "/filecodebox.db" diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go index a9c9609..e83067a 100644 --- a/internal/handlers/admin.go +++ b/internal/handlers/admin.go @@ -143,19 +143,16 @@ func (h *AdminHandler) GetConfig(c *gin.Context) { // UpdateConfig 更新配置 func (h *AdminHandler) UpdateConfig(c *gin.Context) { - // 尝试绑定到结构化配置更新DTO - var configUpdate models.ConfigUpdateFields - if err := c.ShouldBind(&configUpdate); err != nil { + // 绑定为 AdminConfigRequest 并使用服务层处理(服务会构建 map 并持久化) + var req web.AdminConfigRequest + if err := c.ShouldBind(&req); err != nil { common.BadRequestResponse(c, "请求参数错误: "+err.Error()) return } - if configUpdate.HasUpdates() { - // 使用结构化配置更新 - if err := h.service.UpdateConfigWithDTO(&configUpdate); err != nil { - common.InternalServerErrorResponse(c, "更新配置失败: "+err.Error()) - return - } + if err := h.service.UpdateConfigFromRequest(&req); err != nil { + common.InternalServerErrorResponse(c, "更新配置失败: "+err.Error()) + return } common.SuccessWithMessage(c, "更新成功", nil) @@ -676,8 +673,21 @@ func (h *AdminHandler) UpdateUser(c *gin.Context) { status = "inactive" } - // 更新用户 - err = h.service.UpdateUser(userData, role, status) + // 更新用户:构建 models.User 并调用服务方法 + // 构建更新用的 models.User:只填充仓库 UpdateUserFields 所需字段 + user := models.User{} + // ID will be used by UpdateUserFields via repository; ensure repository method uses provided ID + // NOTE: models.User uses gorm.Model embed; set via zero-value and pass id to repository + user.Email = userData.Email + if userData.Password != "" { + // Hashing handled inside service layer; here we pass raw password in a convention used elsewhere + user.PasswordHash = userData.Password + } + user.Nickname = userData.Nickname + user.Role = role + user.Status = status + + err = h.service.UpdateUser(user) if err != nil { common.InternalServerErrorResponse(c, "更新用户失败: "+err.Error()) return diff --git a/internal/models/dto-/config_updates.go b/internal/models/dto-/config_updates.go deleted file mode 100644 index d7ee9dd..0000000 --- a/internal/models/dto-/config_updates.go +++ /dev/null @@ -1,652 +0,0 @@ -package dto - -// ConfigUpdateFields 配置更新字段结构体 -type ConfigUpdateFields struct { - Base *BaseConfigUpdate `json:"base,omitempty"` - Transfer *TransferConfigUpdate `json:"transfer,omitempty"` - User *UserConfigUpdate `json:"user,omitempty"` - Storage *StorageConfigUpdate `json:"storage,omitempty"` - MCP *MCPConfigUpdate `json:"mcp,omitempty"` - - // 其他配置字段 - NotifyTitle *string `json:"notify_title,omitempty"` - NotifyContent *string `json:"notify_content,omitempty"` - PageExplain *string `json:"page_explain,omitempty"` - Opacity *int `json:"opacity,omitempty"` - ThemesSelect *string `json:"themes_select,omitempty"` -} - -// BaseConfigUpdate 基础配置更新 -type BaseConfigUpdate struct { - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - Keywords *string `json:"keywords,omitempty"` - Port *int `json:"port,omitempty"` - Host *string `json:"host,omitempty"` - DataPath *string `json:"data_path,omitempty"` - Production *bool `json:"production,omitempty"` -} - -// TransferConfigUpdate 传输配置更新 -type TransferConfigUpdate struct { - Upload *UploadConfigUpdate `json:"upload,omitempty"` - Download *DownloadConfigUpdate `json:"download,omitempty"` -} - -// UploadConfigUpdate 上传配置更新 -type UploadConfigUpdate struct { - OpenUpload *int `json:"open_upload,omitempty"` - UploadSize *int64 `json:"upload_size,omitempty"` - EnableChunk *int `json:"enable_chunk,omitempty"` - ChunkSize *int64 `json:"chunk_size,omitempty"` - MaxSaveSeconds *int `json:"max_save_seconds,omitempty"` -} - -// DownloadConfigUpdate 下载配置更新 -type DownloadConfigUpdate struct { - EnableConcurrentDownload *int `json:"enable_concurrent_download,omitempty"` - MaxConcurrentDownloads *int `json:"max_concurrent_downloads,omitempty"` - DownloadTimeout *int `json:"download_timeout,omitempty"` -} - -// UserConfigUpdate 用户配置更新 -type UserConfigUpdate struct { - AllowUserRegistration *int `json:"allow_user_registration,omitempty"` - RequireEmailVerify *int `json:"require_email_verify,omitempty"` - UserUploadSize *int64 `json:"user_upload_size,omitempty"` - UserStorageQuota *int64 `json:"user_storage_quota,omitempty"` - SessionExpiryHours *int `json:"session_expiry_hours,omitempty"` - MaxSessionsPerUser *int `json:"max_sessions_per_user,omitempty"` - JWTSecret *string `json:"jwt_secret,omitempty"` -} - -// StorageConfigUpdate 存储配置更新 -type StorageConfigUpdate struct { - FileStorage *string `json:"file_storage,omitempty"` - StoragePath *string `json:"storage_path,omitempty"` - S3 *S3ConfigUpdate `json:"s3,omitempty"` - WebDAV *WebDAVConfigUpdate `json:"webdav,omitempty"` - OneDrive *OneDriveConfigUpdate `json:"onedrive,omitempty"` - NFS *NFSConfigUpdate `json:"nfs,omitempty"` -} - -// S3ConfigUpdate S3存储配置更新 -type S3ConfigUpdate struct { - S3AccessKeyID *string `json:"s3_access_key_id,omitempty"` - S3SecretAccessKey *string `json:"s3_secret_access_key,omitempty"` - S3BucketName *string `json:"s3_bucket_name,omitempty"` - S3EndpointURL *string `json:"s3_endpoint_url,omitempty"` - S3RegionName *string `json:"s3_region_name,omitempty"` - S3SignatureVersion *string `json:"s3_signature_version,omitempty"` - S3Hostname *string `json:"s3_hostname,omitempty"` - S3Proxy *int `json:"s3_proxy,omitempty"` - AWSSessionToken *string `json:"aws_session_token,omitempty"` -} - -// WebDAVConfigUpdate WebDAV存储配置更新 -type WebDAVConfigUpdate struct { - WebDAVHostname *string `json:"webdav_hostname,omitempty"` - WebDAVRootPath *string `json:"webdav_root_path,omitempty"` - WebDAVProxy *int `json:"webdav_proxy,omitempty"` - WebDAVURL *string `json:"webdav_url,omitempty"` - WebDAVPassword *string `json:"webdav_password,omitempty"` - WebDAVUsername *string `json:"webdav_username,omitempty"` -} - -// OneDriveConfigUpdate OneDrive存储配置更新 -type OneDriveConfigUpdate struct { - OneDriveDomain *string `json:"onedrive_domain,omitempty"` - OneDriveClientID *string `json:"onedrive_client_id,omitempty"` - OneDriveUsername *string `json:"onedrive_username,omitempty"` - OneDrivePassword *string `json:"onedrive_password,omitempty"` - OneDriveRootPath *string `json:"onedrive_root_path,omitempty"` - OneDriveProxy *int `json:"onedrive_proxy,omitempty"` -} - -// NFSConfigUpdate NFS存储配置更新 -type NFSConfigUpdate struct { - NFSServer *string `json:"nfs_server,omitempty"` - NFSPath *string `json:"nfs_path,omitempty"` - NFSMountPoint *string `json:"nfs_mount_point,omitempty"` - NFSVersion *string `json:"nfs_version,omitempty"` - NFSOptions *string `json:"nfs_options,omitempty"` - NFSTimeout *int `json:"nfs_timeout,omitempty"` - NFSAutoMount *int `json:"nfs_auto_mount,omitempty"` - NFSRetryCount *int `json:"nfs_retry_count,omitempty"` - NFSSubPath *string `json:"nfs_sub_path,omitempty"` -} - -// MCPConfigUpdate MCP配置更新 -type MCPConfigUpdate struct { - EnableMCPServer *int `json:"enable_mcp_server,omitempty"` - MCPPort *string `json:"mcp_port,omitempty"` - MCPHost *string `json:"mcp_host,omitempty"` -} - -// ToMap 将结构体转换为 map,只包含非空字段 -func (c *ConfigUpdateFields) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if c.Base != nil { - baseMap := c.Base.ToMap() - if len(baseMap) > 0 { - updates["base"] = baseMap - } - } - - if c.Transfer != nil { - transferMap := c.Transfer.ToMap() - if len(transferMap) > 0 { - updates["transfer"] = transferMap - } - } - - if c.User != nil { - userMap := c.User.ToMap() - if len(userMap) > 0 { - updates["user"] = userMap - } - } - - if c.Storage != nil { - storageMap := c.Storage.ToMap() - if len(storageMap) > 0 { - updates["storage"] = storageMap - } - } - - if c.MCP != nil { - mcpMap := c.MCP.ToMap() - if len(mcpMap) > 0 { - updates["mcp"] = mcpMap - } - } - - if c.NotifyTitle != nil { - updates["notify_title"] = *c.NotifyTitle - } - if c.NotifyContent != nil { - updates["notify_content"] = *c.NotifyContent - } - if c.PageExplain != nil { - updates["page_explain"] = *c.PageExplain - } - if c.Opacity != nil { - updates["opacity"] = *c.Opacity - } - if c.ThemesSelect != nil { - updates["themes_select"] = *c.ThemesSelect - } - - return updates -} - -// ToMap 将基础配置转换为 map -func (b *BaseConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if b.Name != nil { - updates["name"] = *b.Name - } - if b.Description != nil { - updates["description"] = *b.Description - } - if b.Keywords != nil { - updates["keywords"] = *b.Keywords - } - if b.Port != nil { - updates["port"] = *b.Port - } - if b.Host != nil { - updates["host"] = *b.Host - } - if b.DataPath != nil { - updates["data_path"] = *b.DataPath - } - if b.Production != nil { - updates["production"] = *b.Production - } - - return updates -} - -// ToMap 将传输配置转换为 map -func (t *TransferConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if t.Upload != nil { - uploadMap := t.Upload.ToMap() - if len(uploadMap) > 0 { - updates["upload"] = uploadMap - } - } - - if t.Download != nil { - downloadMap := t.Download.ToMap() - if len(downloadMap) > 0 { - updates["download"] = downloadMap - } - } - - return updates -} - -// ToMap 将上传配置转换为 map -func (u *UploadConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if u.OpenUpload != nil { - updates["open_upload"] = *u.OpenUpload - } - if u.UploadSize != nil { - updates["upload_size"] = *u.UploadSize - } - if u.EnableChunk != nil { - updates["enable_chunk"] = *u.EnableChunk - } - if u.ChunkSize != nil { - updates["chunk_size"] = *u.ChunkSize - } - if u.MaxSaveSeconds != nil { - updates["max_save_seconds"] = *u.MaxSaveSeconds - } - - return updates -} - -// ToMap 将下载配置转换为 map -func (d *DownloadConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if d.EnableConcurrentDownload != nil { - updates["enable_concurrent_download"] = *d.EnableConcurrentDownload - } - if d.MaxConcurrentDownloads != nil { - updates["max_concurrent_downloads"] = *d.MaxConcurrentDownloads - } - if d.DownloadTimeout != nil { - updates["download_timeout"] = *d.DownloadTimeout - } - - return updates -} - -// ToMap 将用户配置转换为 map -func (u *UserConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if u.AllowUserRegistration != nil { - updates["allow_user_registration"] = *u.AllowUserRegistration - } - if u.RequireEmailVerify != nil { - updates["require_email_verify"] = *u.RequireEmailVerify - } - if u.UserUploadSize != nil { - updates["user_upload_size"] = *u.UserUploadSize - } - if u.UserStorageQuota != nil { - updates["user_storage_quota"] = *u.UserStorageQuota - } - if u.SessionExpiryHours != nil { - updates["session_expiry_hours"] = *u.SessionExpiryHours - } - if u.MaxSessionsPerUser != nil { - updates["max_sessions_per_user"] = *u.MaxSessionsPerUser - } - if u.JWTSecret != nil { - updates["jwt_secret"] = *u.JWTSecret - } - - return updates -} - -// ToMap 将MCP配置转换为 map -func (m *MCPConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if m.EnableMCPServer != nil { - updates["enable_mcp_server"] = *m.EnableMCPServer - } - if m.MCPPort != nil { - updates["mcp_port"] = *m.MCPPort - } - if m.MCPHost != nil { - updates["mcp_host"] = *m.MCPHost - } - - return updates -} - -// ToMap 将存储配置转换为 map -func (s *StorageConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if s.FileStorage != nil { - updates["file_storage"] = *s.FileStorage - } - if s.StoragePath != nil { - updates["storage_path"] = *s.StoragePath - } - if s.S3 != nil { - s3Map := s.S3.ToMap() - if len(s3Map) > 0 { - updates["s3"] = s3Map - } - } - if s.WebDAV != nil { - webdavMap := s.WebDAV.ToMap() - if len(webdavMap) > 0 { - updates["webdav"] = webdavMap - } - } - if s.OneDrive != nil { - onedriveMap := s.OneDrive.ToMap() - if len(onedriveMap) > 0 { - updates["onedrive"] = onedriveMap - } - } - if s.NFS != nil { - nfsMap := s.NFS.ToMap() - if len(nfsMap) > 0 { - updates["nfs"] = nfsMap - } - } - - return updates -} - -// ToMap 将S3配置转换为 map -func (s *S3ConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if s.S3AccessKeyID != nil { - updates["s3_access_key_id"] = *s.S3AccessKeyID - } - if s.S3SecretAccessKey != nil { - updates["s3_secret_access_key"] = *s.S3SecretAccessKey - } - if s.S3BucketName != nil { - updates["s3_bucket_name"] = *s.S3BucketName - } - if s.S3EndpointURL != nil { - updates["s3_endpoint_url"] = *s.S3EndpointURL - } - if s.S3RegionName != nil { - updates["s3_region_name"] = *s.S3RegionName - } - if s.S3SignatureVersion != nil { - updates["s3_signature_version"] = *s.S3SignatureVersion - } - if s.S3Hostname != nil { - updates["s3_hostname"] = *s.S3Hostname - } - if s.S3Proxy != nil { - updates["s3_proxy"] = *s.S3Proxy - } - if s.AWSSessionToken != nil { - updates["aws_session_token"] = *s.AWSSessionToken - } - - return updates -} - -// ToMap 将WebDAV配置转换为 map -func (w *WebDAVConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if w.WebDAVHostname != nil { - updates["webdav_hostname"] = *w.WebDAVHostname - } - if w.WebDAVRootPath != nil { - updates["webdav_root_path"] = *w.WebDAVRootPath - } - if w.WebDAVProxy != nil { - updates["webdav_proxy"] = *w.WebDAVProxy - } - if w.WebDAVURL != nil { - updates["webdav_url"] = *w.WebDAVURL - } - if w.WebDAVPassword != nil { - updates["webdav_password"] = *w.WebDAVPassword - } - if w.WebDAVUsername != nil { - updates["webdav_username"] = *w.WebDAVUsername - } - - return updates -} - -// ToMap 将OneDrive配置转换为 map -func (o *OneDriveConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if o.OneDriveDomain != nil { - updates["onedrive_domain"] = *o.OneDriveDomain - } - if o.OneDriveClientID != nil { - updates["onedrive_client_id"] = *o.OneDriveClientID - } - if o.OneDriveUsername != nil { - updates["onedrive_username"] = *o.OneDriveUsername - } - if o.OneDrivePassword != nil { - updates["onedrive_password"] = *o.OneDrivePassword - } - if o.OneDriveRootPath != nil { - updates["onedrive_root_path"] = *o.OneDriveRootPath - } - if o.OneDriveProxy != nil { - updates["onedrive_proxy"] = *o.OneDriveProxy - } - - return updates -} - -// ToMap 将NFS配置转换为 map -func (n *NFSConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if n.NFSServer != nil { - updates["nfs_server"] = *n.NFSServer - } - if n.NFSPath != nil { - updates["nfs_path"] = *n.NFSPath - } - if n.NFSMountPoint != nil { - updates["nfs_mount_point"] = *n.NFSMountPoint - } - if n.NFSVersion != nil { - updates["nfs_version"] = *n.NFSVersion - } - if n.NFSOptions != nil { - updates["nfs_options"] = *n.NFSOptions - } - if n.NFSTimeout != nil { - updates["nfs_timeout"] = *n.NFSTimeout - } - if n.NFSAutoMount != nil { - updates["nfs_auto_mount"] = *n.NFSAutoMount - } - if n.NFSRetryCount != nil { - updates["nfs_retry_count"] = *n.NFSRetryCount - } - if n.NFSSubPath != nil { - updates["nfs_sub_path"] = *n.NFSSubPath - } - - return updates -} - -// HasUpdates 检查是否有任何更新字段 -func (c *ConfigUpdateFields) HasUpdates() bool { - return c.Base != nil || c.Transfer != nil || c.User != nil || - c.Storage != nil || c.MCP != nil || - c.NotifyTitle != nil || c.NotifyContent != nil || - c.PageExplain != nil || c.Opacity != nil || c.ThemesSelect != nil -} - -// FlatConfigUpdate 平面化配置更新(用于兼容老的API格式) -type FlatConfigUpdate struct { - // 基础配置 - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - Keywords *string `json:"keywords,omitempty"` - Port *int `json:"port,omitempty"` - Host *string `json:"host,omitempty"` - DataPath *string `json:"data_path,omitempty"` - Production *bool `json:"production,omitempty"` - - // 传输配置 - OpenUpload *int `json:"open_upload,omitempty"` - UploadSize *int64 `json:"upload_size,omitempty"` - EnableChunk *int `json:"enable_chunk,omitempty"` - ChunkSize *int64 `json:"chunk_size,omitempty"` - MaxSaveSeconds *int `json:"max_save_seconds,omitempty"` - EnableConcurrentDownload *int `json:"enable_concurrent_download,omitempty"` - MaxConcurrentDownloads *int `json:"max_concurrent_downloads,omitempty"` - DownloadTimeout *int `json:"download_timeout,omitempty"` - - // 用户配置 - AllowUserRegistration *int `json:"allow_user_registration,omitempty"` - RequireEmailVerify *int `json:"require_email_verify,omitempty"` - UserUploadSize *int64 `json:"user_upload_size,omitempty"` - UserStorageQuota *int64 `json:"user_storage_quota,omitempty"` - SessionExpiryHours *int `json:"session_expiry_hours,omitempty"` - MaxSessionsPerUser *int `json:"max_sessions_per_user,omitempty"` - JWTSecret *string `json:"jwt_secret,omitempty"` - - // MCP配置 - EnableMCPServer *int `json:"enable_mcp_server,omitempty"` - MCPPort *string `json:"mcp_port,omitempty"` - MCPHost *string `json:"mcp_host,omitempty"` - - // 其他配置 - NotifyTitle *string `json:"notify_title,omitempty"` - NotifyContent *string `json:"notify_content,omitempty"` - PageExplain *string `json:"page_explain,omitempty"` - Opacity *int `json:"opacity,omitempty"` - ThemesSelect *string `json:"themes_select,omitempty"` -} - -// ToMap 将平面化配置转换为 map -func (f *FlatConfigUpdate) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - // 基础配置 - if f.Name != nil { - updates["name"] = *f.Name - } - if f.Description != nil { - updates["description"] = *f.Description - } - if f.Keywords != nil { - updates["keywords"] = *f.Keywords - } - if f.Port != nil { - updates["port"] = *f.Port - } - if f.Host != nil { - updates["host"] = *f.Host - } - if f.DataPath != nil { - updates["data_path"] = *f.DataPath - } - if f.Production != nil { - updates["production"] = *f.Production - } - - // 传输配置 - if f.OpenUpload != nil { - updates["open_upload"] = *f.OpenUpload - } - if f.UploadSize != nil { - updates["upload_size"] = *f.UploadSize - } - if f.EnableChunk != nil { - updates["enable_chunk"] = *f.EnableChunk - } - if f.ChunkSize != nil { - updates["chunk_size"] = *f.ChunkSize - } - if f.MaxSaveSeconds != nil { - updates["max_save_seconds"] = *f.MaxSaveSeconds - } - if f.EnableConcurrentDownload != nil { - updates["enable_concurrent_download"] = *f.EnableConcurrentDownload - } - if f.MaxConcurrentDownloads != nil { - updates["max_concurrent_downloads"] = *f.MaxConcurrentDownloads - } - if f.DownloadTimeout != nil { - updates["download_timeout"] = *f.DownloadTimeout - } - - // 用户配置 - if f.AllowUserRegistration != nil { - updates["allow_user_registration"] = *f.AllowUserRegistration - } - if f.RequireEmailVerify != nil { - updates["require_email_verify"] = *f.RequireEmailVerify - } - if f.UserUploadSize != nil { - updates["user_upload_size"] = *f.UserUploadSize - } - if f.UserStorageQuota != nil { - updates["user_storage_quota"] = *f.UserStorageQuota - } - if f.SessionExpiryHours != nil { - updates["session_expiry_hours"] = *f.SessionExpiryHours - } - if f.MaxSessionsPerUser != nil { - updates["max_sessions_per_user"] = *f.MaxSessionsPerUser - } - if f.JWTSecret != nil { - updates["jwt_secret"] = *f.JWTSecret - } - - // MCP配置 - if f.EnableMCPServer != nil { - updates["enable_mcp_server"] = *f.EnableMCPServer - } - if f.MCPPort != nil { - updates["mcp_port"] = *f.MCPPort - } - if f.MCPHost != nil { - updates["mcp_host"] = *f.MCPHost - } - - // 其他配置 - if f.NotifyTitle != nil { - updates["notify_title"] = *f.NotifyTitle - } - if f.NotifyContent != nil { - updates["notify_content"] = *f.NotifyContent - } - if f.PageExplain != nil { - updates["page_explain"] = *f.PageExplain - } - if f.Opacity != nil { - updates["opacity"] = *f.Opacity - } - if f.ThemesSelect != nil { - updates["themes_select"] = *f.ThemesSelect - } - - return updates -} - -// HasUpdates 检查是否有任何更新字段 -func (f *FlatConfigUpdate) HasUpdates() bool { - return f.Name != nil || f.Description != nil || f.Keywords != nil || - f.Port != nil || f.Host != nil || f.DataPath != nil || f.Production != nil || - f.OpenUpload != nil || f.UploadSize != nil || f.EnableChunk != nil || - f.ChunkSize != nil || f.MaxSaveSeconds != nil || - f.EnableConcurrentDownload != nil || f.MaxConcurrentDownloads != nil || - f.DownloadTimeout != nil || f.AllowUserRegistration != nil || - f.RequireEmailVerify != nil || f.UserUploadSize != nil || - f.UserStorageQuota != nil || f.SessionExpiryHours != nil || - f.MaxSessionsPerUser != nil || f.JWTSecret != nil || - f.EnableMCPServer != nil || f.MCPPort != nil || f.MCPHost != nil || - f.NotifyTitle != nil || f.NotifyContent != nil || - f.PageExplain != nil || f.Opacity != nil || f.ThemesSelect != nil -} diff --git a/internal/models/dto-/user_updates.go b/internal/models/dto-/user_updates.go deleted file mode 100644 index 1cde5c0..0000000 --- a/internal/models/dto-/user_updates.go +++ /dev/null @@ -1,142 +0,0 @@ -package dto - -import "time" - -// UserUpdateFields 用户更新字段结构体 -type UserUpdateFields struct { - Email *string `json:"email,omitempty"` - PasswordHash *string `json:"password_hash,omitempty"` - Nickname *string `json:"nickname,omitempty"` - Avatar *string `json:"avatar,omitempty"` - Role *string `json:"role,omitempty"` - Status *string `json:"status,omitempty"` - EmailVerified *bool `json:"email_verified,omitempty"` - LastLoginAt *time.Time `json:"last_login_at,omitempty"` - LastLoginIP *string `json:"last_login_ip,omitempty"` - TotalUploads *int `json:"total_uploads,omitempty"` - TotalDownloads *int `json:"total_downloads,omitempty"` - TotalStorage *int64 `json:"total_storage,omitempty"` -} - -// ToMap 将结构体转换为 map,只包含非空字段 -func (u *UserUpdateFields) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if u.Email != nil { - updates["email"] = *u.Email - } - if u.PasswordHash != nil { - updates["password_hash"] = *u.PasswordHash - } - if u.Nickname != nil { - updates["nickname"] = *u.Nickname - } - if u.Avatar != nil { - updates["avatar"] = *u.Avatar - } - if u.Role != nil { - updates["role"] = *u.Role - } - if u.Status != nil { - updates["status"] = *u.Status - } - if u.EmailVerified != nil { - updates["email_verified"] = *u.EmailVerified - } - if u.LastLoginAt != nil { - updates["last_login_at"] = *u.LastLoginAt - } - if u.LastLoginIP != nil { - updates["last_login_ip"] = *u.LastLoginIP - } - if u.TotalUploads != nil { - updates["total_uploads"] = *u.TotalUploads - } - if u.TotalDownloads != nil { - updates["total_downloads"] = *u.TotalDownloads - } - if u.TotalStorage != nil { - updates["total_storage"] = *u.TotalStorage - } - - return updates -} - -// HasUpdates 检查是否有任何更新字段 -func (u *UserUpdateFields) HasUpdates() bool { - return u.Email != nil || u.PasswordHash != nil || u.Nickname != nil || - u.Avatar != nil || u.Role != nil || u.Status != nil || - u.EmailVerified != nil || u.LastLoginAt != nil || u.LastLoginIP != nil || - u.TotalUploads != nil || u.TotalDownloads != nil || u.TotalStorage != nil -} - -// UserProfileUpdateFields 用户资料更新字段(用户自己更新) -type UserProfileUpdateFields struct { - Email *string `json:"email,omitempty"` - Nickname *string `json:"nickname,omitempty"` - Avatar *string `json:"avatar,omitempty"` - PasswordHash *string `json:"password_hash,omitempty"` -} - -// ToMap 将用户资料更新字段转换为 map -func (u *UserProfileUpdateFields) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if u.Email != nil { - updates["email"] = *u.Email - } - if u.Nickname != nil { - updates["nickname"] = *u.Nickname - } - if u.Avatar != nil { - updates["avatar"] = *u.Avatar - } - if u.PasswordHash != nil { - updates["password_hash"] = *u.PasswordHash - } - - return updates -} - -// HasUpdates 检查是否有任何更新字段 -func (u *UserProfileUpdateFields) HasUpdates() bool { - return u.Email != nil || u.Nickname != nil || u.Avatar != nil || u.PasswordHash != nil -} - -// UserStatsUpdateFields 用户统计信息更新字段 -type UserStatsUpdateFields struct { - TotalUploads *int `json:"total_uploads,omitempty"` - TotalDownloads *int `json:"total_downloads,omitempty"` - TotalStorage *int64 `json:"total_storage,omitempty"` - LastLoginAt *time.Time `json:"last_login_at,omitempty"` - LastLoginIP *string `json:"last_login_ip,omitempty"` -} - -// ToMap 将用户统计更新字段转换为 map -func (u *UserStatsUpdateFields) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if u.TotalUploads != nil { - updates["total_uploads"] = *u.TotalUploads - } - if u.TotalDownloads != nil { - updates["total_downloads"] = *u.TotalDownloads - } - if u.TotalStorage != nil { - updates["total_storage"] = *u.TotalStorage - } - if u.LastLoginAt != nil { - updates["last_login_at"] = *u.LastLoginAt - } - if u.LastLoginIP != nil { - updates["last_login_ip"] = *u.LastLoginIP - } - - return updates -} - -// HasUpdates 检查是否有任何更新字段 -func (u *UserStatsUpdateFields) HasUpdates() bool { - return u.TotalUploads != nil || u.TotalDownloads != nil || u.TotalStorage != nil || - u.LastLoginAt != nil || u.LastLoginIP != nil -} diff --git a/internal/models/dto/README.md b/internal/models/dto/README.md deleted file mode 100644 index 5b54719..0000000 --- a/internal/models/dto/README.md +++ /dev/null @@ -1,5 +0,0 @@ -This directory is a placeholder to allow `internal/models/dto` package. - -The real Go source files are present in the repository under `internal/models/dto-`. - -If you want to move files, rename the directory `dto-` to `dto`. diff --git a/internal/models/models.go b/internal/models/models.go index 3f7c2dd..c215d12 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -3,7 +3,6 @@ package models import ( "github.com/zy84338719/filecodebox/internal/models/db" - "github.com/zy84338719/filecodebox/internal/models/dto" "github.com/zy84338719/filecodebox/internal/models/mcp" "github.com/zy84338719/filecodebox/internal/models/service" ) @@ -54,24 +53,6 @@ type ( ChunkVerifyResponse = service.ChunkVerifyResponse ChunkUploadCompleteResponse = service.ChunkUploadCompleteResponse - // DTO 模型别名 - UserUpdateFields = dto.UserUpdateFields - UserProfileUpdateFields = dto.UserProfileUpdateFields - UserStatsUpdateFields = dto.UserStatsUpdateFields - ConfigUpdateFields = dto.ConfigUpdateFields - FlatConfigUpdate = dto.FlatConfigUpdate - MCPConfigUpdate = dto.MCPConfigUpdate - BaseConfigUpdate = dto.BaseConfigUpdate - TransferConfigUpdate = dto.TransferConfigUpdate - UploadConfigUpdate = dto.UploadConfigUpdate - DownloadConfigUpdate = dto.DownloadConfigUpdate - UserConfigUpdate = dto.UserConfigUpdate - StorageConfigUpdate = dto.StorageConfigUpdate - S3ConfigUpdate = dto.S3ConfigUpdate - WebDAVConfigUpdate = dto.WebDAVConfigUpdate - OneDriveConfigUpdate = dto.OneDriveConfigUpdate - NFSConfigUpdate = dto.NFSConfigUpdate - // MCP 模型别名 SystemConfigResponse = mcp.SystemConfigResponse ) diff --git a/internal/repository/user.go b/internal/repository/user.go index 7756367..fc77a2b 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -93,16 +93,6 @@ func (dao *UserDAO) UpdateUserProfile(id uint, user *models.User) error { return dao.db.Model(&models.User{}).Where("id = ?", id).Updates(user.ToMap()).Error } -// UpdateUserStats 更新用户统计信息 -func (dao *UserDAO) UpdateUserStats(id uint, statsFields *models.UserStatsUpdateFields) error { - if statsFields == nil || !statsFields.HasUpdates() { - return errors.New("没有需要更新的统计字段") - } - - updates := statsFields.ToMap() - return dao.db.Model(&models.User{}).Where("id = ?", id).Updates(updates).Error -} - // UpdatePassword 更新用户密码 func (dao *UserDAO) UpdatePassword(id uint, passwordHash string) error { return dao.db.Model(&models.User{}).Where("id = ?", id).Update("password_hash", passwordHash).Error diff --git a/internal/services/admin/config.go b/internal/services/admin/config.go index bafb9af..0bebd75 100644 --- a/internal/services/admin/config.go +++ b/internal/services/admin/config.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/zy84338719/filecodebox/internal/config" - "github.com/zy84338719/filecodebox/internal/models" "github.com/zy84338719/filecodebox/internal/models/web" ) @@ -24,19 +23,19 @@ func (s *Service) UpdateConfig(configData map[string]interface{}) error { filteredConfigData[key] = value } - configUpdates := s.convertMapToConfigUpdate(filteredConfigData) - return s.SaveConfigUpdate(configUpdates) + // Use nested map directly (no DTO conversion) + return s.SaveConfigUpdate(filteredConfigData) } // UpdateConfigWithDTO 使用DTO更新配置 -func (s *Service) UpdateConfigWithDTO(configUpdate *models.ConfigUpdateFields) error { +func (s *Service) UpdateConfigWithDTO(configUpdate map[string]interface{}) error { return s.SaveConfigUpdate(configUpdate) } // UpdateConfigWithFlatDTO 使用平面化DTO更新配置 -func (s *Service) UpdateConfigWithFlatDTO(flatUpdate *models.FlatConfigUpdate) error { - configUpdate := s.convertFlatDTOToNested(flatUpdate) - return s.SaveConfigUpdate(configUpdate) +func (s *Service) UpdateConfigWithFlatDTO(flatUpdate map[string]interface{}) error { + // Expect caller to provide nested map; treat flat as already suitable + return s.SaveConfigUpdate(flatUpdate) } // UpdateConfigFromRequest 从结构化请求更新配置 @@ -215,205 +214,19 @@ func (s *Service) ReloadConfig() error { return s.manager.ReloadConfig() } -// convertMapToConfigUpdate 将map转换为配置更新DTO -func (s *Service) convertMapToConfigUpdate(data map[string]interface{}) *models.ConfigUpdateFields { - configUpdate := &models.ConfigUpdateFields{} - - // 用户配置字段映射 - userFields := map[string]bool{ - "allow_user_registration": true, - "require_email_verify": true, - "user_upload_size": true, - "user_storage_quota": true, - "session_expiry_hours": true, - "max_sessions_per_user": true, - "jwt_secret": true, - } - - // 传输配置字段映射 - transferUploadFields := map[string]bool{ - "open_upload": true, - "upload_size": true, - "enable_chunk": true, - "chunk_size": true, - "max_save_seconds": true, - } - - transferDownloadFields := map[string]bool{ - "enable_concurrent_download": true, - "max_concurrent_downloads": true, - "download_timeout": true, - } +// DTO conversion removed — use nested map[string]interface{} directly - // 基础配置字段映射 - baseFields := map[string]bool{ - "name": true, - "description": true, - "keywords": true, - "port": true, - "host": true, - "data_path": true, - "production": true, - } - - // MCP配置字段映射 - mcpFields := map[string]bool{ - "enable_mcp_server": true, - "mcp_port": true, - "mcp_host": true, - } - - // 分类处理字段 - var userConfig *models.UserConfigUpdate - var uploadConfig *models.UploadConfigUpdate - var downloadConfig *models.DownloadConfigUpdate - var baseConfig *models.BaseConfigUpdate - var mcpConfig *models.MCPConfigUpdate - - for key, value := range data { - if userFields[key] { - if userConfig == nil { - userConfig = &models.UserConfigUpdate{} - } - s.setUserConfigField(userConfig, key, value) - } else if transferUploadFields[key] { - if uploadConfig == nil { - uploadConfig = &models.UploadConfigUpdate{} - } - s.setUploadConfigField(uploadConfig, key, value) - } else if transferDownloadFields[key] { - if downloadConfig == nil { - downloadConfig = &models.DownloadConfigUpdate{} - } - s.setDownloadConfigField(downloadConfig, key, value) - } else if baseFields[key] { - if baseConfig == nil { - baseConfig = &models.BaseConfigUpdate{} - } - s.setBaseConfigField(baseConfig, key, value) - } else if mcpFields[key] { - if mcpConfig == nil { - mcpConfig = &models.MCPConfigUpdate{} - } - s.setMCPConfigField(mcpConfig, key, value) - } else { - // 其他字段直接设置 - s.setOtherConfigField(configUpdate, key, value) - } - } - - // 构建嵌套结构 - if userConfig != nil { - configUpdate.User = userConfig - } - - if uploadConfig != nil || downloadConfig != nil { - transferConfig := &models.TransferConfigUpdate{} - if uploadConfig != nil { - transferConfig.Upload = uploadConfig - } - if downloadConfig != nil { - transferConfig.Download = downloadConfig - } - configUpdate.Transfer = transferConfig - } - - if baseConfig != nil { - configUpdate.Base = baseConfig - } - - if mcpConfig != nil { - configUpdate.MCP = mcpConfig - } - - return configUpdate -} - -// convertFlatDTOToNested 将平面化DTO转换为嵌套DTO -func (s *Service) convertFlatDTOToNested(flatUpdate *models.FlatConfigUpdate) *models.ConfigUpdateFields { - configUpdate := &models.ConfigUpdateFields{} - - // 基础配置 - if hasBaseConfig(flatUpdate) { - configUpdate.Base = &models.BaseConfigUpdate{ - Name: flatUpdate.Name, - Description: flatUpdate.Description, - Keywords: flatUpdate.Keywords, - Port: flatUpdate.Port, - Host: flatUpdate.Host, - DataPath: flatUpdate.DataPath, - Production: flatUpdate.Production, - } - } - - // 传输配置 - if hasTransferConfig(flatUpdate) { - transferConfig := &models.TransferConfigUpdate{} - - if hasUploadConfig(flatUpdate) { - transferConfig.Upload = &models.UploadConfigUpdate{ - OpenUpload: flatUpdate.OpenUpload, - UploadSize: flatUpdate.UploadSize, - EnableChunk: flatUpdate.EnableChunk, - ChunkSize: flatUpdate.ChunkSize, - MaxSaveSeconds: flatUpdate.MaxSaveSeconds, - } - } - - if hasDownloadConfig(flatUpdate) { - transferConfig.Download = &models.DownloadConfigUpdate{ - EnableConcurrentDownload: flatUpdate.EnableConcurrentDownload, - MaxConcurrentDownloads: flatUpdate.MaxConcurrentDownloads, - DownloadTimeout: flatUpdate.DownloadTimeout, - } - } - - configUpdate.Transfer = transferConfig - } - - // 用户配置 - if hasUserConfig(flatUpdate) { - configUpdate.User = &models.UserConfigUpdate{ - AllowUserRegistration: flatUpdate.AllowUserRegistration, - RequireEmailVerify: flatUpdate.RequireEmailVerify, - UserUploadSize: flatUpdate.UserUploadSize, - UserStorageQuota: flatUpdate.UserStorageQuota, - SessionExpiryHours: flatUpdate.SessionExpiryHours, - MaxSessionsPerUser: flatUpdate.MaxSessionsPerUser, - JWTSecret: flatUpdate.JWTSecret, - } - } - - // MCP配置 - if hasMCPConfig(flatUpdate) { - configUpdate.MCP = &models.MCPConfigUpdate{ - EnableMCPServer: flatUpdate.EnableMCPServer, - MCPPort: flatUpdate.MCPPort, - MCPHost: flatUpdate.MCPHost, - } - } - - // 其他配置 - configUpdate.NotifyTitle = flatUpdate.NotifyTitle - configUpdate.NotifyContent = flatUpdate.NotifyContent - configUpdate.PageExplain = flatUpdate.PageExplain - configUpdate.Opacity = flatUpdate.Opacity - configUpdate.ThemesSelect = flatUpdate.ThemesSelect - - return configUpdate -} +// convertFlatDTOToNested removed // SaveConfigUpdate 保存配置更新 -func (s *Service) SaveConfigUpdate(configUpdate *models.ConfigUpdateFields) error { - // 转换为map格式 - configMap := configUpdate.ToMap() +func (s *Service) SaveConfigUpdate(configUpdate map[string]interface{}) error { // Apply structured updates to the ConfigManager modules - if cfgBase, ok := configMap["base"].(map[string]interface{}); ok { + if cfgBase, ok := configUpdate["base"].(map[string]interface{}); ok { if err := s.manager.Base.Update(cfgBase); err != nil { return fmt.Errorf("更新 base 配置失败: %w", err) } } - if cfgTransfer, ok := configMap["transfer"].(map[string]interface{}); ok { + if cfgTransfer, ok := configUpdate["transfer"].(map[string]interface{}); ok { if upload, ok2 := cfgTransfer["upload"].(map[string]interface{}); ok2 { if err := s.manager.Transfer.Upload.Update(upload); err != nil { return fmt.Errorf("更新 transfer.upload 配置失败: %w", err) @@ -425,31 +238,38 @@ func (s *Service) SaveConfigUpdate(configUpdate *models.ConfigUpdateFields) erro } } } - if cfgUser, ok := configMap["user"].(map[string]interface{}); ok { + if cfgUser, ok := configUpdate["user"].(map[string]interface{}); ok { if err := s.manager.User.Update(cfgUser); err != nil { return fmt.Errorf("更新 user 配置失败: %w", err) } } - if cfgMCP, ok := configMap["mcp"].(map[string]interface{}); ok { + if cfgMCP, ok := configUpdate["mcp"].(map[string]interface{}); ok { if err := s.manager.MCP.Update(cfgMCP); err != nil { return fmt.Errorf("更新 mcp 配置失败: %w", err) } } // Other top-level fields - if v, ok := configMap["notify_title"].(string); ok { + if v, ok := configUpdate["notify_title"].(string); ok { s.manager.NotifyTitle = v } - if v, ok := configMap["notify_content"].(string); ok { + if v, ok := configUpdate["notify_content"].(string); ok { s.manager.NotifyContent = v } - if v, ok := configMap["page_explain"].(string); ok { + if v, ok := configUpdate["page_explain"].(string); ok { s.manager.PageExplain = v } - if v, ok := configMap["opacity"].(int); ok { - s.manager.Opacity = float64(v) + if v, ok := configUpdate["opacity"]; ok { + switch t := v.(type) { + case int: + s.manager.Opacity = float64(t) + case int64: + s.manager.Opacity = float64(t) + case float64: + s.manager.Opacity = t + } } - if v, ok := configMap["themes_select"].(string); ok { + if v, ok := configUpdate["themes_select"].(string); ok { s.manager.ThemesSelect = v } @@ -464,194 +284,4 @@ func (s *Service) SaveConfigUpdate(configUpdate *models.ConfigUpdateFields) erro return nil } -// 辅助方法:设置用户配置字段 -func (s *Service) setUserConfigField(config *models.UserConfigUpdate, key string, value interface{}) { - switch key { - case "allow_user_registration": - if v, ok := value.(int); ok { - config.AllowUserRegistration = &v - } - case "require_email_verify": - if v, ok := value.(int); ok { - config.RequireEmailVerify = &v - } - case "user_upload_size": - if v, ok := value.(int64); ok { - config.UserUploadSize = &v - } - case "user_storage_quota": - if v, ok := value.(int64); ok { - config.UserStorageQuota = &v - } - case "session_expiry_hours": - if v, ok := value.(int); ok { - config.SessionExpiryHours = &v - } - case "max_sessions_per_user": - if v, ok := value.(int); ok { - config.MaxSessionsPerUser = &v - } - case "jwt_secret": - if v, ok := value.(string); ok { - config.JWTSecret = &v - } - } -} - -// 辅助方法:设置上传配置字段 -func (s *Service) setUploadConfigField(config *models.UploadConfigUpdate, key string, value interface{}) { - switch key { - case "open_upload": - if v, ok := value.(int); ok { - config.OpenUpload = &v - } - case "upload_size": - if v, ok := value.(int64); ok { - config.UploadSize = &v - } - case "enable_chunk": - if v, ok := value.(int); ok { - config.EnableChunk = &v - } - case "chunk_size": - if v, ok := value.(int64); ok { - config.ChunkSize = &v - } - case "max_save_seconds": - if v, ok := value.(int); ok { - config.MaxSaveSeconds = &v - } - } -} - -// 辅助方法:设置下载配置字段 -func (s *Service) setDownloadConfigField(config *models.DownloadConfigUpdate, key string, value interface{}) { - switch key { - case "enable_concurrent_download": - if v, ok := value.(int); ok { - config.EnableConcurrentDownload = &v - } - case "max_concurrent_downloads": - if v, ok := value.(int); ok { - config.MaxConcurrentDownloads = &v - } - case "download_timeout": - if v, ok := value.(int); ok { - config.DownloadTimeout = &v - } - } -} - -// 辅助方法:设置基础配置字段 -func (s *Service) setBaseConfigField(config *models.BaseConfigUpdate, key string, value interface{}) { - switch key { - case "name": - if v, ok := value.(string); ok { - config.Name = &v - } - case "description": - if v, ok := value.(string); ok { - config.Description = &v - } - case "keywords": - if v, ok := value.(string); ok { - config.Keywords = &v - } - case "port": - if v, ok := value.(int); ok { - config.Port = &v - } - case "host": - if v, ok := value.(string); ok { - config.Host = &v - } - case "data_path": - if v, ok := value.(string); ok { - config.DataPath = &v - } - case "production": - if v, ok := value.(bool); ok { - config.Production = &v - } - } -} - -// 辅助方法:设置MCP配置字段 -func (s *Service) setMCPConfigField(config *models.MCPConfigUpdate, key string, value interface{}) { - switch key { - case "enable_mcp_server": - if v, ok := value.(int); ok { - config.EnableMCPServer = &v - } - case "mcp_port": - if v, ok := value.(string); ok { - config.MCPPort = &v - } - case "mcp_host": - if v, ok := value.(string); ok { - config.MCPHost = &v - } - } -} - -// 辅助方法:设置其他配置字段 -func (s *Service) setOtherConfigField(config *models.ConfigUpdateFields, key string, value interface{}) { - switch key { - case "notify_title": - if v, ok := value.(string); ok { - config.NotifyTitle = &v - } - case "notify_content": - if v, ok := value.(string); ok { - config.NotifyContent = &v - } - case "page_explain": - if v, ok := value.(string); ok { - config.PageExplain = &v - } - case "opacity": - if v, ok := value.(int); ok { - config.Opacity = &v - } - case "themes_select": - if v, ok := value.(string); ok { - config.ThemesSelect = &v - } - } -} - -// 辅助方法:检查是否有基础配置 -func hasBaseConfig(flat *models.FlatConfigUpdate) bool { - return flat.Name != nil || flat.Description != nil || flat.Keywords != nil || - flat.Port != nil || flat.Host != nil || flat.DataPath != nil || flat.Production != nil -} - -// 辅助方法:检查是否有传输配置 -func hasTransferConfig(flat *models.FlatConfigUpdate) bool { - return hasUploadConfig(flat) || hasDownloadConfig(flat) -} - -// 辅助方法:检查是否有上传配置 -func hasUploadConfig(flat *models.FlatConfigUpdate) bool { - return flat.OpenUpload != nil || flat.UploadSize != nil || flat.EnableChunk != nil || - flat.ChunkSize != nil || flat.MaxSaveSeconds != nil -} - -// 辅助方法:检查是否有下载配置 -func hasDownloadConfig(flat *models.FlatConfigUpdate) bool { - return flat.EnableConcurrentDownload != nil || flat.MaxConcurrentDownloads != nil || - flat.DownloadTimeout != nil -} - -// 辅助方法:检查是否有用户配置 -func hasUserConfig(flat *models.FlatConfigUpdate) bool { - return flat.AllowUserRegistration != nil || flat.RequireEmailVerify != nil || - flat.UserUploadSize != nil || flat.UserStorageQuota != nil || - flat.SessionExpiryHours != nil || flat.MaxSessionsPerUser != nil || - flat.JWTSecret != nil -} - -// 辅助方法:检查是否有MCP配置 -func hasMCPConfig(flat *models.FlatConfigUpdate) bool { - return flat.EnableMCPServer != nil || flat.MCPPort != nil || flat.MCPHost != nil -} +// DTO helper functions removed — using map-based updates instead diff --git a/internal/services/user/profile.go b/internal/services/user/profile.go index bd6c98e..b90c162 100644 --- a/internal/services/user/profile.go +++ b/internal/services/user/profile.go @@ -16,18 +16,6 @@ func (s *Service) GetProfile(userID uint) (*models.User, error) { // UpdateProfile 更新用户资料 - 使用结构化更新 func (s *Service) UpdateProfile(userID uint, updates map[string]interface{}) error { - // 验证更新字段 - allowedFields := map[string]bool{ - "nickname": true, - "email": true, - "avatar": true, - } - - for key := range updates { - if !allowedFields[key] { - return errors.New("field not allowed to update: " + key) - } - } user, err := s.repositoryManager.User.GetByID(userID) if err != nil { From 154a628d0d5b95559e4a62343cc23f501c78c3af Mon Sep 17 00:00:00 2001 From: murphyyi Date: Wed, 17 Sep 2025 14:12:39 +0800 Subject: [PATCH 11/21] docs: remove DTO descriptions; refactor middleware into per-file implementations --- docs/CONFIG_DTO_REFACTOR_SUMMARY.md | 31 +-- internal/middleware/admin_auth.go | 28 +++ internal/middleware/cors.go | 16 ++ internal/middleware/middleware.go | 248 +--------------------- internal/middleware/optional_user_auth.go | 49 +++++ internal/middleware/ratelimiter.go | 85 ++++++++ internal/middleware/share_auth.go | 19 ++ internal/middleware/user_auth.go | 51 +++++ 8 files changed, 259 insertions(+), 268 deletions(-) create mode 100644 internal/middleware/admin_auth.go create mode 100644 internal/middleware/cors.go create mode 100644 internal/middleware/optional_user_auth.go create mode 100644 internal/middleware/ratelimiter.go create mode 100644 internal/middleware/share_auth.go create mode 100644 internal/middleware/user_auth.go diff --git a/docs/CONFIG_DTO_REFACTOR_SUMMARY.md b/docs/CONFIG_DTO_REFACTOR_SUMMARY.md index ce05e61..75b7a07 100644 --- a/docs/CONFIG_DTO_REFACTOR_SUMMARY.md +++ b/docs/CONFIG_DTO_REFACTOR_SUMMARY.md @@ -4,36 +4,21 @@ 根据用户要求:"请将struct 放到models里相应位置 make(map[string]interface{}) 这个替换成结构体 依次类推 该创建对应的dto就去创建 使用 可以改变 请求返回值 记得改动对应的前端代码" -本次重构将临时struct定义移至models包,并用结构化DTO替换了所有`map[string]interface{}`用法,实现了类型安全的配置管理系统。 +本次重构精简了配置更新路径,移除了原来以 DTO(数据传输对象)为中心的多层转换逻辑,采用更直接的 map 驱动更新和 YAML-first 的持久化策略: + +- 接收结构化或扁平化的更新请求(由 handlers 解析),服务层统一将其转换为嵌套的 `map[string]interface{}` 结构; +- 对应的配置模块实现 `Update(map[string]interface{})` 方法,直接在内存配置上应用变更; +- 所有配置的持久化以 `config.yaml` 为首选存储(YAML-first),在更新后通过 `PersistYAML()` 保存并通过 `ReloadConfig()` 热重载生效; +- 为了减少重复与兼容层,原来的 DTO 定义与繁重的转换逻辑已移除,只保留清晰的模块接口与文档说明。 ## 主要变更 ### 1. 模型层 (Models Layer) -#### 新增文件:`models/dto/config_updates.go` -- **ConfigUpdateFields**: 主要的配置更新DTO结构 -- **FlatConfigUpdate**: 平面化配置更新DTO(向后兼容) -- **16个专门的配置DTO**: - - `BaseConfigUpdate`: 基础配置 - - `MCPConfigUpdate`: MCP服务器配置 - - `UserConfigUpdate`: 用户系统配置 - - `TransferConfigUpdate`: 传输配置 - - `DatabaseConfigUpdate`: 数据库配置 - - `StorageConfigUpdate`: 存储配置 - - `S3ConfigUpdate`: S3存储配置 - - `OneDriveConfigUpdate`: OneDrive存储配置 - - `WebDAVConfigUpdate`: WebDAV存储配置 - - `NFSConfigUpdate`: NFS存储配置 - - `NotificationConfigUpdate`: 通知配置 - - `EmailConfigUpdate`: 邮件配置 - - `ThemeConfigUpdate`: 主题配置 - - `SecurityConfigUpdate`: 安全配置 - - `SystemConfigUpdate`: 系统配置 - - `CacheConfigUpdate`: 缓存配置 +> 注意:原先提到的 `models/dto` 文件夹与 DTO 类型定义已被清理,本项目现以模块 `Update(map[string]interface{})` 接口与配置管理器为主进行配置变更管理。 #### 更新文件:`models/models.go` -- 添加了16个DTO类型别名,提供统一的导入接口 -- 保持向后兼容性,便于其他包引用 +- 保持模型导出与类型别名(db/service/mcp)用于兼容历史代码,删除了对 `dto` 层的依赖。 ### 2. 服务层 (Service Layer) diff --git a/internal/middleware/admin_auth.go b/internal/middleware/admin_auth.go new file mode 100644 index 0000000..10bd63b --- /dev/null +++ b/internal/middleware/admin_auth.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/common" + "github.com/zy84338719/filecodebox/internal/config" +) + +// AdminAuth 管理员认证中间件(基于用户权限) +func AdminAuth(manager *config.ConfigManager) gin.HandlerFunc { + return func(c *gin.Context) { + role, exists := c.Get("role") + if !exists { + common.UnauthorizedResponse(c, "用户权限信息不存在") + c.Abort() + return + } + + roleStr, ok := role.(string) + if !ok || roleStr != "admin" { + common.ForbiddenResponse(c, "需要管理员权限") + c.Abort() + return + } + + c.Next() + } +} diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go new file mode 100644 index 0000000..6aa58c9 --- /dev/null +++ b/internal/middleware/cors.go @@ -0,0 +1,16 @@ +package middleware + +import ( + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +// CORS 中间件 +func CORS() gin.HandlerFunc { + config := cors.DefaultConfig() + config.AllowAllOrigins = true + config.AllowCredentials = true + config.AllowHeaders = []string{"*"} + config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} + return cors.New(config) +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 915ffc5..7e03ba1 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -1,247 +1,5 @@ package middleware -import ( - "strings" - "sync" - "time" - - "github.com/zy84338719/filecodebox/internal/common" - "github.com/zy84338719/filecodebox/internal/config" - "github.com/zy84338719/filecodebox/internal/services" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" - "golang.org/x/time/rate" -) - -// CORS 中间件 -func CORS() gin.HandlerFunc { - config := cors.DefaultConfig() - config.AllowAllOrigins = true - config.AllowCredentials = true - config.AllowHeaders = []string{"*"} - config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} - return cors.New(config) -} - -// RateLimiter 限流器 -type RateLimiter struct { - limiters map[string]*rate.Limiter - mu sync.RWMutex -} - -func NewRateLimiter() *RateLimiter { - return &RateLimiter{ - limiters: make(map[string]*rate.Limiter), - } -} - -func (rl *RateLimiter) GetLimiter(key string, r rate.Limit, b int) *rate.Limiter { - rl.mu.Lock() - defer rl.mu.Unlock() - - limiter, exists := rl.limiters[key] - if !exists { - limiter = rate.NewLimiter(r, b) - rl.limiters[key] = limiter - } - - return limiter -} - -// Cleanup 清理过期的限流器 -func (rl *RateLimiter) Cleanup() { - ticker := time.NewTicker(time.Hour) - go func() { - for range ticker.C { - rl.mu.Lock() - for key, limiter := range rl.limiters { - if limiter.Allow() { - delete(rl.limiters, key) - } - } - rl.mu.Unlock() - } - }() -} - -var ( - uploadLimiter = NewRateLimiter() - errorLimiter = NewRateLimiter() -) - -func init() { - uploadLimiter.Cleanup() - errorLimiter.Cleanup() -} - -// RateLimit 限流中间件 -func RateLimit(manager *config.ConfigManager) gin.HandlerFunc { - return func(c *gin.Context) { - ip := c.ClientIP() - - // 根据路径选择不同的限流策略 - if c.Request.URL.Path == "/share/file/" || c.Request.URL.Path == "/share/text/" { - // 上传限流 - limiter := uploadLimiter.GetLimiter( - ip, - rate.Every(time.Duration(manager.UploadMinute)*time.Minute/time.Duration(manager.UploadCount)), - manager.UploadCount, - ) - if !limiter.Allow() { - common.TooManyRequestsResponse(c, "上传频率过快,请稍后再试") - c.Abort() - return - } - } else if c.Request.URL.Path == "/share/select/" && c.Request.Method == "GET" { - // 只对GET请求的select进行错误限流,POST请求更宽松 - limiter := errorLimiter.GetLimiter( - ip, - rate.Every(time.Duration(manager.ErrorMinute)*time.Minute/time.Duration(manager.ErrorCount)), - manager.ErrorCount, - ) - if !limiter.Allow() { - common.TooManyRequestsResponse(c, "请求频率过快,请稍后再试") - c.Abort() - return - } - } - - c.Next() - } -} - -// AdminAuth 管理员认证中间件(基于用户权限) -func AdminAuth(manager *config.ConfigManager) gin.HandlerFunc { - return func(c *gin.Context) { - // 从上下文中获取用户角色(由UserAuth中间件设置) - role, exists := c.Get("role") - if !exists { - common.UnauthorizedResponse(c, "用户权限信息不存在") - c.Abort() - return - } - - // 检查用户角色是否为管理员 - roleStr, ok := role.(string) - if !ok || roleStr != "admin" { - common.ForbiddenResponse(c, "需要管理员权限") - c.Abort() - return - } - - c.Next() - } -} - -// ShareAuth 分享认证中间件 -func ShareAuth(manager *config.ConfigManager) gin.HandlerFunc { - return func(c *gin.Context) { - if manager.Transfer.Upload.OpenUpload == 0 { - common.ForbiddenResponse(c, "上传功能已关闭") - c.Abort() - return - } - c.Next() - } -} - -// UserAuth 用户认证中间件 -func UserAuth(manager *config.ConfigManager, userService interface { - ValidateToken(string) (interface{}, error) -}) gin.HandlerFunc { - return func(c *gin.Context) { - // 用户系统始终启用,直接进行认证验证 - - // 获取Authorization头 - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - common.UnauthorizedResponse(c, "缺少认证信息") - c.Abort() - return - } - - // 检查Bearer前缀 - tokenParts := strings.SplitN(authHeader, " ", 2) - if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { - common.UnauthorizedResponse(c, "认证格式错误") - c.Abort() - return - } - - // 验证token - claimsInterface, err := userService.ValidateToken(tokenParts[1]) - if err != nil { - common.UnauthorizedResponse(c, "认证失败: "+err.Error()) - c.Abort() - return - } - - // 类型断言获取claims - if claims, ok := claimsInterface.(*services.AuthClaims); ok { - // 将用户信息设置到上下文 - c.Set("user_id", claims.UserID) - c.Set("username", claims.Username) - c.Set("role", claims.Role) - c.Set("session_id", claims.SessionID) - } else { - common.UnauthorizedResponse(c, "token格式错误") - c.Abort() - return - } - - c.Next() - } -} - -// UserClaims JWT claims 结构体定义 -// OptionalUserAuth 可选用户认证中间件(支持匿名和登录用户) -func OptionalUserAuth(manager *config.ConfigManager, userService interface { - ValidateToken(string) (interface{}, error) -}) gin.HandlerFunc { - return func(c *gin.Context) { - // 用户系统始终启用,直接进行认证验证 - - // 获取Authorization头 - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - // 没有认证信息,允许匿名访问 - c.Set("is_anonymous", true) - c.Next() - return - } - - // 检查Bearer前缀 - tokenParts := strings.SplitN(authHeader, " ", 2) - if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { - // 认证格式错误,但仍允许匿名访问 - c.Set("is_anonymous", true) - c.Next() - return - } - - // 尝试验证token - claimsInterface, err := userService.ValidateToken(tokenParts[1]) - if err != nil { - // token验证失败,但仍允许匿名访问 - c.Set("is_anonymous", true) - c.Next() - return - } - - // 类型断言获取claims - if claims, ok := claimsInterface.(*services.AuthClaims); ok { - // 将用户信息设置到上下文 - c.Set("user_id", claims.UserID) - c.Set("username", claims.Username) - c.Set("role", claims.Role) - c.Set("session_id", claims.SessionID) - c.Set("is_anonymous", false) - } else { - // claims格式错误,但仍允许匿名访问 - c.Set("is_anonymous", true) - } - - c.Next() - } -} +// This file intentionally left minimal. Individual middleware implementations +// live in separate files under this package (cors.go, ratelimiter.go, admin_auth.go, share_auth.go, +// user_auth.go, optional_user_auth.go etc.). diff --git a/internal/middleware/optional_user_auth.go b/internal/middleware/optional_user_auth.go new file mode 100644 index 0000000..bab2198 --- /dev/null +++ b/internal/middleware/optional_user_auth.go @@ -0,0 +1,49 @@ +package middleware + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/config" + "github.com/zy84338719/filecodebox/internal/services" +) + +// OptionalUserAuth 可选用户认证中间件(支持匿名和登录用户) +func OptionalUserAuth(manager *config.ConfigManager, userService interface { + ValidateToken(string) (interface{}, error) +}) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.Set("is_anonymous", true) + c.Next() + return + } + + tokenParts := strings.SplitN(authHeader, " ", 2) + if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { + c.Set("is_anonymous", true) + c.Next() + return + } + + claimsInterface, err := userService.ValidateToken(tokenParts[1]) + if err != nil { + c.Set("is_anonymous", true) + c.Next() + return + } + + if claims, ok := claimsInterface.(*services.AuthClaims); ok { + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + c.Set("session_id", claims.SessionID) + c.Set("is_anonymous", false) + } else { + c.Set("is_anonymous", true) + } + + c.Next() + } +} diff --git a/internal/middleware/ratelimiter.go b/internal/middleware/ratelimiter.go new file mode 100644 index 0000000..e6c16c7 --- /dev/null +++ b/internal/middleware/ratelimiter.go @@ -0,0 +1,85 @@ +package middleware + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/common" + "github.com/zy84338719/filecodebox/internal/config" + "golang.org/x/time/rate" +) + +// RateLimiter 限流器 +type RateLimiter struct { + limiters map[string]*rate.Limiter +} + +func NewRateLimiter() *RateLimiter { + return &RateLimiter{limiters: make(map[string]*rate.Limiter)} +} + +func (rl *RateLimiter) GetLimiter(key string, r rate.Limit, b int) *rate.Limiter { + // Note: for simplicity we avoid mutex here; callers should ensure safe usage + limiter, exists := rl.limiters[key] + if !exists { + limiter = rate.NewLimiter(r, b) + rl.limiters[key] = limiter + } + return limiter +} + +func (rl *RateLimiter) Cleanup() { + ticker := time.NewTicker(time.Hour) + go func() { + for range ticker.C { + for key, limiter := range rl.limiters { + if limiter.Allow() { + delete(rl.limiters, key) + } + } + } + }() +} + +var ( + uploadLimiter = NewRateLimiter() + errorLimiter = NewRateLimiter() +) + +func init() { + uploadLimiter.Cleanup() + errorLimiter.Cleanup() +} + +// RateLimit 限流中间件 +func RateLimit(manager *config.ConfigManager) gin.HandlerFunc { + return func(c *gin.Context) { + ip := c.ClientIP() + + if c.Request.URL.Path == "/share/file/" || c.Request.URL.Path == "/share/text/" { + limiter := uploadLimiter.GetLimiter( + ip, + rate.Every(time.Duration(manager.UploadMinute)*time.Minute/time.Duration(manager.UploadCount)), + manager.UploadCount, + ) + if !limiter.Allow() { + common.TooManyRequestsResponse(c, "上传频率过快,请稍后再试") + c.Abort() + return + } + } else if c.Request.URL.Path == "/share/select/" && c.Request.Method == "GET" { + limiter := errorLimiter.GetLimiter( + ip, + rate.Every(time.Duration(manager.ErrorMinute)*time.Minute/time.Duration(manager.ErrorCount)), + manager.ErrorCount, + ) + if !limiter.Allow() { + common.TooManyRequestsResponse(c, "请求频率过快,请稍后再试") + c.Abort() + return + } + } + + c.Next() + } +} diff --git a/internal/middleware/share_auth.go b/internal/middleware/share_auth.go new file mode 100644 index 0000000..e894029 --- /dev/null +++ b/internal/middleware/share_auth.go @@ -0,0 +1,19 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/common" + "github.com/zy84338719/filecodebox/internal/config" +) + +// ShareAuth 分享认证中间件 +func ShareAuth(manager *config.ConfigManager) gin.HandlerFunc { + return func(c *gin.Context) { + if manager.Transfer.Upload.OpenUpload == 0 { + common.ForbiddenResponse(c, "上传功能已关闭") + c.Abort() + return + } + c.Next() + } +} diff --git a/internal/middleware/user_auth.go b/internal/middleware/user_auth.go new file mode 100644 index 0000000..f328906 --- /dev/null +++ b/internal/middleware/user_auth.go @@ -0,0 +1,51 @@ +package middleware + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/common" + "github.com/zy84338719/filecodebox/internal/config" + "github.com/zy84338719/filecodebox/internal/services" +) + +// UserAuth 用户认证中间件 +func UserAuth(manager *config.ConfigManager, userService interface { + ValidateToken(string) (interface{}, error) +}) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + common.UnauthorizedResponse(c, "缺少认证信息") + c.Abort() + return + } + + tokenParts := strings.SplitN(authHeader, " ", 2) + if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { + common.UnauthorizedResponse(c, "认证格式错误") + c.Abort() + return + } + + claimsInterface, err := userService.ValidateToken(tokenParts[1]) + if err != nil { + common.UnauthorizedResponse(c, "认证失败: "+err.Error()) + c.Abort() + return + } + + if claims, ok := claimsInterface.(*services.AuthClaims); ok { + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + c.Set("session_id", claims.SessionID) + } else { + common.UnauthorizedResponse(c, "token格式错误") + c.Abort() + return + } + + c.Next() + } +} From 998ae1872aabac013ec08bcd2e02c8882c93544e Mon Sep 17 00:00:00 2001 From: murphyyi Date: Wed, 17 Sep 2025 14:15:12 +0800 Subject: [PATCH 12/21] docs: clean up DTO references in changelogs and config summary --- docs/changelogs/REFACTOR_SUMMARY.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/changelogs/REFACTOR_SUMMARY.md b/docs/changelogs/REFACTOR_SUMMARY.md index d81b400..c207851 100644 --- a/docs/changelogs/REFACTOR_SUMMARY.md +++ b/docs/changelogs/REFACTOR_SUMMARY.md @@ -39,8 +39,8 @@ - **生命周期管理**:通过 MCPManager 提供完整的 MCP 服务器启动/停止/重启功能 #### 7. 模型架构分层完善 -- **四层模型架构**:`db/`, `service/`, `dto/`, `mcp/` 按用途分层组织 -- **类型别名兼容层**:`internal/models/models.go` 通过类型别名提供向后兼容性 +- **模型层优化**:模型按用途分层(`db/`, `service/`, `mcp/`),移除了独立的 `dto/` 层,简化数据流和类型转换; +- **类型别名兼容层**:`internal/models/models.go` 仍通过类型别名提供向后兼容性,便于逐步迁移历史调用点; - **统一导出**:集中管理所有模型的导出,简化导入复杂度 ## 📁 新的项目结构 @@ -72,7 +72,6 @@ internal/ │ ├── models.go # 统一导出层,类型别名提供向后兼容性 │ ├── db/ # 数据库实体模型 │ ├── service/ # 服务层传输对象 -│ ├── dto/ # 数据传输对象 │ └── mcp/ # MCP 协议相关模型 ├── database/ # 🔄 数据库模块(多类型支持) │ └── database.go # 支持 SQLite、MySQL、PostgreSQL From babb011e4c1a36d83217e34f877b36dab1f4b602 Mon Sep 17 00:00:00 2001 From: murphyyi Date: Wed, 17 Sep 2025 23:14:36 +0800 Subject: [PATCH 13/21] refactor(static): centralize static asset registration and serve functions into internal/static --- internal/routes/admin.go | 75 +++++------------------------ internal/routes/base.go | 80 +++---------------------------- internal/static/README.md | 42 +++++++++++++++++ internal/static/assets.go | 99 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 158 insertions(+), 138 deletions(-) create mode 100644 internal/static/README.md create mode 100644 internal/static/assets.go diff --git a/internal/routes/admin.go b/internal/routes/admin.go index 8aaf1d7..b8d3233 100644 --- a/internal/routes/admin.go +++ b/internal/routes/admin.go @@ -1,15 +1,10 @@ package routes import ( - "fmt" - "net/http" - "os" - "path/filepath" - "strings" - "github.com/zy84338719/filecodebox/internal/config" "github.com/zy84338719/filecodebox/internal/handlers" - "github.com/zy84338719/filecodebox/internal/services" + "github.com/zy84338719/filecodebox/internal/middleware" + "github.com/zy84338719/filecodebox/internal/static" "github.com/gin-gonic/gin" ) @@ -31,7 +26,7 @@ func SetupAdminRoutes( { // 管理页面 adminGroup.GET("/", func(c *gin.Context) { - ServeAdminPage(c, cfg) + static.ServeAdminPage(c, cfg) }) // 管理员登录(通过用户名/密码获取 JWT) @@ -57,45 +52,11 @@ func SetupAdminRoutes( } // 模块化管理后台静态文件 - themeDir := fmt.Sprintf("./%s", cfg.ThemesSelect) - adminGroup.Static("/css", fmt.Sprintf("%s/admin/css", themeDir)) - adminGroup.Static("/js", fmt.Sprintf("%s/admin/js", themeDir)) - adminGroup.Static("/templates", fmt.Sprintf("%s/admin/templates", themeDir)) - adminGroup.Static("/assets", fmt.Sprintf("%s/assets", themeDir)) - adminGroup.Static("/components", fmt.Sprintf("%s/components", themeDir)) + static.RegisterAdminStaticRoutes(adminGroup, cfg) } - // 创建一个支持两种认证方式的中间件 - combinedAuthMiddleware := func(c *gin.Context) { - // 先尝试JWT用户认证 - authHeader := c.GetHeader("Authorization") - if authHeader != "" { - tokenParts := strings.SplitN(authHeader, " ", 2) - if len(tokenParts) == 2 && tokenParts[0] == "Bearer" { - // 尝试验证JWT token - claimsInterface, err := userService.ValidateToken(tokenParts[1]) - if err == nil { - // JWT验证成功,检查是否为管理员角色 - if claims, ok := claimsInterface.(*services.AuthClaims); ok && claims.Role == "admin" { - // 设置用户信息到上下文 - c.Set("user_id", claims.UserID) - c.Set("username", claims.Username) - c.Set("role", claims.Role) - c.Set("session_id", claims.SessionID) - c.Set("auth_type", "jwt") - c.Next() - return - } - } - - // JWT验证失败,不再支持静态管理员令牌回退 - } - } - - // 两种认证都失败 - c.JSON(401, gin.H{"code": 401, "message": "认证失败"}) - c.Abort() - } + // 使用复用的中间件实现(JWT 用户认证并要求 admin 角色) + combinedAuthMiddleware := middleware.CombinedAdminAuth(cfg, userService) // 需要管理员认证的API路由组 authGroup := adminGroup.Group("") @@ -122,29 +83,15 @@ func SetupAdminRoutes( // 用户管理 setupUserRoutes(authGroup, adminHandler) - // 存储管理 - setupStorageRoutes(adminGroup, storageHandler) + // 存储管理 (需要管理员认证) + setupStorageRoutes(authGroup, storageHandler) - // MCP 服务器管理 - setupMCPRoutes(adminGroup, adminHandler) + // MCP 服务器管理 (需要管理员认证) + setupMCPRoutes(authGroup, adminHandler) } } -// ServeAdminPage 服务管理页面 -func ServeAdminPage(c *gin.Context, cfg *config.ConfigManager) { - // 使用新的模块化管理页面 - adminPath := filepath.Join(".", cfg.ThemesSelect, "admin", "index.html") - - content, err := os.ReadFile(adminPath) - if err != nil { - c.String(http.StatusNotFound, "Admin page not found") - return - } - - c.Header("Cache-Control", "no-cache") - c.Header("Content-Type", "text/html; charset=utf-8") - c.String(http.StatusOK, string(content)) -} +// ServeAdminPage moved to internal/static // setupMaintenanceRoutes 设置系统维护路由 func setupMaintenanceRoutes(authGroup *gin.RouterGroup, adminHandler *handlers.AdminHandler) { diff --git a/internal/routes/base.go b/internal/routes/base.go index 091d92f..ddb300d 100644 --- a/internal/routes/base.go +++ b/internal/routes/base.go @@ -1,15 +1,12 @@ package routes import ( - "fmt" "net/http" - "os" - "path/filepath" - "strings" "github.com/zy84338719/filecodebox/internal/common" "github.com/zy84338719/filecodebox/internal/config" "github.com/zy84338719/filecodebox/internal/handlers" + "github.com/zy84338719/filecodebox/internal/static" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" @@ -19,19 +16,7 @@ import ( // SetupBaseRoutes 设置基础路由(首页、健康检查、静态文件等) func SetupBaseRoutes(router *gin.Engine, userHandler *handlers.UserHandler, cfg *config.ConfigManager) { // 静态文件服务 - 统一挂载所有前端资源 - themeDir := fmt.Sprintf("./%s", cfg.ThemesSelect) - - // 挂载主要静态资源 - router.Static("/assets", fmt.Sprintf("%s/assets", themeDir)) - - // 挂载组件化CSS文件 - router.Static("/css", fmt.Sprintf("%s/css", themeDir)) - - // 挂载组件化JS文件 - router.Static("/js", fmt.Sprintf("%s/js", themeDir)) - - // 挂载组件目录(如果存在) - router.Static("/components", fmt.Sprintf("%s/components", themeDir)) + static.RegisterStaticRoutes(router, cfg) // Swagger 文档路由 router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) @@ -57,12 +42,12 @@ func SetupBaseRoutes(router *gin.Engine, userHandler *handlers.UserHandler, cfg return } } - ServeIndex(c, cfg) + static.ServeIndex(c, cfg) }) // 系统初始化页面 router.GET("/setup", func(c *gin.Context) { - ServeSetup(c, cfg) + static.ServeSetup(c, cfg) }) // 永远注册 /user/system-info 接口: @@ -104,7 +89,7 @@ func SetupBaseRoutes(router *gin.Engine, userHandler *handlers.UserHandler, cfg } router.NoRoute(func(c *gin.Context) { - ServeIndex(c, cfg) + static.ServeIndex(c, cfg) }) // robots.txt @@ -131,60 +116,7 @@ func SetupBaseRoutes(router *gin.Engine, userHandler *handlers.UserHandler, cfg }) } -// ServeIndex 服务首页 -func ServeIndex(c *gin.Context, cfg *config.ConfigManager) { - indexPath := filepath.Join(".", cfg.ThemesSelect, "index.html") - - content, err := os.ReadFile(indexPath) - if err != nil { - c.String(http.StatusNotFound, "Index file not found") - return - } - - html := string(content) - // 替换模板变量 - html = strings.ReplaceAll(html, "{{title}}", cfg.Base.Name) - html = strings.ReplaceAll(html, "{{description}}", cfg.Base.Description) - html = strings.ReplaceAll(html, "{{keywords}}", cfg.Base.Keywords) - html = strings.ReplaceAll(html, "{{page_explain}}", cfg.PageExplain) - html = strings.ReplaceAll(html, "{{opacity}}", fmt.Sprintf("%.1f", cfg.Opacity)) - // 将相对路径转换为绝对路径,避免在子路径下请求相对路径(例如 /user/login -> /user/js/...) - html = strings.ReplaceAll(html, "src=\"js/", "src=\"/js/") - html = strings.ReplaceAll(html, "href=\"css/", "href=\"/css/") - html = strings.ReplaceAll(html, "src=\"assets/", "src=\"/assets/") - html = strings.ReplaceAll(html, "href=\"assets/", "href=\"/assets/") - html = strings.ReplaceAll(html, "src=\"components/", "src=\"/components/") - html = strings.ReplaceAll(html, "{{background}}", cfg.Background) - - c.Header("Cache-Control", "no-cache") - c.Header("Content-Type", "text/html; charset=utf-8") - c.String(http.StatusOK, html) -} - -// ServeSetup 服务系统初始化页面 -func ServeSetup(c *gin.Context, cfg *config.ConfigManager) { - setupPath := filepath.Join(".", cfg.ThemesSelect, "setup.html") - - content, err := os.ReadFile(setupPath) - if err != nil { - c.String(http.StatusNotFound, "Setup page not found") - return - } - - html := string(content) - // 替换模板变量 - html = strings.ReplaceAll(html, "{{title}}", cfg.Base.Name+" - 系统初始化") - html = strings.ReplaceAll(html, "{{description}}", cfg.Base.Description) - html = strings.ReplaceAll(html, "{{keywords}}", cfg.Base.Keywords) - // 将相对资源路径转换为绝对路径 - html = strings.ReplaceAll(html, "src=\"js/", "src=\"/js/") - html = strings.ReplaceAll(html, "href=\"css/", "href=\"/css/") - html = strings.ReplaceAll(html, "src=\"assets/", "src=\"/assets/") - - c.Header("Cache-Control", "no-cache") - c.Header("Content-Type", "text/html; charset=utf-8") - c.String(http.StatusOK, html) -} +// ServeIndex/ServeSetup moved to internal/static for central management // GetEnableChunk 获取分片上传配置 func GetEnableChunk(cfg *config.ConfigManager) int { diff --git a/internal/static/README.md b/internal/static/README.md new file mode 100644 index 0000000..fb3e444 --- /dev/null +++ b/internal/static/README.md @@ -0,0 +1,42 @@ +集中管理静态资源(internal/static) +================================= + +概述 +---- +`internal/static` 提供集中注册静态资源的帮助函数,避免在各个路由文件中重复调用 `router.Static(...)` 或 `group.Static(...)`。 + +提供的 API +----------- +- `RegisterStaticRoutes(router *gin.Engine, cfg *config.ConfigManager)` + - 在应用根上注册公共静态资源(`/assets`, `/css`, `/js`, `/components`)。 + +- `RegisterAdminStaticRoutes(adminGroup *gin.RouterGroup, cfg *config.ConfigManager)` + - 在管理路由组上注册管理后台需要的静态资源(例如 `/admin/css`, `/admin/js`, `/admin/templates` 等)。 + +使用示例 +-------- +在 `internal/routes/base.go` 中: + +```go +import "github.com/zy84338719/filecodebox/internal/static" + +// ... +static.RegisterStaticRoutes(router, cfg) +``` + +在 `internal/routes/admin.go` 中: + +```go +import "github.com/zy84338719/filecodebox/internal/static" + +// ... +static.RegisterAdminStaticRoutes(adminGroup, cfg) +``` + +扩展性 +---- +- 如果需要添加缓存 header、CDN 前缀或将资源改为嵌入(`embed`),可以在此包中统一实现并将选项通过参数传入 `Register*` 函数。 + +注意 +--- +- 本模块使用 `cfg.ThemesSelect` 与相对路径 `./` 来定位资源,行为与历史实现一致。 diff --git a/internal/static/assets.go b/internal/static/assets.go new file mode 100644 index 0000000..367eff1 --- /dev/null +++ b/internal/static/assets.go @@ -0,0 +1,99 @@ +package static + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/config" +) + +// RegisterStaticRoutes registers public-facing static routes (assets, css, js, components) +func RegisterStaticRoutes(router *gin.Engine, cfg *config.ConfigManager) { + themeDir := fmt.Sprintf("./%s", cfg.ThemesSelect) + + router.Static("/assets", fmt.Sprintf("%s/assets", themeDir)) + router.Static("/css", fmt.Sprintf("%s/css", themeDir)) + router.Static("/js", fmt.Sprintf("%s/js", themeDir)) + router.Static("/components", fmt.Sprintf("%s/components", themeDir)) +} + +// RegisterAdminStaticRoutes registers admin panel static routes (admin css/js/templates) +func RegisterAdminStaticRoutes(adminGroup *gin.RouterGroup, cfg *config.ConfigManager) { + themeDir := fmt.Sprintf("./%s", cfg.ThemesSelect) + adminGroup.Static("/css", fmt.Sprintf("%s/admin/css", themeDir)) + adminGroup.Static("/js", fmt.Sprintf("%s/admin/js", themeDir)) + adminGroup.Static("/templates", fmt.Sprintf("%s/admin/templates", themeDir)) + adminGroup.Static("/assets", fmt.Sprintf("%s/assets", themeDir)) + adminGroup.Static("/components", fmt.Sprintf("%s/components", themeDir)) +} + +// ServeIndex serves the main index page with basic template replacements. +func ServeIndex(c *gin.Context, cfg *config.ConfigManager) { + indexPath := filepath.Join(".", cfg.ThemesSelect, "index.html") + + content, err := os.ReadFile(indexPath) + if err != nil { + c.String(http.StatusNotFound, "Index file not found") + return + } + + html := string(content) + // template replacements + html = strings.ReplaceAll(html, "{{title}}", cfg.Base.Name) + html = strings.ReplaceAll(html, "{{description}}", cfg.Base.Description) + html = strings.ReplaceAll(html, "{{keywords}}", cfg.Base.Keywords) + html = strings.ReplaceAll(html, "{{page_explain}}", cfg.PageExplain) + html = strings.ReplaceAll(html, "{{opacity}}", fmt.Sprintf("%.1f", cfg.Opacity)) + html = strings.ReplaceAll(html, "src=\"js/", "src=\"/js/") + html = strings.ReplaceAll(html, "href=\"css/", "href=\"/css/") + html = strings.ReplaceAll(html, "src=\"assets/", "src=\"/assets/") + html = strings.ReplaceAll(html, "href=\"assets/", "href=\"/assets/") + html = strings.ReplaceAll(html, "src=\"components/", "src=\"/components/") + html = strings.ReplaceAll(html, "{{background}}", cfg.Background) + + c.Header("Cache-Control", "no-cache") + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, html) +} + +// ServeSetup serves the setup page with template replacements. +func ServeSetup(c *gin.Context, cfg *config.ConfigManager) { + setupPath := filepath.Join(".", cfg.ThemesSelect, "setup.html") + + content, err := os.ReadFile(setupPath) + if err != nil { + c.String(http.StatusNotFound, "Setup page not found") + return + } + + html := string(content) + html = strings.ReplaceAll(html, "{{title}}", cfg.Base.Name+" - 系统初始化") + html = strings.ReplaceAll(html, "{{description}}", cfg.Base.Description) + html = strings.ReplaceAll(html, "{{keywords}}", cfg.Base.Keywords) + html = strings.ReplaceAll(html, "src=\"js/", "src=\"/js/") + html = strings.ReplaceAll(html, "href=\"css/", "href=\"/css/") + html = strings.ReplaceAll(html, "src=\"assets/", "src=\"/assets/") + + c.Header("Cache-Control", "no-cache") + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, html) +} + +// ServeAdminPage serves the admin index page +func ServeAdminPage(c *gin.Context, cfg *config.ConfigManager) { + adminPath := filepath.Join(".", cfg.ThemesSelect, "admin", "index.html") + + content, err := os.ReadFile(adminPath) + if err != nil { + c.String(http.StatusNotFound, "Admin page not found") + return + } + + c.Header("Cache-Control", "no-cache") + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, string(content)) +} From e6fed8b96b7d656543a64bbae7905734cfedba9c Mon Sep 17 00:00:00 2001 From: murphyyi Date: Wed, 17 Sep 2025 23:14:50 +0800 Subject: [PATCH 14/21] feat(middleware): add CombinedAdminAuth middleware and tests --- internal/middleware/combined_auth.go | 52 +++++++++++++++++++++ internal/middleware/combined_auth_test.go | 55 +++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 internal/middleware/combined_auth.go create mode 100644 internal/middleware/combined_auth_test.go diff --git a/internal/middleware/combined_auth.go b/internal/middleware/combined_auth.go new file mode 100644 index 0000000..4c18350 --- /dev/null +++ b/internal/middleware/combined_auth.go @@ -0,0 +1,52 @@ +package middleware + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "github.com/zy84338719/filecodebox/internal/common" + "github.com/zy84338719/filecodebox/internal/config" + "github.com/zy84338719/filecodebox/internal/services" +) + +// CombinedAdminAuth 尝试基于 JWT 的用户认证并确保角色为 admin +func CombinedAdminAuth(manager *config.ConfigManager, userService interface { + ValidateToken(string) (interface{}, error) +}) gin.HandlerFunc { + return func(c *gin.Context) { + // 先尝试JWT用户认证 + authHeader := c.GetHeader("Authorization") + if authHeader != "" { + tokenParts := strings.SplitN(authHeader, " ", 2) + if len(tokenParts) == 2 && tokenParts[0] == "Bearer" { + claimsInterface, err := userService.ValidateToken(tokenParts[1]) + if err == nil { + if claims, ok := claimsInterface.(*services.AuthClaims); ok { + if claims.Role == "admin" { + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + c.Set("session_id", claims.SessionID) + c.Set("auth_type", "jwt") + c.Next() + return + } + // role mismatch + if manager == nil || (manager.Base != nil && !manager.Base.Production) { + logrus.WithFields(logrus.Fields{"role": claims.Role}).Debug("combined auth: token role is not admin") + } + } + } else { + if manager == nil || (manager.Base != nil && !manager.Base.Production) { + logrus.WithError(err).Debug("combined auth: ValidateToken returned error") + } + } + // JWT 验证失败或非管理员角色,继续到失败处理(不回退到旧的静态令牌) + } + } + + common.UnauthorizedResponse(c, "需要管理员权限") + c.Abort() + } +} diff --git a/internal/middleware/combined_auth_test.go b/internal/middleware/combined_auth_test.go new file mode 100644 index 0000000..bf4e766 --- /dev/null +++ b/internal/middleware/combined_auth_test.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/config" + "github.com/zy84338719/filecodebox/internal/services" +) + +// fakeUserService implements minimal ValidateToken for tests +type fakeUserService struct { + claims *services.AuthClaims + err error +} + +func (f *fakeUserService) ValidateToken(token string) (interface{}, error) { + return f.claims, f.err +} + +func TestCombinedAdminAuth_Success(t *testing.T) { + g := gin.New() + cfg := config.NewConfigManager() + fsvc := &fakeUserService{claims: &services.AuthClaims{UserID: 1, Username: "admin", Role: "admin", SessionID: "s1"}, err: nil} + g.Use(CombinedAdminAuth(cfg, fsvc)) + g.GET("/ok", func(c *gin.Context) { c.String(200, "ok") }) + + req := httptest.NewRequest("GET", "/ok", nil) + req.Header.Set("Authorization", "Bearer token") + w := httptest.NewRecorder() + g.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } +} + +func TestCombinedAdminAuth_Fail(t *testing.T) { + g := gin.New() + cfg := config.NewConfigManager() + fsvc := &fakeUserService{claims: nil, err: http.ErrNoCookie} + g.Use(CombinedAdminAuth(cfg, fsvc)) + g.GET("/ok", func(c *gin.Context) { c.String(200, "ok") }) + + req := httptest.NewRequest("GET", "/ok", nil) + req.Header.Set("Authorization", "Bearer badtoken") + w := httptest.NewRecorder() + g.ServeHTTP(w, req) + + if w.Code != http.StatusUnauthorized { + t.Fatalf("expected 401, got %d", w.Code) + } +} From f2009198050ce37114ea9576502fd79882c20560 Mon Sep 17 00:00:00 2001 From: murphyyi Date: Wed, 17 Sep 2025 23:15:06 +0800 Subject: [PATCH 15/21] chore: misc fixes from refactor (tests, handlers, scripts) --- internal/config/manager_test.go | 10 +++++--- internal/handlers/setup.go | 30 +++------------------- internal/handlers/storage.go | 9 ++++--- internal/services/admin/config.go | 33 +------------------------ internal/services/admin/maintenance.go | 11 ++++++--- internal/utils/disk_test.go | 2 +- scripts/export_config_from_db.go | 8 +++--- scripts/legacy/export_config_from_db.go | 8 +++--- 8 files changed, 33 insertions(+), 78 deletions(-) diff --git a/internal/config/manager_test.go b/internal/config/manager_test.go index 7df48b6..0b4a379 100644 --- a/internal/config/manager_test.go +++ b/internal/config/manager_test.go @@ -23,7 +23,7 @@ func TestLoadFromYAML(t *testing.T) { if err := os.WriteFile(f, b, 0644); err != nil { t.Fatalf("write tmp yaml: %v", err) } - defer os.Remove(f) + defer func() { _ = os.Remove(f) }() cm := NewConfigManager() if err := cm.LoadFromYAML(f); err != nil { @@ -51,10 +51,12 @@ func TestEnvOverride(t *testing.T) { if err := os.WriteFile(f, b2, 0644); err != nil { t.Fatalf("write tmp yaml: %v", err) } - defer os.Remove(f) + defer func() { _ = os.Remove(f) }() - os.Setenv("PORT", "9090") - defer os.Unsetenv("PORT") + if err := os.Setenv("PORT", "9090"); err != nil { + t.Fatalf("setenv failed: %v", err) + } + defer func() { _ = os.Unsetenv("PORT") }() cm := NewConfigManager() if err := cm.LoadFromYAML(f); err != nil { diff --git a/internal/handlers/setup.go b/internal/handlers/setup.go index 761e8a5..ed82eb6 100644 --- a/internal/handlers/setup.go +++ b/internal/handlers/setup.go @@ -59,32 +59,6 @@ type AdminConfig struct { AllowUserRegistration bool `json:"allowUserRegistration"` } -// validateDatabaseConfig 验证数据库配置 -func (h *SetupHandler) validateDatabaseConfig(db DatabaseConfig) error { - switch db.Type { - case "sqlite": - if db.File == "" { - return fmt.Errorf("SQLite 数据库文件路径不能为空") - } - case "mysql", "postgres": - if db.Host == "" { - return fmt.Errorf("数据库主机地址不能为空") - } - if db.Port <= 0 || db.Port > 65535 { - return fmt.Errorf("数据库端口无效") - } - if db.User == "" { - return fmt.Errorf("数据库用户名不能为空") - } - if db.Database == "" { - return fmt.Errorf("数据库名不能为空") - } - default: - return fmt.Errorf("不支持的数据库类型: %s", db.Type) - } - return nil -} - // updateDatabaseConfig 更新数据库配置 func (h *SetupHandler) updateDatabaseConfig(db DatabaseConfig) error { // 更新配置管理器中的数据库配置 @@ -250,7 +224,9 @@ func InitializeNoDB(manager *config.ConfigManager) gin.HandlerFunc { common.InternalServerErrorResponse(c, "本地存储路径不可写: "+err.Error()) return } else { - f.Close() + if err := f.Close(); err != nil { + log.Printf("warning: failed to close perm check file: %v", err) + } _ = os.Remove(testFile) } diff --git a/internal/handlers/storage.go b/internal/handlers/storage.go index 3a82ccb..277fcff 100644 --- a/internal/handlers/storage.go +++ b/internal/handlers/storage.go @@ -47,7 +47,8 @@ func (sh *StorageHandler) GetStorageInfo(c *gin.Context) { } // 尝试附加路径与使用率信息 - if storageType == "local" { + switch storageType { + case "local": // 本地存储使用配置中的 StoragePath detail.StoragePath = sh.storageConfig.StoragePath @@ -58,16 +59,16 @@ func (sh *StorageHandler) GetStorageInfo(c *gin.Context) { detail.UsagePercent = &val } } - } else if storageType == "s3" { + case "s3": // S3 使用 bucket 名称作为标识 if sh.storageConfig.S3 != nil { detail.StoragePath = sh.storageConfig.S3.BucketName } - } else if storageType == "webdav" { + case "webdav": if sh.storageConfig.WebDAV != nil { detail.StoragePath = sh.storageConfig.WebDAV.Hostname } - } else if storageType == "nfs" { + case "nfs": if sh.storageConfig.NFS != nil { detail.StoragePath = sh.storageConfig.NFS.MountPoint } diff --git a/internal/services/admin/config.go b/internal/services/admin/config.go index 0bebd75..222a2bd 100644 --- a/internal/services/admin/config.go +++ b/internal/services/admin/config.go @@ -27,17 +27,6 @@ func (s *Service) UpdateConfig(configData map[string]interface{}) error { return s.SaveConfigUpdate(filteredConfigData) } -// UpdateConfigWithDTO 使用DTO更新配置 -func (s *Service) UpdateConfigWithDTO(configUpdate map[string]interface{}) error { - return s.SaveConfigUpdate(configUpdate) -} - -// UpdateConfigWithFlatDTO 使用平面化DTO更新配置 -func (s *Service) UpdateConfigWithFlatDTO(flatUpdate map[string]interface{}) error { - // Expect caller to provide nested map; treat flat as already suitable - return s.SaveConfigUpdate(flatUpdate) -} - // UpdateConfigFromRequest 从结构化请求更新配置 func (s *Service) UpdateConfigFromRequest(configRequest *web.AdminConfigRequest) error { // 构建配置更新数据 @@ -161,27 +150,7 @@ func (s *Service) UpdateConfigFromRequest(configRequest *web.AdminConfigRequest) } // flattenConfig 扁平化配置数据 -func (s *Service) flattenConfig(prefix string, value interface{}, result map[string]interface{}) error { - switch v := value.(type) { - case map[string]interface{}: - // 对于嵌套的对象,递归处理 - for key, val := range v { - newKey := key - if prefix != "" { - newKey = prefix + "." + key - } - if err := s.flattenConfig(newKey, val, result); err != nil { - return err - } - } - default: - // 直接的值 - if prefix != "" { - result[prefix] = value - } - } - return nil -} +// flattenConfig removed - not used after refactor // GetFullConfig 获取完整配置 - 返回配置管理器结构体 func (s *Service) GetFullConfig() *config.ConfigManager { diff --git a/internal/services/admin/maintenance.go b/internal/services/admin/maintenance.go index 28142db..1460a13 100644 --- a/internal/services/admin/maintenance.go +++ b/internal/services/admin/maintenance.go @@ -158,7 +158,8 @@ func (s *Service) GetStorageStatus() (*models.StorageStatus, error) { // 根据当前配置尝试附加 path 与使用率信息 storageType := s.manager.Storage.Type - if storageType == "local" { + switch storageType { + case "local": details["storage_path"] = s.manager.Storage.StoragePath if s.manager.Storage.StoragePath != "" { if usage, err := utils.GetUsagePercent(s.manager.Storage.StoragePath); err == nil { @@ -166,18 +167,20 @@ func (s *Service) GetStorageStatus() (*models.StorageStatus, error) { details["usage_percent"] = int(usage) } } - } else if storageType == "s3" { + case "s3": if s.manager.Storage.S3 != nil { details["storage_path"] = s.manager.Storage.S3.BucketName } - } else if storageType == "webdav" { + case "webdav": if s.manager.Storage.WebDAV != nil { details["storage_path"] = s.manager.Storage.WebDAV.Hostname } - } else if storageType == "nfs" { + case "nfs": if s.manager.Storage.NFS != nil { details["storage_path"] = s.manager.Storage.NFS.MountPoint } + default: + // Handle other storage types if necessary } return &models.StorageStatus{ diff --git a/internal/utils/disk_test.go b/internal/utils/disk_test.go index f3c5a53..6c3c3b4 100644 --- a/internal/utils/disk_test.go +++ b/internal/utils/disk_test.go @@ -16,7 +16,7 @@ func TestGetUsagePercent_ValidPath(t *testing.T) { if err != nil { t.Fatalf("failed to create temp dir: %v", err) } - defer os.RemoveAll(dir) + defer func() { _ = os.RemoveAll(dir) }() usage, err := GetUsagePercent(dir) if err != nil { diff --git a/scripts/export_config_from_db.go b/scripts/export_config_from_db.go index aac27c5..a573602 100644 --- a/scripts/export_config_from_db.go +++ b/scripts/export_config_from_db.go @@ -24,13 +24,13 @@ func main() { if err != nil { log.Fatalf("open db: %v", err) } - defer db.Close() + defer func() { _ = db.Close() }() r, err := db.Query("SELECT key, value FROM key_values") if err != nil { log.Fatalf("query: %v", err) } - defer r.Close() + defer func() { _ = r.Close() }() cfg := make(map[string]map[string]interface{}) cfg["base"] = map[string]interface{}{} @@ -99,7 +99,9 @@ func main() { if err := enc.Encode(cfg); err != nil { log.Fatalf("encode yaml: %v", err) } - outF.Close() + if err := outF.Close(); err != nil { + log.Printf("warning: failed to close outF: %v", err) + } fmt.Printf("wrote %s\n", *out) } diff --git a/scripts/legacy/export_config_from_db.go b/scripts/legacy/export_config_from_db.go index f5a577e..3643745 100644 --- a/scripts/legacy/export_config_from_db.go +++ b/scripts/legacy/export_config_from_db.go @@ -25,13 +25,13 @@ func main() { if err != nil { log.Fatalf("open db: %v", err) } - defer db.Close() + defer func() { _ = db.Close() }() r, err := db.Query("SELECT key, value FROM key_values") if err != nil { log.Fatalf("query: %v", err) } - defer r.Close() + defer func() { _ = r.Close() }() cfg := make(map[string]map[string]interface{}) cfg["base"] = map[string]interface{}{} @@ -96,7 +96,9 @@ func main() { if err := enc.Encode(cfg); err != nil { log.Fatalf("encode yaml: %v", err) } - outF.Close() + if err := outF.Close(); err != nil { + log.Printf("warning: failed to close outF: %v", err) + } fmt.Printf("wrote %s\n", *out) } From 22b7d6fb713db13cec29e6d3a90e4d84ebae353b Mon Sep 17 00:00:00 2001 From: murphyyi Date: Fri, 19 Sep 2025 14:50:48 +0800 Subject: [PATCH 16/21] static: deprecate RegisterAdminStaticRoutes to avoid accidental public admin static registration; enforce protected handlers in routes/admin.go --- internal/config/manager.go | 24 ++++- internal/handlers/setup.go | 7 +- internal/routes/admin.go | 74 ++++++++++++++-- internal/routes/setup.go | 11 ++- internal/routes/user.go | 35 ++------ internal/services/auth/auth.go | 8 +- internal/static/assets.go | 37 ++++++-- main.go | 63 +++++++++++++ themes/2025/js/dashboard.js | 157 +++++++++++++++++++++++++++++++-- themes/2025/js/main.js | 11 ++- themes/2025/login.html | 40 +++++++-- themes/2025/setup.html | 60 +++++++++---- 12 files changed, 446 insertions(+), 81 deletions(-) diff --git a/internal/config/manager.go b/internal/config/manager.go index a2c3495..0cf75b1 100644 --- a/internal/config/manager.go +++ b/internal/config/manager.go @@ -4,6 +4,7 @@ import ( "errors" "os" "strconv" + "strings" "gopkg.in/yaml.v3" @@ -100,7 +101,28 @@ func (cm *ConfigManager) LoadFromYAML(path string) error { cm.Storage = fileCfg.Storage } if fileCfg.User != nil { - cm.User = fileCfg.User + // Merge user config fields rather than clobbering defaults with an empty struct + if fileCfg.User.AllowUserRegistration != 0 { + cm.User.AllowUserRegistration = fileCfg.User.AllowUserRegistration + } + if fileCfg.User.RequireEmailVerify != 0 { + cm.User.RequireEmailVerify = fileCfg.User.RequireEmailVerify + } + if fileCfg.User.UserUploadSize != 0 { + cm.User.UserUploadSize = fileCfg.User.UserUploadSize + } + if fileCfg.User.UserStorageQuota != 0 { + cm.User.UserStorageQuota = fileCfg.User.UserStorageQuota + } + if fileCfg.User.SessionExpiryHours != 0 { + cm.User.SessionExpiryHours = fileCfg.User.SessionExpiryHours + } + if fileCfg.User.MaxSessionsPerUser != 0 { + cm.User.MaxSessionsPerUser = fileCfg.User.MaxSessionsPerUser + } + if strings.TrimSpace(fileCfg.User.JWTSecret) != "" { + cm.User.JWTSecret = fileCfg.User.JWTSecret + } } if fileCfg.MCP != nil { cm.MCP = fileCfg.MCP diff --git a/internal/handlers/setup.go b/internal/handlers/setup.go index ed82eb6..634e2f9 100644 --- a/internal/handlers/setup.go +++ b/internal/handlers/setup.go @@ -18,7 +18,6 @@ import ( "gorm.io/gorm" ) -// SetupHandler 系统初始化处理器 type SetupHandler struct { daoManager *repository.RepositoryManager manager *config.ConfigManager @@ -175,6 +174,8 @@ func contains(s, substr string) bool { return false } +// legacy flat mapping removed per request - only nested JSON supported now + // OnDatabaseInitialized 当数据库初始化完成时,handlers 包中的回调(由 main 设置) var OnDatabaseInitialized func(daoManager *repository.RepositoryManager) @@ -191,14 +192,16 @@ func InitializeNoDB(manager *config.ConfigManager) gin.HandlerFunc { return } defer atomic.StoreInt32(&initInProgress, 0) + // 解析 JSON(仅接受嵌套结构),不再兼容 legacy 扁平字段 var req SetupRequest if err := c.ShouldBindJSON(&req); err != nil { common.BadRequestResponse(c, "请求参数错误: "+err.Error()) return } - // 继续使用 req 进行验证和初始化 var desiredStoragePath string + // 不再从请求体中读取 legacy storage_path;如果配置管理器已包含 storage path,则后续逻辑会处理 + if manager.Storage.Type == "local" { sp := manager.Storage.StoragePath // 若为相对路径,则相对于 manager.Base.DataPath diff --git a/internal/routes/admin.go b/internal/routes/admin.go index b8d3233..bff000b 100644 --- a/internal/routes/admin.go +++ b/internal/routes/admin.go @@ -1,6 +1,9 @@ package routes import ( + "os" + "path/filepath" + "github.com/zy84338719/filecodebox/internal/config" "github.com/zy84338719/filecodebox/internal/handlers" "github.com/zy84338719/filecodebox/internal/middleware" @@ -22,12 +25,10 @@ func SetupAdminRoutes( // 管理相关路由 adminGroup := router.Group("/admin") - // 管理页面和静态文件 - 不需要认证就能访问HTML和静态资源 + // 管理页面和静态文件 - 管理页面本身应当需要管理员认证,静态资源仍然注册为公开以便前端加载 { - // 管理页面 - adminGroup.GET("/", func(c *gin.Context) { - static.ServeAdminPage(c, cfg) - }) + // 管理页面 - 不在此注册(移到需要认证的路由组),确保只有管理员可以访问前端入口 + // 登录接口保持公开 // 管理员登录(通过用户名/密码获取 JWT) // 如果已经存在相同的 POST /admin/login 路由(例如在未初始化数据库时注册的占位处理器), @@ -51,8 +52,6 @@ func SetupAdminRoutes( }) } - // 模块化管理后台静态文件 - static.RegisterAdminStaticRoutes(adminGroup, cfg) } // 使用复用的中间件实现(JWT 用户认证并要求 admin 角色) @@ -62,6 +61,67 @@ func SetupAdminRoutes( authGroup := adminGroup.Group("") authGroup.Use(combinedAuthMiddleware) { + // 显式为管理后台静态资源注册受保护的 GET 处理器,确保这些静态文件也需要管理员认证 + themeDir := "./" + cfg.ThemesSelect + + // css + authGroup.GET("/css/*filepath", func(c *gin.Context) { + fp := c.Param("filepath") + p := filepath.Join(themeDir, "admin", "css", fp) + if _, err := os.Stat(p); err != nil { + c.Status(404) + return + } + c.File(p) + }) + + // js + authGroup.GET("/js/*filepath", func(c *gin.Context) { + fp := c.Param("filepath") + p := filepath.Join(themeDir, "admin", "js", fp) + if _, err := os.Stat(p); err != nil { + c.Status(404) + return + } + c.File(p) + }) + + // templates + authGroup.GET("/templates/*filepath", func(c *gin.Context) { + fp := c.Param("filepath") + p := filepath.Join(themeDir, "admin", "templates", fp) + if _, err := os.Stat(p); err != nil { + c.Status(404) + return + } + c.File(p) + }) + + // assets and components + authGroup.GET("/assets/*filepath", func(c *gin.Context) { + fp := c.Param("filepath") + p := filepath.Join(themeDir, "assets", fp) + if _, err := os.Stat(p); err != nil { + c.Status(404) + return + } + c.File(p) + }) + authGroup.GET("/components/*filepath", func(c *gin.Context) { + fp := c.Param("filepath") + p := filepath.Join(themeDir, "components", fp) + if _, err := os.Stat(p); err != nil { + c.Status(404) + return + } + c.File(p) + }) + + // 管理前端入口受保护:仅管理员可访问 /admin/ + authGroup.GET("/", func(c *gin.Context) { + static.ServeAdminPage(c, cfg) + }) + // 仪表板和统计 authGroup.GET("/dashboard", adminHandler.Dashboard) authGroup.GET("/stats", adminHandler.GetStats) diff --git a/internal/routes/setup.go b/internal/routes/setup.go index 228dfdd..77c27f5 100644 --- a/internal/routes/setup.go +++ b/internal/routes/setup.go @@ -12,6 +12,7 @@ import ( "github.com/zy84338719/filecodebox/internal/middleware" "github.com/zy84338719/filecodebox/internal/repository" "github.com/zy84338719/filecodebox/internal/services" + "github.com/zy84338719/filecodebox/internal/static" "github.com/zy84338719/filecodebox/internal/storage" "github.com/gin-gonic/gin" @@ -96,13 +97,17 @@ func CreateAndSetupRouter( // 即便数据库尚未初始化,也应当能访问用户登录/注册页面(只返回静态HTML), // 以便用户能够在首次部署时完成初始化或查看登录页面。 router.GET("/user/login", func(c *gin.Context) { - ServeUserPage(c, manager, "login.html") + static.ServeUserPage(c, manager, "login.html") }) router.GET("/user/register", func(c *gin.Context) { - ServeUserPage(c, manager, "register.html") + static.ServeUserPage(c, manager, "register.html") + }) + // 在未初始化数据库时,也允许访问用户仪表板静态页面,避免被 NoRoute 回退到首页 + router.GET("/user/dashboard", func(c *gin.Context) { + static.ServeUserPage(c, manager, "dashboard.html") }) router.GET("/user/forgot-password", func(c *gin.Context) { - ServeUserPage(c, manager, "forgot-password.html") + static.ServeUserPage(c, manager, "forgot-password.html") }) // 在未初始化数据库时,不直接注册真实的 POST /admin/login 处理器以避免后续动态注册冲突。 diff --git a/internal/routes/user.go b/internal/routes/user.go index 0f40cbb..28588ff 100644 --- a/internal/routes/user.go +++ b/internal/routes/user.go @@ -1,14 +1,10 @@ package routes import ( - "net/http" - "os" - "path/filepath" - "strings" - "github.com/zy84338719/filecodebox/internal/config" "github.com/zy84338719/filecodebox/internal/handlers" "github.com/zy84338719/filecodebox/internal/middleware" + "github.com/zy84338719/filecodebox/internal/static" "github.com/gin-gonic/gin" ) @@ -29,16 +25,16 @@ func SetupUserRoutes( userPageGroup := router.Group("/user") { userPageGroup.GET("/login", func(c *gin.Context) { - ServeUserPage(c, cfg, "login.html") + static.ServeUserPage(c, cfg, "login.html") }) userPageGroup.GET("/register", func(c *gin.Context) { - ServeUserPage(c, cfg, "register.html") + static.ServeUserPage(c, cfg, "register.html") }) userPageGroup.GET("/dashboard", func(c *gin.Context) { - ServeUserPage(c, cfg, "dashboard.html") + static.ServeUserPage(c, cfg, "dashboard.html") }) userPageGroup.GET("/forgot-password", func(c *gin.Context) { - ServeUserPage(c, cfg, "forgot-password.html") + static.ServeUserPage(c, cfg, "forgot-password.html") }) } } @@ -79,23 +75,4 @@ func SetupUserAPIRoutes( } // ServeUserPage 服务用户页面 -func ServeUserPage(c *gin.Context, cfg *config.ConfigManager, pageName string) { - userPagePath := filepath.Join(".", cfg.ThemesSelect, pageName) - - content, err := os.ReadFile(userPagePath) - if err != nil { - c.String(http.StatusNotFound, "User page not found: "+pageName) - return - } - - html := string(content) - // 将相对静态资源路径转换为绝对路径,避免在子路径下(如 /user/login)请求到 /user/js/... 导致返回 HTML - html = strings.ReplaceAll(html, "src=\"js/", "src=\"/js/") - html = strings.ReplaceAll(html, "href=\"css/", "href=\"/css/") - html = strings.ReplaceAll(html, "src=\"assets/", "src=\"/assets/") - html = strings.ReplaceAll(html, "href=\"assets/", "href=\"/assets/") - - c.Header("Cache-Control", "no-cache") - c.Header("Content-Type", "text/html; charset=utf-8") - c.String(http.StatusOK, html) -} +// ServeUserPage has been moved to internal/static package (static.ServeUserPage) diff --git a/internal/services/auth/auth.go b/internal/services/auth/auth.go index 88fc718..a8d8112 100644 --- a/internal/services/auth/auth.go +++ b/internal/services/auth/auth.go @@ -308,12 +308,18 @@ func (s *Service) CreateUserSession(user *models.User, ipAddress, userAgent stri } // 创建会话记录 + // 确保会话过期时长合理:如果配置为0或负数,使用默认7天(168小时)作为兜底 + expiryHours := s.manager.User.SessionExpiryHours + if expiryHours <= 0 { + expiryHours = 24 * 7 // 7天 + } + session := &models.UserSession{ UserID: user.ID, SessionID: sessionID, IPAddress: ipAddress, UserAgent: userAgent, - ExpiresAt: time.Now().Add(time.Hour * time.Duration(s.manager.User.SessionExpiryHours)), + ExpiresAt: time.Now().Add(time.Hour * time.Duration(expiryHours)), IsActive: true, } diff --git a/internal/static/assets.go b/internal/static/assets.go index 367eff1..27a4a7e 100644 --- a/internal/static/assets.go +++ b/internal/static/assets.go @@ -23,12 +23,15 @@ func RegisterStaticRoutes(router *gin.Engine, cfg *config.ConfigManager) { // RegisterAdminStaticRoutes registers admin panel static routes (admin css/js/templates) func RegisterAdminStaticRoutes(adminGroup *gin.RouterGroup, cfg *config.ConfigManager) { - themeDir := fmt.Sprintf("./%s", cfg.ThemesSelect) - adminGroup.Static("/css", fmt.Sprintf("%s/admin/css", themeDir)) - adminGroup.Static("/js", fmt.Sprintf("%s/admin/js", themeDir)) - adminGroup.Static("/templates", fmt.Sprintf("%s/admin/templates", themeDir)) - adminGroup.Static("/assets", fmt.Sprintf("%s/assets", themeDir)) - adminGroup.Static("/components", fmt.Sprintf("%s/components", themeDir)) + // Deprecated: do NOT register admin static routes publicly here. + // Admin static assets are security-sensitive and must be served + // through protected handlers (see internal/routes/admin.go) which + // apply the required authentication middleware. Keeping this + // function as a no-op avoids accidental public registration while + // preserving the API for older callers. + _ = adminGroup + _ = cfg + return } // ServeIndex serves the main index page with basic template replacements. @@ -97,3 +100,25 @@ func ServeAdminPage(c *gin.Context, cfg *config.ConfigManager) { c.Header("Content-Type", "text/html; charset=utf-8") c.String(http.StatusOK, string(content)) } + +// ServeUserPage serves user-facing static pages (login/register/dashboard/etc.) +func ServeUserPage(c *gin.Context, cfg *config.ConfigManager, pageName string) { + userPagePath := filepath.Join(".", cfg.ThemesSelect, pageName) + + content, err := os.ReadFile(userPagePath) + if err != nil { + c.String(http.StatusNotFound, "User page not found: "+pageName) + return + } + + html := string(content) + // normalize relative static paths to absolute paths so pages under /user/* load correctly + html = strings.ReplaceAll(html, "src=\"js/", "src=\"/js/") + html = strings.ReplaceAll(html, "href=\"css/", "href=\"/css/") + html = strings.ReplaceAll(html, "src=\"assets/", "src=\"/assets/") + html = strings.ReplaceAll(html, "href=\"assets/", "href=\"/assets/") + + c.Header("Cache-Control", "no-cache") + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, html) +} diff --git a/main.go b/main.go index c8c6546..4e7d5b0 100644 --- a/main.go +++ b/main.go @@ -25,12 +25,14 @@ import ( "context" "os" "os/signal" + "path/filepath" "syscall" "time" "github.com/gin-gonic/gin" "github.com/zy84338719/filecodebox/internal/cli" "github.com/zy84338719/filecodebox/internal/config" + "github.com/zy84338719/filecodebox/internal/database" "github.com/zy84338719/filecodebox/internal/handlers" "github.com/zy84338719/filecodebox/internal/mcp" "github.com/zy84338719/filecodebox/internal/repository" @@ -125,6 +127,67 @@ func main() { logrus.Info("应用初始化完成") + // 如果 data/filecodebox.db 已经存在,尝试提前初始化数据库并注册动态路由, + // 这样在已初始化环境下,API(如 POST /user/login)能直接可用,而不是返回静态 HTML。 + // 这对于用户已经自行初始化数据库但以 "daoManager == nil" 启动的场景非常重要。 + // 尝试多路径检测数据库文件:优先使用 manager.Database.Name(若配置了),其次尝试基于 Base.DataPath 的常见位置 + var candidates []string + if manager.Database.Name != "" { + candidates = append(candidates, manager.Database.Name) + } + if manager.Base != nil && manager.Base.DataPath != "" { + candidates = append(candidates, filepath.Join(manager.Base.DataPath, "filecodebox.db")) + // 如果 manager.Database.Name 是相对路径,尝试基于 DataPath 拼接 + if manager.Database.Name != "" && !filepath.IsAbs(manager.Database.Name) { + candidates = append(candidates, filepath.Join(manager.Base.DataPath, manager.Database.Name)) + } + } + + logrus.Infof("数据库检测候选路径: %v", candidates) + found := "" + for _, dbFile := range candidates { + if dbFile == "" { + continue + } + if _, err := os.Stat(dbFile); err == nil { + found = dbFile + break + } + } + + if found != "" { + logrus.Infof("检测到已有数据库文件,尝试提前初始化数据库: %s", found) + // 如果 Base.DataPath 为空,设置为 found 的目录,避免 database.InitWithManager 在创建目录时使用空字符串 + if manager.Base == nil { + manager.Base = &config.BaseConfig{} + } + if manager.Base.DataPath == "" { + dir := filepath.Dir(found) + if dir == "." || dir == "" { + dir = "./data" + } + if abs, err := filepath.Abs(dir); err == nil { + manager.Base.DataPath = abs + } else { + manager.Base.DataPath = dir + } + logrus.Infof("为 manager.Base.DataPath 赋值: %s", manager.Base.DataPath) + } + + if db, err := database.InitWithManager(manager); err == nil { + if db != nil { + manager.SetDB(db) + dmgr := repository.NewRepositoryManager(db) + if ginEngine, ok := routerEngine.(*gin.Engine); ok { + routes.RegisterDynamicRoutes(ginEngine, manager, dmgr, storageManager) + logrus.Info("动态路由已注册(基于已存在的数据库)") + } + } + } else { + logrus.Warnf("尝试提前初始化数据库失败: %v", err) + } + } + // 等待中断信号,优雅退出 sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) diff --git a/themes/2025/js/dashboard.js b/themes/2025/js/dashboard.js index 389ebd6..5d5fef1 100644 --- a/themes/2025/js/dashboard.js +++ b/themes/2025/js/dashboard.js @@ -12,11 +12,34 @@ const Dashboard = { * 初始化仪表板 */ async init() { + // 如果有 token 但缺少 user_info,先尝试在初始化阶段拉取用户信息(自愈),最多重试3次 + const token = UserAuth.getToken(); + if (token && !UserAuth.getUserInfo()) { + console.log('[dashboard] 检测到 token 存在但 user_info 缺失,开始最多 3 次尝试拉取用户信息'); + let success = false; + for (let attempt = 1; attempt <= 3; attempt++) { + try { + console.log(`[dashboard] 拉取 user_info 尝试 #${attempt}`); + const userInfo = await this.loadUserInfo(); + if (userInfo) { + success = true; + break; + } + } catch (err) { + console.error('[dashboard] 尝试拉取 user_info 时出错:', err); + } + // 指数退避等待 + await new Promise(res => setTimeout(res, 300 * attempt)); + } + if (!success) { + console.warn('[dashboard] 多次尝试后仍无法获取 user_info'); + this.showProfileRetryPrompt(); + } + } + + // 认证检查(如果没有 token,会在页面内显示登录提示) if (!this.checkAuth()) return; - // 加载用户信息 - await this.loadUserInfo(); - const userInfo = UserAuth.getUserInfo(); if (userInfo) { this.updateUserDisplay(userInfo); @@ -44,11 +67,56 @@ const Dashboard = { checkAuth() { const token = UserAuth.getToken(); if (!token) { - window.location.href = '/user/login'; + // 不再直接重定向到登录页,避免在某些环境下导致页面闪现为空白。 + // 改为在页面内显示友好的登录提示,用户可以点击跳转登录。 + this.showLoginPrompt(); return false; } return true; }, + + /** + * 在页面中间显示登录提示(当用户未登录或 token 缺失时) + */ + showLoginPrompt() { + try { + const container = document.querySelector('.container') || document.body; + // 避免重复创建 + if (document.getElementById('dashboard-login-prompt')) return; + + const prompt = document.createElement('div'); + prompt.id = 'dashboard-login-prompt'; + prompt.style.position = 'fixed'; + prompt.style.left = '50%'; + prompt.style.top = '50%'; + prompt.style.transform = 'translate(-50%, -50%)'; + prompt.style.zIndex = '9999'; + prompt.style.background = 'rgba(255,255,255,0.96)'; + prompt.style.padding = '24px 32px'; + prompt.style.borderRadius = '8px'; + prompt.style.boxShadow = '0 6px 20px rgba(0,0,0,0.12)'; + prompt.style.textAlign = 'center'; + prompt.innerHTML = ` +

您尚未登录

+

要访问用户中心,请先登录账户。

+
+ + +
+ `; + + container.appendChild(prompt); + + document.getElementById('dashboard-login-btn').addEventListener('click', () => { + window.location.href = '/user/login'; + }); + document.getElementById('dashboard-refresh-btn').addEventListener('click', () => { + window.location.reload(); + }); + } catch (err) { + console.error('显示登录提示失败:', err); + } + }, /** * 更新用户显示信息 @@ -73,19 +141,92 @@ const Dashboard = { const response = await fetch('/user/profile', { headers: UserAuth.getAuthHeaders() }); - - if (response.ok) { - const result = await response.json(); + if (!response.ok) { + console.warn('[dashboard] /user/profile 返回非 2xx 状态', response.status); + return null; + } + + const result = await response.json().catch(err => { + console.error('[dashboard] 解析 /user/profile 返回 JSON 失败:', err); + return null; + }); + + if (!result) return null; + + if (result.code === 200 && result.data) { const userInfo = result.data; UserAuth.setUserInfo(userInfo); + // 更新 UI 状态以反映登录状态 + UserAuth.updateUI(); + console.log('[dashboard] 已获取并保存 user_info'); return userInfo; + } else { + console.warn('[dashboard] /user/profile 返回结构非预期:', result); + return null; } } catch (error) { console.error('获取用户信息失败:', error); } return null; }, - + + /** + * 当拉取 user_info 多次失败时,提供一个可操作提示(重试或重新登录) + */ + showProfileRetryPrompt() { + try { + const container = document.querySelector('.container') || document.body; + // 避免重复创建 + if (document.getElementById('dashboard-profile-retry')) return; + + const prompt = document.createElement('div'); + prompt.id = 'dashboard-profile-retry'; + prompt.style.position = 'fixed'; + prompt.style.left = '50%'; + prompt.style.top = '60%'; + prompt.style.transform = 'translate(-50%, -50%)'; + prompt.style.zIndex = '9999'; + prompt.style.background = 'rgba(255,255,255,0.96)'; + prompt.style.padding = '16px 20px'; + prompt.style.borderRadius = '6px'; + prompt.style.boxShadow = '0 6px 20px rgba(0,0,0,0.12)'; + prompt.style.textAlign = 'center'; + prompt.innerHTML = ` +
获取用户信息失败
+
系统检测到你已登录(token 存在),但无法获取到账户信息,可能是网络或会话问题。
+
+ + +
+ `; + + container.appendChild(prompt); + + document.getElementById('dashboard-retry-profile').addEventListener('click', async () => { + document.getElementById('dashboard-profile-retry').remove(); + console.log('[dashboard] 用户触发重试获取 user_info'); + await this.loadUserInfo(); + const ui = UserAuth.getUserInfo(); + if (ui) { + this.updateUserDisplay(ui); + this.loadDashboard(); + } else { + // 如果仍失败,重新展示提示 + this.showProfileRetryPrompt(); + } + }); + + document.getElementById('dashboard-rel-login').addEventListener('click', () => { + // 清理本地登录信息并跳转登录页 + UserAuth.removeToken(); + UserAuth.removeUserInfo(); + window.location.href = '/user/login'; + }); + } catch (err) { + console.error('显示 profile 重试提示失败:', err); + } + }, + /** * 切换标签页 */ diff --git a/themes/2025/js/main.js b/themes/2025/js/main.js index 50bebcc..6bdd670 100644 --- a/themes/2025/js/main.js +++ b/themes/2025/js/main.js @@ -298,9 +298,14 @@ class FileCodeBoxApp { applyTemplateConfig() { if (window.AppConfig) { // 应用不透明度 - if (window.AppConfig.opacity && window.AppConfig.opacity !== '{{opacity}}') { - document.body.style.opacity = window.AppConfig.opacity; - } + if (window.AppConfig.opacity && window.AppConfig.opacity !== '{{opacity}}') { + // 如果 opacity 为字符串 '0' 或 '0.0',不应把整个页面设为完全透明。 + // 只在解析为数字且大于0时应用不透明度设置。 + const op = parseFloat(window.AppConfig.opacity); + if (!isNaN(op) && op > 0) { + document.body.style.opacity = op; + } + } // 应用背景图片 if (window.AppConfig.background && window.AppConfig.background !== '{{background}}') { diff --git a/themes/2025/login.html b/themes/2025/login.html index e3be9ee..a7efca0 100644 --- a/themes/2025/login.html +++ b/themes/2025/login.html @@ -130,20 +130,48 @@ const result = await response.json(); if (result.code === 200) { + // 存储 token localStorage.setItem('user_token', result.data.token); - showMessage('登录成功!正在跳转...', false); - + console.log('[login] 登录成功,已保存 token'); + showMessage('登录成功!正在获取用户信息...', false); + + // 立即请求用户信息并保存到 localStorage,以便前端其它模块不会因为缺少 user_info 而阻塞渲染 + try { + const profileResp = await fetch('/user/profile', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + result.data.token + } + }); + + const profileResult = await profileResp.json(); + if (profileResult && profileResult.code === 200 && profileResult.data) { + localStorage.setItem('user_info', JSON.stringify(profileResult.data)); + console.log('[login] 已获取并保存 user_info'); + } else { + console.warn('[login] 获取 user_info 未返回预期结果', profileResult); + } + } catch (err) { + console.error('[login] 获取用户信息失败:', err); + // 不阻塞跳转,仅告知用户 + showMessage('登录成功,但获取用户信息失败,稍后可能需要刷新页面', true); + } + + // 跳转到期望页面(使用 replace 更可靠,不会留下历史记录) setTimeout(() => { - // 检查是否需要跳转回管理页面 const redirectUrl = sessionStorage.getItem('redirect_after_login'); if (redirectUrl) { sessionStorage.removeItem('redirect_after_login'); - window.location.href = redirectUrl; + console.log('[login] 跳转到 redirect_after_login:', redirectUrl); + window.location.replace(redirectUrl); } else { - window.location.href = '/user/dashboard'; + console.log('[login] 跳转到 /user/dashboard'); + window.location.replace('/user/dashboard'); } - }, 1000); + }, 800); } else { + console.log('[login] 登录失败响应:', result); showMessage(result.message || '登录失败,请检查用户名和密码'); } } catch (error) { diff --git a/themes/2025/setup.html b/themes/2025/setup.html index 4a952e2..f0f2a07 100644 --- a/themes/2025/setup.html +++ b/themes/2025/setup.html @@ -238,26 +238,26 @@
-
-
-
- +
+
+ +
+

2. 仪表板API测试

+ +
+
+ +
+

3. 用户列表API测试

+ +
+
+ + + + \ No newline at end of file diff --git a/config.yaml b/config.yaml index 72e46d3..aaf5860 100644 --- a/config.yaml +++ b/config.yaml @@ -1,54 +1,71 @@ base: - data_path: /tmp/filecodebox_test - description: 开箱即用的文件快传系统 - host: 0.0.0.0 - name: FileCodeBox - port: 12346 - production: false + name: FileCodeBox + description: 开箱即用的文件快传系统 + keywords: "" + port: 12346 + host: 0.0.0.0 + datapath: /Users/zhangyi/zy/FileCodeBox/data + production: false database: - host: "" - name: ./data/filecodebox.db - port: 0 - ssl: disable - type: sqlite - user: "" + type: sqlite + host: "" + port: 0 + name: ./data/filecodebox.db + user: "" + pass: "" + ssl: disable +transfer: + upload: + openupload: 1 + uploadsize: 10485760 + enablechunk: 0 + chunksize: 2097152 + maxsaveseconds: 0 + download: + enableconcurrentdownload: 1 + maxconcurrentdownloads: 10 + downloadtimeout: 300 +storage: + type: "" + storagepath: "" + s3: null + webdav: null + onedrive: null + nfs: null +user: + allowuserregistration: 1 + requireemailverify: 0 + useruploadsize: 52428800 + userstoragequota: 1073741824 + sessionexpiryhours: 168 + maxsessionsperuser: 5 + jwtsecret: FileCodeBox2025JWT mcp: - host: 0.0.0.0 - port: 8081 -storage: {} + enablemcpserver: 0 + mcpport: "" + mcphost: "" +notifytitle: "" +notifycontent: "" ui: - allow_user_registration: 0 - background: "" - chunk_size: 2097152 - download_timeout: 300 - enable_chunk: 0 - enable_concurrent_download: 1 - enable_mcp_server: 0 - error_count: 1 - error_minute: 1 - file_storage: local - jwt_secret: FileCodeBox2025JWT - keywords: FileCodeBox, 文件快递柜, 口令传送箱, 匿名口令分享文本, 文件 - max_concurrent_downloads: 10 - max_save_seconds: 0 - max_sessions_per_user: 5 - notify_content: "欢迎使用 FileCodeBox,本程序开源于 Github ,欢迎Star和Fork。" - notify_title: 系统通知 - opacity: 0 - open_upload: 1 - page_explain: "请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。" - require_email_verify: 0 - robots_text: |- + themes_select: themes/2025 + background: "" + page_explain: 请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。 + robots_text: |- + User-agent: * + Disallow: / + show_admin_addr: 0 + opacity: 0 +themes_select: themes/2025 +robots_text: |- User-agent: * Disallow: / - session_expiry_hours: 168 - show_admin_address: 0 - storage_path: "" - sys_start: 1757914992279 - themes_select: themes/2025 - upload_count: 10 - upload_minute: 1 - upload_size: 100 - user_storage_quota: 1073741824 - user_upload_size: 52428800 -user: {} +page_explain: 请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。 +show_admin_addr: 0 +opacity: 0 +background: "" +sys_start: "" +upload_minute: 0 +upload_count: 0 +error_minute: 0 +error_count: 0 +expire_style: [] diff --git a/docs/CONFIG_REFACTOR_SUMMARY.md b/docs/CONFIG_REFACTOR_SUMMARY.md index 13e0f21..a2bd1a0 100644 --- a/docs/CONFIG_REFACTOR_SUMMARY.md +++ b/docs/CONFIG_REFACTOR_SUMMARY.md @@ -75,8 +75,6 @@ 每个配置模块都实现以下标准接口: ```go - Validate() error // 配置验证 -- ToMap() map[string]string // 转换为键值对 -- FromMap(map[string]string) error // 从键值对加载 - Update(map[string]interface{}) error // 更新配置 - Clone() *ConfigType // 克隆配置 ``` diff --git a/docs/changelogs/REFACTOR_SUMMARY.md b/docs/changelogs/REFACTOR_SUMMARY.md index c207851..3a469b0 100644 --- a/docs/changelogs/REFACTOR_SUMMARY.md +++ b/docs/changelogs/REFACTOR_SUMMARY.md @@ -90,7 +90,7 @@ internal/ ### 配置管理增强 - **分层配置结构**:每个功能模块都有独立的配置结构体 -- **序列化支持**:`ToMap()` 和 `FromMap()` 方法支持配置的序列化和反序列化 +- **类型安全**:使用结构体 Clone() 方法和直接字段访问,避免 map 转换开销 - **配置验证**:每个配置模块都有 `Validate()` 方法进行配置验证 - **热重载**:`ReloadConfig()` 支持运行时配置更新,无需重启应用 diff --git a/docs/config.new.yaml b/docs/config.new.yaml new file mode 100644 index 0000000..1ea6c99 --- /dev/null +++ b/docs/config.new.yaml @@ -0,0 +1,76 @@ +# FileCodeBox 新版 config.yaml 示例 +# 完全分层、无平铺字段,所有配置项归属功能模块 + +base: + name: 文件快递柜 - FileCodeBox + description: "开箱即用的文件快传系统" + keywords: "FileCodeBox, 文件快递柜, 匿名口令分享, 文件" + port: 12345 + host: "0.0.0.0" + data_path: "./data" + production: false + +transfer: + upload: + open_upload: 1 + upload_size: 10485760 + enable_chunk: 1 + chunk_size: 2097152 + max_save_seconds: 0 + download: + enable_concurrent_download: 1 + max_concurrent_downloads: 10 + download_timeout: 300 + +user: + allow_user_registration: 1 + require_email_verify: 0 + user_upload_size: 10485760 + user_storage_quota: 1073741824 + session_expiry_hours: 72 + max_sessions_per_user: 5 + jwt_secret: "your_jwt_secret" + +ui: + notify_title: "欢迎使用 FileCodeBox" + notify_content: "安全、便捷的文件快传系统" + themes_select: "themes/2025" + opacity: 0.95 + background: "" + page_explain: "本系统支持大文件快传、断点续传、匿名分享等功能。" + robots_text: "User-agent: *\nDisallow: /admin/" + show_admin_addr: 1 + +mcp: + enable_mcp_server: 1 + mcp_port: "12346" + mcp_host: "127.0.0.1" + +storage: + type: "local" + local: + path: "./uploads" + s3: + endpoint: "" + access_key: "" + secret_key: "" + bucket: "" + region: "" + webdav: + url: "" + username: "" + password: "" + onedrive: + client_id: "" + client_secret: "" + refresh_token: "" + drive_id: "" + +database: + type: "sqlite" + sqlite: + path: "./data/filecodebox.db" + mysql: + dsn: "" + postgres: + dsn: "" diff --git a/docs/config_migration_advice.md b/docs/config_migration_advice.md new file mode 100644 index 0000000..9f571cc --- /dev/null +++ b/docs/config_migration_advice.md @@ -0,0 +1,27 @@ +# FileCodeBox 配置协议重构迁移建议 + +1. config.yaml 迁移: + - 所有平铺字段(如 notify_title、opacity、themes_select 等)全部归入对应功能模块(如 ui、base、transfer、user 等)。 + - 结构调整后,所有配置项都在一级模块下,便于维护和热重载。 + +2. Go struct 迁移: + - ConfigManager 及所有子 struct 按 config.yaml 完全分层,字段命名、类型、json/yaml tag 保持一致。 + - AdminConfigRequest/Response 直接复用 ConfigManager,无需再单独定义冗余字段。 + +3. handler 层迁移: + - /admin/config 响应直接返回 ConfigManager 对象,无需 hack、无需字段展开。 + - 更新配置时,直接反序列化为 ConfigManager 结构体。 + +4. 前端迁移: + - 只需按模块对象解析(如 config.base.name、config.ui.notify_title),无需兼容处理。 + - 配置表单、展示、保存等全部按分层结构处理。 + +5. 兼容建议: + - 迁移期间可保留旧字段一段时间,前后端同步切换。 + - 配置热重载、持久化、校验等逻辑建议全部基于新版分层结构实现。 + +6. 未来扩展: + - 新增模块/字段时只需在 config.yaml、ConfigManager、前端 schema 同步添加即可。 + - 支持自动生成配置文档、前端表单 schema、API 文档等。 + +如需自动迁移脚本或批量转换工具,可进一步定制! diff --git a/internal/common/response.go b/internal/common/response.go index ff01c99..c05d1ac 100644 --- a/internal/common/response.go +++ b/internal/common/response.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" "github.com/zy84338719/filecodebox/internal/models/web" ) @@ -44,6 +45,8 @@ func BadRequestResponse(c *gin.Context, message string) { // UnauthorizedResponse 401 未授权响应 func UnauthorizedResponse(c *gin.Context, message string) { + // log the unauthorized response with request path to aid debugging + logrus.WithField("path", c.Request.URL.Path).Infof("UnauthorizedResponse: %s", message) c.JSON(http.StatusUnauthorized, web.ErrorResponse{ Code: http.StatusUnauthorized, Message: message, diff --git a/internal/config/base_config.go b/internal/config/base_config.go index c701bea..d607142 100644 --- a/internal/config/base_config.go +++ b/internal/config/base_config.go @@ -95,46 +95,6 @@ func (bc *BaseConfig) IsPublic() bool { return bc.Host == "0.0.0.0" } -// ToMap 转换为map格式 -func (bc *BaseConfig) ToMap() map[string]string { - return map[string]string{ - "name": bc.Name, - "description": bc.Description, - "keywords": bc.Keywords, - "port": fmt.Sprintf("%d", bc.Port), - "host": bc.Host, - "data_path": bc.DataPath, - "production": fmt.Sprintf("%t", bc.Production), - } -} - -// Update 更新配置 -func (bc *BaseConfig) Update(updates map[string]interface{}) error { - if name, ok := updates["name"].(string); ok { - bc.Name = name - } - if desc, ok := updates["description"].(string); ok { - bc.Description = desc - } - if keywords, ok := updates["keywords"].(string); ok { - bc.Keywords = keywords - } - if port, ok := updates["port"].(int); ok { - bc.Port = port - } - if host, ok := updates["host"].(string); ok { - bc.Host = host - } - if dataPath, ok := updates["data_path"].(string); ok { - bc.DataPath = dataPath - } - if production, ok := updates["production"].(bool); ok { - bc.Production = production - } - - return bc.Validate() -} - // Clone 克隆配置 func (bc *BaseConfig) Clone() *BaseConfig { return &BaseConfig{ diff --git a/internal/config/database_config.go b/internal/config/database_config.go index d0caa9b..8e00171 100644 --- a/internal/config/database_config.go +++ b/internal/config/database_config.go @@ -4,7 +4,6 @@ package config import ( "errors" "fmt" - "strconv" "strings" ) @@ -123,72 +122,6 @@ func (dc *DatabaseConfig) GetDefaultPort() int { } } -// ToMap 转换为map格式 -func (dc *DatabaseConfig) ToMap() map[string]string { - return map[string]string{ - "database_type": dc.Type, - "database_host": dc.Host, - "database_port": fmt.Sprintf("%d", dc.Port), - "database_name": dc.Name, - "database_user": dc.User, - "database_ssl": dc.SSL, - // 注意:密码出于安全考虑不包含在map中 - } -} - -// FromMap 从map加载配置 -func (dc *DatabaseConfig) FromMap(data map[string]string) error { - if val, ok := data["database_type"]; ok { - dc.Type = val - } - if val, ok := data["database_host"]; ok { - dc.Host = val - } - if val, ok := data["database_port"]; ok { - if port, err := strconv.Atoi(val); err == nil { - dc.Port = port - } - } - if val, ok := data["database_name"]; ok { - dc.Name = val - } - if val, ok := data["database_user"]; ok { - dc.User = val - } - if val, ok := data["database_ssl"]; ok { - dc.SSL = val - } - - return dc.Validate() -} - -// Update 更新配置 -func (dc *DatabaseConfig) Update(updates map[string]interface{}) error { - if dbType, ok := updates["type"].(string); ok { - dc.Type = dbType - } - if host, ok := updates["host"].(string); ok { - dc.Host = host - } - if port, ok := updates["port"].(int); ok { - dc.Port = port - } - if name, ok := updates["name"].(string); ok { - dc.Name = name - } - if user, ok := updates["user"].(string); ok { - dc.User = user - } - if pass, ok := updates["pass"].(string); ok { - dc.Pass = pass - } - if ssl, ok := updates["ssl"].(string); ok { - dc.SSL = ssl - } - - return dc.Validate() -} - // Clone 克隆配置 func (dc *DatabaseConfig) Clone() *DatabaseConfig { return &DatabaseConfig{ diff --git a/internal/config/layered_manager.go b/internal/config/layered_manager.go deleted file mode 100644 index fbdfce5..0000000 --- a/internal/config/layered_manager.go +++ /dev/null @@ -1,419 +0,0 @@ -// Package config 分层配置管理器 -package config - -import ( - "encoding/json" - "fmt" - "sync" - "time" - - "gorm.io/gorm" -) - -// LayeredConfigManager 分层配置管理器 -type LayeredConfigManager struct { - storageStrategy *ConfigStorageStrategy - cache map[string]interface{} - cacheMutex sync.RWMutex - cacheExpiry map[string]time.Time - db *gorm.DB - - // 配置模块 - Base *BaseConfig `json:"base"` - Database *DatabaseConfig `json:"database"` - Transfer *TransferConfig `json:"transfer"` - Storage *StorageConfig `json:"storage"` - User *UserSystemConfig `json:"user"` - MCP *MCPConfig `json:"mcp"` - - // 业务配置 - Notification *NotificationConfig `json:"notification"` - RateLimit *RateLimitConfig `json:"rate_limit"` - Theme *ThemeConfig `json:"theme"` -} - -// NotificationConfig 通知配置 -type NotificationConfig struct { - Title string `json:"title"` - Content string `json:"content"` - Explain string `json:"explain"` -} - -// RateLimitConfig 限流配置 -type RateLimitConfig struct { - UploadMinute int `json:"upload_minute"` - UploadCount int `json:"upload_count"` - ErrorMinute int `json:"error_minute"` - ErrorCount int `json:"error_count"` -} - -// ThemeConfig 主题配置 -type ThemeConfig struct { - Select string `json:"select"` - Choices []Theme `json:"choices"` - Opacity float64 `json:"opacity"` - Background string `json:"background"` -} - -// NewLayeredConfigManager 创建分层配置管理器 -func NewLayeredConfigManager(db *gorm.DB) *LayeredConfigManager { - manager := &LayeredConfigManager{ - storageStrategy: NewConfigStorageStrategy(db), - cache: make(map[string]interface{}), - cacheExpiry: make(map[string]time.Time), - db: db, - } - - // 初始化配置模块 - manager.initConfigModules() - - return manager -} - -// initConfigModules 初始化配置模块 -func (m *LayeredConfigManager) initConfigModules() { - m.Base = &BaseConfig{} - m.Database = &DatabaseConfig{} - m.Transfer = &TransferConfig{} - m.Storage = &StorageConfig{} - m.User = &UserSystemConfig{} - m.MCP = &MCPConfig{} - m.Notification = &NotificationConfig{} - m.RateLimit = &RateLimitConfig{} - m.Theme = &ThemeConfig{} -} - -// InitTables 初始化数据库表 -func (m *LayeredConfigManager) InitTables() error { - return m.storageStrategy.InitTables() -} - -// LoadAllConfigs 加载所有配置 -func (m *LayeredConfigManager) LoadAllConfigs() error { - // 1. 加载静态配置(基础设置) - if err := m.loadStaticConfigs(); err != nil { - return fmt.Errorf("加载静态配置失败: %v", err) - } - - // 2. 加载系统配置(存储、传输、数据库) - if err := m.loadSystemConfigs(); err != nil { - return fmt.Errorf("加载系统配置失败: %v", err) - } - - // 3. 加载运行时配置(用户、MCP) - if err := m.loadRuntimeConfigs(); err != nil { - return fmt.Errorf("加载运行时配置失败: %v", err) - } - - // 4. 加载业务配置(通知、限流) - if err := m.loadBusinessConfigs(); err != nil { - return fmt.Errorf("加载业务配置失败: %v", err) - } - - return nil -} - -// loadStaticConfigs 加载静态配置 -func (m *LayeredConfigManager) loadStaticConfigs() error { - // 加载基础配置 - if err := m.getConfig("base.name", &m.Base.Name); err != nil { - m.Base.Name = "文件快递柜 - FileCodeBox" // 默认值 - } - if err := m.getConfig("base.description", &m.Base.Description); err != nil { - m.Base.Description = "开箱即用的文件快传系统" - } - - // 加载主题配置 - if err := m.getConfig("theme.select", &m.Theme.Select); err != nil { - m.Theme.Select = "themes/2025" - } - - return nil -} - -// loadSystemConfigs 加载系统配置 -func (m *LayeredConfigManager) loadSystemConfigs() error { - // 加载存储配置 - if err := m.getConfig("storage.config", m.Storage); err != nil { - // 设置默认存储配置 - m.Storage.Type = "local" - m.Storage.StoragePath = "" - } - - // 加载传输配置 - if err := m.getConfig("transfer.config", m.Transfer); err != nil { - // 设置默认传输配置 - m.Transfer.Upload = &UploadConfig{ - OpenUpload: 1, - UploadSize: 10 * 1024 * 1024, - EnableChunk: 0, - ChunkSize: 2 * 1024 * 1024, - MaxSaveSeconds: 0, - } - m.Transfer.Download = &DownloadConfig{ - EnableConcurrentDownload: 1, - MaxConcurrentDownloads: 10, - DownloadTimeout: 300, - } - } - - // 加载数据库配置 - if err := m.getConfig("database.config", m.Database); err != nil { - // 设置默认数据库配置 - m.Database.Type = "sqlite" - m.Database.Host = "localhost" - m.Database.Port = 3306 - m.Database.Name = "filecodebox" - m.Database.User = "root" - m.Database.Pass = "" - m.Database.SSL = "disable" - } - - return nil -} - -// loadRuntimeConfigs 加载运行时配置 -func (m *LayeredConfigManager) loadRuntimeConfigs() error { - // 用户系统始终启用,无需加载配置 - - // 加载MCP配置 - var mcpEnabled int - if err := m.getConfig("mcp.enabled", &mcpEnabled); err != nil { - mcpEnabled = 0 // 默认禁用 - } - m.MCP.EnableMCPServer = mcpEnabled - - return nil -} - -// loadBusinessConfigs 加载业务配置 -func (m *LayeredConfigManager) loadBusinessConfigs() error { - // 加载通知配置 - if err := m.getConfig("notification.config", m.Notification); err != nil { - // 设置默认通知配置 - m.Notification.Title = "系统通知" - m.Notification.Content = "欢迎使用 FileCodeBox" - m.Notification.Explain = "请勿上传或分享违法内容" - } - - // 加载限流配置 - if err := m.getConfig("ratelimit.config", m.RateLimit); err != nil { - // 设置默认限流配置 - m.RateLimit.UploadMinute = 1 - m.RateLimit.UploadCount = 10 - m.RateLimit.ErrorMinute = 1 - m.RateLimit.ErrorCount = 1 - } - - return nil -} - -// getConfig 获取配置(带缓存) -func (m *LayeredConfigManager) getConfig(key string, result interface{}) error { - // 首先检查缓存 - if cached, ok := m.getCachedConfig(key); ok { - // 复制缓存值到result - return m.copyValue(cached, result) - } - - // 从存储策略获取 - if err := m.storageStrategy.GetConfig(key, result); err != nil { - return err - } - - // 更新缓存 - m.setCachedConfig(key, result) - - return nil -} - -// setConfig 设置配置 -func (m *LayeredConfigManager) setConfig(key string, value interface{}) error { - // 设置到存储策略 - if err := m.storageStrategy.SetConfig(key, value); err != nil { - return err - } - - // 更新缓存 - m.setCachedConfig(key, value) - - return nil -} - -// getCachedConfig 获取缓存配置 -func (m *LayeredConfigManager) getCachedConfig(key string) (interface{}, bool) { - m.cacheMutex.RLock() - defer m.cacheMutex.RUnlock() - - // 检查是否过期 - if expiry, exists := m.cacheExpiry[key]; exists && time.Now().After(expiry) { - delete(m.cache, key) - delete(m.cacheExpiry, key) - return nil, false - } - - value, exists := m.cache[key] - return value, exists -} - -// setCachedConfig 设置缓存配置 -func (m *LayeredConfigManager) setCachedConfig(key string, value interface{}) { - m.cacheMutex.Lock() - defer m.cacheMutex.Unlock() - - m.cache[key] = value - - // 设置缓存过期时间(5分钟) - m.cacheExpiry[key] = time.Now().Add(5 * time.Minute) -} - -// copyValue 复制值 -func (m *LayeredConfigManager) copyValue(src, dst interface{}) error { - srcBytes, err := json.Marshal(src) - if err != nil { - return err - } - return json.Unmarshal(srcBytes, dst) -} - -// SaveAllConfigs 保存所有配置 -func (m *LayeredConfigManager) SaveAllConfigs() error { - // 保存静态配置 - if err := m.saveStaticConfigs(); err != nil { - return fmt.Errorf("保存静态配置失败: %v", err) - } - - // 保存系统配置 - if err := m.saveSystemConfigs(); err != nil { - return fmt.Errorf("保存系统配置失败: %v", err) - } - - // 保存运行时配置 - if err := m.saveRuntimeConfigs(); err != nil { - return fmt.Errorf("保存运行时配置失败: %v", err) - } - - // 保存业务配置 - if err := m.saveBusinessConfigs(); err != nil { - return fmt.Errorf("保存业务配置失败: %v", err) - } - - return nil -} - -// saveStaticConfigs 保存静态配置 -func (m *LayeredConfigManager) saveStaticConfigs() error { - if err := m.setConfig("base.name", m.Base.Name); err != nil { - return err - } - if err := m.setConfig("base.description", m.Base.Description); err != nil { - return err - } - if err := m.setConfig("theme.select", m.Theme.Select); err != nil { - return err - } - return nil -} - -// saveSystemConfigs 保存系统配置 -func (m *LayeredConfigManager) saveSystemConfigs() error { - if err := m.setConfig("storage.config", m.Storage); err != nil { - return err - } - if err := m.setConfig("transfer.config", m.Transfer); err != nil { - return err - } - if err := m.setConfig("database.config", m.Database); err != nil { - return err - } - return nil -} - -// saveRuntimeConfigs 保存运行时配置 -func (m *LayeredConfigManager) saveRuntimeConfigs() error { - // 用户系统始终启用,无需保存配置 - if err := m.setConfig("mcp.enabled", m.MCP.EnableMCPServer); err != nil { - return err - } - return nil -} - -// saveBusinessConfigs 保存业务配置 -func (m *LayeredConfigManager) saveBusinessConfigs() error { - if err := m.setConfig("notification.config", m.Notification); err != nil { - return err - } - if err := m.setConfig("ratelimit.config", m.RateLimit); err != nil { - return err - } - return nil -} - -// GetConfigByCategory 按分类获取配置 -func (m *LayeredConfigManager) GetConfigByCategory(category ConfigCategory) (map[string]interface{}, error) { - configs, err := m.storageStrategy.ListConfigsByCategory(category) - if err != nil { - return nil, err - } - - result := make(map[string]interface{}) - for _, config := range configs { - var value interface{} - if err := m.getConfig(config.Key, &value); err == nil { - result[config.Key] = value - } - } - - return result, nil -} - -// ValidateAllConfigs 验证所有配置 -func (m *LayeredConfigManager) ValidateAllConfigs() error { - // 验证各个模块 - if err := m.Base.Validate(); err != nil { - return fmt.Errorf("基础配置验证失败: %v", err) - } - if err := m.Database.Validate(); err != nil { - return fmt.Errorf("数据库配置验证失败: %v", err) - } - if err := m.Transfer.Validate(); err != nil { - return fmt.Errorf("传输配置验证失败: %v", err) - } - if err := m.Storage.Validate(); err != nil { - return fmt.Errorf("存储配置验证失败: %v", err) - } - if err := m.User.Validate(); err != nil { - return fmt.Errorf("用户配置验证失败: %v", err) - } - if err := m.MCP.Validate(); err != nil { - return fmt.Errorf("MCP配置验证失败: %v", err) - } - - return nil -} - -// ClearCache 清除缓存 -func (m *LayeredConfigManager) ClearCache() { - m.cacheMutex.Lock() - defer m.cacheMutex.Unlock() - - m.cache = make(map[string]interface{}) - m.cacheExpiry = make(map[string]time.Time) -} - -// GetCacheStats 获取缓存统计 -func (m *LayeredConfigManager) GetCacheStats() map[string]interface{} { - m.cacheMutex.RLock() - defer m.cacheMutex.RUnlock() - - stats := map[string]interface{}{ - "cache_size": len(m.cache), - "cache_entries": make([]string, 0, len(m.cache)), - } - - for key := range m.cache { - stats["cache_entries"] = append(stats["cache_entries"].([]string), key) - } - - return stats -} diff --git a/internal/config/manager.go b/internal/config/manager.go index 0cf75b1..d749d2b 100644 --- a/internal/config/manager.go +++ b/internal/config/manager.go @@ -11,44 +11,33 @@ import ( "gorm.io/gorm" ) -// ConfigManager implements Option A semantics (Env > YAML > DB > Defaults). -// Keys present in config.yaml are recorded in yamlManagedKeys and are authoritative. +// ConfigManager 配置管理器,实现 环境变量 > YAML > 默认值 的优先级语义 type ConfigManager struct { - Base *BaseConfig - Database *DatabaseConfig - Transfer *TransferConfig - Storage *StorageConfig - User *UserSystemConfig - MCP *MCPConfig - - NotifyTitle string - NotifyContent string - - // UI config grouped under `ui` in config.yaml. We keep top-level - // compatibility fields but prefer `UI` when loading/saving YAML. - UI *UIConfig `yaml:"ui" json:"ui"` - - // UI / Theme / Page top-level compatibility fields - ThemesSelect string `yaml:"themes_select" json:"themes_select"` - RobotsText string `yaml:"robots_text" json:"robots_text"` - PageExplain string `yaml:"page_explain" json:"page_explain"` - ShowAdminAddr int `yaml:"show_admin_addr" json:"show_admin_addr"` - Opacity float64 `yaml:"opacity" json:"opacity"` - Background string `yaml:"background" json:"background"` - - // Persistent runtime metadata - SysStart string `yaml:"sys_start" json:"sys_start"` - - // rate limit / business fields (kept at top-level for backwards compatibility) - UploadMinute int `yaml:"upload_minute" json:"upload_minute"` - UploadCount int `yaml:"upload_count" json:"upload_count"` - ErrorMinute int `yaml:"error_minute" json:"error_minute"` - ErrorCount int `yaml:"error_count" json:"error_count"` - ExpireStyle []string `yaml:"expire_style" json:"expire_style"` + // === 核心配置模块 === + Base *BaseConfig `json:"base" yaml:"base"` + Database *DatabaseConfig `json:"database" yaml:"database"` + Transfer *TransferConfig `json:"transfer" yaml:"transfer"` + Storage *StorageConfig `json:"storage" yaml:"storage"` + User *UserSystemConfig `json:"user" yaml:"user"` + MCP *MCPConfig `json:"mcp" yaml:"mcp"` + UI *UIConfig `json:"ui" yaml:"ui"` - db *gorm.DB + // === 通知配置 === + NotifyTitle string `json:"notify_title" yaml:"notify_title"` + NotifyContent string `json:"notify_content" yaml:"notify_content"` + + // === 运行时元数据 === + SysStart string `json:"sys_start" yaml:"sys_start"` + + // === 业务配置(保持顶层以兼容性) === + UploadMinute int `json:"upload_minute" yaml:"upload_minute"` + UploadCount int `json:"upload_count" yaml:"upload_count"` + ErrorMinute int `json:"error_minute" yaml:"error_minute"` + ErrorCount int `json:"error_count" yaml:"error_count"` + ExpireStyle []string `json:"expire_style" yaml:"expire_style"` - // ConfigManager now reads/writes the whole struct directly to YAML. + // === 内部状态 === + db *gorm.DB } func NewConfigManager() *ConfigManager { @@ -63,14 +52,18 @@ func NewConfigManager() *ConfigManager { } } -// InitManager loads config.yaml early if present and applies environment overrides. +// InitManager 初始化配置管理器,加载 YAML 配置并应用环境变量覆盖 func InitManager() *ConfigManager { cm := NewConfigManager() - if p := os.Getenv("CONFIG_PATH"); p != "" { - _ = cm.LoadFromYAML(p) + + // 尝试加载 YAML 配置文件 + if configPath := os.Getenv("CONFIG_PATH"); configPath != "" { + _ = cm.LoadFromYAML(configPath) } else if _, err := os.Stat("./config.yaml"); err == nil { _ = cm.LoadFromYAML("./config.yaml") } + + // 应用环境变量覆盖 cm.applyEnvironmentOverrides() return cm } @@ -78,16 +71,37 @@ func InitManager() *ConfigManager { func (cm *ConfigManager) SetDB(db *gorm.DB) { cm.db = db } func (cm *ConfigManager) GetDB() *gorm.DB { return cm.db } -// LoadFromYAML loads hierarchical YAML into module structs and records their flat keys. -func (cm *ConfigManager) LoadFromYAML(path string) error { - b, err := os.ReadFile(path) - if err != nil { - return err +// mergeUserConfig 合并用户配置,避免覆盖默认值 +func (cm *ConfigManager) mergeUserConfig(fileUser *UserSystemConfig) { + if fileUser == nil { + return } - var fileCfg ConfigManager - if err := yaml.Unmarshal(b, &fileCfg); err != nil { - return err + + if fileUser.AllowUserRegistration != 0 { + cm.User.AllowUserRegistration = fileUser.AllowUserRegistration + } + if fileUser.RequireEmailVerify != 0 { + cm.User.RequireEmailVerify = fileUser.RequireEmailVerify + } + if fileUser.UserUploadSize != 0 { + cm.User.UserUploadSize = fileUser.UserUploadSize + } + if fileUser.UserStorageQuota != 0 { + cm.User.UserStorageQuota = fileUser.UserStorageQuota + } + if fileUser.SessionExpiryHours != 0 { + cm.User.SessionExpiryHours = fileUser.SessionExpiryHours + } + if fileUser.MaxSessionsPerUser != 0 { + cm.User.MaxSessionsPerUser = fileUser.MaxSessionsPerUser + } + if strings.TrimSpace(fileUser.JWTSecret) != "" { + cm.User.JWTSecret = fileUser.JWTSecret } +} + +// mergeConfigModules 合并配置模块 +func (cm *ConfigManager) mergeConfigModules(fileCfg *ConfigManager) { if fileCfg.Base != nil { cm.Base = fileCfg.Base } @@ -100,212 +114,161 @@ func (cm *ConfigManager) LoadFromYAML(path string) error { if fileCfg.Storage != nil { cm.Storage = fileCfg.Storage } - if fileCfg.User != nil { - // Merge user config fields rather than clobbering defaults with an empty struct - if fileCfg.User.AllowUserRegistration != 0 { - cm.User.AllowUserRegistration = fileCfg.User.AllowUserRegistration - } - if fileCfg.User.RequireEmailVerify != 0 { - cm.User.RequireEmailVerify = fileCfg.User.RequireEmailVerify - } - if fileCfg.User.UserUploadSize != 0 { - cm.User.UserUploadSize = fileCfg.User.UserUploadSize - } - if fileCfg.User.UserStorageQuota != 0 { - cm.User.UserStorageQuota = fileCfg.User.UserStorageQuota - } - if fileCfg.User.SessionExpiryHours != 0 { - cm.User.SessionExpiryHours = fileCfg.User.SessionExpiryHours - } - if fileCfg.User.MaxSessionsPerUser != 0 { - cm.User.MaxSessionsPerUser = fileCfg.User.MaxSessionsPerUser - } - if strings.TrimSpace(fileCfg.User.JWTSecret) != "" { - cm.User.JWTSecret = fileCfg.User.JWTSecret - } - } if fileCfg.MCP != nil { cm.MCP = fileCfg.MCP } + if fileCfg.UI != nil { + cm.UI = fileCfg.UI + } +} + +// mergeSimpleFields 合并简单字段 +func (cm *ConfigManager) mergeSimpleFields(fileCfg *ConfigManager) { if fileCfg.NotifyTitle != "" { cm.NotifyTitle = fileCfg.NotifyTitle } if fileCfg.NotifyContent != "" { cm.NotifyContent = fileCfg.NotifyContent } - - // Prefer structured UI block if present; otherwise fallback to legacy top-level fields. - if fileCfg.UI != nil { - cm.UI = fileCfg.UI - // sync top-level compatibility fields - cm.ThemesSelect = cm.UI.ThemesSelect - cm.Background = cm.UI.Background - cm.PageExplain = cm.UI.PageExplain - cm.Opacity = cm.UI.Opacity - cm.RobotsText = cm.UI.RobotsText - cm.ShowAdminAddr = cm.UI.ShowAdminAddr - } else { - if fileCfg.ThemesSelect != "" { - cm.ThemesSelect = fileCfg.ThemesSelect - cm.UI.ThemesSelect = fileCfg.ThemesSelect - } - if fileCfg.RobotsText != "" { - cm.RobotsText = fileCfg.RobotsText - cm.UI.RobotsText = fileCfg.RobotsText - } - if fileCfg.PageExplain != "" { - cm.PageExplain = fileCfg.PageExplain - cm.UI.PageExplain = fileCfg.PageExplain - } - if fileCfg.ShowAdminAddr != 0 { - cm.ShowAdminAddr = fileCfg.ShowAdminAddr - cm.UI.ShowAdminAddr = fileCfg.ShowAdminAddr - } - if fileCfg.Opacity != 0 { - cm.Opacity = fileCfg.Opacity - cm.UI.Opacity = fileCfg.Opacity - } - if fileCfg.Background != "" { - cm.Background = fileCfg.Background - cm.UI.Background = fileCfg.Background - } - } - - // Persistent runtime metadata if fileCfg.SysStart != "" { cm.SysStart = fileCfg.SysStart } - // no runtime KeyValues persisted here anymore - // Backwards-compat: some configs place UI-related fields under a `ui` map - // (e.g. config.yaml uses `ui: { themes_select: themes/2025 }`). Parse the - // raw YAML and copy `ui.themes_select` into the top-level ThemesSelect when - // present so ServeAdminPage and static file routes resolve correctly. - var raw map[string]any - if err := yaml.Unmarshal(b, &raw); err == nil && raw != nil { - if uiRaw, ok := raw["ui"]; ok { - if uiMap, ok2 := uiRaw.(map[string]any); ok2 { - if ts, ok3 := uiMap["themes_select"].(string); ok3 && ts != "" { - cm.ThemesSelect = ts - } - if bg, ok3 := uiMap["background"].(string); ok3 && bg != "" { - cm.Background = bg - } - if pe, ok3 := uiMap["page_explain"].(string); ok3 && pe != "" { - cm.PageExplain = pe - } - if opacityVal, ok3 := uiMap["opacity"]; ok3 { - // Keep existing numeric parsing in callers; only set when simple types present - switch v := opacityVal.(type) { - case float64: - if v != 0 { - cm.Opacity = v - } - case int: - if float64(v) != 0 { - cm.Opacity = float64(v) - } - } - } - } - } +} + +// LoadFromYAML 从 YAML 文件加载配置 +func (cm *ConfigManager) LoadFromYAML(path string) error { + b, err := os.ReadFile(path) + if err != nil { + return err + } + + var fileCfg ConfigManager + if err := yaml.Unmarshal(b, &fileCfg); err != nil { + return err } + + // 按模块合并配置 + cm.mergeConfigModules(&fileCfg) + cm.mergeUserConfig(fileCfg.User) + cm.mergeSimpleFields(&fileCfg) + return nil } -// InitWithDB has been removed. Use SetDB(db) to inject a database connection. - +// ReloadConfig 重新加载配置(仅支持环境变量,保持端口不变) func (cm *ConfigManager) ReloadConfig() error { - // ReloadConfig no longer reads configuration from the database. - // Configuration should be provided via `config.yaml` and environment variables. - // Preserve in-memory immutable fields across reload (port). - curPort := cm.Base.Port + // 保存当前端口设置 + currentPort := cm.Base.Port + + // 重新应用环境变量覆盖 cm.applyEnvironmentOverrides() - cm.Base.Port = curPort + + // 恢复端口设置(端口在运行时不可变) + cm.Base.Port = currentPort return nil } -// PersistYAML writes the current ConfigManager to the YAML config file (CONFIG_PATH or ./config.yaml). -// It serializes the entire struct (yaml tags) and overwrites the file. +// PersistYAML 将当前配置保存到 YAML 文件 func (cm *ConfigManager) PersistYAML() error { - path := os.Getenv("CONFIG_PATH") - if path == "" { - path = "./config.yaml" + configPath := os.Getenv("CONFIG_PATH") + if configPath == "" { + configPath = "./config.yaml" } - // Ensure UI block reflects top-level compatibility fields before marshalling + // 确保 UI 配置存在 if cm.UI == nil { cm.UI = &UIConfig{} } - cm.UI.ThemesSelect = cm.ThemesSelect - cm.UI.Background = cm.Background - cm.UI.PageExplain = cm.PageExplain - cm.UI.Opacity = cm.Opacity - cm.UI.RobotsText = cm.RobotsText - cm.UI.ShowAdminAddr = cm.ShowAdminAddr - out, err := yaml.Marshal(cm) + data, err := yaml.Marshal(cm) if err != nil { return err } - return os.WriteFile(path, out, 0o644) -} -// Runtime key/value helpers removed: runtime arbitrary key storage is no longer -// persisted inside `ConfigManager`. Configuration should be updated via the -// structured module Update(...) methods and persisted with PersistYAML(). + return os.WriteFile(configPath, data, 0644) +} +// applyEnvironmentOverrides 应用环境变量覆盖配置 func (cm *ConfigManager) applyEnvironmentOverrides() { - if p := os.Getenv("PORT"); p != "" { - if n, err := strconv.Atoi(p); err == nil { + // 基础配置环境变量 + if port := os.Getenv("PORT"); port != "" { + if n, err := strconv.Atoi(port); err == nil { cm.Base.Port = n } } - if dp := os.Getenv("DATA_PATH"); dp != "" { - cm.Base.DataPath = dp + if dataPath := os.Getenv("DATA_PATH"); dataPath != "" { + cm.Base.DataPath = dataPath + } + + // MCP 配置环境变量 + if enableMCP := os.Getenv("ENABLE_MCP_SERVER"); enableMCP != "" { + if enableMCP == "true" || enableMCP == "1" { + cm.MCP.EnableMCPServer = 1 + } else { + cm.MCP.EnableMCPServer = 0 + } + } + if mcpPort := os.Getenv("MCP_PORT"); mcpPort != "" { + cm.MCP.MCPPort = mcpPort + } + if mcpHost := os.Getenv("MCP_HOST"); mcpHost != "" { + cm.MCP.MCPHost = mcpHost } } -// Save saves the configuration to the database (if db is set). -// Save persists configuration. Persisting to DB is intentionally removed; -// this method returns an error to surface that saving is unsupported. +// Save 保存配置(已废弃,请使用 config.yaml 和环境变量) func (cm *ConfigManager) Save() error { - return errors.New("saving configuration to database is not supported; use config.yaml and environment variables") + return errors.New("数据库配置保存已不支持,请使用 config.yaml 和环境变量") } -// Get helpers +// === 配置访问助手方法 === + func (cm *ConfigManager) GetAddress() string { return cm.Base.GetAddress() } func (cm *ConfigManager) GetDatabaseDSN() (string, error) { return cm.Database.GetDSN() } func (cm *ConfigManager) IsUserSystemEnabled() bool { return cm.User.IsUserSystemEnabled() } func (cm *ConfigManager) IsMCPEnabled() bool { return cm.MCP.IsMCPEnabled() } + +// Clone 创建配置管理器的深拷贝 func (cm *ConfigManager) Clone() *ConfigManager { - nc := NewConfigManager() - nc.Base = cm.Base.Clone() - nc.Database = cm.Database.Clone() - nc.Transfer = cm.Transfer.Clone() - nc.Storage = cm.Storage.Clone() - nc.User = cm.User.Clone() - nc.MCP = cm.MCP.Clone() - nc.NotifyTitle = cm.NotifyTitle - nc.NotifyContent = cm.NotifyContent + newManager := NewConfigManager() + + // 克隆配置模块 + newManager.Base = cm.Base.Clone() + newManager.Database = cm.Database.Clone() + newManager.Transfer = cm.Transfer.Clone() + newManager.Storage = cm.Storage.Clone() + newManager.User = cm.User.Clone() + newManager.MCP = cm.MCP.Clone() + + // 克隆简单字段 + newManager.NotifyTitle = cm.NotifyTitle + newManager.NotifyContent = cm.NotifyContent + newManager.SysStart = cm.SysStart + + // 克隆 UI 配置 if cm.UI != nil { ui := *cm.UI - nc.UI = &ui + newManager.UI = &ui } - nc.SysStart = cm.SysStart - // AdminToken removed - return nc + + return newManager } -// Validate validates all configuration modules. +// Validate 验证所有配置模块 func (cm *ConfigManager) Validate() error { - if cm.Base == nil || cm.Database == nil || cm.Transfer == nil || cm.Storage == nil || cm.User == nil || cm.MCP == nil { + // 检查配置模块是否初始化 + if cm.Base == nil || cm.Database == nil || cm.Transfer == nil || + cm.Storage == nil || cm.User == nil || cm.MCP == nil { return errors.New("配置模块未完全初始化") } + + // 验证关键配置模块 if err := cm.Base.Validate(); err != nil { return err } if err := cm.Database.Validate(); err != nil { return err } + return nil } diff --git a/internal/config/manager_test.go b/internal/config/manager_test.go index 0b4a379..9be60a8 100644 --- a/internal/config/manager_test.go +++ b/internal/config/manager_test.go @@ -10,10 +10,9 @@ import ( // TestLoadFromYAML ensures YAML fields are loaded and marked as yamlManaged func TestLoadFromYAML(t *testing.T) { data := map[string]interface{}{ - "base": map[string]interface{}{"name": "TCB", "port": 12345}, - "themes_select": "themes/test", - "page_explain": "test explain", - "notify_title": "nt", + "base": map[string]interface{}{"name": "TCB", "port": 12345}, + "ui": map[string]interface{}{"themes_select": "themes/test", "page_explain": "test explain"}, + "notify_title": "nt", } b, err := yaml.Marshal(data) if err != nil { @@ -32,8 +31,8 @@ func TestLoadFromYAML(t *testing.T) { if cm.Base == nil || cm.Base.Name != "TCB" { t.Fatalf("expected base.name TCB, got %#v", cm.Base) } - if cm.ThemesSelect != "themes/test" { - t.Fatalf("expected themes_select themes/test, got %s", cm.ThemesSelect) + if cm.UI == nil || cm.UI.ThemesSelect != "themes/test" { + t.Fatalf("expected ui.themes_select themes/test, got %#v", cm.UI) } // basic fields loaded } diff --git a/internal/config/mcp_config.go b/internal/config/mcp_config.go index 4ed16ac..f61dcf1 100644 --- a/internal/config/mcp_config.go +++ b/internal/config/mcp_config.go @@ -54,20 +54,6 @@ func (mc *MCPConfig) IsMCPEnabled() bool { } // Update 更新配置 -func (mc *MCPConfig) Update(updates map[string]interface{}) error { - if enableMCP, ok := updates["enable_mcp_server"].(int); ok { - mc.EnableMCPServer = enableMCP - } - if port, ok := updates["mcp_port"].(string); ok { - mc.MCPPort = port - } - if host, ok := updates["mcp_host"].(string); ok { - mc.MCPHost = host - } - - return mc.Validate() -} - // Clone 克隆配置 func (mc *MCPConfig) Clone() *MCPConfig { return &MCPConfig{ diff --git a/internal/config/storage_config.go b/internal/config/storage_config.go index 80d5158..05ff13a 100644 --- a/internal/config/storage_config.go +++ b/internal/config/storage_config.go @@ -3,7 +3,6 @@ package config import ( "fmt" - "strconv" "strings" ) @@ -278,268 +277,6 @@ func (sc *StorageConfig) IsNFS() bool { return sc.Type == "nfs" } -// ToMap S3配置转换为map格式 -func (s3c *S3Config) ToMap() map[string]string { - return map[string]string{ - "s3_access_key_id": s3c.AccessKeyID, - "s3_secret_access_key": s3c.SecretAccessKey, - "s3_bucket_name": s3c.BucketName, - "s3_endpoint_url": s3c.EndpointURL, - "s3_region_name": s3c.RegionName, - "s3_signature_version": s3c.SignatureVersion, - "s3_hostname": s3c.Hostname, - "s3_proxy": fmt.Sprintf("%d", s3c.Proxy), - "aws_session_token": s3c.SessionToken, - } -} - -// ToMap WebDAV配置转换为map格式 -func (wc *WebDAVConfig) ToMap() map[string]string { - return map[string]string{ - "webdav_hostname": wc.Hostname, - "webdav_root_path": wc.RootPath, - "webdav_proxy": fmt.Sprintf("%d", wc.Proxy), - "webdav_url": wc.URL, - "webdav_password": wc.Password, - "webdav_username": wc.Username, - } -} - -// ToMap OneDrive配置转换为map格式 -func (oc *OneDriveConfig) ToMap() map[string]string { - return map[string]string{ - "onedrive_domain": oc.Domain, - "onedrive_client_id": oc.ClientID, - "onedrive_username": oc.Username, - "onedrive_password": oc.Password, - "onedrive_root_path": oc.RootPath, - "onedrive_proxy": fmt.Sprintf("%d", oc.Proxy), - } -} - -// ToMap NFS配置转换为map格式 -func (nc *NFSConfig) ToMap() map[string]string { - return map[string]string{ - "nfs_server": nc.Server, - "nfs_path": nc.Path, - "nfs_mount_point": nc.MountPoint, - "nfs_version": nc.Version, - "nfs_options": nc.Options, - "nfs_timeout": fmt.Sprintf("%d", nc.Timeout), - "nfs_auto_mount": fmt.Sprintf("%d", nc.AutoMount), - "nfs_retry_count": fmt.Sprintf("%d", nc.RetryCount), - "nfs_sub_path": nc.SubPath, - } -} - -// ToMap 存储配置转换为map格式 -func (sc *StorageConfig) ToMap() map[string]string { - result := map[string]string{ - "file_storage": sc.Type, - "storage_path": sc.StoragePath, - } - - // 根据存储类型添加对应配置 - switch sc.Type { - case "s3": - if sc.S3 != nil { - for k, v := range sc.S3.ToMap() { - result[k] = v - } - } - case "webdav": - if sc.WebDAV != nil { - for k, v := range sc.WebDAV.ToMap() { - result[k] = v - } - } - case "onedrive": - if sc.OneDrive != nil { - for k, v := range sc.OneDrive.ToMap() { - result[k] = v - } - } - case "nfs": - if sc.NFS != nil { - for k, v := range sc.NFS.ToMap() { - result[k] = v - } - } - } - - return result -} - -// FromMap 从map加载S3配置 -func (s3c *S3Config) FromMap(data map[string]string) error { - if val, ok := data["s3_access_key_id"]; ok { - s3c.AccessKeyID = val - } - if val, ok := data["s3_secret_access_key"]; ok { - s3c.SecretAccessKey = val - } - if val, ok := data["s3_bucket_name"]; ok { - s3c.BucketName = val - } - if val, ok := data["s3_endpoint_url"]; ok { - s3c.EndpointURL = val - } - if val, ok := data["s3_region_name"]; ok { - s3c.RegionName = val - } - if val, ok := data["s3_signature_version"]; ok { - s3c.SignatureVersion = val - } - if val, ok := data["s3_hostname"]; ok { - s3c.Hostname = val - } - if val, ok := data["s3_proxy"]; ok { - if proxy, err := strconv.Atoi(val); err == nil { - s3c.Proxy = proxy - } - } - if val, ok := data["aws_session_token"]; ok { - s3c.SessionToken = val - } - - return nil -} - -// FromMap 从map加载WebDAV配置 -func (wc *WebDAVConfig) FromMap(data map[string]string) error { - if val, ok := data["webdav_hostname"]; ok { - wc.Hostname = val - } - if val, ok := data["webdav_root_path"]; ok { - wc.RootPath = val - } - if val, ok := data["webdav_proxy"]; ok { - if proxy, err := strconv.Atoi(val); err == nil { - wc.Proxy = proxy - } - } - if val, ok := data["webdav_url"]; ok { - wc.URL = val - } - if val, ok := data["webdav_password"]; ok { - wc.Password = val - } - if val, ok := data["webdav_username"]; ok { - wc.Username = val - } - - return nil -} - -// FromMap 从map加载OneDrive配置 -func (oc *OneDriveConfig) FromMap(data map[string]string) error { - if val, ok := data["onedrive_domain"]; ok { - oc.Domain = val - } - if val, ok := data["onedrive_client_id"]; ok { - oc.ClientID = val - } - if val, ok := data["onedrive_username"]; ok { - oc.Username = val - } - if val, ok := data["onedrive_password"]; ok { - oc.Password = val - } - if val, ok := data["onedrive_root_path"]; ok { - oc.RootPath = val - } - if val, ok := data["onedrive_proxy"]; ok { - if proxy, err := strconv.Atoi(val); err == nil { - oc.Proxy = proxy - } - } - - return nil -} - -// FromMap 从map加载NFS配置 -func (nc *NFSConfig) FromMap(data map[string]string) error { - if val, ok := data["nfs_server"]; ok { - nc.Server = val - } - if val, ok := data["nfs_path"]; ok { - nc.Path = val - } - if val, ok := data["nfs_mount_point"]; ok { - nc.MountPoint = val - } - if val, ok := data["nfs_version"]; ok { - nc.Version = val - } - if val, ok := data["nfs_options"]; ok { - nc.Options = val - } - if val, ok := data["nfs_timeout"]; ok { - if timeout, err := strconv.Atoi(val); err == nil { - nc.Timeout = timeout - } - } - if val, ok := data["nfs_auto_mount"]; ok { - if autoMount, err := strconv.Atoi(val); err == nil { - nc.AutoMount = autoMount - } - } - if val, ok := data["nfs_retry_count"]; ok { - if retryCount, err := strconv.Atoi(val); err == nil { - nc.RetryCount = retryCount - } - } - if val, ok := data["nfs_sub_path"]; ok { - nc.SubPath = val - } - - return nil -} - -// FromMap 从map加载存储配置 -func (sc *StorageConfig) FromMap(data map[string]string) error { - if val, ok := data["file_storage"]; ok { - sc.Type = val - } - if val, ok := data["storage_path"]; ok { - sc.StoragePath = val - } - - // 根据存储类型加载对应配置 - switch sc.Type { - case "s3": - if sc.S3 == nil { - sc.S3 = NewS3Config() - } - if err := sc.S3.FromMap(data); err != nil { - return fmt.Errorf("failed to parse S3 config: %w", err) - } - case "webdav": - if sc.WebDAV == nil { - sc.WebDAV = NewWebDAVConfig() - } - if err := sc.WebDAV.FromMap(data); err != nil { - return fmt.Errorf("failed to parse WebDAV config: %w", err) - } - case "onedrive": - if sc.OneDrive == nil { - sc.OneDrive = NewOneDriveConfig() - } - if err := sc.OneDrive.FromMap(data); err != nil { - return fmt.Errorf("failed to parse OneDrive config: %w", err) - } - case "nfs": - if sc.NFS == nil { - sc.NFS = NewNFSConfig() - } - if err := sc.NFS.FromMap(data); err != nil { - return fmt.Errorf("failed to parse NFS config: %w", err) - } - } - - return sc.Validate() -} - // Clone 克隆S3配置 func (s3c *S3Config) Clone() *S3Config { return &S3Config{ diff --git a/internal/config/storage_strategy.go b/internal/config/storage_strategy.go deleted file mode 100644 index 8efe2ac..0000000 --- a/internal/config/storage_strategy.go +++ /dev/null @@ -1,386 +0,0 @@ -// Package config 配置存储策略 -package config - -import ( - "encoding/json" - "fmt" - "time" - - "gorm.io/gorm" -) - -// ConfigStorageType 配置存储类型 -type ConfigStorageType string - -const ( - StorageTypeFile ConfigStorageType = "file" // 配置文件 - StorageTypeDatabase ConfigStorageType = "database" // 数据库表 - StorageTypeJSON ConfigStorageType = "json" // JSON配置 -) - -// ConfigCategory 配置分类 -type ConfigCategory string - -const ( - CategoryStatic ConfigCategory = "static" // 静态配置(文件) - CategoryRuntime ConfigCategory = "runtime" // 运行时配置(数据库) - CategorySystem ConfigCategory = "system" // 系统配置(专用表) - CategoryBusiness ConfigCategory = "business" // 业务配置(业务表) -) - -// ConfigMetadata 配置元数据 -type ConfigMetadata struct { - Key string `json:"key"` - Category ConfigCategory `json:"category"` - StorageType ConfigStorageType `json:"storage_type"` - TableName string `json:"table_name,omitempty"` - FileName string `json:"file_name,omitempty"` - Description string `json:"description"` - Volatile bool `json:"volatile"` // 是否易变 - Cacheable bool `json:"cacheable"` // 是否可缓存 - Version int `json:"version"` // 配置版本 - LastModified time.Time `json:"last_modified"` -} - -// StaticConfig 静态配置(存储在文件中) -type StaticConfig struct { - ID uint `gorm:"primaryKey" json:"id"` - ConfigKey string `gorm:"uniqueIndex;size:100" json:"config_key"` - ConfigValue string `gorm:"type:text" json:"config_value"` - Category string `gorm:"size:50;index" json:"category"` - Description string `gorm:"size:255" json:"description"` - IsActive bool `gorm:"default:true" json:"is_active"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// RuntimeConfig 运行时配置(存储在数据库中) -type RuntimeConfig struct { - ID uint `gorm:"primaryKey" json:"id"` - ConfigKey string `gorm:"uniqueIndex;size:100" json:"config_key"` - ConfigValue string `gorm:"type:text" json:"config_value"` - DataType string `gorm:"size:20" json:"data_type"` // string, int, bool, json - Category string `gorm:"size:50;index" json:"category"` - UserID *uint `gorm:"index" json:"user_id,omitempty"` // 用户专属配置 - IsGlobal bool `gorm:"default:true" json:"is_global"` - Priority int `gorm:"default:0" json:"priority"` // 优先级 - ExpiresAt *time.Time `json:"expires_at,omitempty"` // 过期时间 - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// SystemConfig 系统配置(存储配置、传输配置等) -type SystemConfig struct { - ID uint `gorm:"primaryKey" json:"id"` - ConfigKey string `gorm:"uniqueIndex;size:100" json:"config_key"` - ConfigValue string `gorm:"type:json" json:"config_value"` - ConfigSchema string `gorm:"type:text" json:"config_schema"` // JSON Schema - Version int `gorm:"default:1" json:"version"` - Environment string `gorm:"size:20;default:'production'" json:"environment"` - IsEncrypted bool `gorm:"default:false" json:"is_encrypted"` - Checksum string `gorm:"size:64" json:"checksum"` // 配置校验和 - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// BusinessConfig 业务配置(通知、限流等) -type BusinessConfig struct { - ID uint `gorm:"primaryKey" json:"id"` - ConfigKey string `gorm:"uniqueIndex;size:100" json:"config_key"` - ConfigValue string `gorm:"type:json" json:"config_value"` - Module string `gorm:"size:50;index" json:"module"` // 模块名称 - SubModule string `gorm:"size:50;index" json:"sub_module,omitempty"` - IsEnabled bool `gorm:"default:true" json:"is_enabled"` - ValidFrom time.Time `json:"valid_from"` - ValidTo *time.Time `json:"valid_to,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// ConfigStorageStrategy 配置存储策略 -type ConfigStorageStrategy struct { - db *gorm.DB - configMapping map[string]ConfigMetadata -} - -// NewConfigStorageStrategy 创建配置存储策略 -func NewConfigStorageStrategy(db *gorm.DB) *ConfigStorageStrategy { - strategy := &ConfigStorageStrategy{ - db: db, - configMapping: make(map[string]ConfigMetadata), - } - - strategy.initConfigMapping() - return strategy -} - -// initConfigMapping 初始化配置映射 -func (s *ConfigStorageStrategy) initConfigMapping() { - // 静态配置(基础设置、主题) - s.configMapping["base.name"] = ConfigMetadata{ - Key: "base.name", - Category: CategoryStatic, - StorageType: StorageTypeFile, - FileName: "static_config.json", - Description: "应用名称", - Cacheable: true, - } - - s.configMapping["base.description"] = ConfigMetadata{ - Key: "base.description", - Category: CategoryStatic, - StorageType: StorageTypeFile, - FileName: "static_config.json", - Description: "应用描述", - Cacheable: true, - } - - s.configMapping["theme.select"] = ConfigMetadata{ - Key: "theme.select", - Category: CategoryStatic, - StorageType: StorageTypeFile, - FileName: "theme_config.json", - Description: "主题选择", - Cacheable: true, - } - - // 运行时配置(用户设置) - s.configMapping["user.system_enabled"] = ConfigMetadata{ - Key: "user.system_enabled", - Category: CategoryRuntime, - StorageType: StorageTypeDatabase, - TableName: "runtime_configs", - Description: "用户系统启用状态", - Volatile: true, - Cacheable: true, - } - - s.configMapping["mcp.enabled"] = ConfigMetadata{ - Key: "mcp.enabled", - Category: CategoryRuntime, - StorageType: StorageTypeDatabase, - TableName: "runtime_configs", - Description: "MCP服务器启用状态", - Volatile: true, - Cacheable: false, // MCP配置变化需要立即生效 - } - - // 系统配置(存储、传输) - s.configMapping["storage.config"] = ConfigMetadata{ - Key: "storage.config", - Category: CategorySystem, - StorageType: StorageTypeJSON, - TableName: "system_configs", - Description: "存储配置", - Volatile: false, - Cacheable: true, - } - - s.configMapping["transfer.config"] = ConfigMetadata{ - Key: "transfer.config", - Category: CategorySystem, - StorageType: StorageTypeJSON, - TableName: "system_configs", - Description: "传输配置", - Volatile: false, - Cacheable: true, - } - - s.configMapping["database.config"] = ConfigMetadata{ - Key: "database.config", - Category: CategorySystem, - StorageType: StorageTypeJSON, - TableName: "system_configs", - Description: "数据库配置", - Volatile: false, - Cacheable: true, - } - - // 业务配置(通知、限流) - s.configMapping["notification.config"] = ConfigMetadata{ - Key: "notification.config", - Category: CategoryBusiness, - StorageType: StorageTypeJSON, - TableName: "business_configs", - Description: "通知配置", - Volatile: true, - Cacheable: true, - } - - s.configMapping["ratelimit.config"] = ConfigMetadata{ - Key: "ratelimit.config", - Category: CategoryBusiness, - StorageType: StorageTypeJSON, - TableName: "business_configs", - Description: "限流配置", - Volatile: true, - Cacheable: true, - } -} - -// GetConfig 获取配置 -func (s *ConfigStorageStrategy) GetConfig(key string, result interface{}) error { - metadata, exists := s.configMapping[key] - if !exists { - return fmt.Errorf("配置键 %s 不存在", key) - } - - switch metadata.StorageType { - case StorageTypeFile: - return s.getFileConfig(key, metadata, result) - case StorageTypeDatabase: - return s.getDatabaseConfig(key, metadata, result) - case StorageTypeJSON: - return s.getJSONConfig(key, metadata, result) - default: - return fmt.Errorf("不支持的存储类型: %s", metadata.StorageType) - } -} - -// SetConfig 设置配置 -func (s *ConfigStorageStrategy) SetConfig(key string, value interface{}) error { - metadata, exists := s.configMapping[key] - if !exists { - return fmt.Errorf("配置键 %s 不存在", key) - } - - switch metadata.StorageType { - case StorageTypeFile: - return s.setFileConfig(key, metadata, value) - case StorageTypeDatabase: - return s.setDatabaseConfig(key, metadata, value) - case StorageTypeJSON: - return s.setJSONConfig(key, metadata, value) - default: - return fmt.Errorf("不支持的存储类型: %s", metadata.StorageType) - } -} - -// 文件配置操作 -func (s *ConfigStorageStrategy) getFileConfig(key string, metadata ConfigMetadata, result interface{}) error { - // 从静态配置表获取 - var staticConfig StaticConfig - err := s.db.Where("config_key = ? AND is_active = ?", key, true).First(&staticConfig).Error - if err != nil { - return err - } - - return json.Unmarshal([]byte(staticConfig.ConfigValue), result) -} - -func (s *ConfigStorageStrategy) setFileConfig(key string, metadata ConfigMetadata, value interface{}) error { - valueBytes, err := json.Marshal(value) - if err != nil { - return err - } - - staticConfig := StaticConfig{ - ConfigKey: key, - ConfigValue: string(valueBytes), - Category: string(metadata.Category), - Description: metadata.Description, - IsActive: true, - } - - return s.db.Save(&staticConfig).Error -} - -// 数据库配置操作 -func (s *ConfigStorageStrategy) getDatabaseConfig(key string, metadata ConfigMetadata, result interface{}) error { - var runtimeConfig RuntimeConfig - err := s.db.Where("config_key = ? AND is_global = ?", key, true).First(&runtimeConfig).Error - if err != nil { - return err - } - - return json.Unmarshal([]byte(runtimeConfig.ConfigValue), result) -} - -func (s *ConfigStorageStrategy) setDatabaseConfig(key string, metadata ConfigMetadata, value interface{}) error { - valueBytes, err := json.Marshal(value) - if err != nil { - return err - } - - runtimeConfig := RuntimeConfig{ - ConfigKey: key, - ConfigValue: string(valueBytes), - DataType: "json", - Category: string(metadata.Category), - IsGlobal: true, - Priority: 0, - } - - return s.db.Save(&runtimeConfig).Error -} - -// JSON配置操作 -func (s *ConfigStorageStrategy) getJSONConfig(key string, metadata ConfigMetadata, result interface{}) error { - var systemConfig SystemConfig - err := s.db.Where("config_key = ?", key).First(&systemConfig).Error - if err != nil { - return err - } - - return json.Unmarshal([]byte(systemConfig.ConfigValue), result) -} - -func (s *ConfigStorageStrategy) setJSONConfig(key string, metadata ConfigMetadata, value interface{}) error { - valueBytes, err := json.Marshal(value) - if err != nil { - return err - } - - systemConfig := SystemConfig{ - ConfigKey: key, - ConfigValue: string(valueBytes), - Version: 1, - Environment: "production", - } - - return s.db.Save(&systemConfig).Error -} - -// InitTables 初始化配置相关表 -func (s *ConfigStorageStrategy) InitTables() error { - // 自动迁移所有配置表 - return s.db.AutoMigrate( - &StaticConfig{}, - &RuntimeConfig{}, - &SystemConfig{}, - &BusinessConfig{}, - ) -} - -// GetConfigMetadata 获取配置元数据 -func (s *ConfigStorageStrategy) GetConfigMetadata(key string) (ConfigMetadata, error) { - metadata, exists := s.configMapping[key] - if !exists { - return ConfigMetadata{}, fmt.Errorf("配置键 %s 不存在", key) - } - return metadata, nil -} - -// ListConfigsByCategory 按分类列出配置 -func (s *ConfigStorageStrategy) ListConfigsByCategory(category ConfigCategory) ([]ConfigMetadata, error) { - var result []ConfigMetadata - for _, metadata := range s.configMapping { - if metadata.Category == category { - result = append(result, metadata) - } - } - return result, nil -} - -// ValidateConfig 验证配置 -func (s *ConfigStorageStrategy) ValidateConfig(key string, value interface{}) error { - _, exists := s.configMapping[key] - if !exists { - return fmt.Errorf("配置键 %s 不存在", key) - } - - // 这里可以添加具体的验证逻辑 - // 比如检查JSON Schema、数据类型等 - - return nil -} diff --git a/internal/config/transfer_config.go b/internal/config/transfer_config.go index dc7610b..ffdad60 100644 --- a/internal/config/transfer_config.go +++ b/internal/config/transfer_config.go @@ -3,7 +3,6 @@ package config import ( "fmt" - "strconv" "strings" ) @@ -167,147 +166,6 @@ func (dc *DownloadConfig) GetDownloadTimeoutMinutes() float64 { return float64(dc.DownloadTimeout) / 60 } -// ToMap 上传配置转换为map格式 -func (uc *UploadConfig) ToMap() map[string]string { - return map[string]string{ - "open_upload": fmt.Sprintf("%d", uc.OpenUpload), - "upload_size": fmt.Sprintf("%d", uc.UploadSize), - "enable_chunk": fmt.Sprintf("%d", uc.EnableChunk), - "chunk_size": fmt.Sprintf("%d", uc.ChunkSize), - "max_save_seconds": fmt.Sprintf("%d", uc.MaxSaveSeconds), - } -} - -// ToMap 下载配置转换为map格式 -func (dc *DownloadConfig) ToMap() map[string]string { - return map[string]string{ - "enable_concurrent_download": fmt.Sprintf("%d", dc.EnableConcurrentDownload), - "max_concurrent_downloads": fmt.Sprintf("%d", dc.MaxConcurrentDownloads), - "download_timeout": fmt.Sprintf("%d", dc.DownloadTimeout), - } -} - -// ToMap 传输配置转换为map格式 -func (tc *TransferConfig) ToMap() map[string]string { - result := make(map[string]string) - - // 合并上传配置 - for k, v := range tc.Upload.ToMap() { - result[k] = v - } - - // 合并下载配置 - for k, v := range tc.Download.ToMap() { - result[k] = v - } - - return result -} - -// FromMap 从map加载上传配置 -func (uc *UploadConfig) FromMap(data map[string]string) error { - if val, ok := data["open_upload"]; ok { - if v, err := strconv.Atoi(val); err == nil { - uc.OpenUpload = v - } - } - if val, ok := data["upload_size"]; ok { - if v, err := strconv.ParseInt(val, 10, 64); err == nil { - uc.UploadSize = v - } - } - if val, ok := data["enable_chunk"]; ok { - if v, err := strconv.Atoi(val); err == nil { - uc.EnableChunk = v - } - } - if val, ok := data["chunk_size"]; ok { - if v, err := strconv.ParseInt(val, 10, 64); err == nil { - uc.ChunkSize = v - } - } - if val, ok := data["max_save_seconds"]; ok { - if v, err := strconv.Atoi(val); err == nil { - uc.MaxSaveSeconds = v - } - } - - return uc.Validate() -} - -// FromMap 从map加载下载配置 -func (dc *DownloadConfig) FromMap(data map[string]string) error { - if val, ok := data["enable_concurrent_download"]; ok { - if v, err := strconv.Atoi(val); err == nil { - dc.EnableConcurrentDownload = v - } - } - if val, ok := data["max_concurrent_downloads"]; ok { - if v, err := strconv.Atoi(val); err == nil { - dc.MaxConcurrentDownloads = v - } - } - if val, ok := data["download_timeout"]; ok { - if v, err := strconv.Atoi(val); err == nil { - dc.DownloadTimeout = v - } - } - - return dc.Validate() -} - -// FromMap 从map加载传输配置 -func (tc *TransferConfig) FromMap(data map[string]string) error { - if err := tc.Upload.FromMap(data); err != nil { - return err - } - return tc.Download.FromMap(data) -} - -// Update 更新上传配置 -func (uc *UploadConfig) Update(updates map[string]interface{}) error { - if openUpload, ok := updates["open_upload"].(int); ok { - uc.OpenUpload = openUpload - } - if uploadSize, ok := updates["upload_size"].(int64); ok { - uc.UploadSize = uploadSize - } - if enableChunk, ok := updates["enable_chunk"].(int); ok { - uc.EnableChunk = enableChunk - } - if chunkSize, ok := updates["chunk_size"].(int64); ok { - uc.ChunkSize = chunkSize - } - if maxSaveSeconds, ok := updates["max_save_seconds"].(int); ok { - uc.MaxSaveSeconds = maxSaveSeconds - } - - return uc.Validate() -} - -// Update 更新下载配置 -func (dc *DownloadConfig) Update(updates map[string]interface{}) error { - if enableConcurrent, ok := updates["enable_concurrent_download"].(int); ok { - dc.EnableConcurrentDownload = enableConcurrent - } - if maxConcurrent, ok := updates["max_concurrent_downloads"].(int); ok { - dc.MaxConcurrentDownloads = maxConcurrent - } - if timeout, ok := updates["download_timeout"].(int); ok { - dc.DownloadTimeout = timeout - } - - return dc.Validate() -} - -// Update 更新传输配置 -func (tc *TransferConfig) Update(updates map[string]interface{}) error { - if err := tc.Upload.Update(updates); err != nil { - return err - } - return tc.Download.Update(updates) -} - // Clone 克隆上传配置 func (uc *UploadConfig) Clone() *UploadConfig { return &UploadConfig{ diff --git a/internal/config/ui_config.go b/internal/config/ui_config.go index f4ac41e..60555f8 100644 --- a/internal/config/ui_config.go +++ b/internal/config/ui_config.go @@ -9,46 +9,3 @@ type UIConfig struct { ShowAdminAddr int `yaml:"show_admin_addr" json:"show_admin_addr"` Opacity float64 `yaml:"opacity" json:"opacity"` } - -func NewUIConfig() *UIConfig { - return &UIConfig{} -} - -func (u *UIConfig) Clone() *UIConfig { - if u == nil { - return NewUIConfig() - } - nu := *u - return &nu -} - -// Update applies values from a map to the UIConfig. It supports values typed as -// simple primitives (string/number). -func (u *UIConfig) Update(m map[string]interface{}) error { - if u == nil { - return nil - } - if v, ok := m["themes_select"].(string); ok { - u.ThemesSelect = v - } - if v, ok := m["background"].(string); ok { - u.Background = v - } - if v, ok := m["page_explain"].(string); ok { - u.PageExplain = v - } - if v, ok := m["robots_text"].(string); ok { - u.RobotsText = v - } - if v, ok := m["show_admin_addr"].(int); ok { - u.ShowAdminAddr = v - } else if v2, ok2 := m["show_admin_addr"].(float64); ok2 { - u.ShowAdminAddr = int(v2) - } - if v, ok := m["opacity"].(float64); ok { - u.Opacity = v - } else if v2, ok2 := m["opacity"].(int); ok2 { - u.Opacity = float64(v2) - } - return nil -} diff --git a/internal/config/user_config.go b/internal/config/user_config.go index d0f61fc..16f7b6f 100644 --- a/internal/config/user_config.go +++ b/internal/config/user_config.go @@ -3,7 +3,6 @@ package config import ( "fmt" - "strconv" "strings" "time" ) @@ -126,85 +125,6 @@ func (usc *UserSystemConfig) IsStorageQuotaUnlimited() bool { return usc.UserStorageQuota == 0 } -// ToMap 转换为map格式 -func (usc *UserSystemConfig) ToMap() map[string]string { - return map[string]string{ - "allow_user_registration": fmt.Sprintf("%d", usc.AllowUserRegistration), - "require_email_verify": fmt.Sprintf("%d", usc.RequireEmailVerify), - "user_upload_size": fmt.Sprintf("%d", usc.UserUploadSize), - "user_storage_quota": fmt.Sprintf("%d", usc.UserStorageQuota), - "session_expiry_hours": fmt.Sprintf("%d", usc.SessionExpiryHours), - "max_sessions_per_user": fmt.Sprintf("%d", usc.MaxSessionsPerUser), - "jwt_secret": usc.JWTSecret, - } -} - -// FromMap 从map加载配置 -func (usc *UserSystemConfig) FromMap(data map[string]string) error { - if val, ok := data["allow_user_registration"]; ok { - if v, err := strconv.Atoi(val); err == nil { - usc.AllowUserRegistration = v - } - } - if val, ok := data["require_email_verify"]; ok { - if v, err := strconv.Atoi(val); err == nil { - usc.RequireEmailVerify = v - } - } - if val, ok := data["user_upload_size"]; ok { - if v, err := strconv.ParseInt(val, 10, 64); err == nil { - usc.UserUploadSize = v - } - } - if val, ok := data["user_storage_quota"]; ok { - if v, err := strconv.ParseInt(val, 10, 64); err == nil { - usc.UserStorageQuota = v - } - } - if val, ok := data["session_expiry_hours"]; ok { - if v, err := strconv.Atoi(val); err == nil { - usc.SessionExpiryHours = v - } - } - if val, ok := data["max_sessions_per_user"]; ok { - if v, err := strconv.Atoi(val); err == nil { - usc.MaxSessionsPerUser = v - } - } - if val, ok := data["jwt_secret"]; ok { - usc.JWTSecret = val - } - - return usc.Validate() -} - -// Update 更新配置 -func (usc *UserSystemConfig) Update(updates map[string]interface{}) error { - if allowRegistration, ok := updates["allow_user_registration"].(int); ok { - usc.AllowUserRegistration = allowRegistration - } - if requireEmailVerify, ok := updates["require_email_verify"].(int); ok { - usc.RequireEmailVerify = requireEmailVerify - } - if userUploadSize, ok := updates["user_upload_size"].(int64); ok { - usc.UserUploadSize = userUploadSize - } - if userStorageQuota, ok := updates["user_storage_quota"].(int64); ok { - usc.UserStorageQuota = userStorageQuota - } - if sessionExpiryHours, ok := updates["session_expiry_hours"].(int); ok { - usc.SessionExpiryHours = sessionExpiryHours - } - if maxSessionsPerUser, ok := updates["max_sessions_per_user"].(int); ok { - usc.MaxSessionsPerUser = maxSessionsPerUser - } - if jwtSecret, ok := updates["jwt_secret"].(string); ok { - usc.JWTSecret = jwtSecret - } - - return usc.Validate() -} - // Clone 克隆配置 func (usc *UserSystemConfig) Clone() *UserSystemConfig { return &UserSystemConfig{ diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go index e83067a..19e01bf 100644 --- a/internal/handlers/admin.go +++ b/internal/handlers/admin.go @@ -12,6 +12,7 @@ import ( "github.com/zy84338719/filecodebox/internal/models" "github.com/zy84338719/filecodebox/internal/models/web" "github.com/zy84338719/filecodebox/internal/services" + "github.com/zy84338719/filecodebox/internal/utils" "github.com/gin-gonic/gin" ) @@ -32,8 +33,7 @@ func NewAdminHandler(service *services.AdminService, config *config.ConfigManage // Login 管理员登录 func (h *AdminHandler) Login(c *gin.Context) { var req web.AdminLoginRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &req) { return } @@ -77,27 +77,15 @@ func (h *AdminHandler) GetStats(c *gin.Context) { // GetFiles 获取文件列表 func (h *AdminHandler) GetFiles(c *gin.Context) { - pageStr := c.DefaultQuery("page", "1") - pageSizeStr := c.DefaultQuery("page_size", "20") - search := c.Query("search") - - page, err := strconv.Atoi(pageStr) - if err != nil || page < 1 { - page = 1 - } - - pageSize, err := strconv.Atoi(pageSizeStr) - if err != nil || pageSize < 1 || pageSize > 100 { - pageSize = 20 - } + pagination := utils.ParsePaginationParams(c) - files, total, err := h.service.GetFiles(page, pageSize, search) + files, total, err := h.service.GetFiles(pagination.Page, pagination.PageSize, pagination.Search) if err != nil { common.InternalServerErrorResponse(c, "获取文件列表失败: "+err.Error()) return } - common.SuccessWithPagination(c, files, int(total), page, pageSize) + common.SuccessWithPagination(c, files, int(total), pagination.Page, pagination.PageSize) } // DeleteFile 删除文件 @@ -136,8 +124,20 @@ func (h *AdminHandler) GetFile(c *gin.Context) { // GetConfig 获取配置 func (h *AdminHandler) GetConfig(c *gin.Context) { - config := h.service.GetFullConfig() - common.SuccessResponse(c, config) + cfg := h.service.GetFullConfig() + resp := web.AdminConfigResponse{ + AdminConfigRequest: web.AdminConfigRequest{ + Base: cfg.Base, + Database: cfg.Database, + Transfer: cfg.Transfer, + Storage: cfg.Storage, + User: cfg.User, + MCP: cfg.MCP, + UI: cfg.UI, + SysStart: &cfg.SysStart, + }, + } + common.SuccessResponse(c, resp) } // UpdateConfig 更新配置 @@ -570,14 +570,12 @@ func (h *AdminHandler) getUserStats() (*web.AdminUserStatsResponse, error) { // GetUser 获取单个用户 func (h *AdminHandler) GetUser(c *gin.Context) { - userIDStr := c.Param("id") - userID64, err := strconv.ParseUint(userIDStr, 10, 32) - if err != nil { - common.BadRequestResponse(c, "用户ID错误") + userID, ok := utils.ParseUserIDFromParam(c, "id") + if !ok { return } - user, err := h.service.GetUserByID(uint(userID64)) + user, err := h.service.GetUserByID(userID) if err != nil { common.NotFoundResponse(c, "用户不存在") return @@ -612,8 +610,7 @@ func (h *AdminHandler) GetUser(c *gin.Context) { // CreateUser 创建用户 func (h *AdminHandler) CreateUser(c *gin.Context) { var userData web.UserDataRequest - if err := c.ShouldBindJSON(&userData); err != nil { - common.BadRequestResponse(c, "参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &userData) { return } @@ -642,10 +639,8 @@ func (h *AdminHandler) CreateUser(c *gin.Context) { // UpdateUser 更新用户 func (h *AdminHandler) UpdateUser(c *gin.Context) { - userIDStr := c.Param("id") - userID64, err := strconv.ParseUint(userIDStr, 10, 32) - if err != nil { - common.BadRequestResponse(c, "用户ID错误") + userID, ok := utils.ParseUserIDFromParam(c, "id") + if !ok { return } @@ -657,8 +652,7 @@ func (h *AdminHandler) UpdateUser(c *gin.Context) { IsActive bool `json:"is_active"` } - if err := c.ShouldBindJSON(&userData); err != nil { - common.BadRequestResponse(c, "参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &userData) { return } @@ -687,14 +681,14 @@ func (h *AdminHandler) UpdateUser(c *gin.Context) { user.Role = role user.Status = status - err = h.service.UpdateUser(user) + err := h.service.UpdateUser(user) if err != nil { common.InternalServerErrorResponse(c, "更新用户失败: "+err.Error()) return } common.SuccessWithMessage(c, "用户更新成功", web.IDResponse{ - ID: uint(userID64), + ID: userID, }) } @@ -939,13 +933,7 @@ func (h *AdminHandler) BatchDeleteUsers(c *gin.Context) { // GetMCPConfig 获取 MCP 配置 func (h *AdminHandler) GetMCPConfig(c *gin.Context) { - mcpConfig := map[string]interface{}{ - "enable_mcp_server": h.config.MCP.EnableMCPServer, - "mcp_port": h.config.MCP.MCPPort, - "mcp_host": h.config.MCP.MCPHost, - } - - common.SuccessResponse(c, mcpConfig) + common.SuccessResponse(c, h.config.MCP) } // UpdateMCPConfig 更新 MCP 配置 @@ -961,23 +949,21 @@ func (h *AdminHandler) UpdateMCPConfig(c *gin.Context) { return } - // 构建配置更新映射 - configUpdates := make(map[string]interface{}) - + // 直接更新配置结构 if mcpConfig.EnableMCPServer != nil { - configUpdates["enable_mcp_server"] = *mcpConfig.EnableMCPServer + h.config.MCP.EnableMCPServer = *mcpConfig.EnableMCPServer } if mcpConfig.MCPPort != nil { - configUpdates["mcp_port"] = *mcpConfig.MCPPort + h.config.MCP.MCPPort = *mcpConfig.MCPPort } if mcpConfig.MCPHost != nil { - configUpdates["mcp_host"] = *mcpConfig.MCPHost + h.config.MCP.MCPHost = *mcpConfig.MCPHost } - // 更新配置 - err := h.service.UpdateConfig(configUpdates) + // 保存配置 + err := h.config.Save() if err != nil { - common.InternalServerErrorResponse(c, "更新MCP配置失败: "+err.Error()) + common.InternalServerErrorResponse(c, "保存MCP配置失败: "+err.Error()) return } @@ -1013,13 +999,18 @@ func (h *AdminHandler) GetMCPStatus(c *gin.Context) { } status := mcpManager.GetStatus() - status["config"] = map[string]interface{}{ - "enabled": h.config.MCP.EnableMCPServer == 1, - "port": h.config.MCP.MCPPort, - "host": h.config.MCP.MCPHost, + + statusText := "inactive" + if status.Running { + statusText = "active" } - common.SuccessResponse(c, status) + response := web.MCPStatusResponse{ + Status: statusText, + Config: h.config.MCP, + } + + common.SuccessResponse(c, response) } // RestartMCPServer 重启 MCP 服务器 @@ -1100,48 +1091,74 @@ func (h *AdminHandler) TestMCPConnection(c *gin.Context) { return } - // 使用提供的端口或默认配置 - port := testData.Port + address, _, _, err := normalizeMCPAddress(testData.Host, testData.Port, h.config) + if err != nil { + common.BadRequestResponse(c, "参数错误: "+err.Error()) + return + } + + if err := tcpProbe(address, 3*time.Second); err != nil { + common.ErrorResponse(c, 400, fmt.Sprintf("连接测试失败: %s,端口可能未开放或MCP服务器未启动", err.Error())) + return + } + + response := web.MCPTestResponse{ + MCPStatusResponse: web.MCPStatusResponse{ + Status: "连接正常", + Config: h.config.MCP, + }, + } + + common.SuccessWithMessage(c, "MCP连接测试成功", response) +} + +// normalizeMCPAddress 解析并验证 host/port,返回用于 TCP 测试的 address、实际 host(用于响应)和 port。 +func normalizeMCPAddress(reqHost, reqPort string, cfg *config.ConfigManager) (address, host, port string, err error) { + // port 优先级:请求参数 -> 配置 -> 默认 8081 + port = reqPort if port == "" { - port = h.config.MCP.MCPPort + port = cfg.MCP.MCPPort } if port == "" { port = "8081" } - host := testData.Host + // 验证端口号 + pnum, perr := strconv.Atoi(port) + if perr != nil || pnum < 1 || pnum > 65535 { + err = fmt.Errorf("无效端口号: %s", port) + return + } + + // host 优先级:请求参数 -> 配置 -> 默认 0.0.0.0 + host = reqHost if host == "" { - host = h.config.MCP.MCPHost + host = cfg.MCP.MCPHost } if host == "" { host = "0.0.0.0" } - // 进行简单的端口连通性测试 - address := host + ":" + port + address = host + ":" + port if host == "0.0.0.0" { + // 绑定到 0.0.0.0 时,测试本机回环地址 address = "127.0.0.1:" + port } + return +} - // 尝试连接端口 - conn, err := net.DialTimeout("tcp", address, time.Second*3) +// tcpProbe 尝试在给定超时内建立 TCP 连接以检测端口连通性 +func tcpProbe(address string, timeout time.Duration) error { + conn, err := net.DialTimeout("tcp", address, timeout) if err != nil { - // 端口未开放或连接失败 - common.ErrorResponse(c, 400, fmt.Sprintf("连接测试失败: %s,端口可能未开放或MCP服务器未启动", err.Error())) - return + return err } defer func() { if err := conn.Close(); err != nil { log.Printf("关闭连接失败: %v", err) } }() - - common.SuccessWithMessage(c, "MCP连接测试成功", map[string]interface{}{ - "address": address, - "status": "连接正常", - "port": port, - "host": host, - }) + return nil } // GetSystemLogs 获取系统日志 @@ -1171,10 +1188,12 @@ func (h *AdminHandler) GetSystemLogs(c *gin.Context) { logs = filteredLogs } - common.SuccessResponse(c, map[string]interface{}{ - "logs": logs, - "total": len(logs), - }) + response := web.LogsResponse{ + Logs: logs, + Total: len(logs), + } + + common.SuccessResponse(c, response) } // GetRunningTasks 获取运行中的任务 @@ -1185,10 +1204,12 @@ func (h *AdminHandler) GetRunningTasks(c *gin.Context) { return } - common.SuccessResponse(c, map[string]interface{}{ - "tasks": tasks, - "total": len(tasks), - }) + response := web.TasksResponse{ + Tasks: tasks, + Total: len(tasks), + } + + common.SuccessResponse(c, response) } // CancelTask 取消任务 diff --git a/internal/handlers/admin_config_new_example.go b/internal/handlers/admin_config_new_example.go new file mode 100644 index 0000000..ac34b18 --- /dev/null +++ b/internal/handlers/admin_config_new_example.go @@ -0,0 +1,13 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/common" +) + +// 新版 handler 响应示例,直接序列化 ConfigManager + +func (h *AdminHandler) GetConfigExample(c *gin.Context) { + cfg := h.service.GetFullConfig() + common.SuccessResponse(c, cfg) +} diff --git a/internal/handlers/chunk.go b/internal/handlers/chunk.go index 69eb2a9..6b8b465 100644 --- a/internal/handlers/chunk.go +++ b/internal/handlers/chunk.go @@ -2,11 +2,11 @@ package handlers import ( "fmt" - "strconv" "github.com/zy84338719/filecodebox/internal/common" "github.com/zy84338719/filecodebox/internal/models/web" "github.com/zy84338719/filecodebox/internal/services" + "github.com/zy84338719/filecodebox/internal/utils" "github.com/gin-gonic/gin" ) @@ -33,8 +33,7 @@ func NewChunkHandler(service *services.ChunkService) *ChunkHandler { // @Router /chunk/upload/init/ [post] func (h *ChunkHandler) InitChunkUpload(c *gin.Context) { var req web.ChunkUploadInitRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &req) { return } @@ -67,17 +66,13 @@ func (h *ChunkHandler) InitChunkUpload(c *gin.Context) { // @Router /chunk/upload/chunk/{upload_id}/{chunk_index} [post] func (h *ChunkHandler) UploadChunk(c *gin.Context) { uploadID := c.Param("upload_id") - chunkIndexStr := c.Param("chunk_index") - - chunkIndex, err := strconv.Atoi(chunkIndexStr) - if err != nil { - common.BadRequestResponse(c, "分片索引错误") + chunkIndex, success := utils.ParseIntFromParam(c, "chunk_index", "分片索引错误") + if !success { return } - file, err := c.FormFile("chunk") - if err != nil { - common.BadRequestResponse(c, "获取分片文件失败") + file, success := utils.ParseFileFromForm(c, "chunk") + if !success { return } @@ -145,11 +140,9 @@ func (h *ChunkHandler) GetUploadStatus(c *gin.Context) { // VerifyChunk 验证分片完整性 func (h *ChunkHandler) VerifyChunk(c *gin.Context) { uploadID := c.Param("upload_id") - chunkIndexStr := c.Param("chunk_index") - chunkIndex, err := strconv.Atoi(chunkIndexStr) - if err != nil { - common.BadRequestResponse(c, "分片索引错误") + chunkIndex, success := utils.ParseIntFromParam(c, "chunk_index", "分片索引错误") + if !success { return } @@ -157,8 +150,7 @@ func (h *ChunkHandler) VerifyChunk(c *gin.Context) { ChunkHash string `json:"chunk_hash" binding:"required"` } - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &req) { return } diff --git a/internal/handlers/setup.go b/internal/handlers/setup.go index 78e9753..0ac34c3 100644 --- a/internal/handlers/setup.go +++ b/internal/handlers/setup.go @@ -15,6 +15,7 @@ import ( "github.com/zy84338719/filecodebox/internal/models" "github.com/zy84338719/filecodebox/internal/repository" "github.com/zy84338719/filecodebox/internal/services/auth" + "github.com/zy84338719/filecodebox/internal/utils" "gorm.io/gorm" ) @@ -197,8 +198,7 @@ func InitializeNoDB(manager *config.ConfigManager) gin.HandlerFunc { defer atomic.StoreInt32(&initInProgress, 0) // 解析 JSON(仅接受嵌套结构),不再兼容 legacy 扁平字段 var req SetupRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "请求参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &req) { return } // 继续使用 req 进行验证和初始化 @@ -433,8 +433,7 @@ func (h *SetupHandler) Initialize(c *gin.Context) { } var req SetupRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "请求参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &req) { return } diff --git a/internal/handlers/share.go b/internal/handlers/share.go index 36ed002..47302ba 100644 --- a/internal/handlers/share.go +++ b/internal/handlers/share.go @@ -1,13 +1,12 @@ package handlers import ( - "strconv" - "github.com/zy84338719/filecodebox/internal/common" "github.com/zy84338719/filecodebox/internal/models" "github.com/zy84338719/filecodebox/internal/models/web" "github.com/zy84338719/filecodebox/internal/services/share" "github.com/zy84338719/filecodebox/internal/storage" + "github.com/zy84338719/filecodebox/internal/utils" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" @@ -42,42 +41,28 @@ func (h *ShareHandler) ShareText(c *gin.Context) { expireStyle := c.DefaultPostForm("expire_style", "day") requireAuthStr := c.DefaultPostForm("require_auth", "false") - expireValue, err := strconv.Atoi(expireValueStr) - if err != nil { - common.BadRequestResponse(c, "过期时间参数错误") - return - } - - // 对于forever模式,允许expireValue为0 - // 对于count模式,expireValue必须大于0 - // 对于时间模式,expireValue必须大于0 - if expireValue < 0 || (expireStyle != "forever" && expireValue == 0) { - common.BadRequestResponse(c, "过期时间参数错误") - return - } - if text == "" { common.BadRequestResponse(c, "文本内容不能为空") return } - // 检查是否需要登录才能下载 - requireAuth := requireAuthStr == "true" + // 解析过期参数 + expireParams, err := utils.ParseExpireParams(expireValueStr, expireStyle, requireAuthStr) + if err != nil { + common.BadRequestResponse(c, err.Error()) + return + } // 构建请求 req := web.ShareTextRequest{ Text: text, - ExpireValue: expireValue, - ExpireStyle: expireStyle, - RequireAuth: requireAuth, + ExpireValue: expireParams.ExpireValue, + ExpireStyle: expireParams.ExpireStyle, + RequireAuth: expireParams.RequireAuth, } // 检查是否为认证用户上传 - var userID *uint - if uid, exists := c.Get("user_id"); exists { - id := uid.(uint) - userID = &id - } + userID := utils.GetUserIDFromContext(c) fileResult, err := h.service.ShareTextWithAuth(req.Text, req.ExpireValue, req.ExpireStyle, userID) if err != nil { @@ -110,47 +95,33 @@ func (h *ShareHandler) ShareText(c *gin.Context) { // @Failure 500 {object} map[string]interface{} "服务器内部错误" // @Router /share/file/ [post] func (h *ShareHandler) ShareFile(c *gin.Context) { - // 绑定表单参数 - var req web.ShareFileRequest - if err := c.ShouldBind(&req); err != nil { - common.BadRequestResponse(c, "请求参数错误: "+err.Error()) - return - } + // 解析表单参数 + expireValueStr := c.DefaultPostForm("expire_value", "1") + expireStyle := c.DefaultPostForm("expire_style", "day") + requireAuthStr := c.DefaultPostForm("require_auth", "false") - // 默认值处理和验证 - if req.ExpireValue < 0 { - common.BadRequestResponse(c, "过期时间参数不能为负数") + // 解析过期参数 + expireParams, err := utils.ParseExpireParams(expireValueStr, expireStyle, requireAuthStr) + if err != nil { + common.BadRequestResponse(c, err.Error()) return } - // 对于非forever模式,ExpireValue不能为0 - if req.ExpireStyle != "forever" && req.ExpireValue == 0 { - req.ExpireValue = 1 // 默认值 - } - - if req.ExpireStyle == "" { - req.ExpireStyle = "day" - } - - file, err := c.FormFile("file") - if err != nil { - common.BadRequestResponse(c, "获取文件失败") + // 解析文件 + file, success := utils.ParseFileFromForm(c, "file") + if !success { return } // 检查是否为认证用户上传 - var userID *uint - if uid, exists := c.Get("user_id"); exists { - id := uid.(uint) - userID = &id - } + userID := utils.GetUserIDFromContext(c) // 构建服务层请求(这里需要适配服务层的接口) serviceReq := models.ShareFileRequest{ File: file, - ExpireValue: req.ExpireValue, - ExpireStyle: req.ExpireStyle, - RequireAuth: req.RequireAuth, + ExpireValue: expireParams.ExpireValue, + ExpireStyle: expireParams.ExpireStyle, + RequireAuth: expireParams.RequireAuth, ClientIP: c.ClientIP(), UserID: userID, } diff --git a/internal/handlers/storage.go b/internal/handlers/storage.go index 277fcff..c3b79d9 100644 --- a/internal/handlers/storage.go +++ b/internal/handlers/storage.go @@ -32,10 +32,10 @@ func (sh *StorageHandler) GetStorageInfo(c *gin.Context) { currentStorage := sh.storageManager.GetCurrentStorage() // 获取各存储类型的详细信息 - storageDetails := make(map[string]web.StorageDetail) + storageDetails := make(map[string]web.WebStorageDetail) for _, storageType := range availableStorages { - detail := web.StorageDetail{ + detail := web.WebStorageDetail{ Type: storageType, Available: true, } @@ -77,18 +77,14 @@ func (sh *StorageHandler) GetStorageInfo(c *gin.Context) { storageDetails[storageType] = detail } - // 构建存储配置信息 - storageConfig := map[string]interface{}{ - "local": sh.storageConfig, - "webdav": sh.storageConfig.WebDAV, - "nfs": sh.storageConfig.NFS, - } + // 为前端创建适配的存储配置 + adaptedStorageConfig := sh.createAdaptedStorageConfig() response := web.StorageInfoResponse{ Current: currentStorage, Available: availableStorages, StorageDetails: storageDetails, - StorageConfig: storageConfig, + StorageConfig: adaptedStorageConfig, } common.SuccessResponse(c, response) @@ -97,8 +93,7 @@ func (sh *StorageHandler) GetStorageInfo(c *gin.Context) { // SwitchStorage 切换存储类型 func (sh *StorageHandler) SwitchStorage(c *gin.Context) { var req web.StorageSwitchRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &req) { return } @@ -144,240 +139,127 @@ func (sh *StorageHandler) TestStorageConnection(c *gin.Context) { // UpdateStorageConfig 更新存储配置 func (sh *StorageHandler) UpdateStorageConfig(c *gin.Context) { - var req struct { - StorageType string `json:"storage_type" binding:"required"` - Config map[string]interface{} `json:"config" binding:"required"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "参数错误: "+err.Error()) + var req web.StorageTestRequest + if !utils.BindJSONWithValidation(c, &req) { return } // 根据存储类型更新配置 - switch req.StorageType { + switch req.Type { case "local": - if val, ok := req.Config["storage_path"]; ok { - if storagePath, ok := val.(string); ok { - sh.storageConfig.StoragePath = storagePath - } else { - common.BadRequestResponse(c, "storage_path 必须是字符串类型") - return - } + if req.Config != nil && req.Config.StoragePath != "" { + sh.storageConfig.StoragePath = req.Config.StoragePath } case "webdav": - if val, ok := req.Config["hostname"]; ok { - if hostname, ok := val.(string); ok { - sh.storageConfig.WebDAV.Hostname = hostname - } else { - common.BadRequestResponse(c, "hostname 必须是字符串类型") - return + if req.Config != nil && req.Config.WebDAV != nil { + if sh.storageConfig.WebDAV == nil { + sh.storageConfig.WebDAV = &config.WebDAVConfig{} } - } - if val, ok := req.Config["username"]; ok { - if username, ok := val.(string); ok { - sh.storageConfig.WebDAV.Username = username - } else { - common.BadRequestResponse(c, "username 必须是字符串类型") - return + if req.Config.WebDAV.Hostname != "" { + sh.storageConfig.WebDAV.Hostname = req.Config.WebDAV.Hostname } - } - if val, ok := req.Config["password"]; ok { - if password, ok := val.(string); ok && password != "" { - sh.storageConfig.WebDAV.Password = password - } else if !ok { - common.BadRequestResponse(c, "password 必须是字符串类型") - return + if req.Config.WebDAV.Username != "" { + sh.storageConfig.WebDAV.Username = req.Config.WebDAV.Username } - } - if val, ok := req.Config["root_path"]; ok { - if rootPath, ok := val.(string); ok { - sh.storageConfig.WebDAV.RootPath = rootPath - } else { - common.BadRequestResponse(c, "root_path 必须是字符串类型") - return + if req.Config.WebDAV.Password != "" { + sh.storageConfig.WebDAV.Password = req.Config.WebDAV.Password } - } - if val, ok := req.Config["url"]; ok { - if url, ok := val.(string); ok { - sh.storageConfig.WebDAV.URL = url - } else { - common.BadRequestResponse(c, "url 必须是字符串类型") - return + if req.Config.WebDAV.RootPath != "" { + sh.storageConfig.WebDAV.RootPath = req.Config.WebDAV.RootPath + } + if req.Config.WebDAV.URL != "" { + sh.storageConfig.WebDAV.URL = req.Config.WebDAV.URL } - } - // 重新创建 WebDAV 存储以应用新配置 - // 由于使用了策略模式,我们需要重新创建存储实例 - if err := sh.storageManager.ReconfigureWebDAV( - sh.storageConfig.WebDAV.Hostname, - sh.storageConfig.WebDAV.Username, - sh.storageConfig.WebDAV.Password, - sh.storageConfig.WebDAV.RootPath, - ); err != nil { - common.InternalServerErrorResponse(c, "重新配置WebDAV存储失败: "+err.Error()) - return + // 重新创建 WebDAV 存储以应用新配置 + if err := sh.storageManager.ReconfigureWebDAV( + sh.storageConfig.WebDAV.Hostname, + sh.storageConfig.WebDAV.Username, + sh.storageConfig.WebDAV.Password, + sh.storageConfig.WebDAV.RootPath, + ); err != nil { + common.InternalServerErrorResponse(c, "重新配置WebDAV存储失败: "+err.Error()) + return + } } case "s3": - if val, ok := req.Config["access_key_id"]; ok { - if accessKeyID, ok := val.(string); ok { - sh.storageConfig.S3.AccessKeyID = accessKeyID - } else { - common.BadRequestResponse(c, "access_key_id 必须是字符串类型") - return + if req.Config != nil && req.Config.S3 != nil { + if sh.storageConfig.S3 == nil { + sh.storageConfig.S3 = &config.S3Config{} } - } - if val, ok := req.Config["secret_access_key"]; ok { - if secretAccessKey, ok := val.(string); ok && secretAccessKey != "" { - sh.storageConfig.S3.SecretAccessKey = secretAccessKey - } else if !ok { - common.BadRequestResponse(c, "secret_access_key 必须是字符串类型") - return + if req.Config.S3.AccessKeyID != "" { + sh.storageConfig.S3.AccessKeyID = req.Config.S3.AccessKeyID } - } - if val, ok := req.Config["bucket_name"]; ok { - if bucketName, ok := val.(string); ok { - sh.storageConfig.S3.BucketName = bucketName - } else { - common.BadRequestResponse(c, "bucket_name 必须是字符串类型") - return + if req.Config.S3.SecretAccessKey != "" { + sh.storageConfig.S3.SecretAccessKey = req.Config.S3.SecretAccessKey } - } - if val, ok := req.Config["endpoint_url"]; ok { - if endpointURL, ok := val.(string); ok { - sh.storageConfig.S3.EndpointURL = endpointURL - } else { - common.BadRequestResponse(c, "endpoint_url 必须是字符串类型") - return + if req.Config.S3.BucketName != "" { + sh.storageConfig.S3.BucketName = req.Config.S3.BucketName } - } - if val, ok := req.Config["region_name"]; ok { - if regionName, ok := val.(string); ok { - sh.storageConfig.S3.RegionName = regionName - } else { - common.BadRequestResponse(c, "region_name 必须是字符串类型") - return + if req.Config.S3.EndpointURL != "" { + sh.storageConfig.S3.EndpointURL = req.Config.S3.EndpointURL } - } - if val, ok := req.Config["hostname"]; ok { - if hostname, ok := val.(string); ok { - sh.storageConfig.S3.Hostname = hostname - } else { - common.BadRequestResponse(c, "hostname 必须是字符串类型") - return + if req.Config.S3.RegionName != "" { + sh.storageConfig.S3.RegionName = req.Config.S3.RegionName } - } - if val, ok := req.Config["proxy"]; ok { - if proxy, ok := val.(bool); ok { - if proxy { - sh.storageConfig.S3.Proxy = 1 - } else { - sh.storageConfig.S3.Proxy = 0 - } - } else { - common.BadRequestResponse(c, "proxy 必须是布尔类型") - return + if req.Config.S3.Hostname != "" { + sh.storageConfig.S3.Hostname = req.Config.S3.Hostname } + // Proxy 字段直接赋值 + sh.storageConfig.S3.Proxy = req.Config.S3.Proxy } case "nfs": - if val, ok := req.Config["server"]; ok { - if server, ok := val.(string); ok { - sh.storageConfig.NFS.Server = server - } else { - common.BadRequestResponse(c, "server 必须是字符串类型") - return + if req.Config != nil && req.Config.NFS != nil { + if sh.storageConfig.NFS == nil { + sh.storageConfig.NFS = &config.NFSConfig{} } - } - if val, ok := req.Config["nfs_path"]; ok { - if nfsPath, ok := val.(string); ok { - sh.storageConfig.NFS.Path = nfsPath - } else { - common.BadRequestResponse(c, "nfs_path 必须是字符串类型") - return + if req.Config.NFS.Server != "" { + sh.storageConfig.NFS.Server = req.Config.NFS.Server } - } - if val, ok := req.Config["mount_point"]; ok { - if mountPoint, ok := val.(string); ok { - sh.storageConfig.NFS.MountPoint = mountPoint - } else { - common.BadRequestResponse(c, "mount_point 必须是字符串类型") - return + if req.Config.NFS.Path != "" { + sh.storageConfig.NFS.Path = req.Config.NFS.Path } - } - if val, ok := req.Config["version"]; ok { - if version, ok := val.(string); ok { - sh.storageConfig.NFS.Version = version - } else { - common.BadRequestResponse(c, "version 必须是字符串类型") - return + if req.Config.NFS.MountPoint != "" { + sh.storageConfig.NFS.MountPoint = req.Config.NFS.MountPoint } - } - if val, ok := req.Config["options"]; ok { - if options, ok := val.(string); ok { - sh.storageConfig.NFS.Options = options - } else { - common.BadRequestResponse(c, "options 必须是字符串类型") - return + if req.Config.NFS.Version != "" { + sh.storageConfig.NFS.Version = req.Config.NFS.Version } - } - if val, ok := req.Config["timeout"]; ok { - if timeout, ok := val.(float64); ok { - sh.storageConfig.NFS.Timeout = int(timeout) - } else { - common.BadRequestResponse(c, "timeout 必须是数值类型") - return + if req.Config.NFS.Options != "" { + sh.storageConfig.NFS.Options = req.Config.NFS.Options } - } - if val, ok := req.Config["auto_mount"]; ok { - if autoMount, ok := val.(bool); ok { - if autoMount { - sh.storageConfig.NFS.AutoMount = 1 - } else { - sh.storageConfig.NFS.AutoMount = 0 - } - } else { - common.BadRequestResponse(c, "auto_mount 必须是布尔类型") - return + if req.Config.NFS.Timeout > 0 { + sh.storageConfig.NFS.Timeout = req.Config.NFS.Timeout } - } - if val, ok := req.Config["retry_count"]; ok { - if retryCount, ok := val.(float64); ok { - sh.storageConfig.NFS.RetryCount = int(retryCount) - } else { - common.BadRequestResponse(c, "retry_count 必须是数值类型") - return + if req.Config.NFS.SubPath != "" { + sh.storageConfig.NFS.SubPath = req.Config.NFS.SubPath } - } - if val, ok := req.Config["sub_path"]; ok { - if subPath, ok := val.(string); ok { - sh.storageConfig.NFS.SubPath = subPath - } else { - common.BadRequestResponse(c, "sub_path 必须是字符串类型") + // 直接赋值的字段 + sh.storageConfig.NFS.AutoMount = req.Config.NFS.AutoMount + sh.storageConfig.NFS.RetryCount = req.Config.NFS.RetryCount + + // 重新创建 NFS 存储以应用新配置 + if err := sh.storageManager.ReconfigureNFS( + sh.storageConfig.NFS.Server, + sh.storageConfig.NFS.Path, + sh.storageConfig.NFS.MountPoint, + sh.storageConfig.NFS.Version, + sh.storageConfig.NFS.Options, + sh.storageConfig.NFS.Timeout, + sh.storageConfig.NFS.AutoMount == 1, + sh.storageConfig.NFS.RetryCount, + sh.storageConfig.NFS.SubPath, + ); err != nil { + common.InternalServerErrorResponse(c, "重新配置NFS存储失败: "+err.Error()) return } } - // 重新创建 NFS 存储以应用新配置 - if err := sh.storageManager.ReconfigureNFS( - sh.storageConfig.NFS.Server, - sh.storageConfig.NFS.Path, - sh.storageConfig.NFS.MountPoint, - sh.storageConfig.NFS.Version, - sh.storageConfig.NFS.Options, - sh.storageConfig.NFS.Timeout, - sh.storageConfig.NFS.AutoMount == 1, - sh.storageConfig.NFS.RetryCount, - sh.storageConfig.NFS.SubPath, - ); err != nil { - common.InternalServerErrorResponse(c, "重新配置NFS存储失败: "+err.Error()) - return - } - default: - common.BadRequestResponse(c, "不支持的存储类型: "+req.StorageType) + common.BadRequestResponse(c, "不支持的存储类型: "+req.Type) return } @@ -390,4 +272,21 @@ func (sh *StorageHandler) UpdateStorageConfig(c *gin.Context) { common.SuccessWithMessage(c, "存储配置更新成功", nil) } +// createAdaptedStorageConfig 创建适配前端的存储配置 +func (sh *StorageHandler) createAdaptedStorageConfig() *config.StorageConfig { + adapted := sh.storageConfig.Clone() + + // 确保Type字段正确设置 + if adapted.Type == "" { + adapted.Type = "local" + } + + // 设置存储路径的默认值 + if adapted.StoragePath == "" { + adapted.StoragePath = "./data" + } + + return adapted +} + // ...existing code... diff --git a/internal/handlers/user.go b/internal/handlers/user.go index 58b319f..1f64102 100644 --- a/internal/handlers/user.go +++ b/internal/handlers/user.go @@ -8,6 +8,7 @@ import ( "github.com/zy84338719/filecodebox/internal/models" "github.com/zy84338719/filecodebox/internal/models/web" "github.com/zy84338719/filecodebox/internal/services" + "github.com/zy84338719/filecodebox/internal/utils" "github.com/gin-gonic/gin" ) @@ -32,8 +33,7 @@ func (h *UserHandler) Register(c *gin.Context) { } var req web.AuthRegisterRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "请求参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &req) { return } @@ -77,8 +77,7 @@ func (h *UserHandler) Login(c *gin.Context) { } var req web.AuthLoginRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "请求参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &req) { return } @@ -175,8 +174,7 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) { } var req web.UserProfileUpdateRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "请求参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &req) { return } @@ -197,8 +195,7 @@ func (h *UserHandler) ChangePassword(c *gin.Context) { } var req web.UserPasswordChangeRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.BadRequestResponse(c, "请求参数错误: "+err.Error()) + if !utils.BindJSONWithValidation(c, &req) { return } diff --git a/internal/mcp/manager.go b/internal/mcp/manager.go index 66da1bc..99c9fb9 100644 --- a/internal/mcp/manager.go +++ b/internal/mcp/manager.go @@ -15,6 +15,13 @@ import ( "github.com/zy84338719/filecodebox/internal/storage" ) +// MCPStatus MCP服务器状态 +type MCPStatus struct { + Running bool `json:"running"` + Timestamp string `json:"timestamp"` + ServerInfo ServerInfo `json:"server_info,omitempty"` +} + // MCPManager MCP 服务器管理器 type MCPManager struct { manager *config.ConfigManager @@ -161,19 +168,19 @@ func (m *MCPManager) IsRunning() bool { } // GetStatus 获取 MCP 服务器状态 -func (m *MCPManager) GetStatus() map[string]interface{} { +func (m *MCPManager) GetStatus() MCPStatus { m.mu.RLock() defer m.mu.RUnlock() - status := map[string]interface{}{ - "running": m.running, - "timestamp": time.Now().Format("2006-01-02 15:04:05"), + status := MCPStatus{ + Running: m.running, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), } if m.running && m.server != nil { - status["server_info"] = map[string]interface{}{ - "name": "FileCodeBox MCP Server", - "version": "1.0.0", + status.ServerInfo = ServerInfo{ + Name: "FileCodeBox MCP Server", + Version: "1.0.0", } } diff --git a/internal/middleware/combined_auth.go b/internal/middleware/combined_auth.go index adc867a..ad8ba33 100644 --- a/internal/middleware/combined_auth.go +++ b/internal/middleware/combined_auth.go @@ -4,36 +4,42 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/zy84338719/filecodebox/internal/common" "github.com/zy84338719/filecodebox/internal/config" "github.com/zy84338719/filecodebox/internal/services" ) -// CombinedAdminAuth 尝试基于 JWT 的用户认证并确保角色为 admin +// CombinedAdminAuth 支持两类管理员认证: +// - session-based JWT(由 userService.ValidateToken 验证,返回 *services.AuthClaims,且 Role=="admin") +// - admin JWT(由 AdminService 生成,使用 manager.User.JWTSecret 签名,claims 为 jwt.MapClaims 且包含 is_admin:true 或 role:"admin") func CombinedAdminAuth(manager *config.ConfigManager, userService interface { ValidateToken(string) (interface{}, error) }) gin.HandlerFunc { return func(c *gin.Context) { - // Allow public access to admin front-end entry and static assets - // This prevents accidental interception of requests for the login page - // when an invalid/stale Authorization header is present. p := c.Request.URL.Path - if p == "/admin/" || p == "/admin" || - strings.HasPrefix(p, "/admin/css/") || strings.HasPrefix(p, "/admin/js/") || - strings.HasPrefix(p, "/admin/assets/") || strings.HasPrefix(p, "/admin/templates/") || - strings.HasPrefix(p, "/admin/components/") { + + // 公开静态/入口路径(允许匿名访问 admin 前端和静态资源) + if strings.HasPrefix(p, "/admin/css/") || strings.HasPrefix(p, "/admin/js/") || strings.HasPrefix(p, "/admin/templates/") || strings.HasPrefix(p, "/admin/assets/") || strings.HasPrefix(p, "/admin/components/") || p == "/admin" || p == "/admin/" || p == "/admin/login" { c.Next() return } - // 先尝试JWT用户认证 + + // 读取 Authorization header authHeader := c.GetHeader("Authorization") + var tokenStr string if authHeader != "" { - tokenParts := strings.SplitN(authHeader, " ", 2) - if len(tokenParts) == 2 && tokenParts[0] == "Bearer" { - claimsInterface, err := userService.ValidateToken(tokenParts[1]) - if err == nil { - if claims, ok := claimsInterface.(*services.AuthClaims); ok { + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) == 2 && parts[0] == "Bearer" { + tokenStr = parts[1] + } + } + + if tokenStr != "" { + // 仅使用 session-based token(复用普通用户登录逻辑),并加强角色校验 + if userService != nil { + if claimsIface, err := userService.ValidateToken(tokenStr); err == nil { + if claims, ok := claimsIface.(*services.AuthClaims); ok { + // 如果不是管理员直接拒绝 if claims.Role == "admin" { c.Set("user_id", claims.UserID) c.Set("username", claims.Username) @@ -43,17 +49,8 @@ func CombinedAdminAuth(manager *config.ConfigManager, userService interface { c.Next() return } - // role mismatch - if manager == nil || (manager.Base != nil && !manager.Base.Production) { - logrus.WithFields(logrus.Fields{"role": claims.Role}).Debug("combined auth: token role is not admin") - } - } - } else { - if manager == nil || (manager.Base != nil && !manager.Base.Production) { - logrus.WithError(err).Debug("combined auth: ValidateToken returned error") } } - // JWT 验证失败或非管理员角色,继续到失败处理(不回退到旧的静态令牌) } } diff --git a/internal/middleware/ratelimiter.go b/internal/middleware/ratelimiter.go index e6c16c7..9866d98 100644 --- a/internal/middleware/ratelimiter.go +++ b/internal/middleware/ratelimiter.go @@ -57,10 +57,15 @@ func RateLimit(manager *config.ConfigManager) gin.HandlerFunc { ip := c.ClientIP() if c.Request.URL.Path == "/share/file/" || c.Request.URL.Path == "/share/text/" { + // 防止除零错误 + uploadCount := manager.UploadCount + if uploadCount <= 0 { + uploadCount = 1 // 默认值 + } limiter := uploadLimiter.GetLimiter( ip, - rate.Every(time.Duration(manager.UploadMinute)*time.Minute/time.Duration(manager.UploadCount)), - manager.UploadCount, + rate.Every(time.Duration(manager.UploadMinute)*time.Minute/time.Duration(uploadCount)), + uploadCount, ) if !limiter.Allow() { common.TooManyRequestsResponse(c, "上传频率过快,请稍后再试") @@ -68,10 +73,15 @@ func RateLimit(manager *config.ConfigManager) gin.HandlerFunc { return } } else if c.Request.URL.Path == "/share/select/" && c.Request.Method == "GET" { + // 防止除零错误 + errorCount := manager.ErrorCount + if errorCount <= 0 { + errorCount = 1 // 默认值 + } limiter := errorLimiter.GetLimiter( ip, - rate.Every(time.Duration(manager.ErrorMinute)*time.Minute/time.Duration(manager.ErrorCount)), - manager.ErrorCount, + rate.Every(time.Duration(manager.ErrorMinute)*time.Minute/time.Duration(errorCount)), + errorCount, ) if !limiter.Allow() { common.TooManyRequestsResponse(c, "请求频率过快,请稍后再试") diff --git a/internal/models/db/user.go b/internal/models/db/user.go index 41dec6a..59a3a7f 100644 --- a/internal/models/db/user.go +++ b/internal/models/db/user.go @@ -48,47 +48,3 @@ type UserStats struct { TotalStorage int64 `json:"total_storage"` FileCount int `json:"file_count"` } - -// ToMap 将结构体转换为 map,只包含非空字段 -func (u *User) ToMap() map[string]interface{} { - updates := make(map[string]interface{}) - - if u.Email != "" { - updates["email"] = u.Email - } - if u.PasswordHash != "" { - updates["password_hash"] = u.PasswordHash - } - if u.Nickname != "" { - updates["nickname"] = u.Nickname - } - if u.Avatar != "" { - updates["avatar"] = u.Avatar - } - if u.Role != "" { - updates["role"] = u.Role - } - if u.Status != "" { - updates["status"] = u.Status - } - if !u.EmailVerified { - updates["email_verified"] = u.EmailVerified - } - if u.LastLoginAt != nil { - updates["last_login_at"] = u.LastLoginAt - } - if u.LastLoginIP != "" { - updates["last_login_ip"] = u.LastLoginIP - } - if u.TotalUploads != 0 { - updates["total_uploads"] = u.TotalUploads - } - if u.TotalDownloads != 0 { - updates["total_downloads"] = u.TotalDownloads - } - if u.TotalStorage != 0 { - updates["total_storage"] = u.TotalStorage - } - - return updates -} diff --git a/internal/models/service/admin.go b/internal/models/service/admin.go index 49bb85a..8f0b5dd 100644 --- a/internal/models/service/admin.go +++ b/internal/models/service/admin.go @@ -83,10 +83,19 @@ type DatabaseStats struct { // StorageStatus 存储状态信息 type StorageStatus struct { - Type string `json:"type"` - Status string `json:"status"` - Available bool `json:"available"` - Details map[string]interface{} `json:"details"` + Type string `json:"type"` + Status string `json:"status"` + Available bool `json:"available"` + Details AdminStorageDetail `json:"details"` +} + +// AdminStorageDetail 管理员视图的存储详细信息 +type AdminStorageDetail struct { + TotalSpace int64 `json:"total_space"` + AvailableSpace int64 `json:"available_space"` + UsedSpace int64 `json:"used_space"` + UsagePercent float64 `json:"usage_percent"` + Type string `json:"type"` } // DiskUsage 磁盘使用情况 diff --git a/internal/models/web/admin.go b/internal/models/web/admin.go index 083031d..4cab9b6 100644 --- a/internal/models/web/admin.go +++ b/internal/models/web/admin.go @@ -1,5 +1,7 @@ package web +import "github.com/zy84338719/filecodebox/internal/config" + // AdminLoginRequest 管理员登录请求 type AdminLoginRequest struct { Username string `json:"username,omitempty"` @@ -142,66 +144,22 @@ type AdminUserStatusRequest struct { // AdminConfigResponse 管理员配置响应 type AdminConfigResponse struct { - Config interface{} `json:"config"` + AdminConfigRequest } // AdminConfigRequest 管理员配置更新请求 type AdminConfigRequest struct { - Base *AdminBaseConfig `json:"base,omitempty"` - Transfer *AdminTransferConfig `json:"transfer,omitempty"` - User *AdminUserConfig `json:"user,omitempty"` - // 其他单独的配置字段 - NotifyTitle *string `json:"notify_title,omitempty"` - NotifyContent *string `json:"notify_content,omitempty"` - PageExplain *string `json:"page_explain,omitempty"` - Opacity *float64 `json:"opacity,omitempty"` - ThemesSelect *string `json:"themes_select,omitempty"` -} - -// AdminBaseConfig 基础配置请求 -type AdminBaseConfig struct { - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - Keywords *string `json:"keywords,omitempty"` -} - -// AdminTransferConfig 传输配置请求 -type AdminTransferConfig struct { - Upload *AdminUploadConfig `json:"upload,omitempty"` - Download *AdminDownloadConfig `json:"download,omitempty"` -} - -// AdminUploadConfig 上传配置请求 -type AdminUploadConfig struct { - OpenUpload *int `json:"open_upload,omitempty"` - UploadSize *int64 `json:"upload_size,omitempty"` - EnableChunk *int `json:"enable_chunk,omitempty"` - ChunkSize *int64 `json:"chunk_size,omitempty"` - MaxSaveSeconds *int `json:"max_save_seconds,omitempty"` -} - -// AdminDownloadConfig 下载配置请求 -type AdminDownloadConfig struct { - EnableConcurrentDownload *int `json:"enable_concurrent_download,omitempty"` - MaxConcurrentDownloads *int `json:"max_concurrent_downloads,omitempty"` - DownloadTimeout *int `json:"download_timeout,omitempty"` -} + // 配置模块,直接使用各配置结构体避免字段重复 + Base *config.BaseConfig `json:"base,omitempty"` + Database *config.DatabaseConfig `json:"database,omitempty"` + Transfer *config.TransferConfig `json:"transfer,omitempty"` + Storage *config.StorageConfig `json:"storage,omitempty"` + User *config.UserSystemConfig `json:"user,omitempty"` + MCP *config.MCPConfig `json:"mcp,omitempty"` + UI *config.UIConfig `json:"ui,omitempty"` -// AdminUserConfig 用户配置请求 -type AdminUserConfig struct { - AllowUserRegistration *int `json:"allow_user_registration,omitempty"` - RequireEmailVerify *int `json:"require_email_verify,omitempty"` - UserUploadSize *int64 `json:"user_upload_size,omitempty"` - UserStorageQuota *int64 `json:"user_storage_quota,omitempty"` - SessionExpiryHours *int `json:"session_expiry_hours,omitempty"` - MaxSessionsPerUser *int `json:"max_sessions_per_user,omitempty"` - JWTSecret *string `json:"jwt_secret,omitempty"` -} - -// AdminSystemStatusResponse 管理员系统状态响应 -type AdminSystemStatusResponse struct { - Status string `json:"status"` - Info interface{} `json:"info"` + // 系统运行时特有字段(不属于配置模块的字段) + SysStart *string `json:"sys_start,omitempty"` } // CountResponse 通用计数响应 @@ -288,3 +246,26 @@ type AdminFileDetail struct { RequireAuth bool `json:"require_auth"` UploadType string `json:"upload_type"` } + +// MCPStatusResponse MCP状态响应 +type MCPStatusResponse struct { + Status string `json:"status"` + Config *config.MCPConfig `json:"config"` +} + +// MCPTestResponse MCP连接测试响应 +type MCPTestResponse struct { + MCPStatusResponse +} + +// LogsResponse 日志响应 +type LogsResponse struct { + Logs []string `json:"logs"` + Total int `json:"total"` +} + +// TasksResponse 任务响应 +type TasksResponse struct { + Tasks interface{} `json:"tasks"` // 使用 interface{} 以兼容现有类型 + Total int `json:"total"` +} diff --git a/internal/models/web/admin_config_new.go b/internal/models/web/admin_config_new.go new file mode 100644 index 0000000..abbe182 --- /dev/null +++ b/internal/models/web/admin_config_new.go @@ -0,0 +1,4 @@ +package web + +// NOTE: this file intentionally left minimal. The canonical AdminConfigRequest/Response types +// are defined in admin.go. Keep this file for potential future extensions. diff --git a/internal/models/web/storage.go b/internal/models/web/storage.go index 6986e27..fe9d4dc 100644 --- a/internal/models/web/storage.go +++ b/internal/models/web/storage.go @@ -1,15 +1,17 @@ package web +import "github.com/zy84338719/filecodebox/internal/config" + // StorageInfoResponse 存储信息响应 type StorageInfoResponse struct { - Current string `json:"current"` - Available []string `json:"available"` - StorageDetails map[string]StorageDetail `json:"storage_details"` - StorageConfig map[string]interface{} `json:"storage_config"` + Current string `json:"current"` + Available []string `json:"available"` + StorageDetails map[string]WebStorageDetail `json:"storage_details"` + StorageConfig *config.StorageConfig `json:"storage_config"` } -// StorageDetail 存储详情 -type StorageDetail struct { +// WebStorageDetail Web API 存储详情 +type WebStorageDetail struct { Type string `json:"type"` Available bool `json:"available"` Error string `json:"error,omitempty"` @@ -21,8 +23,8 @@ type StorageDetail struct { // StorageTestRequest 存储测试请求 type StorageTestRequest struct { - Type string `json:"type" binding:"required"` - Config map[string]interface{} `json:"config" binding:"required"` + Type string `json:"type" binding:"required"` + Config *config.StorageConfig `json:"config" binding:"required"` } // StorageTestResponse 存储测试响应 @@ -34,8 +36,8 @@ type StorageTestResponse struct { // StorageSwitchRequest 存储切换请求 type StorageSwitchRequest struct { - Type string `json:"type" binding:"required"` - Config map[string]interface{} `json:"config" binding:"required"` + Type string `json:"type" binding:"required"` + Config *config.StorageConfig `json:"config,omitempty"` } // StorageSwitchResponse 存储切换响应 diff --git a/internal/repository/user.go b/internal/repository/user.go index fc77a2b..ef07f85 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -75,13 +75,15 @@ func (dao *UserDAO) UpdateColumns(id uint, updates map[string]interface{}) error // UpdateUserFields 更新用户字段(结构化方式) func (dao *UserDAO) UpdateUserFields(id uint, user models.User) error { - - userMap := user.ToMap() - if len(userMap) == 0 { - return errors.New("没有需要更新的字段") + // 直接使用结构体进行更新,GORM 会自动处理非零值字段 + result := dao.db.Model(&models.User{}).Where("id = ?", id).Updates(user) + if result.Error != nil { + return result.Error } - - return dao.db.Model(&models.User{}).Where("id = ?", id).Updates(userMap).Error + if result.RowsAffected == 0 { + return errors.New("没有需要更新的字段或用户不存在") + } + return nil } // UpdateUserProfile 更新用户资料(用户自己更新) @@ -90,7 +92,8 @@ func (dao *UserDAO) UpdateUserProfile(id uint, user *models.User) error { return errors.New("用户信息不能为空") } - return dao.db.Model(&models.User{}).Where("id = ?", id).Updates(user.ToMap()).Error + // 直接使用结构体进行更新,GORM 会自动处理非零值字段 + return dao.db.Model(&models.User{}).Where("id = ?", id).Updates(user).Error } // UpdatePassword 更新用户密码 diff --git a/internal/routes/admin.go b/internal/routes/admin.go index 37c53de..724c54a 100644 --- a/internal/routes/admin.go +++ b/internal/routes/admin.go @@ -56,7 +56,7 @@ func SetupAdminRoutes( // 将管理后台静态资源与前端入口注册为公开路由,允许未认证用户加载登录页面和相关静态资源 // 注意:API 路由仍然放在受保护的 authGroup 中 - themeDir := "./" + cfg.ThemesSelect + themeDir := "./" + cfg.UI.ThemesSelect // css adminGroup.GET("/css/*filepath", func(c *gin.Context) { diff --git a/internal/routes/base.go b/internal/routes/base.go index ddb300d..bc42587 100644 --- a/internal/routes/base.go +++ b/internal/routes/base.go @@ -95,7 +95,7 @@ func SetupBaseRoutes(router *gin.Engine, userHandler *handlers.UserHandler, cfg // robots.txt router.GET("/robots.txt", func(c *gin.Context) { c.Header("Content-Type", "text/plain") - c.String(http.StatusOK, cfg.RobotsText) + c.String(http.StatusOK, cfg.UI.RobotsText) }) // 获取配置接口(兼容性保留) @@ -103,14 +103,14 @@ func SetupBaseRoutes(router *gin.Engine, userHandler *handlers.UserHandler, cfg common.SuccessResponse(c, gin.H{ "name": cfg.Base.Name, "description": cfg.Base.Description, - "explain": cfg.PageExplain, + "explain": cfg.UI.PageExplain, "uploadSize": cfg.Transfer.Upload.UploadSize, "expireStyle": cfg.ExpireStyle, "enableChunk": GetEnableChunk(cfg), "openUpload": cfg.Transfer.Upload.OpenUpload, "notify_title": cfg.NotifyTitle, "notify_content": cfg.NotifyContent, - "show_admin_address": cfg.ShowAdminAddr, + "show_admin_address": cfg.UI.ShowAdminAddr, "max_save_seconds": cfg.Transfer.Upload.MaxSaveSeconds, }) }) diff --git a/internal/routes/setup.go b/internal/routes/setup.go index 50ea13e..6898e34 100644 --- a/internal/routes/setup.go +++ b/internal/routes/setup.go @@ -9,6 +9,7 @@ import ( "github.com/zy84338719/filecodebox/internal/config" "github.com/zy84338719/filecodebox/internal/handlers" + "github.com/zy84338719/filecodebox/internal/mcp" "github.com/zy84338719/filecodebox/internal/middleware" "github.com/zy84338719/filecodebox/internal/repository" "github.com/zy84338719/filecodebox/internal/services" @@ -196,6 +197,19 @@ func RegisterDynamicRoutes( chunkService := services.NewChunkService(daoManager, manager, storageService) adminService := services.NewAdminService(daoManager, manager, storageService) + // 重新初始化 MCP 管理器并根据配置启动(确保动态路由注册时MCP管理器可用) + mcpManager := mcp.NewMCPManager(manager, daoManager, storageManager, shareServiceInstance, adminService, userService) + handlers.SetMCPManager(mcpManager) + if manager.MCP.EnableMCPServer == 1 { + if err := mcpManager.StartMCPServer(manager.MCP.MCPPort); err != nil { + logrus.Errorf("启动 MCP 服务器失败: %v", err) + } else { + logrus.Info("MCP 服务器已启动") + } + } else { + logrus.Info("MCP 服务器未启用") + } + // 初始化处理器 shareHandler := handlers.NewShareHandler(shareServiceInstance) chunkHandler := handlers.NewChunkHandler(chunkService) diff --git a/internal/routes/user.go b/internal/routes/user.go index 28588ff..c4a6b43 100644 --- a/internal/routes/user.go +++ b/internal/routes/user.go @@ -69,7 +69,7 @@ func SetupUserAPIRoutes( authGroup.GET("/files", userHandler.GetUserFiles) authGroup.GET("/stats", userHandler.GetUserStats) authGroup.GET("/check-auth", userHandler.CheckAuth) - authGroup.DELETE("/files/:id", userHandler.DeleteFile) + authGroup.DELETE("/files/:code", userHandler.DeleteFile) } } } diff --git a/internal/services/admin/auth.go b/internal/services/admin/auth.go index 6e883bc..d822e65 100644 --- a/internal/services/admin/auth.go +++ b/internal/services/admin/auth.go @@ -56,7 +56,7 @@ func (s *Service) ValidateToken(tokenString string) error { return errors.New("无效的token") } -// GenerateTokenForAdmin 验证管理员用户名/密码并生成管理员JWT(使用 user.JWTSecret 签名) +// GenerateTokenForAdmin 验证管理员用户名/密码并生成 session-based JWT(复用普通用户登录逻辑) func (s *Service) GenerateTokenForAdmin(username, password string) (string, error) { // 查找用户 user, err := s.repositoryManager.User.GetByUsername(username) @@ -74,17 +74,10 @@ func (s *Service) GenerateTokenForAdmin(username, password string) (string, erro return "", fmt.Errorf("认证失败") } - // 创建JWT claims - claims := jwt.MapClaims{ - "is_admin": true, - "user_id": user.ID, - "exp": time.Now().Add(24 * time.Hour).Unix(), - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(s.manager.User.JWTSecret)) + // 使用 authService.CreateUserSession 创建 session-based token,这样能被 userService.ValidateToken 接受 + tokenString, err := s.authService.CreateUserSession(user, "", "admin-login") if err != nil { - return "", fmt.Errorf("生成token失败: %w", err) + return "", fmt.Errorf("创建会话失败: %w", err) } return tokenString, nil diff --git a/internal/services/admin/config.go b/internal/services/admin/config.go index 222a2bd..e4ccc80 100644 --- a/internal/services/admin/config.go +++ b/internal/services/admin/config.go @@ -12,146 +12,175 @@ func (s *Service) GetConfig() *config.ConfigManager { return s.manager } -// UpdateConfig 更新配置 - 使用结构化DTO +// UpdateConfig 更新配置 - 已弃用,保留向后兼容 func (s *Service) UpdateConfig(configData map[string]interface{}) error { - // 过滤掉端口和管理员密码配置,这些不应该通过API更新 - filteredConfigData := make(map[string]interface{}) - for key, value := range configData { - if key == "port" { - continue - } - filteredConfigData[key] = value - } - - // Use nested map directly (no DTO conversion) - return s.SaveConfigUpdate(filteredConfigData) + // 这个方法保留用于向后兼容,但不再建议使用 + // 新代码应该使用 UpdateConfigFromRequest + return fmt.Errorf("deprecated: use UpdateConfigFromRequest instead") } // UpdateConfigFromRequest 从结构化请求更新配置 func (s *Service) UpdateConfigFromRequest(configRequest *web.AdminConfigRequest) error { - // 构建配置更新数据 - configUpdates := make(map[string]interface{}) + // 直接更新配置管理器的各个模块,不使用 map 转换 // 处理基础配置 if configRequest.Base != nil { - base := make(map[string]interface{}) - if configRequest.Base.Name != nil { - base["name"] = *configRequest.Base.Name + baseConfig := configRequest.Base + if baseConfig.Name != "" { + s.manager.Base.Name = baseConfig.Name + } + if baseConfig.Description != "" { + s.manager.Base.Description = baseConfig.Description } - if configRequest.Base.Description != nil { - base["description"] = *configRequest.Base.Description + if baseConfig.Keywords != "" { + s.manager.Base.Keywords = baseConfig.Keywords } - if configRequest.Base.Keywords != nil { - base["keywords"] = *configRequest.Base.Keywords + if baseConfig.Port != 0 { + s.manager.Base.Port = baseConfig.Port } - if len(base) > 0 { - configUpdates["base"] = base + if baseConfig.Host != "" { + s.manager.Base.Host = baseConfig.Host + } + if baseConfig.DataPath != "" { + s.manager.Base.DataPath = baseConfig.DataPath + } + s.manager.Base.Production = baseConfig.Production + } + + // 处理数据库配置 + if configRequest.Database != nil { + dbConfig := configRequest.Database + if dbConfig.Type != "" { + s.manager.Database.Type = dbConfig.Type + } + if dbConfig.Host != "" { + s.manager.Database.Host = dbConfig.Host + } + if dbConfig.Port != 0 { + s.manager.Database.Port = dbConfig.Port + } + if dbConfig.Name != "" { + s.manager.Database.Name = dbConfig.Name + } + if dbConfig.User != "" { + s.manager.Database.User = dbConfig.User + } + if dbConfig.Pass != "" { + s.manager.Database.Pass = dbConfig.Pass + } + if dbConfig.SSL != "" { + s.manager.Database.SSL = dbConfig.SSL } } // 处理传输配置 if configRequest.Transfer != nil { - transfer := make(map[string]interface{}) - if configRequest.Transfer.Upload != nil { - upload := make(map[string]interface{}) uploadConfig := configRequest.Transfer.Upload - if uploadConfig.OpenUpload != nil { - upload["open_upload"] = *uploadConfig.OpenUpload - } - if uploadConfig.UploadSize != nil { - upload["upload_size"] = *uploadConfig.UploadSize - } - if uploadConfig.EnableChunk != nil { - upload["enable_chunk"] = *uploadConfig.EnableChunk - } - if uploadConfig.ChunkSize != nil { - upload["chunk_size"] = *uploadConfig.ChunkSize - } - if uploadConfig.MaxSaveSeconds != nil { - upload["max_save_seconds"] = *uploadConfig.MaxSaveSeconds - } - if len(upload) > 0 { - transfer["upload"] = upload - } + s.manager.Transfer.Upload.OpenUpload = uploadConfig.OpenUpload + s.manager.Transfer.Upload.UploadSize = uploadConfig.UploadSize + s.manager.Transfer.Upload.EnableChunk = uploadConfig.EnableChunk + s.manager.Transfer.Upload.ChunkSize = uploadConfig.ChunkSize + s.manager.Transfer.Upload.MaxSaveSeconds = uploadConfig.MaxSaveSeconds } if configRequest.Transfer.Download != nil { - download := make(map[string]interface{}) downloadConfig := configRequest.Transfer.Download - if downloadConfig.EnableConcurrentDownload != nil { - download["enable_concurrent_download"] = *downloadConfig.EnableConcurrentDownload - } - if downloadConfig.MaxConcurrentDownloads != nil { - download["max_concurrent_downloads"] = *downloadConfig.MaxConcurrentDownloads - } - if downloadConfig.DownloadTimeout != nil { - download["download_timeout"] = *downloadConfig.DownloadTimeout - } - if len(download) > 0 { - transfer["download"] = download - } + s.manager.Transfer.Download.EnableConcurrentDownload = downloadConfig.EnableConcurrentDownload + s.manager.Transfer.Download.MaxConcurrentDownloads = downloadConfig.MaxConcurrentDownloads + s.manager.Transfer.Download.DownloadTimeout = downloadConfig.DownloadTimeout } + } - if len(transfer) > 0 { - configUpdates["transfer"] = transfer + // 处理存储配置 + if configRequest.Storage != nil { + storageConfig := configRequest.Storage + if storageConfig.Type != "" { + s.manager.Storage.Type = storageConfig.Type + } + if storageConfig.StoragePath != "" { + s.manager.Storage.StoragePath = storageConfig.StoragePath + } + if storageConfig.S3 != nil { + s.manager.Storage.S3 = storageConfig.S3 + } + if storageConfig.WebDAV != nil { + s.manager.Storage.WebDAV = storageConfig.WebDAV + } + if storageConfig.OneDrive != nil { + s.manager.Storage.OneDrive = storageConfig.OneDrive + } + if storageConfig.NFS != nil { + s.manager.Storage.NFS = storageConfig.NFS } } - // 处理用户配置 + // 处理用户系统配置 if configRequest.User != nil { - user := make(map[string]interface{}) userConfig := configRequest.User - if userConfig.AllowUserRegistration != nil { - user["allow_user_registration"] = *userConfig.AllowUserRegistration + s.manager.User.AllowUserRegistration = userConfig.AllowUserRegistration + s.manager.User.RequireEmailVerify = userConfig.RequireEmailVerify + if userConfig.UserStorageQuota != 0 { + s.manager.User.UserStorageQuota = userConfig.UserStorageQuota } - if userConfig.RequireEmailVerify != nil { - user["require_email_verify"] = *userConfig.RequireEmailVerify + if userConfig.UserUploadSize != 0 { + s.manager.User.UserUploadSize = userConfig.UserUploadSize } - if userConfig.UserUploadSize != nil { - user["user_upload_size"] = *userConfig.UserUploadSize + if userConfig.SessionExpiryHours != 0 { + s.manager.User.SessionExpiryHours = userConfig.SessionExpiryHours } - if userConfig.UserStorageQuota != nil { - user["user_storage_quota"] = *userConfig.UserStorageQuota + if userConfig.MaxSessionsPerUser != 0 { + s.manager.User.MaxSessionsPerUser = userConfig.MaxSessionsPerUser } - if userConfig.SessionExpiryHours != nil { - user["session_expiry_hours"] = *userConfig.SessionExpiryHours + if userConfig.JWTSecret != "" { + s.manager.User.JWTSecret = userConfig.JWTSecret } - if userConfig.MaxSessionsPerUser != nil { - user["max_sessions_per_user"] = *userConfig.MaxSessionsPerUser - } - if userConfig.JWTSecret != nil { - user["jwt_secret"] = *userConfig.JWTSecret + } + + // 处理 MCP 配置 + if configRequest.MCP != nil { + mcpConfig := configRequest.MCP + s.manager.MCP.EnableMCPServer = mcpConfig.EnableMCPServer + if mcpConfig.MCPPort != "" { + s.manager.MCP.MCPPort = mcpConfig.MCPPort } - if len(user) > 0 { - configUpdates["user"] = user + if mcpConfig.MCPHost != "" { + s.manager.MCP.MCPHost = mcpConfig.MCPHost } } - // 处理其他配置 - if configRequest.NotifyTitle != nil { - configUpdates["notify_title"] = *configRequest.NotifyTitle - } - if configRequest.NotifyContent != nil { - configUpdates["notify_content"] = *configRequest.NotifyContent - } - if configRequest.PageExplain != nil { - configUpdates["page_explain"] = *configRequest.PageExplain - } - if configRequest.Opacity != nil { - configUpdates["opacity"] = *configRequest.Opacity + // 处理 UI 配置 + if configRequest.UI != nil { + uiConfig := configRequest.UI + if uiConfig.ThemesSelect != "" { + s.manager.UI.ThemesSelect = uiConfig.ThemesSelect + } + if uiConfig.Background != "" { + s.manager.UI.Background = uiConfig.Background + } + if uiConfig.PageExplain != "" { + s.manager.UI.PageExplain = uiConfig.PageExplain + } + if uiConfig.RobotsText != "" { + s.manager.UI.RobotsText = uiConfig.RobotsText + } + if uiConfig.ShowAdminAddr != 0 { + s.manager.UI.ShowAdminAddr = uiConfig.ShowAdminAddr + } + if uiConfig.Opacity != 0 { + s.manager.UI.Opacity = uiConfig.Opacity + } } - if configRequest.ThemesSelect != nil { - configUpdates["themes_select"] = *configRequest.ThemesSelect + + // 处理系统运行时字段 + if configRequest.SysStart != nil { + s.manager.SysStart = *configRequest.SysStart } - // 调用原有的更新方法 - return s.UpdateConfig(configUpdates) + // 保存配置到数据库和文件 + return s.manager.Save() } -// flattenConfig 扁平化配置数据 -// flattenConfig removed - not used after refactor - // GetFullConfig 获取完整配置 - 返回配置管理器结构体 func (s *Service) GetFullConfig() *config.ConfigManager { // 直接返回配置管理器的克隆,保护原始配置不被修改 @@ -182,75 +211,3 @@ func (s *Service) ValidateConfig() error { func (s *Service) ReloadConfig() error { return s.manager.ReloadConfig() } - -// DTO conversion removed — use nested map[string]interface{} directly - -// convertFlatDTOToNested removed - -// SaveConfigUpdate 保存配置更新 -func (s *Service) SaveConfigUpdate(configUpdate map[string]interface{}) error { - // Apply structured updates to the ConfigManager modules - if cfgBase, ok := configUpdate["base"].(map[string]interface{}); ok { - if err := s.manager.Base.Update(cfgBase); err != nil { - return fmt.Errorf("更新 base 配置失败: %w", err) - } - } - if cfgTransfer, ok := configUpdate["transfer"].(map[string]interface{}); ok { - if upload, ok2 := cfgTransfer["upload"].(map[string]interface{}); ok2 { - if err := s.manager.Transfer.Upload.Update(upload); err != nil { - return fmt.Errorf("更新 transfer.upload 配置失败: %w", err) - } - } - if download, ok2 := cfgTransfer["download"].(map[string]interface{}); ok2 { - if err := s.manager.Transfer.Download.Update(download); err != nil { - return fmt.Errorf("更新 transfer.download 配置失败: %w", err) - } - } - } - if cfgUser, ok := configUpdate["user"].(map[string]interface{}); ok { - if err := s.manager.User.Update(cfgUser); err != nil { - return fmt.Errorf("更新 user 配置失败: %w", err) - } - } - if cfgMCP, ok := configUpdate["mcp"].(map[string]interface{}); ok { - if err := s.manager.MCP.Update(cfgMCP); err != nil { - return fmt.Errorf("更新 mcp 配置失败: %w", err) - } - } - - // Other top-level fields - if v, ok := configUpdate["notify_title"].(string); ok { - s.manager.NotifyTitle = v - } - if v, ok := configUpdate["notify_content"].(string); ok { - s.manager.NotifyContent = v - } - if v, ok := configUpdate["page_explain"].(string); ok { - s.manager.PageExplain = v - } - if v, ok := configUpdate["opacity"]; ok { - switch t := v.(type) { - case int: - s.manager.Opacity = float64(t) - case int64: - s.manager.Opacity = float64(t) - case float64: - s.manager.Opacity = t - } - } - if v, ok := configUpdate["themes_select"].(string); ok { - s.manager.ThemesSelect = v - } - - // Persist structured config to YAML and reload - if err := s.manager.PersistYAML(); err != nil { - return fmt.Errorf("持久化配置到config.yaml失败: %w", err) - } - if err := s.manager.ReloadConfig(); err != nil { - return fmt.Errorf("热重载配置失败: %w", err) - } - - return nil -} - -// DTO helper functions removed — using map-based updates instead diff --git a/internal/services/admin/maintenance.go b/internal/services/admin/maintenance.go index 1460a13..6af652d 100644 --- a/internal/services/admin/maintenance.go +++ b/internal/services/admin/maintenance.go @@ -6,6 +6,7 @@ import ( "time" "github.com/zy84338719/filecodebox/internal/models" + "github.com/zy84338719/filecodebox/internal/models/service" "github.com/zy84338719/filecodebox/internal/utils" ) @@ -152,35 +153,22 @@ func (s *Service) GetStorageStatus() (*models.StorageStatus, error) { return nil, err } - details := map[string]interface{}{ - "used_storage": totalSize, + // 获取当前存储类型 + storageType := s.manager.Storage.Type + + details := service.AdminStorageDetail{ + UsedSpace: totalSize, + Type: storageType, } - // 根据当前配置尝试附加 path 与使用率信息 - storageType := s.manager.Storage.Type + // 根据当前配置尝试附加使用率信息 switch storageType { case "local": - details["storage_path"] = s.manager.Storage.StoragePath if s.manager.Storage.StoragePath != "" { if usage, err := utils.GetUsagePercent(s.manager.Storage.StoragePath); err == nil { - // 四舍五入到整数 - details["usage_percent"] = int(usage) + details.UsagePercent = usage } } - case "s3": - if s.manager.Storage.S3 != nil { - details["storage_path"] = s.manager.Storage.S3.BucketName - } - case "webdav": - if s.manager.Storage.WebDAV != nil { - details["storage_path"] = s.manager.Storage.WebDAV.Hostname - } - case "nfs": - if s.manager.Storage.NFS != nil { - details["storage_path"] = s.manager.Storage.NFS.MountPoint - } - default: - // Handle other storage types if necessary } return &models.StorageStatus{ diff --git a/internal/static/assets.go b/internal/static/assets.go index e12226d..32e53aa 100644 --- a/internal/static/assets.go +++ b/internal/static/assets.go @@ -13,7 +13,7 @@ import ( // RegisterStaticRoutes registers public-facing static routes (assets, css, js, components) func RegisterStaticRoutes(router *gin.Engine, cfg *config.ConfigManager) { - themeDir := fmt.Sprintf("./%s", cfg.ThemesSelect) + themeDir := fmt.Sprintf("./%s", cfg.UI.ThemesSelect) router.Static("/assets", fmt.Sprintf("%s/assets", themeDir)) router.Static("/css", fmt.Sprintf("%s/css", themeDir)) @@ -29,7 +29,7 @@ func RegisterStaticRoutes(router *gin.Engine, cfg *config.ConfigManager) { // ServeIndex serves the main index page with basic template replacements. func ServeIndex(c *gin.Context, cfg *config.ConfigManager) { - indexPath := filepath.Join(".", cfg.ThemesSelect, "index.html") + indexPath := filepath.Join(".", cfg.UI.ThemesSelect, "index.html") content, err := os.ReadFile(indexPath) if err != nil { @@ -42,14 +42,14 @@ func ServeIndex(c *gin.Context, cfg *config.ConfigManager) { html = strings.ReplaceAll(html, "{{title}}", cfg.Base.Name) html = strings.ReplaceAll(html, "{{description}}", cfg.Base.Description) html = strings.ReplaceAll(html, "{{keywords}}", cfg.Base.Keywords) - html = strings.ReplaceAll(html, "{{page_explain}}", cfg.PageExplain) - html = strings.ReplaceAll(html, "{{opacity}}", fmt.Sprintf("%.1f", cfg.Opacity)) + html = strings.ReplaceAll(html, "{{page_explain}}", cfg.UI.PageExplain) + html = strings.ReplaceAll(html, "{{opacity}}", fmt.Sprintf("%.1f", cfg.UI.Opacity)) html = strings.ReplaceAll(html, "src=\"js/", "src=\"/js/") html = strings.ReplaceAll(html, "href=\"css/", "href=\"/css/") html = strings.ReplaceAll(html, "src=\"assets/", "src=\"/assets/") html = strings.ReplaceAll(html, "href=\"assets/", "href=\"/assets/") html = strings.ReplaceAll(html, "src=\"components/", "src=\"/components/") - html = strings.ReplaceAll(html, "{{background}}", cfg.Background) + html = strings.ReplaceAll(html, "{{background}}", cfg.UI.Background) c.Header("Cache-Control", "no-cache") c.Header("Content-Type", "text/html; charset=utf-8") @@ -58,7 +58,7 @@ func ServeIndex(c *gin.Context, cfg *config.ConfigManager) { // ServeSetup serves the setup page with template replacements. func ServeSetup(c *gin.Context, cfg *config.ConfigManager) { - setupPath := filepath.Join(".", cfg.ThemesSelect, "setup.html") + setupPath := filepath.Join(".", cfg.UI.ThemesSelect, "setup.html") content, err := os.ReadFile(setupPath) if err != nil { @@ -81,7 +81,7 @@ func ServeSetup(c *gin.Context, cfg *config.ConfigManager) { // ServeAdminPage serves the admin index page func ServeAdminPage(c *gin.Context, cfg *config.ConfigManager) { - adminPath := filepath.Join(".", cfg.ThemesSelect, "admin", "index.html") + adminPath := filepath.Join(".", cfg.UI.ThemesSelect, "admin", "index.html") content, err := os.ReadFile(adminPath) if err != nil { @@ -96,7 +96,7 @@ func ServeAdminPage(c *gin.Context, cfg *config.ConfigManager) { // ServeUserPage serves user-facing static pages (login/register/dashboard/etc.) func ServeUserPage(c *gin.Context, cfg *config.ConfigManager, pageName string) { - userPagePath := filepath.Join(".", cfg.ThemesSelect, pageName) + userPagePath := filepath.Join(".", cfg.UI.ThemesSelect, pageName) content, err := os.ReadFile(userPagePath) if err != nil { diff --git a/internal/storage/storage.go b/internal/storage/storage.go index e52d648..9857716 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -38,6 +38,11 @@ func NewStorageManager(manager *config.ConfigManager) *StorageManager { current: manager.Storage.Type, } + // 如果配置中的存储类型为空,默认使用本地存储 + if sm.current == "" { + sm.current = "local" + } + // 创建 PathManager basePath := manager.Storage.StoragePath if basePath == "" { diff --git a/internal/utils/handlers.go b/internal/utils/handlers.go new file mode 100644 index 0000000..ba4275e --- /dev/null +++ b/internal/utils/handlers.go @@ -0,0 +1,161 @@ +package utils + +import ( + "fmt" + "mime/multipart" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/common" +) + +// BindJSONWithValidation 绑定JSON请求并处理验证错误 +func BindJSONWithValidation(c *gin.Context, req interface{}) bool { + if err := c.ShouldBindJSON(req); err != nil { + common.BadRequestResponse(c, "参数错误: "+err.Error()) + return false + } + return true +} + +// ParseUserIDFromParam 从URL参数解析用户ID +func ParseUserIDFromParam(c *gin.Context, paramName string) (uint, bool) { + userIDStr := c.Param(paramName) + userID64, err := strconv.ParseUint(userIDStr, 10, 32) + if err != nil { + common.BadRequestResponse(c, "用户ID错误") + return 0, false + } + return uint(userID64), true +} + +// ParseIntFromParam 从URL参数解析整数 +func ParseIntFromParam(c *gin.Context, paramName string, errorMessage string) (int, bool) { + paramStr := c.Param(paramName) + value, err := strconv.Atoi(paramStr) + if err != nil { + common.BadRequestResponse(c, errorMessage) + return 0, false + } + return value, true +} + +// ParseFileFromForm 从表单解析文件,统一错误处理 +func ParseFileFromForm(c *gin.Context, fieldName string) (*multipart.FileHeader, bool) { + file, err := c.FormFile(fieldName) + if err != nil { + common.BadRequestResponse(c, "文件解析失败: "+err.Error()) + return nil, false + } + return file, true +} + +// ValidateExpireStyle 验证过期样式是否有效 +func ValidateExpireStyle(expireStyle string) bool { + validStyles := []string{"minute", "hour", "day", "week", "month", "year", "forever"} + for _, style := range validStyles { + if style == expireStyle { + return true + } + } + return false +} + +// PaginationParams 分页参数 +type PaginationParams struct { + Page int + PageSize int + Search string +} + +// ParsePaginationParams 解析分页参数 +func ParsePaginationParams(c *gin.Context) PaginationParams { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + search := c.Query("search") + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + return PaginationParams{ + Page: page, + PageSize: pageSize, + Search: search, + } +} + +// ExpireParams 过期参数 +type ExpireParams struct { + ExpireValue int + ExpireStyle string + RequireAuth bool +} + +// ParseExpireParams 解析过期参数(支持POST表单和结构体) +func ParseExpireParams(expireValueStr, expireStyle, requireAuthStr string) (ExpireParams, error) { + expireValue := 1 + if expireValueStr != "" { + var err error + expireValue, err = strconv.Atoi(expireValueStr) + if err != nil { + return ExpireParams{}, fmt.Errorf("过期时间参数错误") + } + } + + // 验证过期值 + if expireValue < 0 || (expireStyle != "forever" && expireValue == 0) { + return ExpireParams{}, fmt.Errorf("过期时间参数错误") + } + + // 默认值处理 + if expireStyle == "" { + expireStyle = "day" + } + + // 验证过期样式 + if !ValidateExpireStyle(expireStyle) { + return ExpireParams{}, fmt.Errorf("过期样式参数错误") + } + + requireAuth := false + if requireAuthStr == "true" { + requireAuth = true + } + + return ExpireParams{ + ExpireValue: expireValue, + ExpireStyle: expireStyle, + RequireAuth: requireAuth, + }, nil +} + +// GetUserIDFromContext 从gin.Context获取用户ID(如果存在) +func GetUserIDFromContext(c *gin.Context) *uint { + if uid, exists := c.Get("user_id"); exists { + id := uid.(uint) + return &id + } + return nil +} + +// GetStringParamRequired 获取必需的字符串参数,如果为空则返回错误 +func GetStringParamRequired(c *gin.Context, paramName, errorMessage string) (string, bool) { + value := c.Param(paramName) + if value == "" { + common.BadRequestResponse(c, errorMessage) + return "", false + } + return value, true +} + +// GetQueryWithDefault 获取查询参数,如果不存在则返回默认值 +func GetQueryWithDefault(c *gin.Context, key, defaultValue string) string { + if value := c.Query(key); value != "" { + return value + } + return defaultValue +} diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..fd6be92 --- /dev/null +++ b/test.txt @@ -0,0 +1,2 @@ +Hello World Test File +This is a test file for upload functionality. \ No newline at end of file diff --git a/themes/2025/admin/index.html b/themes/2025/admin/index.html index bf9e68a..86cc098 100644 --- a/themes/2025/admin/index.html +++ b/themes/2025/admin/index.html @@ -699,12 +699,12 @@

存储配置

-