Overview
This guide shows how to move and copy files and folders in an Expo (React Native) app using expo-file-system. You will learn how to:
- Work inside your app’s sandbox (document and cache directories)
- Copy or move individual files
- Copy entire folders
- Verify results and handle errors
All examples target managed Expo projects, but APIs are the same in bare apps.
Quickstart
- Install the dependency
- Run:
npx expo install expo-file-system
- Import and pick directories
- Use FileSystem.documentDirectory for persistent storage
- Use FileSystem.cacheDirectory for temporary data
- Copy or move files/folders
- copyAsync({ from, to }) to duplicate
- moveAsync({ from, to }) to rename/relocate
- Verify
- readDirectoryAsync and getInfoAsync help confirm results
Minimal working example (copy and move)
import React, { useState } from 'react';
import { Button, Text, View, StyleSheet } from 'react-native';
import * as FileSystem from 'expo-file-system';
const root = FileSystem.documentDirectory!; // e.g., file:///data/... or file:///var/...
export default function App() {
const [log, setLog] = useState<string>('');
const append = (line: string) => setLog(prev => prev + (prev ? '\n' : '') + line);
const run = async () => {
try {
append('Preparing directories...');
const srcDir = root + 'demo/';
const dstDir = root + 'backup/';
const copyDir = root + 'demo-copy/';
// Ensure directories exist
await FileSystem.makeDirectoryAsync(srcDir, { intermediates: true });
await FileSystem.makeDirectoryAsync(dstDir, { intermediates: true });
// Create a source file
const srcFile = srcDir + 'note.txt';
await FileSystem.writeAsStringAsync(srcFile, 'Hello from Expo!');
append(`Created: ${srcFile}`);
// Copy the file into backup
const copiedFile = dstDir + 'note.txt';
await removeIfExists(copiedFile);
await FileSystem.copyAsync({ from: srcFile, to: copiedFile });
append(`Copied file to: ${copiedFile}`);
// Move/rename the original file
const movedFile = srcDir + 'note-moved.txt';
await removeIfExists(movedFile);
await FileSystem.moveAsync({ from: srcFile, to: movedFile });
append(`Moved file to: ${movedFile}`);
// Copy the entire folder (recursive)
await removeIfExists(copyDir);
await FileSystem.copyAsync({ from: srcDir, to: copyDir });
append(`Copied folder to: ${copyDir}`);
// Verify results
const srcList = await FileSystem.readDirectoryAsync(srcDir);
const dstList = await FileSystem.readDirectoryAsync(dstDir);
const copyList = await FileSystem.readDirectoryAsync(copyDir);
append('Src contents: ' + JSON.stringify(srcList));
append('Backup contents: ' + JSON.stringify(dstList));
append('Demo-copy contents: ' + JSON.stringify(copyList));
} catch (e: any) {
append('Error: ' + e?.message);
}
};
return (
<View style={styles.container}>
<Button title="Run file ops" onPress={run} />
<Text style={styles.log}>{log}</Text>
</View>
);
}
async function removeIfExists(uri: string) {
const info = await FileSystem.getInfoAsync(uri);
if (info.exists) {
await FileSystem.deleteAsync(uri, { idempotent: true });
}
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, paddingTop: 64 },
log: { marginTop: 12 },
});
What it does:
- Creates a demo directory and note.txt
- Copies note.txt to backup/
- Renames/moves the original file
- Recursively copies demo/ to demo-copy/
- Lists contents to verify
Copy a whole folder (with optional fine-grained control)
copyAsync supports copying directories recursively. If you need custom filtering or progress, implement a manual walk:
import * as FileSystem from 'expo-file-system';
export async function copyDirRecursive(fromDir: string, toDir: string) {
const info = await FileSystem.getInfoAsync(fromDir);
if (!info.exists || !info.isDirectory) throw new Error('Source is not a directory');
await FileSystem.makeDirectoryAsync(toDir, { intermediates: true });
const entries = await FileSystem.readDirectoryAsync(fromDir);
for (const name of entries) {
const from = fromDir + name;
const to = toDir + name;
const child = await FileSystem.getInfoAsync(from);
if (child.isDirectory) {
await copyDirRecursive(from + '/', to + '/');
} else {
await FileSystem.copyAsync({ from, to });
}
}
}
Use this when you need to skip files, rename during copy, or implement conflict rules.
Key APIs you’ll use
- FileSystem.documentDirectory: persistent app sandbox
- FileSystem.cacheDirectory: temporary storage (system may wipe)
- FileSystem.copyAsync({ from, to })
- FileSystem.moveAsync({ from, to })
- FileSystem.makeDirectoryAsync(uri)
- FileSystem.readDirectoryAsync(uri), FileSystem.getInfoAsync(uri)
- FileSystem.deleteAsync(uri)
Platform and permission notes
- App sandbox only by default
- iOS and Android allow free read/write in your app’s documentDirectory and cacheDirectory without extra permissions.
- External/shared storage (Android)
- To work outside the sandbox, use StorageAccessFramework to request a user-selected directory. Direct paths generally won’t work on modern Android.
- iOS bundle to sandbox
- To copy bundled assets, use expo-asset: download the asset, then copy from asset.localUri into documentDirectory.
- Web
- expo-file-system has limited/no support on web. Prefer platform checks before calling APIs.
Common pitfalls and how to avoid them
- Destination already exists
- copyAsync/moveAsync may fail if the destination exists. Delete first or generate a unique name.
- Parent directory missing
- Ensure the destination’s parent directory exists (makeDirectoryAsync with intermediates: true).
- Mixing string paths and URIs
- Use the file:// URIs that expo-file-system returns (e.g., documentDirectory). Don’t assume OS-native path separators.
- Trailing slash consistency
- For directories, keep a trailing slash when building nested paths to avoid accidental name concatenation issues.
- Large reads into memory
- Avoid reading whole files as strings/base64 unless necessary. Prefer streaming where possible (e.g., downloadAsync) or work with files directly.
- Moving across volumes
- moveAsync typically works within the same storage area. If crossing areas fails, copy then delete.
Performance notes
- Prefer move over copy for renames; move is O(1) when staying in the same volume.
- Batch operations
- When copying many small files, queue a reasonable number at once (e.g., Promise.all with chunks of 5–10) to reduce overhead without overwhelming the bridge.
- Avoid unnecessary JSON/base64
- Keep operations at the file level; converting to base64 inflates memory and CPU usage.
- Use cacheDirectory for transient data
- Faster lifecycle and safe to purge; use documentDirectory for user data that must persist.
- Measure
- Time your operations on device builds; the emulator/simulator can be slower or behave differently with I/O.
Tiny FAQ
How do I copy from the app bundle to documentDirectory?
- Load the asset with expo-asset (Asset.fromModule(require(...))). Call downloadAsync() and then copyAsync from asset.localUri.
Do I need permissions to copy/move inside documentDirectory?
- No. Your app has full access to its sandbox on iOS and Android.
How can I access Downloads or shared folders on Android?
- Use StorageAccessFramework to request user access to a directory, then read/write through the provided URIs.
Can I copy directories recursively?
- Yes. copyAsync supports directories. For filtering or custom logic, implement a recursive copy like in the example above.
Is this supported on web?
- Not fully. Guard calls with Platform.OS checks or provide web-specific alternatives.