Skip to main content
The BufferedFile class provides information about uploaded files and saves files using the saveToDisk() method.

UploadedFile Overview

File Info

filename, mimetype, size extname, md5

saveToDisk()

Save to storage Auto URL generation

buffer

Buffer access .buffer getter

URL 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,
    };
  }
}
Property List:
  • filename: string - Original filename
  • mimetype: string - MIME type
  • size: number - File size (bytes)
  • extname: string | false - Extension (without dot)
  • url: string | undefined - Public URL after saving
  • signedUrl: 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 configuration

Next Steps

File Upload Setup

@upload decorator setup

Saving Files

Using Storage Manager

URL Generation

Generating URLs

@api Decorator

API basic usage