Storage Examples

Comprehensive examples for implementing file storage with the Asterisms JS SDK Backend.

Overview

This guide provides detailed examples of storage implementation using the Asterisms JS SDK Backend. It covers various storage scenarios, from simple file uploads to complex file management systems with caching and batch operations.

Basic File Operations

Simple File Upload

// Basic file upload
import { getAsterismsBackendSDK } from '@asterisms/sdk-backend';

const sdk = getAsterismsBackendSDK(props);
await sdk.boot();

const storage = sdk.storage();

async function uploadUserDocument(file: File, userId: string) {
  try {
    const result = await storage.upload(file, {
      bucketId: 'user-documents',
      prefix: `users/${userId}/documents/`
    });

    console.log('File uploaded successfully:', {
      externalId: result.externalId,
      fileName: result.fileName,
      size: result.size
    });

    return result;
  } catch (error) {
    console.error('Failed to upload file:', error);
    throw error;
  }
}

File Download

// Download file with proper response handling
async function downloadUserDocument(externalId: string, fileName: string) {
  const storage = sdk.storage();

  try {
    const response = await storage.download(externalId, { fileName });

    // For JSON files
    if (fileName.endsWith('.json')) {
      const content = await response.text();
      return JSON.parse(content);
    }

    // For binary files
    const blob = await response.blob();
    return blob;
  } catch (error) {
    console.error('Failed to download file:', error);
    throw error;
  }
}

File Metadata Operations

// Get file information
async function getFileInfo(externalId: string) {
  const storage = sdk.storage();

  try {
    const fileData = await storage.getData(externalId);

    return {
      fileName: fileData.fileName,
      size: fileData.size,
      contentType: fileData.contentType,
      lastModified: fileData.lastModified,
      isPrivate: fileData.private,
      bucketId: fileData.bucketId
    };
  } catch (error) {
    console.error('Failed to get file metadata:', error);
    throw error;
  }
}

SvelteKit Storage API

File Upload Route

// src/routes/api/storage/upload/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async ({ request, locals }) => {
  try {
    const formData = await request.formData();
    const file = formData.get('file') as File;
    const bucketId = formData.get('bucketId') as string;
    const prefix = formData.get('prefix') as string;
    const isPrivate = formData.get('private') === 'true';

    if (!file) {
      return json({ error: 'No file provided' }, { status: 400 });
    }

    // Validate file
    const maxSize = 50 * 1024 * 1024; // 50MB
    if (file.size > maxSize) {
      return json({ error: 'File too large' }, { status: 413 });
    }

    const allowedTypes = [
      'image/jpeg',
      'image/png',
      'image/gif',
      'image/webp',
      'application/pdf',
      'text/plain',
      'application/json',
      'application/zip',
      'application/x-zip-compressed'
    ];

    if (!allowedTypes.includes(file.type)) {
      return json({ error: 'File type not allowed' }, { status: 400 });
    }

    const sdk = locals.sdk;
    if (!sdk) {
      return json({ error: 'SDK not available' }, { status: 503 });
    }

    const storage = sdk.storage();
    const result = await storage.upload(file, {
      bucketId: bucketId || 'default',
      prefix: prefix || '',
      metadata: { private: isPrivate }
    });

    return json({
      success: true,
      file: {
        externalId: result.externalId,
        fileName: result.fileName,
        size: result.size,
        contentType: result.contentType,
        path: result.path
      }
    });
  } catch (error) {
    console.error('Storage upload error:', error);
    return json({ error: 'Failed to upload file' }, { status: 500 });
  }
};

File Download Route

// src/routes/api/storage/[externalId]/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ params, url, locals }) => {
  try {
    const sdk = locals.sdk;
    if (!sdk) {
      return json({ error: 'SDK not available' }, { status: 503 });
    }

    const storage = sdk.storage();
    const fileName = url.searchParams.get('fileName');
    const inline = url.searchParams.get('inline') === 'true';

    const response = await storage.download(params.externalId, { fileName });

    const headers = new Headers();
    headers.set('Content-Type', response.headers.get('content-type') || 'application/octet-stream');

    if (inline) {
      headers.set('Content-Disposition', 'inline');
    } else {
      headers.set(
        'Content-Disposition',
        response.headers.get('content-disposition') || 'attachment'
      );
    }

    return new Response(response.body, { headers });
  } catch (error) {
    console.error('Storage download error:', error);
    return json({ error: 'Failed to download file' }, { status: 500 });
  }
};

export const DELETE: RequestHandler = async ({ params, locals }) => {
  try {
    const sdk = locals.sdk;
    if (!sdk) {
      return json({ error: 'SDK not available' }, { status: 503 });
    }

    const storage = sdk.storage();
    await storage.delete(params.externalId);

    return json({ success: true });
  } catch (error) {
    console.error('Storage delete error:', error);
    return json({ error: 'Failed to delete file' }, { status: 500 });
  }
};

File Listing Route

