Skip to the content.

Firebase Storage

Firebase ORM provides seamless integration with Firebase Storage for file uploads and management. This allows you to easily handle file uploads, downloads, and URL generation directly from your models.

Setup

Initialize Firebase Storage Connection

import { initializeApp } from 'firebase/app';
import { getStorage } from 'firebase/storage';
import { FirestoreOrmRepository } from '@arbel/firebase-orm';

const app = initializeApp(firebaseConfig);
const storage = getStorage(app);
FirestoreOrmRepository.initGlobalStorage(storage);

With Firebase Admin SDK

import * as admin from 'firebase-admin';
import { FirestoreOrmRepository } from '@arbel/firebase-orm';

const adminApp = admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  storageBucket: 'your-project-id.appspot.com'
});

const adminStorage = admin.storage();
FirestoreOrmRepository.initGlobalStorage(adminStorage);

Basic Usage

Model Definition

import { Field, BaseModel, Model } from "@arbel/firebase-orm";

@Model({
  reference_path: "products",
  path_id: "product_id"
})
export class Product extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @Field({ is_required: false, field_name: "photo_url" })
  public photoUrl?: string;

  @Field({ is_required: false, field_name: "gallery_urls" })
  public galleryUrls?: string[];

  @Field({ is_required: false, field_name: "document_url" })
  public documentUrl?: string;
}

Getting Storage Reference

// Get storage reference for a field
const product = new Product();
product.name = "Test Product";

const storageRef = product.getStorageFile("photoUrl");

File Upload Methods

Upload from File Object

const product = new Product();
product.name = "Test Product";

// Upload file (e.g., from file input)
const file = document.getElementById('fileInput').files[0];
await product.getStorageFile("photoUrl").uploadFile(file);

// Save the model (photoUrl will be automatically set)
await product.save();

Upload from Base64 String

const product = new Product();
product.name = "Test Product";

// Upload from base64 string
const base64String = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAA...";
await product.getStorageFile("photoUrl").uploadString(base64String, 'base64');

// Save the model
await product.save();

Upload from URL (Copy to Storage)

const product = new Product();
product.name = "Test Product";

// Copy file from external URL to Firebase Storage
const externalUrl = "https://example.com/image.jpg";
await product.getStorageFile("photoUrl").uploadFromUrl(externalUrl);

// Save the model
await product.save();

Upload from Buffer (Node.js)

const product = new Product();
product.name = "Test Product";

// Upload from buffer (server-side)
const buffer = Buffer.from(imageData);
await product.getStorageFile("photoUrl").uploadBuffer(buffer, 'image/jpeg');

// Save the model
await product.save();

Upload Progress Tracking

const product = new Product();
product.name = "Test Product";

const storageRef = product.getStorageFile("photoUrl");

await storageRef.uploadFromUrl(
  "https://example.com/image.jpg",
  // Progress callback
  (snapshot) => {
    const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
    console.log(`Upload progress: ${progress}%`);
    
    switch (snapshot.state) {
      case 'paused':
        console.log('Upload paused');
        break;
      case 'running':
        console.log('Upload running');
        break;
    }
  },
  // Error callback
  (error) => {
    console.error('Upload failed:', error);
  },
  // Complete callback
  (task) => {
    console.log('Upload completed successfully');
    task.snapshot.ref.getDownloadURL().then((downloadURL) => {
      console.log('File available at:', downloadURL);
    });
  }
);

await product.save();

File Management

Get Storage Reference

const product = new Product();
await product.load('product-id');

// Get Firebase Storage reference
const ref = product.getStorageFile("photoUrl").getRef();
console.log('Storage path:', ref.fullPath);

Download File

const product = new Product();
await product.load('product-id');

const storageRef = product.getStorageFile("photoUrl");

// Get download URL
const downloadURL = await storageRef.getDownloadURL();
console.log('Download URL:', downloadURL);

// Download file as blob (browser)
const blob = await storageRef.getBlob();

// Download file as buffer (Node.js)
const buffer = await storageRef.getBuffer();

Delete File

const product = new Product();
await product.load('product-id');

// Delete file from storage
await product.getStorageFile("photoUrl").delete();

// Clear the URL from the model
product.photoUrl = null;
await product.save();

File Metadata

const product = new Product();
await product.load('product-id');

// Get file metadata
const metadata = await product.getStorageFile("photoUrl").getMetadata();
console.log('File size:', metadata.size);
console.log('Content type:', metadata.contentType);
console.log('Created:', metadata.timeCreated);

Multiple File Uploads

Gallery/Array Fields

@Model({
  reference_path: "products",
  path_id: "product_id"
})
export class Product extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @Field({ is_required: false, field_name: "gallery_urls" })
  public galleryUrls?: string[];
}

