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