1import fs from 'fs';
2import path from 'path';
3import { Stream } from 'stream';
4import temporary from 'tempy';
5import { promisify } from 'util';
6
7import { ensureDirectoryAsync } from './dir';
8import { CommandError } from './errors';
9import { extractAsync } from './tar';
10import { createCachedFetch, fetchAsync } from '../api/rest/client';
11import { FetchLike, ProgressCallback } from '../api/rest/client.types';
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