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