// src/routes/api/storage/list/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ url, locals }) => {
  try {
    const sdk = locals.sdk;
    if (!sdk) {
      return json({ error: 'SDK not available' }, { status: 503 });
    }

    const bucketId = url.searchParams.get('bucketId');
    const path = url.searchParams.get('path');
    const limit = parseInt(url.searchParams.get('limit') || '50');
    const offset = parseInt(url.searchParams.get('offset') || '0');

    const storage = sdk.storage();
    const files = await storage.list({ bucketId, path });

    // Simple pagination
    const paginatedFiles = files.slice(offset, offset + limit);

    return json({
      success: true,
      files: paginatedFiles.map((file) => ({
        externalId: file.externalId,
        fileName: file.fileName,
        size: file.size,
        contentType: file.contentType,
        lastModified: file.lastModified,
        private: file.private
      })),
      pagination: {
        total: files.length,
        offset,
        limit,
        hasMore: offset + limit < files.length
      }
    });
  } catch (error) {
    console.error('Storage list error:', error);
    return json({ error: 'Failed to list files' }, { status: 500 });
  }
};

Bulk File Operations

// src/routes/api/storage/bulk/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

interface BulkDeleteRequest {
  externalIds: string[];
}

export const DELETE: RequestHandler = async ({ request, locals }) => {
  try {
    const { externalIds } = (await request.json()) as BulkDeleteRequest;

    if (!Array.isArray(externalIds) || externalIds.length === 0) {
      return json({ error: 'External IDs array is required' }, { status: 400 });
    }

    const sdk = locals.sdk;
    if (!sdk) {
      return json({ error: 'SDK not available' }, { status: 503 });
    }

    const storage = sdk.storage();

    // Delete files in batches to avoid overwhelming the system
    const batchSize = 50;
    const results = [];

    for (let i = 0; i < externalIds.length; i += batchSize) {
      const batch = externalIds.slice(i, i + batchSize);

      const batchPromises = batch.map(async (externalId) => {
        try {
          await storage.delete(externalId);
          return { externalId, success: true };
        } catch (error) {
          return { externalId, success: false, error: error.message };
        }
      });

      const batchResults = await Promise.all(batchPromises);
      results.push(...batchResults);
    }

    const successCount = results.filter((r) => r.success).length;
    const failureCount = results.filter((r) => !r.success).length;

    return json({
      success: true,
      results: {
        total: externalIds.length,
        successful: successCount,
        failed: failureCount,
        details: results
      }
    });
  } catch (error) {
    console.error('Bulk delete error:', error);
    return json({ error: 'Failed to delete files' }, { status: 500 });
  }
};

Advanced Storage Patterns

File Management Service

// lib/services/fileManager.ts
import type { AsterismsBackendSDK, StorageFileData } from '@asterisms/sdk-backend';

export class FileManager {
  private sdk: AsterismsBackendSDK;
  private cache = new Map<string, StorageFileData>();
  private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes

  constructor(sdk: AsterismsBackendSDK) {
    this.sdk = sdk;
  }

  async uploadFile(
    file: File,
    options: {
      bucketId?: string;
      prefix?: string;
      tags?: string[];
      isPrivate?: boolean;
    } = {}
  ): Promise<StorageFileData> {
    const storage = this.sdk.storage();

    try {
      const result = await storage.upload(file, {
        bucketId: options.bucketId || 'default',
        prefix: options.prefix || '',
        metadata: {
          tags: options.tags?.join(','),
          private: options.isPrivate
        }
      });

      // Cache the result
      this.cache.set(result.externalId, result);

      return result;
    } catch (error) {
      console.error('Failed to upload file:', error);
      throw error;
    }
  }

  async getFileInfo(externalId: string): Promise<StorageFileData | null> {
    // Check cache first
    const cached = this.cache.get(externalId);
    if (cached) {
      return cached;
    }

    const storage = this.sdk.storage();

    try {
      const fileData = await storage.getData(externalId);
      this.cache.set(externalId, fileData);
      return fileData;
    } catch (error) {
      console.error('Failed to get file info:', error);
      return null;
    }
  }

  async downloadFile(externalId: string, fileName?: string): Promise<Blob> {
    const storage = this.sdk.storage();

    try {
      const response = await storage.download(externalId, { fileName });
      return await response.blob();
    } catch (error) {
      console.error('Failed to download file:', error);
      throw error;
    }
  }

  async replaceFile(externalId: string, newFile: File): Promise<StorageFileData> {
    const storage = this.sdk.storage();

    try {
      const result = await storage.replace(externalId, newFile);

      // Update cache
      this.cache.set(externalId, result);

      return result;
    } catch (error) {
      console.error('Failed to replace file:', error);
      throw error;
    }
  }

  async deleteFile(externalId: string): Promise<void> {
    const storage = this.sdk.storage();

    try {
      await storage.delete(externalId);

      // Remove from cache
      this.cache.delete(externalId);
    } catch (error) {
      console.error('Failed to delete file:', error);
      throw error;
    }
  }

  async listFiles(
    options: {
      bucketId?: string;
      path?: string;
      tags?: string[];
    } = {}
  ): Promise<StorageFileData[]> {
    const storage = this.sdk.storage();

    try {
      const files = await storage.list({
        bucketId: options.bucketId,
        path: options.path
      });

      // Filter by tags if provided
      if (options.tags && options.tags.length > 0) {
        return files.filter((file) => {
          const fileTags = file.metadata?.tags?.split(',') || [];
          return options.tags.some((tag) => fileTags.includes(tag));
        });
      }

      return files;
    } catch (error) {
      console.error('Failed to list files:', error);
      throw error;
    }
  }

