Storage Manager ๊ฐ์
ํตํฉ ์ธํฐํ์ด์ค
๋จ์ผ API์ฌ๋ฌ ์คํ ๋ฆฌ์ง ์ง์
๋๋ผ์ด๋ฒ ์์คํ
local, s3, gcsํ์ฅ ๊ฐ๋ฅ
saveToDisk()
๊ฐํธํ ์ ์ฅURL ์๋ ์์ฑ
๋์คํฌ ๊ด๋ฆฌ
์ฌ๋ฌ ๋์คํฌ ์ค์ ๋ชฉ์ ๋ณ ๋ถ๋ฆฌ
saveToDisk() ๋ฉ์๋ (๊ถ์ฅ)
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
UploadedFile์ saveToDisk() ๋ฉ์๋๊ฐ ๊ฐ์ฅ ๊ฐํธํ ํ์ผ ์ ์ฅ ๋ฐฉ๋ฒ์
๋๋ค.
๋ณต์ฌ
import type { UploadedFile } from "sonamu";
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async upload(params: {
file: UploadedFile;
}): Promise<{ url: string }> {
const { file } = params;
// ํ์ผ ์ ์ฅ (๊ธฐ๋ณธ ๋์คํฌ)
const url = await file.saveToDisk(`uploads/${Date.now()}-${file.filename}`);
return { url };
}
}
๋์คํฌ ์ ํ
๋ณต์ฌ
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadToS3(params: {
file: UploadedFile;
}): Promise<{ url: string }> {
const { file } = params;
// S3 ๋์คํฌ์ ์ ์ฅ
const url = await file.saveToDisk(
`uploads/${Date.now()}-${file.filename}`,
"s3" // ๋์คํฌ ์ด๋ฆ
);
return { url };
}
}
Storage Manager ์ง์ ์ฌ์ฉ
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
saveToDisk()๋ฅผ ์ฌ์ฉํ ์ ์๋ ๊ฒฝ์ฐ Storage Manager๋ฅผ ์ง์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๋ณต์ฌ
import { Sonamu } from "sonamu";
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async uploadBuffer(params: {
buffer: Buffer;
filename: string;
mimetype: string;
}): Promise<{ url: string }> {
const { buffer, filename, mimetype } = params;
// ๊ธฐ๋ณธ ๋์คํฌ ๊ฐ์ ธ์ค๊ธฐ
const disk = Sonamu.storage.use();
// ํ์ผ ์ ์ฅ
const key = `uploads/${Date.now()}-${filename}`;
await disk.put(key, new Uint8Array(buffer), {
contentType: mimetype,
});
// URL ์์ฑ
const url = await disk.getUrl(key);
return { url };
}
}
ํน์ ๋์คํฌ ์ฌ์ฉ
๋ณต์ฌ
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async uploadToS3(params: {
buffer: Buffer;
filename: string;
}): Promise<{ url: string }> {
const { buffer, filename } = params;
// S3 ๋์คํฌ ๊ฐ์ ธ์ค๊ธฐ
const s3Disk = Sonamu.storage.use("s3");
// ํ์ผ ์ ์ฅ
const key = `uploads/${Date.now()}-${filename}`;
await s3Disk.put(key, new Uint8Array(buffer), {
contentType: "application/octet-stream",
});
// URL ์์ฑ
const url = await s3Disk.getUrl(key);
return { url };
}
}
Storage ์ค์
storage.config.ts
๋ณต์ฌ
// storage.config.ts
import type { StorageConfig } from "sonamu";
export const storageConfig: StorageConfig = {
default: "local", // ๊ธฐ๋ณธ ๋์คํฌ
disks: {
// ๋ก์ปฌ ์คํ ๋ฆฌ์ง
local: {
driver: "local",
root: "uploads",
url: process.env.APP_URL || "http://localhost:3000",
},
// AWS S3
s3: {
driver: "s3",
bucket: process.env.S3_BUCKET!,
region: process.env.S3_REGION || "us-east-1",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
endpoint: process.env.S3_ENDPOINT, // MinIO ๋ฑ
},
// Google Cloud Storage
gcs: {
driver: "gcs",
bucket: process.env.GCS_BUCKET!,
projectId: process.env.GCS_PROJECT_ID!,
keyFilename: process.env.GCS_KEY_FILENAME!,
},
// Public ํ์ผ์ฉ
public: {
driver: "s3",
bucket: process.env.PUBLIC_S3_BUCKET!,
region: "us-east-1",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
},
// ํ๋ผ์ด๋น ํ์ผ์ฉ
private: {
driver: "s3",
bucket: process.env.PRIVATE_S3_BUCKET!,
region: "us-east-1",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
},
},
};
Storage Driver API
put() - ํ์ผ ์ ์ฅ
๋ณต์ฌ
const disk = Sonamu.storage.use();
await disk.put(key, data, options);
key: string- ์ ์ฅ ๊ฒฝ๋กdata: Uint8Array- ํ์ผ ๋ฐ์ดํฐoptions: { contentType?: string }- ์ต์
get() - ํ์ผ ์ฝ๊ธฐ
๋ณต์ฌ
const buffer = await disk.get(key);
delete() - ํ์ผ ์ญ์
๋ณต์ฌ
await disk.delete(key);
exists() - ํ์ผ ์กด์ฌ ํ์ธ
๋ณต์ฌ
const exists = await disk.exists(key);
getUrl() - Public URL ์์ฑ
๋ณต์ฌ
const url = await disk.getUrl(key);
getSignedUrl() - Signed URL ์์ฑ
๋ณต์ฌ
const signedUrl = await disk.getSignedUrl(key, expiresIn);
์ค์ ์์
๋ ์ง๋ณ ํด๋ ๊ตฌ์กฐ
๋ณต์ฌ
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async upload(params: {
file: UploadedFile;
}): Promise<{ url: string }> {
const { file } = params;
// ๋ ์ง๋ณ ๊ฒฝ๋ก ์์ฑ
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const key = `uploads/${year}/${month}/${day}/${Date.now()}.${file.extname}`;
const url = await file.saveToDisk(key);
return { url };
}
}
์ฌ์ฉ์๋ณ ํด๋ ๊ตฌ์กฐ
๋ณต์ฌ
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async upload(params: {
file: UploadedFile;
}): Promise<{ url: string }> {
const context = Sonamu.getContext();
if (!context.user) {
throw new Error("Authentication required");
}
const { file } = params;
// ์ฌ์ฉ์๋ณ ๊ฒฝ๋ก
const key = `users/${context.user.id}/${Date.now()}.${file.extname}`;
const url = await file.saveToDisk(key);
return { url };
}
}
์นดํ ๊ณ ๋ฆฌ๋ณ ๋์คํฌ
๋ณต์ฌ
class MediaModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadMedia(params: {
file: UploadedFile;
category: "public" | "private";
}): Promise<{ url: string }> {
const { file, category } = params;
// ์นดํ
๊ณ ๋ฆฌ๋ณ ๋์คํฌ ์ ํ
const diskName = category === "public" ? "public" : "private";
const key = `media/${Date.now()}.${file.extname}`;
const url = await file.saveToDisk(key, diskName);
return { url };
}
}
์ด๋ฏธ์ง ์ต์ ํ ํ ์ ์ฅ
๋ณต์ฌ
import sharp from "sharp";
class ImageModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadOptimized(params: {
image: UploadedFile;
}): Promise<{
originalUrl: string;
thumbnailUrl: string;
mediumUrl: string;
}> {
const { image } = params;
// ์๋ณธ ์ด๋ฏธ์ง Buffer
const buffer = await image.toBuffer();
const timestamp = Date.now();
const ext = image.extname;
// ์๋ณธ ์ ์ฅ
const originalKey = `images/original/${timestamp}.${ext}`;
const originalUrl = await image.saveToDisk(originalKey);
// Storage Manager ๊ฐ์ ธ์ค๊ธฐ
const disk = Sonamu.storage.use();
// ์ธ๋ค์ผ ์์ฑ ๋ฐ ์ ์ฅ (200x200)
const thumbnailBuffer = await sharp(buffer)
.resize(200, 200, { fit: "cover" })
.jpeg({ quality: 80 })
.toBuffer();
const thumbnailKey = `images/thumbnail/${timestamp}.jpg`;
await disk.put(thumbnailKey, new Uint8Array(thumbnailBuffer), {
contentType: "image/jpeg",
});
const thumbnailUrl = await disk.getUrl(thumbnailKey);
// ์ค๊ฐ ํฌ๊ธฐ ์์ฑ ๋ฐ ์ ์ฅ (800x800)
const mediumBuffer = await sharp(buffer)
.resize(800, 800, { fit: "inside" })
.jpeg({ quality: 85 })
.toBuffer();
const mediumKey = `images/medium/${timestamp}.jpg`;
await disk.put(mediumKey, new Uint8Array(mediumBuffer), {
contentType: "image/jpeg",
});
const mediumUrl = await disk.getUrl(mediumKey);
return {
originalUrl,
thumbnailUrl,
mediumUrl,
};
}
}
์ฌ๋ฌ ๋์คํฌ์ ๋์ ์ ์ฅ
๋ณต์ฌ
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadWithBackup(params: {
file: UploadedFile;
}): Promise<{
primaryUrl: string;
backupUrl: string;
}> {
const { file } = params;
const key = `uploads/${Date.now()}.${file.extname}`;
const buffer = await file.toBuffer();
// Primary ๋์คํฌ์ ์ ์ฅ
const primaryUrl = await file.saveToDisk(key, "s3");
// Backup ๋์คํฌ์๋ ์ ์ฅ
const backupDisk = Sonamu.storage.use("backup");
await backupDisk.put(key, new Uint8Array(buffer), {
contentType: file.mimetype,
});
const backupUrl = await backupDisk.getUrl(key);
return {
primaryUrl,
backupUrl,
};
}
}
ํ์ผ ์ญ์
ํ์ผ ์ญ์
๋ณต์ฌ
class FileModel extends BaseModelClass {
@api({ httpMethod: "DELETE" })
async remove(fileId: number): Promise<void> {
const rdb = this.getPuri("r");
// ํ์ผ ์ ๋ณด ์กฐํ
const file = await rdb
.table("files")
.where("id", fileId)
.first();
if (!file) {
throw new Error("File not found");
}
// Storage์์ ์ญ์
const disk = Sonamu.storage.use(file.disk_name);
await disk.delete(file.key);
// DB์์ ์ญ์
const wdb = this.getPuri("w");
await wdb.table("files").where("id", fileId).delete();
}
}
ํ์ผ ์กด์ฌ ํ์ธ ํ ์ญ์
๋ณต์ฌ
class FileModel extends BaseModelClass {
@api({ httpMethod: "DELETE" })
async removeSafe(fileId: number): Promise<void> {
const rdb = this.getPuri("r");
const file = await rdb
.table("files")
.where("id", fileId)
.first();
if (!file) {
throw new Error("File not found");
}
const disk = Sonamu.storage.use(file.disk_name);
// ํ์ผ ์กด์ฌ ํ์ธ
const exists = await disk.exists(file.key);
if (exists) {
await disk.delete(file.key);
} else {
console.warn(`File not found in storage: ${file.key}`);
}
// DB์์ ์ญ์
const wdb = this.getPuri("w");
await wdb.table("files").where("id", fileId).delete();
}
}
๊ณ ๊ธ ์ฌ์ฉ
์คํธ๋ฆฌ๋ฐ ์ ๋ก๋ (๋์ฉ๋ ํ์ผ)
๋์ฉ๋ ํ์ผ์ ๊ฒฝ์ฐ Buffer ๋์ ์คํธ๋ฆฌ๋ฐ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.๋ณต์ฌ
import { pipeline } from "stream/promises";
import fs from "fs";
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadLarge(params: {
file: UploadedFile;
}): Promise<{ url: string }> {
const { file } = params;
// ์์ ํ์ผ๋ก ์ ์ฅ
const tempPath = `/tmp/${Date.now()}.${file.extname}`;
const writeStream = fs.createWriteStream(tempPath);
await pipeline(file.raw.file, writeStream);
// ์์ ํ์ผ์ Storage์ ์
๋ก๋
const disk = Sonamu.storage.use();
const key = `uploads/${Date.now()}.${file.extname}`;
const buffer = await fs.promises.readFile(tempPath);
await disk.put(key, new Uint8Array(buffer), {
contentType: file.mimetype,
});
// ์์ ํ์ผ ์ญ์
await fs.promises.unlink(tempPath);
const url = await disk.getUrl(key);
return { url };
}
}
ํ์ผ ๋ฉํ๋ฐ์ดํฐ ์ ์ฅ
๋ณต์ฌ
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadWithMetadata(params: {
file: UploadedFile;
title?: string;
description?: string;
tags?: string[];
}): Promise<{ fileId: number; url: string }> {
const context = Sonamu.getContext();
if (!context.user) {
throw new Error("Authentication required");
}
const { file, title, description, tags } = params;
// MD5 ํด์ ๊ณ์ฐ
const md5Hash = await file.md5();
// ํ์ผ ์ ์ฅ
const key = `uploads/${Date.now()}.${file.extname}`;
const url = await file.saveToDisk(key);
// DB์ ๋ฉํ๋ฐ์ดํฐ ์ ์ฅ
const wdb = this.getPuri("w");
const [record] = await wdb
.table("files")
.insert({
user_id: context.user.id,
key,
filename: file.filename,
mime_type: file.mimetype,
size: file.size,
md5_hash: md5Hash,
url,
title,
description,
tags: tags ? JSON.stringify(tags) : null,
uploaded_at: new Date(),
})
.returning({ id: "id" });
return {
fileId: record.id,
url,
};
}
}
์ฃผ์์ฌํญ
ํ์ผ ์ ์ฅ ์ ์ฃผ์์ฌํญ:
saveToDisk()๊ฐ ๊ฐ์ฅ ๊ฐํธํ ๋ฐฉ๋ฒ- ๋์คํฌ ์ค์ ํ์ธ
- ํ์ผ ํฌ๊ธฐ ์ ํ ์ค์
- ์๋ฌ ์ฒ๋ฆฌ ํ์
- ๋์ฉ๋ ํ์ผ์ ์คํธ๋ฆฌ๋ฐ ๊ณ ๋ ค