[feature/backend] overall enhancement of image uploading
All checks were successful
Build Backend / Build Docker Image (push) Successful in 5m3s

This commit is contained in:
CDN 2025-02-23 04:42:48 +08:00
parent 6e1be3d513
commit 3e6181e578
Signed by: CDN
GPG key ID: 0C656827F9F80080
13 changed files with 740 additions and 314 deletions

View file

@ -11,6 +11,8 @@ import (
"path/filepath"
"strings"
"time"
"github.com/rs/zerolog/log"
)
type LocalStorage struct {
@ -44,6 +46,24 @@ func (s *LocalStorage) generateID() (string, error) {
return hex.EncodeToString(bytes), nil
}
func (s *LocalStorage) generateFilePath(id string, ext string, createTime time.Time) string {
// Create year/month directory structure
year := createTime.Format("2006")
month := createTime.Format("01")
// If id already has an extension, don't add ext
if filepath.Ext(id) != "" {
return filepath.Join(year, month, id)
}
// Otherwise, add the extension if provided
filename := id
if ext != "" {
filename = id + ext
}
return filepath.Join(year, month, filename)
}
func (s *LocalStorage) saveMetadata(id string, info *FileInfo) error {
metaPath := filepath.Join(s.metaDir, id+".meta")
file, err := os.Create(metaPath)
@ -89,11 +109,64 @@ func (s *LocalStorage) Save(ctx context.Context, name string, contentType string
return nil, fmt.Errorf("failed to generate file ID: %w", err)
}
// Create the file path
filePath := filepath.Join(s.rootDir, id)
// Get file extension from original name or content type
ext := filepath.Ext(name)
if ext == "" {
// If no extension in name, try to get it from content type
switch contentType {
case "image/jpeg":
ext = ".jpg"
case "image/png":
ext = ".png"
case "image/gif":
ext = ".gif"
case "image/webp":
ext = ".webp"
case "image/svg+xml":
ext = ".svg"
case "video/mp4":
ext = ".mp4"
case "video/webm":
ext = ".webm"
case "audio/mpeg":
ext = ".mp3"
case "audio/ogg":
ext = ".ogg"
case "audio/wav":
ext = ".wav"
case "application/pdf":
ext = ".pdf"
case "application/msword":
ext = ".doc"
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
ext = ".docx"
case "application/vnd.ms-excel":
ext = ".xls"
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
ext = ".xlsx"
case "application/zip":
ext = ".zip"
case "application/x-rar-compressed":
ext = ".rar"
case "text/plain":
ext = ".txt"
case "text/csv":
ext = ".csv"
}
}
// Create the file path with year/month structure
now := time.Now()
relPath := s.generateFilePath(id, ext, now)
fullPath := filepath.Join(s.rootDir, relPath)
// Create directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}
// Create the file
file, err := os.Create(filePath)
file, err := os.Create(fullPath)
if err != nil {
return nil, fmt.Errorf("failed to create file: %w", err)
}
@ -102,52 +175,73 @@ func (s *LocalStorage) Save(ctx context.Context, name string, contentType string
// Copy the content
size, err := io.Copy(file, reader)
if err != nil {
// Clean up the file if there's an error
os.Remove(filePath)
os.Remove(fullPath) // Clean up on error
return nil, fmt.Errorf("failed to write file content: %w", err)
}
now := time.Now()
// Save metadata
info := &FileInfo{
ID: id,
Name: name,
Size: size,
ContentType: contentType,
Size: size,
CreatedAt: now,
UpdatedAt: now,
URL: fmt.Sprintf("/api/media/file/%s", id),
URL: fmt.Sprintf("/media/%s/%s/%s", now.Format("2006"), now.Format("01"), filepath.Base(relPath)),
}
// Save metadata
if err := s.saveMetadata(id, info); err != nil {
os.Remove(filePath)
return nil, err
os.Remove(fullPath) // Clean up on error
return nil, fmt.Errorf("failed to save metadata: %w", err)
}
return info, nil
}
func (s *LocalStorage) Get(ctx context.Context, id string) (io.ReadCloser, *FileInfo, error) {
filePath := filepath.Join(s.rootDir, id)
// 从 id 中提取文件扩展名和基础 ID
ext := filepath.Ext(id)
baseID := strings.TrimSuffix(id, ext)
// Open the file
// 获取文件的创建时间(从元数据或当前时间)
metaPath := filepath.Join(s.metaDir, baseID+".meta")
var createTime time.Time
if stat, err := os.Stat(metaPath); err == nil {
createTime = stat.ModTime()
} else {
createTime = time.Now() // 如果找不到元数据,使用当前时间
}
// 生成完整的文件路径
year := createTime.Format("2006")
month := createTime.Format("01")
filePath := filepath.Join(s.rootDir, year, month, id) // 直接使用完整的 id包含扩展名
// 调试日志
log.Debug().
Str("id", id).
Str("baseID", baseID).
Str("ext", ext).
Str("filePath", filePath).
Time("createTime", createTime).
Msg("Attempting to get file")
// 打开文件
file, err := os.Open(filePath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil, fmt.Errorf("file not found: %s", id)
return nil, nil, fmt.Errorf("file not found: %s (path: %s)", id, filePath)
}
return nil, nil, fmt.Errorf("failed to open file: %w", err)
}
// Get file info
// 获取文件信息
stat, err := file.Stat()
if err != nil {
file.Close()
return nil, nil, fmt.Errorf("failed to get file info: %w", err)
}
// Load metadata
name, contentType, err := s.loadMetadata(id)
// 加载元数据
name, contentType, err := s.loadMetadata(baseID)
if err != nil {
file.Close()
return nil, nil, err
@ -158,27 +252,44 @@ func (s *LocalStorage) Get(ctx context.Context, id string) (io.ReadCloser, *File
Name: name,
Size: stat.Size(),
ContentType: contentType,
CreatedAt: stat.ModTime(),
CreatedAt: createTime,
UpdatedAt: stat.ModTime(),
URL: fmt.Sprintf("/api/media/file/%s", id),
URL: fmt.Sprintf("/media/%s/%s/%s", year, month, id),
}
return file, info, nil
}
func (s *LocalStorage) Delete(ctx context.Context, id string) error {
filePath := filepath.Join(s.rootDir, id)
if err := os.Remove(filePath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("file not found: %s", id)
}
return fmt.Errorf("failed to delete file: %w", err)
// 从 id 中提取文件扩展名
ext := filepath.Ext(id)
baseID := strings.TrimSuffix(id, ext)
// 获取文件的创建时间(从元数据或当前时间)
metaPath := filepath.Join(s.metaDir, baseID+".meta")
var createTime time.Time
if stat, err := os.Stat(metaPath); err == nil {
createTime = stat.ModTime()
} else {
createTime = time.Now() // 如果找不到元数据,使用当前时间
}
// Remove metadata
metaPath := filepath.Join(s.metaDir, id+".meta")
if err := os.Remove(metaPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove metadata: %w", err)
// 生成完整的文件路径
relPath := s.generateFilePath(baseID, ext, createTime)
filePath := filepath.Join(s.rootDir, relPath)
// 删除文件
if err := os.Remove(filePath); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to delete file: %w", err)
}
}
// 删除元数据
if err := os.Remove(metaPath); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to delete metadata: %w", err)
}
}
return nil