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