  clearCache(): void {
    this.cache.clear();
  }
}

Image Processing Service

// lib/services/imageProcessor.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

export class ImageProcessor {
  private sdk: AsterismsBackendSDK;

  constructor(sdk: AsterismsBackendSDK) {
    this.sdk = sdk;
  }

  async resizeImage(
    file: File,
    maxWidth: number,
    maxHeight: number,
    quality: number = 0.8
  ): Promise<File> {
    return new Promise((resolve, reject) => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      const img = new Image();

      img.onload = () => {
        // Calculate new dimensions
        let { width, height } = img;

        if (width > height) {
          if (width > maxWidth) {
            height = (height * maxWidth) / width;
            width = maxWidth;
          }
        } else {
          if (height > maxHeight) {
            width = (width * maxHeight) / height;
            height = maxHeight;
          }
        }

        canvas.width = width;
        canvas.height = height;

        // Draw resized image
        ctx.drawImage(img, 0, 0, width, height);

        canvas.toBlob(
          (blob) => {
            if (blob) {
              const resizedFile = new File([blob], file.name, {
                type: 'image/jpeg',
                lastModified: Date.now()
              });
              resolve(resizedFile);
            } else {
              reject(new Error('Failed to resize image'));
            }
          },
          'image/jpeg',
          quality
        );
      };

      img.onerror = () => reject(new Error('Failed to load image'));
      img.src = URL.createObjectURL(file);
    });
  }

  async uploadWithThumbnail(
    file: File,
    bucketId: string = 'images'
  ): Promise<{
    original: StorageFileData;
    thumbnail: StorageFileData;
  }> {
    const storage = this.sdk.storage();

    try {
      // Upload original
      const original = await storage.upload(file, {
        bucketId,
        prefix: 'originals/'
      });

      // Create and upload thumbnail
      const thumbnail = await this.resizeImage(file, 200, 200, 0.7);
      const thumbnailResult = await storage.upload(thumbnail, {
        bucketId,
        prefix: 'thumbnails/',
        metadata: { originalId: original.externalId }
      });

      return {
        original,
        thumbnail: thumbnailResult
      };
    } catch (error) {
      console.error('Failed to upload with thumbnail:', error);
      throw error;
    }
  }
}

Document Management System

// lib/services/documentManager.ts
import type { AsterismsBackendSDK, StorageFileData } from '@asterisms/sdk-backend';

export interface DocumentMetadata {
  title: string;
  description?: string;
  tags: string[];
  category: string;
  author: string;
  createdAt: Date;
  version: number;
}

export class DocumentManager {
  private sdk: AsterismsBackendSDK;
  private readonly DOCUMENTS_BUCKET = 'documents';

  constructor(sdk: AsterismsBackendSDK) {
    this.sdk = sdk;
  }

  async uploadDocument(
    file: File,
    metadata: DocumentMetadata
  ): Promise<{
    file: StorageFileData;
    metadata: DocumentMetadata;
  }> {
    const storage = this.sdk.storage();

    try {
      // Upload the file
      const fileResult = await storage.upload(file, {
        bucketId: this.DOCUMENTS_BUCKET,
        prefix: `${metadata.category}/`,
        metadata: {
          ...metadata,
          createdAt: metadata.createdAt.toISOString()
        }
      });

      // Upload metadata as JSON
      const metadataFile = new File(
        [JSON.stringify(metadata)],
        `${fileResult.externalId}.metadata.json`,
        { type: 'application/json' }
      );

      await storage.upload(metadataFile, {
        bucketId: this.DOCUMENTS_BUCKET,
        prefix: 'metadata/',
        metadata: { documentId: fileResult.externalId }
      });

      return { file: fileResult, metadata };
    } catch (error) {
      console.error('Failed to upload document:', error);
      throw error;
    }
  }

  async getDocumentMetadata(externalId: string): Promise<DocumentMetadata | null> {
    const storage = this.sdk.storage();

    try {
      const response = await storage.download(`${externalId}.metadata.json`);
      const metadataText = await response.text();
      const metadata = JSON.parse(metadataText);

      return {
        ...metadata,
        createdAt: new Date(metadata.createdAt)
      };
    } catch (error) {
      console.error('Failed to get document metadata:', error);
      return null;
    }
  }

  async searchDocuments(query: {
    tags?: string[];
    category?: string;
    author?: string;
    dateRange?: { from: Date; to: Date };
  }): Promise<Array<{ file: StorageFileData; metadata: DocumentMetadata }>> {
    const storage = this.sdk.storage();

    try {
      const files = await storage.list({
        bucketId: this.DOCUMENTS_BUCKET,
        path: query.category ? `${query.category}/` : undefined
      });

      const results = [];

      for (const file of files) {
        const metadata = await this.getDocumentMetadata(file.externalId);
        if (!metadata) continue;

        // Apply filters
        if (query.tags && !query.tags.some((tag) => metadata.tags.includes(tag))) {
          continue;
        }

        if (query.author && metadata.author !== query.author) {
          continue;
        }

        if (query.dateRange) {
          const createdAt = metadata.createdAt;
          if (createdAt < query.dateRange.from || createdAt > query.dateRange.to) {
            continue;
          }
        }

        results.push({ file, metadata });
      }

      return results;
    } catch (error) {
      console.error('Failed to search documents:', error);
      throw error;
    }
  }

