Phonegap - How to access file in www-folder? - javascript

I saw multiple solutions how to access a file in the www folder but no solution works for me. I test the application under iOS with the iOS-simulator.
I want to access the file test.txtin the www folder.
My current solution looks like this:
var filePathURI = getPhoneGapPath() + "test.txt";
window.resolveLocalFileSystemURI(filePathURI, onResolveSuccess, onFail);
function getPhoneGapPath() {
'use strict';
var path = window.location.pathname;
var phoneGapPath = path.substring(0, path.lastIndexOf('/') + 1);
return phoneGapPath;
};
This solution does not work for me. I get an error with errorCode = 2 which obviously means FileError.SECURITY_ERR. However I try, with resolveLocalFileSystemURI I can not access to the file.
INFO: I tried following filePathURI:
/Users/UserName/Library/Application%20Support/iPhone%20Simulator/7.0/Applications/GUID/AppName.app/www/test.txt
file:///Users/UserName/Library/Application%20Support/iPhone%20Simulator/7.0/Applications/GUID/AppName.app/www/test.txt
Can anyone give me a working solution?

I would suggest utilizing the resolveLocalFileSystemURL method provided by PhoneGap's file plugin. You can then use the cordova.file.applicationDirectory property to access where your www folder is located.
Make sure you install the plugin: $ cordova plugin add org.apache.cordova.file
Then you could use an object such as the following to parse the files and do whatever is needed:
var FileManager = {
/**
* Execute this.entryHandler against all files and directories in phonegap's www folder
*/
run: function () {
window.resolveLocalFileSystemURL(
cordova.file.applicationDirectory + 'www/',
this.directoryFoundHandler,
this.errorHandler
);
},
/**
* The directory has been successfully read. Now read the entries.
*
* #param {DirectoryEntry} directoryEntry
*/
directoryFoundHandler: function (directoryEntry) {
var directoryReader = directoryEntry.createReader();
directoryReader.readEntries(
this.entryHandler,
this.errorHandler
);
},
/**
* Files were successfully found. Parse them!
*
* #param {Array.<FileEntry>} entries
*/
entryHandler: function (entries) {
entries.forEach(function (entry) {
// Deal with your files here
if (entry.isDirectory) {
// It's a directory might need to loop through again
} else {
// It's a file, do something
}
});
},
/**
* #param {FileError} error
*/
errorHandler: function (error) {
console.log("ERROR", error);
}
};

I load my language files with ajax like this...
$.get( "test.txt", function( data ) {
console.log( "Load was performed.", data );
});
I think for your solution you have to add read access to your app --> config.xml
<feature name="http://api.phonegap.com/1.0/file" />

Try this, part of my functions. You first need to get the file system and then get the root path. Modify it to fit your needs.
The you just can do the following.
app_FileSystem for me is a global variable that gets assigned by GetAppFS
After getting the FS and the root path you can just simple use a ajax call or a getjson call with the appropriate dataType set. It works for me.
Also check the doc which is helpful:
http://docs.phonegap.com/en/3.3.0/cordova_file_file.md.html#LocalFileSystem
app_FileSystem.root.fullPath; // Get the app file system root full path
function GetAppFS ()
{
var self = this;
self.state = ""; // store the state of the process for debuggin purposes
self.fileSystem = {};
window.requestFileSystem ( LocalFileSystem.PERSISTENT, 0, getFileSystemSuccess, dispatchFailure );
/**
*
* Called when we receive a valid file system. Once we do that, we need to ask for all
* the documents within the file system.
*
*/
function getFileSystemSuccess ( fileSystem )
{
self.state = "Received File System";
self.fileSystem = fileSystem;
app_FileSystem = fileSystem;
OnFSReady ();
};
/**
*
* All our functions need a failure callback, so we provide dispatchFailure. If an error occurs, we'll
* at least log it to the console, and then call the failure function attached to self.failure(), if any.
*
*/
function dispatchFailure ( e )
{
// some sort of failure :-(
console.log ("While " + self.state + ", encountered error: " + JSON.stringify(e));
alert ("dev FS ERROR ");
};
};