// Upload multiple files
const product = new Product();
product.name = "Test Product";

const files = document.getElementById('fileInput').files;
const uploadPromises = [];

for (let i = 0; i < files.length; i++) {
  const file = files[i];
  const storageRef = product.getStorageFile(`gallery_${i}`);
  uploadPromises.push(storageRef.uploadFile(file));
}

// Wait for all uploads to complete
const results = await Promise.all(uploadPromises);
product.galleryUrls = results.map(result => result.downloadURL);

await product.save();

Organized File Structure

// Upload with custom path structure
const product = new Product();
product.name = "Test Product";

// Files will be organized as: products/{product_id}/images/photo.jpg
const storageRef = product.getStorageFile("photoUrl", {
  customPath: `products/${product.getId()}/images/photo.jpg`
});

await storageRef.uploadFile(file);
await product.save();

Advanced Features

Custom Storage Paths

const product = new Product();
product.name = "Test Product";

// Define custom storage path
const customPath = `products/${product.getId()}/images/${Date.now()}.jpg`;
const storageRef = product.getStorageFile("photoUrl", { customPath });

await storageRef.uploadFile(file);
await product.save();

File Validation

const product = new Product();
product.name = "Test Product";

// Validate file before upload
const file = document.getElementById('fileInput').files[0];

// Check file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
  throw new Error('File size exceeds 5MB limit');
}

// Check file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
  throw new Error('Invalid file type');
}

// Upload if validation passes
await product.getStorageFile("photoUrl").uploadFile(file);
await product.save();

Image Processing

const product = new Product();
product.name = "Test Product";

// Upload original image
const originalFile = document.getElementById('fileInput').files[0];
await product.getStorageFile("photoUrl").uploadFile(originalFile);

// Create thumbnail (client-side using canvas)
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();

img.onload = async () => {
  // Resize to thumbnail
  canvas.width = 200;
  canvas.height = 200;
  ctx.drawImage(img, 0, 0, 200, 200);
  
  // Convert to blob
  canvas.toBlob(async (blob) => {
    await product.getStorageFile("thumbnailUrl").uploadFile(blob);
    await product.save();
  }, 'image/jpeg', 0.8);
};

img.src = URL.createObjectURL(originalFile);

Error Handling

Upload Errors

const product = new Product();
product.name = "Test Product";

try {
  await product.getStorageFile("photoUrl").uploadFile(file);
  await product.save();
} catch (error) {
  if (error.code === 'storage/unauthorized') {
    console.error('User not authorized to upload files');
  } else if (error.code === 'storage/quota-exceeded') {
    console.error('Storage quota exceeded');
  } else if (error.code === 'storage/invalid-format') {
    console.error('Invalid file format');
  } else {
    console.error('Upload failed:', error);
  }
}

Download Errors

const product = new Product();
await product.load('product-id');

try {
  const downloadURL = await product.getStorageFile("photoUrl").getDownloadURL();
  console.log('Download URL:', downloadURL);
} catch (error) {
  if (error.code === 'storage/object-not-found') {
    console.error('File not found in storage');
  } else if (error.code === 'storage/unauthorized') {
    console.error('User not authorized to download file');
  } else {
    console.error('Download failed:', error);
  }
}

Security and Best Practices

Storage Security Rules

// Firebase Storage Security Rules
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    // Users can only upload their own files
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
    
    // Product images can be read by everyone, written by authenticated users
    match /products/{productId}/{allPaths=**} {
      allow read: if true;
      allow write: if request.auth != null;
    }
    
    // Restrict file size (5MB max)
    match /{allPaths=**} {
      allow write: if request.resource.size < 5 * 1024 * 1024;
    }
  }
}

File Organization

// Organize files by model type and ID
const getUserStoragePath = (userId: string, fileName: string) => {
  return `users/${userId}/files/${fileName}`;
};

const getProductStoragePath = (productId: string, fileName: string) => {
  return `products/${productId}/images/${fileName}`;
};

// Use organized paths
const product = new Product();
const customPath = getProductStoragePath(product.getId(), 'main-image.jpg');
const storageRef = product.getStorageFile("photoUrl", { customPath });

Cleanup Strategies

// Clean up old files when updating
const product = new Product();
await product.load('product-id');

// Delete old file before uploading new one
if (product.photoUrl) {
  await product.getStorageFile("photoUrl").delete();
}

// Upload new file
await product.getStorageFile("photoUrl").uploadFile(newFile);
await product.save();

Performance Optimization

Lazy Loading

// Load models without immediately loading file URLs
const products = await Product.getAll();

// Load file URLs only when needed
const displayProducts = await Promise.all(
  products.map(async (product) => {
    if (product.photoUrl) {
      const downloadURL = await product.getStorageFile("photoUrl").getDownloadURL();
      return { ...product, displayPhotoUrl: downloadURL };
    }
    return product;
  })
);

