Efficiently download and upload blobs in React Native

react-native-blob-courier

Use this library to efficiently download and upload blobs in React Native. The library was inspired by rn-fetch-blob, and aims to focus strictly on blob transfers.

Installation

Install using yarn

yarn add react-native-blob-courier

Or install using npm

npm install react-native-blob-courier

Link the library:

NB. Linking can be skipped when the project uses React Native 0.60 or greater, because autolinking will take care of it

react-native link react-native-blob-courier

If CocoaPods is used in the project, make sure to install the pod:

cd ios && pod install

Requirements

  • Android >= 19
  • iOS >= 9 Changing to iOS 10.x in July 2021
  • Kotlin >= 1.4.x

Usage

The library provides both a fluent and a more concise interface. In the examples the concise approach is applied; fluent interface is demonstrated later in this document.

Straightforward down- and upload

import BlobCourier from 'react-native-blob-courier';

// ...

// Download a file
const request0 = {
  filename: '5MB.zip',
  method: 'GET',
  mimeType: 'application/zip',
  url: 'http://ipv4.download.thinkbroadband.com/5MB.zip',
};

const fetchedResult = await BlobCourier.fetchBlob(request0);
console.log(fetchedResult);
// {
//   "data": {
//     "absoluteFilePath": "/path/to/app/cache/5MB.zip",
//     "response": {
//       "code":200,
//       "headers": {
//         "some_header": "some_value",
//         ...
//       }
//     },
//   },
//   "type":"Unmanaged"
// }

// ...

// Upload a file
const absoluteFilePath = fetchedResult.data.absoluteFilePath;

const request1 = {
  absoluteFilePath,
  method: 'POST',
  mimeType: 'application/zip',
  url: 'https://file.io',
};

const uploadResult = await BlobCourier.uploadBlob(request1);

console.log(uploadResult):
// {
//   "response": {
//     "code": {
//     "data": "<some response>",
//     "headers": {
//       "some_header": "some_value",
//        ...
//      }
//   }
// }

// Multipart file upload
const absoluteFilePath = fetchedResult.data.absoluteFilePath;

const request2 = {
  method: 'POST',
  parts: {
    body: {
      payload: 'some_value',
      type: 'string',
    },
    file: {
      payload: {
        absoluteFilePath,
        mimeType: 'application/zip',
      },
      type: 'file',
    },
  },
  url: 'https://file.io',
};

const multipartUploadResult = await BlobCourier.uploadBlob(request1);

console.log(multipartUploadResult):
// {
//   "response": {
//     "code": {
//     "data": "<some response>",
//     "headers": {
//       "some_header": "some_value",
//        ...
//      }
//   }
// }

Transfer progress reporting

import BlobCourier from 'react-native-blob-courier';

// ...

// Download a file
const request0 = {
  // ...
  onProgress: ((e: BlobProgressEvent) => {
    console.log(e)
    // {
    //  "written": <some_number_of_bytes_written>,
    //  "total": <some_total_number_of_bytes>
    // }
  })
};

const fetchedResult = await BlobCourier.fetchBlob(request0);

// ...

// Upload a file
const request1 = {
  // ...
  onProgress: ((e: BlobProgressEvent) => {
    console.log(e)
    // {
    //  "written": <some_number_of_bytes_written>,
    //  "total": <some_total_number_of_bytes>
    // }
  })
};

const uploadResult = await BlobCourier.uploadBlob(request1)

// ...

// Set progress updater interval
const request2 = ...

const someResult =
  await BlobCourier
    .fetchBlob({
      ...request2,
      progressIntervalMilliseconds: 1000,
    });

Managed download on Android (not available on iOS)

import BlobCourier from 'react-native-blob-courier';

// ...

const request = {
  android: {
    useDownloadManager: true // <--- set useDownloadManager to "true"
  },
  filename: '5MB.zip',
  method: 'GET',
  mimeType: 'application/zip',
  url: 'http://ipv4.download.thinkbroadband.com/5MB.zip',
};

const fetchResult = await BlobCourier.fetchBlob(request);

