fileUtil.ts (4756B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {ChildProcessByStdio} from 'node:child_process'; 8 import {spawnSync, spawn} from 'node:child_process'; 9 import {createReadStream} from 'node:fs'; 10 import {mkdir, readdir} from 'node:fs/promises'; 11 import * as path from 'node:path'; 12 import type {Readable, Transform, Writable} from 'node:stream'; 13 import {Stream} from 'node:stream'; 14 15 import debug from 'debug'; 16 17 const debugFileUtil = debug('puppeteer:browsers:fileUtil'); 18 19 /** 20 * @internal 21 */ 22 export async function unpackArchive( 23 archivePath: string, 24 folderPath: string, 25 ): Promise<void> { 26 if (!path.isAbsolute(folderPath)) { 27 folderPath = path.resolve(process.cwd(), folderPath); 28 } 29 if (archivePath.endsWith('.zip')) { 30 const extractZip = await import('extract-zip'); 31 await extractZip.default(archivePath, {dir: folderPath}); 32 } else if (archivePath.endsWith('.tar.bz2')) { 33 await extractTar(archivePath, folderPath, 'bzip2'); 34 } else if (archivePath.endsWith('.dmg')) { 35 await mkdir(folderPath); 36 await installDMG(archivePath, folderPath); 37 } else if (archivePath.endsWith('.exe')) { 38 // Firefox on Windows. 39 const result = spawnSync(archivePath, [`/ExtractDir=${folderPath}`], { 40 env: { 41 __compat_layer: 'RunAsInvoker', 42 }, 43 }); 44 if (result.status !== 0) { 45 throw new Error( 46 `Failed to extract ${archivePath} to ${folderPath}: ${result.output}`, 47 ); 48 } 49 } else if (archivePath.endsWith('.tar.xz')) { 50 await extractTar(archivePath, folderPath, 'xz'); 51 } else { 52 throw new Error(`Unsupported archive format: ${archivePath}`); 53 } 54 } 55 56 function createTransformStream( 57 child: ChildProcessByStdio<Writable, Readable, null>, 58 ): Transform { 59 const stream = new Stream.Transform({ 60 transform(chunk, encoding, callback) { 61 if (!child.stdin.write(chunk, encoding)) { 62 child.stdin.once('drain', callback); 63 } else { 64 callback(); 65 } 66 }, 67 68 flush(callback) { 69 if (child.stdout.destroyed) { 70 callback(); 71 } else { 72 child.stdin.end(); 73 child.stdout.on('close', callback); 74 } 75 }, 76 }); 77 78 child.stdin.on('error', e => { 79 if ('code' in e && e.code === 'EPIPE') { 80 // finished before reading the file finished (i.e. head) 81 stream.emit('end'); 82 } else { 83 stream.destroy(e); 84 } 85 }); 86 87 child.stdout 88 .on('data', data => { 89 return stream.push(data); 90 }) 91 .on('error', e => { 92 return stream.destroy(e); 93 }); 94 95 child.once('close', () => { 96 return stream.end(); 97 }); 98 99 return stream; 100 } 101 102 /** 103 * @internal 104 */ 105 export const internalConstantsForTesting = { 106 xz: 'xz', 107 bzip2: 'bzip2', 108 }; 109 110 /** 111 * @internal 112 */ 113 async function extractTar( 114 tarPath: string, 115 folderPath: string, 116 decompressUtilityName: keyof typeof internalConstantsForTesting, 117 ): Promise<void> { 118 const tarFs = await import('tar-fs'); 119 return await new Promise<void>((fulfill, reject) => { 120 function handleError(utilityName: string) { 121 return (error: Error) => { 122 if ('code' in error && error.code === 'ENOENT') { 123 error = new Error( 124 `\`${utilityName}\` utility is required to unpack this archive`, 125 { 126 cause: error, 127 }, 128 ); 129 } 130 reject(error); 131 }; 132 } 133 const unpack = spawn( 134 internalConstantsForTesting[decompressUtilityName], 135 ['-d'], 136 { 137 stdio: ['pipe', 'pipe', 'inherit'], 138 }, 139 ) 140 .once('error', handleError(decompressUtilityName)) 141 .once('exit', code => { 142 debugFileUtil(`${decompressUtilityName} exited, code=${code}`); 143 }); 144 145 const tar = tarFs.extract(folderPath); 146 tar.once('error', handleError('tar')); 147 tar.once('finish', fulfill); 148 createReadStream(tarPath).pipe(createTransformStream(unpack)).pipe(tar); 149 }); 150 } 151 152 /** 153 * @internal 154 */ 155 async function installDMG(dmgPath: string, folderPath: string): Promise<void> { 156 const {stdout} = spawnSync(`hdiutil`, [ 157 'attach', 158 '-nobrowse', 159 '-noautoopen', 160 dmgPath, 161 ]); 162 163 const volumes = stdout.toString('utf8').match(/\/Volumes\/(.*)/m); 164 if (!volumes) { 165 throw new Error(`Could not find volume path in ${stdout}`); 166 } 167 const mountPath = volumes[0]!; 168 169 try { 170 const fileNames = await readdir(mountPath); 171 const appName = fileNames.find(item => { 172 return typeof item === 'string' && item.endsWith('.app'); 173 }); 174 if (!appName) { 175 throw new Error(`Cannot find app in ${mountPath}`); 176 } 177 const mountedPath = path.join(mountPath!, appName); 178 179 spawnSync('cp', ['-R', mountedPath, folderPath]); 180 } finally { 181 spawnSync('hdiutil', ['detach', mountPath, '-quiet']); 182 } 183 }