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
This commit is contained in:
@@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,10 +18,12 @@ import (
|
|||||||
var (
|
var (
|
||||||
FirebaseApp *firebase.App
|
FirebaseApp *firebase.App
|
||||||
FirebaseClient *storage.Client
|
FirebaseClient *storage.Client
|
||||||
|
FirebaseBucket string // Store bucket name for URL construction
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitFirebase initializes Firebase Admin SDK and Storage client
|
// InitFirebase initializes Firebase Admin SDK and Storage client
|
||||||
func InitFirebase(cfg *config.Config) (*storage.Client, error) {
|
func InitFirebase(cfg *config.Config) (*storage.Client, error) {
|
||||||
|
// Note: Returns Firebase Storage client, not GCS client
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Determine credentials file path
|
// Determine credentials file path
|
||||||
@@ -72,6 +75,7 @@ func InitFirebase(cfg *config.Config) (*storage.Client, error) {
|
|||||||
|
|
||||||
FirebaseApp = app
|
FirebaseApp = app
|
||||||
FirebaseClient = client
|
FirebaseClient = client
|
||||||
|
FirebaseBucket = cfg.FirebaseStorageBucket
|
||||||
|
|
||||||
return client, nil
|
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)
|
return "", fmt.Errorf("failed to close writer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get public URL from Firebase
|
// Make the object publicly accessible
|
||||||
attrs, err := obj.Attrs(ctx)
|
// Firebase Storage v4 uses string literals for ACL
|
||||||
if err != nil {
|
acl := obj.ACL()
|
||||||
return "", fmt.Errorf("failed to get object attributes: %w", err)
|
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)
|
// Construct clean GCS public URL
|
||||||
publicURL := attrs.MediaLink
|
// Format: https://storage.googleapis.com/<bucket>/<path>
|
||||||
|
encodedPath := url.PathEscape(objectPath)
|
||||||
|
publicURL := fmt.Sprintf(
|
||||||
|
"https://storage.googleapis.com/%s/%s",
|
||||||
|
FirebaseBucket,
|
||||||
|
encodedPath,
|
||||||
|
)
|
||||||
|
|
||||||
return publicURL, nil
|
return publicURL, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user