Files
sori.studio/server/utils/zip-writer.js
zenn de767095ba v1.10.88 품질·보안 개선과 미디어 카탈로그 database 전환을 반영한다.
세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 11:05:40 +09:00

96 lines
2.4 KiB
JavaScript

import { createWriteStream } from 'node:fs'
import { rm } from 'node:fs/promises'
import { ZipArchive } from 'archiver'
/**
* ZIP 엔트리 경로를 안전하게 정리한다.
* @param {string} pathValue - 엔트리 경로
* @returns {string} 정리된 경로
*/
const normalizeZipPath = (pathValue) => String(pathValue || '')
.replace(/\\/g, '/')
.replace(/^\/+/g, '')
.split('/')
.filter((part) => part && part !== '.' && part !== '..')
.join('/')
/**
* ZIP 파일을 디스크로 스트리밍 생성한다.
* 이미 압축된 이미지·영상·첨부 파일은 원본 바이트를 유지한 채 store 방식으로 담는다.
* @param {string} outputPath - 생성할 ZIP 절대 경로
* @param {Array<{ path: string, data?: Buffer|string, sourcePath?: string, store?: boolean }>} files - ZIP 엔트리
* @returns {Promise<void>}
*/
export const writeZipFile = async (outputPath, files) => {
await new Promise((resolve, reject) => {
const output = createWriteStream(outputPath, {
flags: 'wx'
})
const archive = new ZipArchive({
zlib: {
level: 6
},
forceZip64: true
})
let settled = false
/**
* ZIP 스트림을 한 번만 종료한다.
* @param {Error|null} error - 실패 오류
* @returns {void}
*/
const finish = (error = null) => {
if (settled) {
return
}
settled = true
if (error) {
reject(error)
return
}
resolve()
}
output.on('close', () => finish())
output.on('error', (error) => finish(error))
archive.on('error', (error) => finish(error))
archive.on('warning', (error) => {
if (error?.code !== 'ENOENT') {
finish(error)
}
})
archive.pipe(output)
for (const file of files) {
const path = normalizeZipPath(file.path)
if (!path) {
continue
}
if (file.sourcePath) {
archive.file(file.sourcePath, {
name: path,
store: file.store !== false
})
continue
}
const data = Buffer.isBuffer(file.data)
? file.data
: Buffer.from(String(file.data || ''), 'utf8')
archive.append(data, {
name: path,
store: Boolean(file.store)
})
}
archive.finalize().catch((error) => finish(error))
}).catch(async (error) => {
await rm(outputPath, {
force: true
})
throw error
})
}