How to create a .pdf or .csv file automatically in React Native for Android 11 (or later) - react-native

The following code works up to Android 10, it is creating a csv file in the DCIM folder:
import * as FileSystem from 'expo-file-system';
import * as MediaLibrary from 'expo-media-library';
export async function saveCSV() {
const permission = await MediaLibrary.requestPermissionsAsync();
if (permission.status != 'granted') {
console.log("Permission not Granted!")
return;
}
// CSVLocation
const directoryUri = FileSystem.documentDirectory;
const fileUri = directoryUri + `formData.csv`;
// Save to DCIM folder
const asset = await MediaLibrary.createAssetAsync(fileUri);
try {
const album = await MediaLibrary.getAlbumAsync('album');
if (album == null) {
console.log("ASSET", asset)
await MediaLibrary.createAlbumAsync('album', asset, true);
} else {
await MediaLibrary.addAssetsToAlbumAsync([asset], album, true)
.then(() => {
console.log('File Saved Successfully!');
})
.catch((err: string) => {
console.log('Error In Saving File!', err);
});
}
} catch (e) {
console.log(e);
}
}
Previously this line of code was executed in another function to create a file in the fileUri used above:
await FileSystem.writeAsStringAsync(fileUri, CSVheader + newInfo);
This issue has been described here: https://github.com/expo/expo/issues/12060
In short: Expo Media library is able to save image/video/audio assets so it will fail with other file types. Weirdly enough it was working fine with .pdf and .csv up to Android 10.
In the link above, and also on stackoverflow there are solutions using StorageAccessFramework. However, the user needs to create a subdirectory inside Downloads every time a file needs to be saved. I would like to make it automatically without any popups (after permission is granted).
The destination folder doesn't matter as long as it is accessible by the user later.

Related

Save image from expo assets to user device but first let the user select the target folder

