Files
toutoukan/controllers/file/picUpload.go

204 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package file
import (
"errors"
"fmt"
"io"
"mime/multipart"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/gin-gonic/gin"
)
// 阿里云OSS配置 - 请替换为你的实际配置
const (
ossEndpoint = "oss-cn-shanghai.aliyuncs.com" // OSS地域节点
ossAccessKeyID = "LTAI5tQDJnnajQS8uCXJLKfP" // 你的AccessKeyID
ossAccessKeySecret = "4wmJNcnYg5wKgBWePTQ9x4CPfUqgzn" // 你的AccessKeySecret
ossBucketName = "ttk-userdata" // 你的Bucket名称
ossBasePath = "/" // 图片存储基础路径
)
// 允许的图片文件扩展名
var allowedImageExts = map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".webp": true,
}
// 上传响应结构
type UploadResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
URL string `json:"url"` // 图片访问URL
Filename string `json:"filename"` // OSS中的文件名
Size int64 `json:"size"` // 图片大小(字节)
} `json:"data"`
}
// PicUpload 处理图片上传请求
func PicUpload(c *gin.Context) {
// 1. 获取上传的文件
file, err := c.FormFile("image")
if err != nil {
c.JSON(400, UploadResponse{
Code: 400,
Message: "获取上传文件失败: " + err.Error(),
})
return
}
// 2. 验证文件
if err := validateImageFile(file); err != nil {
c.JSON(400, UploadResponse{
Code: 400,
Message: err.Error(),
})
return
}
// 3. 打开文件
srcFile, err := file.Open()
if err != nil {
c.JSON(500, UploadResponse{
Code: 500,
Message: "打开文件失败: " + err.Error(),
})
return
}
defer srcFile.Close()
// 4. 生成OSS中的文件名
objectName, err := generateObjectName(file.Filename)
if err != nil {
c.JSON(500, UploadResponse{
Code: 500,
Message: "生成文件名失败: " + err.Error(),
})
return
}
// 5. 上传到OSS
ossURL, err := uploadToOSS(srcFile, objectName, file.Size)
if err != nil {
c.JSON(500, UploadResponse{
Code: 500,
Message: "上传到OSS失败: " + err.Error(),
})
return
}
// 6. 返回成功响应
c.JSON(200, UploadResponse{
Code: 200,
Message: "上传成功",
Data: struct {
URL string `json:"url"`
Filename string `json:"filename"`
Size int64 `json:"size"`
}{
URL: ossURL,
Filename: objectName,
Size: file.Size,
},
})
}
// 验证图片文件
func validateImageFile(file *multipart.FileHeader) error {
// 验证大小10MB
if file.Size > 10*1024*1024 {
return errors.New("文件大小不能超过10MB")
}
// 验证扩展名
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedImageExts[ext] {
return errors.New("不支持的图片格式仅支持jpg、jpeg、png、gif、webp")
}
return nil
}
// 生成唯一文件名
func generateObjectName(originalName string) (string, error) {
// 步骤1提取原始文件的扩展名如 .jpg .png
ext := strings.ToLower(filepath.Ext(originalName))
if ext == "" {
return "", errors.New("文件没有扩展名,无法识别文件类型")
}
// 验证扩展名是否为允许的图片格式(避免非法扩展名)
if !allowedImageExts[ext] {
return "", errors.New("不支持的图片格式仅支持jpg、jpeg、png、gif、webp")
}
// 步骤2过滤原始文件名中的非法字符仅保留 a-z A-Z 0-9 - _ .
// 先移除扩展名,只处理文件名部分
rawFileName := strings.TrimSuffix(originalName, ext)
// 正则表达式:保留合法字符,其他替换为空
reg := regexp.MustCompile(`[^a-zA-Z0-9_\-.]`)
filteredFileName := reg.ReplaceAllString(rawFileName, "")
// 若过滤后文件名为空,用"unknown"代替(避免生成空文件名)
if filteredFileName == "" {
filteredFileName = "unknown"
}
// 步骤3生成唯一文件名时间戳 + 过滤后的文件名 + 扩展名)
// 时间戳用秒级+毫秒级,确保唯一性(避免同一秒上传相同文件名)
timestamp := time.Now().Format("20060102150405.000") // 格式:年月日时分秒.毫秒
uniqueFileName := fmt.Sprintf("%s-%s%s", timestamp, filteredFileName, ext)
// 步骤4拼接最终 ObjectName确保路径规范无首尾/、无连续/
// 清理 ossBasePath如 "images/" 转为 "images",避免结尾/
cleanBasePath := strings.TrimSuffix(ossBasePath, "/")
// 拼接路径(如 "images" + "20240520143000.123-unknown.jpg" → "images/20240520143000.123-unknown.jpg"
objectName := fmt.Sprintf("%s/%s", cleanBasePath, uniqueFileName)
// 最终验证:确保 ObjectName 无首尾/、无连续/
objectName = strings.Trim(objectName, "/")
objectName = regexp.MustCompile(`/+`).ReplaceAllString(objectName, "/") // 多/替换为单/
return objectName, nil
}
// 上传到OSS
func uploadToOSS(reader io.Reader, objectName string, fileSize int64) (string, error) {
// 1. 创建OSS客户端旧版SDK的创建方式
client, err := oss.New(ossEndpoint, ossAccessKeyID, ossAccessKeySecret)
if err != nil {
return "", fmt.Errorf("创建OSS客户端失败: %w", err)
}
// 2. 获取Bucket
bucket, err := client.Bucket(ossBucketName)
if err != nil {
return "", fmt.Errorf("获取Bucket失败: %w", err)
}
// 3. 上传文件旧版SDK的PutObject方法
err = bucket.PutObject(objectName, reader)
if err != nil {
return "", fmt.Errorf("上传文件失败: %w", err)
}
// 4. 生成访问URL
// 公开Bucket可以直接使用此URL
url := fmt.Sprintf("https://%s.%s/%s", ossBucketName, ossEndpoint, objectName)
// 如果是私有Bucket需要生成带签名的URL有效期1小时
// signedURL, err := bucket.SignURL(objectName, oss.HTTPGet, 3600)
// if err != nil {
// return "", fmt.Errorf("生成签名URL失败: %w", err)
// }
// return signedURL, nil
return url, nil
}