  async updateDocumentMetadata(
    externalId: string,
    updates: Partial<DocumentMetadata>
  ): Promise<void> {
    const storage = this.sdk.storage();

    try {
      const currentMetadata = await this.getDocumentMetadata(externalId);
      if (!currentMetadata) {
        throw new Error('Document metadata not found');
      }

      const updatedMetadata = {
        ...currentMetadata,
        ...updates,
        version: currentMetadata.version + 1
      };

      const metadataFile = new File(
        [JSON.stringify(updatedMetadata)],
        `${externalId}.metadata.json`,
        { type: 'application/json' }
      );

      await storage.replace(`${externalId}.metadata.json`, metadataFile);
    } catch (error) {
      console.error('Failed to update document metadata:', error);
      throw error;
    }
  }
}

Svelte Components

File Upload Component

<!-- lib/components/FileUpload.svelte -->
<script lang="ts">
    import { createEventDispatcher } from 'svelte';
    import type { StorageFileData } from '@asterisms/sdk-backend';

    export let bucketId: string = 'default';
    export let prefix: string = '';
    export let accept: string = '*/*';
    export let multiple: boolean = false;
    export let maxSize: number = 10 * 1024 * 1024; // 10MB
    export let disabled: boolean = false;

    const dispatch = createEventDispatcher<{
        upload: { files: StorageFileData[] };
        error: { message: string };
        progress: { percent: number };
    }>();

    let dragOver = false;
    let uploading = false;
    let uploadProgress = 0;

    async function handleFiles(files: FileList) {
        if (disabled || uploading) return;

        const fileArray = Array.from(files);

        // Validate files
        for (const file of fileArray) {
            if (file.size > maxSize) {
                dispatch('error', { message: `File ${file.name} is too large` });
                return;
            }
        }

        uploading = true;
        uploadProgress = 0;

        try {
            const uploadedFiles: StorageFileData[] = [];

            for (let i = 0; i < fileArray.length; i++) {
                const file = fileArray[i];

                const formData = new FormData();
                formData.append('file', file);
                formData.append('bucketId', bucketId);
                formData.append('prefix', prefix);

                const response = await fetch('/api/storage/upload', {
                    method: 'POST',
                    body: formData
                });

                if (!response.ok) {
                    throw new Error(`Failed to upload ${file.name}`);
                }

                const result = await response.json();
                uploadedFiles.push(result.file);

                uploadProgress = Math.round(((i + 1) / fileArray.length) * 100);
                dispatch('progress', { percent: uploadProgress });
            }

            dispatch('upload', { files: uploadedFiles });
        } catch (error) {
            dispatch('error', { message: error.message });
        } finally {
            uploading = false;
            uploadProgress = 0;
        }
    }

    function handleDragOver(event: DragEvent) {
        event.preventDefault();
        dragOver = true;
    }

    function handleDragLeave(event: DragEvent) {
        event.preventDefault();
        dragOver = false;
    }

    function handleDrop(event: DragEvent) {
        event.preventDefault();
        dragOver = false;

        const files = event.dataTransfer?.files;
        if (files && files.length > 0) {
            handleFiles(files);
        }
    }
</script>

<div class="file-upload">
    <div
        class="drop-zone"
        class:drag-over={dragOver}
        class:uploading
        class:disabled
        on:dragover={handleDragOver}
        on:dragleave={handleDragLeave}
        on:drop={handleDrop}
    >
        <input
            type="file"
            {accept}
            {multiple}
            on:change={(e) => handleFiles(e.target.files)}
            disabled={disabled || uploading}
        />

        {#if uploading}
            <div class="upload-status">
                <div class="progress-bar">
                    <div class="progress-fill" style="width: {uploadProgress}%"></div>
                </div>
                <p>Uploading... {uploadProgress}%</p>
            </div>
        {:else}
            <div class="upload-prompt">
                <svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
                    <polyline points="7,10 12,15 17,10" />
                    <line x1="12" y1="15" x2="12" y2="3" />
                </svg>
                <p>Drop files here or click to browse</p>
                <p class="hint">Max size: {Math.round(maxSize / 1024 / 1024)}MB</p>
            </div>
        {/if}
    </div>
</div>

<style>
    .file-upload {
        width: 100%;
        max-width: 600px;
        margin: 0 auto;
    }

    .drop-zone {
        border: 2px dashed #ccc;
        border-radius: 8px;
        padding: 40px 20px;
        text-align: center;
        cursor: pointer;
        transition: all 0.3s ease;
        position: relative;
    }

    .drop-zone:hover:not(.disabled):not(.uploading) {
        border-color: #007bff;
        background-color: #f8f9fa;
    }

    .drop-zone.drag-over {
        border-color: #007bff;
        background-color: #e3f2fd;
    }

    .drop-zone.uploading {
        border-color: #28a745;
        background-color: #f8fff9;
    }

    .drop-zone.disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }

    input[type="file"] {
        position: absolute;
        width: 100%;
        height: 100%;
        opacity: 0;
        cursor: pointer;
    }

    .upload-icon {
        width: 48px;
        height: 48px;
        margin: 0 auto 16px;
        color: #666;
    }

    .upload-prompt p {
        margin: 8px 0;
        color: #666;
    }

    .hint {
        font-size: 0.9em;
        color: #999;
    }

    .upload-status {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 16px;
    }

    .progress-bar {
        width: 100%;
        max-width: 300px;
        height: 8px;
        background-color: #e9ecef;
        border-radius: 4px;
        overflow: hidden;
    }

    .progress-fill {
        height: 100%;
        background-color: #28a745;
        transition: width 0.3s ease;
    }
