package api import ( "net/http" "github.com/gin-gonic/gin" "gorm.io/gorm" "fileshare/internal/audit" "fileshare/internal/config" "fileshare/internal/middleware" "fileshare/internal/response" "fileshare/internal/settings" "fileshare/internal/storage" ) // Deps API 层共享依赖(main.go 装配后注入)。 type Deps struct { DB *gorm.DB Cfg *config.Config Mgr *settings.Manager AuditSvc *audit.Service Limiter *middleware.RateLimiter Store *storage.Manager // 26.9:可热切换引擎管理器(实现 Storage 接口) Version string } // jwtSecret 当前 JWT 签名密钥(settings KV 运行时可变)。 func (d *Deps) jwtSecret() string { return d.Mgr.SecretProvider()() } // Register 注册全部 API 路由与前端静态资源回退。 // 业务路由挂根路径(/share /chunk /presign /admin),与审计中间件 // DefaultClassifier 的路由模式一致(t1 冻结契约);公共接口保留 // /api/v1/health 与 /api/v1/config(对齐 t1 骨架)。 func Register(r *gin.Engine, d *Deps) { // —— 公共接口 —— r.GET("/api/v1/health", d.health) r.GET("/api/v1/config", d.publicConfig) // Info3:robotsText 配置键此前无路由承接,补上(内容可在管理端自定义) r.GET("/robots.txt", d.robotsText) // —— 初始化向导(未初始化时唯一可用入口,GuardNotInitialized 白名单)—— registerSetup(r, d) // —— 分享 —— share := r.Group("/share") { share.POST("/text", d.shareText) share.POST("/file", middleware.UploadBandwidthMiddleware(d.Cfg.UploadRate), d.shareFile) // metadata:每次访问即计数(RequireRateLimit=进入检查+完成计数) share.GET("/metadata", d.Limiter.RequireRateLimit(middleware.LimitMeta), d.shareMetadata) share.POST("/metadata", d.Limiter.RequireRateLimit(middleware.LimitMeta), d.shareMetadataPost) share.GET("/select", d.shareSelect) share.POST("/select", d.shareSelectPost) share.GET("/download", d.shareDownload) } // —— 分片上传 —— chunk := r.Group("/chunk") { chunk.POST("/upload/init", d.chunkInit) // 主路径(参考语义):/chunk/upload/{uploadID}/{index}; // 扁平兼容:/chunk/upload + 表单/query 传 upload_id/chunk_index chunk.POST("/upload/:uploadID/:chunkIndex", middleware.UploadBandwidthMiddleware(d.Cfg.UploadRate), d.chunkUpload) chunk.POST("/upload", middleware.UploadBandwidthMiddleware(d.Cfg.UploadRate), d.chunkUploadFlat) chunk.GET("/upload/status/:uploadID", d.chunkStatus) chunk.POST("/upload/complete/:uploadID", d.chunkComplete) chunk.DELETE("/upload/:uploadID", d.chunkCancel) } // —— 预签名直传 —— presign := r.Group("/presign") { presign.POST("/upload/init", d.presignInit) presign.PUT("/upload/proxy/:uploadID", middleware.UploadBandwidthMiddleware(d.Cfg.UploadRate), d.presignProxy) presign.POST("/upload/confirm/:uploadID", d.presignConfirm) presign.GET("/upload/status/:uploadID", d.presignStatus) presign.DELETE("/upload/:uploadID", d.presignCancel) } // —— 管理端(login 公开,其余需管理员 JWT)—— registerAdmin(r, d) // —— 前端静态资源 + SPA 回退(须最后注册)—— registerWeb(r, d) } // health 健康检查(对齐 t1 骨架,保持 /api/v1/health 语义不变)。 func (d *Deps) health(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "code": 200, "msg": "ok", "data": gin.H{ "status": "ok", "version": d.Version, "storage": d.Cfg.Engine(), "time": nowRFC3339(), }, }) } // robotsText 输出管理端可配置的 robots.txt 内容。 func (d *Deps) robotsText(c *gin.Context) { c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(d.Cfg.GetString("robotsText"))) } // publicConfig 公共配置(前端首页/上传页所需;26.9 需求 ①②③④⑩ 扩展): // - 展示字段:站点信息、Logo/favicon、背景图、页脚文案/备案号、通知; // - 策略范围(上传页动态渲染):大小上限、类型白名单、过期方式、保存 // 时间/次数上限、上传频率(仅范围,不含内部实现键)。 // // 敏感键(admin_token/jwt_secret,settings.SensitiveKeys)与本端点无关: // 下发字段为白名单显式构造,任何敏感键均不会出现在响应中。 func (d *Deps) publicConfig(c *gin.Context) { cfg := d.Cfg policy := d.CurrentUploadPolicy() uploadCount := cfg.GetInt("uploadCount") uploadMinute := cfg.GetInt("uploadMinute") // uploadSize 为参考语义的回落上限,单独下发供管理端联动展示 c.JSON(http.StatusOK, gin.H{ "code": 200, "msg": "ok", "data": gin.H{ "config": gin.H{ "name": cfg.SiteName(), "description": cfg.GetString("description"), "explain": cfg.GetString("page_explain"), // 需求 ①:Logo/favicon/背景图 "logo_url": cfg.LogoURL(), "favicon_url": cfg.FaviconURL(), "background_url": cfg.BackgroundURL(), // 需求 ②:页脚自定义内容与备案号 "footer_text": cfg.FooterText(), "footer_beian": cfg.FooterBeian(), // 26.9:当前存储引擎名(仅名称,任何引擎参数/凭据不下发) "storage_engine": d.Store.CurrentName(), "site_domain": d.Cfg.SiteDomain(), // 26.9:上下行带宽字节/秒(公开下发,0=不限速,方便管理端展示当前值) "upload_rate": d.Cfg.UploadRate(), "download_rate": d.Cfg.DownloadRate(), // 需求 ③:系统通知(开关 + 内容,前台右上角悬浮窗) // L7:读取侧再做一次白名单净化,覆盖历史存量与直改库的数据 "notify_enabled": boolToInt(cfg.NotifyEnabled()), "notify_title": cfg.GetString("notify_title"), "notify_content": settings.SanitizeInlineHTML(cfg.GetString("notify_content")), // 策略范围(需求 ④⑩):上传页动态读取并在范围内选择 "uploadSize": cfg.UploadSize(), "max_file_size": policy.MaxFileSize, "maxFileSize": policy.MaxFileSize, "allowedFileTypes": policy.AllowedTypes, "expireStyle": policy.ExpireStyles, "max_save_seconds": policy.MaxSaveSeconds, "maxSaveSeconds": policy.MaxSaveSeconds, "max_save_count": policy.MaxSaveCount, "maxSaveCount": policy.MaxSaveCount, "uploadCount": uploadCount, "uploadMinute": uploadMinute, "enableChunk": cfg.EnableChunk(), "openUpload": cfg.OpenUpload(), }, "meta": gin.H{ "version": d.Version, "features": gin.H{ "chunkUpload": cfg.EnableChunk(), "guestUpload": cfg.OpenUpload(), }, }, }, }) } // requireShareLogin 分享上传权限(对齐参考 share_required_login): // openUpload 开启时游客可传;关闭时要求管理员 Bearer token(403)。 func (d *Deps) requireShareLogin(c *gin.Context) bool { if d.Cfg.OpenUpload() { return true } header := c.GetHeader("Authorization") const prefix = "Bearer " if len(header) <= len(prefix) || header[:len(prefix)] != prefix { response.Fail(c, http.StatusForbidden, "本站未开启游客上传,如需上传请先登录后台") return false } token := header[len(prefix):] if _, err := middleware.VerifyAdminToken(d.jwtSecret(), token); err != nil { response.Fail(c, http.StatusForbidden, "本站未开启游客上传,如需上传请先登录后台") return false } return true }