1import { EventEmitter, Subscription, UnavailabilityError } from 'expo-modules-core';
2import { Platform } from 'react-native';
3import { v4 as uuidv4 } from 'uuid';
4
5import ExponentFileSystem from './ExponentFileSystem';
6import {
7  DownloadOptions,
8  DownloadPauseState,
9  DownloadProgressCallback,
10  FileSystemNetworkTaskProgressCallback,
11  DownloadProgressData,
12  UploadProgressData,
13  DownloadResult,
14  EncodingType,
15  FileInfo,
16  FileSystemAcceptedUploadHttpMethod,
17  FileSystemDownloadResult,
18  FileSystemRequestDirectoryPermissionsResult,
19  FileSystemSessionType,
20  FileSystemUploadOptions,
21  FileSystemUploadResult,
22  FileSystemUploadType,
23  ProgressEvent,
24  ReadingOptions,
25  WritingOptions,
26} from './FileSystem.types';
27
28if (!ExponentFileSystem) {
29  console.warn(
30    "No native ExponentFileSystem module found, are you sure the expo-file-system's module is linked properly?"
31  );
32}
33// Prevent webpack from pruning this.
34const _unused = new EventEmitter(ExponentFileSystem); // eslint-disable-line
35
36export {
37  DownloadOptions,
38  DownloadPauseState,
39  DownloadProgressCallback,
40  DownloadProgressData,
41  DownloadResult,
42  EncodingType,
43  FileInfo,
44  FileSystemDownloadResult,
45  FileSystemRequestDirectoryPermissionsResult,
46  FileSystemAcceptedUploadHttpMethod,
47  FileSystemSessionType,
48  FileSystemUploadOptions,
49  FileSystemUploadResult,
50  FileSystemUploadType,
51  FileSystemNetworkTaskProgressCallback,
52  ReadingOptions,
53  WritingOptions,
54};
55
56function normalizeEndingSlash(p: string | null): string | null {
57  if (p != null) {
58    return p.replace(/\/*$/, '') + '/';
59  }
60  return null;
61}
62
63export const documentDirectory = normalizeEndingSlash(ExponentFileSystem.documentDirectory);
64export const cacheDirectory = normalizeEndingSlash(ExponentFileSystem.cacheDirectory);
65
66export const { bundledAssets, bundleDirectory } = ExponentFileSystem;
67
68export async function getInfoAsync(
69  fileUri: string,
70  options: { md5?: boolean; size?: boolean } = {}
71): Promise<FileInfo> {
72  if (!ExponentFileSystem.getInfoAsync) {
73    throw new UnavailabilityError('expo-file-system', 'getInfoAsync');
74  }
75  return await ExponentFileSystem.getInfoAsync(fileUri, options);
76}
77
78export async function readAsStringAsync(
79  fileUri: string,
80  options?: ReadingOptions
81): Promise<string> {
82  if (!ExponentFileSystem.readAsStringAsync) {
83    throw new UnavailabilityError('expo-file-system', 'readAsStringAsync');
84  }
85  return await ExponentFileSystem.readAsStringAsync(fileUri, options || {});
86}
87
88export async function getContentUriAsync(fileUri: string): Promise<string> {
89  if (Platform.OS === 'android') {
90    if (!ExponentFileSystem.getContentUriAsync) {
91      throw new UnavailabilityError('expo-file-system', 'getContentUriAsync');
92    }
93    return await ExponentFileSystem.getContentUriAsync(fileUri);
94  } else {
95    return new Promise(function (resolve, reject) {
96      resolve(fileUri);
97    });
98  }
99}
100
101export async function writeAsStringAsync(
102  fileUri: string,
103  contents: string,
104  options: WritingOptions = {}
105): Promise<void> {
106  if (!ExponentFileSystem.writeAsStringAsync) {
107    throw new UnavailabilityError('expo-file-system', 'writeAsStringAsync');
108  }
109  return await ExponentFileSystem.writeAsStringAsync(fileUri, contents, options);
110}
111
112export async function deleteAsync(
113  fileUri: string,
114  options: { idempotent?: boolean } = {}
115): Promise<void> {
116  if (!ExponentFileSystem.deleteAsync) {
117    throw new UnavailabilityError('expo-file-system', 'deleteAsync');
118  }
119  return await ExponentFileSystem.deleteAsync(fileUri, options);
120}
121
122export async function deleteLegacyDocumentDirectoryAndroid(): Promise<void> {
123  if (Platform.OS !== 'android' || documentDirectory == null) {
124    return;
125  }
126  const legacyDocumentDirectory = `${documentDirectory}ExperienceData/`;
127  return await deleteAsync(legacyDocumentDirectory, { idempotent: true });
128}
129
130export async function moveAsync(options: { from: string; to: string }): Promise<void> {
131  if (!ExponentFileSystem.moveAsync) {
132    throw new UnavailabilityError('expo-file-system', 'moveAsync');
133  }
134  return await ExponentFileSystem.moveAsync(options);
135}
136
137export async function copyAsync(options: { from: string; to: string }): Promise<void> {
138  if (!ExponentFileSystem.copyAsync) {
139    throw new UnavailabilityError('expo-file-system', 'copyAsync');
140  }
141  return await ExponentFileSystem.copyAsync(options);
142}
143
144export async function makeDirectoryAsync(
145  fileUri: string,
146  options: { intermediates?: boolean } = {}
147): Promise<void> {
148  if (!ExponentFileSystem.makeDirectoryAsync) {
149    throw new UnavailabilityError('expo-file-system', 'makeDirectoryAsync');
150  }
151  return await ExponentFileSystem.makeDirectoryAsync(fileUri, options);
152}
153
154export async function readDirectoryAsync(fileUri: string): Promise<string[]> {
155  if (!ExponentFileSystem.readDirectoryAsync) {
156    throw new UnavailabilityError('expo-file-system', 'readDirectoryAsync');
157  }
158  return await ExponentFileSystem.readDirectoryAsync(fileUri, {});
159}
160
161export async function getFreeDiskStorageAsync(): Promise<number> {
162  if (!ExponentFileSystem.getFreeDiskStorageAsync) {
163    throw new UnavailabilityError('expo-file-system', 'getFreeDiskStorageAsync');
164  }
165  return await ExponentFileSystem.getFreeDiskStorageAsync();
166}
167
168export async function getTotalDiskCapacityAsync(): Promise<number> {
169  if (!ExponentFileSystem.getTotalDiskCapacityAsync) {
170    throw new UnavailabilityError('expo-file-system', 'getTotalDiskCapacityAsync');
171  }
172  return await ExponentFileSystem.getTotalDiskCapacityAsync();
173}
174
175export async function downloadAsync(
176  uri: string,
177  fileUri: string,
178  options: DownloadOptions = {}
179): Promise<FileSystemDownloadResult> {
180  if (!ExponentFileSystem.downloadAsync) {
181    throw new UnavailabilityError('expo-file-system', 'downloadAsync');
182  }
183
184  return await ExponentFileSystem.downloadAsync(uri, fileUri, {
185    sessionType: FileSystemSessionType.BACKGROUND,
186    ...options,
187  });
188}
189
190export async function uploadAsync(
191  url: string,
192  fileUri: string,
193  options: FileSystemUploadOptions = {}
194): Promise<FileSystemUploadResult> {
195  if (!ExponentFileSystem.uploadAsync) {
196    throw new UnavailabilityError('expo-file-system', 'uploadAsync');
197  }
198
199  return await ExponentFileSystem.uploadAsync(url, fileUri, {
200    sessionType: FileSystemSessionType.BACKGROUND,
201    uploadType: FileSystemUploadType.BINARY_CONTENT,
202    ...options,
203    httpMethod: (options.httpMethod || 'POST').toUpperCase(),
204  });
205}
206
207export function createDownloadResumable(
208  uri: string,
209  fileUri: string,
210  options?: DownloadOptions,
211  callback?: FileSystemNetworkTaskProgressCallback<DownloadProgressData>,
212  resumeData?: string
213): DownloadResumable {
214  return new DownloadResumable(uri, fileUri, options, callback, resumeData);
215}
216
217export function createUploadTask(
218  url: string,
219  fileUri: string,
220  options?: FileSystemUploadOptions,
221  callback?: FileSystemNetworkTaskProgressCallback<UploadProgressData>
222): UploadTask {
223  return new UploadTask(url, fileUri, options, callback);
224}
225
226export abstract class FileSystemCancellableNetworkTask<
227  T extends DownloadProgressData | UploadProgressData
228> {
229  private _uuid = uuidv4();
230  protected taskWasCanceled = false;
231  private emitter = new EventEmitter(ExponentFileSystem);
232  private subscription?: Subscription | null;
233
234  public async cancelAsync(): Promise<void> {
235    if (!ExponentFileSystem.networkTaskCancelAsync) {
236      throw new UnavailabilityError('expo-file-system', 'networkTaskCancelAsync');
237    }
238
239    this.removeSubscription();
240    this.taskWasCanceled = true;
241    return await ExponentFileSystem.networkTaskCancelAsync(this.uuid);
242  }
243
244  protected isTaskCancelled(): boolean {
245    if (this.taskWasCanceled) {
246      console.warn('This task was already canceled.');
247      return true;
248    }
249
250    return false;
251  }
252
253  protected get uuid(): string {
254    return this._uuid;
255  }
256
257  protected abstract getEventName(): string;
258
259  protected abstract getCallback(): FileSystemNetworkTaskProgressCallback<T> | undefined;
260
261  protected addSubscription() {
262    if (this.subscription) {
263      return;
264    }
265
266    this.subscription = this.emitter.addListener(this.getEventName(), (event: ProgressEvent<T>) => {
267      if (event.uuid === this.uuid) {
268        const callback = this.getCallback();
269        if (callback) {
270          callback(event.data);
271        }
272      }
273    });
274  }
275
276  protected removeSubscription() {
277    if (!this.subscription) {
278      return;
279    }
280    this.emitter.removeSubscription(this.subscription);
281    this.subscription = null;
282  }
283}
284
285export class UploadTask extends FileSystemCancellableNetworkTask<UploadProgressData> {
286  private options: FileSystemUploadOptions;
287
288  constructor(
289    private url: string,
290    private fileUri: string,
291    options?: FileSystemUploadOptions,
292    private callback?: FileSystemNetworkTaskProgressCallback<UploadProgressData>
293  ) {
294    super();
295
296    const httpMethod = (options?.httpMethod?.toUpperCase ||
297      'POST') as FileSystemAcceptedUploadHttpMethod;
298
299    this.options = {
300      sessionType: FileSystemSessionType.BACKGROUND,
301      uploadType: FileSystemUploadType.BINARY_CONTENT,
302      ...options,
303      httpMethod,
304    };
305  }
306
307  protected getEventName(): string {
308    return 'expo-file-system.uploadProgress';
309  }
310  protected getCallback(): FileSystemNetworkTaskProgressCallback<UploadProgressData> | undefined {
311    return this.callback;
312  }
313
314  public async uploadAsync(): Promise<FileSystemUploadResult | undefined> {
315    if (!ExponentFileSystem.uploadTaskStartAsync) {
316      throw new UnavailabilityError('expo-file-system', 'uploadTaskStartAsync');
317    }
318
319    if (this.isTaskCancelled()) {
320      return;
321    }
322
323    this.addSubscription();
324    const result = await ExponentFileSystem.uploadTaskStartAsync(
325      this.url,
326      this.fileUri,
327      this.uuid,
328      this.options
329    );
330    this.removeSubscription();
331
332    return result;
333  }
334}
335
336export class DownloadResumable extends FileSystemCancellableNetworkTask<DownloadProgressData> {
337  constructor(
338    private url: string,
339    private _fileUri: string,
340    private options: DownloadOptions = {},
341    private callback?: FileSystemNetworkTaskProgressCallback<DownloadProgressData>,
342    private resumeData?: string
343  ) {
344    super();
345  }
346
347  public get fileUri(): string {
348    return this._fileUri;
349  }
350
351  protected getEventName(): string {
352    return 'expo-file-system.downloadProgress';
353  }
354
355  protected getCallback(): FileSystemNetworkTaskProgressCallback<DownloadProgressData> | undefined {
356    return this.callback;
357  }
358
359  async downloadAsync(): Promise<FileSystemDownloadResult | undefined> {
360    if (!ExponentFileSystem.downloadResumableStartAsync) {
361      throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
362    }
363
364    if (this.isTaskCancelled()) {
365      return;
366    }
367
368    this.addSubscription();
369    return await ExponentFileSystem.downloadResumableStartAsync(
370      this.url,
371      this._fileUri,
372      this.uuid,
373      this.options,
374      this.resumeData
375    );
376  }
377
378  async pauseAsync(): Promise<DownloadPauseState> {
379    if (!ExponentFileSystem.downloadResumablePauseAsync) {
380      throw new UnavailabilityError('expo-file-system', 'downloadResumablePauseAsync');
381    }
382
383    if (this.isTaskCancelled()) {
384      return {
385        fileUri: this._fileUri,
386        options: this.options,
387        url: this.url,
388      };
389    }
390
391    const pauseResult = await ExponentFileSystem.downloadResumablePauseAsync(this.uuid);
392    this.removeSubscription();
393    if (pauseResult) {
394      this.resumeData = pauseResult.resumeData;
395      return this.savable();
396    } else {
397      throw new Error('Unable to generate a savable pause state');
398    }
399  }
400
401  async resumeAsync(): Promise<FileSystemDownloadResult | undefined> {
402    if (!ExponentFileSystem.downloadResumableStartAsync) {
403      throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
404    }
405
406    if (this.isTaskCancelled()) {
407      return;
408    }
409
410    this.addSubscription();
411    return await ExponentFileSystem.downloadResumableStartAsync(
412      this.url,
413      this.fileUri,
414      this.uuid,
415      this.options,
416      this.resumeData
417    );
418  }
419
420  savable(): DownloadPauseState {
421    return {
422      url: this.url,
423      fileUri: this.fileUri,
424      options: this.options,
425      resumeData: this.resumeData,
426    };
427  }
428}
429
430const baseReadAsStringAsync = readAsStringAsync;
431const baseWriteAsStringAsync = writeAsStringAsync;
432const baseDeleteAsync = deleteAsync;
433const baseMoveAsync = moveAsync;
434const baseCopyAsync = copyAsync;
435/**
436 * Android only
437 */
438export namespace StorageAccessFramework {
439  export function getUriForDirectoryInRoot(folderName: string) {
440    return `content://com.android.externalstorage.documents/tree/primary:${folderName}/document/primary:${folderName}`;
441  }
442
443  export async function requestDirectoryPermissionsAsync(
444    initialFileUrl: string | null = null
445  ): Promise<FileSystemRequestDirectoryPermissionsResult> {
446    if (!ExponentFileSystem.requestDirectoryPermissionsAsync) {
447      throw new UnavailabilityError(
448        'expo-file-system',
449        'StorageAccessFramework.requestDirectoryPermissionsAsync'
450      );
451    }
452
453    return await ExponentFileSystem.requestDirectoryPermissionsAsync(initialFileUrl);
454  }
455
456  export async function readDirectoryAsync(dirUri: string): Promise<string[]> {
457    if (!ExponentFileSystem.readSAFDirectoryAsync) {
458      throw new UnavailabilityError(
459        'expo-file-system',
460        'StorageAccessFramework.readDirectoryAsync'
461      );
462    }
463    return await ExponentFileSystem.readSAFDirectoryAsync(dirUri, {});
464  }
465
466  export async function makeDirectoryAsync(parentUri: string, dirName: string): Promise<string> {
467    if (!ExponentFileSystem.makeSAFDirectoryAsync) {
468      throw new UnavailabilityError(
469        'expo-file-system',
470        'StorageAccessFramework.makeDirectoryAsync'
471      );
472    }
473    return await ExponentFileSystem.makeSAFDirectoryAsync(parentUri, dirName);
474  }
475
476  export async function createFileAsync(
477    parentUri: string,
478    fileName: string,
479    mimeType: string
480  ): Promise<string> {
481    if (!ExponentFileSystem.createSAFFileAsync) {
482      throw new UnavailabilityError('expo-file-system', 'StorageAccessFramework.createFileAsync');
483    }
484    return await ExponentFileSystem.createSAFFileAsync(parentUri, fileName, mimeType);
485  }
486
487  export const writeAsStringAsync = baseWriteAsStringAsync;
488  export const readAsStringAsync = baseReadAsStringAsync;
489  export const deleteAsync = baseDeleteAsync;
490  export const moveAsync = baseMoveAsync;
491  export const copyAsync = baseCopyAsync;
492}
493