0
votes

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.

1

1 Answers

0
votes

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?