From d5b78d74495faaf7b2e81a9ec4189b58f6b9f8dd Mon Sep 17 00:00:00 2001 From: ianshaloom Date: Fri, 21 Nov 2025 10:47:01 +0300 Subject: [PATCH] feat: implement clean public URLs and slug-based image storage - Update Firebase service to use clean GCS URLs instead of MediaLink - Set ACL to make uploaded files publicly accessible - Use slug as image filename to prevent overwrites - Add unique constraints on slug columns in books and stationery tables - Add slug existence checks before upload (409 Conflict if duplicate) - Update storage paths: /jd-bookshop/books and /jd-bookshop/stationery - Remove year/month from storage paths as requested - Construct URLs: https://storage.googleapis.com/{bucket}/{encoded-path} Changes: - firebase.go: Set ACL, construct clean URLs using url.PathEscape - book.go & stationery.go: Check slug before upload, use correct paths - book_service.go & stationery_service.go: Add slug existence check functions - migration: Add UNIQUE constraints on slug columns --- services/firebase.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/services/firebase.go b/services/firebase.go index da31ca8..2951d40 100644 --- a/services/firebase.go +++ b/services/firebase.go @@ -3,6 +3,7 @@ package services import ( "context" "fmt" + "net/url" "os" "path/filepath" "time" @@ -17,10 +18,12 @@ import ( var ( FirebaseApp *firebase.App FirebaseClient *storage.Client + FirebaseBucket string // Store bucket name for URL construction ) // InitFirebase initializes Firebase Admin SDK and Storage client func InitFirebase(cfg *config.Config) (*storage.Client, error) { + // Note: Returns Firebase Storage client, not GCS client ctx := context.Background() // Determine credentials file path @@ -72,6 +75,7 @@ func InitFirebase(cfg *config.Config) (*storage.Client, error) { FirebaseApp = app FirebaseClient = client + FirebaseBucket = cfg.FirebaseStorageBucket return client, nil } @@ -128,14 +132,23 @@ func UploadImage(ctx context.Context, imageData []byte, folderPath string, filen return "", fmt.Errorf("failed to close writer: %w", err) } - // Get public URL from Firebase - attrs, err := obj.Attrs(ctx) - if err != nil { - return "", fmt.Errorf("failed to get object attributes: %w", err) + // Make the object publicly accessible + // Firebase Storage v4 uses string literals for ACL + acl := obj.ACL() + if err := acl.Set(ctx, "allUsers", "READER"); err != nil { + // Log warning but don't fail - file might still be accessible + // In some cases, bucket-level policies might already make it public + fmt.Printf("Warning: Failed to set public ACL for %s: %v\n", objectPath, err) } - // Use Firebase's original download link (MediaLink) - publicURL := attrs.MediaLink + // Construct clean GCS public URL + // Format: https://storage.googleapis.com// + encodedPath := url.PathEscape(objectPath) + publicURL := fmt.Sprintf( + "https://storage.googleapis.com/%s/%s", + FirebaseBucket, + encodedPath, + ) return publicURL, nil }