As I just ran into the same problem but did not want to use jQuery, I thought I post my solution here as well.
But before that an import remark: The files in the www folder of Cordova / Phone Gap are stored in the Android world as so called assets which means:
They are part of the .apk distribution file which is a zipped archive. Android reads the files directly from this .apk file and does not store these files separately in the local file system.
Therefore the files are read only and cannot be accessed with the Cordova File plugin.
If you take a deep dive in the corresponding Android sources of Cordova you can see, that Cordova filters all URIs with a 'file' scheme, whose path starts with '/android_asset/' and handles them specially using Android's asset access functions. (Would be interesting to hear from the iOS experts how Cordova handles it in their world.)
This means all in all that using a XMLHttpRequest is probably the only portable way to access www folder files if you need access to the file contents. (If you only need the path to the file for some system functions other methods may work as well.)
Here is the code, filename is the path within the www folder without a "www/" prefix:
var readFileInWWWFolder = function(filename, onSuccess, onFailure){
var request = new XMLHttpRequest();
request.onload = function() {
var arrayBuffer = request.response;
if (arrayBuffer) {
onSuccess(new Uint8Array(arrayBuffer));
}
else {
onFailure();
}
};
request.open("GET", filename, true);
request.responseType = "arraybuffer";
request.send();
};
This has been tested with Cordova 4.3.0 and Android 4.4.2 (Kitkat).

One trick that works is to fs.download each file from the www folder into Cordova’s persistent file system. See my original post.
First, in Terminal:
npm install cordova-promise-fs
cordova plugin add cordova-plugin-file --save
cordova plugin add cordova-plugin-file-transfer --save
Then, in your front-end:
import CordovaPromiseFS from 'cordova-promise-fs'
const fs = CordovaPromiseFS({
persistent: true,
storageSize: 200 * 1024 * 1024,
concurrency: 3
})
If you use React, the above has to be declared before the component Class is created, while the below code should be in its own function inside the component Class. See my GitHub comment for more details.
window.resolveLocalFileSystemURL(
cordova.file.applicationDirectory + 'www/epubs/alice.epub',
// If successful...
(fileSystem) => {
const downloadUrl = fileSystem.toURL()
const localUrl = 'alice.epub' // the filename it is stored as in the device
fs.download(
downloadUrl,
localUrl,
(progressEvent) => {
if (progressEvent.loaded && progressEvent.total) {
console.log('progress', Math.round((progressEvent.loaded / progressEvent.total) * 100))
}
}
).then((filedata) => {
return fs.toInternalURL(localUrl)
})
.then((localPath) => {
this.setState({ epubPath: localPath })
}).catch((error) => {
console.log('some error happend', error)
})
},
// If unsuccessful
(err) => {
console.log(err)
}
)

Related

Load offline lang data in tesseract.js

