saveToDisk() method.
UploadedFile Overview
File Info
filename, mimetype, size extname, md5
saveToDisk()
Save to storage Auto URL generation
buffer
Buffer access
.buffer getterURL Properties
url, signedUrl Auto-set after saving
Basic Properties
File Information
import type { BufferedFile } from "sonamu";
class FileModel extends BaseModelClass {
@upload()
async upload(): Promise<any> {
const { bufferedFiles } = Sonamu.getContext();
const file = bufferedFiles?.[0];
if (!file) throw new Error("File is required");
// Access file info
console.log({
filename: file.filename, // Original filename: "photo.jpg"
mimetype: file.mimetype, // MIME type: "image/jpeg"
size: file.size, // File size (bytes): 524288
extname: file.extname, // Extension: "jpg" (without dot)
});
return {
filename: file.filename,
mimetype: file.mimetype,
size: file.size,
extname: file.extname,
};
}
}
filename: string- Original filenamemimetype: string- MIME typesize: number- File size (bytes)extname: string | false- Extension (without dot)url: string | undefined- Public URL after savingsignedUrl: string | undefined- Signed URL after saving
saveToDisk() Method
Basic Usage
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 (using default disk)
const url = await file.saveToDisk("fs", `uploads/${Date.now()}-${file.filename}`);
// Automatically saved to url property
console.log(file.url); // Public URL
console.log(file.signedUrl); // Signed URL
return { url };
}
}
Save to Specific Disk
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 };
}
@upload()
async uploadPublic(): Promise<{ url: string }> {
const { bufferedFiles } = Sonamu.getContext();
const file = bufferedFiles?.[0];
if (!file) throw new Error("File is required");
// Save to public disk
const url = await file.saveToDisk("public", `public/${Date.now()}-${file.filename}`);
return { url };
}
}
buffer Getter
buffer Property
class FileModel extends BaseModelClass {
@upload()
async processImage(): Promise<{ url: string }> {
const { bufferedFiles } = Sonamu.getContext();
const image = bufferedFiles?.[0];
if (!image) throw new Error("Image is required");
// Access Buffer directly
const buffer = image.buffer;
console.log("Buffer size:", buffer.length);
// Image processing using Buffer
const sharp = require("sharp");
const resized = await sharp(buffer).resize(800, 600).toBuffer();
// Save processed image
// (saveToDisk saves original file,
// use Storage Manager directly for processed Buffer)
const disk = Sonamu.storage.use();
const key = `images/${Date.now()}.jpg`;
await disk.put(key, new Uint8Array(resized), {
contentType: "image/jpeg",
});
const url = await disk.getUrl(key);
return { url };
}
}
.buffer is a getter that returns the pre-loaded Buffer from multipart parsing.MD5 Hash
md5() Method
class FileModel extends BaseModelClass {
@upload()
async uploadWithHash(): Promise<{
url: string;
md5: string;
}> {
const { bufferedFiles } = Sonamu.getContext();
const file = bufferedFiles?.[0];
if (!file) throw new Error("File is required");
// Calculate MD5 hash
const md5Hash = await file.md5();
console.log("MD5:", md5Hash); // "abc123def456..."
// Check for duplicate files
const rdb = this.getPuri("r");
const existing = await rdb.table("files").where("md5_hash", md5Hash).first();
if (existing) {
// Same file already exists
return {
url: existing.url,
md5: md5Hash,
};
}
// Save new file
const url = await file.saveToDisk("fs", `uploads/${md5Hash}.${file.extname}`);
// Save to DB
const wdb = this.getPuri("w");
await wdb.table("files").insert({
filename: file.filename,
mime_type: file.mimetype,
size: file.size,
md5_hash: md5Hash,
url,
});
return { url, md5: md5Hash };
}
}
File Validation
Size Validation
class FileValidator {
static validateSize(file: UploadedFile, maxSize: number): void {
if (file.size > maxSize) {
throw new Error(`File too large: ${file.size} bytes (max ${maxSize} bytes)`);
}
}
}
// Usage
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");
// Size validation (10MB)
FileValidator.validateSize(file, 10 * 1024 * 1024);
const url = await file.saveToDisk("fs", `uploads/${Date.now()}.${file.extname}`);
return { url };
}
}
MIME Type Validation
class FileValidator {
static validateMimeType(file: UploadedFile, allowedTypes: string[]): void {
if (!allowedTypes.includes(file.mimetype)) {
throw new Error(
`Invalid file type: ${file.mimetype}. ` + `Allowed: ${allowedTypes.join(", ")}`,
);
}
}
}
// Usage
class ImageModel extends BaseModelClass {
@upload()
async uploadImage(): Promise<{ url: string }> {
const { bufferedFiles } = Sonamu.getContext();
const image = bufferedFiles?.[0];
if (!image) throw new Error("Image is required");
// MIME type validation
FileValidator.validateMimeType(image, ["image/jpeg", "image/png", "image/gif", "image/webp"]);
const url = await image.saveToDisk("fs", `images/${Date.now()}.${image.extname}`);
return { url };
}
}
Extension Validation
class FileValidator {
static validateExtension(file: UploadedFile, allowedExtensions: string[]): void {
const ext = file.extname;
if (!ext || !allowedExtensions.includes(ext.toLowerCase())) {
throw new Error(
`Invalid file extension: ${ext}. ` + `Allowed: ${allowedExtensions.join(", ")}`,
);
}
}
}
// Usage
class DocumentModel extends BaseModelClass {
@upload()
async uploadDocument(): Promise<{ url: string }> {
const { bufferedFiles } = Sonamu.getContext();
const document = bufferedFiles?.[0];
if (!document) throw new Error("Document is required");
// Extension validation
FileValidator.validateExtension(document, ["pdf", "doc", "docx", "txt"]);
const url = await document.saveToDisk("fs", `documents/${Date.now()}.${document.extname}`);
return { url };
}
}
Integrated Validation Class
class FileValidator {
static validateImage(file: UploadedFile): void {
// Check size (5MB)
if (file.size > 5 * 1024 * 1024) {
throw new Error("Image too large (max 5MB)");
}
// Check MIME type
const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
if (!allowedTypes.includes(file.mimetype)) {
throw new Error(`Invalid image type: ${file.mimetype}`);
}
// Check extension
const allowedExtensions = ["jpg", "jpeg", "png", "gif", "webp"];
if (!file.extname || !allowedExtensions.includes(file.extname.toLowerCase())) {
throw new Error(`Invalid image extension: ${file.extname}`);
}
}
static validateDocument(file: UploadedFile): void {
// Check size (20MB)
if (file.size > 20 * 1024 * 1024) {
throw new Error("Document too large (max 20MB)");
}
// Check MIME type
const allowedTypes = [
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"text/plain",
];
if (!allowedTypes.includes(file.mimetype)) {
throw new Error(`Invalid document type: ${file.mimetype}`);
}
}
static validateVideo(file: UploadedFile): void {
// Check size (100MB)
if (file.size > 100 * 1024 * 1024) {
throw new Error("Video too large (max 100MB)");
}
// Check MIME type
const allowedTypes = ["video/mp4", "video/mpeg", "video/quicktime", "video/x-msvideo"];
if (!allowedTypes.includes(file.mimetype)) {
throw new Error(`Invalid video type: ${file.mimetype}`);
}
}
}
// Usage
class MediaModel extends BaseModelClass {
@upload()
async uploadImage(): Promise<{ url: string }> {
const { bufferedFiles } = Sonamu.getContext();
const image = bufferedFiles?.[0];
if (!image) throw new Error("Image is required");
FileValidator.validateImage(image);
const url = await image.saveToDisk("fs", `images/${Date.now()}.${image.extname}`);
return { url };
}
}
Practical Examples
Profile Image Upload
class UserModel extends BaseModelClass {
@upload()
async uploadProfileImage(): Promise<{
imageId: number;
url: string;
thumbnailUrl: string;
}> {
const context = Sonamu.getContext();
if (!context.user) {
throw new Error("Authentication required");
}
const { bufferedFiles } = context;
const image = bufferedFiles?.[0];
if (!image) throw new Error("Image is required");
// Validation
FileValidator.validateImage(image);
// Original image Buffer
const buffer = image.buffer;
// Create thumbnail
const sharp = require("sharp");
const thumbnailBuffer = await sharp(buffer)
.resize(200, 200, { fit: "cover" })
.jpeg({ quality: 80 })
.toBuffer();
// Save original
const originalKey = `profiles/${context.user.id}/original-${Date.now()}.${image.extname}`;
const originalUrl = await image.saveToDisk("fs", originalKey);
// Save thumbnail (using Storage Manager directly)
const disk = Sonamu.storage.use();
const thumbnailKey = `profiles/${context.user.id}/thumb-${Date.now()}.jpg`;
await disk.put(thumbnailKey, new Uint8Array(thumbnailBuffer), {
contentType: "image/jpeg",
});
const thumbnailUrl = await disk.getUrl(thumbnailKey);
// Save to DB
const wdb = this.getPuri("w");
const [record] = await wdb
.table("profile_images")
.insert({
user_id: context.user.id,
original_key: originalKey,
thumbnail_key: thumbnailKey,
original_url: originalUrl,
thumbnail_url: thumbnailUrl,
mime_type: image.mimetype,
size: image.size,
})
.returning({ id: "id" });
return {
imageId: record.id,
url: originalUrl,
thumbnailUrl,
};
}
}
Batch Processing Multiple Files
class FileModel extends BaseModelClass {
@upload()
async uploadMultiple(params: { category?: string }): Promise<{
uploadedFiles: Array<{
fileId: number;
filename: string;
url: string;
md5: string;
}>;
}> {
const { bufferedFiles } = Sonamu.getContext();
const { category } = params;
if (!bufferedFiles || bufferedFiles.length === 0) {
throw new Error("At least one file is required");
}
if (bufferedFiles.length > 10) {
throw new Error("Maximum 10 files allowed");
}
const uploadedFiles = [];
const wdb = this.getPuri("w");
for (const file of bufferedFiles) {
// Validation
if (file.size > 20 * 1024 * 1024) {
throw new Error(`File ${file.filename} too large (max 20MB)`);
}
// MD5 hash
const md5Hash = await file.md5();
// Duplicate check
const rdb = this.getPuri("r");
const existing = await rdb.table("files").where("md5_hash", md5Hash).first();
if (existing) {
// File already exists
uploadedFiles.push({
fileId: existing.id,
filename: existing.filename,
url: existing.url,
md5: md5Hash,
});
continue;
}
// Save new file
const key = `uploads/${category}/${Date.now()}.${file.extname}`;
const url = await file.saveToDisk("fs", key);
// Save to DB
const [record] = await wdb
.table("files")
.insert({
key,
filename: file.filename,
mime_type: file.mimetype,
size: file.size,
md5_hash: md5Hash,
url,
category,
})
.returning({ id: "id" });
uploadedFiles.push({
fileId: record.id,
filename: file.filename,
url,
md5: md5Hash,
});
}
return { uploadedFiles };
}
}
Accessing Original MultipartFile
raw Property
class FileModel extends BaseModelClass {
@upload()
async uploadAdvanced(): Promise<any> {
const { bufferedFiles } = Sonamu.getContext();
const file = bufferedFiles?.[0];
if (!file) throw new Error("File is required");
// Access original Fastify MultipartFile
const rawFile = file.raw;
console.log({
encoding: rawFile.encoding,
fieldname: rawFile.fieldname,
// ...
});
const url = await file.saveToDisk("fs", `uploads/${Date.now()}.${file.extname}`);
return { url };
}
}
Cautions
Cautions when using UploadedFile: 1. Validation is required before calling
saveToDisk() 2.
.buffer gives direct access to the pre-loaded Buffer 3. url, signedUrl are only available
after saving 4. Consider streaming for large files 5. Verify Storage configurationNext Steps
File Upload Setup
@upload decorator setup
Saving Files
Using Storage Manager
URL Generation
Generating URLs
@api Decorator
API basic usage