// lib/file-storage.ts - File과 ArrayBuffer를 위한 분리된 함수들 import { promises as fs } from "fs"; import path from "path"; import crypto from "crypto"; interface FileStorageConfig { baseDir: string; publicUrl: string; isProduction: boolean; } // 파일명 해시 생성 유틸리티 export function generateHashedFileName(originalName: string): string { const fileExtension = path.extname(originalName); const fileNameWithoutExt = path.basename(originalName, fileExtension); const timestamp = Date.now(); const randomHash = crypto.createHash('md5') .update(`${fileNameWithoutExt}-${timestamp}-${Math.random()}`) .digest('hex') .substring(0, 8); return `${timestamp}-${randomHash}${fileExtension}`; } // ✅ File 저장용 인터페이스 interface SaveFileOptions { file: File; directory: string; originalName?: string; } // ✅ Buffer/ArrayBuffer 저장용 인터페이스 interface SaveBufferOptions { buffer: Buffer | ArrayBuffer; fileName: string; directory: string; originalName?: string; } interface SaveFileResult { success: boolean; filePath?: string; publicPath?: string; fileName?: string; error?: string; } const nasPath = process.env.NAS_PATH || "/evcp_nas" // 환경별 설정 function getStorageConfig(): FileStorageConfig { const isProduction = process.env.NODE_ENV === "production"; if (isProduction) { return { baseDir: nasPath, publicUrl: "/api/files", isProduction: true, }; } else { return { baseDir: path.join(process.cwd(), "public"), publicUrl: "", isProduction: false, }; } } // ✅ 1. File 객체 저장 함수 (기존 방식) export async function saveFile({ file, directory, originalName }: SaveFileOptions): Promise { try { const config = getStorageConfig(); const finalFileName = originalName || file.name; const hashedFileName = generateHashedFileName(finalFileName); // 저장 경로 설정 const saveDir = path.join(config.baseDir, directory); const filePath = path.join(saveDir, hashedFileName); // 웹 접근 경로 let publicPath: string; if (config.isProduction) { publicPath = `${config.publicUrl}/${directory}/${hashedFileName}`; } else { publicPath = `/${directory}/${hashedFileName}`; } console.log(`📄 File 객체 저장: ${finalFileName}`); console.log(`📁 저장 위치: ${filePath}`); console.log(`🌐 웹 접근 경로: ${publicPath}`); // 디렉토리 생성 await fs.mkdir(saveDir, { recursive: true }); // File 객체에서 데이터 추출 const arrayBuffer = await file.arrayBuffer(); const dataBuffer = Buffer.from(arrayBuffer); // 파일 저장 await fs.writeFile(filePath, dataBuffer); console.log(`✅ File 저장 완료: ${hashedFileName} (${dataBuffer.length} bytes)`); return { success: true, filePath, publicPath, fileName: hashedFileName, }; } catch (error) { console.error("File 저장 실패:", error); return { success: false, error: error instanceof Error ? error.message : "File 저장 중 오류가 발생했습니다.", }; } } // ✅ 2. Buffer/ArrayBuffer 저장 함수 (DRM 복호화용) export async function saveBuffer({ buffer, fileName, directory, originalName }: SaveBufferOptions): Promise { try { const config = getStorageConfig(); const finalFileName = originalName || fileName; const hashedFileName = generateHashedFileName(finalFileName); // 저장 경로 설정 const saveDir = path.join(config.baseDir, directory); const filePath = path.join(saveDir, hashedFileName); // 웹 접근 경로 let publicPath: string; if (config.isProduction) { publicPath = `${config.publicUrl}/${directory}/${hashedFileName}`; } else { publicPath = `/${directory}/${hashedFileName}`; } console.log(`🔓 Buffer/ArrayBuffer 저장: ${finalFileName}`); console.log(`📁 저장 위치: ${filePath}`); console.log(`🌐 웹 접근 경로: ${publicPath}`); // 디렉토리 생성 await fs.mkdir(saveDir, { recursive: true }); // Buffer 준비 const dataBuffer = buffer instanceof ArrayBuffer ? Buffer.from(buffer) : buffer; // 파일 저장 await fs.writeFile(filePath, dataBuffer); console.log(`✅ Buffer 저장 완료: ${hashedFileName} (${dataBuffer.length} bytes)`); return { success: true, filePath, publicPath, fileName: hashedFileName, }; } catch (error) { console.error("Buffer 저장 실패:", error); return { success: false, error: error instanceof Error ? error.message : "Buffer 저장 중 오류가 발생했습니다.", }; } } // ✅ 업데이트 함수들 export async function updateFile( options: SaveFileOptions, oldFilePath?: string ): Promise { try { const result = await saveFile(options); if (result.success && oldFilePath) { await deleteFile(oldFilePath); } return result; } catch (error) { console.error("File 업데이트 실패:", error); return { success: false, error: error instanceof Error ? error.message : "File 업데이트 중 오류가 발생했습니다.", }; } } export async function updateBuffer( options: SaveBufferOptions, oldFilePath?: string ): Promise { try { const result = await saveBuffer(options); if (result.success && oldFilePath) { await deleteFile(oldFilePath); } return result; } catch (error) { console.error("Buffer 업데이트 실패:", error); return { success: false, error: error instanceof Error ? error.message : "Buffer 업데이트 중 오류가 발생했습니다.", }; } } // 파일 삭제 함수 export async function deleteFile(publicPath: string): Promise { try { const config = getStorageConfig(); let absolutePath: string; if (config.isProduction) { const relativePath = publicPath.replace('/api/files/', ''); absolutePath = path.join(nasPath, relativePath); } else { absolutePath = path.join(process.cwd(), 'public', publicPath); } console.log(`🗑️ 파일 삭제: ${absolutePath}`); await fs.access(absolutePath); await fs.unlink(absolutePath); return true; } catch (error) { console.log("파일 삭제 실패 또는 파일이 없음:", error); return false; } } // ✅ 편의 함수들 (하위 호환성) export const save = { file: saveFile, buffer: saveBuffer, }; // ✅ DRM 워크플로우 통합 함수 export async function saveDRMFile( originalFile: File, decryptFunction: (file: File) => Promise, directory: string ): Promise { try { console.log(`🔐 DRM 파일 처리 시작: ${originalFile.name}`); // 1. DRM 복호화 const decryptedData = await decryptFunction(originalFile); // 2. 복호화된 데이터 저장 const result = await saveBuffer({ buffer: decryptedData, fileName: originalFile.name, directory }); if (result.success) { console.log(`✅ DRM 파일 처리 완료: ${originalFile.name}`); } return result; } catch (error) { console.error(`❌ DRM 파일 처리 실패: ${originalFile.name}`, error); return { success: false, error: error instanceof Error ? error.message : "DRM 파일 처리 중 오류가 발생했습니다.", }; } }