v3.2:上下行带宽限速 + 品牌更新 + 产品修复
Release 镜像 / 测试(推送前置门禁) (push) Successful in 45s
Release 镜像 / 多架构构建并推送 ACR (push) Successful in 1m40s

亮点
- v3.2 带宽限速(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 双语
- v3.2 文档:新增 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 全部覆盖

品牌(v3.1 收尾)
- 文件快递柜 → 文件快传(前端/后端默认值/文档/产物/运行 KV)
- 「复制取件码」按钮删除;取件码块点击即复制(保持原尺寸)
- 「复制链接」→「复制链接和提取码」(一并复制链接和提取码)

产品修复(v3.1.1)
- 文本分享 Content-Type text/plain + urlencoded body:前端显式声明 urlencoded 头根治;
  后端 bindJSONOrForm 兜底兼容 text/plain + JSON/urlencoded 嗅探
- Docker 部署文档校对到 v3.1 现状(热切换 + 端口/卷/健康检查)
This commit is contained in:
2026-09-06 10:20:04 +08:00
parent 113c514531
commit 157db0e9e5
74 changed files with 9405 additions and 8 deletions
+103
View File
@@ -0,0 +1,103 @@
package middleware
// v3.2:上传/下载带宽限速(字节/秒,0=不限速)。
//
// 设计要点:
// - 令牌桶(token bucket):每 Read 计算自上次起累计可消费字节,
// 未达上限则阻塞至令牌补足;保证长期平均速率,瞬时按桶容量突发。
// - 桶容量 = rate(1 秒突发量):简单且足够友好;后续若要更细粒度可参数化。
// - 0 速率直接透传,不引入任何开销(性能基线保持)。
// - 对 gin 中间件"上传限速":包裹 c.Request.Body,使 multipart 解析/单分片读取
// 都被天然节流;下载侧由 api 层在 storage.Open 之后包裹 ReadCloser。
import (
"io"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"fileshare/internal/response"
)
// rateLimitedReader 按 bytes/sec 限速的 io.Reader(基于时间窗的精确调度)。
// 实现:累计已读字节,按 rate 推算"应到达时间";超前则 sleep 补齐。
// - rate <= 0 时退化为直通;
// - 长期速率严格 ≤ rate,瞬时按调用方 Read 块大小自然突发;
// - 并发安全(每次请求持独立实例即可,但保留锁便于复用)。
type rateLimitedReader struct {
r io.Reader
rate int // 字节/秒
start time.Time // 起始时间(首 Read 时记)
bytes int64 // 累计已读字节
mu sync.Mutex
}
// NewRateLimitedReader 构造限速 readerrate<=0 返回原始 reader。
func NewRateLimitedReader(r io.Reader, rate int) io.Reader {
if r == nil || rate <= 0 {
return r
}
return &rateLimitedReader{r: r, rate: rate}
}
// Read 限速:底层 Read 后若已超时应到达时间,sleep 补齐;错误/EOF 透传。
func (rl *rateLimitedReader) Read(p []byte) (int, error) {
if rl.rate <= 0 {
return rl.r.Read(p)
}
n, err := rl.r.Read(p)
if n <= 0 {
return n, err
}
rl.mu.Lock()
if rl.start.IsZero() {
rl.start = time.Now()
}
rl.bytes += int64(n)
// 已读 n 字节的"应到达时间" = start + bytes/rate
expected := rl.start.Add(time.Duration(float64(time.Second) * float64(rl.bytes) / float64(rl.rate)))
now := time.Now()
rl.mu.Unlock()
if now.Before(expected) {
time.Sleep(expected.Sub(now))
}
return n, err
}
// UploadBandwidthMiddleware 限速上传请求体(包裹 c.Request.Body)。
// rateProvider 每次请求动态读取最新配置(管理端改 KV 后立即生效)。
func UploadBandwidthMiddleware(rateProvider func() int) gin.HandlerFunc {
return func(c *gin.Context) {
rate := 0
if rateProvider != nil {
rate = rateProvider()
}
if rate > 0 && c.Request != nil && c.Request.Body != nil {
c.Request.Body = WrapReadCloser(c.Request.Body, rate)
}
c.Next()
}
}
// WrapReadCloser 包裹任意 io.ReadCloser 为限速版本(io.Copy 等下游使用)。
func WrapReadCloser(rc io.ReadCloser, rate int) io.ReadCloser {
if rc == nil || rate <= 0 {
return rc
}
return &rateLimitedReadCloser{rc: rc, rl: &rateLimitedReader{r: rc, rate: rate}}
}
type rateLimitedReadCloser struct {
rc io.ReadCloser
rl *rateLimitedReader
}
func (r *rateLimitedReadCloser) Read(p []byte) (int, error) { return r.rl.Read(p) }
func (r *rateLimitedReadCloser) Close() error { return r.rc.Close() }
// DownloadRateReporter 限速命中提示:超长下载时可选上报。
// 当前仅占位——真正接入可在 audit 中按"下载耗时/字节比"标记慢速,但用户暂不需要。
var _ = response.Fail // 保留 response 包以备后续扩展
var _ = http.StatusOK
@@ -0,0 +1,81 @@
package middleware
import (
"bytes"
"io"
"testing"
"time"
)
// TestRateLimitedReaderAtRate 验证长读大致符合设定速率。
// 设 100 KB/s,读取 400 KB 总字节,期望耗时约 4s(±1s 容忍)。
func TestRateLimitedReaderAtRate(t *testing.T) {
const rate = 100 * 1024
const total = 400 * 1024
src := io.LimitReader(bytes.NewReader(make([]byte, total+1024)), total)
rl := NewRateLimitedReader(src, rate).(*rateLimitedReader)
buf := make([]byte, 32*1024) // 32KB 块
start := time.Now()
read := 0
for {
n, err := rl.Read(buf)
read += n
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
}
elapsed := time.Since(start)
want := time.Duration(float64(time.Second) * float64(total) / float64(rate))
if elapsed < want-time.Second {
t.Fatalf("读取过快 elapsed=%v want>=%v", elapsed, want)
}
if elapsed > want+1500*time.Millisecond {
t.Fatalf("读取过慢 elapsed=%v want<=%v", elapsed, want+1500*time.Millisecond)
}
if read != total {
t.Fatalf("读到 %d 字节,期望 %d", read, total)
}
}
// TestRateLimitedReaderZeroPassthrough 速率 0 时不应引入任何延迟/包封。
func TestRateLimitedReaderZeroPassthrough(t *testing.T) {
src := bytes.NewReader([]byte("hello"))
rl := NewRateLimitedReader(src, 0)
if rl == src {
// 透传:返回原 reader
} else if _, ok := rl.(*rateLimitedReader); ok {
// 0 速率时按实现可走 enabled=false(不退化亦可)
}
// 关键:必须能读完
b, err := io.ReadAll(rl)
if err != nil || string(b) != "hello" {
t.Fatalf("0 速率透传失败: %q %v", b, err)
}
}
// TestWrapReadCloserClose 验证包裹后 Close 透传到底层。
func TestWrapReadCloserClose(t *testing.T) {
src := &closeCount{Reader: bytes.NewReader([]byte("xyz")), closed: 0}
rc := WrapReadCloser(src, 50*1024)
if rc == nil {
t.Fatal("WrapReadCloser nil")
}
_, _ = io.ReadAll(rc)
if err := rc.Close(); err != nil {
t.Fatalf("close err: %v", err)
}
if src.closed != 1 {
t.Fatalf("底层 Close 未被调用: %d", src.closed)
}
}
type closeCount struct {
io.Reader
closed int
}
func (c *closeCount) Close() error { c.closed++; return nil }