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 |
Installation
Section titled “Installation”npm install @symbiote-native/file-systemScaffolding 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?): IFileSystemWatchSubscriptionDirectory
Section titled “Directory”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?): IFileSystemWatchSubscriptionPaths.cache / Paths.document / Paths.bundle: DirectoryPaths.appleSharedContainers: Record<string, Directory> // ios onlyPaths.totalDiskSpace / Paths.availableDiskSpace: numberPaths.info(...uris): IPathInfoPaths.join / .relative / .isAbsolute / .normalize / .dirname / .basename / .extname / .parseUploadTask / DownloadTask
Section titled “UploadTask / DownloadTask”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 restartstatic fromSavable(savable): DownloadTask.state: IFileSystemDownloadTaskState // + pausedErrors
Section titled “Errors”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 |
Common questions
Section titled “Common questions”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.
Legacy API (/legacy)
Section titled “Legacy API (/legacy)”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();Legacy API reference
Section titled “Legacy API reference”documentDirectory / cacheDirectory / bundleDirectory: string | null // trailing-slash-normalizedgetInfoAsync(fileUri, options?): Promise<IFileSystemFileInfo>readAsStringAsync(fileUri, options?): Promise<string>writeAsStringAsync(fileUri, contents, options?): Promise<void>getContentUriAsync(fileUri): Promise<string> // android; echoes input on iosdeleteAsync(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> // bytesdownloadAsync(uri, fileUri, options?): Promise<IFileSystemDownloadResult>uploadAsync(url, fileUri, options?): Promise<IFileSystemUploadResult>createDownloadResumable(uri, fileUri, options?, callback?, resumeData?): DownloadResumablecreateUploadTask(url, fileUri, options?, callback?): UploadTaskStorageAccessFramework.{ getUriForDirectoryInRoot, requestDirectoryPermissionsAsync, readDirectoryAsync, makeDirectoryAsync, createFileAsync, writeAsStringAsync, readAsStringAsync, deleteAsync, moveAsync, copyAsync } // androidFileSystemSessionType, FileSystemUploadType, FileSystemEncodingType // runtime enumsDownloadResumable carries cancelAsync/pauseAsync/resumeAsync/savable(); UploadTask
carries cancelAsync/uploadAsync. Both report progress through the callback passed at
construction.
Legacy notes
Section titled “Legacy notes”StorageAccessFrameworkis Android-only — every function throwsUnavailabilityErroron iOS; use the ordinarydocument/cacheDirectoryfunctions there instead.getContentUriAsyncis Android-only — on iOS it resolves to the inputfileUriunchanged 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.