1import fs from 'fs'; 2import fetch from 'node-fetch'; 3import path from 'path'; 4import { Stream } from 'stream'; 5import temporary from 'tempy'; 6import { promisify } from 'util'; 7 8import { createCachedFetch } from '../api/rest/client'; 9import { FetchLike, ProgressCallback } from '../api/rest/client.types'; 10import { wrapFetchWithProgress } from '../api/rest/wrapFetchWithProgress'; 11import { Log } from '../log'; 12import { ensureDirectoryAsync } from './dir'; 13import { CommandError } from './errors'; 14import { extractAsync } from './tar'; 15 16const TIMER_DURATION = 30000; 17 18const pipeline = promisify(Stream.pipeline); 19 20async function downloadAsync({ 21 url, 22 outputPath, 23 cacheDirectory, 24 onProgress, 25}: { 26 url: string; 27 outputPath: string; 28 cacheDirectory?: string; 29 onProgress?: ProgressCallback; 30}) { 31 let fetchInstance: FetchLike = fetch; 32 if (cacheDirectory) { 33 // Reconstruct the cached fetch since caching could be disabled. 34 fetchInstance = createCachedFetch({ 35 // We'll use a 1 week cache for versions so older values get flushed out eventually. 36 ttl: 1000 * 60 * 60 * 24 * 7, 37 // Users can also nuke their `~/.expo` directory to clear the cache. 38 cacheDirectory, 39 }); 40 } 41 42 const res = await wrapFetchWithProgress(fetchInstance)(url, { 43 timeout: TIMER_DURATION, 44 onProgress, 45 }); 46 if (!res.ok) { 47 throw new CommandError( 48 'FILE_DOWNLOAD', 49 `Unexpected response: ${res.statusText}. From url: ${url}` 50 ); 51 } 52 return pipeline(res.body, fs.createWriteStream(outputPath)); 53} 54 55export async function downloadAppAsync({ 56 url, 57 outputPath, 58 extract = false, 59 cacheDirectory, 60 onProgress, 61}: { 62 url: string; 63 outputPath: string; 64 extract?: boolean; 65 cacheDirectory?: string; 66 onProgress?: ProgressCallback; 67}): Promise<void> { 68 if (extract) { 69 // For iOS we download the ipa to a file then pass that file into the extractor. 70 // In the future we should just pipe the `res.body -> tar.extract` directly. 71 // I tried this and it created some weird errors where observing the data stream 72 // would corrupt the file causing tar to fail with `TAR_BAD_ARCHIVE`. 73 const tmpPath = temporary.file({ name: path.basename(outputPath) }); 74 await downloadAsync({ url, outputPath: tmpPath, cacheDirectory, onProgress }); 75 Log.debug(`Extracting ${tmpPath} to ${outputPath}`); 76 await ensureDirectoryAsync(outputPath); 77 await extractAsync(tmpPath, outputPath); 78 } else { 79 await ensureDirectoryAsync(path.dirname(outputPath)); 80 await downloadAsync({ url, outputPath, cacheDirectory, onProgress }); 81 } 82} 83