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