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 { ensureDirectoryAsync } from './dir'; 10import { CommandError } from './errors'; 11import { extractAsync } from './tar'; 12 13const debug = require('debug')('expo:utils:downloadAppAsync') as typeof console.log; 14 15const TIMER_DURATION = 30000; 16 17const pipeline = promisify(Stream.pipeline); 18 19async function downloadAsync({ 20 url, 21 outputPath, 22 cacheDirectory, 23 onProgress, 24}: { 25 url: string; 26 outputPath: string; 27 cacheDirectory?: string; 28 onProgress?: ProgressCallback; 29}) { 30 let fetchInstance: FetchLike = fetchAsync; 31 if (cacheDirectory) { 32 // Reconstruct the cached fetch since caching could be disabled. 33 fetchInstance = createCachedFetch({ 34 // We'll use a 1 week cache for versions so older values get flushed out eventually. 35 ttl: 1000 * 60 * 60 * 24 * 7, 36 // Users can also nuke their `~/.expo` directory to clear the cache. 37 cacheDirectory, 38 }); 39 } 40 41 debug(`Downloading ${url} to ${outputPath}`); 42 const res = await 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 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