diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-25 07:51:15 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-25 07:51:15 +0000 |
| commit | 2650b7c0bb0ea12b68a58c0439f72d61df04b2f1 (patch) | |
| tree | 17156183fd74b69d78178065388ac61a18ac07b4 /lib/file-stroage.ts | |
| parent | d32acea05915bd6c1ed4b95e56c41ef9204347bc (diff) | |
(대표님) 정기평가 대상, 미들웨어 수정, nextauth 토큰 처리 개선, GTC 등
(최겸) 기술영업
Diffstat (limited to 'lib/file-stroage.ts')
| -rw-r--r-- | lib/file-stroage.ts | 107 |
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 |