I have researched a lot , even used ChatGTP3 solutions ( which by the way are cool ) but nothing works .
So i will explain my case ( i am targeting android ) :
Inside the local expo assets i have 30 images
i want to allow the user to download this images to his device for whatever reason so i have provided a download button :
When you press the download button i want the user to be able to pick the folder where he wants to save the image and then save it that's it here is an example of many codes i have tried it just doesn't work :
import * as FileSystem from 'expo-file-system'
const saveToPhone = async () => {
try {
// Assuming that you have a file called '30.jpg' in your Expo assets folder
const fileUri = Asset.fromModule(require('../../../assets/images/motivational/30.jpg')).uri
// Let the user pick a folder to save the file to
const saveDir = await FileSystem.getDocumentDirectoryAsync()
// Create a new file in the save directory with the same name as the file in assets
const savePath = `${saveDir}/imageName.jpg`
// Copy the file from assets to the save directory
await FileSystem.copyAsync({ from: fileUri, to: savePath })
} catch (error) {
console.log(error)
}
}
I have tried so many codes for days i can't find out how to do that .I am using latest expo sdk 47 .
Update 24/12/2022
After using #proto answer:
import * as FileSystem from 'expo-file-system'
import * as DocumentPicker from 'expo-document-picker'
const saveToPhone = async () => {
try {
// Assuming that you have a file called '30.jpg' in your Expo assets folder
const fileUri = Asset.fromModule(require('../../../assets/images/motivational/30.jpg')).uri
// Let the user pick a folder to save the file to
const saveDir = await DocumentPicker.getDocumentAsync()
// Create a new file in the save directory with the same name as the file in assets
const savePath = `${saveDir.uri}/imageName.jpg`
// Copy the file from assets to the save directory
await FileSystem.copyAsync({ from: fileUri, to: savePath })
} catch (error) {
console.log(error)
}
}
I am getting the following error
LOG [Error: Location 'http://192.168.1.13:8081/assets/assets/images/motivational/30.jpg?platform=android&hash=0cf4bfe9c8ccb915c1d43a641d662544?platform=android&dev=true&hot=false' isn't readable.]
You need an additional module called DocumentPicker from expo-document-picker. Instead of getting directory directly, try selecting a document and extract its path.
Something like this :
import * as DocumentPicker from 'expo-document-picker'
const saveDir = await DocumentPicker.getDocumentAsync()
const savePath = `${saveDir.uri}/imageName.jpg`
So now the code should look like this :
import * as FileSystem from 'expo-file-system'
import * as DocumentPicker from 'expo-document-picker'
const saveToPhone = async () => {
try {
// Assuming that you have a file called '30.jpg' in your Expo assets folder
const fileUri = Asset.fromModule(require('../../../assets/images/motivational/30.jpg')).uri
// Let the user pick a folder to save the file to
const saveDir = await DocumentPicker.getDocumentAsync()
// Create a new file in the save directory with the same name as the file in assets
const savePath = `${saveDir.uri}/imageName.jpg`
// Copy the file from assets to the save directory
await FileSystem.copyAsync({ from: fileUri, to: savePath })
} catch (error) {
console.log(error)
}
}

react-native (Expo) upload file on background

In my Expo (react-native) application, I want to do the upload task even if the application is in the background or killed.
the upload should be done to firebase storage, so we don't have a REST API.
checked out the Expo task manager library, but I could not figure out how it should be done. is it even possible to achieve this goal with Expo? is the TaskManager the correct package for this task?
there are only some Expo packages that could be registered as a task (e.g. backgroundFetch), and it is not possible to register a custom function (in this case uploadFile method).
I even got more confused as we should enable add UIBackgroundModes key for iOS but it only has audio,location,voip,external-accessory,bluetooth-central,bluetooth-peripheral,fetch,remote-notification,processing as possible values.
I would appreciate it if you can at least guide me on where to start or what to search for, to be able to upload the file even if the app is in the background is killed/terminated.
import { getStorage, ref, uploadBytes } from "firebase/storage";
const storage = getStorage();
const storageRef = ref(storage, 'videos');
const uploadFile = async (file)=>{
// the file is Blob object
await uploadBytes(storageRef, file);
}
I have already reviewed react-native-background-fetch, react-native-background-upload, react-native-background-job . upload should eject Expo, job does not support iOS, and fetch is a fetching task designed for doing task in intervals.
if there is a way to use mentioned libraries for my purpose, please guide me :)
to my understanding, the Firebase Cloud JSON API does not accept files, does it ? if so please give me an example. If I can make storage json API work with file upload, then I can use Expo asyncUpload probably without ejecting.
I have done something similar like you want, you can use expo-task-manager and expo-background-fetch. Here is the code as I used it. I Hope this would be useful for you.
import * as BackgroundFetch from 'expo-background-fetch';
import * as TaskManager from 'expo-task-manager';
const BACKGROUND_FETCH_TASK = 'background-fetch';
const [isRegistered, setIsRegistered] = useState(false);
const [status, setStatus] = useState(null);
//Valor para que se ejecute en IOS
BackgroundFetch.setMinimumIntervalAsync(60 * 15);
// Define the task to execute
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
const now = Date.now();
console.log(`Got background fetch call at date: ${new Date(now).toISOString()}`);
// Your function or instructions you want
return BackgroundFetch.Result.NewData;
});
// Register the task in BACKGROUND_FETCH_TASK
async function registerBackgroundFetchAsync() {
return BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
minimumInterval: 60 * 15, // 1 minutes
stopOnTerminate: false, // android only,
startOnBoot: true, // android only
});
}
// Task Status
const checkStatusAsync = async () => {
const status = await BackgroundFetch.getStatusAsync();
const isRegistered = await TaskManager.isTaskRegisteredAsync(
BACKGROUND_FETCH_TASK
);
setStatus(status);
setIsRegistered(isRegistered);
};
// Check if the task is already register
const toggleFetchTask = async () => {
if (isRegistered) {
console.log('Task ready');
} else {
await registerBackgroundFetchAsync();
console.log('Task registered');
}
checkStatusAsync();
};
useEffect(() => {
toggleFetchTask();
}, []);
Hope this isn't too late to be helpful.
I've been dealing with a variety of expo <-> firebase storage integrations recently, and here's some info that might be helpful.
First, I'd recommend not using the uploadBytes / uploadBytesResumable methods from Firebase. This Thread has a long ongoing discussion about it, but basically it's broken in v9. Maybe in the future the Firebase team will solve the issues, but it's pretty broken with Expo right now.
Instead, I'd recommend either going down the route of writing a small Firebase function that either gives a signed-upload-url or handles the upload itself.
Basically, if you can get storage uploads to work via an http endpoint, you can get any kind of upload mechanism working. (e.g. the FileSystem.uploadAsync() method you're probably looking for here, like #brentvatne pointed out, or fetch, or axios. I'll show a basic wiring at the end).
Server Side
Option 1: Signed URL Upload.
Basically, have a small firebase function that returns a signed url. Your app calls a cloud function like /get-signed-upload-url , which returns the url, which you then use. Check out: https://cloud.google.com/storage/docs/access-control/signed-urls for how you'd go about this.
This might work well for your use case. It can be configured just like any httpsCallable function, so it's not much work to set up, compared to option 2.
However, this doesn't work for the firebase storage / functions emulator! For this reason, I don't use this method, because I like to intensively use the emulators, and they only offer a subset of all the functionalities.
Option 2: Upload the file entirely through a function
This is a little hairier, but gives you a lot more fidelity over your uploads, and will work on an emulator! I like this too because it allows doing upload process within the endpoint execution, instead of as a side effect.
For example, you can have a photo-upload endpoint generate thumbnails, and if the endpoint 201's, then you're good! Rather than the traditional Firebase approach of having a listener to cloud storage which would generate thumbnails as a side effect, which then has all kinds of bad race conditions (checking for processing completion via exponentiational backoff? Gross!)
Here are three resources I'd recommend to go about this approach:
https://cloud.google.com/functions/docs/writing/http#multipart_data
https://github.com/firebase/firebase-js-sdk/issues/5848
https://github.com/mscdex/busboy
Basically, if you can make a Firebase cloud endpoint that accepts a File within formdata, you can have busboy parse it, and then you can do anything you want with it... like upload it to Cloud Storage!
an outline of this:
import * as functions from "firebase-functions";
import * as busboy from "busboy";
import * as os from "os";
import * as path from "path";
import * as fs from "fs";
type FieldMap = {
[fieldKey: string]: string;
};
type Upload = {
filepath: string;
mimeType: string;
};
type UploadMap = {
[fileName: string]: Upload;
};
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
export const uploadPhoto = functions.https.onRequest(async (req, res) => {
verifyRequest(req); // Verify parameters, auth, etc. Better yet, use a middleware system for this like express.
// This object will accumulate all the fields, keyed by their name
const fields: FieldMap = {};
// This object will accumulate all the uploaded files, keyed by their name.
const uploads: UploadMap = {};
// This will accumulator errors during the busboy process, allowing us to end early.
const errors: string[] = [];
const tmpdir = os.tmpdir();
const fileWrites: Promise<unknown>[] = [];
function cleanup() {
Object.entries(uploads).forEach(([filename, { filepath }]) => {
console.log(`unlinking: ${filename} from ${path}`);
fs.unlinkSync(filepath);
});
}
const bb = busboy({
headers: req.headers,
limits: {
files: 1,
fields: 1,
fileSize: MAX_FILE_SIZE,
},
});
bb.on("file", (name, file, info) => {
verifyFile(name, file, info); // Verify your mimeType / filename, etc.
file.on("limit", () => {
console.log("too big of file!");
});
const { filename, mimeType } = info;
// Note: os.tmpdir() points to an in-memory file system on GCF
// Thus, any files in it must fit in the instance's memory.
console.log(`Processed file ${filename}`);
const filepath = path.join(tmpdir, filename);
uploads[filename] = {
filepath,
mimeType,
};
const writeStream = fs.createWriteStream(filepath);
file.pipe(writeStream);
// File was processed by Busboy; wait for it to be written.
// Note: GCF may not persist saved files across invocations.
// Persistent files must be kept in other locations
// (such as Cloud Storage buckets).
const promise = new Promise((resolve, reject) => {
file.on("end", () => {
writeStream.end();
});
writeStream.on("finish", resolve);
writeStream.on("error", reject);
});
fileWrites.push(promise);
});
bb.on("close", async () => {
await Promise.all(fileWrites);
// Fail if errors:
if (errors.length > 0) {
functions.logger.error("Upload failed", errors);
res.status(400).send(errors.join());
} else {
try {
const upload = Object.values(uploads)[0];
if (!upload) {
functions.logger.debug("No upload found");
res.status(400).send("No file uploaded");
return;
}
const { uploadId } = await processUpload(upload, userId);
cleanup();
res.status(201).send({
uploadId,
});
} catch (error) {
cleanup();
functions.logger.error("Error processing file", error);
res.status(500).send("Error processing file");
}
}
});
bb.end(req.rawBody);
});
Then, that processUpload function can do anything you want with the file, like upload it to cloud storage:
async function processUpload({ filepath, mimeType }: Upload, userId: string) {
const fileId = uuidv4();
const bucket = admin.storage().bucket();
await bucket.upload(filepath, {
destination: `users/${userId}/${fileId}`,
{
contentType: mimeType,
},
});
return { fileId };
}
Mobile Side
Then, on the mobile side, you can interact with it like this:
async function uploadFile(uri: string) {
function getFunctionsUrl(): string {
if (USE_EMULATOR) {
const origin =
Constants?.manifest?.debuggerHost?.split(":").shift() || "localhost";
const functionsPort = 5001;
const functionsHost = `http://${origin}:${functionsPort}/{PROJECT_NAME}/${PROJECT_LOCATION}`;
return functionsHost;
} else {
return `https://{PROJECT_LOCATION}-{PROJECT_NAME}.cloudfunctions.net`;
}
}
// The url of your endpoint. Make this as smart as you want.
const url = `${getFunctionsUrl()}/uploadPhoto`;
await FileSystem.uploadAsync(uploadUrl, uri, {
httpMethod: "POST",
uploadType: FileSystem.FileSystemUploadType.MULTIPART,
fieldName: "file", // Important! make sure this matches however you want bussboy to validate the "name" field on file.
mimeType,
headers: {
"content-type": "multipart/form-data",
Authorization: `${idToken}`,
},
});
});
TLDR
Wrap Cloud Storage in your own endpoint, treat it like a normal http upload, everything plays nice.