Caching Download URLs

// Cache download URLs to avoid repeated requests
const urlCache = new Map<string, string>();

const getCachedDownloadURL = async (product: Product): Promise<string> => {
  const cacheKey = `${product.getId()}_photoUrl`;
  
  if (urlCache.has(cacheKey)) {
    return urlCache.get(cacheKey)!;
  }
  
  const downloadURL = await product.getStorageFile("photoUrl").getDownloadURL();
  urlCache.set(cacheKey, downloadURL);
  
  // Cache for 1 hour
  setTimeout(() => {
    urlCache.delete(cacheKey);
  }, 60 * 60 * 1000);
  
  return downloadURL;
};

Batch Operations

// Upload multiple files in parallel
const uploadFiles = async (product: Product, files: File[]) => {
  const uploadPromises = files.map(async (file, index) => {
    const fieldName = `gallery_${index}`;
    return product.getStorageFile(fieldName).uploadFile(file);
  });
  
  const results = await Promise.all(uploadPromises);
  return results.map(result => result.downloadURL);
};

Integration Examples

React Component

import React, { useState } from 'react';
import { Product } from './models/Product';

const ProductUpload: React.FC = () => {
  const [product, setProduct] = useState(new Product());
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);

  const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (!file) return;

    setUploading(true);
    setProgress(0);

    try {
      await product.getStorageFile("photoUrl").uploadFile(file, {
        onProgress: (snapshot) => {
          const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
          setProgress(progress);
        }
      });

      await product.save();
      console.log('Upload successful!');
    } catch (error) {
      console.error('Upload failed:', error);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <input 
        type="file" 
        onChange={handleFileUpload}
        disabled={uploading}
        accept="image/*"
      />
      {uploading && (
        <div>
          <progress value={progress} max="100" />
          <span>{Math.round(progress)}%</span>
        </div>
      )}
    </div>
  );
};

Node.js API

import express from 'express';
import multer from 'multer';
import { Product } from './models/Product';

const app = express();
const upload = multer({ storage: multer.memoryStorage() });

app.post('/products/:id/upload', upload.single('image'), async (req, res) => {
  try {
    const product = new Product();
    await product.load(req.params.id);

    if (!req.file) {
      return res.status(400).json({ error: 'No file uploaded' });
    }

    // Upload buffer to Firebase Storage
    await product.getStorageFile("photoUrl").uploadBuffer(
      req.file.buffer,
      req.file.mimetype
    );

    await product.save();

    res.json({ 
      message: 'File uploaded successfully',
      productId: product.getId(),
      photoUrl: product.photoUrl
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Common Use Cases

Profile Picture Upload

@Model({
  reference_path: "users",
  path_id: "user_id"
})
export class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @Field({ is_required: false, field_name: "avatar_url" })
  public avatarUrl?: string;

  async uploadAvatar(file: File): Promise<void> {
    // Delete old avatar if exists
    if (this.avatarUrl) {
      await this.getStorageFile("avatarUrl").delete();
    }

    // Upload new avatar
    const customPath = `users/${this.getId()}/avatar.jpg`;
    await this.getStorageFile("avatarUrl", { customPath }).uploadFile(file);
    
    await this.save();
  }
}

Document Management

@Model({
  reference_path: "documents",
  path_id: "document_id"
})
export class Document extends BaseModel {
  @Field({ is_required: true })
  public title!: string;

  @Field({ is_required: false, field_name: "file_url" })
  public fileUrl?: string;

  @Field({ is_required: false, field_name: "file_type" })
  public fileType?: string;

  @Field({ is_required: false, field_name: "file_size" })
  public fileSize?: number;

  async uploadDocument(file: File): Promise<void> {
    // Set file metadata
    this.fileType = file.type;
    this.fileSize = file.size;

    // Upload file
    const customPath = `documents/${this.getId()}/${file.name}`;
    await this.getStorageFile("fileUrl", { customPath }).uploadFile(file);
    
    await this.save();
  }
}

Bulk File Operations

const cleanupOrphanedFiles = async (): Promise<void> => {
  const products = await Product.getAll();
  
  // Get all file URLs from database
  const dbFileUrls = new Set<string>();
  products.forEach(product => {
    if (product.photoUrl) dbFileUrls.add(product.photoUrl);
    if (product.galleryUrls) {
      product.galleryUrls.forEach(url => dbFileUrls.add(url));
    }
  });

  // List all files in storage
  const storageFiles = await listAllFiles('products/');
  
  // Delete files not referenced in database
  for (const file of storageFiles) {
    if (!dbFileUrls.has(file.downloadURL)) {
      await file.delete();
      console.log(`Deleted orphaned file: ${file.name}`);
    }
  }
};