Enhance attachment upload functionality with image compression and metadata caching

- Introduced new fields for image compression options in the UploadChunk struct, allowing for better handling of image uploads.
- Updated the upload process to utilize cached compression settings from the first chunk, improving efficiency and consistency.
- Implemented a fallback mechanism for image compression, logging errors and using original files when compression fails.
- Refactored the compressStoredImage function to return the compressed size, enhancing the upload process and error handling.
This commit is contained in:
Max 2025-11-06 11:53:24 +08:00
parent e435c03e0e
commit 2f6cf1a08c

View file

@ -22,6 +22,7 @@ import (
"github.com/yaoapp/gou/fs" "github.com/yaoapp/gou/fs"
"github.com/yaoapp/gou/model" "github.com/yaoapp/gou/model"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/attachment/local" "github.com/yaoapp/yao/attachment/local"
"github.com/yaoapp/yao/attachment/s3" "github.com/yaoapp/yao/attachment/s3"
"github.com/yaoapp/yao/config" "github.com/yaoapp/yao/config"
@ -44,6 +45,8 @@ type UploadChunk struct {
ContentType string ContentType string
Filename string Filename string
UserPath string UserPath string
CompressImage bool
CompressSize int
} }
// Parse parses an attachment wrapper string and returns uploader name and file ID // Parse parses an attachment wrapper string and returns uploader name and file ID
@ -403,6 +406,8 @@ func (manager Manager) Upload(ctx context.Context, fileheader *FileHeader, reade
ContentType: file.ContentType, ContentType: file.ContentType,
Filename: file.Filename, Filename: file.Filename,
UserPath: file.UserPath, UserPath: file.UserPath,
CompressImage: option.CompressImage,
CompressSize: option.CompressSize,
}) })
} }
@ -462,19 +467,28 @@ func (manager Manager) Upload(ctx context.Context, fileheader *FileHeader, reade
return nil, err return nil, err
} }
// Set initial file size from chunks
file.Bytes = int(chunkdata.Total)
// Apply image compression if requested and it's the final file // Apply image compression if requested and it's the final file
if option.CompressImage && strings.HasPrefix(file.ContentType, "image/") { // Use cached compress options from first chunk
err = manager.compressStoredImage(ctx, file, option) if chunkdata.CompressImage && strings.HasPrefix(file.ContentType, "image/") {
// Create a temporary option with cached compress size
compressOption := UploadOption{
CompressSize: chunkdata.CompressSize,
}
compressedBytes, err := manager.compressStoredImageAndGetSize(ctx, file, compressOption)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Update file size to compressed size
file.Bytes = compressedBytes
} }
// Remove the chunk data // Remove the chunk data
uploadChunks.Delete(file.ID) uploadChunks.Delete(file.ID)
// Fix the file size and update status to uploaded // Update status to uploaded
file.Bytes = int(chunkdata.Total)
file.Status = "uploaded" file.Status = "uploaded"
// Update only bytes and status for the last chunk // Update only bytes and status for the last chunk
@ -507,6 +521,10 @@ func (manager Manager) Upload(ctx context.Context, fileheader *FileHeader, reade
size = 1920 size = 1920
} }
// Read original data for fallback
var originalData []byte
var err error
// If gzip was applied, we need to decompress first // If gzip was applied, we need to decompress first
if option.Gzip { if option.Gzip {
data, err := io.ReadAll(finalReader) data, err := io.ReadAll(finalReader)
@ -517,13 +535,25 @@ func (manager Manager) Upload(ctx context.Context, fileheader *FileHeader, reade
if err != nil { if err != nil {
return nil, err return nil, err
} }
originalData = decompressed
finalReader = bytes.NewReader(decompressed) finalReader = bytes.NewReader(decompressed)
} } else {
originalData, err = io.ReadAll(finalReader)
compressed, err := CompressImage(finalReader, file.ContentType, size)
if err != nil { if err != nil {
return nil, err return nil, err
} }
finalReader = bytes.NewReader(originalData)
}
// Try to compress the image with failback mechanism
compressed, err := CompressImage(finalReader, file.ContentType, size)
if err != nil {
// Log the error and use original file as fallback
log.Warn("Failed to compress image (content-type: %s, file: %s): %v. Using original file.",
file.ContentType, file.Filename, err)
// Use original data
compressed = originalData
}
// Re-apply gzip if it was requested // Re-apply gzip if it was requested
if option.Gzip { if option.Gzip {
@ -560,12 +590,12 @@ func (manager Manager) Upload(ctx context.Context, fileheader *FileHeader, reade
return file, nil return file, nil
} }
// compressStoredImage compresses an already stored image // compressStoredImageAndGetSize compresses the stored image and returns the compressed size
func (manager Manager) compressStoredImage(ctx context.Context, file *File, option UploadOption) error { func (manager Manager) compressStoredImageAndGetSize(ctx context.Context, file *File, option UploadOption) (int, error) {
// Download the stored file using storage path // Download the stored file using storage path
reader, err := manager.storage.Reader(ctx, file.Path) reader, err := manager.storage.Reader(ctx, file.Path)
if err != nil { if err != nil {
return err return 0, err
} }
defer reader.Close() defer reader.Close()
@ -574,15 +604,30 @@ func (manager Manager) compressStoredImage(ctx context.Context, file *File, opti
size = 1920 size = 1920
} }
// Compress the image // Read original data for fallback
compressed, err := CompressImage(reader, file.ContentType, size) originalData, err := io.ReadAll(reader)
if err != nil { if err != nil {
return err return 0, err
}
// Try to compress the image with failback mechanism
compressed, err := CompressImage(bytes.NewReader(originalData), file.ContentType, size)
if err != nil {
// Log the error and keep original file
log.Warn("Failed to compress stored image (content-type: %s, file: %s): %v. Keeping original file.",
file.ContentType, file.Filename, err)
// File is already stored (merged chunks), just return original size
return len(originalData), nil
} }
// Re-upload the compressed image using storage path // Re-upload the compressed image using storage path
_, err = manager.storage.Upload(ctx, file.Path, bytes.NewReader(compressed), file.ContentType) _, err = manager.storage.Upload(ctx, file.Path, bytes.NewReader(compressed), file.ContentType)
return err if err != nil {
return 0, err
}
// Return the compressed size
return len(compressed), nil
} }
// Download downloads a file // Download downloads a file