Remove deprecated studio package and refactor agent integration - Deleted the studio package, which is no longer in use, to streamline the codebase. - Updated references in the agent and chat modules to utilize the new agent package instead of the deprecated neo package. - Ensured that all related middleware and routing functionalities are now aligned with the agent architecture, enhancing overall system coherence.
266 lines
6.7 KiB
Go
266 lines
6.7 KiB
Go
package s3
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
)
|
|
|
|
// DefaultExpiration default expiration time for presigned URLs (5 minutes)
|
|
const DefaultExpiration = 5 * time.Minute
|
|
|
|
// MaxImageSize maximum image size (1920x1080)
|
|
const MaxImageSize = 1920
|
|
|
|
// Storage the S3 storage driver
|
|
type Storage struct {
|
|
Endpoint string `json:"endpoint" yaml:"endpoint"`
|
|
Region string `json:"region" yaml:"region"`
|
|
Key string `json:"key" yaml:"key"`
|
|
Secret string `json:"secret" yaml:"secret"`
|
|
Bucket string `json:"bucket" yaml:"bucket"`
|
|
Expiration time.Duration `json:"expiration" yaml:"expiration"`
|
|
client *s3.Client
|
|
prefix string
|
|
compression bool
|
|
}
|
|
|
|
// New create a new S3 storage
|
|
func New(options map[string]interface{}) (*Storage, error) {
|
|
storage := &Storage{
|
|
Region: "auto",
|
|
Expiration: DefaultExpiration,
|
|
compression: true,
|
|
}
|
|
|
|
if endpoint, ok := options["endpoint"].(string); ok {
|
|
storage.Endpoint = endpoint
|
|
}
|
|
|
|
if region, ok := options["region"].(string); ok {
|
|
storage.Region = region
|
|
}
|
|
|
|
if key, ok := options["key"].(string); ok {
|
|
storage.Key = key
|
|
}
|
|
|
|
if secret, ok := options["secret"].(string); ok {
|
|
storage.Secret = secret
|
|
}
|
|
|
|
if bucket, ok := options["bucket"].(string); ok {
|
|
storage.Bucket = bucket
|
|
}
|
|
|
|
if prefix, ok := options["prefix"].(string); ok {
|
|
storage.prefix = prefix
|
|
}
|
|
|
|
if exp, ok := options["expiration"].(time.Duration); ok {
|
|
storage.Expiration = exp
|
|
}
|
|
|
|
if compression, ok := options["compression"].(bool); ok {
|
|
storage.compression = compression
|
|
}
|
|
|
|
// Validate required fields
|
|
if storage.Key == "" || storage.Secret == "" {
|
|
return nil, fmt.Errorf("key and secret are required")
|
|
}
|
|
|
|
if storage.Bucket == "" {
|
|
return nil, fmt.Errorf("bucket is required")
|
|
}
|
|
|
|
// Create S3 client
|
|
opts := s3.Options{
|
|
Region: storage.Region,
|
|
Credentials: credentials.NewStaticCredentialsProvider(storage.Key, storage.Secret, ""),
|
|
UsePathStyle: true,
|
|
}
|
|
|
|
if storage.Endpoint != "" {
|
|
// Remove bucket name from endpoint if present
|
|
endpoint := storage.Endpoint
|
|
if strings.Contains(endpoint, "/"+storage.Bucket) {
|
|
endpoint = strings.TrimSuffix(endpoint, "/"+storage.Bucket)
|
|
}
|
|
opts.BaseEndpoint = aws.String(endpoint)
|
|
}
|
|
|
|
storage.client = s3.New(opts)
|
|
return storage, nil
|
|
}
|
|
|
|
// Upload upload file to S3
|
|
func (storage *Storage) Upload(ctx context.Context, filename string, reader io.Reader, contentType string) (string, error) {
|
|
if storage.client == nil {
|
|
return "", fmt.Errorf("s3 client not initialized")
|
|
}
|
|
|
|
// Generate file ID
|
|
fileID := storage.makeID(filename, filepath.Ext(filename))
|
|
key := filepath.Join(storage.prefix, fileID)
|
|
|
|
// Check if compression is enabled and if it's an image
|
|
var body io.Reader
|
|
if storage.compression && isImage(contentType) {
|
|
// Read the entire image into memory
|
|
content, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read image: %w", err)
|
|
}
|
|
|
|
// Compress image
|
|
compressed, err := compressImage(content, contentType)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to compress image: %w", err)
|
|
}
|
|
|
|
body = bytes.NewReader(compressed)
|
|
} else {
|
|
body = reader
|
|
}
|
|
|
|
// Upload file
|
|
_, err := storage.client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: aws.String(storage.Bucket),
|
|
Key: aws.String(key),
|
|
Body: body,
|
|
ContentType: aws.String(contentType),
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to upload file: %w", err)
|
|
}
|
|
|
|
return fileID, nil
|
|
}
|
|
|
|
// Download download file from S3
|
|
func (storage *Storage) Download(ctx context.Context, fileID string) (io.ReadCloser, string, error) {
|
|
if storage.client == nil {
|
|
return nil, "", fmt.Errorf("s3 client not initialized")
|
|
}
|
|
|
|
key := filepath.Join(storage.prefix, fileID)
|
|
|
|
// Get object
|
|
result, err := storage.client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: aws.String(storage.Bucket),
|
|
Key: aws.String(key),
|
|
})
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to download file: %w", err)
|
|
}
|
|
|
|
contentType := "application/octet-stream"
|
|
if result.ContentType != nil {
|
|
contentType = *result.ContentType
|
|
}
|
|
|
|
return result.Body, contentType, nil
|
|
}
|
|
|
|
// URL get file url with expiration
|
|
func (storage *Storage) URL(ctx context.Context, fileID string) string {
|
|
if storage.client == nil {
|
|
return ""
|
|
}
|
|
|
|
key := filepath.Join(storage.prefix, fileID)
|
|
presignClient := s3.NewPresignClient(storage.client)
|
|
request, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: aws.String(storage.Bucket),
|
|
Key: aws.String(key),
|
|
}, s3.WithPresignExpires(storage.Expiration))
|
|
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return request.URL
|
|
}
|
|
|
|
func (storage *Storage) makeID(filename string, ext string) string {
|
|
date := time.Now().Format("20060102")
|
|
name := strings.TrimSuffix(filepath.Base(filename), ext)
|
|
return fmt.Sprintf("%s/%s-%d%s", date, name, time.Now().UnixNano(), ext)
|
|
}
|
|
|
|
// isImage checks if the content type is an image
|
|
func isImage(contentType string) bool {
|
|
return strings.HasPrefix(contentType, "image/")
|
|
}
|
|
|
|
// compressImage compresses the image while maintaining aspect ratio
|
|
func compressImage(data []byte, contentType string) ([]byte, error) {
|
|
// Decode image
|
|
img, _, err := image.Decode(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode image: %w", err)
|
|
}
|
|
|
|
// Calculate new dimensions
|
|
bounds := img.Bounds()
|
|
width := bounds.Dx()
|
|
height := bounds.Dy()
|
|
var newWidth, newHeight int
|
|
|
|
if width > height {
|
|
if width > MaxImageSize {
|
|
newWidth = MaxImageSize
|
|
newHeight = int(float64(height) * (float64(MaxImageSize) / float64(width)))
|
|
} else {
|
|
return data, nil // No need to resize
|
|
}
|
|
} else {
|
|
if height > MaxImageSize {
|
|
newHeight = MaxImageSize
|
|
newWidth = int(float64(width) * (float64(MaxImageSize) / float64(height)))
|
|
} else {
|
|
return data, nil // No need to resize
|
|
}
|
|
}
|
|
|
|
// Create new image with new dimensions
|
|
newImg := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
|
|
|
|
// Scale the image using bilinear interpolation
|
|
for y := 0; y < newHeight; y++ {
|
|
for x := 0; x < newWidth; x++ {
|
|
srcX := float64(x) * float64(width) / float64(newWidth)
|
|
srcY := float64(y) * float64(height) / float64(newHeight)
|
|
newImg.Set(x, y, img.At(int(srcX), int(srcY)))
|
|
}
|
|
}
|
|
|
|
// Encode image
|
|
var buf bytes.Buffer
|
|
switch contentType {
|
|
case "image/jpeg":
|
|
err = jpeg.Encode(&buf, newImg, &jpeg.Options{Quality: 85})
|
|
case "image/png":
|
|
err = png.Encode(&buf, newImg)
|
|
default:
|
|
return data, nil // Unsupported format, return original
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encode image: %w", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|