Files
urldb/handlers/file_handler.go

443 lines
11 KiB
Go
Raw Normal View History

2025-08-15 18:41:09 +08:00
package handlers
import (
"crypto/rand"
2025-08-18 02:30:15 +08:00
"crypto/sha256"
2025-08-15 18:41:09 +08:00
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/ctwj/urldb/db/converter"
"github.com/ctwj/urldb/db/dto"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/utils"
"github.com/gin-gonic/gin"
)
// FileHandler 文件处理器
type FileHandler struct {
fileRepo repo.FileRepository
systemConfigRepo repo.SystemConfigRepository
userRepo repo.UserRepository
}
// NewFileHandler 创建文件处理器
func NewFileHandler(fileRepo repo.FileRepository, systemConfigRepo repo.SystemConfigRepository, userRepo repo.UserRepository) *FileHandler {
return &FileHandler{
fileRepo: fileRepo,
systemConfigRepo: systemConfigRepo,
userRepo: userRepo,
}
}
// UploadFile 上传文件
func (h *FileHandler) UploadFile(c *gin.Context) {
// 获取当前用户ID
userID, exists := c.Get("user_id")
if !exists {
ErrorResponse(c, "用户未登录", http.StatusUnauthorized)
return
}
// 从数据库获取用户信息
currentUser, err := h.userRepo.FindByID(userID.(uint))
if err != nil {
ErrorResponse(c, "用户不存在", http.StatusUnauthorized)
return
}
2025-08-18 02:30:15 +08:00
// 获取文件哈希值
fileHash := c.PostForm("file_hash")
2025-08-18 15:08:49 +08:00
2025-08-18 02:30:15 +08:00
// 如果提供了文件哈希,先检查是否已存在
if fileHash != "" {
existingFile, err := h.fileRepo.FindByHash(fileHash)
if err == nil && existingFile != nil {
// 文件已存在,直接返回已存在的文件信息
utils.Info("文件已存在,跳过上传 - Hash: %s, 文件名: %s", fileHash, existingFile.OriginalName)
2025-08-18 15:08:49 +08:00
2025-08-18 02:30:15 +08:00
response := dto.FileUploadResponse{
2025-08-18 15:08:49 +08:00
File: converter.FileToResponse(existingFile),
Message: "文件已存在,极速上传成功",
Success: true,
2025-08-18 02:30:15 +08:00
IsDuplicate: true,
}
2025-08-18 15:08:49 +08:00
2025-08-18 02:30:15 +08:00
SuccessResponse(c, response)
return
}
}
2025-08-15 18:41:09 +08:00
// 获取上传目录配置(从环境变量或使用默认值)
uploadDir := os.Getenv("UPLOAD_DIR")
if uploadDir == "" {
uploadDir = "./uploads" // 默认值
}
// 创建年月子文件夹
now := time.Now()
yearMonth := now.Format("200601") // 格式202508
monthlyDir := filepath.Join(uploadDir, yearMonth)
// 确保年月目录存在
if err := os.MkdirAll(monthlyDir, 0755); err != nil {
ErrorResponse(c, "创建年月目录失败", http.StatusInternalServerError)
return
}
// 获取上传的文件
file, header, err := c.Request.FormFile("file")
if err != nil {
ErrorResponse(c, "获取上传文件失败", http.StatusBadRequest)
return
}
defer file.Close()
// 检查文件大小5MB
maxFileSize := int64(5 * 1024 * 1024) // 5MB
if header.Size > maxFileSize {
ErrorResponse(c, "文件大小不能超过5MB", http.StatusBadRequest)
return
}
// 检查文件类型,只允许图片
allowedTypes := []string{
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
"image/webp",
"image/bmp",
"image/svg+xml",
}
contentType := header.Header.Get("Content-Type")
isAllowedType := false
for _, allowedType := range allowedTypes {
if contentType == allowedType {
isAllowedType = true
break
}
}
if !isAllowedType {
ErrorResponse(c, "只支持图片格式文件 (JPEG, PNG, GIF, WebP, BMP, SVG)", http.StatusBadRequest)
return
}
// 生成随机文件名
fileName := h.generateRandomFileName(header.Filename)
filePath := filepath.Join(monthlyDir, fileName)
// 创建目标文件
dst, err := os.Create(filePath)
if err != nil {
ErrorResponse(c, "创建文件失败", http.StatusInternalServerError)
return
}
defer dst.Close()
// 复制文件内容
if _, err := io.Copy(dst, file); err != nil {
ErrorResponse(c, "保存文件失败", http.StatusInternalServerError)
return
}
2025-08-18 02:30:15 +08:00
// 计算文件哈希值(如果前端没有提供)
if fileHash == "" {
fileHash, err = h.calculateFileHash(filePath)
if err != nil {
ErrorResponse(c, "计算文件哈希失败", http.StatusInternalServerError)
return
}
}
// 再次检查文件是否已存在(使用计算出的哈希)
existingFile, err := h.fileRepo.FindByHash(fileHash)
if err == nil && existingFile != nil {
// 文件已存在,删除刚上传的文件,返回已存在的文件信息
os.Remove(filePath)
utils.Info("文件已存在,跳过上传 - Hash: %s, 文件名: %s", fileHash, existingFile.OriginalName)
2025-08-18 15:08:49 +08:00
2025-08-18 02:30:15 +08:00
response := dto.FileUploadResponse{
2025-08-18 15:08:49 +08:00
File: converter.FileToResponse(existingFile),
Message: "文件已存在,极速上传成功",
Success: true,
2025-08-18 02:30:15 +08:00
IsDuplicate: true,
}
2025-08-18 15:08:49 +08:00
2025-08-18 02:30:15 +08:00
SuccessResponse(c, response)
return
}
2025-08-15 18:41:09 +08:00
// 获取文件类型
fileType := h.getFileType(header.Filename)
mimeType := header.Header.Get("Content-Type")
// 获取是否公开
isPublic := true
if isPublicStr := c.PostForm("is_public"); isPublicStr != "" {
if isPublicBool, err := strconv.ParseBool(isPublicStr); err == nil {
isPublic = isPublicBool
}
}
// 构建访问URL使用绝对路径不包含域名
accessURL := fmt.Sprintf("/uploads/%s/%s", yearMonth, fileName)
// 创建文件记录
fileEntity := &entity.File{
OriginalName: header.Filename,
FileName: fileName,
FilePath: filePath,
FileSize: header.Size,
FileType: fileType,
MimeType: mimeType,
2025-08-18 02:30:15 +08:00
FileHash: fileHash,
2025-08-15 18:41:09 +08:00
AccessURL: accessURL,
UserID: currentUser.ID,
Status: entity.FileStatusActive,
IsPublic: isPublic,
IsDeleted: false,
}
// 保存到数据库
if err := h.fileRepo.Create(fileEntity); err != nil {
// 删除已上传的文件
os.Remove(filePath)
ErrorResponse(c, "保存文件记录失败", http.StatusInternalServerError)
return
}
// 返回响应
response := dto.FileUploadResponse{
File: converter.FileToResponse(fileEntity),
Message: "文件上传成功",
Success: true,
}
SuccessResponse(c, response)
}
// GetFileList 获取文件列表
func (h *FileHandler) GetFileList(c *gin.Context) {
var req dto.FileListRequest
if err := c.ShouldBindQuery(&req); err != nil {
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
return
}
// 设置默认值
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
2025-08-18 15:08:49 +08:00
// 添加调试日志
utils.Info("文件列表请求参数: page=%d, pageSize=%d, search='%s', fileType='%s', status='%s', userID=%d",
req.Page, req.PageSize, req.Search, req.FileType, req.Status, req.UserID)
// 获取当前用户ID和角色现在总是有认证
userID := c.GetUint("user_id")
role := c.GetString("role")
utils.Info("GetFileList - 用户ID: %d, 角色: %s", userID, role)
2025-08-15 18:41:09 +08:00
// 根据用户角色决定查询范围
var files []entity.File
var total int64
var err error
if role == "admin" {
2025-08-15 18:41:09 +08:00
// 管理员可以查看所有文件
files, total, err = h.fileRepo.SearchFiles(req.Search, req.FileType, req.Status, req.UserID, req.Page, req.PageSize)
} else {
2025-08-15 18:41:09 +08:00
// 普通用户只能查看自己的文件
files, total, err = h.fileRepo.SearchFiles(req.Search, req.FileType, req.Status, userID, req.Page, req.PageSize)
}
if err != nil {
ErrorResponse(c, "获取文件列表失败", http.StatusInternalServerError)
return
}
response := converter.FileListToResponse(files, total, req.Page, req.PageSize)
SuccessResponse(c, response)
}
// DeleteFiles 删除文件
func (h *FileHandler) DeleteFiles(c *gin.Context) {
var req dto.FileDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
return
}
// 获取当前用户ID和角色
userIDInterface, exists := c.Get("user_id")
roleInterface, _ := c.Get("role")
if !exists {
ErrorResponse(c, "用户未登录", http.StatusUnauthorized)
return
}
userID := userIDInterface.(uint)
role := ""
if roleInterface != nil {
role = roleInterface.(string)
}
// 检查权限
if role != "admin" {
// 普通用户只能删除自己的文件
for _, id := range req.IDs {
file, err := h.fileRepo.FindByID(id)
if err != nil {
ErrorResponse(c, "文件不存在", http.StatusNotFound)
return
}
if file.UserID != userID {
ErrorResponse(c, "没有权限删除此文件", http.StatusForbidden)
return
}
}
}
// 获取要删除的文件信息
var filesToDelete []entity.File
for _, id := range req.IDs {
file, err := h.fileRepo.FindByID(id)
if err != nil {
ErrorResponse(c, "文件不存在", http.StatusNotFound)
return
}
filesToDelete = append(filesToDelete, *file)
}
// 删除本地文件
for _, file := range filesToDelete {
if err := os.Remove(file.FilePath); err != nil {
utils.Error("删除本地文件失败: %s, 错误: %v", file.FilePath, err)
// 继续删除其他文件,不因为单个文件删除失败而中断
}
}
// 删除数据库记录
for _, id := range req.IDs {
if err := h.fileRepo.Delete(id); err != nil {
utils.Error("删除文件记录失败: ID=%d, 错误: %v", id, err)
// 继续删除其他文件,不因为单个文件删除失败而中断
}
}
SuccessResponse(c, gin.H{"message": "文件删除成功"})
}
// UpdateFile 更新文件信息
func (h *FileHandler) UpdateFile(c *gin.Context) {
var req dto.FileUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
return
}
// 获取当前用户ID和角色
userIDInterface, exists := c.Get("user_id")
roleInterface, _ := c.Get("role")
if !exists {
ErrorResponse(c, "用户未登录", http.StatusUnauthorized)
return
}
userID := userIDInterface.(uint)
role := ""
if roleInterface != nil {
role = roleInterface.(string)
}
// 查找文件
file, err := h.fileRepo.FindByID(req.ID)
if err != nil {
ErrorResponse(c, "文件不存在", http.StatusNotFound)
return
}
// 检查权限
if role != "admin" && userID != file.UserID {
ErrorResponse(c, "没有权限修改此文件", http.StatusForbidden)
return
}
// 更新文件信息
if req.IsPublic != nil {
if err := h.fileRepo.UpdateFilePublic(req.ID, *req.IsPublic); err != nil {
ErrorResponse(c, "更新文件状态失败", http.StatusInternalServerError)
return
}
}
if req.Status != "" {
if err := h.fileRepo.UpdateFileStatus(req.ID, req.Status); err != nil {
ErrorResponse(c, "更新文件状态失败", http.StatusInternalServerError)
return
}
}
SuccessResponse(c, gin.H{"message": "文件更新成功"})
}
// generateRandomFileName 生成随机文件名
func (h *FileHandler) generateRandomFileName(originalName string) string {
// 获取文件扩展名
ext := filepath.Ext(originalName)
// 生成随机字符串
bytes := make([]byte, 16)
rand.Read(bytes)
randomStr := fmt.Sprintf("%x", bytes)
// 添加时间戳
timestamp := time.Now().Unix()
return fmt.Sprintf("%d_%s%s", timestamp, randomStr, ext)
}
// getFileType 获取文件类型
func (h *FileHandler) getFileType(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
// 图片类型
imageExts := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"}
for _, imgExt := range imageExts {
if ext == imgExt {
return "image"
}
}
return "image" // 默认返回image因为只支持图片格式
}
2025-08-18 02:30:15 +08:00
// calculateFileHash 计算文件哈希值
func (h *FileHandler) calculateFileHash(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}