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