Skip to content

Commit 9740fed

Browse files
committed
feat: add support for public endpoint
1 parent 4416467 commit 9740fed

File tree

3 files changed

+71
-49
lines changed

3 files changed

+71
-49
lines changed

internal/config/config.go

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,38 @@ import (
1111
)
1212

1313
type Config struct {
14-
Port string
15-
S3Endpoint string
16-
S3Bucket string
17-
S3Region string
18-
AWSAccessKey string
19-
AWSSecretKey string
20-
CacheMaxAge string
14+
Port string
15+
S3Endpoint string // Endpoint for internal operations
16+
PublicS3Endpoint string // Endpoint for presigned URLs (falls back to S3Endpoint if not set)
17+
S3Bucket string
18+
S3Region string
19+
AWSAccessKey string
20+
AWSSecretKey string
21+
CacheMaxAge string
2122
// API authentication
22-
APIKey string
23+
APIKey string
2324
}
2425

2526
func Load() *Config {
27+
s3Endpoint := getEnv("S3_ENDPOINT", "")
28+
publicEndpoint := getEnv("PUBLIC_S3_ENDPOINT", "")
29+
30+
// If PUBLIC_S3_ENDPOINT is not set, fall back to S3_ENDPOINT
31+
if publicEndpoint == "" {
32+
publicEndpoint = s3Endpoint
33+
}
34+
2635
return &Config{
27-
Port: getEnv("PORT", "8080"),
28-
S3Endpoint: getEnv("S3_ENDPOINT", ""),
29-
S3Bucket: getEnv("S3_BUCKET", ""),
30-
S3Region: getEnv("S3_REGION", "us-east-1"),
31-
AWSAccessKey: getEnv("AWS_ACCESS_KEY_ID", ""),
32-
AWSSecretKey: getEnv("AWS_SECRET_ACCESS_KEY", ""),
33-
CacheMaxAge: getEnv("CACHE_MAX_AGE", "86400"),
36+
Port: getEnv("PORT", "8080"),
37+
S3Endpoint: s3Endpoint,
38+
PublicS3Endpoint: publicEndpoint,
39+
S3Bucket: getEnv("S3_BUCKET", ""),
40+
S3Region: getEnv("S3_REGION", "us-east-1"),
41+
AWSAccessKey: getEnv("AWS_ACCESS_KEY_ID", ""),
42+
AWSSecretKey: getEnv("AWS_SECRET_ACCESS_KEY", ""),
43+
CacheMaxAge: getEnv("CACHE_MAX_AGE", "86400"),
3444
// API authentication
35-
APIKey: getEnv("API_KEY", ""),
45+
APIKey: getEnv("API_KEY", ""),
3646
}
3747
}
3848

