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 } // ResizeImage 调整图片尺寸 (用于头像处理) func ResizeImage(srcPath, destPath string, width, height int) error { // 打开原图 src, err := imaging.Open(srcPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } // 调整图片尺寸 (正方形裁剪) resized := imaging.Fill(src, width, height, imaging.Center, imaging.Lanczos) // 保存调整后的图片 err = imaging.Save(resized, destPath) if err != nil { return fmt.Errorf("保存调整后的图片失败: %v", err) } return nil } // CreateAvatar 创建头像(包含压缩和格式转换) func CreateAvatar(srcPath, destPath string, size int) error { // 打开原图 src, err := imaging.Open(srcPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } // 创建正方形头像 (居中裁剪) avatar := imaging.Fill(src, size, size, imaging.Center, imaging.Lanczos) // 保存为JPEG格式 (压缩优化) err = imaging.Save(avatar, destPath, imaging.JPEGQuality(85)) if err != nil { return fmt.Errorf("保存头像失败: %v", err) } return nil }