Files
OpenFlare/backend/plugins/domain/upload/handler/file_management.go
T
ryan 37ad58699d autoresearch iter 2: compare error sentinels with errors.Is
Five sites used == against sentinels (redis.Nil, ingest.ErrForbidden,
errs.ErrDatabaseUninitialized). The neighbouring not-found checks already
went through errors.Is helpers, so a wrapped error would silently downgrade
a 403 to a 400 and a 500 to a 400.
2026-08-29 07:58:59 +08:00

301 lines
9.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package handler
import (
"Wavelet/core/contracts"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/response"
"Wavelet/plugins/domain/upload/ingest"
"Wavelet/plugins/domain/upload/models"
"Wavelet/plugins/domain/upload/repository"
"Wavelet/plugins/domain/upload/shared"
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
uploadstorage "Wavelet/plugins/domain/upload/storage"
)
type listFilesRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
Keyword string `form:"keyword"`
Type string `form:"type"`
Extension string `form:"extension"`
UserID uint64 `form:"user_id"`
}
type listFilesResponse struct {
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Items []models.Upload `json:"items"`
}
// ListFiles 获取系统上传的文件列表
// @Summary 获取文件列表
// @Description 分页获取系统上传的文件列表,支持文件名关键词、业务类型、扩展名、上传用户ID过滤
// @Tags admin
// @Produce json
// @Param page query int false "页码(默认 1)"
// @Param page_size query int false "每页数量(默认 20,最大 100)"
// @Param keyword query string false "文件名关键词(模糊匹配)"
// @Param type query string false "业务分类过滤"
// @Param extension query string false "扩展名过滤"
// @Param user_id query int false "上传用户 ID 过滤"
// @Security SessionCookie
// @Success 200 {object} response.Any{data=listFilesResponse} "查询成功"
// @Failure 400 {object} response.Any "参数错误"
// @Router /api/v1/admin/uploads/files [get]
func ListFiles(c *gin.Context) {
ctx := c.Request.Context()
var req listFilesRequest
if err := c.ShouldBindQuery(&req); err != nil {
response.AbortBadRequest(c, shared.ErrInvalidParams)
return
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 || req.PageSize > 100 {
req.PageSize = 20
}
total, items, err := listUploadFiles(ctx, repository.UploadListFilter{
UserID: req.UserID,
Keyword: req.Keyword,
Type: req.Type,
Extension: req.Extension,
Page: req.Page,
PageSize: req.PageSize,
})
if err != nil {
response.AbortBadRequest(c, shared.ErrQueryFileListFailed)
return
}
c.JSON(http.StatusOK, response.OK(listFilesResponse{
Total: total,
Page: req.Page,
PageSize: req.PageSize,
Items: items,
}))
}
// DeleteFile 软删除指定的文件记录
// @Summary 删除文件
// @Description 将指定 ID 的文件状态置为 deleted(软删除)
// @Tags admin
// @Produce json
// @Param id path string true "文件 ID"
// @Security SessionCookie
// @Success 200 {object} response.Any "删除成功"
// @Failure 404 {object} response.Any "文件不存在"
// @Router /api/v1/admin/uploads/files/{id} [delete]
func DeleteFile(c *gin.Context) {
ctx := c.Request.Context()
if uploadstorage.ReadOnly(ctx) {
response.AbortConflict(c, shared.ErrStorageReadOnly)
return
}
uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortBadRequest(c, shared.ErrInvalidFileID)
return
}
if _, err := softDeleteUpload(ctx, uploadID); err != nil {
if isRecordNotFound(err) {
response.AbortNotFound(c, shared.ErrFileRecordNotFound)
return
}
response.AbortBadRequest(c, shared.ErrDeleteFileFailed)
return
}
c.JSON(http.StatusOK, response.OKNil())
}
// GetDistinctUploadTypes 获取所有已存在的文件业务分类列表
// @Summary 获取业务分类列表
// @Description 查询系统内所有不重复的上传业务分类标识(如 avatar, doc 等)
// @Tags admin
// @Produce json
// @Security SessionCookie
// @Success 200 {object} response.Any{data=[]string} "查询成功"
// @Router /api/v1/admin/uploads/types [get]
func GetDistinctUploadTypes(c *gin.Context) {
ctx := c.Request.Context()
types, err := listDistinctUploadTypes(ctx)
if err != nil {
response.AbortBadRequest(c, shared.ErrQueryTypeListFailed)
return
}
c.JSON(http.StatusOK, response.OK(types))
}
type listMyFilesRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
Keyword string `form:"keyword"`
Type string `form:"type"`
Extension string `form:"extension"`
}
type listMyFilesResponse struct {
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Items []models.Upload `json:"items"`
}
// ListMyFiles 获取当前用户上传的文件列表
// @Summary 获取我的文件列表
// @Description 分页获取当前登录用户上传的文件,支持文件名关键词、业务类型、扩展名过滤
// @Tags upload
// @Produce json
// @Param page query int false "页码(默认 1)"
// @Param page_size query int false "每页数量(默认 20,最大 100)"
// @Param keyword query string false "文件名关键词(模糊匹配)"
// @Param type query string false "业务分类过滤"
// @Param extension query string false "扩展名过滤"
// @Security SessionCookie
// @Success 200 {object} response.Any{data=listMyFilesResponse} "查询成功"
// @Failure 401 {object} response.Any "未登录"
// @Router /api/v1/upload/my [get]
func ListMyFiles(c *gin.Context) {
currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
ctx := c.Request.Context()
var req listMyFilesRequest
if err := c.ShouldBindQuery(&req); err != nil {
response.AbortBadRequest(c, shared.ErrInvalidParams)
return
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 || req.PageSize > 100 {
req.PageSize = 20
}
total, items, err := listMyUploadFiles(ctx, currUser.ID, repository.UploadListFilter{
Keyword: req.Keyword,
Type: req.Type,
Extension: req.Extension,
Page: req.Page,
PageSize: req.PageSize,
})
if err != nil {
response.AbortBadRequest(c, shared.ErrQueryFileListFailed)
return
}
c.JSON(http.StatusOK, response.OK(listMyFilesResponse{
Total: total,
Page: req.Page,
PageSize: req.PageSize,
Items: items,
}))
}
// DeleteMyFile 软删除当前用户本人的文件
// @Summary 删除我的文件
// @Description 将当前用户本人的文件状态置为 deleted(软删除)
// @Tags upload
// @Produce json
// @Param id path string true "文件 ID"
// @Security SessionCookie
// @Success 200 {object} response.Any "删除成功"
// @Failure 403 {object} response.Any "无权操作"
// @Failure 404 {object} response.Any "文件不存在"
// @Router /api/v1/upload/{id} [delete]
func DeleteMyFile(c *gin.Context) {
currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
ctx := c.Request.Context()
if uploadstorage.ReadOnly(ctx) {
response.AbortConflict(c, shared.ErrStorageReadOnly)
return
}
uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortBadRequest(c, shared.ErrInvalidFileID)
return
}
if _, err := softDeleteOwnedUpload(ctx, currUser.ID, uploadID); err != nil {
if isRecordNotFound(err) {
response.AbortNotFound(c, shared.ErrFileRecordNotFound)
return
}
if errors.Is(err, ingest.ErrForbidden) {
response.AbortForbidden(c, shared.ErrOperationForbidden)
return
}
response.AbortBadRequest(c, shared.ErrDeleteFileFailed)
return
}
c.JSON(http.StatusOK, response.OKNil())
}
type updateMyFileRequest struct {
FileName string `json:"file_name" binding:"max=255"`
AccessMode *int `json:"access_mode" binding:"omitempty,oneof=0 1"`
}
// UpdateMyFile 更新当前用户本人的文件信息
// @Summary 更新我的文件信息
// @Description 更新当前用户本人的文件名或访问权限模式 (AccessMode)
// @Tags upload
// @Accept json
// @Produce json
// @Param id path string true "文件 ID"
// @Param request body updateMyFileRequest true "更新字段"
// @Security SessionCookie
// @Success 200 {object} response.Any{data=models.Upload} "更新成功"
// @Failure 403 {object} response.Any "无权操作"
// @Failure 404 {object} response.Any "文件不存在"
// @Router /api/v1/upload/{id} [put]
func UpdateMyFile(c *gin.Context) {
currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
ctx := c.Request.Context()
if uploadstorage.ReadOnly(ctx) {
response.AbortConflict(c, shared.ErrStorageReadOnly)
return
}
uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortBadRequest(c, shared.ErrInvalidFileID)
return
}
var req updateMyFileRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, shared.ErrInvalidParams)
return
}
updated, err := updateOwnedUpload(ctx, currUser.ID, uploadID, updateMyUploadInput(req))
if err != nil {
if isRecordNotFound(err) {
response.AbortNotFound(c, shared.ErrFileRecordNotFound)
return
}
if errors.Is(err, ingest.ErrForbidden) {
response.AbortForbidden(c, shared.ErrOperationForbidden)
return
}
response.AbortBadRequest(c, shared.ErrUpdateFileFailed)
return
}
c.JSON(http.StatusOK, response.OK(updated))
}