Files
uniare.sori.studio/api/src/services/previewImage.js

112 lines
3.4 KiB
JavaScript

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* 미리보기 이미지 저장 디렉터리
* @returns {string}
*/
export function getPreviewsDir() {
const dir = process.env.PREVIEWS_PATH || path.join(__dirname, '..', '..', 'data', 'previews');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
return dir;
}
/**
* base64 data URL을 PNG 파일로 저장
* @param {string} shareCode
* @param {string} dataUrl
* @returns {string} 파일명
*/
export function savePreviewImage(shareCode, dataUrl) {
if (!dataUrl || !dataUrl.startsWith('data:image')) {
return null;
}
const base64 = dataUrl.replace(/^data:image\/\w+;base64,/, '');
const fileName = `${shareCode}.png`;
fs.writeFileSync(path.join(getPreviewsDir(), fileName), Buffer.from(base64, 'base64'));
return fileName;
}
/**
* @param {string} shareCode
* @returns {string|null}
*/
export function getPreviewFilePath(shareCode) {
const filePath = path.join(getPreviewsDir(), `${shareCode}.png`);
return fs.existsSync(filePath) ? filePath : null;
}
/**
* @param {string} shareCode
* @param {string|null} [previewImageRecord] DB preview_image 컬럼
* @returns {string|null}
*/
export function getPreviewUrl(shareCode, previewImageRecord = null) {
const hasFile = Boolean(getPreviewFilePath(shareCode));
if (hasFile || previewImageRecord) {
return `/api/v1/decks/${shareCode}/preview`;
}
return null;
}
/**
* 공유 코드에 연결된 미리보기 파일 삭제
* @param {string} shareCode
* @returns {boolean}
*/
export function deletePreviewImage(shareCode) {
const filePath = path.join(getPreviewsDir(), `${shareCode}.png`);
if (!fs.existsSync(filePath)) return false;
try {
fs.unlinkSync(filePath);
return true;
} catch (err) {
console.warn(`[preview] 삭제 실패: ${filePath}`, err);
return false;
}
}
/**
* DB에서 참조하지 않는 미리보기 파일 목록
* @param {Iterable<string>} referencedShareCodes
* @returns {Array<{fileName: string, size: number, modifiedAt: string}>}
*/
export function listOrphanPreviewImages(referencedShareCodes) {
const referenced = new Set([...referencedShareCodes].map((code) => `${code}.png`));
return fs
.readdirSync(getPreviewsDir(), { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.png'))
.filter((entry) => !referenced.has(entry.name))
.map((entry) => {
const stat = fs.statSync(path.join(getPreviewsDir(), entry.name));
return {
fileName: entry.name,
size: stat.size,
modifiedAt: stat.mtime.toISOString(),
};
})
.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
}
/**
* 미참조 미리보기 파일 일괄 삭제
* @param {Iterable<string>} referencedShareCodes
* @returns {{deletedCount: number, deletedBytes: number}}
*/
export function deleteOrphanPreviewImages(referencedShareCodes) {
const orphans = listOrphanPreviewImages(referencedShareCodes);
let deletedBytes = 0;
orphans.forEach((file) => {
fs.unlinkSync(path.join(getPreviewsDir(), file.fileName));
deletedBytes += file.size;
});
return { deletedCount: orphans.length, deletedBytes };
}