亮点 - 26.9 带宽限速(upload_rate/download_rate,字节/秒,0=不限速;管理端立即生效) - middleware/bandwidth.go 时间窗对齐 sleep 算法 + 单元测试(400KB@100KB/s 4.00s 精度) - 下载:serveFile 包裹 storage.ReadCloser(统一覆盖 local/webdav/s3 代理下载) - 上传:UploadBandwidthMiddleware 包裹 Request.Body(shareFile/chunk/presign proxy) - S3 预签名直传不可服务端限速——UI/文档明示 - 后台 SettingsView 增加 MB/s 友好输入;i18n zh-CN/en-US 双语 - 26.9 文档:新增 docs/api/13-bandwidth.md 专题;10-config 指针;00-overview changelog 与限流表带宽行;openapi.yaml 三处 schema + description 更新 - README.md / web/README.md / server/README.md / deploy/README.md 全部覆盖 品牌(26.9 收尾) - 文件快递柜 → 文件快传(前端/后端默认值/文档/产物/运行 KV) - 「复制取件码」按钮删除;取件码块点击即复制(保持原尺寸) - 「复制链接」→「复制链接和提取码」(一并复制链接和提取码) 产品修复(26.9) - 文本分享 Content-Type text/plain + urlencoded body:前端显式声明 urlencoded 头根治; 后端 bindJSONOrForm 兜底兼容 text/plain + JSON/urlencoded 嗅探 - Docker 部署文档校对到 26.9 现状(热切换 + 端口/卷/健康检查)
191 lines
7.3 KiB
Go
191 lines
7.3 KiB
Go
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 // v3:可热切换引擎管理器(实现 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 公共配置(前端首页/上传页所需;v2 需求 ①②③④⑩ 扩展):
|
||
// - 展示字段:站点信息、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(),
|
||
// v3:当前存储引擎名(仅名称,任何引擎参数/凭据不下发)
|
||
"storage_engine": d.Store.CurrentName(),
|
||
"site_domain": d.Cfg.SiteDomain(),
|
||
// v3.2:上下行带宽字节/秒(公开下发,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
|
||
}
|