Sonamu์ Storage Manager๋ฅผ ์ฌ์ฉํ์ฌ ๋ก์ปฌ, S3, GCS ๋ฑ์ ํ์ผ์ ์ ์ฅํ๋ ๋ฐฉ๋ฒ์ ์์๋ด
๋๋ค.
Storage Manager ๊ฐ์
ํตํฉ ์ธํฐํ์ด์ค
๋จ์ผ API ์ฌ๋ฌ ์คํ ๋ฆฌ์ง ์ง์
๋๋ผ์ด๋ฒ ์์คํ
local, s3, gcs ํ์ฅ ๊ฐ๋ฅ
saveToDisk()
๊ฐํธํ ์ ์ฅ URL ์๋ ์์ฑ
๋์คํฌ ๊ด๋ฆฌ
์ฌ๋ฌ ๋์คํฌ ์ค์ ๋ชฉ์ ๋ณ ๋ถ๋ฆฌ
saveToDisk() ๋ฉ์๋ (๊ถ์ฅ)
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
@upload() ๋ฐ์ฝ๋ ์ดํฐ๋ก ์
๋ก๋๋ ํ์ผ์ Sonamu.getContext().bufferedFiles๋ฅผ ํตํด ์ ๊ทผํฉ๋๋ค. BufferedFile์ saveToDisk() ๋ฉ์๋๊ฐ ๊ฐ์ฅ ๊ฐํธํ ํ์ผ ์ ์ฅ ๋ฐฉ๋ฒ์
๋๋ค.
class FileModel extends BaseModelClass {
@upload()
async upload(): Promise<{ url: string }> {
const { bufferedFiles } = Sonamu.getContext();
const file = bufferedFiles?.[0];
if (!file) throw new Error("ํ์ผ์ด ํ์ํฉ๋๋ค");
// ํ์ผ ์ ์ฅ (๊ธฐ๋ณธ ๋์คํฌ)
const url = await file.saveToDisk("fs", `uploads/${Date.now()}-${file.filename}`);
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("ํ์ผ์ด ํ์ํฉ๋๋ค");
// S3 ๋์คํฌ์ ์ ์ฅ
const url = await file.saveToDisk("s3", `uploads/${Date.now()}-${file.filename}`);
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() - ํ์ผ ์ญ์
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 {
@upload()
async upload(): Promise<{ url: string }> {
const { bufferedFiles } = Sonamu.getContext();
const file = bufferedFiles?.[0];
if (!file) throw new Error("ํ์ผ์ด ํ์ํฉ๋๋ค");
// ๋ ์ง๋ณ ๊ฒฝ๋ก ์์ฑ
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("fs", key);
return { url };
}
}
์ฌ์ฉ์๋ณ ํด๋ ๊ตฌ์กฐ
class FileModel extends BaseModelClass {
@upload()
async upload(): Promise<{ url: string }> {
const context = Sonamu.getContext();
if (!context.user) {
throw new Error("Authentication required");
}
const { bufferedFiles } = context;
const file = bufferedFiles?.[0];
if (!file) throw new Error("ํ์ผ์ด ํ์ํฉ๋๋ค");
// ์ฌ์ฉ์๋ณ ๊ฒฝ๋ก
const key = `users/${context.user.id}/${Date.now()}.${file.extname}`;
const url = await file.saveToDisk("fs", key);
return { url };
}
}
์นดํ
๊ณ ๋ฆฌ๋ณ ๋์คํฌ
class MediaModel extends BaseModelClass {
@upload()
async uploadMedia(params: { category: "public" | "private" }): Promise<{ url: string }> {
const { bufferedFiles } = Sonamu.getContext();
const file = bufferedFiles?.[0];
if (!file) throw new Error("ํ์ผ์ด ํ์ํฉ๋๋ค");
const { category } = params;
// ์นดํ
๊ณ ๋ฆฌ๋ณ ๋์คํฌ ์ ํ
const diskName = category === "public" ? "public" : "private";
const key = `media/${Date.now()}.${file.extname}`;
const url = await file.saveToDisk(diskName, key);
return { url };
}
}
์ด๋ฏธ์ง ์ต์ ํ ํ ์ ์ฅ
import sharp from "sharp";
class ImageModel extends BaseModelClass {
@upload()
async uploadOptimized(): Promise<{
originalUrl: string;
thumbnailUrl: string;
mediumUrl: string;
}> {
const { bufferedFiles } = Sonamu.getContext();
const image = bufferedFiles?.[0];
if (!image) throw new Error("์ด๋ฏธ์ง๊ฐ ํ์ํฉ๋๋ค");
// ์๋ณธ ์ด๋ฏธ์ง Buffer
const buffer = image.buffer;
const timestamp = Date.now();
const ext = image.extname;
// ์๋ณธ ์ ์ฅ
const originalKey = `images/original/${timestamp}.${ext}`;
const originalUrl = await image.saveToDisk("fs", 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 {
@upload()
async uploadWithBackup(): Promise<{
primaryUrl: string;
backupUrl: string;
}> {
const { bufferedFiles } = Sonamu.getContext();
const file = bufferedFiles?.[0];
if (!file) throw new Error("ํ์ผ์ด ํ์ํฉ๋๋ค");
const key = `uploads/${Date.now()}.${file.extname}`;
const buffer = file.buffer;
// Primary ๋์คํฌ์ ์ ์ฅ
const primaryUrl = await file.saveToDisk("s3", key);
// 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();
}
}
๊ณ ๊ธ ์ฌ์ฉ
์คํธ๋ฆฌ๋ฐ ์
๋ก๋ (๋์ฉ๋ ํ์ผ)
๋์ฉ๋ ํ์ผ์ ๊ฒฝ์ฐ @upload({ consume: "stream" }) ์ต์
์ ์ฌ์ฉํ๋ฉด ํ์ผ์ด ์ง์ ๋ ์คํ ๋ฆฌ์ง์ ์ง์ ์คํธ๋ฆฌ๋ฐ ์ ์ฅ๋ฉ๋๋ค. ์ด ๊ฒฝ์ฐ context.uploadedFiles๋ฅผ ํตํด ์ด๋ฏธ ์
๋ก๋๋ ํ์ผ ์ ๋ณด์ ์ ๊ทผํฉ๋๋ค.
class FileModel extends BaseModelClass {
@upload({ consume: "stream", destination: "s3" })
async uploadLarge(): Promise<{ url: string }> {
const { uploadedFiles } = Sonamu.getContext();
const file = uploadedFiles?.[0];
if (!file) throw new Error("ํ์ผ์ด ํ์ํฉ๋๋ค");
// stream ๋ชจ๋์์๋ ํ์ผ์ด ์ด๋ฏธ destination์ ์ ์ฅ๋์ด ์์
return { url: file.url };
}
}
ํ์ผ ๋ฉํ๋ฐ์ดํฐ ์ ์ฅ
class FileModel extends BaseModelClass {
@upload()
async uploadWithMetadata(params: {
title?: string;
description?: string;
tags?: string[];
}): Promise<{ fileId: number; url: string }> {
const context = Sonamu.getContext();
if (!context.user) {
throw new Error("Authentication required");
}
const { bufferedFiles } = context;
const file = bufferedFiles?.[0];
if (!file) throw new Error("ํ์ผ์ด ํ์ํฉ๋๋ค");
const { title, description, tags } = params;
// ํ์ผ ์ ์ฅ
const key = `uploads/${Date.now()}-${file.filename}`;
const url = await file.saveToDisk("fs", 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,
url,
title,
description,
tags: tags ? JSON.stringify(tags) : null,
uploaded_at: new Date(),
})
.returning({ id: "id" });
return {
fileId: record.id,
url,
};
}
}
์ฃผ์์ฌํญ
ํ์ผ ์ ์ฅ ์ ์ฃผ์์ฌํญ: 1. saveToDisk()๊ฐ ๊ฐ์ฅ ๊ฐํธํ ๋ฐฉ๋ฒ 2. ๋์คํฌ ์ค์ ํ์ธ 3. ํ์ผ ํฌ๊ธฐ
์ ํ ์ค์ 4. ์๋ฌ ์ฒ๋ฆฌ ํ์ 5. ๋์ฉ๋ ํ์ผ์ ์คํธ๋ฆฌ๋ฐ ๊ณ ๋ ค
๋ค์ ๋จ๊ณ
ํ์ผ ์
๋ก๋ ์ค์
@upload ๋ฐ์ฝ๋ ์ดํฐ
UploadedFile ํด๋์ค
ํ์ผ ์ ๋ณด ์ ๊ทผ
URL ์์ฑ
URL ์์ฑํ๊ธฐ
@api ๋ฐ์ฝ๋ ์ดํฐ
API ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