</style>
<!-- lib/components/FileGallery.svelte -->
<script lang="ts">
    import { onMount } from 'svelte';
    import type { StorageFileData } from '@asterisms/sdk-backend';

    export let bucketId: string = 'default';
    export let path: string = '';
    export let allowDelete: boolean = true;
    export let allowDownload: boolean = true;

    let files: StorageFileData[] = [];
    let loading = true;
    let error: string | null = null;

    onMount(() => {
        loadFiles();
    });

    async function loadFiles() {
        loading = true;
        error = null;

        try {
            const params = new URLSearchParams();
            if (bucketId) params.append('bucketId', bucketId);
            if (path) params.append('path', path);

            const response = await fetch(`/api/storage/list?${params}`);
            if (!response.ok) {
                throw new Error('Failed to load files');
            }

            const result = await response.json();
            files = result.files;
        } catch (err) {
            error = err.message;
        } finally {
            loading = false;
        }
    }

    async function downloadFile(file: StorageFileData) {
        try {
            const response = await fetch(`/api/storage/${file.externalId}?fileName=${encodeURIComponent(file.fileName)}`);
            if (!response.ok) {
                throw new Error('Failed to download file');
            }

            const blob = await response.blob();
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = file.fileName;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        } catch (err) {
            alert(`Failed to download file: ${err.message}`);
        }
    }

    async function deleteFile(file: StorageFileData) {
        if (!confirm(`Are you sure you want to delete ${file.fileName}?`)) {
            return;
        }

        try {
            const response = await fetch(`/api/storage/${file.externalId}`, {
                method: 'DELETE'
            });

            if (!response.ok) {
                throw new Error('Failed to delete file');
            }

            // Remove from local list
            files = files.filter(f => f.externalId !== file.externalId);
        } catch (err) {
            alert(`Failed to delete file: ${err.message}`);
        }
    }

    function formatFileSize(bytes: number): string {
        const sizes = ['Bytes', 'KB', 'MB', 'GB'];
        if (bytes === 0) return '0 Bytes';
        const i = Math.floor(Math.log(bytes) / Math.log(1024));
        return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
    }

    function isImage(contentType: string): boolean {
        return contentType.startsWith('image/');
    }

    function getFileIcon(contentType: string): string {
        if (contentType.startsWith('image/')) return '🖼️';
        if (contentType.startsWith('video/')) return '🎥';
        if (contentType.startsWith('audio/')) return '🎵';
        if (contentType.includes('pdf')) return '📄';
        if (contentType.includes('text')) return '📝';
        if (contentType.includes('zip')) return '📦';
        return '📁';
    }
</script>

