Skip to content

File system

Read, write, copy, move and download files on the device. @symbiote-native/file-system wraps expo-file-system for every SymbioteNative adapter and ports both of upstream’s surfaces: the default export is the modern File/Directory/Paths API built on JSI shared objects; the legacy function-based API (read/ write/copy/move/delete, directory listing, disk-space queries, resumable download/upload, Android Storage Access Framework) lives at the /legacy subpath.

Which directory? Paths.document is for data the user expects to keep. Paths.cache is for anything you can re-create, and the OS may clear it when storage runs low.

OS platform Support
iOS ✅ live
Android ✅ live
Framework adapter Support
React ✅ live
Vue ✅ live
Angular ✅ live
Svelte ✅ live
Solid ✅ live
Terminal window
npm install @symbiote-native/file-system

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --file-system (or add --file-system in an existing app) installs and wires this for you — see @symbiote-native/cli.

expo-file-system and expo-modules-core come along as regular dependencies, pinned to exact versions — never install them yourself, and never add the expo meta-package to your project.

import { File, Directory, Paths } from '@symbiote-native/file-system';
const file = new File(Paths.document, 'notes.txt');
file.write('hello world');
const contents = file.text();
const dir = new Directory(Paths.document, 'photos');
dir.create();
const entries = dir.list();
const downloaded = await File.downloadFileAsync('https://example.com/big-file.zip', Paths.cache, {
onProgress: ({ bytesWritten, totalBytes }) => console.log(bytesWritten / totalBytes),
});

Identical import surface on every adapter — @symbiote-native/file-system/react, /vue, /svelte, /solid, /angular all re-export the same classes.

new File(...uris: (string | File | Directory)[])
static downloadFileAsync(url, destination, options?): Promise<File>
static pickFileAsync(options?): Promise<IPickSingleFileResult | IPickMultipleFilesResult>
static createDownloadTask(url, destination, options?): DownloadTask
.uri / .exists / .size / .md5 / .type / .lastModified / .creationTime / .parentDirectory /
.extension / .name
.write(contents, options?) / .text() / .bytes() / .base64() // + Sync variants
.create(options?) / .delete() / .copy(dest) / .move(dest) // + Sync variants
.open(mode) -> IFileSystemHandle
.readableStream() / .writableStream() / .stream() / .arrayBuffer() / .json() / .formData()
.slice(start?, end?, contentType?): Blob
.upload(url, options?): Promise<IFileSystemUploadResult>
.createUploadTask(url, options?): UploadTask
.watch(callback, options?): IFileSystemWatchSubscription
new Directory(...uris: (string | File | Directory)[])
static pickDirectoryAsync(initialUri?): Promise<Directory>
.uri / .exists / .size / .parentDirectory / .name
.create(options?) / .delete() / .copy(dest) / .move(dest)
.list(): (File | Directory)[]
.createFile(name, options?): File
.createDirectory(name, options?): Directory
.watch(callback, options?): IFileSystemWatchSubscription
Paths.cache / Paths.document / Paths.bundle: Directory
Paths.appleSharedContainers: Record<string, Directory> // ios only
Paths.totalDiskSpace / Paths.availableDiskSpace: number
Paths.info(...uris): IPathInfo
Paths.join / .relative / .isAbsolute / .normalize / .dirname / .basename / .extname / .parse

State machines over a network transfer, both cancellable via AbortSignal:

new UploadTask(file, url, options?)
.uploadAsync(): Promise<IFileSystemUploadResult>
.state: IFileSystemUploadTaskState // idle/active/completed/cancelled/error
new DownloadTask(url, destination, options?)
.downloadAsync(): Promise<File>
.pause() / .resume()
.savable(): IFileSystemDownloadPauseState // resume after app restart
static fromSavable(savable): DownloadTask
.state: IFileSystemDownloadTaskState // + paused

Every native exception surfaces as an ordinary thrown Error or rejected Promise — no custom error-class hierarchy.

Trigger When
Constructing a File/Directory with an invalid/empty path Throws synchronously in the constructor
Reading/writing through a stale or already-closed handle IFileSystemHandle methods throw
pickFileAsync/pickDirectoryAsync cancelled by the user Rejects AbortError, or resolves { canceled: true }
upload()/createUploadTask()/createDownloadTask() aborted via options.signal Rejects AbortError
.copy()/.move() onto an existing destination without overwrite: true Native exception — destination already exists
Any operation on a path outside the app sandbox without permission Native exception — permission/sandbox violation

