사이트 설정 백업 현황 추가
This commit is contained in:
176
server/utils/backup-storage-summary.js
Normal file
176
server/utils/backup-storage-summary.js
Normal file
@@ -0,0 +1,176 @@
|
||||
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'
|
||||
}
|
||||
|
||||
/**
|
||||
* 업로드 상대 경로를 공개 URL로 변환한다.
|
||||
* @param {string} relativePath - 업로드 루트 기준 상대 경로
|
||||
* @returns {string} 공개 URL
|
||||
*/
|
||||
const toUploadUrl = (relativePath) => `/uploads/${relativePath.split(sep).map(encodeURIComponent).join('/')}`
|
||||
|
||||
/**
|
||||
* 큰 파일 목록에 파일을 추가하고 상위 5개만 유지한다.
|
||||
* @param {Array<Object>} largestFiles - 큰 파일 목록
|
||||
* @param {Object} file - 파일 정보
|
||||
* @returns {void}
|
||||
*/
|
||||
const addLargestFile = (largestFiles, file) => {
|
||||
largestFiles.push(file)
|
||||
largestFiles.sort((a, b) => b.sizeBytes - a.sizeBytes)
|
||||
|
||||
if (largestFiles.length > 5) {
|
||||
largestFiles.length = 5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 업로드 파일을 분류 집계에 반영한다.
|
||||
* @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 publicUrl = toUploadUrl(relativePath)
|
||||
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)
|
||||
}
|
||||
|
||||
addLargestFile(summary.largestFiles, {
|
||||
name: segments.at(-1) || relativePath,
|
||||
url: publicUrl,
|
||||
kind,
|
||||
sizeBytes
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 업로드 디렉터리를 재귀적으로 스캔한다.
|
||||
* @param {string} directory - 현재 디렉터리
|
||||
* @param {Object} summary - 업로드 집계
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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<Object>} 업로드 파일 집계
|
||||
*/
|
||||
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()
|
||||
},
|
||||
largestFiles: []
|
||||
}
|
||||
|
||||
await scanUploadDirectory(uploadRoot, summary)
|
||||
|
||||
return summary
|
||||
}
|
||||
Reference in New Issue
Block a user