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  FileSystemNetworkTaskProgressCallback,
10  DownloadProgressData,
11  UploadProgressData,
12  FileInfo,
13  FileSystemAcceptedUploadHttpMethod,
14  FileSystemDownloadResult,
15  FileSystemRequestDirectoryPermissionsResult,
16  FileSystemSessionType,
17  FileSystemUploadOptions,
18  FileSystemUploadResult,
19  FileSystemUploadType,
20  ProgressEvent,
21  ReadingOptions,
22  WritingOptions,
23  DeletingOptions,
24  InfoOptions,
25  RelocatingOptions,
26  MakeDirectoryOptions,
27} from './FileSystem.types';
28
29if (!ExponentFileSystem) {
30  console.warn(
31    "No native ExponentFileSystem module found, are you sure the expo-file-system's module is linked properly?"
32  );
33}
34// Prevent webpack from pruning this.
35const _unused = new EventEmitter(ExponentFileSystem); // eslint-disable-line
36
37function normalizeEndingSlash(p: string | null): string | null {
38  if (p != null) {
39    return p.replace(/\/*$/, '') + '/';
40  }
41  return null;
42}
43
44/**
45 * `file://` URI pointing to the directory where user documents for this app will be stored.
46 * Files stored here will remain until explicitly deleted by the app. Ends with a trailing `/`.
47 * Example uses are for files the user saves that they expect to see again.
48 */
49export const documentDirectory = normalizeEndingSlash(ExponentFileSystem.documentDirectory);
50
51/**
52 * `file://` URI pointing to the directory where temporary files used by this app will be stored.
53 * Files stored here may be automatically deleted by the system when low on storage.
54 * Example uses are for downloaded or generated files that the app just needs for one-time usage.
55 */
56export const cacheDirectory = normalizeEndingSlash(ExponentFileSystem.cacheDirectory);
57
58// @docsMissing
59export const { bundledAssets, bundleDirectory } = ExponentFileSystem;
60
61/**
62 * Get metadata information about a file, directory or external content/asset.
63 * @param fileUri URI to the file or directory. See [supported URI schemes](#supported-uri-schemes).
64 * @param options A map of options represented by [`GetInfoAsyncOptions`](#getinfoasyncoptions) type.
65 * @return A Promise that resolves to a `FileInfo` object. If no item exists at this URI,
66 * the returned Promise resolves to `FileInfo` object in form of `{ exists: false, isDirectory: false }`.
67 */
68export async function getInfoAsync(fileUri: string, options: InfoOptions = {}): Promise<FileInfo> {
69  if (!ExponentFileSystem.getInfoAsync) {
70    throw new UnavailabilityError('expo-file-system', 'getInfoAsync');
71  }
72  return await ExponentFileSystem.getInfoAsync(fileUri, options);
73}
74
75/**
76 * Read the entire contents of a file as a string. Binary will be returned in raw format, you will need to append `data:image/png;base64,` to use it as Base64.
77 * @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
78 * @param options A map of read options represented by [`ReadingOptions`](#readingoptions) type.
79 * @return A Promise that resolves to a string containing the entire contents of the file.
80 */
81export async function readAsStringAsync(
82  fileUri: string,
83  options: ReadingOptions = {}
84): Promise<string> {
85  if (!ExponentFileSystem.readAsStringAsync) {
86    throw new UnavailabilityError('expo-file-system', 'readAsStringAsync');
87  }
88  return await ExponentFileSystem.readAsStringAsync(fileUri, options);
89}
90
91/**
92 * Takes a `file://` URI and converts it into content URI (`content://`) so that it can be accessed by other applications outside of Expo.
93 * @param fileUri The local URI of the file. If there is no file at this URI, an exception will be thrown.
94 * @example
95 * ```js
96 * FileSystem.getContentUriAsync(uri).then(cUri => {
97 *   console.log(cUri);
98 *   IntentLauncher.startActivityAsync('android.intent.action.VIEW', {
99 *     data: cUri,
100 *     flags: 1,
101 *   });
102 * });
103 * ```
104 * @return Returns a Promise that resolves to a `string` containing a `content://` URI pointing to the file.
105 * The URI is the same as the `fileUri` input parameter but in a different format.
106 */
107export async function getContentUriAsync(fileUri: string): Promise<string> {
108  if (Platform.OS === 'android') {
109    if (!ExponentFileSystem.getContentUriAsync) {
110      throw new UnavailabilityError('expo-file-system', 'getContentUriAsync');
111    }
112    return await ExponentFileSystem.getContentUriAsync(fileUri);
113  } else {
114    return new Promise(function (resolve, reject) {
115      resolve(fileUri);
116    });
117  }
118}
119
120/**
121 * Write the entire contents of a file as a string.
122 * @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
123 * > Note: when you're using SAF URI the file needs to exist. You can't create a new file.
124 * @param contents The string to replace the contents of the file with.
125 * @param options A map of write options represented by [`WritingOptions`](#writingoptions) type.
126 */
127export async function writeAsStringAsync(
128  fileUri: string,
129  contents: string,
130  options: WritingOptions = {}
131): Promise<void> {
132  if (!ExponentFileSystem.writeAsStringAsync) {
133    throw new UnavailabilityError('expo-file-system', 'writeAsStringAsync');
134  }
135  return await ExponentFileSystem.writeAsStringAsync(fileUri, contents, options);
136}
137
138/**
139 * Delete a file or directory. If the URI points to a directory, the directory and all its contents are recursively deleted.
140 * @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
141 * @param options A map of write options represented by [`DeletingOptions`](#deletingoptions) type.
142 */
143export async function deleteAsync(fileUri: string, options: DeletingOptions = {}): Promise<void> {
144  if (!ExponentFileSystem.deleteAsync) {
145    throw new UnavailabilityError('expo-file-system', 'deleteAsync');
146  }
147  return await ExponentFileSystem.deleteAsync(fileUri, options);
148}
149
150export async function deleteLegacyDocumentDirectoryAndroid(): Promise<void> {
151  if (Platform.OS !== 'android' || documentDirectory == null) {
152    return;
153  }
154  const legacyDocumentDirectory = `${documentDirectory}ExperienceData/`;
155  return await deleteAsync(legacyDocumentDirectory, { idempotent: true });
156}
157
158/**
159 * Move a file or directory to a new location.
160 * @param options A map of move options represented by [`RelocatingOptions`](#relocatingoptions) type.
161 */
162export async function moveAsync(options: RelocatingOptions): Promise<void> {
163  if (!ExponentFileSystem.moveAsync) {
164    throw new UnavailabilityError('expo-file-system', 'moveAsync');
165  }
166  return await ExponentFileSystem.moveAsync(options);
167}
168
169/**
170 * Create a copy of a file or directory. Directories are recursively copied with all of their contents.
171 * It can be also used to copy content shared by other apps to local filesystem.
172 * @param options A map of move options represented by [`RelocatingOptions`](#relocatingoptions) type.
173 */
174export async function copyAsync(options: RelocatingOptions): Promise<void> {
175  if (!ExponentFileSystem.copyAsync) {
176    throw new UnavailabilityError('expo-file-system', 'copyAsync');
177  }
178  return await ExponentFileSystem.copyAsync(options);
179}
180
181/**
182 * Create a new empty directory.
183 * @param fileUri `file://` URI to the new directory to create.
184 * @param options A map of create directory options represented by [`MakeDirectoryOptions`](#makedirectoryoptions) type.
185 */
186export async function makeDirectoryAsync(
187  fileUri: string,
188  options: MakeDirectoryOptions = {}
189): Promise<void> {
190  if (!ExponentFileSystem.makeDirectoryAsync) {
191    throw new UnavailabilityError('expo-file-system', 'makeDirectoryAsync');
192  }
193  return await ExponentFileSystem.makeDirectoryAsync(fileUri, options);
194}
195
196/**
197 * Enumerate the contents of a directory.
198 * @param fileUri `file://` URI to the directory.
199 * @return A Promise that resolves to an array of strings, each containing the name of a file or directory contained in the directory at `fileUri`.
200 */
201export async function readDirectoryAsync(fileUri: string): Promise<string[]> {
202  if (!ExponentFileSystem.readDirectoryAsync) {
203    throw new UnavailabilityError('expo-file-system', 'readDirectoryAsync');
204  }
205  return await ExponentFileSystem.readDirectoryAsync(fileUri, {});
206}
207
208/**
209 * Gets the available internal disk storage size, in bytes. This returns the free space on the data partition that hosts all of the internal storage for all apps on the device.
210 * @return Returns a Promise that resolves to the number of bytes available on the internal disk, or JavaScript's [`MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
211 * if the capacity is greater than 2<sup>53</sup> - 1 bytes.
212 */
213export async function getFreeDiskStorageAsync(): Promise<number> {
214  if (!ExponentFileSystem.getFreeDiskStorageAsync) {
215    throw new UnavailabilityError('expo-file-system', 'getFreeDiskStorageAsync');
216  }
217  return await ExponentFileSystem.getFreeDiskStorageAsync();
218}
219
220/**
221 * Gets total internal disk storage size, in bytes. This is the total capacity of the data partition that hosts all the internal storage for all apps on the device.
222 * @return Returns a Promise that resolves to a number that specifies the total internal disk storage capacity in bytes, or JavaScript's [`MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
223 * if the capacity is greater than 2<sup>53</sup> - 1 bytes.
224 */
225export async function getTotalDiskCapacityAsync(): Promise<number> {
226  if (!ExponentFileSystem.getTotalDiskCapacityAsync) {
227    throw new UnavailabilityError('expo-file-system', 'getTotalDiskCapacityAsync');
228  }
229  return await ExponentFileSystem.getTotalDiskCapacityAsync();
230}
231
232/**
233 * Download the contents at a remote URI to a file in the app's file system. The directory for a local file uri must exist prior to calling this function.
234 * @param uri The remote URI to download from.
235 * @param fileUri The local URI of the file to download to. If there is no file at this URI, a new one is created.
236 * If there is a file at this URI, its contents are replaced. The directory for the file must exist.
237 * @param options A map of download options represented by [`DownloadOptions`](#downloadoptions) type.
238 * @example
239 * ```js
240 * FileSystem.downloadAsync(
241 *   'http://techslides.com/demos/sample-videos/small.mp4',
242 *   FileSystem.documentDirectory + 'small.mp4'
243 * )
244 *   .then(({ uri }) => {
245 *     console.log('Finished downloading to ', uri);
246 *   })
247 *   .catch(error => {
248 *     console.error(error);
249 *   });
250 * ```
251 * @return Returns a Promise that resolves to a `FileSystemDownloadResult` object.
252 */
253export async function downloadAsync(
254  uri: string,
255  fileUri: string,
256  options: DownloadOptions = {}
257): Promise<FileSystemDownloadResult> {
258  if (!ExponentFileSystem.downloadAsync) {
259    throw new UnavailabilityError('expo-file-system', 'downloadAsync');
260  }
261
262  return await ExponentFileSystem.downloadAsync(uri, fileUri, {
263    sessionType: FileSystemSessionType.BACKGROUND,
264    ...options,
265  });
266}
267
268/**
269 * Upload the contents of the file pointed by `fileUri` to the remote url.
270 * @param url The remote URL, where the file will be sent.
271 * @param fileUri The local URI of the file to send. The file must exist.
272 * @param options A map of download options represented by [`FileSystemUploadOptions`](#filesystemuploadoptions) type.
273 * @example
274 * **Client**
275 *
276 * ```js
277 * import * as FileSystem from 'expo-file-system';
278 *
279 * try {
280 *   const response = await FileSystem.uploadAsync(`http://192.168.0.1:1234/binary-upload`, fileUri, {
281 *     fieldName: 'file',
282 *     httpMethod: 'PATCH',
283 *     uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT,
284 *   });
285 *   console.log(JSON.stringify(response, null, 4));
286 * } catch (error) {
287 *   console.log(error);
288 * }
289 * ```
290 *
291 * **Server**
292 *
293 * Please refer to the "[Server: Handling multipart requests](#server-handling-multipart-requests)" example - there is code for a simple Node.js server.
294 * @return Returns a Promise that resolves to `FileSystemUploadResult` object.
295 */
296export async function uploadAsync(
297  url: string,
298  fileUri: string,
299  options: FileSystemUploadOptions = {}
300): Promise<FileSystemUploadResult> {
301  if (!ExponentFileSystem.uploadAsync) {
302    throw new UnavailabilityError('expo-file-system', 'uploadAsync');
303  }
304
305  return await ExponentFileSystem.uploadAsync(url, fileUri, {
306    sessionType: FileSystemSessionType.BACKGROUND,
307    uploadType: FileSystemUploadType.BINARY_CONTENT,
308    ...options,
309    httpMethod: (options.httpMethod || 'POST').toUpperCase(),
310  });
311}
312
313/**
314 * Create a `DownloadResumable` object which can start, pause, and resume a download of contents at a remote URI to a file in the app's file system.
315 * > Note: You need to call `downloadAsync()`, on a `DownloadResumable` instance to initiate the download.
316 * The `DownloadResumable` object has a callback that provides download progress updates.
317 * Downloads can be resumed across app restarts by using `AsyncStorage` to store the `DownloadResumable.savable()` object for later retrieval.
318 * The `savable` object contains the arguments required to initialize a new `DownloadResumable` object to resume the download after an app restart.
319 * The directory for a local file uri must exist prior to calling this function.
320 * @param uri The remote URI to download from.
321 * @param fileUri The local URI of the file to download to. If there is no file at this URI, a new one is created.
322 * If there is a file at this URI, its contents are replaced. The directory for the file must exist.
323 * @param options A map of download options represented by [`DownloadOptions`](#downloadoptions) type.
324 * @param callback This function is called on each data write to update the download progress.
325 * > **Note**: When the app has been moved to the background, this callback won't be fired until it's moved to the foreground.
326 * @param resumeData The string which allows the api to resume a paused download. This is set on the `DownloadResumable` object automatically when a download is paused.
327 * When initializing a new `DownloadResumable` this should be `null`.
328 */
329export function createDownloadResumable(
330  uri: string,
331  fileUri: string,
332  options?: DownloadOptions,
333  callback?: FileSystemNetworkTaskProgressCallback<DownloadProgressData>,
334  resumeData?: string
335): DownloadResumable {
336  return new DownloadResumable(uri, fileUri, options, callback, resumeData);
337}
338
339export function createUploadTask(
340  url: string,
341  fileUri: string,
342  options?: FileSystemUploadOptions,
343  callback?: FileSystemNetworkTaskProgressCallback<UploadProgressData>
344): UploadTask {
345  return new UploadTask(url, fileUri, options, callback);
346}
347
348export abstract class FileSystemCancellableNetworkTask<
349  T extends DownloadProgressData | UploadProgressData
350> {
351  private _uuid = uuidv4();
352  protected taskWasCanceled = false;
353  private emitter = new EventEmitter(ExponentFileSystem);
354  private subscription?: Subscription | null;
355
356  // @docsMissing
357  public async cancelAsync(): Promise<void> {
358    if (!ExponentFileSystem.networkTaskCancelAsync) {
359      throw new UnavailabilityError('expo-file-system', 'networkTaskCancelAsync');
360    }
361
362    this.removeSubscription();
363    this.taskWasCanceled = true;
364    return await ExponentFileSystem.networkTaskCancelAsync(this.uuid);
365  }
366
367  protected isTaskCancelled(): boolean {
368    if (this.taskWasCanceled) {
369      console.warn('This task was already canceled.');
370      return true;
371    }
372
373    return false;
374  }
375
376  protected get uuid(): string {
377    return this._uuid;
378  }
379
380  protected abstract getEventName(): string;
381
382  protected abstract getCallback(): FileSystemNetworkTaskProgressCallback<T> | undefined;
383
384  protected addSubscription() {
385    if (this.subscription) {
386      return;
387    }
388
389    this.subscription = this.emitter.addListener(this.getEventName(), (event: ProgressEvent<T>) => {
390      if (event.uuid === this.uuid) {
391        const callback = this.getCallback();
392        if (callback) {
393          callback(event.data);
394        }
395      }
396    });
397  }
398
399  protected removeSubscription() {
400    if (!this.subscription) {
401      return;
402    }
403    this.emitter.removeSubscription(this.subscription);
404    this.subscription = null;
405  }
406}
407
408export class UploadTask extends FileSystemCancellableNetworkTask<UploadProgressData> {
409  private options: FileSystemUploadOptions;
410
411  constructor(
412    private url: string,
413    private fileUri: string,
414    options?: FileSystemUploadOptions,
415    private callback?: FileSystemNetworkTaskProgressCallback<UploadProgressData>
416  ) {
417    super();
418
419    const httpMethod = (options?.httpMethod?.toUpperCase() ||
420      'POST') as FileSystemAcceptedUploadHttpMethod;
421
422    this.options = {
423      sessionType: FileSystemSessionType.BACKGROUND,
424      uploadType: FileSystemUploadType.BINARY_CONTENT,
425      ...options,
426      httpMethod,
427    };
428  }
429
430  protected getEventName(): string {
431    return 'expo-file-system.uploadProgress';
432  }
433  protected getCallback(): FileSystemNetworkTaskProgressCallback<UploadProgressData> | undefined {
434    return this.callback;
435  }
436
437  // @docsMissing
438  public async uploadAsync(): Promise<FileSystemUploadResult | undefined> {
439    if (!ExponentFileSystem.uploadTaskStartAsync) {
440      throw new UnavailabilityError('expo-file-system', 'uploadTaskStartAsync');
441    }
442
443    if (this.isTaskCancelled()) {
444      return;
445    }
446
447    this.addSubscription();
448    const result = await ExponentFileSystem.uploadTaskStartAsync(
449      this.url,
450      this.fileUri,
451      this.uuid,
452      this.options
453    );
454    this.removeSubscription();
455
456    return result;
457  }
458}
459
460export class DownloadResumable extends FileSystemCancellableNetworkTask<DownloadProgressData> {
461  constructor(
462    private url: string,
463    private _fileUri: string,
464    private options: DownloadOptions = {},
465    private callback?: FileSystemNetworkTaskProgressCallback<DownloadProgressData>,
466    private resumeData?: string
467  ) {
468    super();
469  }
470
471  public get fileUri(): string {
472    return this._fileUri;
473  }
474
475  protected getEventName(): string {
476    return 'expo-file-system.downloadProgress';
477  }
478
479  protected getCallback(): FileSystemNetworkTaskProgressCallback<DownloadProgressData> | undefined {
480    return this.callback;
481  }
482
483  /**
484   * Download the contents at a remote URI to a file in the app's file system.
485   * @return Returns a Promise that resolves to `FileSystemDownloadResult` object, or to `undefined` when task was cancelled.
486   */
487  async downloadAsync(): Promise<FileSystemDownloadResult | undefined> {
488    if (!ExponentFileSystem.downloadResumableStartAsync) {
489      throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
490    }
491
492    if (this.isTaskCancelled()) {
493      return;
494    }
495
496    this.addSubscription();
497    return await ExponentFileSystem.downloadResumableStartAsync(
498      this.url,
499      this._fileUri,
500      this.uuid,
501      this.options,
502      this.resumeData
503    );
504  }
505
506  /**
507   * Pause the current download operation. `resumeData` is added to the `DownloadResumable` object after a successful pause operation.
508   * Returns an object that can be saved with `AsyncStorage` for future retrieval (the same object that is returned from calling `FileSystem.DownloadResumable.savable()`).
509   * @return Returns a Promise that resolves to `DownloadPauseState` object.
510   */
511  async pauseAsync(): Promise<DownloadPauseState> {
512    if (!ExponentFileSystem.downloadResumablePauseAsync) {
513      throw new UnavailabilityError('expo-file-system', 'downloadResumablePauseAsync');
514    }
515
516    if (this.isTaskCancelled()) {
517      return {
518        fileUri: this._fileUri,
519        options: this.options,
520        url: this.url,
521      };
522    }
523
524    const pauseResult = await ExponentFileSystem.downloadResumablePauseAsync(this.uuid);
525    this.removeSubscription();
526    if (pauseResult) {
527      this.resumeData = pauseResult.resumeData;
528      return this.savable();
529    } else {
530      throw new Error('Unable to generate a savable pause state');
531    }
532  }
533
534  /**
535   * Resume a paused download operation.
536   * @return Returns a Promise that resolves to `FileSystemDownloadResult` object, or to `undefined` when task was cancelled.
537   */
538  async resumeAsync(): Promise<FileSystemDownloadResult | undefined> {
539    if (!ExponentFileSystem.downloadResumableStartAsync) {
540      throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
541    }
542
543    if (this.isTaskCancelled()) {
544      return;
545    }
546
547    this.addSubscription();
548    return await ExponentFileSystem.downloadResumableStartAsync(
549      this.url,
550      this.fileUri,
551      this.uuid,
552      this.options,
553      this.resumeData
554    );
555  }
556
557  /**
558   * Method to get the object which can be saved with `AsyncStorage` for future retrieval.
559   * @returns Returns object in shape of `DownloadPauseState` type.
560   */
561  savable(): DownloadPauseState {
562    return {
563      url: this.url,
564      fileUri: this.fileUri,
565      options: this.options,
566      resumeData: this.resumeData,
567    };
568  }
569}
570
571const baseReadAsStringAsync = readAsStringAsync;
572const baseWriteAsStringAsync = writeAsStringAsync;
573const baseDeleteAsync = deleteAsync;
574const baseMoveAsync = moveAsync;
575const baseCopyAsync = copyAsync;
576
577/**
578 * The `StorageAccessFramework` is a namespace inside of the `expo-file-system` module, which encapsulates all functions which can be used with [SAF URIs](#saf-uri).
579 * You can read more about SAF in the [Android documentation](https://developer.android.com/guide/topics/providers/document-provider).
580 *
581 * @example
582 * # Basic Usage
583 *
584 * ```ts
585 * import { StorageAccessFramework } from 'expo-file-system';
586 *
587 * // Requests permissions for external directory
588 * const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
589 *
590 * if (permissions.granted) {
591 *   // Gets SAF URI from response
592 *   const uri = permissions.directoryUri;
593 *
594 *   // Gets all files inside of selected directory
595 *   const files = await StorageAccessFramework.readDirectoryAsync(uri);
596 *   alert(`Files inside ${uri}:\n\n${JSON.stringify(files)}`);
597 * }
598 * ```
599 *
600 * # Migrating an album
601 *
602 * ```ts
603 * import * as MediaLibrary from 'expo-media-library';
604 * import * as FileSystem from 'expo-file-system';
605 * const { StorageAccessFramework } = FileSystem;
606 *
607 * async function migrateAlbum(albumName: string) {
608 *   // Gets SAF URI to the album
609 *   const albumUri = StorageAccessFramework.getUriForDirectoryInRoot(albumName);
610 *
611 *   // Requests permissions
612 *   const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync(albumUri);
613 *   if (!permissions.granted) {
614 *     return;
615 *   }
616 *
617 *   const permittedUri = permissions.directoryUri;
618 *   // Checks if users selected the correct folder
619 *   if (!permittedUri.includes(albumName)) {
620 *     return;
621 *   }
622 *
623 *   const mediaLibraryPermissions = await MediaLibrary.requestPermissionsAsync();
624 *   if (!mediaLibraryPermissions.granted) {
625 *     return;
626 *   }
627 *
628 *   // Moves files from external storage to internal storage
629 *   await StorageAccessFramework.moveAsync({
630 *     from: permittedUri,
631 *     to: FileSystem.documentDirectory!,
632 *   });
633 *
634 *   const outputDir = FileSystem.documentDirectory! + albumName;
635 *   const migratedFiles = await FileSystem.readDirectoryAsync(outputDir);
636 *
637 *   // Creates assets from local files
638 *   const [newAlbumCreator, ...assets] = await Promise.all(
639 *     migratedFiles.map<Promise<MediaLibrary.Asset>>(
640 *       async fileName => await MediaLibrary.createAssetAsync(outputDir + '/' + fileName)
641 *     )
642 *   );
643 *
644 *   // Album was empty
645 *   if (!newAlbumCreator) {
646 *     return;
647 *   }
648 *
649 *   // Creates a new album in the scoped directory
650 *   const newAlbum = await MediaLibrary.createAlbumAsync(albumName, newAlbumCreator, false);
651 *   if (assets.length) {
652 *     await MediaLibrary.addAssetsToAlbumAsync(assets, newAlbum, false);
653 *   }
654 * }
655 * ```
656 * @platform Android
657 */
658export namespace StorageAccessFramework {
659  /**
660   * Gets a [SAF URI](#saf-uri) pointing to a folder in the Android root directory. You can use this function to get URI for
661   * `StorageAccessFramework.requestDirectoryPermissionsAsync()` when you trying to migrate an album. In that case, the name of the album is the folder name.
662   * @param folderName The name of the folder which is located in the Android root directory.
663   * @return Returns a [SAF URI](#saf-uri) to a folder.
664   */
665  export function getUriForDirectoryInRoot(folderName: string) {
666    return `content://com.android.externalstorage.documents/tree/primary:${folderName}/document/primary:${folderName}`;
667  }
668
669  /**
670   * Allows users to select a specific directory, granting your app access to all of the files and sub-directories within that directory.
671   * @param initialFileUrl The [SAF URI](#saf-uri) of the directory that the file picker should display when it first loads.
672   * If URI is incorrect or points to a non-existing folder, it's ignored.
673   * @platform android 11+
674   * @return Returns a Promise that resolves to `FileSystemRequestDirectoryPermissionsResult` object.
675   */
676  export async function requestDirectoryPermissionsAsync(
677    initialFileUrl: string | null = null
678  ): Promise<FileSystemRequestDirectoryPermissionsResult> {
679    if (!ExponentFileSystem.requestDirectoryPermissionsAsync) {
680      throw new UnavailabilityError(
681        'expo-file-system',
682        'StorageAccessFramework.requestDirectoryPermissionsAsync'
683      );
684    }
685
686    return await ExponentFileSystem.requestDirectoryPermissionsAsync(initialFileUrl);
687  }
688
689  /**
690   * Enumerate the contents of a directory.
691   * @param dirUri [SAF](#saf-uri) URI to the directory.
692   * @return A Promise that resolves to an array of strings, each containing the full [SAF URI](#saf-uri) of a file or directory contained in the directory at `fileUri`.
693   */
694  export async function readDirectoryAsync(dirUri: string): Promise<string[]> {
695    if (!ExponentFileSystem.readSAFDirectoryAsync) {
696      throw new UnavailabilityError(
697        'expo-file-system',
698        'StorageAccessFramework.readDirectoryAsync'
699      );
700    }
701    return await ExponentFileSystem.readSAFDirectoryAsync(dirUri, {});
702  }
703
704  /**
705   * Creates a new empty directory.
706   * @param parentUri The [SAF](#saf-uri) URI to the parent directory.
707   * @param dirName The name of new directory.
708   * @return A Promise that resolves to a [SAF URI](#saf-uri) to the created directory.
709   */
710  export async function makeDirectoryAsync(parentUri: string, dirName: string): Promise<string> {
711    if (!ExponentFileSystem.makeSAFDirectoryAsync) {
712      throw new UnavailabilityError(
713        'expo-file-system',
714        'StorageAccessFramework.makeDirectoryAsync'
715      );
716    }
717    return await ExponentFileSystem.makeSAFDirectoryAsync(parentUri, dirName);
718  }
719
720  /**
721   * Creates a new empty file.
722   * @param parentUri The [SAF](#saf-uri) URI to the parent directory.
723   * @param fileName The name of new file **without the extension**.
724   * @param mimeType The MIME type of new file.
725   * @return A Promise that resolves to a [SAF URI](#saf-uri) to the created file.
726   */
727  export async function createFileAsync(
728    parentUri: string,
729    fileName: string,
730    mimeType: string
731  ): Promise<string> {
732    if (!ExponentFileSystem.createSAFFileAsync) {
733      throw new UnavailabilityError('expo-file-system', 'StorageAccessFramework.createFileAsync');
734    }
735    return await ExponentFileSystem.createSAFFileAsync(parentUri, fileName, mimeType);
736  }
737
738  /**
739   * Alias for [`writeAsStringAsync`](#filesystemwriteasstringasyncfileuri-contents-options) method.
740   */
741  export const writeAsStringAsync = baseWriteAsStringAsync;
742  /**
743   * Alias for [`readAsStringAsync`](#filesystemreadasstringasyncfileuri-options) method.
744   */
745  export const readAsStringAsync = baseReadAsStringAsync;
746  /**
747   * Alias for [`deleteAsync`](#filesystemdeleteasyncfileuri-options) method.
748   */
749  export const deleteAsync = baseDeleteAsync;
750  /**
751   * Alias for [`moveAsync`](#filesystemmoveasyncoptions) method.
752   */
753  export const moveAsync = baseMoveAsync;
754  /**
755   * Alias fro [`copyAsync`](#filesystemcopyasyncoptions) method.
756   */
757  export const copyAsync = baseCopyAsync;
758}
759