I am trying to load my own trained data to tesseract.js. As the file is placed locally, I tried to load everything offline. The code I used is shown below:
<script src="tesseract.js"></script>
<script>
//Set the worker, core and lang to local files
(function() {
var path = (function() { //absolute path
var pathArray = window.location.pathname.split( '/' );
pathArray.pop(); //Remove the last ("**.html")
return window.location.origin + pathArray.join("/");
})();
console.log(path);
window.Tesseract = Tesseract.create({
workerPath: path + '/worker.js',
//langPath: path + '/traineddata/',
corePath: path + '/index.js',
});
})();
</script>
<script>
function recognizeFile(file){
document.querySelector("#log").innerHTML = ''
Tesseract.recognize(file, {
lang: document.querySelector('#langsel').value
})
.progress(function(packet){
console.info(packet)
progressUpdate(packet)
})
.then(function(data){
console.log(data)
progressUpdate({ status: 'done', data: data })
})
}
</script>
The code above is working fine if the langPath is not set, but when I point the langPath to a local folder, Tesseract failed to load anything with the following error:
Failed loading language 'eng'
Tesseract couldn't load any languages!
...
AdaptedTemplates != NULL:Error:Assert failed:in file ../classify/adaptmatch.cpp, line 190
SCRIPT0: abort() at Error
at Na (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:36:24)
at ka (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:511:83)
at Module.de._abort (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:377:166)
at $L (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:387:55709)
at jpa (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:392:22274)
at lT (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:391:80568)
at mT (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:391:80698)
at BS (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:391:69009)
at bP (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:387:110094)
at jT (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:391:80280)
at RJ (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:387:19088)
at QJ (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:387:17789)
at zI (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:403:90852)
at tw (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:401:49079)
at rw (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:401:48155)
at lw (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:401:39071)
at _v (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:401:22565)
at aw (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:401:24925)
at cw (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:401:27237)
at oj (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:386:24689)
at Og (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:386:10421)
at $.prototype.Recognize (file:///C:/Users/user/Downloads/tesseract.js-master/dist/index.js:558:379)
at Anonymous function (file:///C:/Users/user/Downloads/tesseract.js-master/dist/worker.js:8814:9)
at Anonymous function (file:///C:/Users/user/Downloads/tesseract.js-master/dist/worker.js:8786:9)
at xhr.onerror (file:///C:/Users/user/Downloads/tesseract.js-master/dist/worker.js:8429:9)
If this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.
index.js (8,1)
I have both eng.traineddata and eng.traineddata.gz in the /traineddata folder as apparently the ungzip process is skipped. Is there anything I neglected? Any help is appreciated.
I know this question is an old but recently I needed to use Tesseract.js in one of my projects. I needed to load Data Files locally so here is what I have done.
Instead of creating a new worker. I modified the default worker options available. So I didn't use Tesseract.createWorker and directly set the path and used recognize instead.
Tesseract.workerOptions.langPath =
window.location.origin // take protocol://domain.com part
+ "/scripts/tesseract/dist/"; // location of data files
//you could set core and worker paths too but I didn't need it
Tesseract.workerOptions.workerPath =
window.location.origin // take protocol://domain.com part
+ "/scripts/tesseract/dist/worker.js"; // location of worker.js
//you could set core and worker paths too but I didn't need it
Tesseract.workerOptions.corePath =
window.location.origin // take protocol://domain.com part
+ "/scripts/tesseract/dist/index.js"; // location of index.js
//example lang path would be protocol://domain.com/scripts/tesseract/dist/
By doing this, I left the worker and core paths untouched pointing to Default CDN.
PS: When using local worker.js and core.js paths I was getting uncaught error on postMessage() in worker.js. That's why I am using local path for langData only. I still don't know how to fix it or why it is happening. But, You can follow this issue here and here
I solved the problem by taking the corePath file from tesseract.js-core 0.1.0
window.Tesseract = Tesseract.create({
workerPath: window.location.origin + "/tesseract/worker.js", //tesseract.js-1.0.10
langPath: window.location.origin + "/tesseract/",
corePath: window.location.origin + "/tesseract/index.js", //tesseract.js-core-0.1.0
});
And language gz from https://github.com/naptha/tessdata/tree/gh-pages/3.02

How can I pass parameters when calling a Node.js script from PHP exec()?

I'm trying to implement iOS push notifications. My PHP version stopped working and I haven't been able to get it working again. However, I have a node.js script that works perfectly, using Apple's new Auth Key. I am able to call that from PHP using:
chdir("../apns");
exec("node app.js &", $output);
However, I would like to be able to pass the deviceToken and message to it. Is there any way to pass parameters to the script?
Here's the script I'm trying to run (app.js):
var apn = require('apn');
var apnProvider = new apn.Provider({
token: {
key: 'apns.p8', // Path to the key p8 file
keyId: '<my key id>', // The Key ID of the p8 file (available at https://developer.apple.com/account/ios/certificate/key)
teamId: '<my team id>', // The Team ID of your Apple Developer Account (available at https://developer.apple.com/account/#/membership/)
},
production: false // Set to true if sending a notification to a production iOS app
});
var deviceToken = '<my device token>';
var notification = new apn.Notification();
notification.topic = '<my app>';
notification.expiry = Math.floor(Date.now() / 1000) + 3600;
notification.badge = 3;
notification.sound = 'ping.aiff';
notification.alert = 'This is a test notification \u270C';
notification.payload = {id: 123};
apnProvider.send(notification, deviceToken).then(function(result) {
console.log(result);
process.exit(0)
});
You can pass parameters as you would pass it to any other script.
node index.js param1 param2 paramN
You can access the arguments through process.argv
The process.argv property returns an array containing the command line
arguments passed when the Node.js process was launched. The first
element will be process.execPath. See process.argv0 if access to the
original value of argv[0] is needed. The second element will be the
path to the JavaScript file being executed. The remaining elements
will be any additional command line arguments.
exec("node app.js --token=my-token --mesage=\"my message\" &", $output);
app.js
console.log(process.argv);
/*
Output:
[ '/usr/local/bin/node',
'/your/path/app.js',
'--token=my-token',
'--mesage=my message' ]
*/
You can use minimist to parse the arguments for you:
const argv = require('minimist')(process.argv.slice(2));
console.log(argv);
/*
Output
{
_: [],
token: 'my-token',
mesage: 'my message'
}
*/
console.log(argv.token) //my-token
console.log(argv.message) //my-message
After a lot of work, I have a solution. I am using a local web UI to send and receive data to/from Arduino
using AJAX, the php script is:
<?php
/**
* It is already not necessary to go to Device Manager> Ports (COM & LPT)>Arduino XXX (COMXX)>right, NO, the script PruebaCOMRetrieve.bat detect the COM port Arduino is connected to.
* click>Properties>
* Port Settings>Advanced>uncheck "use FIFO buffers ........."
* In other hand, remeber that the Tx speed has to be the same in writeandread.js as in
* Arduino sketch and in the COM
* properties in Device manager, I selected 115200 b/s.
*
*/
$t = $_POST['text1'];
include 'PruebaBatchCOM.php';
$puerto = escapeshellarg($usbCOM);
$dato = escapeshellarg($t);
exec("node C:\\xampp\\htdocs\\DisenoWEBTerminados\\BatteryTesterFinal\\Scripts\\writeandread.js
{$puerto} {$dato} 2>&1", $output1);
$str = implode($output1);
$str1 = explode(",",$str);
$myJSON = json_encode($str1);
echo $myJSON;
?>
PruebaBatchCOM.php
<?php
$puerto = array();
$file111 = "PruebaCOMRetrieve.bat";
exec($file111, $puerto);
$usbCOM = implode(",",$puerto);
?>
PruebaCOMRetrieve.bat
#echo off
setlocal
for /f "tokens=1* delims==" %%I in ('wmic path win32_pnpentity get caption
/format:list ^| find "Arduino Uno"') do (
call :setCOM "%%~J"
)
:: end main batch
goto :EOF
:setCOM <WMIC_output_line>
:: sets _COM#=line
setlocal
set "str=%~1"
set "num=%str:*(COM=%"
set "num=%num:)=%"
set port=COM%num%
echo %port%
The Node Js script is:
//writeandread.js
var portName = process.argv[2];
var dato = process.argv[3];
var SerialPort = require("serialport");
var Readline = require('#serialport/parser-readline');
var serialport = new SerialPort(portName, { baudRate: 115200 });
// Look for return and newline at the end of each data packet
var parser = serialport.pipe(new Readline({ delimiter: '\n' }));
serialport.on('open', function(err) {
// A timeout is necessary to wait the port to open (if not working, try to
increase the milliseconds value)
setTimeout(function() {
serialport.write(dato);
}, 1700);
if(err) {
console.log('Error when trying to open:' + err);
}
parser.on('data', function(data) {
console.log(data);
serialport.close(function (err) {
if(err){
console.log('port closed', err);
}
});
});
});
serialport.on('close', () => {
console.log('Bye');
});
With this scripts one can send and receive data from Arduino and pass it to AJAX script in the client side and do what one wants. Now I am adding a script to php firt one to detect programmatically the COM port Arduino is connected to.
enjoy.

The images from Google Cloud bucket are not immediately updated

When updating an image in Google Cloud bucket, even if the image update is successful, the url serves the old version for a while (few minutes, e.g. 5 min or so).
The link we are using looks like:
https://storage.googleapis.com/<bucket-name>/path/to/images/1.jpg
The relevant part of the code which updates the image is:
var storageFile = bucket.file(imageToUpdatePath);
var storageFileStream = storageFile.createWriteStream({
metadata: {
contentType: req.file.mimetype
}
});
storageFileStream.on('error', function(err) {
...
});
storageFileStream.on('finish', function() {
// cloudFile.makePublic after the upload has finished, because otherwise the file is only accessible to the owner:
storageFile.makePublic(function(err, data) {
//if(err)
//console.log(err);
if (err) {
return res.render("error", {
err: err
});
}
...
});
});
fs.createReadStream(filePath).pipe(storageFileStream);
It looks like a caching issue on the Google Cloud side. How to solve it? How to get the updated image at the requested url, after being updated?
In the Google Cloud admin, the new image does appear correctly.
By default, public objects get cached for up to 60 minutes - see Cache Control and Consistency. To fix this, you should set the cache-control property of the object to private when you create/upload the object. In your code above, this would go in the metadata block, like so:
var storageFileStream = storageFile.createWriteStream({
metadata: {
contentType: req.file.mimetype,
cacheControl: 'private'
}
});
Reference: https://cloud.google.com/storage/docs/viewing-editing-metadata#code-samples_1
1. await bucket
.file(filePath)
.delete({ ignoreNotFound: true });
// Deleting file with a name.
const blob = bucket.file(filePath);
2. await blob.save(fil?.buffer);
//Saving File with the same name
3. const [metadata] = await storage
.bucket(bucketName)
.file(filePath)
.getMetadata();
newDocObj.location = metadata.mediaLink;
I have used metadata.mediaLink to get the
latest download link of the image from Google Bucket Storage.

How to download multiple files from Google Drive as .zip using JSZip on Salesforce

The case:
On Salesforce platform I use Google Drive to store files (images for this case) with configured Apex Google Drive API Framework. So Google Drive API handles authToken and so on. I can upload and browse images in my application. In my case I want to select multiple files and download them in a single zip file. So far I'm trying to do that using JSZip and FileSaver libraries. With the same code below I can zip and download multiple files stored somewhere else with proper response header, but not from GDrive because of CORS error.
https://xxx.salesforce.com/contenthub/download/XXXXXXXXXX%3Afile%XXXXXX_XXXXXXXXX. No'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://xxx.visual.force.com' is therefore not allowed access. If I just click on this link, file starts to download.
Is there any way to configure GDrive to enable response header: Access-Control-Allow-Origin: * or Access-Control-Allow-Origin: https://*/mydomain.com somehow or I just have to use something else, maybe server side compression? Now I am using the download link provided by Apex Google Drive API (looks like this:
https://xxx.salesforce.com/contenthub/download/XXXXXXXXXXX%3Afile%XXXXXXXX), it works fine when used as src="fileURL" or when pasted directly to the browser. GDrive connector add 'accesToken' and so on.
My code:
//ajax request to get files using JSZipUtils
let urlToPromise = (url) =>{
return new Promise(function(resolve, reject) {
JSZipUtils.getBinaryContent(url, function (err, data) {
if(err) {
reject(err);
} else {
resolve(data);
}
});
});
};
this.downloadAssets = () => {
let zip = new JSZip();
//here 'selectedAssets' array of objects each of them has 'assetFiles'
//with fileURL where I have url. Download and add them to 'zip' one by one
for (var a of this.selectedAssets){
for (let f of a.assetFiles){
let url = f.fileURL;
let name = a.assetName + "." + f.fileType;
let filename = name.replace(/ /g, "");
zip.file(filename, urlToPromise(url), {binary:true});
}
}
//generate zip and download using 'FileSaver.js'
zip.generateAsync({type:"blob"})
.then(function callback(blob) {
saveAs(blob, "test.zip");
});
};
I also tried to change let url = f.fileURL to let url = f.fileURL + '?alt=media'; and &access_token=CURRENT_TOKEN added by GDrive connector.
this link handled by GRDrive connector so if I just enter it in browser it download the image. However, for multiple download using JS I got CORS error.
I think this feature is not yet supported. If you check the Download Files guide from Drive API, there's no mention of downloading multiple files at once. That's because you have to make individual API requests for each file. This is confirmed in this SO thread.
But that selected multiple files are convert into single zip file and download that single zip file which is possible with google drive API. So how can i convert them into single Zip File? please tell me.
According to me, just download all files and store them at temporary directory location and then add that directory to zip file and store that zip to physical device.
public Entity.Result<Entity.GoogleDrive> DownloadMultipleFile(string[] fileidList)
{
var result = new Entity.Result<Entity.GoogleDrive>();
ZipFile zip = new ZipFile();
try
{
var service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Download File",
});
FilesResource.ListRequest listRequest = service.Files.List();
//listRequest.PageSize = 10;
listRequest.Fields = "nextPageToken, files(id, name, mimeType, fullFileExtension)";
IList<File> files = listRequest.Execute().Files;
if (files != null && files.Count > 0)
{
foreach (var fileid in fileidList)
{
foreach (var file in files)
{
if (file.Id == fileid)
{
result.Data = new Entity.GoogleDrive { FileId = fileid };
FilesResource.GetRequest request = service.Files.Get(fileid);
request.ExecuteAsync();
var stream = new System.IO.FileStream(HttpContext.Current.Server.MapPath(#"~\TempFiles") + "\\" + file.Name, System.IO.FileMode.Create, System.IO.FileAccess.Write);
request.MediaDownloader.ProgressChanged += (IDownloadProgress progress) =>
{
switch (progress.Status)
{
case DownloadStatus.Downloading:
{
break;
}
case DownloadStatus.Completed:
{
break;
}
case DownloadStatus.Failed:
{
break;
}
}
};
request.Download(stream);
stream.Close();
break;
}
}
}
}
zip.AddDirectory(HttpContext.Current.Server.MapPath(#"~\TempFiles"), "GoogleDrive");
string pathUser = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
string pathDownload = System.IO.Path.Combine(pathUser, "Downloads");
zip.Save(pathDownload + "\\GoogleDrive.zip");
System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(HttpContext.Current.Server.MapPath(#"~\TempFiles"));
foreach (var file in di.GetFiles())
{
file.Delete();
}
result.IsSucceed = true;
result.Message = "File downloaded suceessfully";
}
catch (Exception ex)
{
result.IsSucceed = false;
result.Message = ex.ToString();
}
return result;
}
My previously published code works. Forgot to post a solution.
Just instead of using content hub link I started to use direct link to Google Drive and CORS issue was solved. Still not sure if CORS might be solved somehow at Salesforce side. Tried different setups with no luck.
Direct download link to GDrive works ok in my case. The only thing I had to change is the prefix to GDrive file ID.

How can I prevent modifications of a PNG file uploaded using AFNetworking to a Node.js server (and keep file extension)

From a Swift 2.1-based iOS client app using AFNetworking 2.0, I'm uploading a PNG image file to a Node.js server. The issue I'm running into is that when I use a .png file name extension (as is usual), the file gets larger when it is uploaded. The original file size is: 917,630 bytes, and the uploaded size is 1,298,016 bytes. Curiously, this not a completely corrupting change in the file contents. I.e., I can still view the image with Preview on Mac OS X (though see Update1 below).
Here's the guts of of my client app upload code:
public class func uploadFileTo(serverURL: NSURL, fileToUpload:NSURL, withParameters parameters:[String:AnyObject]?, completion:((serverResponse:[String:AnyObject]?, error:NSError?)->())?) {
Log.special("serverURL: \(serverURL)")
var sendParameters:[String:AnyObject]? = parameters
#if DEBUG
if (SMTest.session.serverDebugTest != nil) {
if parameters == nil {
sendParameters = [String:AnyObject]()
}
sendParameters![SMServerConstants.debugTestCaseKey] = SMTest.session.serverDebugTest
}
#endif
self.manager.POST(serverURL.absoluteString, parameters: sendParameters, constructingBodyWithBlock: { (formData: AFMultipartFormData) in
// NOTE!!! the name: given here *must* match up with that used on the server in the "multer" single parameter.
// Was getting an odd try/catch error here, so this is the reason for "try!"; see https://github.com/AFNetworking/AFNetworking/issues/3005
// 12/12/15; I think this issue was because I wasn't doing the do/try/catch, however.
do {
try formData.appendPartWithFileURL(fileToUpload, name: SMServerConstants.fileUploadFieldName)
} catch let error {
let message = "Failed to appendPartWithFileURL: \(fileToUpload); error: \(error)!"
Log.error(message)
completion?(serverResponse: nil, error: Error.Create(message))
}
}, success: { (request: AFHTTPRequestOperation, response:AnyObject) in
if let responseDict = response as? [String:AnyObject] {
print("AFNetworking Success: \(response)")
completion?(serverResponse: responseDict, error: nil)
}
else {
let error = Error.Create("No dictionary given in response")
print("**** AFNetworking FAILURE: \(error)")
completion?(serverResponse: nil, error: error)
}
}, failure: { (request: AFHTTPRequestOperation?, error:NSError) in
print("**** AFNetworking FAILURE: \(error)")
completion?(serverResponse: nil, error: error)
})
On the Node.js, here's the package.json:
{
"name": "node1",
"version": "1.0.0",
"description": "Test",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.14.1",
"fs-extra": "^0.26.2",
"google-auth-library": "^0.9.7",
"googleapis": "^2.1.6",
"mongodb": "^2.0.49",
"multer": "^1.1.0",
"sweet": "^0.1.1",
"tracer": "^0.8.2"
},
"devDependencies": {
"sweet.js": "^0.7.4"
}
}
Here's the initial part of my server "index.js" file (a lot of which is specific to my project):
// Before the files get moved to their specific-user destination.
const initialUploadDirectory = './initialUploads/';
// TODO: What is safe mode in mongo? E.g., see https://mongodb.github.io/node-mongodb-native/api-generated/collection.html#insert
// See also options on insert https://mongodb.github.io/node-mongodb-native/api-generated/collection.html#insert
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
// https://github.com/expressjs/multer
var multer = require('multer');
var fse = require('fs-extra');
// Local modules.
var ServerConstants = require('./ServerConstants');
var Mongo = require('./Mongo');
var Operation = require('./Operation');
var PSLock = require('./PSLock');
var PSOutboundFileChange = require('./PSOutboundFileChange.sjs');
var FileTransfers = require('./FileTransfers');
var File = require('./File.sjs')
var logger = require('./Logger');
var PSOperationId = require('./PSOperationId.sjs');
var PSFileIndex = require('./PSFileIndex');
// See http://stackoverflow.com/questions/31496100/cannot-app-usemulter-requires-middleware-function-error
// See also https://codeforgeek.com/2014/11/file-uploads-using-node-js/
// TODO: Limit the size of the uploaded file.
// TODO: Is there a way with multer to add a callback that gets called periodically as an upload is occurring? We could use this to "refresh" an activity state for a lock to make sure that, even with a long-running upload (or download) if it is still making progress, that we wouldn't lose a lock.
var upload = multer({ dest: initialUploadDirectory}).single(ServerConstants.fileUploadFieldName)
// http://stackoverflow.com/questions/4295782/how-do-you-extract-post-data-in-node-js
app.use(bodyParser.json({extended : true}));
And here's the initial (relevant) part of the REST/API entry point for the upload:
app.post('/' + ServerConstants.operationUploadFile, upload, function (request, response) {
var op = new Operation(request, response);
if (op.error) {
op.end();
return;
}
/* request.file has the info on the uploaded file: e.g.,
{ fieldname: 'file',
originalname: 'upload.txt',
encoding: '7bit',
mimetype: 'text/plain',
destination: './uploads/',
filename: 'e9a4080c46777d6341518afedec8af31',
path: 'uploads/e9a4080c46777d6341518afedec8af31',
size: 22 }
*/
op.validateUser(function (psLock, psOperationId) {
// User is on the system.
//console.log("request.file: " + JSON.stringify(request.file));
// Make sure user/device has started uploads. i.e., make sure this user/device has the lock.
if (!psLock) {
var message = "Error: Don't have the lock!";
logger.error(message);
op.endWithRCAndErrorDetails(ServerConstants.rcServerAPIError, message);
} else if (psOperationId.operationStatus ==
ServerConstants.rcOperationStatusInProgress) {
// This check is to deal with error recovery.
var message = "Error: Have lock, but operation is already in progress!";
logger.error(message);
op.endWithRCAndErrorDetails(ServerConstants.rcServerAPIError, message);
} else {
logger.info("We've got the lock!");
// Leave the parameter checking below until after checking for the lock because we're just checking for a lock, and not creating a lock.
// 12/12/15; Ran into a bug where the upload failed, and .file object wasn't defined.
if (!isDefined(request.file) || !isDefined(request.file.path)) {
var message = "No file uploaded!";
logger.error(message);
op.endWithRCAndErrorDetails(ServerConstants.rcServerAPIError, message);
return;
}
logger.info(JSON.stringify(request.file));
...
And here's the logger.info output:
{"fieldname":"file","originalname":"Test.png","encoding":"7bit","mimetype":"image/png","destination":"./initialUploads/","filename":"da17e16904ed376fb21052c80b88da12","path":"initialUploads/da17e16904ed376fb21052c80b88da12","size":1298016}
When I change the file extension to .bin (just some non-standard extension), the file size is not increased-- i.e., it remains the smaller value I'd originally expected.
The ratio between the two files sizes is 1.41453091 (= 1,298,016/917,630). Which looks oddly close to the square root of 2. Some encoding issue?
Thoughts?
Update1:
When I use ImageMagick's identify program, I get reasonable output for the image before uploading, but after uploading (with the larger image), I get:
$ identify -verbose example2.png identify: CgBI: unhandled critical
chunk example2.png' # error/png.c/MagickPNGErrorHandler/1630.
identify: corrupt imageexample2.png' #
error/png.c/ReadPNGImage/3959.
Update2:
I think I can now say for sure that this is a client-side issue related to AFNetworking and not a server-side issue related to Node.js. I make this inference because when I make a simplified Node.js server (using all of the same "header" code as my actual server) as below:
index.js
'use strict';
require('sweet.js').loadMacro('./macros.sjs');
var server = require("./Server.sjs");
Server.sjs
// Before the files get moved to their specific-user destination.
const initialUploadDirectory = './initialUploads/';
// TODO: What is safe mode in mongo? E.g., see https://mongodb.github.io/node-mongodb-native/api-generated/collection.html#insert
// See also options on insert https://mongodb.github.io/node-mongodb-native/api-generated/collection.html#insert
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
// https://github.com/expressjs/multer
var multer = require('multer');
var fse = require('fs-extra');
// Local modules.
var ServerConstants = require('./ServerConstants');
var Mongo = require('./Mongo');
var Operation = require('./Operation');
var PSLock = require('./PSLock');
var PSOutboundFileChange = require('./PSOutboundFileChange.sjs');
var FileTransfers = require('./FileTransfers');
var File = require('./File.sjs')
var logger = require('./Logger');
var PSOperationId = require('./PSOperationId.sjs');
var PSFileIndex = require('./PSFileIndex');
// See http://stackoverflow.com/questions/31496100/cannot-app-usemulter-requires-middleware-function-error
// See also https://codeforgeek.com/2014/11/file-uploads-using-node-js/
// TODO: Limit the size of the uploaded file.
// TODO: Is there a way with multer to add a callback that gets called periodically as an upload is occurring? We could use this to "refresh" an activity state for a lock to make sure that, even with a long-running upload (or download) if it is still making progress, that we wouldn't lose a lock.
var upload = multer({ dest: initialUploadDirectory}).single(ServerConstants.fileUploadFieldName)
// http://stackoverflow.com/questions/4295782/how-do-you-extract-post-data-in-node-js
app.use(bodyParser.json({extended : true}));
// Server main.
Mongo.connect();
app.post('/upload', upload, function (request, response) {
console.log(JSON.stringify(request.file));
var result = {};
response.end(JSON.stringify(result));
});
app.listen(8081);
console.log('Server running at http://127.0.0.1:8081/');
and then test that using Postman, uploading my example PNG file, I get the uploaded file with no increase in size. Here's the Node.js output:
{"fieldname":"file","originalname":"Example.png","encoding":"7bit","mimetype":"image/png","destination":"./initialUploads/","filename":"ac7c8c93d50bf48cf6042409ef990658","path":"initialUploads/ac7c8c93d50bf48cf6042409ef990658","size":917630}
Then, when I drop the above app.post method into my actual server, and again test the upload with Postman (not my example app using AFNetworking), I still do not get the increase in file size:
{"fieldname":"file","originalname":"Example.png","encoding":"7bit","mimetype":"image/png","destination":"./initialUploads/","filename":"a40c738a172eb9ea6cccce357338beeb","path":"initialUploads/a40c738a172eb9ea6cccce357338beeb","size":917630}
So far so good, without using AFNetworking.
And finally, when I add an additional test into my iOS client app, using the simplified app.post on the server, and using AFNetworking for the client (and I'm using AFNetworking 3 now), but only using the file upload post on the client, I get:
{"fieldname":"file","originalname":"Example.png","encoding":"7bit","mimetype":"image/png","destination":"./initialUploads/","filename":"8c1116337fd2650d4f113b227252e555","path":"initialUploads/8c1116337fd2650d4f113b227252e555","size":1298016}
That is, using AFNetworking again on the client, I again get the larger file size.
Aha! I've now learned that this is not specific to AFNetworking, but is definitely client side. I switched to uploading an NSData object and from the following code:
let fileData = NSData(contentsOfURL: fileToUpload)
Log.special("size of fileData: \(fileData!.length)")
I find that my file isn't the length I thought it was. This gives a length of 1,298,016 bytes. Note that this file is in the app bundle and shows up in the Terminal as 917,630 bytes. WTF? Is Apple's process of putting the .png into the bundle changing the .png file?
Creating an Archive of the app using Xcode, and digging into that directory structure, I find that yes indeed, in the app bundle, the file size is 1298016 bytes. Ouch. This "solves" my problem by introducing two other questions: (1) Why does Apple change the size/content of .png files in your app bundle, and (2) How to usefully do testing/development in an app where you need sample image data in the bundle?

Categories

Resources