Skip to main content
Learn how to save files to local, S3, GCS, and other storage using Sonamu’s Storage Manager.

Storage Manager Overview

Unified Interface

Single API Multiple storage support

Driver System

local, s3, gcs Extensible

saveToDisk()

Easy saving Auto URL generation

Disk Management

Multiple disk configuration Purpose-based separation

Basic Usage

The saveToDisk() method of UploadedFile is the simplest way to save files.
import type { UploadedFile } from "sonamu";

class FileModel extends BaseModelClass {
  @upload()
  async upload(): Promise<{ url: string }> {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0];
    if (!file) throw new Error("File is required");

    // Save file (default disk)
    const url = await file.saveToDisk("fs", `uploads/${Date.now()}-${file.filename}`);

    return { url };
  }
}

Disk Selection

class FileModel extends BaseModelClass {
  @upload()
  async uploadToS3(): Promise<{ url: string }> {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0];
    if (!file) throw new Error("File is required");

    // Save to S3 disk
    const url = await file.saveToDisk("s3", `uploads/${Date.now()}-${file.filename}`);

    return { url };
  }
}

Using Storage Manager Directly

Basic Usage

You can use Storage Manager directly when saveToDisk() cannot be used.
import { Sonamu } from "sonamu";

class FileModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async uploadBuffer(params: {
    buffer: Buffer;
    filename: string;
    mimetype: string;
  }): Promise<{ url: string }> {
    const { buffer, filename, mimetype } = params;

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

    // Save file
    const key = `uploads/${Date.now()}-${filename}`;
    await disk.put(key, new Uint8Array(buffer), {
      contentType: mimetype,
    });

    // Generate URL
    const url = await disk.getUrl(key);

    return { url };
  }
}

Using Specific Disk

class FileModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async uploadToS3(params: { buffer: Buffer; filename: string }): Promise<{ url: string }> {
    const { buffer, filename } = params;

    // Get S3 disk
    const s3Disk = Sonamu.storage.use("s3");

    // Save file
    const key = `uploads/${Date.now()}-${filename}`;
    await s3Disk.put(key, new Uint8Array(buffer), {
      contentType: "application/octet-stream",
    });

    // Generate URL
    const url = await s3Disk.getUrl(key);

    return { url };
  }
}

Storage Configuration

storage.config.ts

// storage.config.ts
import type { StorageConfig } from "sonamu";

export const storageConfig: StorageConfig = {
  default: "local", // Default disk

  disks: {
    // Local storage
    local: {
      driver: "local",
      root: "uploads",
      url: process.env.APP_URL || "http://localhost:3000",
    },

    // AWS S3
    s3: {
      driver: "s3",
      bucket: process.env.S3_BUCKET!,
      region: process.env.S3_REGION || "us-east-1",
      credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY_ID!,
        secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
      },
      endpoint: process.env.S3_ENDPOINT, // MinIO, etc.
    },

    // Google Cloud Storage
    gcs: {
      driver: "gcs",
      bucket: process.env.GCS_BUCKET!,
      projectId: process.env.GCS_PROJECT_ID!,
      keyFilename: process.env.GCS_KEY_FILENAME!,
    },

    // For public files
    public: {
      driver: "s3",
      bucket: process.env.PUBLIC_S3_BUCKET!,
      region: "us-east-1",
      credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY_ID!,
        secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
      },
    },

    // For private files
    private: {
      driver: "s3",
      bucket: process.env.PRIVATE_S3_BUCKET!,
      region: "us-east-1",
      credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY_ID!,
        secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
      },
    },
  },
};

Storage Driver API

put() - Save File

const disk = Sonamu.storage.use();

await disk.put(key, data, options);
Parameters:
  • key: string - Storage path
  • data: Uint8Array - File data
  • options: { contentType?: string } - Options

get() - Read File

const buffer = await disk.get(key);

delete() - Delete File

await disk.delete(key);

exists() - Check File Existence

const exists = await disk.exists(key);

getUrl() - Generate Public URL

const url = await disk.getUrl(key);

getSignedUrl() - Generate Signed URL

const signedUrl = await disk.getSignedUrl(key, expiresIn);

Practical Examples

Date-based Folder Structure

class FileModel extends BaseModelClass {
  @upload()
  async upload(): Promise<{ url: string }> {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0];
    if (!file) throw new Error("File is required");

    // Generate date-based path
    const now = new Date();
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, "0");
    const day = String(now.getDate()).padStart(2, "0");

    const key = `uploads/${year}/${month}/${day}/${Date.now()}.${file.extname}`;

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

    return { url };
  }
}

User-based Folder Structure

class FileModel extends BaseModelClass {
  @upload()
  async upload(): Promise<{ url: string }> {
    const context = Sonamu.getContext();

    if (!context.user) {
      throw new Error("Authentication required");
    }

    const { bufferedFiles } = context;
    const file = bufferedFiles?.[0];
    if (!file) throw new Error("File is required");

    // User-specific path
    const key = `users/${context.user.id}/${Date.now()}.${file.extname}`;

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

    return { url };
  }
}

Category-based Disk Selection

class MediaModel extends BaseModelClass {
  @upload()
  async uploadMedia(params: { category: "public" | "private" }): Promise<{ url: string }> {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0];
    if (!file) throw new Error("File is required");
    const { category } = params;

    // Select disk by category
    const diskName = category === "public" ? "public" : "private";

    const key = `media/${Date.now()}.${file.extname}`;
    const url = await file.saveToDisk(diskName, key);

    return { url };
  }
}

Image Optimization Before Saving

import sharp from "sharp";

