3

I'm following Heroku's tutorial on direct uploads to Amazon S3. After getting a signed request from AWS through the Node.js app, they use a "normal" XMLHttpRequest to send the file.

This is their function:

function upload_file(file, signed_request, url){
    var xhr = new XMLHttpRequest();
    xhr.open("PUT", signed_request);
    xhr.setRequestHeader('x-amz-acl', 'public-read');
    xhr.onload = function() {
        if (xhr.status === 200) {
            document.getElementById("preview").src = url;
            document.getElementById("avatar_url").value = url;
        }
    };
    xhr.onerror = function() {
        alert("Could not upload file.");
    };
    xhr.send(file);
}

Now, I'm working with Cordova and, since I don't get a File object from the camera plugin, but only the file URI, I used Cordova's FileTransfer to upload pictures to my Node.js app with multipart/form-data and it worked fine.

However, I can't manage to make it work for Amazon S3.

Here's what I have:

$scope.uploadPhoto = function () {
    $scope.getSignedRequest(function (signedRequest) {
        if (!signedRequest)
            return;

        var options = new FileUploadOptions();
        options.fileKey = 'file';
        options.httpMethod = 'PUT';
        options.mimeType = 'image/jpeg';
        options.headers = {
            'x-amz-acl': 'public-read'
        };
        options.chunkedMode = false;

        var ft = new FileTransfer();
        ft.upload($scope.photoURI, encodeURI(signedRequest.signed_request), function () {
            // success
        }, function () {
            // error
        }, options);
    });
};

I've tried both chunkedMode = true and chunkedMode = false, but neither the success nor the error callback is called.

So, is there a way to upload a file to S3 with FileTransfer? Do I actually need the signed request or is it only necessary if I use XHR?

Any hint is appreciated.

hornobster
  • 717
  • 1
  • 7
  • 18
  • I feel your pain. See my answer here, might help. http://stackoverflow.com/questions/30931146/aws-s3-signed-url-encode-resulting-signaturedoesnotmatch/31931168#31931168 – derekdon Aug 11 '15 at 11:32
  • I implemented a workaround just yesterday... I take the base64 data from the camera, decode it to a blob and send it with a normal XHR. But I'll try your solution because, if it works, it's definitely better – hornobster Aug 11 '15 at 15:31
  • Update: it didn't work. I'll stick with the XHR. – hornobster Aug 12 '15 at 10:45

2 Answers2

6

I ended up with this function in Cordova:

$scope.uploadPhoto = function () {
    $scope.getSignedRequest(function (signedRequest) {
        if (!signedRequest)
            return;

        var options = new FileUploadOptions();
        options.chunkedMode = false;
        options.httpMethod = 'PUT';
        options.headers = {
            'Content-Type': 'image/jpeg',
            'X-Amz-Acl': 'public-read'
        };

        var ft = new FileTransfer();
        ft.upload($scope.photoURI, signedRequest.signedUrl, function () {
            $scope.$apply(function () {
                // success
            });
        }, function () {
            $scope.$apply(function () {
                // failure
            });
        }, options);
    });
};

The important bits are setting the Content-Type header, so that multipart/form-data won't be used, and chunkedMode = false to send the file with a single request.

EDIT: Removed changes to the plugin code which were, in hindsight, useless (outdated plugin).

hornobster
  • 717
  • 1
  • 7
  • 18
1

Not able to add comment: Strange. Works for me using $cordovaFileTransfer.upload. I don't have the 'x-amz-acl': 'public-read' header. Also I don't use encodeURI on the signed url. Have you been able to debug it? See any errors? I used chrome://inspect and port forwarding to connect to my app running on the phone, so I was able to debug the response from Amazon. Might be another reason why it's failing.

derekdon
  • 136
  • 9
  • Using chrome://inspect (didn't know it existed, it's great!) I couldn't see any request going out. Using charles I discovered I wasn't setting the headers properly, so the request still used `multipart/form-data`. I corrected it and now files are uploaded, but for some reason some stuff is added at the beginning of the body and so the JPG images are corrupted. `--+++++ Content-Disposition: form-data; name="file"; filename="image.jpg" Content-Type: image/jpeg` is added at the beginning of the file and `--+++++--` at the end. – hornobster Aug 12 '15 at 12:57
  • Update: following [this](http://stackoverflow.com/questions/31175836/cordova-file-transfer-remove-multipart-or-content-disposition-header) SO question, I modified the java code for the plugin, I removed the additional data from the files, which are now correctly uploaded. Amazon sends a 200 OK response but still the success callback is not called. I've no idea why. – hornobster Aug 12 '15 at 14:43
  • Update2: I feel like an idiot. The success callback was being called, but angular wouldn't update the UI. I wrapped the content of the callback inside `$scope.$apply(function(){...})` and everything worked fine. I'll post an answer with all the details. – hornobster Aug 12 '15 at 15:07
  • Re that answer on: http://stackoverflow.com/questions/31175836/cordova-file-transfer-remove-multipart-or-content-disposition-header, you don't need to do that if you set the options header Content-Type correctly. It only does that if it's not set, so set it to match your mimeType... like my linked answer shows, and you should be golden! Also the stuff being added is probably due to this, or if your using chunked mode true, set it to false. – derekdon Aug 12 '15 at 15:12
  • You are right! I had installed the plugin using `cordova plugin add org.apache.cordova.file-transfer` and apparently that's the old version 0.5. I've now installed with `cordova plugin add cordova-plugin-file-transfer` and it works without modifying the java code. I don't know why they keep the old version active... Thanks! – hornobster Aug 12 '15 at 16:05