Introduces the Go photo-api service, nginx/systemd deploy wiring, and Next.js gallery/lightbox pages so event photos can be managed and browsed. Co-authored-by: Cursor <cursoragent@cursor.com>
93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
|
|
"git.azzamo.net/Michilis/Spanglish/photo-api/internal/config"
|
|
)
|
|
|
|
// s3Store targets AWS S3 or path-style compatibles (Garage/MinIO), same as
|
|
// the backend's S3 backend. The bucket is never public: downloads use
|
|
// short-lived presigned GETs so visibility rules keep holding on S3.
|
|
type s3Store struct {
|
|
client *s3.Client
|
|
presign *s3.PresignClient
|
|
bucket string
|
|
}
|
|
|
|
func newS3(cfg config.Config) (*s3Store, error) {
|
|
awsCfg, err := awsconfig.LoadDefaultConfig(context.Background(),
|
|
awsconfig.WithRegion(cfg.S3Region),
|
|
awsconfig.WithCredentialsProvider(
|
|
credentials.NewStaticCredentialsProvider(cfg.S3AccessKeyID, cfg.S3SecretKey, "")),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("s3 config: %w", err)
|
|
}
|
|
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
|
o.BaseEndpoint = aws.String(cfg.S3Endpoint)
|
|
o.UsePathStyle = cfg.S3ForcePathStyle
|
|
})
|
|
return &s3Store{
|
|
client: client,
|
|
presign: s3.NewPresignClient(client),
|
|
bucket: cfg.S3Bucket,
|
|
}, nil
|
|
}
|
|
|
|
func (s *s3Store) Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error {
|
|
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: aws.String(s.bucket),
|
|
Key: aws.String(key),
|
|
Body: r,
|
|
ContentLength: aws.Int64(size),
|
|
ContentType: aws.String(contentType),
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *s3Store) Open(ctx context.Context, key string) (io.ReadCloser, int64, error) {
|
|
out, err := s.client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: aws.String(s.bucket),
|
|
Key: aws.String(key),
|
|
})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return out.Body, aws.ToInt64(out.ContentLength), nil
|
|
}
|
|
|
|
func (s *s3Store) Delete(ctx context.Context, key string) error {
|
|
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
|
Bucket: aws.String(s.bucket),
|
|
Key: aws.String(key),
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *s3Store) PresignGet(ctx context.Context, key, downloadFilename, contentType string, expiry time.Duration) (string, error) {
|
|
in := &s3.GetObjectInput{
|
|
Bucket: aws.String(s.bucket),
|
|
Key: aws.String(key),
|
|
}
|
|
if contentType != "" {
|
|
in.ResponseContentType = aws.String(contentType)
|
|
}
|
|
if downloadFilename != "" {
|
|
in.ResponseContentDisposition = aws.String(fmt.Sprintf("attachment; filename=%q", downloadFilename))
|
|
}
|
|
req, err := s.presign.PresignGetObject(ctx, in, s3.WithPresignExpires(expiry))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return req.URL, nil
|
|
}
|