세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
96 lines
2.4 KiB
JavaScript
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
|
|
})
|
|
}
|