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