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
saveToDisk() Method (Recommended)
Basic Usage
ThesaveToDisk() 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 whensaveToDisk() 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);
key: string- Storage pathdata: Uint8Array- File dataoptions: { 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 filesNext Steps
File Upload Setup
@upload decorator
UploadedFile Class
File info access
URL Generation
Generating URLs
@api Decorator
API basic usage