package services import ( "context" "fmt" "os" "path/filepath" "time" "jd-book-uploader/config" firebase "firebase.google.com/go/v4" "firebase.google.com/go/v4/storage" "google.golang.org/api/option" ) var ( FirebaseApp *firebase.App FirebaseClient *storage.Client ) // InitFirebase initializes Firebase Admin SDK and Storage client func InitFirebase(cfg *config.Config) (*storage.Client, error) { ctx := context.Background() // Determine credentials file path credentialsFile := cfg.FirebaseCredentialsFile if credentialsFile == "" { // Fallback to GOOGLE_APPLICATION_CREDENTIALS env var credentialsFile = os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") } // Check if credentials file exists if credentialsFile != "" { if _, err := os.Stat(credentialsFile); os.IsNotExist(err) { return nil, fmt.Errorf("firebase credentials file not found: %s", credentialsFile) } // Convert relative path to absolute if !filepath.IsAbs(credentialsFile) { credentialsFile, _ = filepath.Abs(credentialsFile) } } // Build Firebase config firebaseConfig := &firebase.Config{ ProjectID: cfg.FirebaseProjectID, StorageBucket: cfg.FirebaseStorageBucket, } // Initialize Firebase app var app *firebase.App var err error if credentialsFile != "" { // Use credentials file opt := option.WithCredentialsFile(credentialsFile) app, err = firebase.NewApp(ctx, firebaseConfig, opt) } else { // Use default credentials (from environment) app, err = firebase.NewApp(ctx, firebaseConfig) } if err != nil { return nil, fmt.Errorf("failed to initialize Firebase app: %w", err) } // Create Storage client client, err := app.Storage(ctx) if err != nil { return nil, fmt.Errorf("failed to create Firebase Storage client: %w", err) } FirebaseApp = app FirebaseClient = client return client, nil } // UploadImage uploads an image to Firebase Storage and returns the public URL // folderPath: storage folder path (e.g., "/jd-bookshop/books" or "/jd-bookshop/stationery") // filename: image filename (should be the slug with .png extension) func UploadImage(ctx context.Context, imageData []byte, folderPath string, filename string) (string, error) { if FirebaseClient == nil { return "", fmt.Errorf("firebase client not initialized") } // Build object path: {folderPath}/{filename} // Use provided path strictly without adding year/month objectPath := fmt.Sprintf("%s/%s", folderPath, filename) // Get bucket handle bucket, err := FirebaseClient.DefaultBucket() if err != nil { return "", fmt.Errorf("failed to get default bucket: %w", err) } // Create object handle obj := bucket.Object(objectPath) // Create writer writer := obj.NewWriter(ctx) writer.ContentType = "image/png" writer.CacheControl = "public, max-age=31536000" // 1 year cache // Write image data with retry logic maxRetries := 3 var writeErr error for i := 0; i < maxRetries; i++ { if i > 0 { // Wait before retry (exponential backoff) time.Sleep(time.Duration(i) * time.Second) } // Write data _, writeErr = writer.Write(imageData) if writeErr == nil { break } } if writeErr != nil { writer.Close() return "", fmt.Errorf("failed to write image data after %d attempts: %w", maxRetries, writeErr) } // Close writer to finalize upload if err := writer.Close(); err != nil { 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) } // Use Firebase's original download link (MediaLink) publicURL := attrs.MediaLink return publicURL, nil }