saveToDisk() λ©μλλ‘ νμΌμ μ μ₯ν©λλ€.
BufferedFile κ°μ
νμΌ μ 보
filename, mimetype, size extname, md5
saveToDisk()
μ€ν 리μ§μ μ μ₯ URL μλ μμ±
buffer
Buffer μ κ·Ό
.buffer getterURL μμ±
url, signedUrl μ μ₯ ν μλ μ€μ
κΈ°λ³Έ μμ±
νμΌ μ 보
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");
// νμΌ μ 보 μ κ·Ό
console.log({
filename: file.filename, // μλ³Έ νμΌλͺ
: "photo.jpg"
mimetype: file.mimetype, // MIME νμ
: "image/jpeg"
size: file.size, // νμΌ ν¬κΈ° (bytes): 524288
extname: file.extname, // νμ₯μ: "jpg" (μ μ μΈ)
});
return {
filename: file.filename,
mimetype: file.mimetype,
size: file.size,
extname: file.extname,
};
}
}
filename: string- μλ³Έ νμΌλͺmimetype: string- MIME νμsize: number- νμΌ ν¬κΈ° (λ°μ΄νΈ)extname: string | false- νμ₯μ (μ μ μΈ)url: string | undefined- μ μ₯ ν Public URLsignedUrl: string | undefined- μ μ₯ ν Signed URL
saveToDisk() λ©μλ
κΈ°λ³Έ μ¬μ©λ²
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");
// νμΌ μ μ₯ (κΈ°λ³Έ λμ€ν¬ μ¬μ©)
const url = await file.saveToDisk("fs", `uploads/${Date.now()}-${file.filename}`);
// url μμ±μ μλμΌλ‘ μ μ₯λ¨
console.log(file.url); // Public URL
console.log(file.signedUrl); // Signed URL
return { url };
}
}
νΉμ λμ€ν¬μ μ μ₯
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");
// S3 λμ€ν¬μ μ μ₯
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");
// Public λμ€ν¬μ μ μ₯
const url = await file.saveToDisk("public", `public/${Date.now()}-${file.filename}`);
return { url };
}
}
buffer Getter
buffer μμ±
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");
// Bufferμ μ§μ μ κ·Ό
const buffer = image.buffer;
console.log("Buffer size:", buffer.length);
// Bufferλ₯Ό μ΄μ©ν μ΄λ―Έμ§ μ²λ¦¬
const sharp = require("sharp");
const resized = await sharp(buffer).resize(800, 600).toBuffer();
// μ²λ¦¬λ μ΄λ―Έμ§ μ μ₯
// (saveToDiskλ μλ³Έ νμΌμ μ μ₯νλ―λ‘,
// μ²λ¦¬λ Bufferλ Storage Managerλ₯Ό μ§μ μ¬μ©)
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λ multipart νμ± μ 미리 λ‘λλ Bufferλ₯Ό λ°ννλ getterμ
λλ€.MD5 ν΄μ
md5() λ©μλ
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");
// MD5 ν΄μ κ³μ°
const md5Hash = await file.md5();
console.log("MD5:", md5Hash); // "abc123def456..."
// μ€λ³΅ νμΌ μ²΄ν¬
const rdb = this.getPuri("r");
const existing = await rdb.table("files").where("md5_hash", md5Hash).first();
if (existing) {
// μ΄λ―Έ λμΌν νμΌμ΄ μ‘΄μ¬
return {
url: existing.url,
md5: md5Hash,
};
}
// μ νμΌ μ μ₯
const url = await file.saveToDisk("fs", `uploads/${md5Hash}.${file.extname}`);
// 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 };
}
}
νμΌ κ²μ¦
ν¬κΈ° κ²μ¦
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)`);
}
}
}
// μ¬μ©
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");
// ν¬κΈ° κ²μ¦ (10MB)
FileValidator.validateSize(file, 10 * 1024 * 1024);
const url = await file.saveToDisk("fs", `uploads/${Date.now()}.${file.extname}`);
return { url };
}
}
MIME νμ κ²μ¦
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(", ")}`,
);
}
}
}
// μ¬μ©
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 νμ
κ²μ¦
FileValidator.validateMimeType(image, ["image/jpeg", "image/png", "image/gif", "image/webp"]);
const url = await image.saveToDisk("fs", `images/${Date.now()}.${image.extname}`);
return { url };
}
}
νμ₯μ κ²μ¦
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(", ")}`,
);
}
}
}
// μ¬μ©
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");
// νμ₯μ κ²μ¦
FileValidator.validateExtension(document, ["pdf", "doc", "docx", "txt"]);
const url = await document.saveToDisk("fs", `documents/${Date.now()}.${document.extname}`);
return { url };
}
}
ν΅ν© κ²μ¦ ν΄λμ€
class FileValidator {
static validateImage(file: UploadedFile): void {
// ν¬κΈ° νμΈ (5MB)
if (file.size > 5 * 1024 * 1024) {
throw new Error("Image too large (max 5MB)");
}
// MIME νμ
νμΈ
const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
if (!allowedTypes.includes(file.mimetype)) {
throw new Error(`Invalid image type: ${file.mimetype}`);
}
// νμ₯μ νμΈ
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 {
// ν¬κΈ° νμΈ (20MB)
if (file.size > 20 * 1024 * 1024) {
throw new Error("Document too large (max 20MB)");
}
// MIME νμ
νμΈ
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 {
// ν¬κΈ° νμΈ (100MB)
if (file.size > 100 * 1024 * 1024) {
throw new Error("Video too large (max 100MB)");
}
// MIME νμ
νμΈ
const allowedTypes = ["video/mp4", "video/mpeg", "video/quicktime", "video/x-msvideo"];
if (!allowedTypes.includes(file.mimetype)) {
throw new Error(`Invalid video type: ${file.mimetype}`);
}
}
}
// μ¬μ©
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 };
}
}
μ€μ μμ
νλ‘ν μ΄λ―Έμ§ μ λ‘λ
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");
// κ²μ¦
FileValidator.validateImage(image);
// μλ³Έ μ΄λ―Έμ§ Buffer
const buffer = image.buffer;
// μΈλ€μΌ μμ±
const sharp = require("sharp");
const thumbnailBuffer = await sharp(buffer)
.resize(200, 200, { fit: "cover" })
.jpeg({ quality: 80 })
.toBuffer();
// μλ³Έ μ μ₯
const originalKey = `profiles/${context.user.id}/original-${Date.now()}.${image.extname}`;
const originalUrl = await image.saveToDisk("fs", originalKey);
// μΈλ€μΌ μ μ₯ (Storage Manager μ§μ μ¬μ©)
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);
// 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");
return {
imageId: record.id,
url: originalUrl,
thumbnailUrl,
};
}
}
μ¬λ¬ νμΌ μΌκ΄ μ²λ¦¬
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) {
// κ²μ¦
if (file.size > 20 * 1024 * 1024) {
throw new Error(`File ${file.filename} too large (max 20MB)`);
}
// MD5 ν΄μ
const md5Hash = await file.md5();
// μ€λ³΅ 체ν¬
const rdb = this.getPuri("r");
const existing = await rdb.table("files").where("md5_hash", md5Hash).first();
if (existing) {
// μ΄λ―Έ μ‘΄μ¬νλ νμΌ
uploadedFiles.push({
fileId: existing.id,
filename: existing.filename,
url: existing.url,
md5: md5Hash,
});
continue;
}
// μ νμΌ μ μ₯
const key = `uploads/${category}/${Date.now()}.${file.extname}`;
const url = await file.saveToDisk("fs", key);
// 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");
uploadedFiles.push({
fileId: record.id,
filename: file.filename,
url,
md5: md5Hash,
});
}
return { uploadedFiles };
}
}
μλ³Έ MultipartFile μ κ·Ό
raw μμ±
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");
// μλ³Έ 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 };
}
}
μ£Όμμ¬ν
UploadedFile μ¬μ© μ μ£Όμμ¬ν: 1.
saveToDisk() νΈμΆ μ κ²μ¦ νμ 2.
.bufferλ‘ λ―Έλ¦¬ λ‘λλ Bufferμ μ§μ μ κ·Ό κ°λ₯ 3. url, signedUrlμ μ μ₯ νμλ§ μ¬μ© κ°λ₯ 4. λμ©λ νμΌμ
μ€νΈλ¦¬λ° κ³ λ € 5. Storage μ€μ νμΈλ€μ λ¨κ³
νμΌ μ λ‘λ μ€μ
@upload λ°μ½λ μ΄ν° μ€μ
νμΌ μ μ₯
Storage Manager μ¬μ©
URL μμ±
URL μμ±νκΈ°
@api λ°μ½λ μ΄ν°
API κΈ°λ³Έ μ¬μ©λ²