게시물 Export ZIP Import 추가 v1.5.27
This commit is contained in:
123
server/utils/zip-reader.js
Normal file
123
server/utils/zip-reader.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import { inflateRawSync } from 'node:zlib'
|
||||
|
||||
/**
|
||||
* ZIP 엔트리 경로를 안전하게 정리한다.
|
||||
* @param {string} value - ZIP 내부 경로
|
||||
* @returns {string} 정리된 경로
|
||||
*/
|
||||
const normalizeZipEntryPath = (value) => String(value || '')
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\/+/g, '')
|
||||
.split('/')
|
||||
.filter((part) => part && part !== '.' && part !== '..')
|
||||
.join('/')
|
||||
|
||||
/**
|
||||
* ZIP 종료 레코드 위치를 찾는다.
|
||||
* @param {Buffer} buffer - ZIP 파일 버퍼
|
||||
* @returns {number} 종료 레코드 위치
|
||||
*/
|
||||
const findEndOfCentralDirectoryOffset = (buffer) => {
|
||||
const signature = 0x06054b50
|
||||
const minOffset = Math.max(0, buffer.length - 65557)
|
||||
|
||||
for (let offset = buffer.length - 22; offset >= minOffset; offset -= 1) {
|
||||
if (buffer.readUInt32LE(offset) === signature) {
|
||||
return offset
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* ZIP 엔트리 데이터를 압축 해제한다.
|
||||
* @param {Buffer} buffer - 전체 ZIP 버퍼
|
||||
* @param {Object} entry - 중앙 디렉터리 엔트리
|
||||
* @returns {Buffer} 압축 해제된 데이터
|
||||
*/
|
||||
const readZipEntryData = (buffer, entry) => {
|
||||
const localHeaderOffset = entry.localHeaderOffset
|
||||
|
||||
if (buffer.readUInt32LE(localHeaderOffset) !== 0x04034b50) {
|
||||
throw new Error('INVALID_ZIP_LOCAL_HEADER')
|
||||
}
|
||||
|
||||
const localNameLength = buffer.readUInt16LE(localHeaderOffset + 26)
|
||||
const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28)
|
||||
const dataOffset = localHeaderOffset + 30 + localNameLength + localExtraLength
|
||||
const compressedData = buffer.subarray(dataOffset, dataOffset + entry.compressedSize)
|
||||
|
||||
if (entry.compressionMethod === 0) {
|
||||
return Buffer.from(compressedData)
|
||||
}
|
||||
|
||||
if (entry.compressionMethod === 8) {
|
||||
return inflateRawSync(compressedData)
|
||||
}
|
||||
|
||||
throw new Error('UNSUPPORTED_ZIP_COMPRESSION')
|
||||
}
|
||||
|
||||
/**
|
||||
* ZIP 파일 버퍼를 파일 엔트리 목록으로 읽는다.
|
||||
* @param {Buffer} buffer - ZIP 파일 버퍼
|
||||
* @returns {Array<{ path: string, data: Buffer }>} 파일 엔트리 목록
|
||||
*/
|
||||
export const readZipBufferEntries = (buffer) => {
|
||||
if (!Buffer.isBuffer(buffer) || buffer.length < 22) {
|
||||
throw new Error('INVALID_ZIP_FILE')
|
||||
}
|
||||
|
||||
const endOffset = findEndOfCentralDirectoryOffset(buffer)
|
||||
|
||||
if (endOffset < 0) {
|
||||
throw new Error('INVALID_ZIP_FILE')
|
||||
}
|
||||
|
||||
const entryCount = buffer.readUInt16LE(endOffset + 10)
|
||||
const centralDirectorySize = buffer.readUInt32LE(endOffset + 12)
|
||||
const centralDirectoryOffset = buffer.readUInt32LE(endOffset + 16)
|
||||
const centralDirectoryEnd = centralDirectoryOffset + centralDirectorySize
|
||||
|
||||
if (centralDirectoryOffset < 0 || centralDirectoryEnd > buffer.length) {
|
||||
throw new Error('INVALID_ZIP_FILE')
|
||||
}
|
||||
|
||||
const entries = []
|
||||
let offset = centralDirectoryOffset
|
||||
|
||||
for (let index = 0; index < entryCount; index += 1) {
|
||||
if (buffer.readUInt32LE(offset) !== 0x02014b50) {
|
||||
throw new Error('INVALID_ZIP_CENTRAL_DIRECTORY')
|
||||
}
|
||||
|
||||
const compressionMethod = buffer.readUInt16LE(offset + 10)
|
||||
const compressedSize = buffer.readUInt32LE(offset + 20)
|
||||
const uncompressedSize = buffer.readUInt32LE(offset + 24)
|
||||
const nameLength = buffer.readUInt16LE(offset + 28)
|
||||
const extraLength = buffer.readUInt16LE(offset + 30)
|
||||
const commentLength = buffer.readUInt16LE(offset + 32)
|
||||
const localHeaderOffset = buffer.readUInt32LE(offset + 42)
|
||||
const rawPath = buffer.subarray(offset + 46, offset + 46 + nameLength).toString('utf8')
|
||||
const normalizedPath = normalizeZipEntryPath(rawPath)
|
||||
const isDirectory = rawPath.replace(/\\/g, '/').endsWith('/')
|
||||
|
||||
if (normalizedPath && !isDirectory && !normalizedPath.startsWith('__MACOSX/')) {
|
||||
entries.push({
|
||||
path: normalizedPath,
|
||||
compressionMethod,
|
||||
compressedSize,
|
||||
uncompressedSize,
|
||||
localHeaderOffset
|
||||
})
|
||||
}
|
||||
|
||||
offset += 46 + nameLength + extraLength + commentLength
|
||||
}
|
||||
|
||||
return entries.map((entry) => ({
|
||||
path: entry.path,
|
||||
data: readZipEntryData(buffer, entry)
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user