The following code works correctly for image files. But when I'm trying to save PDF file or other not-media formates, I get Could not create asset error.
I understand that expo-media-library is designed to work with media format files.
Is there any alternative for expo-media-library to save other files formats?
import * as FileSystem from 'expo-file-system'
import * as Permissions from 'expo-permissions'
import * as MediaLibrary from 'expo-media-library'
const downloadFile = async (uri: string) => {
const targetUri = FileSystem.documentDirectory + getFileName(uri)
const downloadedFile = await FileSystem.downloadAsync(uri, targetUri)
if (downloadedFile.status === 200) {
if (Platform.OS === 'android') {
const permission = await Permissions.askAsync(Permissions.MEDIA_LIBRARY)
if (permission.status !== 'granted') {
return
}
const asset = await MediaLibrary.createAssetAsync(downloadedFile.uri)
const album = await MediaLibrary.getAlbumAsync('Download')
await MediaLibrary.addAssetsToAlbumAsync([asset], album, false)
}
}
}
it works on android device with https://docs.expo.dev/versions/latest/sdk/filesystem/#storageaccessframeworkcreatefileasyncparenturi-string-filename-string-mimetype-string
import * as FileSystem from 'expo-file-system';
import { StorageAccessFramework } from 'expo-file-system';
const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
if (!permissions.granted) {
return;
}
try {
await StorageAccessFramework.createFileAsync(permissions.directoryUri, fileName, 'application/pdf')
.then((r) => {
console.log(r);
})
.catch((e) => {
console.log(e);
});
} catch((e) => {
console.log(e);
});
My pdf is well downloaded !
In my case i had to generate the file from a base64 string.
My code :
import * as FileSystem from 'expo-file-system';
import { StorageAccessFramework } from 'expo-file-system';
const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
if (!permissions.granted) {
return;
}
const base64Data = 'my base 64 data';
try {
await StorageAccessFramework.createFileAsync(permissions.directoryUri, fileName, 'application/pdf')
.then(async(uri) => {
await FileSystem.writeAsStringAsync(uri, base64Data, { encoding: FileSystem.EncodingType.Base64 });
})
.catch((e) => {
console.log(e);
});
} catch (e) {
throw new Error(e);
}
Related
react native pdf file download after i have not open not for android
working for ios iPhone 13 device IOS 15.5
android Pixels 3a android 11v
i think i have added everything needed in android manifest,
It works for ios devices but not for android. after i download the file i can't open it i.e. there is a download error
my code:
import { Alert, PermissionsAndroid, Platform } from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
interface IDownloadOptions {
src: string;
fileName: string;
token: string | null;
setLoading?: (loading: boolean) => void;
}
// commint
class DownloadManager {
public download = async (options: IDownloadOptions): Promise<any> => {
const { token, src, fileName, setLoading } = options;
const { dirs } = RNFetchBlob.fs;
const dirToSave = Platform.OS === 'ios' ? dirs.DocumentDir : dirs.DownloadDir;
console.log('dirToSave', dirToSave);
if (Platform.OS === 'android') {
const permission = await PermissionsAndroid.request('android.permission.WRITE_EXTERNAL_STORAGE');
if (permission !== 'granted') {
console.log('not granted');
setLoading && setLoading(false);
return;
}
}
const ios = RNFetchBlob.ios;
const android = RNFetchBlob.android;
const configfb = {
fileCache: true,
useDownloadManager: true,
notification: true,
mediaScannable: true,
title: fileName,
path: `${dirToSave}/${fileName}`,
};
console.log('config', configfb);
return new Promise((resolve, reject) => {
requestAnimationFrame(() => {
RNFetchBlob.config({
fileCache: configfb.fileCache,
// #ts-ignore
title: configfb.title,
path: configfb.path,
// #ts-ignore
appendExt: 'pdf',
addAndroidDownloads: {
...configfb,
},
})
.fetch('GET', src, {
Authorization: `Bearer ${token}`,
})
.then(res => {
const status = res.info().status;
console.log('status: ' + status);
console.log('res: ' + res);
if (status === 200) {
if (Platform.OS === 'ios') {
ios.openDocument(res.data);
}
if (Platform.OS === 'android') {
android.actionViewIntent(res.path(), 'pdf');
}
}
if (status === 500) {
Alert.alert('Server error 500');
}
setLoading && setLoading(false);
resolve(res);
})
.catch(error => {
console.log('error', error);
setLoading && setLoading(false);
reject(error);
});
});
});
};
}
Here is my code:
const saveImg = async (base64Img: string, success: Function, fail:Function) => {
const isAndroid = Platform.OS === "android"
const isIos = Platform.OS === 'ios'
const dirs = isIos? RNFS.LibraryDirectoryPath : RNFS.ExternalDirectoryPath;
const certificateTitle = 'certificate-'+((Math.random() * 10000000) | 0)
const downloadDest = `${dirs}/${certificateTitle}.png`;
const imageDatas = base64Img.split('data:image/png;base64,');
const imageData = imageDatas[1];
try{
await RNFetchBlob.config({
addAndroidDownloads:{
notification:true,
description:'certificate',
mime:'image/png',
title:certificateTitle +'.png',
path:downloadDest
}
}).fs.writeFile(downloadDest, imageData, 'base64')
if (isAndroid) {
} else {
RNFetchBlob.ios.previewDocument(downloadDest);
}
success()
}catch(error:any){
console.log(error)
fail()
}
}
I get this error:
undefined is not an object (near '...}).fs.writeFile(downloadD...')
at node_modules/react-native-webview/lib/WebView.android.js:207:16 in _this.onMessage
When I hit the download button and this runs I get the mentioned Error.
I use to get the download done with the below code modification, but I really need to show the download feedback from both android and IOS.
This works (but without notification)
await RNFetchBlob.fs.writeFile(downloadDest, imageData, 'base64')
I am using expo
I discovered that the react-fetch-blob does not work with expo, to solve it, I used the following libraries:
expo-file-system, expo-media-library, expo-image-picker,expo-notifications
This was the code to convert, download and show the notification of the image in the "expo way":
import * as FileSystem from 'expo-file-system';
import * as MediaLibrary from 'expo-media-library';
import * as ImagePicker from 'expo-image-picker';
import * as Notifications from 'expo-notifications';
const saveImg = async (base64Img: string, success: Function, fail:Function) => {
const imageDatas = base64Img.split('data:image/png;base64,');
const imageData = imageDatas[1];
try {
const certificateName = 'certificate-'+((Math.random() * 10000000) | 0) + ".png"
const certificatePathInFileSystem = FileSystem.documentDirectory +certificateName ;
await FileSystem.writeAsStringAsync(certificatePathInFileSystem, imageData, {
encoding: FileSystem.EncodingType.Base64,
});
await MediaLibrary.saveToLibraryAsync(certificatePathInFileSystem);
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: true,
}),
});
await Notifications.scheduleNotificationAsync({
content: {
title: certificateName +' saved !',
body: "Click to show the certificate",
},
trigger: null,
});
setCertificatePath(certificatePathInFileSystem)
success()
} catch (e) {
console.error(e);
fail()
}
}
In order to open the images gallery on click I used this code:
useEffect(()=>{
if(certificatePath){
Notifications.addNotificationResponseReceivedListener( async (event )=> {
await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
})
})
}
},[certificatePath])
Try to call fetch after create RNFetchBlob.config
If you just wanna display an Image and not store you can show image as fallows (https://reactnative.dev/docs/next/images#uri-data-images)
<Image
style={{
width: 51,
height: 51,
resizeMode: 'contain'
}}
source={{
uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAEXRFWHRTb2Z0d2FyZQBwbmdjcnVzaEB1SfMAAABQSURBVGje7dSxCQBACARB+2/ab8BEeQNhFi6WSYzYLYudDQYGBgYGBgYGBgYGBgYGBgZmcvDqYGBgmhivGQYGBgYGBgYGBgYGBgYGBgbmQw+P/eMrC5UTVAAAAABJRU5ErkJggg=='
}}
/>
Call fetch on config object:
try{
const fetchConfig = await RNFetchBlob.config({
addAndroidDownloads:{
notification:true,
description:'certificate',
mime:'image/png',
title:certificateTitle +'.png',
path:downloadDest
}
})
fetchConfig.fetch('your.domain.com').fs.writeFile(downloadDest, imageData, 'base64')
if (isAndroid) {
} else {
RNFetchBlob.ios.previewDocument(downloadDest);
}
success()
}catch(error:any){
console.log(error)
fail()
}
I’m downloading a file, and I’ve expected open the file after finished downloading, but this does not work, it’s viewed on a black screen when opening using Linking, and I open this file in the folder where it is and it opens correctly. How can I fix this?
This is the URL of the file in STORAGE:
content://com.android.externalstorage.documents/tree/primary%3ADownload%2FSigaa/document/primary%3ADownload%2FSigaa%2Fhistory.pdf
My code:
const downloadPath = FileSystem.documentDirectory! + (Platform.OS == "android" ? "" : "");
const ensureDirAsync: any = async (dir: any, intermediates = true) => {
const props = await FileSystem.getInfoAsync(dir);
if (props.exists && props.isDirectory) {
return props;
}
let _ = await FileSystem.makeDirectoryAsync(dir, { intermediates });
return await ensureDirAsync(dir, intermediates);
};
const downloadFile = async (fileUrl) => {
if (Platform.OS == "android") {
const dir = ensureDirAsync(downloadPath);
}
let fileName = "history";
const downloadResumable = FileSystem.createDownloadResumable(
fileUrl,
downloadPath + fileName,
{}
);
console.log(downloadPath);
try {
const { uri } = await downloadResumable.downloadAsync();
saveAndroidFile(uri, fileName);
} catch (e) {
console.error("download error:", e);
}
};
const saveAndroidFile = async (fileUri, fileName = "File") => {
try {
const fileString = await FileSystem.readAsStringAsync(fileUri, { encoding: FileSystem.EncodingType.Base64 });
if (local === "") {
const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
if (!permissions.granted) {
return;
}
await AsyncStorage.setItem("local", permissions.directoryUri);
}
try {
await StorageAccessFramework.createFileAsync(local, fileName, "application/pdf")
.then(async (uri) => {
await FileSystem.writeAsStringAsync(uri, fileString, { encoding: FileSystem.EncodingType.Base64 });
Linking.openURL(uri);
alert("Success!");
})
.catch((e) => {});
} catch (e) {
throw new Error(e);
}
} catch (err) {}
};
The black screen when opening:
I solved this by implementing the intent launcher api for android and the sharing api for ios.
To make it work it is important to provide the mimeType of the file (or UTI for IOS). You can extract it manually from the file extension but there's a library for that on RN: react-native-mime-types.
Actually I think any library that doesn't deppend on node core apis shoud work just fine.
Here's the code:
const openFile = async (fileUri: string, type: string) => {
try {
if (Platform.OS === 'android') {
await startActivityAsync('android.intent.action.VIEW', {
data: fileUri,
flags: 1,
type,
});
} else {
await shareAsync(fileUri, {
UTI: type,
mimeType: type,
});
}
} catch (error) {
// do something with error
}
};
I'm using expo file system to download a pdf. It was successfully downloaded however when trying to open a pdf file it says "Cannot display a pdf of invalid format. First I downloaded pdf from backend and then converted to base64 using buffer.
Here's the reference I followed from stackoverflow Expo React Native, saving PDF files to Downloads folder
import * as FileSystem from 'expo-file-system';
import { StorageAccessFramework } from 'expo-file-system';
import {Buffer} from "buffer";
const downloadFile = async (payment) => {
const pdf = await grabPdf();
const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
if (!permissions.granted) {
return;
}
try {
await StorageAccessFramework.createFileAsync(permissions.directoryUri, 'inv'+payment.invoice_number, 'application/pdf')
.then(async(uri) => {
await FileSystem.writeAsStringAsync(uri, pdf, { encoding: FileSystem.EncodingType.Base64 });
Alert.alert('Success', 'Successfully downloaded')
})
.catch((e) => {
console.log(e.response.data);
alert(e)
});
} catch (e) {
throw new Error(e);
alert(e)
}
}
Download pdf from backend and convert to base 64 using buffer.
const grabPdf = async () => {
axiosConfig.defaults.headers.common['Authorization'] = `Bearer ${user.token}`;
const response = await axiosConfig('/user/invoice/C0F19758-0001/247')
.catch(error => {
console.log('Error: ', error.response.data)
alert('Error: '+ error.response.data)
});
const buff = Buffer.from(response.data, 'base64')
return buff.toString('base64')
}
Solved this problem by converting pdf to base64 from php backend
$base64 = chunk_split(base64_encode(file_get_contents($filename)));
return $base64;
before passing to react-native
I'm using the following code to download a file (can be a PDF or a DOC) and then opening it using Linking.
const { dirs } = RNFetchBlob.fs;
let config = {
fileCache : true,
appendExt : extension,
addAndroidDownloads : {
useDownloadManager : false,
notification : false,
title : 'File',
description : 'A file.',
path: `${dirs.DownloadDir}/file.${extension}`,
},
};
RNFetchBlob.config(config)
.fetch(
method,
remoteUrl,
APIHelpers.getDefaultHeaders()
)
.then((res) => {
let status = res.info().status;
if (status == 200) {
Linking.canOpenURL(res.path())
.then((supported) => {
if (!supported) {
alert('Can\'t handle url: ' + res.path());
} else {
Linking.openURL(res.path())
.catch((err) => alert('An error occurred while opening the file. ' + err));
}
})
.catch((err) => alert('The file cannot be opened. ' + err));
} else {
alert('File was not found.')
}
})
.catch((errorMessage, statusCode) => {
alert('There was some error while downloading the file. ' + errorMessage);
});
However, I'm getting the following error:
An error occurred while opening the file. Error: Unable to open URL:
file:///Users/abhishekpokhriyal/Library/Developer/CoreSimulator/Devices/3E2A9C16-0222-40A6-8C1C-EC174B6EE9E8/data/Containers/Data/Application/A37B9D69-583D-4DC8-94B2-0F4AF8272310/Documents/RNFetchBlob_tmp/RNFetchBlobTmp_o259xexg7axbwq3fh6f4.pdf
I need to implement the solution for both iOS and Android.
I think the easiest way to do so is by using react-native-file-viewer package.
It allows you to Prompt the user to choose an app to open the file with (if there are multiple installed apps that support the mimetype).
import FileViewer from 'react-native-file-viewer';
const path = // absolute-path-to-my-local-file.
FileViewer.open(path, { showOpenWithDialog: true })
.then(() => {
// success
})
.catch(error => {
// error
});
So, I finally did this by replacing Linking by the package react-native-file-viewer.
In my APIHelpers.js:
async getRemoteFile(filePath, extension, method = 'GET') {
const remoteUrl = `${API_BASE_URL}/${encodeURIComponent(filePath)}`;
const { dirs } = RNFetchBlob.fs;
let config = {
fileCache : true,
appendExt : extension,
addAndroidDownloads : {
useDownloadManager : false,
notification : false,
title : 'File',
description : 'A file.',
path: `${dirs.DownloadDir}/file.${extension}`,
},
};
return new Promise(async (next, error) => {
try {
let response = await RNFetchBlob.config(config)
.fetch(
method,
remoteUrl,
this.getDefaultHeaders()
);
next(response);
} catch (err) {
error(err);
}
});
}
In my Actions.js
export function openDocument(docPath, ext) {
return async (dispatch) => {
dispatch(fetchingFile());
APIHelpers.getRemoteFile(docPath, ext).then(async function(response) {
dispatch(successFetchingFile());
let status = response.info().status;
if (status == 200) {
const path = response.path();
setTimeout(() => {
FileViewer.open(path, {
showOpenWithDialog: true,
showAppsSuggestions: true,
})
.catch(error => {
dispatch(errorOpeningFile(error));
});
}, 100);
} else {
dispatch(invalidFile());
}
}).catch(function(err) {
dispatch(errorFetchingFile(err));
});
}
}
In my Screen.js
import { openDocument } from 'path/to/Actions';
render() {
return <Button
title={'View file'}
onPress={() => this.props.dispatchOpenDocument(doc.filepath, doc.extension)}
/>;
}
.
.
.
const mapDispatchToProps = {
dispatchOpenDocument: (docPath, ext) => openDocument(docPath, ext),
}
Are you downloading it from the web? I can see the pdf path is attached at the end of the error path.
For web URLs, the protocol ("http://", "https://") must be set accordingly!
Try to append appropriate schemes to your path. Check it out from the link mentioned below.
This can be done with 'rn-fetch-blob'
RNFetchBlob.android.actionViewIntent(fileLocation, mimeType);