Cordova FileReader FileWriter Performance (Large Files) - javascript

I'm using Cordova for an IOS/Android/Chrome music streaming app. I need to store media files on the target device. These files are at least 5mb each.
I've been using the Cordova file plugin to write and read (and manipulate) these files using javascript.
I find the performance of the Cordova File plugin's 'FileReader' and 'FileWriter' to be inadequate. The standard example code (where you create a FileReader object, then read, wait for the reader.onloadend) to read a file of 5mb takes a minimum of 3 seconds. This is on modern hardware (both IOS (IPOD touch device) and Android (Amlogic S912). On a PC running chrome, with HTML5 FileSystem API code - this same procedure takes milliseconds.
The Cordova FileWriter has the same results.
I was able to fix my FileReader results by using an XMLHttpRequest to the file stored within the file system. This reads the same files (5mb) in well under a second. This tells me there is a problem with the Cordova plugin code. But I can't find any way to improve my 'FileWriter' performance. I've experimented with writing files in chunks, rather than all at once (various sized chunks, 0.1mb - 1mb) - but this hasn't helped.
Has anyone else had problems with this? It just doesn't seem right.
I'm running Cordova version 6.4.0. Cordova-Plugin-File is 4.3.0
Here's some sample code for the FileReader:
Here's the Cordova way (3 seconds + for a 5mb file):
function ReadFile_APIWay(pathEntry, fileName, success, error) {
console.log("read file with api start: " + new Date());
_fs.root.getFile(AppendForwardSlash(pathEntry.fullPath) + fileName, { create: false }, function (entry) {
entry.file(
function (file) {
var reader = new FileReader();
reader.onloadend = function () {
console.log("read file with api finish: " + new Date());
var result = this.result;
success(result, entry);
};
reader.readAsArrayBuffer(file);
}, function (e) {
console.log("read file with api fail: " + e);
error(e);
});
}
, function (e) {
console.log("read file with api fail: " + e);
error(e);
});
}
And Here's the AJAX way (fraction of a second for a 5mb file)
function ReadFile_AJAXWay(pathEntry, fileName, success, error) {
console.log("read file with ajax start: " + new Date());
var xhr = new XMLHttpRequest();
xhr.open('GET', AppendForwardSlash(pathEntry.toURL()) + fileName, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function (e) {
if (this.status == 0) {
console.log("read file with ajax finish: " + new Date());
var result = e.target.response;
_fs.root.getFile(pathEntry.fullPath + fileName, { create: false }
, function (entry) {
success(result, entry);
}, function (e) {
error(e);
});
}
};
xhr.onerror = function (e) {
console.log("read file with ajax fail: " + e);
error(e);
}
xhr.send();
}

Related

Download binary data into the app sandbox using XHR2 request instead of cordova-file-transfer

Cordova is "sunsetting" (going to deprecate) cordovan-plugin-file, see their blogpost.
No more work will be done on the file-transfer plugin by the Cordova development community.
You can continue to use the file-transfer plugin if you wish - it should work fine as-is for the foreseeable future.
We highly suggest Cordova users transition to using the standards-compliant way of sending and receiving binary data.
They are encouraging a transition to use XHR2 requests (XHR Requests where the responseType is set to Blob or ArrayBuffer.
The blog post wants to provide an example how binary data can be fetched using XHR2:
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function (fs) {
console.log('file system open: ' + fs.name);
fs.root.getFile('bot.png', { create: true, exclusive: false }, function (fileEntry) {
console.log('fileEntry is file? ' + fileEntry.isFile.toString());
var oReq = new XMLHttpRequest();
// Make sure you add the domain name to the Content-Security-Policy <meta> element.
oReq.open("GET", "http://cordova.apache.org/static/img/cordova_bot.png", true);
// Define how you want the XHR data to come back
oReq.responseType = "blob";
oReq.onload = function (oEvent) {
var blob = oReq.response; // Note: not oReq.responseText
if (blob) {
// Create a URL based on the blob, and set an <img> tag's src to it.
var url = window.URL.createObjectURL(blob);
document.getElementById('bot-img').src = url;
// Or read the data with a FileReader
var reader = new FileReader();
reader.addEventListener("loadend", function() {
// reader.result contains the contents of blob as text
});
reader.readAsText(blob);
} else console.error('we didnt get an XHR response!');
};
oReq.send(null);
}, function (err) { console.error('error getting file! ' + err); });}, function (err) { console.error('error getting persistent fs! ' + err); });
I have some issues understanding the code above and the intention of cordova to drop the file-tranfer plugin in favour of
directly fetching the Blobs via Ajax.
Am I seeing this right:
fs.root.getFile creates a file. The download success handler (oReq.onload) does not attempt
to write the fetched blob to the created file. There is no clear reason why the fileEntry is created.
If I would want to save the fetched blob to the created FileEntry, within oReq.onload
I could go on using a FileWriter, but only for small (I read up to 5 MB) files (since the Blob is handled in-memory).
The blog post is more about how a blob can be fetched in general and not about it can
be downloaded into the filesystem. If I would want to download bigger files (like a couple of 100 MB),
moving away from cordova-plugin-filetransfer is not an option at the moment.
With this code you can download big images as they are written by blocks of 1MB instead of doing the whole write at once.
Without the 1MB writting I wasn't able to write files bigger than 4MB, but with this I've tested with files up to 40MB without problems
window.resolveLocalFileSystemURL(cordova.file.externalDataDirectory,
function (dirEntry) {
console.log('file system open: ' + dirEntry.name);
createFile(dirEntry, "downloadedImage.jpg");
}, onFSError);
function onFSError(error) {
alert(JSON.stringify(error));
}
function createFile(dirEntry, fileName) {
// Creates a new file or returns the file if it already exists.
dirEntry.getFile(fileName, {create: true, exclusive: false}, function(fileEntry) {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://static.vix.com/es/sites/default/files/styles/large/public/imj/3/30-cosas-de-los-gatos-que-no-sabias-3.jpg', true);
xhr.responseType = 'blob';
xhr.onload = function() {
if (this.status == 200) {
var blob = new Blob([this.response], { type: 'image/jpeg' });
writeFile(fileEntry, blob);
}
};
xhr.send();
}, onFSError);
}
function writeFile(fileEntry, data) {
// Create a FileWriter object for our FileEntry (log.txt).
fileEntry.createWriter(function (fileWriter) {
fileWriter.onerror = function(e) {
console.log("Failed file write: " + e.toString());
};
function writeFinish() {
function success(file) {
alert("Wrote file with size: " + file.size);
}
function fail(error) {
alert("Unable to retrieve file properties: " + error.code);
}
fileEntry.file(success, fail);
}
var written = 0;
var BLOCK_SIZE = 1*1024*1024; // write 1M every time of write
function writeNext(cbFinish) {
fileWriter.onwrite = function(evt) {
if (written < data.size)
writeNext(cbFinish);
else
cbFinish();
};
if (written) fileWriter.seek(fileWriter.length);
fileWriter.write(data.slice(written, written + Math.min(BLOCK_SIZE, data.size - written)));
written += Math.min(BLOCK_SIZE, data.size - written);
}
writeNext(writeFinish);
});
}

readAsText(file) only reads the last file in a directory

I'm trying to read all the files in a user's directory and display their content in a text box.
Reading single files works perfectly, however, when I try to read a whole directory, things are getting weird.
While iterating through a directory, only the last file in the directory is read correctly. This behavior is consistent no matter how many files are in the directory.
Here's the code I use for reading the files:
results.forEach(function(item) {
reader = new FileReader();
// This line is reached
console.log("filename: " + item.name);
item.file(function(File) {
// This one only for the last file in that directory
reader.readAsText(File);
console.log("success");
});
// This line is reached
console.log("read: " + item.name);
});
Here's the log (from the dev tools):
filename: app.js
read: app.js
filename: main.js
read: main.js
filename: SharedPreferences.js
read: SharedPreferences.js
filename: KeyConstants.js
read: KeyConstants.js
success
If you have any questions, please ask them, I'm trying this for hours now and I'm slowly getting tired of failing over and over ..
This happens because FileReader works asynchronously, which means approximately that it starts executing a task (reading the file) while the code continues to be executed. If you want to do something with the result for each file as soon as the load is finished, you need to play with this method:
reader.onloadend = function(evt) {
// file is loaded
// do something with evt.target object
};
My final solution:
Like abcdn said, the problem was that I was overriding the reader with a new one.
I solved this by using javascript closures (which I had no idea existed because I'm coming from C#).
Here's the full code I used in the end:
chrome.fileSystem.chooseEntry({type: "openDirectory"}, function(dir) {
readFolderAsArrayBuffer(dir, function() {
console.log("read folder");
});
});
function readFolderAsArrayBuffer(dir, callback) {
if (dir && dir.isDirectory) {
var reader = dir.createReader();
var handlefile = function (entries) {
for (var i = 0; i < entries.length; i++) {
arr[i] = (function(fileEntry, number) {
console.log("returning function " + number);
entries[number].file(function(file) {
handleread(fileEntry, file);
console.log("reading" + url);
});
})(entries[i], i);
}
}
var handleerror = function() {
console.log("error");
};
reader.readEntries(handlefile, handleerror);
}
}
var handleread = function(fileEntry, file) {
var fileReader = new FileReader();
fileReader.onloadend = function(evt) {
console.log("Read file: " + fileEntry.name + "with the content: " + evt.target.result);
};
fileReader.readAsText(file);
}
This reads a whole user selected directory and outputs each file's content to the console.

WKWebView web worker throws error "Dom Exception 18 returns an error."

I am writing an app using PhoneGap. I want to use javascript Web Worker. It works fine on Android. It also works on iOS with UIWebView.
I would like to use WKWebView in iOS 9. I tried to read the .txt files in a local folder successfully using cordova-plugin-file.
I have used the following tutorial:
https://www.scirra.com/blog/ashley/25/hacking-something-useful-out-of-wkwebview
But this did not solve it...
new Worker ('js/tested.js');
Dom Exception 18 returns an error.
new Worker ('www/js/tested.js');
Dom Exception 18 returns an error.
I tried to specify the full path to worker javascript file, but I get the same error:
new Worker (file_path+'js/tested.js');
Dom Exception 18 returns an error.
So how to create the worker?
It worked!
Web workers without a separate Javascript file?
var worker_js = null;
function fetchLocalFileViaCordova(filename, successCallback, errorCallback)
{
var path = cordova.file.applicationDirectory + "www/" + filename;
window.resolveLocalFileSystemURL(path, function (entry)
{
entry.file(successCallback, errorCallback);
}, errorCallback);
};
function fetchLocalFileViaCordovaAsText(filename, successCallback, errorCallback)
{
fetchLocalFileViaCordova(filename, function (file)
{
var reader = new FileReader();
reader.onload = function (e)
{
successCallback(e.target.result);
};
reader.onerror = errorCallback;
reader.readAsText(file);
}, errorCallback);
};
fetchLocalFileViaCordovaAsText("js/worker.js", function (js_script) { // Worker WKWebView
worker_js = window.URL.createObjectURL(
new Blob([js_script], { type: "text/javascript" })
);
});
if (worker_js != null) {
backgroundEngine = new Worker(worker_js);
} else {
backgroundEngine = new Worker("js/worker.js");
}

Does anyone know why using 'fileEntry.file' keeps failing in my Windows 8 app when trying to read a file?

Does anyone know why using 'fileEntry.file' keeps failing in my Windows 8 app?
If I use the following code it fails:
Windows.Storage.StorageFile.getFileFromApplicationUriAsync(new Windows.Foundation.Uri(cordova.file.applicationDirectory + 'www/assets/pages/en/navigation.html')).done(usethisfile, fail);
function usethisfile(fileEntry) {
console.log("Im going to use the file... " + fileEntry.path);
fileEntry.file(function (file) {
var reader = new FileReader();
reader.onloadend = function() {
console.log("Successful file read: " + this.result);
};
reader.readAsText(fileEntry);
}, onErrorReadFile);
}
but if I remove the 'fileEntry.file' part it works fine:
Windows.Storage.StorageFile.getFileFromApplicationUriAsync(new Windows.Foundation.Uri(cordova.file.applicationDirectory + 'www/assets/pages/en/navigation.html')).done(usethisfile, fail);
function usethisfile(fileEntry) {
console.log("Im going to use the file... " + fileEntry.path);
//fileEntry.file(function (file) {
var reader = new FileReader();
reader.onloadend = function() {
console.log("Successful file read: " + this.result);
};
reader.readAsText(fileEntry);
//}, onErrorReadFile);
}
The official docs say to use 'fileEntry.file': https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-file/index.html and I already have the app running on both the Android and the Apple stores so I'm hoping I can continue to use all the current functions that already use 'fileEntry.file' for the Windows version.
The error I get is:
0x800a01b6 - JavaScript runtime error: Object doesn't support property or method 'file'.
I'm using Cordova via the command-line and Visual Studio to run it if that helps at all.
Not 100% sure but try adding e argument when you define the onloadend method

Can Javascript check a file for content changes?

Example:
I have
var r = new FileReader();
r.onload = function(e) {
drawGraph(r.result);
}
r.readAsText(f);
drawing a graph from the file f input by the user.
Is there a way to check to see if the file f has been changed and then re load it's content without needing for the user to pick the file again?
Yes, this can be achieved using Node.js's filesystem API which provides a "watch" function
NodeJS Filesystem API docs: https://nodejs.org/api/fs.html
Similar question:
Observe file changes with node.js
fs.watch('somedir', function (event, filename) {
console.log('event is: ' + event);
if (filename) {
console.log('filename provided: ' + filename);
} else {
console.log('filename not provided');
}
});

Categories

Resources