File Upload Flow
@upload Decorator
Used when creating file upload APIs.Single File Upload
Copy
import { BaseModel, api, upload, Sonamu } from "sonamu";
class UserModelClass extends BaseModel {
@upload()
async uploadAvatar(ctx: Context) {
const { files } = Sonamu.getContext();
const file = files?.[0]; // Use first file
if (!file) {
throw new Error('No file provided');
}
// Save file
const url = await file.saveToDisk(`avatars/${ctx.user.id}.png`);
return { url };
}
}
Copy
const formData = new FormData();
formData.append('file', file);
await fetch('/api/user/uploadAvatar', {
method: 'POST',
body: formData,
});
Multiple File Upload
Copy
class PostModelClass extends BaseModel {
@upload()
async uploadImages(postId: number) {
const { files } = Sonamu.getContext();
if (files.length === 0) {
throw new Error('No files provided');
}
// Save all files
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 };
}
}
Copy
const formData = new FormData();
formData.append('file', file1);
formData.append('file', file2);
formData.append('file', file3);
await fetch('/api/post/uploadImages?postId=1', {
method: 'POST',
body: formData,
});
Accessing Files
Access uploaded files through thefiles property of Sonamu.getContext().
Copy
// Access uploaded files from Context
const { files } = Sonamu.getContext();
// files is UploadedFile[] (optional array)
type Context = {
// ...
files?: UploadedFile[];
};
Copy
const file = files?.[0]; // First file
Copy
for (const file of files) {
// Process each file
}
Usage Example
Copy
@upload()
async upload() {
const { files } = Sonamu.getContext();
const file = files?.[0]; // Use first file
if (!file) {
throw new Error('File is required');
}
// file is an UploadedFile instance
console.log(file.filename); // Original filename
console.log(file.mimetype); // MIME type
console.log(file.size); // File size
return await file.saveToDisk('uploads/file.pdf');
}
UploadedFile Class
A class that wraps uploaded files.Properties
- filename
- mimetype
- size
- extname
- url
- signedUrl
Original filename
Copy
const { files } = Sonamu.getContext();
const file = files?.[0]; // Use first file
console.log(file.filename); // 'avatar.png'
MIME type
Copy
console.log(file.mimetype);
// 'image/png'
// 'application/pdf'
// 'video/mp4'
File size (bytes)
Copy
console.log(file.size); // 1024000 (1MB)
console.log(`${(file.size / 1024 / 1024).toFixed(2)}MB`); // '0.98MB'
Extension (without dot)
Copy
console.log(file.extname);
// 'png'
// 'pdf'
// 'mp4'
// false (no extension)
URL after saving (unsigned)
Copy
await file.saveToDisk('uploads/file.pdf');
console.log(file.url);
// '/uploads/file.pdf' (fs)
// 'https://bucket.s3.amazonaws.com/uploads/file.pdf' (s3)
Signed URL after saving (S3)Use case: Temporary access URL (with expiration)
Copy
await file.saveToDisk('uploads/file.pdf');
console.log(file.signedUrl);
// 'https://bucket.s3.amazonaws.com/uploads/file.pdf?X-Amz-...'
Methods
toBuffer()
Converts the file to a Buffer.Copy
const { files } = Sonamu.getContext();
const file = files?.[0]; // Use first file
const buffer = await file.toBuffer();
console.log(buffer instanceof Buffer); // true
console.log(buffer.length); // File size (bytes)
md5()
Calculates the MD5 hash of the file.Copy
const { files } = Sonamu.getContext();
const file = files?.[0]; // Use first file
const hash = await file.md5();
console.log(hash); // '5d41402abc4b2a76b9719d911017c592'
- File deduplication check
- File integrity verification
- Unique filename generation
saveToDisk(key, diskName?)
Saves the file to disk.Copy
const { files } = Sonamu.getContext();
const file = files?.[0]; // Use first file
// Save to default disk
const url = await file.saveToDisk('uploads/file.pdf');
// Save to specific disk
const url = await file.saveToDisk('uploads/file.pdf', 's3');
Practical Examples
1. Profile Photo Upload
Copy
class UserModelClass extends BaseModel {
@upload()
@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');
}
// Allow only image files
if (!file.mimetype.startsWith('image/')) {
throw new Error('Only image files can be uploaded');
}
// File size limit (5MB)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
throw new Error('File size must be 5MB or less');
}
// Filename: user-{id}-{timestamp}.{ext}
const timestamp = Date.now();
const ext = file.extname || 'png';
const key = `avatars/user-${ctx.user.id}-${timestamp}.${ext}`;
// Save
const url = await file.saveToDisk(key);
// Update DB
await this.updateOne(['id', ctx.user.id], {
avatar_url: url,
updated_at: new Date(),
});
return { url };
}
}
2. Multiple Image Upload
Copy
class PostModelClass extends BaseModel {
@upload()
@api({ httpMethod: 'POST' })
async uploadImages(postId: number, ctx: Context) {
const { files } = Sonamu.getContext();
// Limit number of files
if (files.length > 10) {
throw new Error('Maximum 10 files can be uploaded');
}
// Filter images only
const imageFiles = files.filter(file =>
file.mimetype.startsWith('image/')
);
if (imageFiles.length === 0) {
throw new Error('No image files found');
}
// Save all images
const urls = await Promise.all(
imageFiles.map(async (file, index) => {
const timestamp = Date.now();
const ext = file.extname || 'png';
const key = `posts/${postId}/image-${index}-${timestamp}.${ext}`;
return await file.saveToDisk(key);
})
);
return { urls, count: urls.length };
}
}
3. File Type Validation
Copy
class FileModelClass extends BaseModel {
@upload()
@api({ httpMethod: 'POST' })
async uploadDocument(type: 'pdf' | 'excel' | 'word') {
const { files } = Sonamu.getContext();
const file = files?.[0]; // Use first file
if (!file) {
throw new Error('No file provided');
}
// MIME type validation
const allowedMimeTypes = {
pdf: ['application/pdf'],
excel: [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
],
word: [
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
],
};
if (!allowedMimeTypes[type].includes(file.mimetype)) {
throw new Error(`Only ${type} files can be uploaded`);
}
// Save file
const key = `documents/${type}/${Date.now()}-${file.filename}`;
const url = await file.saveToDisk(key);
return { url, type, filename: file.filename };
}
}
4. Deduplication Check with MD5 Hash
Copy
class MediaModelClass extends BaseModel {
@upload()
@api({ httpMethod: 'POST' })
async uploadMedia() {
const { files } = Sonamu.getContext();
const file = files?.[0]; // Use first file
if (!file) {
throw new Error('No file provided');
}
// Calculate MD5 hash
const hash = await file.md5();
// Check if file already exists
const existing = await this.findOne(['md5_hash', hash]);
if (existing) {
// Return existing URL if already exists
return {
url: existing.url,
duplicate: true,
};
}
// Save new file
const key = `media/${hash}.${file.extname}`;
const url = await file.saveToDisk(key);
// Save to DB
await this.saveOne({
md5_hash: hash,
url,
filename: file.filename,
size: file.size,
mimetype: file.mimetype,
});
return { url, duplicate: false };
}
}
5. Disk Selection
Copy
class FileModelClass extends BaseModel {
@upload()
@api({ httpMethod: 'POST' })
async upload(visibility: 'public' | 'private', ctx: Context) {
const { files } = Sonamu.getContext();
const file = files?.[0]; // Use first file
if (!file) {
throw new Error('No file provided');
}
// Use different disk based on visibility
const disk = visibility === 'public' ? 'public' : 'private';
const key = `${visibility}/${ctx.user.id}/${Date.now()}-${file.filename}`;
// Save
const url = await file.saveToDisk(key, disk);
// Save to DB
await this.saveOne({
user_id: ctx.user.id,
url,
visibility,
filename: file.filename,
});
return { url, visibility };
}
}
File Retrieval
Use Storage Manager to retrieve saved files.Reading Files
Copy
import { Sonamu } from "sonamu";
@api({ httpMethod: 'GET' })
async getFile(key: string) {
const disk = Sonamu.storage.use();
// Check file existence
const exists = await disk.exists(key);
if (!exists) {
throw new Error('File not found');
}
// Read file
const content = await disk.get(key);
return content; // Buffer
}
File Download API
Copy
@api({
httpMethod: 'GET',
contentType: 'application/octet-stream',
})
async downloadFile(key: string, ctx: Context) {
const disk = Sonamu.storage.use();
// Check file existence
const exists = await disk.exists(key);
if (!exists) {
throw new Error('File not found');
}
// Read file
const buffer = await disk.get(key);
// Extract filename
const filename = key.split('/').pop() || 'download';
// Set download headers
ctx.reply.header('Content-Disposition', `attachment; filename="${filename}"`);
return buffer;
}
Getting URL
Copy
@api({ httpMethod: 'GET' })
async getFileUrl(key: string) {
const disk = Sonamu.storage.use();
// Check file existence
const exists = await disk.exists(key);
if (!exists) {
throw new Error('File not found');
}
// Get URL
const url = await disk.getUrl(key);
return { url };
}
Signed URL (S3)
Copy
@api({ httpMethod: 'GET' })
async getSignedUrl(key: string) {
const disk = Sonamu.storage.use('s3');
// Generate Signed URL (valid for 1 hour)
const signedUrl = await disk.getSignedUrl(key, {
expiresIn: '1h'
});
return { url: signedUrl };
}
File Deletion
Copy
@api({ httpMethod: 'DELETE' })
async deleteFile(key: string) {
const disk = Sonamu.storage.use();
// Check file existence
const exists = await disk.exists(key);
if (!exists) {
throw new Error('File not found');
}
// Delete file
await disk.delete(key);
return { success: true };
}
Cautions
Cautions when uploading files:
-
File validation is required: Validate type, size, and extension
Copy
// β Validation if (!file.mimetype.startsWith('image/')) { throw new Error('Images only'); } if (file.size > 5 * 1024 * 1024) { throw new Error('5MB or less'); } -
Use unique filenames: Prevent overwrites
Copy
// β Using original filename await file.saveToDisk(`uploads/${file.filename}`); // β Add timestamp await file.saveToDisk(`uploads/${Date.now()}-${file.filename}`); // β Use UUID await file.saveToDisk(`uploads/${uuid()}.${file.extname}`); -
Path validation: Prevent directory traversal
Copy
// β Dangerous: Can access parent directories const key = userInput; // '../../../etc/passwd' // β Safe: Path validation const key = `uploads/${userInput.replace(/\.\./g, '')}`; -
File size limit: Prevent memory overflow
Copy
const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { throw new Error('File is too large'); } -
MIME type validation: Donβt trust only the extension
Copy
// β Only checking extension if (file.filename.endsWith('.png')) { ... } // β Check MIME type if (file.mimetype === 'image/png') { ... }