console.log(fetchedResult);
// {
//   "data": {
//     "result": "SUCCESS",
//     "absoluteFilePath": "/path/to/app/cache/5MB.zip"
//   },
//   "type":"Managed"
// }

Multipart upload

Sometimes order of multipart fields matters, and Blob Courier respects the order in which parts are provided. There is a catch though: when object keys are regular strings they are kept in the order they were added unless the keys are strings containing numbers, e.g.:

Object.keys({
  "b": "some_value1",
  "c": "some_value2",
  "a": "some_value3",
})

// ['b', 'c', 'a']

Object.keys({
  "b": "some_value1",
  "c": "some_value2",
  "a": "some_value3",
  "3": "some_value4",
  "2": "some_value5",
  "1": "some_value6",
});

// ['1', '2', '3', 'b', 'c', 'a']

The way to work around this, is to wrap all keys in a Symbol, by using Symbol.for. Do not use Symbol(<value>), this will not work, e.g.:

Object.getOwnPropertySymbols({
  [Symbol.for("b")]: "some_value1",
  [Symbol.for("c")]: "some_value2",
  [Symbol.for("a")]: "some_value3",
  [Symbol.for("3")]: "some_value4",
  [Symbol.for("2")]: "some_value5",
  [Symbol.for("1")]: "some_value6",
});

// [Symbol('b'), Symbol('c'), Symbol('a'), Symbol('3'), Symbol('2'), Symbol('1')]

Cancel request

import BlobCourier from 'react-native-blob-courier';

// ...

const abortController = new AbortController();

const { signal } = abortController;

const request0 = {
  // ...
  signal,
};

try {
  BlobCourier.fetchBlob(request0);

  abortController.abort();
} catch (e) {
  if (e.code === ERROR_CANCELED_EXCEPTION) {
    // ...
  }
}

// ...

Fluent interface

Blob Courier provides a fluent interface, that both protects you from using impossible setting combinations and arguably improves readability.

const req0 = ...

const someResult =
  await BlobCourier
    .settings({
      progressIntervalMilliseconds: 1000,
    })
    .onProgress((e: BlobProgressEvent) => {
      // ...
    })
    .useDownloadManagerOnAndroid({
      description: "Some file description",
      enableNotification: true,
      title: "Some title"
    })
    .fetchBlob(req0)

Available methods

fetchBlob(input: BlobFetchRequest)

Required

Field Type Description
filename string The name the file will have on disk after fetch.
mimeType string What is the mime type of the blob being transferred?
url string From which url will the blob be fetched?

Optional

Field Type Description Default
android AndroidSettings Settings to be used on Android { downloadManager: {}, target: 'cache', useDownloadManager: false }
headers { [key: string]: string } Map of headers to send with the request {}
ios IOSSettings Settings to be used on iOS { target: 'cache' }
headers { [key: string]: string } Map of headers to send with the request {}
method string Representing the HTTP method GET
onProgress (e: BlobProgressEvent) => void Function handling progress updates () => { }
signal AbortSignal Request cancellation manager null

Response

Field Type Description
type "Managed" \| "Unmanaged" Was the blob downloaded through Android Download Manager, or without?
data BlobManagedData \| BlobUnmanagedData Either managed or HTTP response data

uploadBlob(input: BlobUploadRequest)

Alias for:

const someResult =
  await BlobCourier
   // ...
   .uploadParts({
     headers,
     method,
     parts: {
       file:
         payload: {
           absoluteFilePath,
           filename,
           mimeType,
         },
         type: 'file',
       },
     },
     returnResponse,
     url,
   })

Required

Field Type Description
absoluteFilePath string Path to the file to be uploaded
mimeType string Mime type of the blob being transferred
url string Url to upload the blob to

Optional

Field Type Description Default
filename string Map of headers to send with the request <name part of 'absoluteFilePath'>
headers { [key: string]: string } Map of headers to send with the request {}
method string The HTTP method to be used in the request "POST"
multipartName string Name for the file multipart "file"
onProgress (e: BlobProgressEvent) => void Function handling progress updates () => { }
returnResponse boolean Return the HTTP response body? false
signal AbortSignal Request cancellation manager null

