How to Save Files to a Device Folder using Expo and React-Native

tl;dr I wrote a library that takes care of this whole process with one function call. It works with both managed Expo apps and regular React Native apps, you can see a demo below and installation instructions here: expo-file-dl

Sections

Demo

This is the functionality we’re trying to achieve. All the code for this demo is available in the expo-file-dl and expo-file-dl-example repos.

Introduction

When looking through the plethora of Expo libraries, it’s often difficult to find exactly the ones you need. To save a file to a folder on the device’s internal storage, one might think that the downloadAsync function from expo-file-system would do the trick, it even let’s you specify the name of the local file you wish to download the content to!

But alas, this isn’t the full answer. Expo apps, like most mobile apps have a file storage that’s wholly their own and difficult to access from, say, a file manager app (think Amaze File Manager) or a media library app (think Google Photos). And it’s that inaccesible storage where downloadAsync stores your files. To save a file on a publicly accessible folder in the device’s internal storage, you need to do a bit more.

Setup

You need to install two libraries to follow along here. These are expo libraries, but you can use them in plain react-native apps as well. Just be sure to follow the installation instructions for “bare” or plain react-native apps.

  • expo-file-system ( docs)
  • expo-media-library ( docs)

After you’ve done that we can proceed.

Downloading the file

To download a remote file’s content to a local file on the device, here’s the code:

import * as FileSystem from 'expo-file-system';

const fileUri: string = `${FileSystem.documentDirectory}${filename}`;
const downloadedFile: FileSystem.FileSystemDownloadResult = await FileSystem.downloadAsync(uri, fileUri);

if (downloadedFile.status != 200) {
  handleError();
}

A bit hidden in the Expo FileSystem docs is this note

Each app only has read and write access to locations under the following directories: FileSystem.documentDirectory and FileSystem.cacheDirectory

The former is more permanent, so when creating our file path to download the remote content to, we use that one.

After this code executes, we’ll have the content downloaded into a folder only our app has access to. Next, we’ll transfer this file to a publicly accessible folder. But it’s not as simple as mv dir1/file1 dir2/!

Moving the file to a publicly accessible folder

on iOS

The process described below for Android will also work on iOS devices, but it will only work if the file being downloaded is an image file (.jpg, .png, .heic, etc.)

For audio files, video files, document files, you’ll need to take a different approach.

The best way is to use the expo-sharing library ( docs), which exposes different ways of dealing with different types of files.

iOS has a concept called “Universal Type Identifiers”, which are special tags attached to files to tell the OS what can be done with said files. So, for example, a file with a UTI of “public.video” can be opened by any video player app on iOS. You can tag files with a UTI of your choosing. The UTI “public.item” is a generic type that tells iOS that the file is some type of file, which can be saved to the user’s filesystem. It doesn’t tell iOS that the file can be opened in any media players or anything like this, so we’ll use the “public.item” UTI to tag the file when we expose a way of dealing with the file to iOS.

With all this in mind, here’s how we’ll handle non-image file downloads on iOS:

import * as Sharing from "expo-sharing";

const imageFileExts = ['jpg', 'png', 'gif', 'heic', 'webp', 'bmp'];

if (isIos && imageFileExts.every(x => !downloadedFile.uri.endsWith(x))) {
  const UTI = 'public.item';
  const shareResult = await Sharing.shareAsync(downloadedFile.uri, {UTI});
}

The above will open a prompt to the user after any non-image file has downloaded. The prompt will ask the user what the user would like to do with the file. At that time the user has the option to select “Save to Files”. If this is chosen, the file will be visible in the user’s Files app.

Image files on iOS can be handled the same way as all files are handled in Android below. Remember that image files on iOS will download straight to the user’s Photo Library and be visible in the user’s Photos app. Non-image files will download to the Files app.

on Android

To move a file from the app-controlled FileSystem.documentDirectory to a publically accessible folder, you’ll need to do the following:

  • get MEDIA_LIBRARY permissions from the user
    • (I’m using the expo-permissions library here to make it easy, you don’t have to)
  • use the expo-media-library to convert the local file to an “asset”
  • find the “album” (aka folder) where you want to transfer the “asset” to, or create the “album” if it doesn’t exist
  • move the “asset” to the “album”

Here’s the code to do that

import * as Permissions from 'expo-permissions';
import * as MediaLibrary from 'expo-media-library';

const perm = await Permissions.askAsync(Permissions.MEDIA_LIBRARY);
if (perm.status != 'granted') {
  return;
}

try {
  const asset = await MediaLibrary.createAssetAsync(downloadedFile.uri);
  const album = await MediaLibrary.getAlbumAsync('Download');
  if (album == null) {
    await MediaLibrary.createAlbumAsync('Download', asset, false);
  } else {
    await MediaLibrary.addAssetsToAlbumAsync([asset], album, false);
  }
} catch (e) {
  handleError(e);
}

Notice that you need to check if the album exists before calling addAssetsToAlbumAsync. If the album doesn’t exist, you call createAlbumAsync instead and pass the asset along with the name of the album to create as arguments to the function. The album will be created and the asset will be added, no need to call addAssetsToAlbumAsync after that.

Conclusion

That’s it. Now you should see your file in the ‘Download’ folder on the device.

But this only takes care of downloading the files. Anyone using your app won’t have any idea if the download started, if it failed, or if it’s still ongoing. To notify app users about the download status of the file, take a look at the expo-file-dl library, which implements download status notifications as an optional feature.

Farhan Kathawala
Farhan Kathawala
Full Stack Web / React Native Developer

A happy full stack developer sharing tidbits of everything and anything web / mobile app dev.

Related