<div class="file-gallery">
    {#if loading}
        <div class="loading">Loading files...</div>
    {:else if error}
        <div class="error">Error: {error}</div>
    {:else if files.length === 0}
        <div class="empty">No files found</div>
    {:else}
        <div class="files-grid">
            {#each files as file (file.externalId)}
                <div class="file-card">
                    <div class="file-preview">
                        {#if isImage(file.contentType)}
                            <img
                                src="/api/storage/{file.externalId}?fileName={encodeURIComponent(file.fileName)}&inline=true"
                                alt={file.fileName}
                                loading="lazy"
                            />
                        {:else}
                            <div class="file-icon">
                                {getFileIcon(file.contentType)}
                            </div>
                        {/if}
                    </div>

                    <div class="file-info">
                        <h3 class="file-name" title={file.fileName}>{file.fileName}</h3>
                        <p class="file-size">{formatFileSize(file.size)}</p>
                        <p class="file-type">{file.contentType}</p>
                        <p class="file-date">{new Date(file.lastModified).toLocaleDateString()}</p>
                    </div>

                    <div class="file-actions">
                        {#if allowDownload}
                            <button
                                class="btn btn-primary"
                                on:click={() => downloadFile(file)}
                            >
                                Download
                            </button>
                        {/if}
                        {#if allowDelete}
                            <button
                                class="btn btn-danger"
                                on:click={() => deleteFile(file)}
                            >
                                Delete
                            </button>
                        {/if}
                    </div>
                </div>
            {/each}
        </div>
    {/if}
</div>

<style>
    .file-gallery {
        width: 100%;
        max-width: 1200px;
        margin: 0 auto;
    }

    .loading, .error, .empty {
        text-align: center;
        padding: 40px;
        color: #666;
    }

    .error {
        color: #dc3545;
    }

    .files-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
        gap: 20px;
        padding: 20px;
    }

    .file-card {
        border: 1px solid #e9ecef;
        border-radius: 8px;
        overflow: hidden;
        transition: transform 0.2s, box-shadow 0.2s;
    }

    .file-card:hover {
        transform: translateY(-2px);
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    }

    .file-preview {
        height: 150px;
        display: flex;
        align-items: center;
        justify-content: center;
        background-color: #f8f9fa;
        overflow: hidden;
    }

    .file-preview img {
        max-width: 100%;
        max-height: 100%;
        object-fit: cover;
    }

    .file-icon {
        font-size: 48px;
    }

    .file-info {
        padding: 16px;
    }

    .file-name {
        font-size: 1.1em;
        font-weight: 600;
        margin: 0 0 8px 0;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .file-size, .file-type, .file-date {
        margin: 4px 0;
        font-size: 0.9em;
        color: #666;
    }

    .file-actions {
        padding: 16px;
        display: flex;
        gap: 8px;
        border-top: 1px solid #e9ecef;
    }

    .btn {
        padding: 6px 12px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 0.9em;
        transition: background-color 0.2s;
    }

    .btn-primary {
        background-color: #007bff;
        color: white;
    }

    .btn-primary:hover {
        background-color: #0056b3;
    }

    .btn-danger {
        background-color: #dc3545;
        color: white;
    }

    .btn-danger:hover {
        background-color: #c82333;
    }
</style>

Testing Storage Operations

Unit Tests

// tests/storage.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { getAsterismsBackendSDK } from '@asterisms/sdk-backend';
import type { StorageService } from '@asterisms/sdk-backend';

describe('Storage Service', () => {
  let sdk: AsterismsBackendSDK;
  let storage: StorageService;

  beforeEach(async () => {
    sdk = getAsterismsBackendSDK({
      // Test configuration
    });
    await sdk.boot();
    storage = sdk.storage();
  });

  describe('File Upload', () => {
    it('should upload a text file successfully', async () => {
      const fileContent = 'Hello, World!';
      const file = new File([fileContent], 'test.txt', { type: 'text/plain' });

      const result = await storage.upload(file, {
        bucketId: 'test-bucket',
        prefix: 'test/'
      });

      expect(result.fileName).toBe('test.txt');
      expect(result.contentType).toBe('text/plain');
      expect(result.size).toBe(fileContent.length);
      expect(result.bucketId).toBe('test-bucket');
    });

    it('should upload a JSON file successfully', async () => {
      const jsonData = { name: 'Test', value: 42 };
      const file = new File([JSON.stringify(jsonData)], 'data.json', {
        type: 'application/json'
      });

      const result = await storage.upload(file);

      expect(result.fileName).toBe('data.json');
      expect(result.contentType).toBe('application/json');
    });
  });

  describe('File Download', () => {
    it('should download uploaded file with correct content', async () => {
      const originalContent = 'Test file content';
      const file = new File([originalContent], 'download-test.txt', {
        type: 'text/plain'
      });

      const uploadResult = await storage.upload(file);
      const downloadResponse = await storage.download(uploadResult.externalId);
      const downloadedContent = await downloadResponse.text();

      expect(downloadedContent).toBe(originalContent);
    });

    it('should download with custom filename', async () => {
      const file = new File(['content'], 'original.txt', { type: 'text/plain' });
      const uploadResult = await storage.upload(file);

      const downloadResponse = await storage.download(uploadResult.externalId, {
        fileName: 'custom-name.txt'
      });

      const contentDisposition = downloadResponse.headers.get('content-disposition');
      expect(contentDisposition).toContain('custom-name.txt');
    });
  });

  describe('File Metadata', () => {
    it('should retrieve file metadata correctly', async () => {
      const file = new File(['metadata test'], 'metadata.txt', {
        type: 'text/plain'
      });

      const uploadResult = await storage.upload(file);
      const metadata = await storage.getData(uploadResult.externalId);

      expect(metadata.fileName).toBe('metadata.txt');
      expect(metadata.contentType).toBe('text/plain');
      expect(metadata.size).toBe(13); // 'metadata test'.length
      expect(metadata.externalId).toBe(uploadResult.externalId);
    });
  });

  describe('File Operations', () => {
    it('should replace file successfully', async () => {
      const originalFile = new File(['original'], 'replace.txt', {
        type: 'text/plain'
      });
      const newFile = new File(['replaced'], 'replace.txt', {
        type: 'text/plain'
      });

      const uploadResult = await storage.upload(originalFile);
      const replaceResult = await storage.replace(uploadResult.externalId, newFile);

      expect(replaceResult.externalId).toBe(uploadResult.externalId);
      expect(replaceResult.size).toBe(8); // 'replaced'.length

      const downloadResponse = await storage.download(uploadResult.externalId);
      const content = await downloadResponse.text();
      expect(content).toBe('replaced');
    });

    it('should delete file successfully', async () => {
      const file = new File(['delete me'], 'delete.txt', { type: 'text/plain' });
      const uploadResult = await storage.upload(file);

      await storage.delete(uploadResult.externalId);

      // Should throw error when trying to get deleted file
      await expect(storage.getData(uploadResult.externalId)).rejects.toThrow();
    });
  });

  describe('File Listing', () => {
    it('should list files in bucket', async () => {
      const bucketId = 'list-test';
      const files = [
        new File(['content1'], 'file1.txt', { type: 'text/plain' }),
        new File(['content2'], 'file2.txt', { type: 'text/plain' })
      ];

      // Upload test files
      for (const file of files) {
        await storage.upload(file, { bucketId });
      }

      const listedFiles = await storage.list({ bucketId });
      expect(listedFiles.length).toBeGreaterThanOrEqual(2);

      const fileNames = listedFiles.map((f) => f.fileName);
      expect(fileNames).toContain('file1.txt');
      expect(fileNames).toContain('file2.txt');
    });

    it('should list files with path prefix', async () => {
      const bucketId = 'prefix-test';
      const prefix = 'documents/';

      const file = new File(['content'], 'prefixed.txt', { type: 'text/plain' });
      await storage.upload(file, { bucketId, prefix });

      const listedFiles = await storage.list({ bucketId, path: prefix });
      expect(listedFiles.some((f) => f.fileName === 'prefixed.txt')).toBe(true);
    });
  });
});

Integration Tests

// tests/storage-integration.test.ts
import { test, expect } from '@playwright/test';

test.describe('Storage API Integration', () => {
  test('should handle file upload flow', async ({ page }) => {
    // Navigate to upload page
    await page.goto('/upload');

    // Create a test file
    const fileContent = 'Integration test file content';
    const file = await page.evaluateHandle(() => {
      const blob = new Blob(['Integration test file content'], { type: 'text/plain' });
      return new File([blob], 'integration-test.txt', { type: 'text/plain' });
    });

    // Upload file
    await page.setInputFiles('input[type="file"]', {
      name: 'integration-test.txt',
      mimeType: 'text/plain',
      buffer: Buffer.from(fileContent)
    });

    // Wait for upload completion
    await expect(page.locator('.upload-success')).toBeVisible();

    // Verify file appears in listing
    await page.goto('/files');
    await expect(page.locator('text=integration-test.txt')).toBeVisible();
  });

  test('should handle file download', async ({ page }) => {
    // First upload a file
    await page.goto('/upload');
    await page.setInputFiles('input[type="file"]', {
      name: 'download-test.txt',
      mimeType: 'text/plain',
      buffer: Buffer.from('Download test content')
    });

    await expect(page.locator('.upload-success')).toBeVisible();

    // Navigate to files page
    await page.goto('/files');

    // Start download
    const downloadPromise = page.waitForDownload();
    await page.locator('text=Download').first().click();
    const download = await downloadPromise;

    // Verify download
    expect(download.suggestedFilename()).toBe('download-test.txt');
  });

  test('should handle file deletion', async ({ page }) => {
    // Upload a file first
    await page.goto('/upload');
    await page.setInputFiles('input[type="file"]', {
      name: 'delete-test.txt',
      mimeType: 'text/plain',
      buffer: Buffer.from('Delete test content')
    });

    await expect(page.locator('.upload-success')).toBeVisible();

    // Go to files page
    await page.goto('/files');
    await expect(page.locator('text=delete-test.txt')).toBeVisible();

    // Delete file
    page.on('dialog', (dialog) => dialog.accept());
    await page.locator('text=Delete').first().click();

    // Verify file is removed
    await expect(page.locator('text=delete-test.txt')).not.toBeVisible();
  });
});

Security Best Practices

Secure Upload Handler

// lib/security/secureUpload.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

// File validation types (inline instead of separate import)
export interface FileValidationRule {
  maxSize?: number;
  allowedTypes?: string[];
  allowedExtensions?: string[];
  requireExtension?: boolean;
  sanitizeFileName?: boolean;
}

export class FileValidator {
  private rules: FileValidationRule;

  constructor(rules: FileValidationRule = {}) {
    this.rules = {
      maxSize: 10 * 1024 * 1024, // 10MB default
      allowedTypes: ['*/*'],
      allowedExtensions: [],
      requireExtension: false,
      sanitizeFileName: true,
      ...rules
    };
  }

  validate(file: File): { valid: boolean; errors: string[] } {
    const errors: string[] = [];

    // Check file size
    if (this.rules.maxSize && file.size > this.rules.maxSize) {
      errors.push(`File size ${file.size} exceeds maximum allowed size ${this.rules.maxSize}`);
    }

    // Check MIME type
    if (this.rules.allowedTypes && !this.rules.allowedTypes.includes('*/*')) {
      const isAllowed = this.rules.allowedTypes.some((type) => {
        if (type.endsWith('/*')) {
          return file.type.startsWith(type.slice(0, -1));
        }
        return file.type === type;
      });

      if (!isAllowed) {
        errors.push(`File type ${file.type} is not allowed`);
      }
    }

    // Check file extension
    const extension = this.getFileExtension(file.name);
    if (this.rules.requireExtension && !extension) {
      errors.push('File must have an extension');
    }

    if (this.rules.allowedExtensions && this.rules.allowedExtensions.length > 0) {
      if (!extension || !this.rules.allowedExtensions.includes(extension)) {
        errors.push(`File extension ${extension} is not allowed`);
      }
    }

    // Check filename
    if (this.rules.sanitizeFileName && !this.isValidFileName(file.name)) {
      errors.push('File name contains invalid characters');
    }

    return {
      valid: errors.length === 0,
      errors
    };
  }

  private getFileExtension(fileName: string): string {
    const parts = fileName.split('.');
    return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : '';
  }

  private isValidFileName(fileName: string): boolean {
    // Allow alphanumeric, spaces, hyphens, underscores, and dots
    return /^[a-zA-Z0-9\s\-_.]+$/.test(fileName);
  }

  sanitizeFileName(fileName: string): string {
    return fileName
      .replace(/[^a-zA-Z0-9\s\-_.]/g, '')
      .replace(/\s+/g, '-')
      .toLowerCase();
  }
}

export class SecureUploadHandler {
  private sdk: AsterismsBackendSDK;
  private validator: FileValidator;

  constructor(sdk: AsterismsBackendSDK, validationRules?: FileValidationRule) {
    this.sdk = sdk;
    this.validator = new FileValidator(validationRules);
  }

  async secureUpload(
    file: File,
    options: {
      bucketId: string;
      prefix?: string;
      userId?: string;
      sanitizeFileName?: boolean;
    }
  ): Promise<StorageFileData> {
    // Validate file
    const validation = this.validator.validate(file);
    if (!validation.valid) {
      throw new Error(`File validation failed: ${validation.errors.join(', ')}`);
    }

    // Sanitize filename if requested
    let sanitizedFile = file;
    if (options.sanitizeFileName) {
      const sanitizedName = this.validator.sanitizeFileName(file.name);
      sanitizedFile = new File([file], sanitizedName, { type: file.type });
    }

    // Add user-specific prefix for isolation
    const prefix = options.userId
      ? `${options.prefix || ''}users/${options.userId}/`
      : options.prefix || '';

    const storage = this.sdk.storage();

    try {
      const result = await storage.upload(sanitizedFile, {
        bucketId: options.bucketId,
        prefix,
        metadata: {
          uploadedBy: options.userId,
          uploadedAt: new Date().toISOString(),
          originalName: file.name
        }
      });

      return result;
    } catch (error) {
      console.error('Secure upload failed:', error);
      throw new Error('Failed to upload file securely');
    }
  }
}

Performance Optimization

Batch Operations

// lib/optimization/batchOperations.ts
import type { AsterismsBackendSDK, StorageFileData } from '@asterisms/sdk-backend';

export class BatchStorageOperations {
  private sdk: AsterismsBackendSDK;

  constructor(sdk: AsterismsBackendSDK) {
    this.sdk = sdk;
  }

  async batchUpload(
    files: File[],
    options: {
      bucketId: string;
      prefix?: string;
      batchSize?: number;
      onProgress?: (completed: number, total: number) => void;
    }
  ): Promise<{ successful: StorageFileData[]; failed: Array<{ file: File; error: string }> }> {
    const storage = this.sdk.storage();
    const batchSize = options.batchSize || 10;
    const successful: StorageFileData[] = [];
    const failed: Array<{ file: File; error: string }> = [];

    for (let i = 0; i < files.length; i += batchSize) {
      const batch = files.slice(i, i + batchSize);

      const batchPromises = batch.map(async (file) => {
        try {
          const result = await storage.upload(file, {
            bucketId: options.bucketId,
            prefix: options.prefix
          });
          return { success: true, result, file };
        } catch (error) {
          return { success: false, error: error.message, file };
        }
      });

      const batchResults = await Promise.all(batchPromises);

      for (const result of batchResults) {
        if (result.success) {
          successful.push(result.result);
        } else {
          failed.push({ file: result.file, error: result.error });
        }
      }

      if (options.onProgress) {
        options.onProgress(i + batch.length, files.length);
      }
    }

    return { successful, failed };
  }

  async batchDelete(
    externalIds: string[],
    options: {
      batchSize?: number;
      onProgress?: (completed: number, total: number) => void;
    } = {}
  ): Promise<{ successful: string[]; failed: Array<{ externalId: string; error: string }> }> {
    const storage = this.sdk.storage();
    const batchSize = options.batchSize || 20;
    const successful: string[] = [];
    const failed: Array<{ externalId: string; error: string }> = [];

    for (let i = 0; i < externalIds.length; i += batchSize) {
      const batch = externalIds.slice(i, i + batchSize);

      const batchPromises = batch.map(async (externalId) => {
        try {
          await storage.delete(externalId);
          return { success: true, externalId };
        } catch (error) {
          return { success: false, error: error.message, externalId };
        }
      });

      const batchResults = await Promise.all(batchPromises);

      for (const result of batchResults) {
        if (result.success) {
          successful.push(result.externalId);
        } else {
          failed.push({ externalId: result.externalId, error: result.error });
        }
      }

      if (options.onProgress) {
        options.onProgress(i + batch.length, externalIds.length);
      }
    }

    return { successful, failed };
  }
}

This comprehensive storage examples file provides:

  1. Basic Operations - Upload, download, metadata, and delete examples
  2. SvelteKit Integration - Complete API routes for storage operations
  3. Advanced Patterns - File management, image processing, and document management services
  4. Svelte Components - Reusable upload and gallery components
  5. Testing - Unit and integration test examples
  6. Security - File validation and secure upload patterns
  7. Performance - Batch operations and optimization techniques

The examples follow the same comprehensive structure as the notification examples and provide practical, production-ready code that developers can use as a foundation for their storage implementations.

See Also