Skip to main content
Sonamu provides a unified file storage system based on Flydrive. You can use the local file system (fs) and AWS S3 with the same interface, and manage multiple disks as needed.

Why Storage Manager?

The Problem

// ❌ Different APIs for each driver
if (env === 'local') {
  fs.writeFileSync('uploads/avatar.png', buffer);
  url = '/uploads/avatar.png';
} else {
  await s3.putObject({
    Bucket: 'my-bucket',
    Key: 'uploads/avatar.png',
    Body: buffer
  });
  url = 'https://s3.amazonaws.com/my-bucket/uploads/avatar.png';
}
Problems:
  • Different code for each environment
  • Code changes required when switching drivers
  • URL generation logic is scattered

Using Storage Manager

// ✅ Unified interface
await Sonamu.storage.use().put('uploads/avatar.png', buffer);
const url = await Sonamu.storage.use().getUrl('uploads/avatar.png');
Benefits:
  • Driver-independent code
  • Switch drivers by just changing configuration
  • Consistent URL generation

Core Concepts

Storage Manager

A manager that handles multiple Disks.

Disk

A Disk is a single driver instance that stores files.
// Use default disk
const disk = Sonamu.storage.use();

// Use specific disk
const s3Disk = Sonamu.storage.use('s3');
const fsDisk = Sonamu.storage.use('fs');

Basic Configuration

sonamu.config.ts

import { drivers, type SonamuConfig } from "sonamu";

export const config: SonamuConfig = {
  server: {
    storage: {
      default: 'fs',  // Default disk
      drivers: {
        fs: drivers.fs({
          location: './uploads',  // Storage location
          urlBuilder: {
            generateURL: (key) => `/uploads/${key}`,
          }
        }),
      }
    }
  }
};
Configuration elements:
  • default: Name of the default disk to use
  • drivers: Driver factories by disk name

Using Disks

Default Disk

// Use default disk (config.default)
const disk = Sonamu.storage.use();

// Save file
await disk.put('avatar.png', buffer);

// Get URL
const url = await disk.getUrl('avatar.png');
// => '/uploads/avatar.png' (fs)
// => 'https://bucket.s3.amazonaws.com/avatar.png' (s3)

Specific Disk

// fs disk
const fsDisk = Sonamu.storage.use('fs');
await fsDisk.put('temp/file.txt', 'content');

// s3 disk
const s3Disk = Sonamu.storage.use('s3');
await s3Disk.put('backup/file.txt', 'content');

Managing Multiple Disks

Multi-Disk Configuration

export const config: SonamuConfig = {
  server: {
    storage: {
      default: process.env.STORAGE_DISK || 'fs',
      drivers: {
        // Local development
        fs: drivers.fs({
          location: './uploads',
          urlBuilder: {
            generateURL: (key) => `/uploads/${key}`,
          }
        }),

        // Production (S3)
        s3: drivers.s3({
          credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
          },
          region: process.env.AWS_REGION!,
          bucket: process.env.AWS_BUCKET!,
          urlBuilder: {
            generateURL: (key) =>
              `https://${process.env.AWS_BUCKET}.s3.amazonaws.com/${key}`,
          }
        }),

        // Public files
        public: drivers.s3({
          credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
          },
          region: process.env.AWS_REGION!,
          bucket: process.env.AWS_PUBLIC_BUCKET!,
          urlBuilder: {
            generateURL: (key) =>
              `https://${process.env.AWS_PUBLIC_BUCKET}.s3.amazonaws.com/${key}`,
          }
        }),
      }
    }
  }
};

Using Disks by Purpose

class UserModelClass extends BaseModel {
  // Profile photos → public disk
  async uploadAvatar(ctx: UploadContext) {
    const file = ctx.file;
    const url = await file.saveToDisk(
      `avatars/${ctx.user.id}.png`,
      'public'  // Public disk
    );
    return url;
  }

  // Private documents → default disk (private)
  async uploadDocument(ctx: UploadContext) {
    const file = ctx.file;
    const url = await file.saveToDisk(
      `documents/${ctx.user.id}/${file.filename}`
      // Uses default disk
    );
    return url;
  }

  // Backup → separate disk
  async backupData(data: any) {
    const json = JSON.stringify(data);
    await Sonamu.storage
      .use('backup')
      .put(`backups/${Date.now()}.json`, json);
  }
}

Environment-based Strategies

Development/Production Separation

Using local file system
// .env.development
STORAGE_DISK=fs

// sonamu.config.ts
storage: {
  default: process.env.STORAGE_DISK || 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `/uploads/${key}`,
      }
    }),
  }
}
Benefits:
  • No AWS account needed
  • Fast development
  • No cost

Hybrid Strategy