uploadParts(input: BlobMultipartUploadRequest)

Required

Field Type Description
parts { [key: string]: BlobMultipart } The parts to be sent
url string Url to upload the blob to

Optional

Field Type Description Default
headers { [key: string]: string } Map of headers to send with the request {}
method string The HTTP method to be used in the request "POST"
onProgress (e: BlobProgressEvent) => void Function handling progress updates () => { }
returnResponse boolean Return the HTTP response body? false
signal AbortSignal Request cancellation manager null

Response

Field Type Description
response BlobUnmanagedHttpResponse The HTTP response

AndroidDownloadManagerSettings

Field Type Description
description? string Description of the downloaded file
enableNotification? boolean Display notification when download completes
title? string Title to be displayed with the download

AndroidSettings

Field Type Description
downloadManager AndroidDownloadManagerSettings Settings to be used on download manager
target "cache" \| "data" Where will the file be stored?
useDownloadManager boolean Enable download manager on Android?

BlobManagedData

Field Type Description
absoluteFilePath string The absolute file path to where the file was stored
result "SUCCESS" \| "FAILURE" Was the request successful or did it fail?

BlobMultipart

Required

Field Type Description
payload BlobMultipartFormData \| BlobMultipartFormDataFile Contains the payload of the part
type "string" \| "file" What is the type of the payload?

BlobMultipartFormData

Type of string | { [key:string] : any }

BlobMultipartFormDataFile

Required

Field Type Description
absoluteFilePath string Path to the file to be uploaded
mimeType string Mime type of the blob being transferred

Optional

Field Type Description Default
filename string Map of headers to send with the request <name part of 'absoluteFilePath'>

BlobProgressEvent

Field Type Description
written number Number of bytes processed
total number Total number of bytes to be processed

BlobUnmanagedData

Field Type Description
absoluteFilePath string The absolute file path to where the file was stored
response BlobUnmanagedHttpResponse HTTP response, including headers and status code

BlobUnmanagedHttpResponse

Field Type Description
code number HTTP status code
headers { [key: string]: string } HTTP response headers

IOSSettings

Field Type Description
target "cache" \| "data" Where will the file be stored?

Example app

You can find an example of how to use the library in the example directory.

Android

Permissions

Android 5.0 and below (API level < 23)

Add the following line to AndroidManifest.xml.

<manifest xmlns:android="http://schemas.android.com/apk/res/android" (...)>

+   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+   <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
    (...)
    <application (...)>
      <activity (...)>
        <intent-filter>
          (...)
+          <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
        </intent-filter>
    (...)

Android 6.0+ (API level 23+)

Grant permissions using the PermissionAndroid API, like so:

const function App = () => {

  // ...

  React.useEffect(() => {
    const requestPermissionAsync = async () => {
      try {
        await PermissionsAndroid.request(
          PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
        );

        // ...
      } catch (err) {
        console.error(err);
      }

      // ...
    };

    requestPermissionAsync();
  }, []);

  // ...

iOS

Add to Info.plist of your app:

<key>NSAllowsArbitraryLoads</key>
<true/>

Using the integrated download manager for Android

This library allows you to use the integrated download manager on Android, this option is not available for iOS.

To enable the download manager, simply set the request's useDownloadManager property of field android to true when passing it to fetchBlob, or call the useDownloadManagerOnAndroid method when using the fluent interface.

Shared directories

As this library is focussed on transferring files, it only supports storage to the app's cache and data directories. To move files from these app specific directories to other locations on the filesystem, use another library like @react-native-community/cameraroll, e.g.:

import BlobCourier from 'react-native-blob-courier';
import CameraRoll from '@react-native-community/cameraroll';

// ...

const request = {
  filename: 'teh_cage640x360.png',
  method: 'GET',
  mimeType: 'image/png',
  url: 'https://www.placecage.com/640/360',
};

const cageResult = await BlobCourier.fetchBlob(request)

const cageLocalPath = cageResult.data.absoluteFilePath

CameraRoll.save(cageLocalPath);

GitHub

https://github.com/edeckers/react-native-blob-courier