import { readdir, stat } from 'node:fs/promises' import { extname, join, relative, sep } from 'node:path' const uploadRoot = join(process.cwd(), 'public', 'uploads') const imageExtensions = new Set(['.avif', '.gif', '.jpg', '.jpeg', '.png', '.svg', '.webp']) const videoExtensions = new Set(['.mp4', '.mov', '.webm']) const audioExtensions = new Set(['.m4a', '.mp3', '.ogg', '.wav']) /** * 빈 파일 분류 집계를 만든다. * @returns {{ count: number, sizeBytes: number }} 파일 집계 */ const createFileBucket = () => ({ count: 0, sizeBytes: 0 }) /** * 파일 집계를 증가시킨다. * @param {{ count: number, sizeBytes: number }} bucket - 집계 대상 * @param {number} sizeBytes - 파일 크기 * @returns {void} */ const addFileBucket = (bucket, sizeBytes) => { bucket.count += 1 bucket.sizeBytes += sizeBytes } /** * 확장자 기준 파일 종류를 판단한다. * @param {string} filename - 파일명 * @returns {'images'|'videos'|'audio'|'files'} 파일 종류 */ const getFileKind = (filename) => { const extension = extname(filename).toLowerCase() if (imageExtensions.has(extension)) { return 'images' } if (videoExtensions.has(extension)) { return 'videos' } if (audioExtensions.has(extension)) { return 'audio' } return 'files' } /** * 업로드 파일을 분류 집계에 반영한다. * @param {Object} summary - 업로드 집계 * @param {string} relativePath - 업로드 루트 기준 상대 경로 * @param {number} sizeBytes - 파일 크기 * @returns {void} */ const addUploadFile = (summary, relativePath, sizeBytes) => { const segments = relativePath.split(sep) const firstSegment = segments[0] || '' const kind = getFileKind(relativePath) if (firstSegment === 'exports') { addFileBucket(summary.exports, sizeBytes) return } addFileBucket(summary.total, sizeBytes) addFileBucket(summary.byKind[kind], sizeBytes) if (segments.includes('thumbs')) { addFileBucket(summary.derivatives, sizeBytes) } else if (firstSegment === 'members') { addFileBucket(summary.profiles, sizeBytes) } else { addFileBucket(summary.originals, sizeBytes) } } /** * 업로드 디렉터리를 재귀적으로 스캔한다. * @param {string} directory - 현재 디렉터리 * @param {Object} summary - 업로드 집계 * @returns {Promise} */ const scanUploadDirectory = async (directory, summary) => { let entries = [] try { entries = await readdir(directory, { withFileTypes: true }) } catch (error) { if (error?.code === 'ENOENT') { return } throw error } for (const entry of entries) { const absolutePath = join(directory, entry.name) if (entry.isDirectory()) { await scanUploadDirectory(absolutePath, summary) continue } if (!entry.isFile()) { continue } const fileStat = await stat(absolutePath) const relativePath = relative(uploadRoot, absolutePath) if (!relativePath || relativePath.startsWith('..')) { continue } addUploadFile(summary, relativePath, fileStat.size) } } /** * 사이트 용량 점검용 업로드 파일 집계를 조회한다. * @returns {Promise} 업로드 파일 집계 */ export const getBackupStorageSummary = async () => { const summary = { total: createFileBucket(), originals: createFileBucket(), derivatives: createFileBucket(), profiles: createFileBucket(), exports: createFileBucket(), byKind: { images: createFileBucket(), videos: createFileBucket(), audio: createFileBucket(), files: createFileBucket() } } await scanUploadDirectory(uploadRoot, summary) return summary }