A project committed to making file access and data transfer easier and more efficient for React Native developers.
This project is a fork of https://www.npmjs.com/package/rn-fetch-blob which on the other hand is a fork of https://github.com/wkh237/react-native-fetch-blob. Both the original repository and its first fork are not maintained anymore.
The project will be continued in this repository. If you want to support the project feel free to contact me or create a pull request with your feature.
react-native-blob-util version 1.0.1 and up supports the New Architecture only and is only compatible with react native 0.84 and up (Android minSdk 24, iOS 15.1). 1.0 also changes the JavaScript API: see Migration.md for what was removed, renamed or made consistent across platforms. (1.0.0 was published by mistake from an unfinished state and is deprecated; do not install it.)
react-native-blob-util version 0.22.0 and up is only compatible with react native 0.76 and up. "0.22.0" -> 0.76 RN "0.22.1" -> 0.77 RN "0.22.2" -> 0.78 RN
react-native-blob-util version 0.17.0 and up is only compatible with react native 0.65 and up.
react-native-blob-util version 0.10.16 and up is only compatible with react native 0.60 and up.
0.25 is the last release that also supports the Old Architecture. From 1.0 the native modules are written in Kotlin (Android), Swift (iOS) and C++/WinRT (Windows). More on the New Architecture: https://reactnative.dev/architecture/landing-page
- Download straight to a file and upload straight from a file, without passing the data through JS as base64
- Upload and download progress, cancellation, multipart forms
- A file system API that works on files natively: read, write, copy, move, hash, slice, stream
- Access to Android's MediaStore (Downloads, Pictures, Music, Movies) under scoped storage
- Open a file in another app, or let the user pick one
- Custom CA certificates per request
- Android, iOS and Windows
This README and index.d.ts are the reference for the 1.0 API. The wiki describes the API before 1.0.
- Installation
- Usage
- Coming from 0.x
- Requests
- File System
- Setting A File Transformer
- Opening and picking files
- Android media storage
- Errors
- Performance Tips
- Caveats
- Development
npm install --save react-native-blob-utiliOS: run pod install from the ios directory. CocoaPods is the only supported way to add the library.
cd ios; pod install; cd ..Android: autolinking picks the library up; there is nothing to link by hand. The library uses the OkHttp that ships with React Native (or the one your app uses).
Expo: the library works with a development build (npx expo prebuild), not in Expo Go. Its config plugin is only needed for custom CA certificates.
You can also install a branch straight from GitHub:
npm install --save github:RonRadtke/react-native-blob-util#<branch_name>The library's manifest already declares INTERNET, ACCESS_NETWORK_STATE (used by wifiOnly),
READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE and DOWNLOAD_WITHOUT_NOTIFICATION; they are
merged into your app. The DownloadManager completion receiver is registered at runtime, so your
manifest needs no DOWNLOAD_COMPLETE intent filter.
Storage permissions are granted at runtime. Your app's own directories (fs.dirs.DocumentDir,
CacheDir, and the app-specific DownloadDir, PictureDir and so on) need no permission. To write
to shared storage on Android 9 and lower (the Legacy*Dir directories), request
WRITE_EXTERNAL_STORAGE with PermissionsAndroid.
On Android 10 and up, use the media API instead.
The default export holds everything:
import ReactNativeBlobUtil from 'react-native-blob-util';
const res = await ReactNativeBlobUtil.fetch('GET', 'https://example.com/data.json');
const exists = await ReactNativeBlobUtil.fs.exists(ReactNativeBlobUtil.fs.dirs.DocumentDir + '/notes.txt');The same objects are also named exports:
import {fetch, config, fs, open, media, wrap, session, base64} from 'react-native-blob-util';
const res = await fetch('GET', 'https://example.com/data.json');
const exists = await fs.exists(fs.dirs.DocumentDir + '/notes.txt');CanceledFetchError, FetchBlobResponse, URIUtil and getUUID are named exports as well.
The API is grouped by what a call does:
| Namespace | For |
|---|---|
fetch, config |
HTTP requests |
fs |
files and directories |
open |
showing a file in another app, or letting the user pick one |
media |
Android's MediaStore, Downloads app and media scanner |
A call a platform cannot make rejects with ENOTSUP.
Migration.md lists everything 1.0 changes: removed APIs (the Web API polyfills,
JSONStream, response.blob()), renamed calls, values that are now the same on every platform,
error codes, request body rules and the Android content:// security fix.
The old names keep working for now and print one deprecation warning each:
- the
android,iosandMediaCollectionnamespaces (useopen.*,media.*andfs.*); fs.readFileWithTransformandfs.writeFileWithTransform(usereadFile/writeFilewith{transform: true});fs.lstat,fs.scanFile,fs.pathForAppGroup,fs.syncPathAppGroup;- the config keys
transformFile,addAndroidDownloads,wifiOnly,targetHostIpandIOSBackgroundTask.
Requests run in native code. The response can be held in memory or written straight to a file.
import {fetch} from 'react-native-blob-util';
async function loadItems() {
try {
const res = await fetch('GET', 'https://example.com/api/items', {
Authorization: 'Bearer access-token',
});
if (!res.ok) {
console.warn('Server answered', res.status);
return null;
}
return await res.json();
} catch (err) {
// The request itself failed: no connection, timeout, TLS error, cancelled ...
console.warn(err.code, err.message);
return null;
}
}An HTTP error status (404, 500, ...) resolves like any other response; check res.ok or
res.status. The promise only rejects when the request fails. See Errors.
Cookies set by other requests in the app (React Native's fetch, axios) are sent along.
| Member | What it is |
|---|---|
res.status |
the HTTP status |
res.ok |
true for a status of 200..299 |
res.headers |
the response headers, names in lower case |
res.url |
the URL the body came from, after redirects; undefined when unknown |
res.info() |
the full response info: status, headers, redirects, respType, ... |
res.text() |
the body as text (UTF-8) |
res.json() |
the body parsed as JSON |
res.base64() |
the body as a base64 string |
res.array() |
the body as byte values 0..255 |
res.arrayBuffer() |
the body as an ArrayBuffer |
res.path() |
the path of the response file, or null when the body is in memory |
res.flush() |
removes the response file; resolves at once when there is none |
res.readFile(encoding), res.readStream(encoding) |
read the response file; reject EINVAL when the body is not a file |
res.session(name) |
adds the response file to a session; throws EINVAL when the body is not a file |
text(), json(), base64(), array(), arrayBuffer() and flush() always return a Promise,
whether the body is in memory or in a file.
A large response should not pass through JS. With fileCache: true it is written to a file with
a random name in the cache directory; appendExt gives that file an extension. With path it is
written where you say.
import {config, fs} from 'react-native-blob-util';
const res = await config({
path: fs.dirs.DocumentDir + '/report.pdf',
}).fetch('GET', 'https://example.com/report.pdf', {
Authorization: 'Bearer access-token',
});
if (!res.ok) {
// The error page was written to the file; don't keep it.
await res.flush();
throw new Error(`Download failed with status ${res.status}`);
}
console.log('Saved to', res.path());Always check res.ok before using a downloaded file: an error status still resolves, and the
file then holds the server's error body.
import {Image} from 'react-native';
import {config} from 'react-native-blob-util';
const res = await config({fileCache: true, appendExt: 'png'})
.fetch('GET', 'https://example.com/image.png');
// An Image source needs the file:// prefix on Android; iOS accepts it as well.
const image = <Image source={{uri: 'file://' + res.path()}} />;Files written by fileCache or path are not removed automatically. See
Cache file management.
Use a file transformer: with transform: true the registered
file transformer runs on the response before it is written to disk
(for example to encrypt it). It only applies when the response is written to a file.
const res = await config({
path: fs.dirs.DocumentDir + '/secret.bin',
transform: true,
}).fetch('GET', 'https://example.com/secret.bin');Say what a body is with one of the explicit forms. They are never guessed from anything:
| Body | Sent as |
|---|---|
{text: string} |
the string, as it is |
{base64: string} |
the bytes the base64 string encodes |
{file: path} |
the contents of a file: a path, or a content:// URI on Android |
an ArrayBuffer or a typed array (Uint8Array, ...) |
the bytes |
| an array of fields | a multipart form |
import {fetch, fs} from 'react-native-blob-util';
// JSON, or any string that comes from a user or a server
await fetch('POST', 'https://example.com/api/notes', {
'Content-Type': 'application/json',
}, {text: JSON.stringify({title: 'Hello'})});
// Binary data you hold as base64
await fetch('POST', 'https://content.dropboxapi.com/2/files/upload', {
Authorization: 'Bearer access-token',
'Dropbox-API-Arg': JSON.stringify({path: '/img-from-react-native.png', mode: 'add', autorename: true, mute: false}),
'Content-Type': 'application/octet-stream',
}, {base64: base64ImageString});
// A file, streamed from storage
await fetch('PUT', 'https://example.com/upload/video.mp4', {
'Content-Type': 'video/mp4',
}, {file: fs.dirs.DocumentDir + '/video.mp4'});
// Bytes
await fetch('POST', 'https://example.com/api/blob', {
'Content-Type': 'application/octet-stream',
}, new Uint8Array([0x89, 0x50, 0x4e, 0x47]));A plain string body is read by the rule 0.x used, the same way on every platform:
- a string made by
wrap(path)is a file (wrapworks for paths andcontent://URIs); - with a
Content-Typethat ends in;base64(removed before sending) or starts withapplication/octet, the string is base64; - anything else is text. Without a
Content-Typea string is text.
import {fetch, wrap} from 'react-native-blob-util';
await fetch('POST', 'https://example.com/upload', {
'Content-Type': 'application/octet-stream',
}, wrap(pathToFile));Prefer {text} for a string you did not write yourself: a plain string that happens to start with
the file prefix is uploaded as that file's contents.
Other rules:
GETandHEADreject a body withEINVAL.DELETEand the other methods send it.- A file body whose file does not exist rejects with
ENOENT. - A header name or value containing CR, LF or NUL rejects with
EINVAL.nullandundefinedheader values are sent as"".
Pass an array of fields as the body. The library sets Content-Type: multipart/form-data with
its boundary.
Each field has a name and data, and optionally a filename and a type (the part's MIME
type). data can use the explicit forms above. A plain string is text when the field has no
filename, and base64 (or a wrap(path) file) when it has one. A file part without a filename
is named after the file.
import {fetch, fs, wrap} from 'react-native-blob-util';
const res = await fetch('POST', 'https://example.com/upload-form', {
Authorization: 'Bearer access-token',
'Content-Type': 'multipart/form-data',
}, [
// text fields
{name: 'name', data: 'user'},
{name: 'info', data: {text: JSON.stringify({mail: 'example@example.com', tel: '12345678'})}},
// a file from storage, with its own MIME type
{name: 'avatar', filename: 'avatar.png', type: 'image/png', data: {file: fs.dirs.DocumentDir + '/avatar.png'}},
// a file from the app bundle
{name: 'ringtone', filename: 'ring.mp3', type: 'audio/mpeg', data: wrap(fs.asset('default-ringtone.mp3'))},
// binary data held as base64
{name: 'thumbnail', filename: 'thumb.jpg', type: 'image/jpeg', data: {base64: thumbnailBase64}},
]);A name or filename containing ", CR or LF is escaped as browsers do (%22, %0D, %0A); a
type containing CR or LF rejects with EINVAL.
progress reports the download, uploadProgress the upload. Both return the task, so they chain,
and both take an optional first argument {interval, count}:
interval: report at most every this many milliseconds. Default 250;0reports every chunk.count: report this many times in total. Default unlimited. Needs aContent-Lengthfrom the server.
import {fetch} from 'react-native-blob-util';
const res = await fetch('POST', 'https://example.com/upload', {
'Content-Type': 'application/octet-stream',
}, {file: pathToFile})
.uploadProgress({interval: 250}, (sent, total) => {
console.log('uploaded', sent / total);
})
.progress({count: 10}, (received, total) => {
console.log('downloaded', received / total);
});stateChange(fn) is called with the response info as soon as the headers arrive, before the body
is complete.
task.cancel() cancels the request. The task rejects right away with a CanceledFetchError
whose code is ECANCELED; the Promise cancel() returns resolves once native has cancelled.
Cancelling a finished task does nothing.
import {CanceledFetchError, fetch} from 'react-native-blob-util';
const task = fetch('GET', 'https://example.com/large-file');
task.then((res) => {
// ...
}).catch((err) => {
if (err.code === 'ECANCELED') {
return; // cancelled by us
}
console.warn(err);
});
// later
await task.cancel();err instanceof CanceledFetchError works as well. task.taskId is the id native knows the task by.
config(options) returns a fetch bound to those options:
import {config} from 'react-native-blob-util';
const res = await config({fileCache: true, timeout: 30000}).fetch('GET', url);| Option | Type | Description |
|---|---|---|
fileCache |
boolean |
Write the response to a file with a random name in the cache directory. |
appendExt |
string |
Extension for the fileCache file name, e.g. png or tar.gz. A value with /, \, : or a control character rejects EINVAL: it is a file extension, not a path. |
path |
string |
Write the response to this path. Overrides fileCache and appendExt. |
overwrite |
boolean |
Replace an existing file at path (default true). false appends the response to the existing file. |
key |
string |
Cache the response under this key: if a file downloaded with the same key exists, it is returned without a request. |
session |
string |
Add the response file to this session. |
timeout |
number |
Request timeout in milliseconds. Default 60000. |
followRedirect |
boolean |
Follow redirects (default true). |
transform |
boolean |
Run the file transformer on a response written to a file. |
trusty |
boolean |
Skip certificate validation. For development only; see Self-Signed SSL Server. |
customCACerts |
string[] |
Trust these bundled CA certificates. See Custom CA Certificates. |
pinnedHosts |
string[] |
Apply customCACerts to these hosts only. |
trustSystemCerts |
boolean |
Keep trusting the system CAs alongside customCACerts. Default false. |
android.downloadManager |
object | Download through Android's DownloadManager. See below. |
android.wifiOnly |
boolean |
Only send the request over WiFi. Fails with ENETUNREACH without WiFi. |
android.targetHostIp |
string |
Send the request over the network interface that can reach this IP. |
ios.backgroundTask |
boolean |
Use a background session, so the download continues while the app is suspended. |
config({
android: {wifiOnly: true},
ios: {backgroundTask: true},
}).fetch('GET', 'https://example.com/large-file.zip');Options for one platform are ignored on the others.
For large downloads on Android, the system's DownloadManager handles the transfer, shows the progress in a notification and makes the file visible in the Downloads app.
With downloadManager, fileCache and path do not apply: set the destination with
downloadManager.path. The DownloadManager only makes GET requests. When it completes,
res.path() is the downloaded file.
import {config} from 'react-native-blob-util';
const res = await config({
android: {
downloadManager: {
useDownloadManager: true, // required for the other options
notification: true,
title: 'report.pdf',
description: 'A file downloaded by the DownloadManager.',
// recommended: the DownloadManager fails when the URL has no file extension
// and no MIME type is given (the default is text/plain)
mime: 'application/pdf',
mediaScannable: true,
},
},
}).fetch('GET', 'https://example.com/report.pdf');
console.log(res.path());downloadManager option |
Description |
|---|---|
useDownloadManager |
Download through the DownloadManager. Required for the other options. |
title, description |
Shown in the notification and the Downloads app. |
path |
Destination; must be on external storage. |
mime |
MIME type of the file. Default text/plain. |
mediaScannable |
Let the media scanner index the file. |
notification |
Show a notification while downloading and when complete. |
storeInDownloads |
Android 10+: store the file in the Downloads collection (may override path). |
storeLocal |
Store the file in the app's own download directory. |
Your app might not be allowed to change or remove a file the DownloadManager created in a
location it chose; set path if you need to.
To install a downloaded APK, download it to a path you set (not the default location, from which
the package installer cannot read it) and open it with open.file:
import {config, fs, open} from 'react-native-blob-util';
const res = await config({
android: {
downloadManager: {
useDownloadManager: true,
path: fs.dirs.DownloadDir + '/awesome.apk',
title: 'awesome.apk',
description: 'An APK that will be installed',
mime: 'application/vnd.android.package-archive',
mediaScannable: true,
notification: true,
},
},
}).fetch('GET', 'https://www.example.com/awesome.apk');
const apk = res.path();
if (res.ok && apk) {
await open.file(apk, {mime: 'application/vnd.android.package-archive'});
}To register a file you downloaded yourself with the Downloads app, use
media.addDownload.
By default, react-native-blob-util does not connect to a server whose certificate does not chain
to a trusted CA. trusty: true skips that check. It is meant for development against a test
server; to trust a private CA in production use Custom CA Certificates.
What trusty skips differs per platform:
- Android uses the
X509TrustManageryour app sets asReactNativeBlobUtilUtils.sharedTrustManager(below). The library ships none, and atrustyrequest fails without one. The host name is still verified; anything else, including certificate dates, is up to your trust manager (the example below accepts any certificate chain). - iOS accepts the server's certificate whatever issued it; a certificate for another host name or an expired one still fails.
- Windows ignores an untrusted chain; a certificate for another host name or an expired one still fails.
// MainApplication.kt
import com.ReactNativeBlobUtil.ReactNativeBlobUtilUtils
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
// ...
ReactNativeBlobUtilUtils.sharedTrustManager = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
}
}// MainApplication.java
import com.ReactNativeBlobUtil.ReactNativeBlobUtilUtils;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
public class MainApplication extends Application implements ReactApplication {
@Override
public void onCreate() {
super.onCreate();
// ...
ReactNativeBlobUtilUtils.sharedTrustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
};
}
}import {config} from 'react-native-blob-util';
const res = await config({trusty: true}).fetch('GET', 'https://mysite.test');If you need to connect to a server using a custom Certificate Authority (e.g., an internal CA, self-signed CA for IoT devices, or a private PKI), you can specify custom CA certificates per request without disabling all certificate validation like trusty does.
This is more secure than trusty: true because it only trusts your specific CA rather than accepting any certificate.
If you use Expo managed workflow, the library ships a config plugin that bundles your certificates into both platforms automatically:
// app.config.js
module.exports = {
plugins: [
['react-native-blob-util', {
customCACerts: [
{ name: 'my_root_ca', path: './certs/my_root_ca.pem' }
]
}]
]
};- iOS: Add your certificate file to the Xcode project's "Copy Bundle Resources" build phase.
- Android: Place the certificate in
android/app/src/main/res/raw/(use underscores in filename, no extension for DER or keep.cer/.pem). - Windows: Add the certificate to the app package so it ships next to the executable (the folder reported as
MainBundleDir).
import {config} from 'react-native-blob-util';
const res = await config({
customCACerts: ['my_root_ca'], // resource names without extension
pinnedHosts: ['10.10.10.10', 'gateway.local'], // optional: only apply to these hosts
trustSystemCerts: false, // optional: also trust system CAs (default: false)
}).fetch('GET', 'https://10.10.10.10/api/data');| Option | Type | Default | Description |
|---|---|---|---|
customCACerts |
string[] |
— | Array of certificate resource names (without extension). Supports .cer, .der, and .pem formats. |
pinnedHosts |
string[] |
— | When set, custom CA trust is only applied to these hosts. Other hosts use default system trust. |
trustSystemCerts |
boolean |
false |
When true, system CAs are also trusted alongside custom CAs. When false, only custom CAs are trusted. |
The same rules apply on iOS, Android and Windows:
- Hostname verification still applies. A custom CA changes which issuers are trusted, not which names a certificate is valid for, so the server certificate must carry the host in its subject alternative names. Connecting to an IP address needs an IP SAN - a common surprise with private PKI, where certificates are often issued for a name the device is never reached by.
- Failure to load a certificate fails the request. If none of the names in
customCACertsresolve to a usable certificate the connection is refused rather than quietly falling back to the system trust store, so a typo cannot silently undo the pinning. pinnedHostsscopes the custom trust. Requests to other hosts are evaluated normally against the system trust store. Host names are compared case-insensitively. The decision is made for each connection, so a redirect to or from a pinned host is covered.
A rejected certificate fails the request with the code ESSL.
For app-wide trust (affecting all HTTP clients, not just react-native-blob-util), consider using Android's Network Security Configuration instead. This is a declarative XML approach that applies to all network requests in your app.
Chunked transfer encoding is off by default, since some servers do not support it. To use it,
set the Transfer-Encoding header:
await fetch('POST', 'https://example.com/upload', {'Transfer-Encoding': 'Chunked'}, {file: pathToFile});Requests use the HTTP cache. To bypass it, send 'Cache-Control': 'no-store'.
fs works on files in native code; only what you read comes into JS.
Pass plain paths, without a file:// prefix.
fs.dirs holds well-known directories. A directory that does not exist on a platform is "".
| Key | What it is |
|---|---|
DocumentDir |
the app's documents (Android: its files directory) |
CacheDir |
the app's cache directory |
MainBundleDir |
the app bundle (iOS), the app's data directory (Android), the app package (Windows) |
LibraryDir, ApplicationSupportDir |
iOS only |
DownloadDir, PictureDir, MusicDir, MovieDir, DCIMDir |
Android: the app-specific directories on external storage; no permission needed |
LegacyDownloadDir, LegacyPictureDir, LegacyMusicDir, LegacyMovieDir, LegacyDCIMDir |
Android: the shared public directories. Writable only up to Android 9, with a permission; use the media API on 10+ |
SDCardDir, SDCardApplicationDir and LegacySDCardDir are deprecated; use fs.sdCardDir()
and fs.sdCardApplicationDir().
Every call that takes an encoding also takes an options object; the positional forms still work.
import {fs} from 'react-native-blob-util';
const path = fs.dirs.DocumentDir + '/notes.txt';
const written = await fs.writeFile(path, 'Hello'); // resolves the number of bytes
await fs.appendFile(path, ', world', {encoding: 'utf8'});
const text = await fs.readFile(path); // utf8 by default
const b64 = await fs.readFile(path, {encoding: 'base64'});
await fs.createFile(fs.dirs.DocumentDir + '/new.txt', 'first line', {encoding: 'utf8'}); // EEXIST if it exists
// through the registered file transformer
await fs.writeFile(path, 'secret', {encoding: 'utf8', transform: true});
const plain = await fs.readFile(path, {encoding: 'utf8', transform: true});| Call | Options |
|---|---|
fs.readFile(path, options) |
{encoding, transform} |
fs.writeFile(path, data, options) |
{encoding, transform} |
fs.appendFile(path, data, options) |
{encoding} |
fs.createFile(path, data, options) |
{encoding} |
fs.readStream(path, options) |
{encoding, bufferSize, tick} |
fs.writeStream(path, options) |
{encoding, append} |
transform: true runs the file transformer; it cannot be combined
with ascii.
| Encoding | Read | Write |
|---|---|---|
utf8 (default) |
a string | a string |
base64 |
a base64 string | a base64 string, decoded before writing |
ascii |
an array of byte values 0..255 | an array of byte values 0..255 |
uri |
— | a path: the file at that path is copied, without passing through JS |
import {fs} from 'react-native-blob-util';
// bytes
const header = fs.dirs.DocumentDir + '/header.bin';
await fs.writeFile(header, [0x89, 0x50, 0x4e, 0x47], {encoding: 'ascii'});
const bytes = await fs.readFile(header, {encoding: 'ascii'}); // [137, 80, 78, 71]
// append one file to another, natively
await fs.appendFile(fs.dirs.DocumentDir + '/all.log', fs.dirs.CacheDir + '/today.log', {encoding: 'uri'});An unknown encoding rejects with EINVAL. Reading invalid UTF-8 as utf8 gives U+FFFD for the
bad bytes. On iOS a utf8 read stops at the first NUL byte; read such files as base64 or ascii.
import {fs} from 'react-native-blob-util';
const dir = fs.dirs.DocumentDir + '/photos';
await fs.mkdir(dir); // creates missing parents; EEXIST if it exists
const names = await fs.ls(dir); // ['a.jpg', 'b.jpg']
const entries = await fs.ls(dir, {stats: true}); // a stat of each entry
const info = await fs.stat(dir + '/a.jpg'); // {filename, path, size, type, lastModified}
const there = await fs.exists(dir + '/a.jpg'); // true or false
const isDirectory = await fs.isDir(dir);
await fs.cp(dir + '/a.jpg', dir + '/copy.jpg'); // an existing destination is overwritten
await fs.mv(dir + '/copy.jpg', dir + '/moved.jpg'); // an existing destination is overwritten
await fs.slice(dir + '/a.jpg', dir + '/head.bin', 0, 1024); // bytes [start, end); negative offsets count from the end
const sha = await fs.hash(dir + '/a.jpg', 'sha256'); // md5, sha1, sha224 (not on Windows), sha256, sha384, sha512
await fs.unlink(dir); // removes a file or a directory; resolves if nothing is there
const {free, total} = await fs.df(); // bytes; Android adds internal_* and external_* fieldssize is in bytes and lastModified in milliseconds since the epoch. type is 'file',
'directory' or 'asset'.
A call resolves undefined unless it returns something you do not already have:
| Resolves | Calls |
|---|---|
undefined |
cp, mv, mkdir, createFile, slice, unlink, excludeFromBackup, media.write, media.copyToInternal |
| the number of bytes written | writeFile, appendFile |
| a value | readFile, stat, ls, exists, isDir, hash, df, media.createFile (a content URI) |
Streams read and write a file in chunks, for files too large to hold in memory.
Read stream: register the handlers, then call open(). open() returns a Promise that
resolves when the stream reaches the end of the file and rejects with the error when it fails.
import {fs} from 'react-native-blob-util';
const stream = await fs.readStream(pathToFile, {
encoding: 'base64',
bufferSize: 12288, // bytes per chunk; default 12288. Use a multiple of 3 for base64.
tick: 10, // milliseconds between chunks; default 10
});
let data = '';
stream.onData((chunk) => {
// a string, or an array of bytes 0..255 for 'ascii'
data += chunk;
});
stream.onEnd(() => {
console.log('read', data.length, 'characters');
});
try {
await stream.open();
} catch (err) {
console.warn('read failed', err.code, err.message);
}If you do not await open(), set onError instead: with onError set, a failure does not
become an unhandled rejection.
stream.onError((err) => console.warn(err.code, err.message));
stream.open();A stream can be opened once; opening a finished stream rejects with EBADF.
Write stream: write() resolves the stream, so writes chain. A write stream must be closed.
import {fs} from 'react-native-blob-util';
const out = await fs.writeStream(pathToFile, {encoding: 'utf8', append: true});
try {
await out.write('foo');
await out.write('bar');
} finally {
await out.close();
}Wait for each write(): a write whose Promise nobody waits for can fail without anyone noticing.
Writing to or closing a closed stream rejects with EBADF. For an ascii stream, write arrays
of bytes.
Files written by fileCache or path stay until you remove them:
import {config, fs} from 'react-native-blob-util';
const res = await config({fileCache: true}).fetch('GET', 'https://example.com/download/file');
// ... use the file, then
await res.flush();
// or by path
await fs.unlink(somePath);A session is a named list of files that can be removed together. Sessions are kept in JS memory; they do not survive an app restart.
import {config, session} from 'react-native-blob-util';
// add the response file when the request completes
await config({fileCache: true, session: 'foo'}).fetch('GET', 'https://example.com/a');
// or afterwards
const res = await config({fileCache: true}).fetch('GET', 'https://example.com/b');
res.session('foo');
// or any file
session('foo').add(someFilePath);
session('foo').remove(someFilePath);
console.log(session('foo').list());
// delete every file in the session and forget it
await session('foo').dispose();fs.session(name) is the same as session(name).
fs.asset(name) returns a path to a file bundled with the app (Android assets/, the iOS app
bundle) that the other fs calls and wrap() accept:
const text = await fs.readFile(fs.asset('licenses.txt'));
await fs.cp(fs.asset('default.db'), fs.dirs.DocumentDir + '/app.db');These reject with ENOTSUP on other platforms.
| Call | Platform | Description |
|---|---|---|
fs.sdCardDir() |
Android | the external storage root |
fs.sdCardApplicationDir() |
Android | the app's directory on external storage |
fs.excludeFromBackup(path) |
iOS | exclude a file or directory from iCloud and iTunes backups |
fs.appGroupDir(groupName) |
iOS | the directory shared by the apps of an app group |
fs.appGroupDirSync(groupName) |
iOS | the same, synchronously; "" on other platforms instead of rejecting |
A content:// URI is opened through its content provider, which decides whether your app may
read or write it. A refusal rejects with EACCES.
readFile,readStream,hash,exists,unlink,cp(as source or destination),writeStream, and uploads with{file: uri}orwrap(uri)work on a URI.statreports what the provider reports:filename,size,lastModified,type: 'file', andpathis the URI itself, not a file path.ls,mv,mkdir,createFile,writeFileand the destination ofslicetake file paths only and reject a URI withENOTSUP. Copy the content to a file first:
import {fs, open} from 'react-native-blob-util';
const uri = await open.pick({mime: 'image/*'});
if (uri) {
const copy = fs.dirs.CacheDir + '/picked-image';
await fs.cp(uri, copy);
// work with `copy` as a regular file
}Setting a file transformer will allow you to specify how data should be transformed whenever the library is writing into storage or reading from storage. A use case for this is if you want the files handled by this library to be encrypted.
If you want to use a file transformer, you must implement an interface defined in:
ReactNativeBlobUtilFileTransformer.h (iOS)
ReactNativeBlobUtilFileTransformer.java (Android)
Both stay in Objective-C and Java on purpose, so implementations written in Kotlin or Swift keep compiling as well.
Then you set the File Transformer during app startup
Android (Kotlin):
class MyCustomEncryptor : ReactNativeBlobUtilFileTransformer.FileTransformer {
override fun onWriteFile(data: ByteArray): ByteArray = data // encrypt here
override fun onReadFile(data: ByteArray): ByteArray = data // decrypt here
}
class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
// ...
ReactNativeBlobUtilFileTransformer.sharedFileTransformer = MyCustomEncryptor()
}
}Android (Java):
public class MainApplication extends Application implements ReactApplication {
...
@Override
public void onCreate() {
...
ReactNativeBlobUtilFileTransformer.sharedFileTransformer = new MyCustomEncryptor();
...
}iOS (Swift). The protocol imports as FileTransformer, the class must inherit from NSObject, and the methods take and return non-optional Data:
import react_native_blob_util
final class MyCustomEncryptor: NSObject, FileTransformer {
func onWriteFile(_ data: Data) -> Data { data } // encrypt here
func onReadFile(_ data: Data) -> Data { data } // decrypt here
}
// in application(_:didFinishLaunchingWithOptions:)
ReactNativeBlobUtilFileTransformer.setFileTransformer(MyCustomEncryptor())iOS (Objective-C):
@implementation AppDelegate
...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
[ReactNativeBlobUtilFileTransformer setFileTransformer: MyCustomEncryptor.new];
...
}
The transformer runs only where you ask for it with transform: true:
fs.readFile(path, {transform: true})fs.writeFile(path, data, {transform: true})config({transform: true}), for a response written to a filemedia.write(uri, path, {transform: true})
open shows a file in another app, or lets the user pick one. Pass a path without file://
(a file:// URL is accepted on iOS too). These calls are not available on Windows.
| Call | Android | iOS |
|---|---|---|
open.file(path, {mime, scheme}) |
opens the default app for mime (an ACTION_VIEW intent) |
shows a full-screen preview |
open.chooser(path, {mime, scheme, title}) |
shows an app chooser titled title |
shows the "open in" menu |
open.optionsMenu(path, {scheme}) |
ENOTSUP |
shows the options menu |
open.pick({mime}) |
shows the system file picker; resolves the chosen file's content:// URI, or null when the user cancels |
ENOTSUP |
mime is used on Android and scheme (a URI scheme your app declares) on iOS.
import {open} from 'react-native-blob-util';
await open.file(pathToPdf, {mime: 'application/pdf'});
await open.chooser(pathToImage, {mime: 'image/png', title: 'Open with'});
const uri = await open.pick({mime: 'application/pdf'});
if (uri === null) {
// the user cancelled
}On Android, open.file rejects with ENOAPP when no app can open the MIME type, and a second
open.pick while the picker is open rejects with EBUSY. React Native's Linking cannot open a
local file on Android, and cannot install an APK; use open.file for both.
Android 10 introduced scoped storage: an app can no longer create directories on shared external
storage or write files outside its own directories. Files that should appear in the Downloads app,
the gallery or a music player go through the MediaStore instead, which media wraps. An app can
only access MediaStore entries it created itself, or that the user picked. See
https://developer.android.com/training/data-storage.
Every media call rejects with ENOTSUP on iOS and Windows.
A MediaStore entry is described by {name, mime, parentFolder}: the file name with its extension,
its MIME type, and an optional folder inside the collection ('MyApp/Files' creates MyApp with
Files inside it). The collection is 'Download', 'Image', 'Video' or 'Audio'.
Copy a file into the MediaStore in one step. Resolves the entry's content:// URI.
import {config, media} from 'react-native-blob-util';
const res = await config({fileCache: true}).fetch('GET', 'https://example.com/image.png');
const downloaded = res.path();
if (res.ok && downloaded) {
const uri = await media.copyToMediaStore(
{name: 'image.png', parentFolder: 'MyApp', mime: 'image/png'},
'Download',
downloaded,
);
}
await res.flush();Data is copied from a file; to store a string, write it to a file with fs.writeFile first.
Create an entry and write it in two steps:
const uri = await media.createFile({name: 'report.pdf', mime: 'application/pdf'}, 'Download');
await media.write(uri, localPath);
// through the registered file transformer
await media.write(uri, localPath, {transform: true});Copy an entry into the app's own storage, overwriting the destination:
import {fs, media} from 'react-native-blob-util';
await media.copyToInternal('content://...', fs.dirs.CacheDir + '/image.png');Read an entry: text, a base64 string, or bytes 0..255 for ascii.
const b64 = await media.read('content://...', {encoding: 'base64'});Register a finished download with the Downloads app:
await media.addDownload({
title: 'report.pdf',
description: 'The monthly report',
mime: 'application/pdf',
path: localPath,
showNotification: true,
});Media scanner: ask the scanner to index files so they show in the gallery and other apps. The MIME type is optional; without it the scanner goes by the file extension.
import {media} from 'react-native-blob-util';
await media.scan([
{path: pathToSong, mime: 'audio/mpeg'},
{path: pathToPhoto},
]);On Android 9 and lower, media.createFile and media.copyToMediaStore reject a name or
parentFolder containing a .. segment.
Every rejection is an Error with a code. A failed request also has err.respInfo, the
response info received before it failed. The message says what happened; match on the code.
import {config} from 'react-native-blob-util';
try {
const res = await config({timeout: 10000}).fetch('GET', 'https://example.com/data');
} catch (err) {
switch (err.code) {
case 'ETIMEDOUT':
// err.respInfo.timeout is true as well
break;
case 'ENOTFOUND':
case 'ECONNREFUSED':
case 'ENETUNREACH':
break;
default:
console.warn(err.code, err.message, err.respInfo);
}
}| Code | Meaning |
|---|---|
ENOENT |
a file or directory does not exist (or the destination's directory) |
EISDIR |
a directory where a file was expected |
ENOTDIR |
a file where a directory was expected; the download directory could not be created |
EEXIST |
the file or directory already exists (mkdir, createFile) |
EACCES |
a content:// provider refused access (Android) |
EBADF |
the stream is already closed |
ETIMEDOUT |
the request timed out |
ENOTFOUND |
the host name could not be resolved |
ECONNREFUSED |
the host refused the connection |
ECONNRESET |
the connection was lost |
ENETUNREACH |
no usable network (Android wifiOnly without WiFi; iOS offline) |
ESSL |
the TLS handshake or certificate check failed, including customCACerts and pinnedHosts rejections |
ECANCELED |
the request was cancelled |
EINVAL |
an invalid argument: URL, method, header, encoding, a body on GET/HEAD |
ENOTSUP |
not available on this platform, or a content:// URI where only a file path works |
EBUSY |
a file picker is already open |
ENOAPP |
no app can open the file (Android) |
EUNSPECIFIED |
anything else; the message says what |
In TypeScript, CodedError and FetchError describe these errors, and ErrorCode is the union
of the codes.
Read Stream and Progress Event Overhead
If reading a large file with fs.readStream keeps the JS thread busy, the chunks may be too small
for the file: each chunk is one event. The default is 12288 bytes. Try a larger bufferSize
(a multiple of 3 for base64, for example 102399) and a larger tick (default 10 ms). For request
progress, raise interval or set count.
Reduce Bridge and BASE64 Overhead
Data that passes between JS and native has to be converted on the way. When data is large, this has a real performance cost. Use file storage instead of BASE64 where you can: download with fileCache or path, upload with {file}. The following chart shows how much faster loading data from storage is than a BASE64 encoded string on iPhone 6.
ASCII Encoding has Terrible Performance
Converting data to a JS array of numbers takes a lot of time. Use ascii only when you need the bytes; the following chart shows how long reading a file takes with each encoding.
Concat and Replacing Files
To concatenate or copy files you don't have to read the data into JS: the uri encoding of writeFile and appendFile, and fs.cp, do the whole thing in native code.
- This library does not urlencode unicode characters in URL automatically, see #146.
- When passing a file path to the library, remove the
file://prefix. - Progress and stream events are sent to JS one by one; limit them as described in Performance Tips.
See release notes
If you're interested in working on this module, see CONTRIBUTING.md. Please feel free to make a PR or file an issue.





