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