141 lines
4.1 KiB
Go
141 lines
4.1 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/sundynix/pets-be/internal/model"
|
|
)
|
|
|
|
func (a *API) fileExists(id string) bool {
|
|
if !validID(id) {
|
|
return false
|
|
}
|
|
var count int64
|
|
a.db.Model(&model.File{}).Where("id = ?", id).Count(&count)
|
|
return count == 1
|
|
}
|
|
|
|
const maxUploadSize = 10 << 20
|
|
|
|
func (a *API) uploadFile(c *gin.Context) {
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxUploadSize+1024)
|
|
header, err := c.FormFile("file")
|
|
if err != nil || header.Size <= 0 || header.Size > maxUploadSize {
|
|
fail(c, http.StatusBadRequest, "请选择不超过 10 MB 的图片")
|
|
return
|
|
}
|
|
reader, err := header.Open()
|
|
if err != nil {
|
|
fail(c, http.StatusBadRequest, "读取图片失败")
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
signature := make([]byte, 512)
|
|
n, _ := io.ReadFull(reader, signature)
|
|
contentType := http.DetectContentType(signature[:n])
|
|
extensions := map[string]string{"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp"}
|
|
ext, allowed := extensions[contentType]
|
|
if !allowed {
|
|
fail(c, http.StatusBadRequest, "仅支持 JPG、PNG、WebP 图片")
|
|
return
|
|
}
|
|
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
|
fail(c, http.StatusBadRequest, "读取图片失败")
|
|
return
|
|
}
|
|
hasher := md5.New()
|
|
if _, err := io.Copy(hasher, reader); err != nil {
|
|
fail(c, http.StatusBadRequest, "读取图片失败")
|
|
return
|
|
}
|
|
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
|
fail(c, http.StatusBadRequest, "读取图片失败")
|
|
return
|
|
}
|
|
digest := hex.EncodeToString(hasher.Sum(nil))
|
|
var existing model.File
|
|
if a.db.Where("md5 = ?", digest).First(&existing).Error == nil {
|
|
if existing.Size != header.Size {
|
|
fail(c, http.StatusConflict, "文件校验冲突")
|
|
return
|
|
}
|
|
ok(c, a.fileView(existing))
|
|
return
|
|
}
|
|
key := "files/" + digest[:2] + "/" + digest + ext
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 45*time.Second)
|
|
defer cancel()
|
|
if err := a.storage.Put(ctx, key, reader, header.Size, contentType); err != nil {
|
|
fail(c, http.StatusBadGateway, "上传对象存储失败")
|
|
return
|
|
}
|
|
file := model.File{
|
|
Bucket: a.storage.Bucket(), ObjectKey: key, MD5: digest, URL: a.storage.PublicURL(key),
|
|
Name: strings.TrimSpace(header.Filename), ContentType: contentType,
|
|
Size: header.Size,
|
|
}
|
|
if err := a.db.Create(&file).Error; err != nil {
|
|
if a.db.Where("md5 = ?", digest).First(&existing).Error == nil && existing.Size == header.Size {
|
|
ok(c, a.fileView(existing))
|
|
return
|
|
}
|
|
fail(c, http.StatusInternalServerError, "保存文件信息失败")
|
|
return
|
|
}
|
|
created(c, a.fileView(file))
|
|
}
|
|
|
|
func (a *API) getFile(c *gin.Context) {
|
|
if !validID(c.Param("id")) {
|
|
fail(c, http.StatusBadRequest, "文件 ID 无效")
|
|
return
|
|
}
|
|
var file model.File
|
|
if err := a.db.Where("id = ?", c.Param("id")).First(&file).Error; err != nil {
|
|
fail(c, http.StatusNotFound, "文件不存在")
|
|
return
|
|
}
|
|
ok(c, a.fileView(file))
|
|
}
|
|
|
|
func (a *API) fileView(file model.File) gin.H {
|
|
return gin.H{"id": file.ID, "name": file.Name, "content_type": file.ContentType,
|
|
"size": file.Size, "md5": file.MD5, "url": file.URL}
|
|
}
|
|
|
|
func (a *API) fileURL(id *string) string {
|
|
if id == nil || !validID(*id) {
|
|
return ""
|
|
}
|
|
var file model.File
|
|
if a.db.Select("url").First(&file, "id = ?", *id).Error != nil {
|
|
return ""
|
|
}
|
|
return file.URL
|
|
}
|
|
|
|
func (a *API) userView(user model.User) gin.H {
|
|
return gin.H{"id": user.ID, "nickname": user.Nickname, "avatar_file_id": user.AvatarFileID,
|
|
"avatar_url": a.fileURL(user.AvatarFileID), "created_at": user.CreatedAt}
|
|
}
|
|
|
|
func (a *API) petView(pet model.Pet) gin.H {
|
|
var birthDate string
|
|
if pet.BirthDate != nil {
|
|
birthDate = pet.BirthDate.Format("2006-01-02")
|
|
}
|
|
return gin.H{"id": pet.ID, "name": pet.Name, "species": pet.Species, "breed": pet.Breed, "breed_id": pet.BreedID,
|
|
"gender": pet.Gender, "birth_date": birthDate, "weight_kg": pet.WeightKg,
|
|
"avatar_file_id": pet.AvatarFileID, "avatar_url": a.fileURL(pet.AvatarFileID),
|
|
"coat": pet.Coat, "allergies": pet.Allergies, "care_notes": pet.CareNotes,
|
|
"created_at": pet.CreatedAt}
|
|
}
|