18d307f52SEvan Baconimport fs from 'fs';
28d307f52SEvan Baconimport path from 'path';
38d307f52SEvan Baconimport { Stream } from 'stream';
48d307f52SEvan Baconimport temporary from 'tempy';
58d307f52SEvan Baconimport { promisify } from 'util';
68d307f52SEvan Bacon
78d307f52SEvan Baconimport { ensureDirectoryAsync } from './dir';
88d307f52SEvan Baconimport { CommandError } from './errors';
98d307f52SEvan Baconimport { extractAsync } from './tar';
10*8a424bebSJames Ideimport { createCachedFetch, fetchAsync } from '../api/rest/client';
11*8a424bebSJames Ideimport { FetchLike, ProgressCallback } from '../api/rest/client.types';
128d307f52SEvan Bacon
13474a7a4bSEvan Baconconst debug = require('debug')('expo:utils:downloadAppAsync') as typeof console.log;
14474a7a4bSEvan Bacon
158d307f52SEvan Baconconst TIMER_DURATION = 30000;
168d307f52SEvan Bacon
178d307f52SEvan Baconconst pipeline = promisify(Stream.pipeline);
188d307f52SEvan Bacon
198d307f52SEvan Baconasync function downloadAsync({
208d307f52SEvan Bacon  url,
218d307f52SEvan Bacon  outputPath,
228d307f52SEvan Bacon  cacheDirectory,
238d307f52SEvan Bacon  onProgress,
248d307f52SEvan Bacon}: {
258d307f52SEvan Bacon  url: string;
268d307f52SEvan Bacon  outputPath: string;
278d307f52SEvan Bacon  cacheDirectory?: string;
288d307f52SEvan Bacon  onProgress?: ProgressCallback;
298d307f52SEvan Bacon}) {
308c8eefe0SEvan Bacon  let fetchInstance: FetchLike = fetchAsync;
318d307f52SEvan Bacon  if (cacheDirectory) {
328d307f52SEvan Bacon    // Reconstruct the cached fetch since caching could be disabled.
338d307f52SEvan Bacon    fetchInstance = createCachedFetch({
348d307f52SEvan Bacon      // We'll use a 1 week cache for versions so older values get flushed out eventually.
358d307f52SEvan Bacon      ttl: 1000 * 60 * 60 * 24 * 7,
368d307f52SEvan Bacon      // Users can also nuke their `~/.expo` directory to clear the cache.
378d307f52SEvan Bacon      cacheDirectory,
388d307f52SEvan Bacon    });
398d307f52SEvan Bacon  }
408d307f52SEvan Bacon
41474a7a4bSEvan Bacon  debug(`Downloading ${url} to ${outputPath}`);
4282f3de79SEvan Bacon  const res = await fetchInstance(url, {
438d307f52SEvan Bacon    timeout: TIMER_DURATION,
448d307f52SEvan Bacon    onProgress,
458d307f52SEvan Bacon  });
468d307f52SEvan Bacon  if (!res.ok) {
478d307f52SEvan Bacon    throw new CommandError(
488d307f52SEvan Bacon      'FILE_DOWNLOAD',
498d307f52SEvan Bacon      `Unexpected response: ${res.statusText}. From url: ${url}`
508d307f52SEvan Bacon    );
518d307f52SEvan Bacon  }
528d307f52SEvan Bacon  return pipeline(res.body, fs.createWriteStream(outputPath));
538d307f52SEvan Bacon}
548d307f52SEvan Bacon
558d307f52SEvan Baconexport async function downloadAppAsync({
568d307f52SEvan Bacon  url,
578d307f52SEvan Bacon  outputPath,
588d307f52SEvan Bacon  extract = false,
598d307f52SEvan Bacon  cacheDirectory,
608d307f52SEvan Bacon  onProgress,
618d307f52SEvan Bacon}: {
628d307f52SEvan Bacon  url: string;
638d307f52SEvan Bacon  outputPath: string;
648d307f52SEvan Bacon  extract?: boolean;
658d307f52SEvan Bacon  cacheDirectory?: string;
668d307f52SEvan Bacon  onProgress?: ProgressCallback;
678d307f52SEvan Bacon}): Promise<void> {
688d307f52SEvan Bacon  if (extract) {
698d307f52SEvan Bacon    // For iOS we download the ipa to a file then pass that file into the extractor.
708d307f52SEvan Bacon    // In the future we should just pipe the `res.body -> tar.extract` directly.
718d307f52SEvan Bacon    // I tried this and it created some weird errors where observing the data stream
728d307f52SEvan Bacon    // would corrupt the file causing tar to fail with `TAR_BAD_ARCHIVE`.
738d307f52SEvan Bacon    const tmpPath = temporary.file({ name: path.basename(outputPath) });
748d307f52SEvan Bacon    await downloadAsync({ url, outputPath: tmpPath, cacheDirectory, onProgress });
75474a7a4bSEvan Bacon    debug(`Extracting ${tmpPath} to ${outputPath}`);
768a782c0fSEvan Bacon    await ensureDirectoryAsync(outputPath);
778d307f52SEvan Bacon    await extractAsync(tmpPath, outputPath);
788d307f52SEvan Bacon  } else {
798d307f52SEvan Bacon    await ensureDirectoryAsync(path.dirname(outputPath));
808d307f52SEvan Bacon    await downloadAsync({ url, outputPath, cacheDirectory, onProgress });
818d307f52SEvan Bacon  }
828d307f52SEvan Bacon}
83