saveToDisk() method.
UploadedFile Overview
File Info
filename, mimetype, sizeextname, md5
saveToDisk()
Save to storageAuto URL generation
toBuffer()
Buffer conversionCaching support
URL Properties
url, signedUrlAuto-set after saving
Basic Properties
File Information
Copy
import type { UploadedFile } from "sonamu";
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async upload(params: {
file: UploadedFile;
}): Promise<any> {
const { file } = params;
// 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
Copy
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async upload(params: {
file: UploadedFile;
}): Promise<{ url: string }> {
const { file } = params;
// Save file (using default disk)
const url = await file.saveToDisk(`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
Copy
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadToS3(params: {
file: UploadedFile;
}): Promise<{ url: string }> {
const { file } = params;
// Save to S3 disk
const url = await file.saveToDisk(
`uploads/${Date.now()}-${file.filename}`,
"s3" // Disk name
);
return { url };
}
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadPublic(params: {
file: UploadedFile;
}): Promise<{ url: string }> {
const { file } = params;
// Save to public disk
const url = await file.saveToDisk(
`public/${Date.now()}-${file.filename}`,
"public"
);
return { url };
}
}
Buffer Conversion
toBuffer() Method
Copy
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async processImage(params: {
image: UploadedFile;
}): Promise<{ url: string }> {
const { image } = params;
// Convert to Buffer (cached)
const buffer = await image.toBuffer();
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 };
}
}
toBuffer() caches the result, so calling multiple times only reads once.MD5 Hash
md5() Method
Copy
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadWithHash(params: {
file: UploadedFile;
}): Promise<{
url: string;
md5: string;
}> {
const { file } = params;
// 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(`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
Copy
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 {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async upload(params: {
file: UploadedFile;
}): Promise<{ url: string }> {
const { file } = params;
// Size validation (10MB)
FileValidator.validateSize(file, 10 * 1024 * 1024);
const url = await file.saveToDisk(`uploads/${Date.now()}.${file.extname}`);
return { url };
}
}
MIME Type Validation
Copy
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 {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadImage(params: {
image: UploadedFile;
}): Promise<{ url: string }> {
const { image } = params;
// MIME type validation
FileValidator.validateMimeType(image, [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
]);
const url = await image.saveToDisk(`images/${Date.now()}.${image.extname}`);
return { url };
}
}
Extension Validation
Copy
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 {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadDocument(params: {
document: UploadedFile;
}): Promise<{ url: string }> {
const { document } = params;
// Extension validation
FileValidator.validateExtension(document, [
"pdf",
"doc",
"docx",
"txt",
]);
const url = await document.saveToDisk(
`documents/${Date.now()}.${document.extname}`
);
return { url };
}
}
Integrated Validation Class
Copy
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 {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadImage(params: {
image: UploadedFile;
}): Promise<{ url: string }> {
const { image } = params;
FileValidator.validateImage(image);
const url = await image.saveToDisk(`images/${Date.now()}.${image.extname}`);
return { url };
}
}
Practical Examples
Profile Image Upload
Copy
class UserModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadProfileImage(params: {
image: UploadedFile;
}): Promise<{
imageId: number;
url: string;
thumbnailUrl: string;
}> {
const context = Sonamu.getContext();
if (!context.user) {
throw new Error("Authentication required");
}
const { image } = params;
// Validation
FileValidator.validateImage(image);
// Original image Buffer
const buffer = await image.toBuffer();
// 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(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
Copy
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "multiple" })
async uploadMultiple(params: {
files: UploadedFile[];
category?: string;
}): Promise<{
uploadedFiles: Array<{
fileId: number;
filename: string;
url: string;
md5: string;
}>;
}> {
const { files, category } = params;
if (!files || files.length === 0) {
throw new Error("At least one file is required");
}
if (files.length > 10) {
throw new Error("Maximum 10 files allowed");
}
const uploadedFiles = [];
const wdb = this.getPuri("w");
for (const file of files) {
// 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(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
Copy
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadAdvanced(params: {
file: UploadedFile;
}): Promise<any> {
const { file } = params;
// Access original Fastify MultipartFile
const rawFile = file.raw;
console.log({
encoding: rawFile.encoding,
fieldname: rawFile.fieldname,
// ...
});
const url = await file.saveToDisk(`uploads/${Date.now()}.${file.extname}`);
return { url };
}
}
Cautions
Cautions when using UploadedFile:
- Validation is required before calling
saveToDisk() toBuffer()is cached so can be called multiple timesurl,signedUrlare only available after saving- Consider streaming for large files
- Verify Storage configuration