file-storage/
├── main.go # 程序入口
├── config/ # 配置模块
│ ├── minio.go # MinIO客户端配置
│ ├── redis.go # Redis连接配置
│ ├── prometheus.go # Prometheus指标注册
│ └── auth.go # JWT密钥与RBAC配置
├── handlers/ # HTTP请求处理器
│ ├── upload.go # 文件上传接口
│ ├── download.go # 文件下载接口
│ ├── meta.go # 元数据查询接口
│ └── cdn.go # CDN缓存刷新接口
├── services/ # 核心业务逻辑
│ ├── storage.go # 存储服务(分片上传/下载)
│ ├── cdn.go # CDN签名URL生成
│ ├── auth.go # 权限校验服务
│ └── rate_limit.go # 限流服务实现
├── models/ # 数据模型
│ ├── file.go # 文件元数据模型
│ └── user.go # 用户模型
├── middleware/ # 中间件
│ ├── auth.go # 鉴权中间件
│ ├── rate_limiter.go # 限流中间件
│ └── tracing.go # 链路追踪中间件
├── utils/ # 工具类
│ ├── hash.go # 文件哈希计算
│ ├── cdn.go # CDN缓存刷新工具
│ └── jwt.go # JWT工具类
├── docker-compose.yml # 容器化部署配置
└── prometheus/ # 监控配置
└── prometheus.yml
config/minio.go
package config
import (
"context"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var MinioClient *minio.Client
func InitMinIO() {
endpoints := []string{"minio1:9000", "minio2:9000", "minio3:9000"}
accessKey := "AKIAIOSFODNN7EXAMPLE"
secretKey := "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
// 集群模式初始化
client, err := minio.New(endpoints[0], &minio.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
Secure: true, // 生产环境启用HTTPS
})
if err != nil {
panic("MinIO集群连接失败: " + err.Error())
}
// 检查存储桶是否存在,不存在则创建
exists, err := client.BucketExists(context.Background(), "uploads")
if err != nil || !exists {
if err := client.MakeBucket(context.Background(), "uploads"); err != nil {
panic("存储桶创建失败: " + err.Error())
}
}
MinioClient = client
}
config/redis.go
package config
import (
"context"
"github.com/go-redis/redis/v8"
)
var RedisClient *redis.Client
func InitRedis() {
RedisClient = redis.NewClient(&redis.Options{
Addr: "redis:6379",
Password: "", // 生产环境应配置密码
DB: 0,
})
if _, err := RedisClient.Ping(context.Background()).Result(); err != nil {
panic("Redis连接失败: " + err.Error())
}
}
config/prometheus.go
package config
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
FileUploadCounter = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "file_uploads_total",
Help: "Total number of file uploads",
},
[]string{"status"},
)
)
func InitPrometheus() {
// 默认指标自动注册
}
config/auth.go
package config
var (
JWTSecretKey = []byte("your-256-bit-secure-key")
TokenExpires = 2 * time.Hour
RBACPolicy = map[string][]string{
"admin": {"upload", "delete", "manage"},
"user": {"upload", "download"},
"guest": {"download"},
}
)
handlers/upload.go
package handlers
import (
"file-storage/services"
"github.com/gin-gonic/gin"
"net/http"
)
func UploadFile(c *gin.Context) {
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效文件"})
return
}
defer file.Close()
// 权限校验
user := c.MustGet("user").(models.User)
if !services.CheckPermission(c, user, "upload") {
c.JSON(http.StatusForbidden, gin.H{"error": "无上传权限"})
return
}
// 分片上传
fileID, err := services.UploadToStorage(c, file, header.Filename)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "上传失败"})
return
}
c.JSON(http.StatusOK, gin.H{"id": fileID})
}
handlers/download.go
package handlers
import (
"file-storage/services"
"github.com/gin-gonic/gin"
"net/http"
)
func DownloadFile(c *gin.Context) {
fileID := c.Param("id")
meta, err := services.GetFileMeta(fileID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
return
}
// 权限校验
user := c.MustGet("user").(models.User)
if meta.IsPrivate && meta.OwnerID != user.ID {
c.JSON(http.StatusForbidden, gin.H{"error": "无访问权限"})
return
}
// 获取文件流
reader, err := services.GetFileStream(c, fileID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "文件获取失败"})
return
}
defer reader.Close()
// 流式传输
c.Header("Content-Type", meta.MimeType)
c.Header("Content-Disposition", "attachment; filename="+meta.Name)
io.Copy(c.Writer, reader)
}
handlers/meta.go
package handlers
import (
"file-storage/services"
"github.com/gin-gonic/gin"
"net/http"
)
func GetFileMeta(c *gin.Context) {
fileID := c.Param("id")
meta, err := services.GetFileMeta(fileID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
return
}
c.JSON(http.StatusOK, meta)
}
handlers/cdn.go
package handlers
import (
"file-storage/utils"
"github.com/gin-gonic/gin"
"net/http"
)
func RefreshCDN(c *gin.Context) {
var fileIDs []string
if err := c.BindJSON(&fileIDs); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
if err := utils.PurgeCDNCache(fileIDs); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "CDN刷新失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "刷新请求已提交"})
}
services/storage.go
package services
import (
"context"
"file-storage/config"
"file-storage/models"
"file-storage/utils"
"github.com/minio/minio-go/v7"
"io"
"time"
)
func UploadToStorage(ctx context.Context, file io.Reader, filename string) (string, error) {
fileID := utils.GenerateFileID(filename)
hash, err := utils.CalculateFileHash(file)
if err != nil {
return "", err
}
// 存储元数据
meta := models.FileMeta{
ID: fileID,
Name: filename,
Hash: hash,
CreatedAt: time.Now(),
OwnerID: ctx.Value("userID").(uint),
}
if err := config.RedisClient.HSet(ctx, "file_meta:"+fileID, meta).Err(); err != nil {
return "", err
}
// 分片上传
_, err = config.MinioClient.PutObject(
ctx,
"uploads",
fileID,
file,
-1,
minio.PutObjectOptions{
PartSize: 5 << 20, // 5MB分片
Concurrency: 3, // 并发分片数
},
)
return fileID, err
}
services/cdn.go
package services
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)
func GenerateSignedURL(fileID string, expires time.Duration) string {
secret := "your-cdn-secret-key"
expireTime := time.Now().Add(expires).Unix()
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(fmt.Sprintf("%s:%d", fileID, expireTime)))
signature := hex.EncodeToString(mac.Sum(nil))
return fmt.Sprintf(
"https://cdn.example.com/%s?exp=%d&sig=%s",
fileID,
expireTime,
signature,
)
}
services/auth.go
package services
import (
"context"
"file-storage/config"
"file-storage/models"
)
func CheckPermission(ctx context.Context, user models.User, action string) bool {
// 获取用户角色权限
allowedActions := config.RBACPolicy[user.Role]
for _, a := range allowedActions {
if a == action {
return true
}
}
return false
}
services/rate_limit.go
package services
import (
"context"
"file-storage/config"
"fmt"
"strconv"
"time"
)
func AllowRequest(ctx context.Context, key string, limit int, window time.Duration) bool {
now := time.Now().UnixNano()
windowNano := window.Nanoseconds()
clearBefore := now - windowNano
// 清理过期请求记录
config.RedisClient.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(clearBefore, 10))
// 检查当前请求数
count, _ := config.RedisClient.ZCard(ctx, key).Uint64()
if count >= uint64(limit) {
return false
}
// 添加当前请求
config.RedisClient.ZAdd(ctx, key, &redis.Z{
Score: float64(now),
Member: now,
})
return true
}
models/file.go
package models
import "time"
type FileMeta struct {
ID string `json:"id" redis:"id"`
Name string `json:"name" redis:"name"`
Size int64 `json:"size" redis:"size"`
Hash string `json:"hash" redis:"hash"`
MimeType string `json:"mime_type" redis:"mime_type"`
IsPrivate bool `json:"is_private" redis:"is_private"`
OwnerID uint `json:"owner_id" redis:"owner_id"`
CreatedAt time.Time `json:"created_at" redis:"created_at"`
}
models/user.go
package models
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"unique"`
Password string `json:"-"`
Role string `json:"role"` // 用户角色(admin/user/guest)
Bucket string `json:"bucket"` // 私有存储桶名称
}
middleware/auth.go
package middleware
import (
"file-storage/config"
"file-storage/utils"
"github.com/gin-gonic/gin"
"strings"
)
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "未提供访问令牌"})
return
}
claims, err := utils.ParseJWT(token)
if err != nil {
c.AbortWithStatusJSON(403, gin.H{"error": "无效令牌"})
return
}
c.Set("userID", claims.UserID)
c.Set("roles", claims.Roles)
c.Next()
}
}
middleware/rate_limiter.go
package middleware
import (
"file-storage/services"
"github.com/gin-gonic/gin"
)
func RateLimiter() gin.HandlerFunc {
return func(c *gin.Context) {
var key string
if userID, exists := c.Get("userID"); exists {
key = "user:" + string(userID.(uint))
} else {
key = "ip:" + c.ClientIP()
}
if !services.AllowRequest(c.Request.Context(), key, 100, time.Minute) {
c.AbortWithStatusJSON(429, gin.H{"error": "请求过于频繁"})
return
}
c.Next()
}
}
utils/cdn.go
package utils
import (
"bytes"
"encoding/json"
"net/http"
)
func PurgeCDNCache(fileIDs []string) error {
payload := map[string]interface{}{"files": fileIDs}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest(
"POST",
"https://cdn-api.example.com/purge",
bytes.NewReader(body),
)
req.Header.Set("Authorization", "Bearer your-cdn-api-token")
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
return fmt.Errorf("CDN刷新失败: %v", err)
}
return nil
}
完整覆盖17个文件:
安全增强:
// JWT生成示例
claims := utils.Claims{UserID: user.ID, Roles: user.Roles}
token, _ := utils.GenerateJWT(claims)
性能优化:
minio.PutObjectOptions{
PartSize: 5 << 20, // 5MB
Concurrency: 3, // 并发数
}
可观测性:
# prometheus.yml
- job_name: 'file-storage'
static_configs:
- targets: ['storage:8080']
- job_name: 'minio'
static_configs:
- targets: ['minio:9000']
灾备设计:
# 设置存储桶复制策略
mc replicate add minio/primary-bucket --remote-bucket minio-secondary/backup-bucket