1import { UnavailabilityError } from 'expo-errors';
2import { EventEmitter, Subscription } from 'expo-core';
3import UUID from 'uuid-js';
4import FS from './ExponentFileSystem';
5
6import {
7  DownloadOptions,
8  DownloadResult,
9  DownloadProgressCallback,
10  DownloadProgressData,
11  DownloadPauseState,
12  FileInfo,
13  EncodingType,
14  ReadingOptions,
15  WritingOptions,
16  ProgressEvent,
17} from './FileSystem.types';
18
19if (!FS) {
20  console.warn(
21    "No native ExponentFileSystem module found, are you sure the expo-file-system's module is linked properly?"
22  );
23}
24
25export {
26  DownloadOptions,
27  DownloadResult,
28  DownloadProgressCallback,
29  DownloadProgressData,
30  DownloadPauseState,
31  FileInfo,
32  EncodingType,
33  ReadingOptions,
34  WritingOptions,
35  ProgressEvent,
36};
37
38function normalizeEndingSlash(p: string | null): string | null {
39  if (p != null) {
40    return p.replace(/\/*$/, '') + '/';
41  }
42  return null;
43}
44
45FS.documentDirectory = normalizeEndingSlash(FS.documentDirectory);
46FS.cacheDirectory = normalizeEndingSlash(FS.cacheDirectory);
47
48export const { documentDirectory, cacheDirectory, bundledAssets, bundleDirectory } = FS;
49
50export async function getInfoAsync(
51  fileUri: string,
52  options: { md5?: boolean; cache?: boolean } = {}
53): Promise<FileInfo> {
54  if (!FS.getInfoAsync) {
55    throw new UnavailabilityError('expo-file-system', 'getInfoAsync');
56  }
57  return await FS.getInfoAsync(fileUri, options);
58}
59
60export async function readAsStringAsync(
61  fileUri: string,
62  options?: ReadingOptions
63): Promise<string> {
64  if (!FS.readAsStringAsync) {
65    throw new UnavailabilityError('expo-file-system', 'readAsStringAsync');
66  }
67  return await FS.readAsStringAsync(fileUri, options || {});
68}
69
70export async function writeAsStringAsync(
71  fileUri: string,
72  contents: string,
73  options: WritingOptions = {}
74): Promise<void> {
75  if (!FS.writeAsStringAsync) {
76    throw new UnavailabilityError('expo-file-system', 'writeAsStringAsync');
77  }
78  return await FS.writeAsStringAsync(fileUri, contents, options);
79}
80
81export async function deleteAsync(
82  fileUri: string,
83  options: { idempotent?: boolean } = {}
84): Promise<void> {
85  if (!FS.deleteAsync) {
86    throw new UnavailabilityError('expo-file-system', 'deleteAsync');
87  }
88  return await FS.deleteAsync(fileUri, options);
89}
90
91export async function moveAsync(options: { from: string; to: string }): Promise<void> {
92  if (!FS.moveAsync) {
93    throw new UnavailabilityError('expo-file-system', 'moveAsync');
94  }
95  return await FS.moveAsync(options);
96}
97
98export async function copyAsync(options: { from: string; to: string }): Promise<void> {
99  if (!FS.copyAsync) {
100    throw new UnavailabilityError('expo-file-system', 'copyAsync');
101  }
102  return await FS.copyAsync(options);
103}
104
105export async function makeDirectoryAsync(
106  fileUri: string,
107  options: { intermediates?: boolean } = {}
108): Promise<void> {
109  if (!FS.makeDirectoryAsync) {
110    throw new UnavailabilityError('expo-file-system', 'makeDirectoryAsync');
111  }
112  return await FS.makeDirectoryAsync(fileUri, options);
113}
114
115export async function readDirectoryAsync(fileUri: string): Promise<string[]> {
116  if (!FS.readDirectoryAsync) {
117    throw new UnavailabilityError('expo-file-system', 'readDirectoryAsync');
118  }
119  return await FS.readDirectoryAsync(fileUri, {});
120}
121
122export async function downloadAsync(
123  uri: string,
124  fileUri: string,
125  options: DownloadOptions = {}
126): Promise<DownloadResult> {
127  if (!FS.downloadAsync) {
128    throw new UnavailabilityError('expo-file-system', 'downloadAsync');
129  }
130  return await FS.downloadAsync(uri, fileUri, options);
131}
132
133export function createDownloadResumable(
134  uri: string,
135  fileUri: string,
136  options?: DownloadOptions,
137  callback?: DownloadProgressCallback,
138  resumeData?: string
139): DownloadResumable {
140  return new DownloadResumable(uri, fileUri, options, callback, resumeData);
141}
142
143export class DownloadResumable {
144  _uuid: string;
145  _url: string;
146  _fileUri: string;
147  _options: DownloadOptions;
148  _resumeData?: string;
149  _callback?: DownloadProgressCallback;
150  _subscription?: Subscription | null;
151  _emitter: EventEmitter;
152
153  constructor(
154    url: string,
155    fileUri: string,
156    options: DownloadOptions = {},
157    callback?: DownloadProgressCallback,
158    resumeData?: string
159  ) {
160    this._uuid = UUID.create(4).toString();
161    this._url = url;
162    this._fileUri = fileUri;
163    this._options = options;
164    this._resumeData = resumeData;
165    this._callback = callback;
166    this._subscription = null;
167    this._emitter = new EventEmitter(FS);
168  }
169
170  async downloadAsync(): Promise<DownloadResult | undefined> {
171    if (!FS.downloadResumableStartAsync) {
172      throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
173    }
174    this._addSubscription();
175    return await FS.downloadResumableStartAsync(
176      this._url,
177      this._fileUri,
178      this._uuid,
179      this._options,
180      this._resumeData
181    );
182  }
183
184  async pauseAsync(): Promise<DownloadPauseState> {
185    if (!FS.downloadResumablePauseAsync) {
186      throw new UnavailabilityError('expo-file-system', 'downloadResumablePauseAsync');
187    }
188    const pauseResult = await FS.downloadResumablePauseAsync(this._uuid);
189    this._removeSubscription();
190    if (pauseResult) {
191      this._resumeData = pauseResult.resumeData;
192      return this.savable();
193    } else {
194      throw new Error('Unable to generate a savable pause state');
195    }
196  }
197
198  async resumeAsync(): Promise<DownloadResult | undefined> {
199    if (!FS.downloadResumableStartAsync) {
200      throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
201    }
202    this._addSubscription();
203    return await FS.downloadResumableStartAsync(
204      this._url,
205      this._fileUri,
206      this._uuid,
207      this._options,
208      this._resumeData
209    );
210  }
211
212  savable(): DownloadPauseState {
213    return {
214      url: this._url,
215      fileUri: this._fileUri,
216      options: this._options,
217      resumeData: this._resumeData,
218    };
219  }
220
221  _addSubscription(): void {
222    if (this._subscription) {
223      return;
224    }
225    this._subscription = this._emitter.addListener(
226      'Exponent.downloadProgress',
227      (event: ProgressEvent) => {
228        if (event.uuid === this._uuid) {
229          const callback = this._callback;
230          if (callback) {
231            callback(event.data);
232          }
233        }
234      }
235    );
236  }
237
238  _removeSubscription(): void {
239    if (!this._subscription) {
240      return;
241    }
242    this._emitter.removeSubscription(this._subscription);
243    this._subscription = null;
244  }
245}
246