class ImageModel extends BaseModelClass {
  @upload()
  async uploadOptimized(): Promise<{
    originalUrl: string;
    thumbnailUrl: string;
    mediumUrl: string;
  }> {
    const { bufferedFiles } = Sonamu.getContext();
    const image = bufferedFiles?.[0];
    if (!image) throw new Error("Image is required");

    // Original image Buffer
    const buffer = image.buffer;

    const timestamp = Date.now();
    const ext = image.extname;

    // Save original
    const originalKey = `images/original/${timestamp}.${ext}`;
    const originalUrl = await image.saveToDisk("fs", originalKey);

    // Get Storage Manager
    const disk = Sonamu.storage.use();

    // Create and save thumbnail (200x200)
    const thumbnailBuffer = await sharp(buffer)
      .resize(200, 200, { fit: "cover" })
      .jpeg({ quality: 80 })
      .toBuffer();

    const thumbnailKey = `images/thumbnail/${timestamp}.jpg`;
    await disk.put(thumbnailKey, new Uint8Array(thumbnailBuffer), {
      contentType: "image/jpeg",
    });
    const thumbnailUrl = await disk.getUrl(thumbnailKey);

    // Create and save medium size (800x800)
    const mediumBuffer = await sharp(buffer)
      .resize(800, 800, { fit: "inside" })
      .jpeg({ quality: 85 })
      .toBuffer();

    const mediumKey = `images/medium/${timestamp}.jpg`;
    await disk.put(mediumKey, new Uint8Array(mediumBuffer), {
      contentType: "image/jpeg",
    });
    const mediumUrl = await disk.getUrl(mediumKey);

    return {
      originalUrl,
      thumbnailUrl,
      mediumUrl,
    };
  }
}

Save to Multiple Disks Simultaneously

class FileModel extends BaseModelClass {
  @upload()
  async uploadWithBackup(): Promise<{
    primaryUrl: string;
    backupUrl: string;
  }> {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0];
    if (!file) throw new Error("File is required");

    const key = `uploads/${Date.now()}.${file.extname}`;
    const buffer = file.buffer;

    // Save to primary disk
    const primaryUrl = await file.saveToDisk("s3", key);

    // Also save to backup disk
    const backupDisk = Sonamu.storage.use("backup");
    await backupDisk.put(key, new Uint8Array(buffer), {
      contentType: file.mimetype,
    });
    const backupUrl = await backupDisk.getUrl(key);

    return {
      primaryUrl,
      backupUrl,
    };
  }
}

File Deletion

Delete File

class FileModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  async remove(fileId: number): Promise<void> {
    const rdb = this.getPuri("r");

    // Query file info
    const file = await rdb.table("files").where("id", fileId).first();

    if (!file) {
      throw new Error("File not found");
    }

    // Delete from Storage
    const disk = Sonamu.storage.use(file.disk_name);
    await disk.delete(file.key);

    // Delete from DB
    const wdb = this.getPuri("w");
    await wdb.table("files").where("id", fileId).delete();
  }
}

Check Existence Before Deletion

class FileModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  async removeSafe(fileId: number): Promise<void> {
    const rdb = this.getPuri("r");

    const file = await rdb.table("files").where("id", fileId).first();

    if (!file) {
      throw new Error("File not found");
    }

    const disk = Sonamu.storage.use(file.disk_name);

    // Check file existence
    const exists = await disk.exists(file.key);

    if (exists) {
      await disk.delete(file.key);
    } else {
      console.warn(`File not found in storage: ${file.key}`);
    }

    // Delete from DB
    const wdb = this.getPuri("w");
    await wdb.table("files").where("id", fileId).delete();
  }
}

Advanced Usage

Streaming Upload (Large Files)

For large files, use @upload({ consume: "stream" }). Files are streamed directly to the specified storage destination. Access the already-uploaded file info via context.uploadedFiles.
class FileModel extends BaseModelClass {
  @upload({ consume: "stream", destination: "s3" })
  async uploadLarge(): Promise<{ url: string }> {
    const { uploadedFiles } = Sonamu.getContext();
    const file = uploadedFiles?.[0];
    if (!file) throw new Error("File is required");

    // In stream mode, files are already saved to the destination
    return { url: file.url };
  }
}

Saving File Metadata

class FileModel extends BaseModelClass {
  @upload()
  async uploadWithMetadata(params: {
    title?: string;
    description?: string;
    tags?: string[];
  }): Promise<{ fileId: number; url: string }> {
    const context = Sonamu.getContext();

    if (!context.user) {
      throw new Error("Authentication required");
    }

    const { bufferedFiles } = context;
    const file = bufferedFiles?.[0];
    if (!file) throw new Error("File is required");
    const { title, description, tags } = params;

    // Save file
    const key = `uploads/${Date.now()}-${file.filename}`;
    const url = await file.saveToDisk("fs", key);

    // Save metadata to DB
    const wdb = this.getPuri("w");
    const [record] = await wdb
      .table("files")
      .insert({
        user_id: context.user.id,
        key,
        filename: file.filename,
        mime_type: file.mimetype,
        size: file.size,
        url,
        title,
        description,
        tags: tags ? JSON.stringify(tags) : null,
        uploaded_at: new Date(),
      })
      .returning({ id: "id" });

    return {
      fileId: record.id,
      url,
    };
  }
}

Cautions

Cautions when saving files: 1. saveToDisk() is the simplest method 2. Verify disk configuration 3. Set file size limits 4. Error handling is essential 5. Consider streaming for large files

Next Steps

File Upload Setup

@upload decorator

UploadedFile Class

File info access

URL Generation

Generating URLs

@api Decorator

API basic usage