I downloaded a file. Where is it, and why can’t the user see it in Files or the Gallery? File.downloadFileAsync writes into the app’s private sandbox, which other apps and the file manager cannot browse. To put a photo or video in the Gallery, save it with the media library package. To let the user keep or send a file, open the share sheet with sharing. On Android you can also ask the user to pick a folder with the legacy StorageAccessFramework and write there.

“Permission denied” or an exception when writing outside the app’s folders. The classes work inside Paths.document and Paths.cache. Anything else is outside the sandbox. On Android, use the Storage Access Framework, which prompts the user to pick a directory; no storage permission is needed for the app’s own folders.

Should I use Paths.document or Paths.cache? Paths.document for data the user expects to keep. Paths.cache for anything you can re-create. The OS may clear the cache when space runs low, and it is also your job to delete files you no longer need there.

Does file.create() overwrite? No. It can throw if the file already exists or you cannot create it. Use file.write() to replace the contents of a file you own.

Why are there two APIs? The class API (File, Directory, Paths) is upstream’s default. The function API is kept at /legacy for code that already uses it.

Sources: Expo forums: unable to download file in expected location, expo/expo#20298 requestDirectoryPermissionsAsync opens a file dialog, Expo docs: FileSystem (legacy), DEV: expo-file-system cacheDirectory has to be cleaned.

Upstream’s original function-based surface — plain async functions over expo-modules-core instead of JSI shared objects, behind a separate native module registration inside the same expo-file-system dependency, so nothing extra to install.

import {
documentDirectory,
writeAsStringAsync,
readAsStringAsync,
downloadAsync,
createDownloadResumable,
} from '@symbiote-native/file-system/legacy';
const fileUri = `${documentDirectory}notes.txt`;
await writeAsStringAsync(fileUri, 'hello world');
const contents = await readAsStringAsync(fileUri);
const resumable = createDownloadResumable(
'https://example.com/big-file.zip',
`${documentDirectory}big-file.zip`,
undefined,
({ totalBytesWritten, totalBytesExpectedToWrite }) => {
console.log(totalBytesWritten / totalBytesExpectedToWrite);
},
);
await resumable.downloadAsync();
documentDirectory / cacheDirectory / bundleDirectory: string | null // trailing-slash-normalized
getInfoAsync(fileUri, options?): Promise<IFileSystemFileInfo>
readAsStringAsync(fileUri, options?): Promise<string>
writeAsStringAsync(fileUri, contents, options?): Promise<void>
getContentUriAsync(fileUri): Promise<string> // android; echoes input on ios
deleteAsync(fileUri, options?): Promise<void>
moveAsync(options) / copyAsync(options): Promise<void> // { from, to }
makeDirectoryAsync(fileUri, options?): Promise<void>
readDirectoryAsync(fileUri): Promise<string[]>
getFreeDiskStorageAsync() / getTotalDiskCapacityAsync(): Promise<number> // bytes
downloadAsync(uri, fileUri, options?): Promise<IFileSystemDownloadResult>
uploadAsync(url, fileUri, options?): Promise<IFileSystemUploadResult>
createDownloadResumable(uri, fileUri, options?, callback?, resumeData?): DownloadResumable
createUploadTask(url, fileUri, options?, callback?): UploadTask
StorageAccessFramework.{ getUriForDirectoryInRoot, requestDirectoryPermissionsAsync,
readDirectoryAsync, makeDirectoryAsync, createFileAsync,
writeAsStringAsync, readAsStringAsync, deleteAsync, moveAsync, copyAsync } // android
FileSystemSessionType, FileSystemUploadType, FileSystemEncodingType // runtime enums

DownloadResumable carries cancelAsync/pauseAsync/resumeAsync/savable(); UploadTask carries cancelAsync/uploadAsync. Both report progress through the callback passed at construction.

  • StorageAccessFramework is Android-only — every function throws UnavailabilityError on iOS; use the ordinary document/cacheDirectory functions there instead.
  • getContentUriAsync is Android-only — on iOS it resolves to the input fileUri unchanged rather than throwing.
  • Progress callbacks fire only while a task is in flight — the native event listener is added for the duration of the call and removed the moment the promise settles.