@@ -47,17 +57,17 @@ type Profile struct {
4757
TokenTTLSeconds int64 `yaml:"token_ttl_seconds"`
4858
StoragePath string `yaml:"storage_path"`
4959
EnableSharding bool `yaml:"enable_sharding"`
50-
60+
5161
// Processing configuration (shared)
52-
ThumbFolder string `yaml:"thumb_folder,omitempty"`
53-
Quality int `yaml:"quality,omitempty"`
54-
CacheDuration int `yaml:"cache_duration,omitempty"` // in seconds
55-
62+
ThumbFolder string `yaml:"thumb_folder,omitempty"`
63+
Quality int `yaml:"quality,omitempty"`
64+
CacheDuration int `yaml:"cache_duration,omitempty"` // in seconds
65+
5666
// Processing configuration (images)
5767
Sizes []string `yaml:"sizes,omitempty"`
5868
DefaultSize string `yaml:"default_size,omitempty"`
5969
ConvertTo string `yaml:"convert_to,omitempty"`
60-
70+
6171
// Processing configuration (videos)
6272
ProxyFolder string `yaml:"proxy_folder,omitempty"`
6373
Formats []string `yaml:"formats,omitempty"`
@@ -138,24 +148,22 @@ func (sc *StorageConfig) GetProfile(profileName string) *Profile {
138148
return nil
139149
}
140150

141-
142151
func DefaultProfile() *Profile {
143152
return &Profile{
144153
Kind: "image",
145154
AllowedMimes: []string{"image/jpeg", "image/png"},
146155
SizeMaxBytes: 10485760, // 10MB
147156
MultipartThresholdMB: 15,
148-
PartSizeMB: 8,
149-
TokenTTLSeconds: 900,
150-
StoragePath: "originals/{shard?}/{key_base}",
151-
EnableSharding: true,
152-
ThumbFolder: "thumbnails",
153-
Sizes: []string{"256", "512", "1024"},
154-
Quality: 90,
157+
PartSizeMB: 8,
158+
TokenTTLSeconds: 900,
159+
StoragePath: "originals/{shard?}/{key_base}",
160+
EnableSharding: true,
161+
ThumbFolder: "thumbnails",
162+
Sizes: []string{"256", "512", "1024"},
163+
Quality: 90,
155164
}
156165
}
157166

158-
159167
func getEnv(key, defaultValue string) string {
160168
if value := os.Getenv(key); value != "" {
161169
return value

internal/s3/client.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ import (
1616
)
1717

1818
type Client struct {
19-
s3Client *s3.Client
20-
bucket string
21-
presigner *s3.PresignClient
19+
s3Client *s3.Client
20+
bucket string
21+
presigner *s3.PresignClient
2222
}
2323

24-
func NewClient(ctx context.Context, region, bucket, accessKey, secretKey, endpoint string) (*Client, error) {
24+
func NewClient(ctx context.Context, region, bucket, accessKey, secretKey, endpoint, publicEndpoint string) (*Client, error) {
2525
var cfg aws.Config
2626
var err error
2727

@@ -40,14 +40,27 @@ func NewClient(ctx context.Context, region, bucket, accessKey, secretKey, endpoi
4040
utils.Shutdown(fmt.Sprintf("🚨 Failed to load AWS config: %v", err))
4141
}
4242

43+
// Create S3 client for internal operations
4344
s3Client := s3.NewFromConfig(cfg, func(o *s3.Options) {
4445
if endpoint != "" {
4546
o.BaseEndpoint = aws.String(endpoint)
4647
o.UsePathStyle = true
4748
}
4849
})
4950

50-
presigner := s3.NewPresignClient(s3Client)
51+
// Create presigner - use public endpoint if different from internal
52+
var presigner *s3.PresignClient
53+
if publicEndpoint != "" && publicEndpoint != endpoint {
54+
// Create a separate S3 client for presigning with the public endpoint
55+
publicS3Client := s3.NewFromConfig(cfg, func(o *s3.Options) {
56+
o.BaseEndpoint = aws.String(publicEndpoint)
57+
o.UsePathStyle = true
58+
})
59+
presigner = s3.NewPresignClient(publicS3Client)
60+
} else {
61+
// Use the same client for presigning
62+
presigner = s3.NewPresignClient(s3Client)
63+
}
5164

5265
return &Client{
5366
s3Client: s3Client,

internal/service/image.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func NewImageService(cfg *config.Config) *ImageService {
3131
cfg.AWSAccessKey,
3232
cfg.AWSSecretKey,
3333
cfg.S3Endpoint,
34+
cfg.PublicS3Endpoint,
3435
)
3536
if err != nil {
3637
panic(fmt.Sprintf("Failed to create S3 client: %v", err))
@@ -52,18 +53,18 @@ func (s *ImageService) buildStoragePath(template, filename string, enableShardin
5253
if len(parts) > 1 {
5354
ext = parts[len(parts)-1]
5455
}
55-
56+
5657
// Generate shard if sharding is enabled
5758
shard := ""
5859
if enableSharding {
5960
shard = s.generateShard(keyBase)
6061
}
61-
62+
6263
// Template replacement for image retrieval
6364
path := template
6465
path = strings.ReplaceAll(path, "{key_base}", keyBase)
6566
path = strings.ReplaceAll(path, "{ext}", ext)
66-
67+
6768
// Handle shard placeholders
6869
if shard != "" {
6970
path = strings.ReplaceAll(path, "{shard?}", shard)
@@ -74,7 +75,7 @@ func (s *ImageService) buildStoragePath(template, filename string, enableShardin
7475
path = strings.ReplaceAll(path, "{shard?}/", "")
7576
path = strings.ReplaceAll(path, "{shard?}", "")
7677
}
77-
78+
7879
return path
7980
}
8081

@@ -87,7 +88,7 @@ func (s *ImageService) generateShard(keyBase string) string {
8788
func (s *ImageService) UploadImage(ctx context.Context, profile *config.Profile, imageData []byte, thumbType, imagePath string) error {
8889
orig_path := s.buildStoragePath(profile.StoragePath, imagePath, profile.EnableSharding)
8990
convertType := profile.ConvertTo
90-
91+
9192
// Upload original image in parallel with thumbnail generation
9293
origUploadChan := make(chan error, 1)
9394
go func() {
@@ -106,10 +107,10 @@ func (s *ImageService) UploadImage(ctx context.Context, profile *config.Profile,
106107
path string
107108
err error
108109
}
109-
110+
110111
thumbJobs := make(chan thumbnailJob, len(profile.Sizes))
111112
uploadErrors := make(chan error, len(profile.Sizes))
112-
113+
113114
// Generate thumbnails in parallel
114115
for _, sizeStr := range profile.Sizes {
115116
go func(size string) {
@@ -127,7 +128,7 @@ func (s *ImageService) UploadImage(ctx context.Context, profile *config.Profile,
127128

128129
thumbSizePath := s.createThumbnailPathForSize(imagePath, size, convertType)
129130
thumbFullPath := fmt.Sprintf("%s/%s", profile.ThumbFolder, thumbSizePath)
130-
131+
131132
thumbJobs <- thumbnailJob{
132133
sizeStr: size,
133134
data: thumbnailData,
@@ -145,7 +146,7 @@ func (s *ImageService) UploadImage(ctx context.Context, profile *config.Profile,
145146
uploadErrors <- job.err
146147
return
147148
}
148-
149+
149150
err := s.S3Client.PutObject(ctx, job.path, bytes.NewReader(job.data))
150151
if err != nil {
151152
uploadErrors <- fmt.Errorf("failed to upload thumbnail for size %s: %w", job.sizeStr, err)
@@ -175,25 +176,25 @@ func (s *ImageService) generateThumbnail(imageData []byte, width, quality int, c
175176
Width: width,
176177
Quality: quality,
177178
}
178-
179+
179180
// Set output format
180181
switch convertTo {
181182
case "webp":
182183
options.Type = bimg.WEBP
183184
case "jpeg", "jpg":
184-
options.Type = bimg.JPEG
185+
options.Type = bimg.JPEG
185186
case "png":
186187
options.Type = bimg.PNG
187188
default:
188189
// Default to JPEG if format is unknown (fallback)
189190
options.Type = bimg.JPEG
190191
}
191-
192+
192193
resizedData, err := bimg.NewImage(imageData).Process(options)
193194
if err != nil {
194195
return nil, fmt.Errorf("failed to process image with bimg: %w", err)
195196
}
196-
197+
197198
return resizedData, nil
198199
}
199200

0 commit comments

Comments
 (0)