1import fs from 'fs'; 2import path from 'path'; 3import { Stream } from 'stream'; 4import temporary from 'tempy'; 5import { promisify } from 'util'; 6 7import { createCachedFetch, fetchAsync } from '../api/rest/client'; 8import { FetchLike, ProgressCallback } from '../api/rest/client.types'; 9import { wrapFetchWithProgress } from '../api/rest/wrapFetchWithProgress'; 10import { ensureDirectoryAsync } from './dir'; 11import { CommandError } from './errors'; 12import { extractAsync } from './tar'; 13 14const debug = require('debug')('expo:utils:downloadAppAsync') as typeof console.log; 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 = fetchAsync; 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 debug(`Downloading ${url} to ${outputPath}`); 43 const res = await wrapFetchWithProgress(fetchInstance)(url, { 44 timeout: TIMER_DURATION, 45 onProgress, 46 }); 47 if (!res.ok) { 48 throw new CommandError( 49 'FILE_DOWNLOAD', 50 `Unexpected response: ${res.statusText}. From url: ${url}` 51 ); 52 } 53 return pipeline(res.body, fs.createWriteStream(outputPath)); 54} 55 56export async function downloadAppAsync({ 57 url, 58 outputPath, 59 extract = false, 60 cacheDirectory, 61 onProgress, 62}: { 63 url: string; 64 outputPath: string; 65 extract?: boolean; 66 cacheDirectory?: string; 67 onProgress?: ProgressCallback; 68}): Promise<void> { 69 if (extract) { 70 // For iOS we download the ipa to a file then pass that file into the extractor. 71 // In the future we should just pipe the `res.body -> tar.extract` directly. 72 // I tried this and it created some weird errors where observing the data stream 73 // would corrupt the file causing tar to fail with `TAR_BAD_ARCHIVE`. 74 const tmpPath = temporary.file({ name: path.basename(outputPath) }); 75 await downloadAsync({ url, outputPath: tmpPath, cacheDirectory, onProgress }); 76 debug(`Extracting ${tmpPath} to ${outputPath}`); 77 await ensureDirectoryAsync(outputPath); 78 await extractAsync(tmpPath, outputPath); 79 } else { 80 await ensureDirectoryAsync(path.dirname(outputPath)); 81 await downloadAsync({ url, outputPath, cacheDirectory, onProgress }); 82 } 83} 84