This commit is contained in:
xujiang
2025-07-10 18:09:11 +08:00
parent 5cbdc5af73
commit 604b9e59ba
95 changed files with 23709 additions and 19 deletions

View 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
}