Downloading a file from expo react-native problem

So in the following snipet everything works fine, problem is the FakeVaultDownloads folder is created inside the pictures folder in the devices storage. Is there something else I can use to create this folder inside the downloads folder? Issues is I am also downloading other file types like pdfs and txt documents and I don’t want those inside the pictures folder it doesn’t make sense those to be there, so I am trying to save them to a universal folder and not inside the pictures folder. Thanks for reading and sorry if I posted this in the wrong category any help is greatly appreciated.
let downloadedFile = await FileSystem.downloadAsync(res.data.url, `${FileSystem.documentDirectory}${res.data.name}`);
MediaLibrary.requestPermissionsAsync().then( async (res) => {
const asset = await MediaLibrary.createAssetAsync(downloadedFile.uri);
const album = await MediaLibrary.getAlbumAsync('Download');
if (album == null) {
await MediaLibrary.createAlbumAsync('Download', asset, false);
alert("File was downloaded to the Download folder ")
} else {
await MediaLibrary.addAssetsToAlbumAsync([asset], album, false);
alert("File was downloaded to the Download folder ")
}
}).catch(err => console.log("Error: ", err));
})
.catch((err) => {
console.log("Failed" , err);
alert("Response failed something is up with the server")

How to create csv file in expo react native

I'm still new to React Native may I know how can I create csv file in react native using expo? I've seen people suggesting expo-file-system as they said it is not recommended to use react-native-fs if using expo but I not sure how to use it. is it using FileSystem.writeAsStringAsync(fileUri, contents, options)?
Here is an example of a function that creates a simple .csv file
I'm using react-native-csv to generate the .csv.
expo-media-library is used to move the file from a folder accessible from the app to a publicly accessible folder.
import { jsonToCSV, readRemoteFile } from 'react-native-csv';
import * as FileSystem from 'expo-file-system';
import * as Permissions from 'expo-permissions';
import * as MediaLibrary from 'expo-media-library';
export async function makeCSV() {
const jsonData = `[
{
"Column 1": "Name",
"Column 2": "Surname",
"Column 3": "Email",
"Column 4": "Info"
}
]`;
const CSV = jsonToCSV(jsonData);
// Name the File
const directoryUri = FileSystem.documentDirectory;
const fileUri = directoryUri + `formData.csv`;
// Ask permission (if not granted)
const perm = await Permissions.askAsync(Permissions.MEDIA_LIBRARY);
if (perm.status != 'granted') {
console.log("Permission not Granted!")
return;
}
// Write the file to system
FileSystem.writeAsStringAsync(fileUri, CSV)
try {
const asset = await MediaLibrary.createAssetAsync(fileUri);
const album = await MediaLibrary.getAlbumAsync('forms');
console.log(album)
if (album == null) {
await MediaLibrary.createAlbumAsync('forms', asset, true);
} else {
await MediaLibrary.addAssetsToAlbumAsync([asset], album, true);
}
} catch (error) {
console.log(error);
}
}

Download using jsPDF on a mobile devices

I have a page that includes a download button using jsPDF. On desktop machines it downloads the page as it should. However, pdf.save() does not work on my tablet or phone.
I tried to add a special case for mobile devices to open the PDF in a new window, since mobile devices don't download things the same as desktops, with the idea being that once the PDF is open in a new window the user can choose to save it manually.
var pdf = new jsPDF('p', 'pt', 'letter');
var specialElementHandlers = {
'#editor': function (element, renderer) {
return true;
}
};
html2canvas($("#pdf-area"), {
onrendered: function (canvas) {
$("#pdf-canvas").append(canvas);
$("#pdf-canvas canvas").css("padding", "20px");
}
});
var options = {
pagesplit: true
};
function download(doctitle) {
pdf.addHTML($("#pdf-area")[0], options, function () {
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
pdf.output('dataurlnewwindow');
} else {
pdf.save(doctitle);
}
});
}
But the download function still does nothing on my tablet/phone. I tested it with this to make sure the pdf.output() function was working:
pdf.addHTML($("#pdf-area")[0], options, function () {
pdf.output('dataurlnewwindow');
});
and it does still work on desktop, but does nothing on mobile.
jsPDF won't download files on mobile apps by this pdf.save(). I have tried and searched for this issue but could not find a complete solution in one place. I am using the file and file-opener plugin. I have developed the solution in Ionic React. Install below modules.
npm i jspdf
npm install cordova-plugin-file
npm install #ionic-native/file
npm install cordova-plugin-file-opener2
npm install #ionic-native/file-opener
ionic cap sync
Go to your file and add these import statements.
import { jsPDF } from "jspdf";
import 'jspdf-autotable';
import { FileOpener } from '#ionic-native/file-opener;
import { File } from '#ionic-native/file';
import { isPlatform } from "#ionic/react";
Check the pdfDownload function
const pdfDownload = async () => {
var doc = new jsPDF();
doc.setFontSize(40);
doc.text("Example jsPDF", 35, 25)
let pdfOutput = doc.output();
if (isPlatform("android")) {
// for Android device
const directory = File.externalRootDirectory + 'Download/';
const fileName = `invoice-${new Date().getTime()}.pdf`
File.writeFile(directory, fileName, pdfOutput, true).then(succ => {
FileOpener.showOpenWithDialog(directory + fileName, 'application/pdf')
.then(() => console.log('File opened'))
.catch(error => console.log('Error opening file', error));
},
err => {
console.log(" writing File error : ", err)
})
} else if (isPlatform("ios")) {
// for iOS device
console.log('ios device')
const directory = File.tempDirectory;
const fileName = `invoice-${new Date().getTime()}.pdf`
File.writeFile(directory, fileName, pdfOutput, true).then(success => {
FileOpener.showOpenWithDialog(directory + fileName, 'application/pdf')
.then(() => console.log('File opened'))
.catch(e => console.log('Error opening file', e));
},
err => {
console.log(" writing File error : ", err)
})
} else {
// for desktop
doc.save("invoice.pdf");
}
}
I had similar issue.
jsPDF won't download file on phones/ tablets / ipads using "pdf.save()".
Do it through File plugin if you are using cordova/phonegap, this will save pdf file in downloads folder (Android) - for the ios you can access pdf file through a path (which is saved somewhere in temp directory) and can send or share.
Hope this helps you.
Here is the solution of download on mobile with jspdf
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))
{
var blob = pdf.output();
window.open(URL.createObjectURL(blob));
}
else
{
pdf.save('filename.pdf');
}
Here is the example if you're using the Cordova platform for your development:
https://github.com/saharcasm/Cordova-jsPDF-Email
The workaround of the pdf not being downloaded in the devices is to use cordova-plugin-file.
Use the output method on the doc that will give you the raw pdf which needs to be written & saved in a file.
For example,
var doc = new JsPDF();
//... some work with the object
var pdfOutput = doc.output("blob"); //returns the raw object of the pdf file
The pdfOutput is then written on an actual file by using the file plugin.
The easiest way which works on both Desktop and Mobile is to use:
window.open(doc.output("bloburl"), "_blank");