From 58b6db86249ad9368a92f202cfe7842c56330191 Mon Sep 17 00:00:00 2001 From: Amphiluke Date: Tue, 29 Nov 2022 18:24:32 +0700 Subject: [PATCH] Write a file in a single transaction --- package.json | 2 +- plugin.xml | 2 +- src/android/SaveDialog.java | 53 ++++++++------- www/android/SaveDialog.js | 125 +++++++++++++++++------------------- www/ios/SaveDialog.js | 29 +++++---- 5 files changed, 106 insertions(+), 105 deletions(-) diff --git a/package.json b/package.json index 06edbee..314f544 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-save-dialog", - "version": "1.1.1", + "version": "2.0.0", "description": "Cordova plugin for opening the native Save dialog and storing a file in the user-selected location", "main": "index.js", "scripts": { diff --git a/plugin.xml b/plugin.xml index 6cec7f5..59fa894 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,5 +1,5 @@ - + Save Dialog Cordova plugin for opening the native Save dialog and storing a file in the user-selected location MIT diff --git a/src/android/SaveDialog.java b/src/android/SaveDialog.java index 06d003d..a1eeed3 100644 --- a/src/android/SaveDialog.java +++ b/src/android/SaveDialog.java @@ -5,32 +5,39 @@ import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; -// import android.provider.DocumentsContract; -import android.util.Base64; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaArgs; -import org.json.JSONArray; import org.json.JSONException; +import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; -import java.nio.channels.FileChannel; public class SaveDialog extends CordovaPlugin { private static final int LOCATE_FILE = 1; private CallbackContext callbackContext; + private final ByteArrayOutputStream fileByteStream = new ByteArrayOutputStream(); @Override - public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException { this.callbackContext = callbackContext; - if (action.equals("locateFile")) { - this.locateFile(args.getString(0), args.getString(1)); - } else if (action.equals("saveFile")) { - this.saveFile(Uri.parse(args.getString(0)), args.getString(1), args.getBoolean(2)); - } else { - return false; + switch (action) { + case "locateFile": + this.locateFile(args.getString(0), args.getString(1)); + this.fileByteStream.reset(); + break; + case "addChunk": + this.addChunk(args.getArrayBuffer(0)); + break; + case "saveFile": + this.saveFile(Uri.parse(args.getString(0)), this.fileByteStream.toByteArray()); + this.fileByteStream.reset(); + break; + default: + return false; } return true; } @@ -40,9 +47,6 @@ private void locateFile(String type, String name) { intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(type); intent.putExtra(Intent.EXTRA_TITLE, name); - // TODO Optionally, specify a URI for the directory that should be opened in - // the system file picker when your app creates the document. - // intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); cordova.startActivityForResult(this, intent, SaveDialog.LOCATE_FILE); } @@ -64,18 +68,21 @@ public void onRestoreStateForActivityResult(Bundle state, CallbackContext callba this.callbackContext = callbackContext; } - private void saveFile(Uri uri, String data, boolean clearFile) { + private void addChunk(byte[] chunk) { try { - byte[] rawData = Base64.decode(data, Base64.DEFAULT); - ParcelFileDescriptor pfd = cordova.getActivity().getContentResolver().openFileDescriptor(uri, "wa"); - FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor()); + this.fileByteStream.write(chunk); + this.callbackContext.success(); + } catch (Exception e) { + this.callbackContext.error(e.getMessage()); + e.printStackTrace(); + } + } + private void saveFile(Uri uri, byte[] rawData) { + try { + ParcelFileDescriptor pfd = cordova.getActivity().getContentResolver().openFileDescriptor(uri, "w"); + FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor()); try { - if (clearFile) { - FileChannel fChan = fileOutputStream.getChannel(); - fChan.truncate(0); - } - fileOutputStream.write(rawData); this.callbackContext.success(uri.toString()); } catch (Exception e) { diff --git a/www/android/SaveDialog.js b/www/android/SaveDialog.js index 13db2d6..e597116 100644 --- a/www/android/SaveDialog.js +++ b/www/android/SaveDialog.js @@ -3,77 +3,66 @@ let {keep: keepBlob, get: getBlob, clear: clearBlob} = require("./BlobKeeper"); let moduleMapper = require("cordova/modulemapper"); let FileReader = moduleMapper.getOriginalSymbol(window, "FileReader") || window.FileReader; -let locateFile = (type, name) => new Promise((resolve, reject) => { - exec(resolve, reject, "SaveDialog", "locateFile", [type || "application/octet-stream", name]); -}); - -let saveFile = (uri, blob, clearFile) => new Promise((resolve, reject) => { - let reader = new FileReader(); - reader.onload = () => { - exec(resolve, reject, "SaveDialog", "saveFile", [uri, reader.result, clearFile]); - }; - reader.onerror = () => { - reject(reader.error); - }; - reader.onabort = () => { - reject("Blob reading has been aborted"); - }; - reader.readAsArrayBuffer(blob); -}); - -let saveFileInChunks = (uri, blob) => { - const BLOCK_SIZE = 1024 * 1024; - let writtenSize = 0; - - function saveNextChunk(clearFile) { - const size = Math.min(BLOCK_SIZE, blob.size - writtenSize); - const chunk = blob.slice(writtenSize, writtenSize + size); - - writtenSize += size; - - return saveFile(uri, chunk, clearFile); - } - - return new Promise(async (resolve, reject) => { - let i = 0; - let uri = ''; - let error = null; - - while(writtenSize < blob.size) { - [uri, error] = await saveNextChunk(i === 0).then((result) => [result, null]).catch((err) => [null, err]); +function blobToArrayBuffer(blob) { + // Using FileReader.readAsArrayBuffer() until Blob.arrayBuffer() is widely supported + return new Promise((resolve, reject) => { + let reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.onerror = () => { + reject(reader.error); + }; + reader.onabort = () => { + reject("Blob reading has been aborted"); + }; + reader.readAsArrayBuffer(blob); + }); +} - if (error !== null) { - reject(error); - return; - } +function asyncExec(action, ...args) { + return new Promise((resolve, reject) => { + exec(resolve, reject, "SaveDialog", action, args); + }); +} - i++; +// Transfer file contents to the plugin chunk by chunk to overcome the limitation on the size of data that can be +// converted to an array buffer, serialized and passed to the native Java component (see PR #2). +async function addChunks(blob) { + let getBlobChunks = function* () { + const CHUNK_SIZE = 1024 ** 2; // 1 MB + let transferredLength = 0; + while (transferredLength < blob.size) { + yield blob.slice(transferredLength, transferredLength + CHUNK_SIZE); + transferredLength += CHUNK_SIZE; } - - resolve(uri); - }); + return null; + }; + for (let blobChunk of getBlobChunks()) { + let arrayBufferChunk = await blobToArrayBuffer(blobChunk); + await asyncExec("addChunk", arrayBufferChunk); + } } module.exports = { - saveFile(blob, name = "") { - return keepBlob(blob) // see the “resume” event handler below - .then(() => locateFile(blob.type, name)) - .then(uri => saveFileInChunks(uri, blob)) - .then(uri => { - clearBlob(); - return uri; - }) - .catch(reason => { - clearBlob(); - return Promise.reject(reason); - }); + async saveFile(blob, name = "") { + try { + await keepBlob(blob); // see the “resume” event handler below + let uri = await asyncExec("locateFile", blob.type || "application/octet-stream", name); + await addChunks(blob); + return await asyncExec("saveFile", uri); + } catch (reason) { + return Promise.reject(reason); + } finally { + clearBlob(); + } } }; // If Android OS has destroyed the Cordova Activity in background, try to complete the Save operation // using the URI passed in the payload of the “resume” event and the blob stored by the BlobKeeper. -// https://cordova.apache.org/docs/en/10.x/guide/platforms/android/plugin.html#launching-other-activities -document.addEventListener("resume", ({pendingResult = {}}) => { +// https://cordova.apache.org/docs/en/11.x/guide/platforms/android/plugin.html#launching-other-activities +document.addEventListener("resume", async ({pendingResult = {}}) => { if (pendingResult.pluginServiceName !== "SaveDialog") { return; } @@ -81,12 +70,14 @@ document.addEventListener("resume", ({pendingResult = {}}) => { clearBlob(); return; } - getBlob().then(blob => { - if (blob instanceof Blob) { - saveFile(pendingResult.result, blob).catch(reason => { - console.warn("[SaveDialog]", reason); - }); + let blob = await getBlob(); + if (blob instanceof Blob) { + try { + await addChunks(blob); + await asyncExec("saveFile", pendingResult.result); + } catch (reason) { + console.warn("[SaveDialog]", reason); } - clearBlob(); - }); + } + clearBlob(); }, false); diff --git a/www/ios/SaveDialog.js b/www/ios/SaveDialog.js index b8454a3..92fb79f 100644 --- a/www/ios/SaveDialog.js +++ b/www/ios/SaveDialog.js @@ -2,19 +2,22 @@ let exec = require("cordova/exec"); let moduleMapper = require("cordova/modulemapper"); let FileReader = moduleMapper.getOriginalSymbol(window, "FileReader") || window.FileReader; -let saveFile = (blob, name) => new Promise((resolve, reject) => { - let reader = new FileReader(); - reader.onload = () => { - exec(resolve, reject, "SaveDialog", "saveFile", [reader.result, name]); - }; - reader.onerror = () => { - reject(reader.error); - }; - reader.onabort = () => { - reject("Blob reading has been aborted"); - }; - reader.readAsArrayBuffer(blob); -}); +function saveFile(blob, name) { + // Using FileReader.readAsArrayBuffer() until Blob.arrayBuffer() is widely supported + return new Promise((resolve, reject) => { + let reader = new FileReader(); + reader.onload = () => { + exec(resolve, reject, "SaveDialog", "saveFile", [reader.result, name]); + }; + reader.onerror = () => { + reject(reader.error); + }; + reader.onabort = () => { + reject("Blob reading has been aborted"); + }; + reader.readAsArrayBuffer(blob); + }); +} module.exports = { saveFile(blob, name = "untitled") {