package file import ( "context" "errors" "net/http" "strings" "hfb_sys/backend/internal/logging" "hfb_sys/backend/pkg/response" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type Handler struct { service *Service storage *Storage objectAuthorizer ObjectAuthorizer uploadOwnerRecorder UploadOwnerRecorder } // ObjectAuthorizer 校验用户是否可读取指定私有对象。 type ObjectAuthorizer func(ctx context.Context, userID uint64, key string) (bool, error) // UploadOwnerRecorder 保存用户上传私有文件的归属。 type UploadOwnerRecorder func(ctx context.Context, userID uint64, objectKeys []string) error func NewHandler(service *Service, storage *Storage, objectAuthorizer ObjectAuthorizer, uploadOwnerRecorder UploadOwnerRecorder) *Handler { return &Handler{ service: service, storage: storage, objectAuthorizer: objectAuthorizer, uploadOwnerRecorder: uploadOwnerRecorder, } } func (h *Handler) Upload(c *gin.Context) { header, err := c.FormFile("file") if err != nil { response.BadRequest(c, "请选择要上传的文件") return } reader, err := header.Open() if err != nil { response.BadRequest(c, "文件读取失败") return } defer func() { _ = reader.Close() }() item, err := h.service.Upload(uploadRequest{ Context: c.Request.Context(), Scene: c.PostForm("scene"), Header: header, Reader: reader, ContentType: header.Header.Get("Content-Type"), }) if err != nil { writeFileError(c, err) return } if userID, ok := c.Get("user_id"); ok && h.uploadOwnerRecorder != nil { id, valid := userID.(uint64) if !valid || h.uploadOwnerRecorder(c.Request.Context(), id, item.ObjectKeys()) != nil { // 归属记录失败时补偿删除刚写入的对象,避免用户重试产生孤儿文件。 if h.storage != nil { if removeErr := h.storage.RemoveObjects(c.Request.Context(), item.ObjectKeys()); removeErr != nil { logging.FromContext(c.Request.Context()).Warn("补偿删除上传对象失败", zap.Error(removeErr)) } } response.ServiceUnavailable(c, "文件归属记录失败") return } } response.Created(c, item) } func (h *Handler) Object(c *gin.Context) { h.writeObject(c, false) } func (h *Handler) PublicObject(c *gin.Context) { h.writeObject(c, true) } func (h *Handler) writeObject(c *gin.Context, publicOnly bool) { if h.storage == nil { response.ServiceUnavailable(c, "文件存储未连接") return } key := strings.TrimSpace(c.Query("key")) if key == "" || strings.Contains(key, "..") { response.BadRequest(c, "文件 key 不正确") return } if publicOnly && !strings.HasPrefix(key, "home-banner/") && !strings.HasPrefix(key, "avatar/") && !strings.HasPrefix(key, "announcement/") && !strings.HasPrefix(key, "mohong/") && !strings.HasPrefix(key, "crash/") && !strings.HasPrefix(key, "aw-recycle/") && !strings.HasPrefix(key, "cooperation-feedback/") { response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问") return } if !publicOnly { if _, isAdmin := c.Get("admin_id"); !isAdmin { userID, ok := c.Get("user_id") if !ok { response.Unauthorized(c, "缺少用户上下文") return } id, ok := userID.(uint64) if !ok || h.objectAuthorizer == nil { response.Error(c, http.StatusForbidden, "forbidden", "无权访问该文件") return } allowed, err := h.objectAuthorizer(c.Request.Context(), id, key) if err != nil { response.ServiceUnavailable(c, "文件权限校验失败") return } if !allowed { response.Error(c, http.StatusForbidden, "forbidden", "无权访问该文件") return } } } object, err := h.storage.Get(c.Request.Context(), key) if err != nil { response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问") return } defer func() { _ = object.Reader.Close() }() contentType := object.ContentType if contentType == "" { contentType = "application/octet-stream" } c.Header("Content-Type", contentType) c.Header("Cache-Control", "private, max-age=300") c.DataFromReader(http.StatusOK, object.Size, contentType, object.Reader, nil) } func writeFileError(c *gin.Context, err error) { response.RecordError(c, err) switch { case errors.Is(err, ErrDependencyUnavailable): response.ServiceUnavailable(c, "文件存储未连接") case errors.Is(err, ErrInvalidFile): response.BadRequest(c, "文件不符合规则,仅支持 10MB 内的 JPG、PNG、WebP 或 PDF") default: response.Error(c, http.StatusInternalServerError, "internal_error", "文件服务暂时不可用") } }