|
| 1 | +import { createReadStream } from 'fs' |
| 2 | +import tar from 'tar' |
| 3 | +import { ensureDir } from 'fs-extra' |
| 4 | +import path from 'path' |
| 5 | +import writeFileAtomic from 'write-file-atomic' |
| 6 | + |
| 7 | +export const extractAtomic = async (archivePath: string, destinationPath: string) => { |
| 8 | + const entryPromises: Promise<void>[] = [] |
| 9 | + |
| 10 | + const parser = new tar.Parse() |
| 11 | + |
| 12 | + parser.on('entry', (entry) => { |
| 13 | + if (entry.type !== 'File') { |
| 14 | + entry.resume() // skip non-files |
| 15 | + |
| 16 | + return |
| 17 | + } |
| 18 | + |
| 19 | + const targetPath = path.join(destinationPath, entry.path) |
| 20 | + |
| 21 | + const p = (async () => { |
| 22 | + await ensureDir(path.dirname(targetPath)) |
| 23 | + |
| 24 | + const chunks: Buffer[] = [] |
| 25 | + |
| 26 | + for await (const chunk of entry) { |
| 27 | + chunks.push(chunk) |
| 28 | + } |
| 29 | + |
| 30 | + const content = Buffer.concat(chunks) |
| 31 | + |
| 32 | + await writeFileAtomic(targetPath, content, { |
| 33 | + mode: entry.mode || 0o644, |
| 34 | + }) |
| 35 | + })() |
| 36 | + |
| 37 | + entryPromises.push(p) |
| 38 | + }) |
| 39 | + |
| 40 | + // Pipe archive into parser |
| 41 | + const stream = createReadStream(archivePath) |
| 42 | + |
| 43 | + stream.pipe(parser) |
| 44 | + |
| 45 | + // Wait for parser to finish and all entry writes to complete |
| 46 | + await new Promise<void>((resolve, reject) => { |
| 47 | + parser.on('end', resolve) |
| 48 | + // Parser extends NodeJS.ReadWriteStream (EventEmitter), so it supports 'error' events |
| 49 | + // even though the types don't explicitly declare it |
| 50 | + |
| 51 | + ;(parser as NodeJS.ReadWriteStream).on('error', reject) |
| 52 | + stream.on('error', reject) |
| 53 | + }) |
| 54 | + |
| 55 | + await Promise.all(entryPromises) |
| 56 | +} |
0 commit comments