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

The method described in this article for downloading files to publicly shared folders supports all media types on Android and only images on iOS. To download audio/video/document files on iOS you can use the method I outlined here: https://forums.expo.io/t/save-a-file-to-device-folder-like-download/45238/6?u=farhansayshi

I’m working on adding this to the expo-file-dl package at the moment.

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

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

  • get CAMERA_ROLL 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.CAMERA_ROLL);
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