fix
This commit is contained in:
204
backend/pkg/utils/file/file.go
Normal file
204
backend/pkg/utils/file/file.go
Normal file
@ -0,0 +1,204 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Config 文件上传配置
|
||||
type Config struct {
|
||||
MaxSize int64 // 最大文件大小 (bytes)
|
||||
UploadDir string // 上传目录
|
||||
AllowedTypes []string // 允许的文件类型
|
||||
}
|
||||
|
||||
// FileInfo 文件信息
|
||||
type FileInfo struct {
|
||||
OriginalName string `json:"original_name"`
|
||||
FileName string `json:"file_name"`
|
||||
FilePath string `json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
ContentType string `json:"content_type"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// UploadResult 上传结果
|
||||
type UploadResult struct {
|
||||
Original FileInfo `json:"original"`
|
||||
Thumbnail FileInfo `json:"thumbnail"`
|
||||
}
|
||||
|
||||
// IsAllowedType 检查文件类型是否允许
|
||||
func IsAllowedType(contentType string, allowedTypes []string) bool {
|
||||
for _, allowedType := range allowedTypes {
|
||||
if contentType == allowedType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GenerateFileName 生成唯一文件名
|
||||
func GenerateFileName(originalName string) string {
|
||||
ext := filepath.Ext(originalName)
|
||||
name := strings.TrimSuffix(originalName, ext)
|
||||
timestamp := time.Now().Unix()
|
||||
uuid := uuid.New().String()[:8]
|
||||
return fmt.Sprintf("%s_%d_%s%s", name, timestamp, uuid, ext)
|
||||
}
|
||||
|
||||
// SaveFile 保存文件
|
||||
func SaveFile(file multipart.File, header *multipart.FileHeader, config Config) (*FileInfo, error) {
|
||||
// 检查文件大小
|
||||
if header.Size > config.MaxSize {
|
||||
return nil, fmt.Errorf("文件大小超过限制: %d bytes", config.MaxSize)
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if !IsAllowedType(contentType, config.AllowedTypes) {
|
||||
return nil, fmt.Errorf("不支持的文件类型: %s", contentType)
|
||||
}
|
||||
|
||||
// 生成文件名
|
||||
fileName := GenerateFileName(header.Filename)
|
||||
|
||||
// 创建上传目录
|
||||
uploadPath := filepath.Join(config.UploadDir, "photos")
|
||||
if err := os.MkdirAll(uploadPath, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建上传目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 完整文件路径
|
||||
filePath := filepath.Join(uploadPath, fileName)
|
||||
|
||||
// 创建目标文件
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建文件失败: %v", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// 复制文件内容
|
||||
file.Seek(0, 0) // 重置文件指针
|
||||
_, err = io.Copy(dst, file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保存文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 返回文件信息
|
||||
return &FileInfo{
|
||||
OriginalName: header.Filename,
|
||||
FileName: fileName,
|
||||
FilePath: filePath,
|
||||
FileSize: header.Size,
|
||||
ContentType: contentType,
|
||||
URL: "/uploads/photos/" + fileName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateThumbnail 创建缩略图
|
||||
func CreateThumbnail(originalPath string, config Config) (*FileInfo, error) {
|
||||
// 打开原图
|
||||
src, err := imaging.Open(originalPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成缩略图文件名
|
||||
ext := filepath.Ext(originalPath)
|
||||
baseName := strings.TrimSuffix(filepath.Base(originalPath), ext)
|
||||
thumbnailName := baseName + "_thumb" + ext
|
||||
|
||||
// 创建缩略图目录
|
||||
thumbnailDir := filepath.Join(config.UploadDir, "thumbnails")
|
||||
if err := os.MkdirAll(thumbnailDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建缩略图目录失败: %v", err)
|
||||
}
|
||||
|
||||
thumbnailPath := filepath.Join(thumbnailDir, thumbnailName)
|
||||
|
||||
// 调整图片大小 (最大宽度 300px,保持比例)
|
||||
thumbnail := imaging.Resize(src, 300, 0, imaging.Lanczos)
|
||||
|
||||
// 保存缩略图
|
||||
err = imaging.Save(thumbnail, thumbnailPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保存缩略图失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
stat, err := os.Stat(thumbnailPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取缩略图信息失败: %v", err)
|
||||
}
|
||||
|
||||
return &FileInfo{
|
||||
OriginalName: thumbnailName,
|
||||
FileName: thumbnailName,
|
||||
FilePath: thumbnailPath,
|
||||
FileSize: stat.Size(),
|
||||
ContentType: "image/jpeg",
|
||||
URL: "/uploads/thumbnails/" + thumbnailName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadPhoto 上传照片(包含原图和缩略图)
|
||||
func UploadPhoto(file multipart.File, header *multipart.FileHeader, config Config) (*UploadResult, error) {
|
||||
// 保存原图
|
||||
original, err := SaveFile(file, header, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建缩略图
|
||||
thumbnail, err := CreateThumbnail(original.FilePath, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UploadResult{
|
||||
Original: *original,
|
||||
Thumbnail: *thumbnail,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件
|
||||
func DeleteFile(filePath string) error {
|
||||
if filePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return nil // 文件不存在,认为删除成功
|
||||
}
|
||||
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
// GetImageDimensions 获取图片尺寸
|
||||
func GetImageDimensions(filePath string) (width, height int, err error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.DecodeConfig(file)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return img.Width, img.Height, nil
|
||||
}
|
||||
Reference in New Issue
Block a user