Add initial project structure with core functionality for book and stationery uploads
- Created main application entry point in `main.go`. - Added configuration management in `config/config.go` and tests in `config/config_test.go`. - Implemented handlers for book and stationery uploads in `handlers/book.go` and `handlers/stationery.go`, including validation logic. - Established database connection and services in `services/database.go` and `services/book_service.go`. - Defined models for books and stationery in `models/book.go` and `models/stationery.go`. - Set up Firebase integration for image uploads in `services/firebase.go`. - Created migration scripts for database schema in `migrations/001_create_tables.sql` and subsequent updates. - Added CORS and error handling middleware. - Included comprehensive tests for handlers, services, and utilities. - Documented API endpoints and usage in `README.md` and migration instructions in `migrations/README.md`. - Introduced `.gitignore` to exclude unnecessary files and directories. - Added Go module support with `go.mod` and `go.sum` files. - Implemented utility functions for slug generation and validation in `utils/slug.go` and `utils/validation.go`.
This commit is contained in:
141
services/firebase.go
Normal file
141
services/firebase.go
Normal file
@@ -0,0 +1,141 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user