mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
9a18bea324
Move storage backend configuration from startup YAML to system_config-backed runtime configuration. Add local, S3-compatible, R2, MinIO, OSS, and WebDAV backend support. Add a storage migration async task using the existing task dispatch framework. Migration target config is carried in task payload, and maintenance mode is derived from task execution state. Split upload file management and storage operations, add the admin storage configuration tab, and update migrations and Swagger docs.
168 lines
5.2 KiB
Go
168 lines
5.2 KiB
Go
// Copyright 2026 Arctel.net
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
package upload
|
||
|
||
import (
|
||
"errors"
|
||
"net/http"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||
"github.com/Rain-kl/Wavelet/internal/db"
|
||
"github.com/Rain-kl/Wavelet/internal/model"
|
||
"github.com/Rain-kl/Wavelet/internal/util"
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
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 []model.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} util.ResponseAny{data=listMyFilesResponse} "查询成功"
|
||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||
// @Router /api/v1/upload/my [get]
|
||
func ListMyFiles(c *gin.Context) {
|
||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||
ctx := c.Request.Context()
|
||
|
||
var req listMyFilesRequest
|
||
if err := c.ShouldBindQuery(&req); err != nil {
|
||
c.JSON(http.StatusOK, util.Err(ErrInvalidParams))
|
||
return
|
||
}
|
||
if req.Page <= 0 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize <= 0 || req.PageSize > 100 {
|
||
req.PageSize = 20
|
||
}
|
||
|
||
query := db.DB(ctx).Model(&model.Upload{}).
|
||
Where("user_id = ? AND status != ?", currUser.ID, model.UploadStatusDeleted)
|
||
|
||
if req.Keyword != "" {
|
||
query = query.Where("LOWER(file_name) LIKE ?", "%"+strings.ToLower(req.Keyword)+"%")
|
||
}
|
||
if req.Type != "" {
|
||
query = query.Where("type = ?", req.Type)
|
||
}
|
||
if req.Extension != "" {
|
||
query = query.Where("extension = ?", strings.ToLower(req.Extension))
|
||
}
|
||
|
||
var total int64
|
||
if err := query.Count(&total).Error; err != nil {
|
||
c.JSON(http.StatusOK, util.Err(ErrQueryFileCountFailed))
|
||
return
|
||
}
|
||
|
||
var items []model.Upload
|
||
offset := (req.Page - 1) * req.PageSize
|
||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&items).Error; err != nil {
|
||
c.JSON(http.StatusOK, util.Err(ErrQueryFileListFailed))
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, util.OK(listMyFilesResponse{
|
||
Total: total,
|
||
Page: req.Page,
|
||
PageSize: req.PageSize,
|
||
Items: items,
|
||
}))
|
||
}
|
||
|
||
// DeleteFile 软删除文件记录
|
||
// @Summary 删除文件
|
||
// @Description 将文件状态置为 deleted(软删除),不会立即清理底层存储对象
|
||
// @Tags upload
|
||
// @Produce json
|
||
// @Param id path string true "文件 ID"
|
||
// @Security SessionCookie
|
||
// @Success 200 {object} util.ResponseAny "删除成功"
|
||
// @Failure 403 {object} util.ResponseAny "无权操作"
|
||
// @Failure 404 {object} util.ResponseAny "文件不存在"
|
||
// @Router /api/v1/upload/{id} [delete]
|
||
func DeleteFile(c *gin.Context) {
|
||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||
ctx := c.Request.Context()
|
||
if storageReadOnly(ctx) {
|
||
c.JSON(http.StatusConflict, util.Err(ErrStorageReadOnly))
|
||
return
|
||
}
|
||
|
||
uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, util.Err(ErrInvalidFileID))
|
||
return
|
||
}
|
||
|
||
var upload model.Upload
|
||
if err := db.DB(ctx).Where("id = ? AND status != ?", uploadID, model.UploadStatusDeleted).First(&upload).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
c.AbortWithStatus(http.StatusNotFound)
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, util.Err(ErrQueryUploadRecordFailed))
|
||
return
|
||
}
|
||
if upload.UserID != currUser.ID && !currUser.IsAdmin {
|
||
c.AbortWithStatus(http.StatusForbidden)
|
||
return
|
||
}
|
||
if err := db.DB(ctx).Model(&upload).Update("status", model.UploadStatusDeleted).Error; err != nil {
|
||
c.JSON(http.StatusOK, util.Err(ErrDeleteFileFailed))
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, util.OKNil())
|
||
}
|
||
|
||
// GetDistinctUploadTypes 获取数据库中所有已存在的文件业务类型
|
||
// @Summary 获取文件业务类型列表
|
||
// @Description 返回数据库中所有已上传文件实际拥有的业务类型列表
|
||
// @Tags admin
|
||
// @Produce json
|
||
// @Security SessionCookie
|
||
// @Success 200 {object} util.ResponseAny{data=[]string} "业务类型列表"
|
||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||
// @Router /api/v1/admin/uploads/types [get]
|
||
func GetDistinctUploadTypes(c *gin.Context) {
|
||
var dbTypes []string
|
||
if err := db.DB(c.Request.Context()).Model(&model.Upload{}).
|
||
Where("type IS NOT NULL AND type != ''").
|
||
Distinct().
|
||
Pluck("type", &dbTypes).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||
return
|
||
}
|
||
sort.Strings(dbTypes)
|
||
c.JSON(http.StatusOK, util.OK(dbTypes))
|
||
}
|