summaryrefslogtreecommitdiff
path: root/lib/file-stroage.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/file-stroage.ts')
-rw-r--r--lib/file-stroage.ts107
1 files changed, 105 insertions, 2 deletions
diff --git a/lib/file-stroage.ts b/lib/file-stroage.ts
index eab52364..c347ffe3 100644
--- a/lib/file-stroage.ts
+++ b/lib/file-stroage.ts
@@ -1,9 +1,10 @@
// lib/file-storage.ts - 보안이 강화된 파일 저장 유틸리티
-import { promises as fs } from "fs";
+import { promises as fs, createWriteStream } from "fs";
import path from "path";
import crypto from "crypto";
import { createHash } from "crypto";
+import { Readable } from 'stream'
interface FileStorageConfig {
baseDir: string;
@@ -45,7 +46,7 @@ const SECURITY_CONFIG = {
]),
// 최대 파일 크기 (100MB)
- MAX_FILE_SIZE: 100 * 1024 * 1024,
+ MAX_FILE_SIZE: 1024 * 1024 * 1024,
// 파일명 최대 길이
MAX_FILENAME_LENGTH: 255,
@@ -756,4 +757,106 @@ export function getSecurityConfig() {
maxFileSizeFormatted: FileUploadLogger['formatFileSize'](SECURITY_CONFIG.MAX_FILE_SIZE),
maxFilenameLength: SECURITY_CONFIG.MAX_FILENAME_LENGTH,
};
+}
+
+export async function saveFileStream({
+ file,
+ directory,
+ originalName,
+ userId,
+}: SaveFileOptions): Promise<SaveFileResult> {
+ const finalFileName = originalName || file.name
+
+ try {
+ console.log(`🚀 스트리밍 저장 시작: ${finalFileName}`)
+
+ // 기본 보안 검증들 (확장자, 파일명 등)
+ const extValidation = FileSecurityValidator.validateExtension(finalFileName)
+ if (!extValidation.valid) {
+ return { success: false, error: extValidation.error }
+ }
+
+ const nameValidation = FileSecurityValidator.validateFileName(finalFileName)
+ if (!nameValidation.valid) {
+ return { success: false, error: nameValidation.error }
+ }
+
+ const sizeValidation = FileSecurityValidator.validateFileSize(file.size)
+ if (!sizeValidation.valid) {
+ return { success: false, error: sizeValidation.error }
+ }
+
+ const config = getStorageConfig()
+ const safeOriginalName = sanitizeFileNameForStorage(finalFileName)
+ const hashedFileName = generateHashedFileName(safeOriginalName)
+
+ const saveDir = path.join(config.baseDir, directory)
+ const filePath = path.join(saveDir, hashedFileName)
+
+ // 디렉토리 생성
+ await fs.mkdir(saveDir, { recursive: true })
+
+ // Node.js 스트림으로 변환하여 저장
+ const nodeStream = Readable.fromWeb(file.stream() as ReadableStream)
+ const writeStream = createWriteStream(filePath)
+
+ // 스트림 파이프라인으로 메모리 효율적 저장
+ await new Promise((resolve, reject) => {
+ nodeStream.pipe(writeStream)
+ writeStream.on('finish', resolve)
+ writeStream.on('error', reject)
+ nodeStream.on('error', reject)
+ })
+
+ console.log(`✅ 스트리밍 저장 완료: ${finalFileName}`)
+
+ // 저장 후 첫 부분만 샘플링하여 내용 검증
+ const contentValidation = await validateLargeFileContentSample(filePath, finalFileName)
+ if (!contentValidation.valid) {
+ await fs.unlink(filePath) // 검증 실패 시 파일 삭제
+ return { success: false, error: contentValidation.error }
+ }
+
+ const publicPath = config.isProduction
+ ? `${config.publicUrl}/${directory}/${hashedFileName}`
+ : `/${directory}/${hashedFileName}`
+
+ return {
+ success: true,
+ filePath,
+ publicPath,
+ fileName: hashedFileName,
+ originalName: finalFileName,
+ fileSize: file.size,
+ }
+
+ } catch (error) {
+ console.error(`❌ 스트리밍 저장 실패: ${finalFileName}`, error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '스트리밍 저장 실패'
+ }
+ }
+}
+
+// 4. 대용량 파일 샘플 검증
+async function validateLargeFileContentSample(
+ filePath: string,
+ fileName: string
+): Promise<{ valid: boolean; error?: string }> {
+ try {
+ const fileHandle = await fs.open(filePath, 'r')
+
+ // 파일 시작 부분 64KB만 읽어서 검증
+ const sampleSize = Math.min(64 * 1024, (await fileHandle.stat()).size)
+ const buffer = Buffer.allocUnsafe(sampleSize)
+ const { bytesRead } = await fileHandle.read(buffer, 0, sampleSize, 0)
+ await fileHandle.close()
+
+ const sampleBuffer = buffer.subarray(0, bytesRead)
+ return await FileSecurityValidator.validateFileContent(sampleBuffer, fileName)
+ } catch (error) {
+ console.error('파일 샘플 검증 실패:', error)
+ return { valid: false, error: '파일 내용 검증 실패' }
+ }
} \ No newline at end of file