174 lines
5.5 KiB
Go
174 lines
5.5 KiB
Go
package service
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
|
||
"nl-pms-api/internal/commonservice"
|
||
"nl-pms-api/internal/config"
|
||
"nl-pms-api/internal/model"
|
||
)
|
||
|
||
// FileService 图片上传与素材库管理。
|
||
type FileService struct {
|
||
DB *gorm.DB
|
||
Cfg *config.Config
|
||
}
|
||
|
||
var extByMime = map[string]string{
|
||
"image/jpeg": ".jpg",
|
||
"image/png": ".png",
|
||
"image/gif": ".gif",
|
||
"image/webp": ".webp",
|
||
}
|
||
|
||
// UploadResult 上传成功响应。
|
||
type UploadResult struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
URL string `json:"url"`
|
||
Size int64 `json:"size"`
|
||
Mime string `json:"mime"`
|
||
TeamID int64 `json:"teamId"`
|
||
}
|
||
|
||
// FileItem 素材库列表项。
|
||
type FileItem struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Original string `json:"original"`
|
||
Mime string `json:"mime"`
|
||
Size int64 `json:"size"`
|
||
UserID int64 `json:"userId"`
|
||
TeamID int64 `json:"teamId"`
|
||
Kind string `json:"kind"`
|
||
CreatedAt string `json:"createdAt"`
|
||
Username string `json:"username"`
|
||
URL string `json:"url" gorm:"-"`
|
||
}
|
||
|
||
// Upload 保存图片;userID 来自 JWT,teamID/kind 来自表单。
|
||
func (s *FileService) Upload(userID, teamID int64, kind, original string, r io.Reader, sizeHint int64, requestHost string) (*UploadResult, error) {
|
||
if sizeHint > s.Cfg.MaxUploadBytes() {
|
||
return nil, &commonservice.AppError{Code: "FILE_TOO_LARGE", Status: http.StatusRequestEntityTooLarge}
|
||
}
|
||
data, err := io.ReadAll(io.LimitReader(r, s.Cfg.MaxUploadBytes()+1))
|
||
if err != nil || int64(len(data)) > s.Cfg.MaxUploadBytes() {
|
||
return nil, &commonservice.AppError{Code: "FILE_TOO_LARGE", Status: http.StatusRequestEntityTooLarge}
|
||
}
|
||
mime := http.DetectContentType(data)
|
||
ext, ok := extByMime[mime]
|
||
if !ok {
|
||
return nil, &commonservice.AppError{Code: "UNSUPPORTED_TYPE", Status: http.StatusUnsupportedMediaType}
|
||
}
|
||
sum := hex.EncodeToString(func() []byte { h := sha256.Sum256(data); return h[:] }())
|
||
var rec model.File
|
||
if s.DB.Where("sha256 = ? AND user_id = ? AND team_id = ?", sum, userID, teamID).First(&rec).Error == nil {
|
||
return s.toUploadResult(rec, requestHost), nil
|
||
}
|
||
name := commonservice.StoredName(ext)
|
||
full := filepath.Join(s.Cfg.StorageDir, filepath.FromSlash(name))
|
||
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
|
||
return nil, commonservice.Internal("SAVE_FAILED")
|
||
}
|
||
if err := os.WriteFile(full, data, 0644); err != nil {
|
||
return nil, commonservice.Internal("SAVE_FAILED")
|
||
}
|
||
rec = model.File{
|
||
Name: name,
|
||
Original: commonservice.Clip(filepath.Base(original), 255),
|
||
Mime: mime,
|
||
Size: int64(len(data)),
|
||
SHA256: sum,
|
||
UserID: userID,
|
||
TeamID: teamID,
|
||
Kind: commonservice.NormalizeKind(kind),
|
||
CreatedAt: time.Now().UTC().Format(time.RFC3339),
|
||
}
|
||
if err := s.DB.Create(&rec).Error; err != nil {
|
||
_ = os.Remove(full)
|
||
return nil, commonservice.Internal("SAVE_FAILED")
|
||
}
|
||
return s.toUploadResult(rec, requestHost), nil
|
||
}
|
||
|
||
func (s *FileService) toUploadResult(f model.File, host string) *UploadResult {
|
||
return &UploadResult{
|
||
ID: f.ID, Name: f.Name, URL: commonservice.PublicURL(s.Cfg, host, f.Name),
|
||
Size: f.Size, Mime: f.Mime, TeamID: f.TeamID,
|
||
}
|
||
}
|
||
|
||
// List 素材库分页;scope=mine|team|all。
|
||
func (s *FileService) List(userID int64, scope string, teamID, page, size int64, requestHost string) (int64, []FileItem, error) {
|
||
if userID <= 0 {
|
||
return 0, nil, commonservice.BadRequest("USER_REQUIRED")
|
||
}
|
||
var where func(*gorm.DB) *gorm.DB
|
||
switch scope {
|
||
case "mine":
|
||
where = func(db *gorm.DB) *gorm.DB { return db.Where("pms_files.user_id = ?", userID) }
|
||
case "team":
|
||
if !commonservice.IsTeamAdmin(s.DB, teamID, userID) {
|
||
return 0, nil, commonservice.Forbidden("FORBIDDEN")
|
||
}
|
||
where = func(db *gorm.DB) *gorm.DB { return db.Where("pms_files.team_id = ?", teamID) }
|
||
case "all":
|
||
if userID != commonservice.AdminUserID {
|
||
return 0, nil, commonservice.Forbidden("FORBIDDEN")
|
||
}
|
||
where = func(db *gorm.DB) *gorm.DB { return db }
|
||
default:
|
||
return 0, nil, commonservice.BadRequest("BAD_SCOPE")
|
||
}
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if size < 1 || size > 100 {
|
||
size = 24
|
||
}
|
||
var total int64
|
||
if err := s.DB.Table("pms_files").Scopes(where).Count(&total).Error; err != nil {
|
||
return 0, nil, commonservice.Internal("QUERY_FAILED")
|
||
}
|
||
items := []FileItem{}
|
||
err := s.DB.Table("pms_files").Scopes(where).
|
||
Select("pms_files.id, pms_files.name, pms_files.original, pms_files.mime, pms_files.size, pms_files.user_id, pms_files.team_id, pms_files.kind, pms_files.created_at, COALESCE(u.username,'') AS username").
|
||
Joins("LEFT JOIN users u ON u.id = pms_files.user_id").
|
||
Order("pms_files.id DESC").Limit(int(size)).Offset(int((page - 1) * size)).
|
||
Scan(&items).Error
|
||
if err != nil {
|
||
return 0, nil, commonservice.Internal("QUERY_FAILED")
|
||
}
|
||
for i := range items {
|
||
items[i].URL = commonservice.PublicURL(s.Cfg, requestHost, items[i].Name)
|
||
}
|
||
return total, items, nil
|
||
}
|
||
|
||
// Delete 删除记录与磁盘文件。
|
||
func (s *FileService) Delete(userID, fileID int64) error {
|
||
var rec model.File
|
||
if s.DB.First(&rec, fileID).Error != nil {
|
||
return commonservice.NotFound("NOT_FOUND")
|
||
}
|
||
allowed := userID == commonservice.AdminUserID ||
|
||
(userID > 0 && rec.UserID == userID) ||
|
||
(rec.TeamID > 0 && commonservice.IsTeamAdmin(s.DB, rec.TeamID, userID))
|
||
if !allowed {
|
||
return commonservice.Forbidden("FORBIDDEN")
|
||
}
|
||
if err := s.DB.Delete(&model.File{}, fileID).Error; err != nil {
|
||
return commonservice.Internal("DELETE_FAILED")
|
||
}
|
||
_ = os.Remove(filepath.Join(s.Cfg.StorageDir, filepath.FromSlash(rec.Name)))
|
||
return nil
|
||
}
|