export const config: SonamuConfig = {
  server: {
    storage: {
      default: 's3',
      drivers: {
        // Main storage: S3
        s3: drivers.s3({ /* ... */ }),

        // Temporary files: Local
        fs: drivers.fs({
          location: './temp',
          urlBuilder: {
            generateURL: (key) => `/temp/${key}`,
          }
        }),
      }
    }
  }
};

// Usage
class FileModelClass extends BaseModel {
  // Permanent storage → S3
  async upload(ctx: UploadContext) {
    return ctx.file.saveToDisk('uploads/file.pdf');  // S3
  }

  // Temporary processing → Local
  async processTempFile(ctx: UploadContext) {
    const tempPath = `temp/${Date.now()}.tmp`;
    await ctx.file.saveToDisk(tempPath, 'fs');  // Local

    // Delete after processing
    await Sonamu.storage.use('fs').delete(tempPath);
  }
}

Practical Examples

1. Profile Photo Upload

class UserModelClass extends BaseModel {
  @upload({ mode: 'single' })
  @api({ httpMethod: 'POST' })
  async uploadAvatar(ctx: Context) {
    const { files } = Sonamu.getContext();
    const file = files?.[0]; // Use first file

    if (!file) {
      throw new Error('No file provided');
    }

    // Filename: user-{id}-{timestamp}.{ext}
    const filename = `user-${ctx.user.id}-${Date.now()}.${file.extname}`;
    const key = `avatars/${filename}`;

    // Save to default disk
    const url = await file.saveToDisk(key);

    // Update DB
    await this.updateOne(['id', ctx.user.id], { avatar_url: url });

    return { url };
  }
}

2. Multiple File Upload

class PostModelClass extends BaseModel {
  @upload({ mode: 'multiple' })
  @api({ httpMethod: 'POST' })
  async uploadImages(postId: number) {
    const { files } = Sonamu.getContext();

    const urls = await Promise.all(
      files.map(async (file, index) => {
        const key = `posts/${postId}/image-${index}.${file.extname}`;
        return await file.saveToDisk(key);
      })
    );

    return { urls };
  }
}

3. Separation by Disk

class FileModelClass extends BaseModel {
  @upload({ mode: 'single' })
  @api({ httpMethod: 'POST' })
  async upload(type: 'public' | 'private') {
    const { files } = Sonamu.getContext();
    const file = files?.[0]; // Use first file

    if (!file) {
      throw new Error('No file provided');
    }

    // Use different disk based on type
    const disk = type === 'public' ? 'public' : undefined;
    const key = `${type}/${Date.now()}-${file.filename}`;

    const url = await file.saveToDisk(key, disk);

    return { url, type };
  }
}

Storage Manager API

use(diskName?)

Returns a disk instance.
// Default disk
const disk = Sonamu.storage.use();

// Specific disk
const s3 = Sonamu.storage.use('s3');
const fs = Sonamu.storage.use('fs');
Return value: Disk instance (Flydrive)

defaultDisk

Returns the default disk name.
const defaultName = Sonamu.storage.defaultDisk;
// => 'fs' or 's3', etc.

Disk API (Flydrive)

The Disk returned by Storage Manager is a Flydrive Disk instance.

Key Methods

const disk = Sonamu.storage.use();

// Save file
await disk.put('path/to/file.txt', 'content');
await disk.put('path/to/file.txt', buffer);

// Read file
const content = await disk.get('path/to/file.txt');

// Check file existence
const exists = await disk.exists('path/to/file.txt');

// Delete file
await disk.delete('path/to/file.txt');

// Get URL
const url = await disk.getUrl('path/to/file.txt');

// Get Signed URL (S3)
const signedUrl = await disk.getSignedUrl('path/to/file.txt', {
  expiresIn: '1h'
});

Cautions

Cautions when using Storage Manager:
  1. Environment variables required: AWS credentials needed for S3
    // ❌ No hardcoding
    credentials: {
      accessKeyId: 'AKIA...',
      secretAccessKey: '...',
    }
    
    // ✅ Use environment variables
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
    }
    
  2. Disk name typos: Error when using non-existent disk
    // ❌ Typo
    Sonamu.storage.use('S3')  // Error!
    
    // ✅ Correct name
    Sonamu.storage.use('s3')
    
  3. Path separator: Use / for platform independence
    // ✅ Always use forward slash
    await disk.put('uploads/avatar.png', buffer);
    
    // ❌ Don't use backslash
    await disk.put('uploads\\avatar.png', buffer);
    
  4. URL builder required: fs driver requires urlBuilder configuration
    // ❌ No urlBuilder
    fs: drivers.fs({
      location: './uploads',
    })
    
    // ✅ urlBuilder configured
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `/uploads/${key}`,
      }
    })
    
  5. Lazy Initialization: Disks are initialized on first use
    // Configuration errors are discovered when use() is called
    try {
      const disk = Sonamu.storage.use('s3');
    } catch (error) {
      // Invalid S3 configuration, etc.
    }
    

Next Steps