package controller import ( "fmt" "net/http" "net/url" "path/filepath" "github.com/gin-gonic/gin" "filestoragesystem/internal/middleware" "filestoragesystem/internal/repository" "filestoragesystem/internal/utils" ) // FileController 文件控制器 type FileController struct { *Base } // NewFileController 创建文件控制器 func NewFileController(b *Base) *FileController { return &FileController{Base: b} } // Upload 上传文件 func (ctl *FileController) Upload(c *gin.Context) { fh, err := c.FormFile("file") if err != nil { utils.FailMsg(c, "缺少上传文件字段file") return } userID := middleware.CurrentUserID(c) f, err := ctl.svc.File.Upload(c, userID, fh) if err != nil { ctl.svc.RecordOpLog(ctl.opCtx(c), "upload", "file", 0, fh.Filename, false, err.Error()) utils.FailMsg(c, err.Error()) return } ctl.svc.RecordOpLog(ctl.opCtx(c), "upload", "file", f.ID, f.Filename, true, "") utils.OK(c, f) } // List 文件列表(支持project_id/filename/prefix/trash过滤) func (ctl *FileController) List(c *gin.Context) { userID := middleware.CurrentUserID(c) page, pageSize := utils.ParsePage(c) filter := repository.FileListFilter{ ProjectID: parseUintQuery(c, "project_id"), Filename: c.Query("filename"), Prefix: utils.SanitizePath(c.Query("prefix")), Trashed: c.Query("trash") == "true", Page: page, PageSize: pageSize, } if v := parseUintQuery(c, "visibility"); v > 0 { filter.Visibility = int8(v) } files, total, err := ctl.svc.File.List(userID, middleware.IsAdmin(c), filter) if err != nil { utils.ServerError(c, err.Error()) return } utils.OKPage(c, files, total, page, pageSize) } // GetInfo 获取文件信息 func (ctl *FileController) GetInfo(c *gin.Context) { id, ok := paramID(c) if !ok { return } userID := middleware.CurrentUserID(c) f, err := ctl.svc.File.GetInfo(userID, id, middleware.IsAdmin(c)) if err != nil { utils.FailMsg(c, err.Error()) return } utils.OK(c, f) } // Download 下载文件 func (ctl *FileController) Download(c *gin.Context) { id, ok := paramID(c) if !ok { return } userID := middleware.CurrentUserID(c) f, fp, err := ctl.svc.File.Open(c, userID, id, middleware.IsAdmin(c)) if err != nil { utils.FailMsg(c, err.Error()) return } defer fp.Close() ctl.svc.File.RecordDownload(c, f) ctl.svc.RecordOpLog(ctl.opCtx(c), "download", "file", f.ID, f.Filename, true, "") c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename*=UTF-8''%s`, url.PathEscape(filepath.Base(f.Filename)))) c.Header("Content-Type", f.MimeType) http.ServeContent(c.Writer, c.Request, filepath.Base(f.Filename), f.UpdatedAt, fp) } // Delete 删除文件(permanent=true硬删除,否则进入回收站) func (ctl *FileController) Delete(c *gin.Context) { id, ok := paramID(c) if !ok { return } userID := middleware.CurrentUserID(c) permanent := c.Query("permanent") == "true" f, _ := ctl.svc.File.GetInfo(userID, id, middleware.IsAdmin(c)) if err := ctl.svc.File.Delete(userID, id, middleware.IsAdmin(c), permanent); err != nil { utils.FailMsg(c, err.Error()) return } name := "" if f != nil { name = f.Filename } ctl.svc.RecordOpLog(ctl.opCtx(c), "delete", "file", id, name, true, "") if permanent { utils.OKMsg(c, "文件已彻底删除") } else { utils.OKMsg(c, "文件已移入回收站") } } // Restore 恢复回收站文件 func (ctl *FileController) Restore(c *gin.Context) { id, ok := paramID(c) if !ok { return } userID := middleware.CurrentUserID(c) if err := ctl.svc.File.Restore(userID, id, middleware.IsAdmin(c)); err != nil { utils.FailMsg(c, err.Error()) return } ctl.svc.RecordOpLog(ctl.opCtx(c), "update", "file", id, "restore", true, "") utils.OKMsg(c, "文件已恢复") } type tempLinkReq struct { Password string `json:"password"` MaxCount int `json:"max_count"` ExpiresIn int64 `json:"expires_in"` // 秒,0使用默认 } // CreateTempLink 生成临时访问链接 func (ctl *FileController) CreateTempLink(c *gin.Context) { id, ok := paramID(c) if !ok { return } var req tempLinkReq if !bindJSON(c, &req) { return } userID := middleware.CurrentUserID(c) link, err := ctl.svc.File.CreateTempLink(userID, id, middleware.IsAdmin(c), req.Password, req.MaxCount, req.ExpiresIn) if err != nil { utils.FailMsg(c, err.Error()) return } ctl.svc.RecordOpLog(ctl.opCtx(c), "create", "file", id, "temp_link", true, "") utils.OK(c, gin.H{ "token": link.Token, "url": "/api/v1/files/temp/" + link.Token, "expires_at": link.ExpiresAt, "has_password": link.HasPwd, "max_count": link.MaxCount, }) } // TempAccess 通过临时链接访问文件(公开接口,支持password参数) func (ctl *FileController) TempAccess(c *gin.Context) { token := c.Param("token") password := c.Query("password") if password == "" { password = c.GetHeader("X-Link-Password") } f, fp, err := ctl.svc.File.ResolveTempLink(token, password) if err != nil { utils.FailMsg(c, err.Error()) return } defer fp.Close() ctl.svc.File.RecordDownload(c, f) c.Header("Content-Disposition", fmt.Sprintf(`inline; filename*=UTF-8''%s`, url.PathEscape(filepath.Base(f.Filename)))) c.Header("Content-Type", f.MimeType) http.ServeContent(c.Writer, c.Request, filepath.Base(f.Filename), f.UpdatedAt, fp) }