Download Remote File With Angular Using $http.get() - OAuth Authentication - javascript

My users have private files that need to be downloaded by an authenticated users. My server first downloads a file from S3 using it's own S3 app_id/secret_token credentials. The downloaded file is then constructed and sent to the client using Rails' send_data method.
Ruby (on Rails):
# documents_controller.rb
def download
some_file = SomeFile.find(params[:id])
# download file from AWS S3 to server
data = open(some_file.document.url)
# construct and send downloaded file to client
send_data data.read, filename: some_file.document_identifier, disposition: 'inline', stream: 'true'
end
Originally, I wanted to do trigger the download directly from the HTML template.
HTML:
<!-- download-template.html -->
<a target="_self" ng-href="{{ document.download_url }}" download="{{document.file_name}}">Download</a>
Looks simple enough but the problem is that Angular's $http interceptor doesn't catch this type of external link click and therefore the appropriate headers are not appended for server-side authentication. The result is a 401 Unauthorized Error.
Instead, I need to trigger the download using ng-click and then performing an $http.get() request from the angular controller.
HTML:
<!-- download-template.html -->
<div ng-controller="DocumentCtrl">
<a ng-click="download(document)">Download</a>
</div>
Javascript:
// DocumentCtrl.js
module.controller( "DocumentCtrl",
[ "$http", "$scope", "FileSaver", "Blob",
function( $http, $scope, FileSaver, Blob ) {
$scope.download = function( document ) {
$http.get(document.download_url, {}, { responseType: "arraybuffer" } )
.success( function( data ) {
var blob = new Blob([data], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" });
FileSaver.saveAs(blob, document.file_name);
});
};
}]);
FileSaver is a simple library to save files using Blobs (on the client, obviously).
This gets me passed by my authentication problem but results in the file being saved/download to the client in an unreadable/unusable format.
Why is the file being downloaded in an unusable format?
Thanks in advance.

Angular's $http method needs to be configured to accept a binary data response.
Rails' send_data documentation:
Sends the given binary data to the browser. This method is similar to
render plain: data, but also allows you to specify whether the browser
should display the response as a file attachment (i.e. in a download
dialog) or as inline data. You may also set the content type, the
apparent file name, and other things.
Angular's $http documentation is very poor regarding $http's configuration of responseType. Essentially, $http needs to be told to expect a binary data response by setting responseType to "arraybuffer" (see below).
$scope.download = function( document ) {
console.log("download: ", document);
$http({
url: document.download_url,
method: "GET",
headers: {
"Content-type": "application/json"
},
responseType: "arraybuffer" // expect to handle binary data response
}).success( function( data, status, headers ) {
var type = headers('Content-Type');
var blob = new Blob([data], { type: type });
FileSaver.saveAs(blob, document.file_name);
});
};
Angular's $http documentation could be a little more descriptive than:
Usage
$http(config);
Arguments
config
responseType - {string} - see XMLHttpRequest.responseType.

Hi I have an example of how I download a file from my server with angular:
I call the file with GET request:
file download html(client side):
<a ng-href="/api/downloadFile/{{download.id}}" type="submit" class="btn btn-primary col-lg-12 btn-modal-costume" >download</a>
file download java(server side):
public static Result download(String id) {
String content = null;
for (controllers.file file : files) {
if (file.getId().equals(id)){
content = file.getContent();
}
}
return ok(new java.io.File("/temp/" + id+ "file" + content)).as("application/force-download");
}
If you like you can see the all code in my github project

I think you were on the right track with the javascript solution, but just had a typo. In the $http.get call you pass an empty object as the second parameter. This is where the options argument with {responseType: arraybuffer} should have gone. See docs for $http.get here:
https://docs.angularjs.org/api/ng/service/$http#get

Related

Download PDF resource from PHP to Javascript via Ajax

So I will explain the problem:
Steps:
1) client (browser javascript) sends an Ajax request to the server that hits a controller method called download.
2) the controller's method creates a PDF resource(without saving on the filesystem), and returns a response with the PDF binary stream back to the client.
3) the client receives the PDF binary stream and download it on the client's computer. Is that possible?
Code:
Things I have already tried -
Client-side:
<script>
(function($) {
var button; // some random DOM button
button.on('click', function(e) {
e.preventDefault();
$.ajax({
url: "/download/:userId"
method: "POST",
dataType: "json"
success: function(response) {
var reader = new FileReader;
var file = new Blob([response.pdf_stream], 'application/pdf');
// create a generic download link
var a = $('<a/>', {
href: file,
download: response.filename
});
// trigger click event on that generic link.
a.get(0).click();
}
});
}
})(jQuery);
</script>
On the server-side:
class Controller
{
public function download($userId)
{
// fetching the user from the database
$user = User::find($userId);
// creating a pdf file using barry pdfdom package
// this will actually parse an HTML view and give us the PDF blob.
$pdf = PDF::loadView('pdf.view')->output();
// using Laravel helper function
return response()->json([
'pdf_stream' => utf8_encode($pdf),
'filename' => 'blahblah.pdf"
]);
// Or if you will in native PHP, just in case you don't use laravel.
echo json_encode([
'pdf_stream' => utf8_encode($pdf),
'filename' => 'blahblah.pdf"
]);
}
}
Any Idea what am I doing wrong here? How could I download that PDF file without saving it to the system (security and space concerns).
Any help would be appreciated.
Eden
If you want download pdf on client side, just open this pdf in new window. Use GET request for that things, like in RESTfull application (e.g. download/user/:id or somehow like that).
Could be useful:
Download and open pdf file using Ajax
The main problem is the returned response from controller. Try this:
public function download($userId)
{
// fetching the user from the database
$user = User::find($userId);
// creating a pdf file using barry pdfdom package
// this will actually parse an HTML view and give us the PDF blob.
$pdf = PDF::loadView('pdf.view')->output();
return response($pdf, 200,
[
'Content-Type' => 'application/pdf',
'Content-Length' => strlen($pdf),
'Cache-Control' => 'private, max-age=0, must-revalidate',
'Pragma' => 'public'
]
);
About calling the route which executes download($userid) method:
You do not have to use Ajax. Easy way:
Click view PDF

cordova-plugin-file-transfer: How do you upload a file to S3 using a signed URL?

I am able to upload to S3 using a file picker and regular XMLHttpRequest (which I was using to test the S3 setup), but cannot figure out how to do it successfully using the cordova file transfer plugin.
I believe it is either to do with the plugin not constructing the correct signable request, or not liking the local file uri given. I have tried playing with every single parameter from headers to uri types, but the docs aren't much help, and the plugin source is bolognese.
The string the request needs to sign match is like:
PUT
1391784394
x-amz-acl:public-read
/the-app/317fdf654f9e3299f238d97d39f10fb1
Any ideas, or possibly a working code example?
A bit late, but I just spent a couple of days struggling with this so in case anybody else is having problems, this is how managed to upload an image using the javascript version of the AWS SDK to create the presigned URL.
The key to solving the problem is in the StringToSign element of the XML SignatureDoesNotMatch error that comes back from Amazon. In my case it looked something like this:
<StringToSign>
PUT\n\nmultipart/form-data; boundary=+++++org.apache.cordova.formBoundary\n1481366396\n/bucketName/fileName.jpg
</StringToSign>
When you use the aws-sdk to generate a presigned URL for upload to S3, internally it will build a string based on various elements of the request you want to make, then create an SHA1 hash of it using your AWS secret. This hash is the signature that gets appended to the URL as a parameter, and what doesn't match when you get the SignatureDoesNotMatch error.
So you've created your presigned URL, and passed it to cordova-plugin-file-transfer to make your HTTP request to upload a file. When that request hits Amazon's server, the server will itself build a string based on the request headers etc, hash it and compare that hash to the signature on the URL. If the hashes don't match then it returns the dreaded...
The request signature we calculated does not match the signature you provided. Check your key and signing method.
The contents of the StringToSign element I mentioned above is the string that the server builds and hashes to compare against the signature on the presigned URL. So to avoid getting the error, you need to make sure that the string built by the aws-sdk is the same as the one built by the server.
After some digging about, I eventually found the code responsible for creating the string to hash in the aws-sdk. It is located (as of version 2.7.12) in:
node_modules/aws-sdk/lib/signers/s3.js
Down the bottom at line 168 there is a sign method:
sign: function sign(secret, string) {
return AWS.util.crypto.hmac(secret, string, 'base64', 'sha1');
}
If you put a console.log in there, string is what you're after. Once you make the string that gets passed into this method the same as the contents of StringToSign in the error message coming back from Amazon, the heavens will open and your files will flow effortlessly into your bucket.
On my server running node.js, I originally created my presigned URL like this:
var AWS = require('aws-sdk');
var s3 = new AWS.S3(options = {
endpoint: 'https://s3-eu-west-1.amazonaws.com',
accessKeyId: "ACCESS_KEY",
secretAccessKey: "SECRET_KEY"
});
var params = {
Bucket: 'bucketName',
Key: imageName,
Expires: 60
};
var signedUrl = s3.getSignedUrl('putObject', params);
//return signedUrl
This produced a signing string like this, similar to the OP's:
PUT
1481366396
/bucketName/fileName.jpg
On the client side, I used this presigned URL with cordova-plugin-file-transfer like so (I'm using Ionic 2 so the plugin is wrapped in their native wrapper):
let success = (result: any) : void => {
console.log("upload success");
}
let failed = (err: any) : void => {
let code = err.code;
alert("upload error - " + code);
}
let ft = new Transfer();
var options = {
fileName: filename,
mimeType: 'image/jpeg',
chunkedMode: false,
httpMethod:'PUT',
encodeURI: false,
};
ft.upload(localDataURI, presignedUrlFromServer, options, false)
.then((result: any) => {
success(result);
}).catch((error: any) => {
failed(error);
});
Running the code produced the signature doesn't match error, and the string in the <StringToSign> element looks like this:
PUT
multipart/form-data; boundary=+++++org.apache.cordova.formBoundary
1481366396
/bucketName/fileName.jpg
So we can see that cordova-plugin-file-transfer has added in its own Content-Type header which has caused a discrepancy in the signing strings. In the docs relating to the options object that get passed into the upload method it says:
headers: A map of header name/header values. Use an array to specify more than one value. On iOS, FireOS, and Android, if a header named Content-Type is present, multipart form data will NOT be used. (Object)
so basically, if no Content-Type header is set it will default to multipart form data.
Ok so now we know the cause of the problem, it's a pretty simple fix. On the server side I added a ContentType to the params object passed to the S3 getSignedUrl method:
var params = {
Bucket: 'bucketName',
Key: imageName,
Expires: 60,
ContentType: 'image/jpeg' // <---- content type added here
};
and on the client added a headers object to the options passed to cordova-plugin-file-transfer's upload method:
var options = {
fileName: filename,
mimeType: 'image/jpeg',
chunkedMode: false,
httpMethod:'PUT',
encodeURI: false,
headers: { // <----- headers object added here
'Content-Type': 'image/jpeg',
}
};
and hey presto! The uploads now work as expected.
I run into such issues with this plugin
The only working way I found to upload a file with a signature is the method of Christophe Coenraets : http://coenraets.org/blog/2013/09/how-to-upload-pictures-from-a-phonegap-app-to-amazon-s3/
With this method you will be able to upload your files using the cordova-plugin-file-transfer
First, I wanted to use the aws-sdk on my server to sign with getSignedUrl()
It returns the signed link and you only have to upload to it.
But, using the plugin it always end with 403 : signatures don't match
It may be related to the content length parameter but I didn't found for now a working solution with aws-sdk and the plugin

Configure Grails controller to download file in browser after retrieving data from service

My controller in Grails first fetches the data from a remote service API, and the returned data is a String. Then I would like to have the data downloaded as a csv file in the browser. I came across a similar post on SO and stole the code for using the response as below:
String exportResults = dataService.getDataFromService()
response.setHeader "Content-disposition", "attachment; filename=data_export.csv"
response.contentType = 'text/csv'
response.outputStream << exportResults.getBytes() //getBytes() not portable
response.outputStream.flush()
But this does not trigger any download window in the browser. I was wondering why. I use AngularJS to make a POST request to the controller and resolve the promise as below (JavaScript code):
ExportService.exportData(some_params).then(function(data) {
$log.info('export promise resolved: ');
//window.open(data, '_blank', ''); //not working
}).catch(function(err) {
$scope.message = "failed to retrieve data from export service";
$log.error(err);
}).finally(function(complete) {
$scope.message = "Data export completed.";
});
You need to replace 'text/csv' with 'application/octet-stream' for response.contentType. The content type 'application/octet-stream' is used for a binary file.
Try adding this after response.outputStream.flush() and see if it works.
webRequest.renderView = false
You can even try looking here.

Angulars $http.jsonp() is returning 404 [duplicate]

I am trying to obtain a .json file from remote firebase server.
function fetchData(remoteJsonId){
var url = "https://myAppName.firebaseapp.com/topics/"+remoteJsonID;
console.log(url); //This variable expands to the full domain name which is valid and returns success both on wget and the browser
$http.jsonp(url).then(
function(resp){
},
function(err){
console.log(err.status) // This posts "404" on console.
}
);
}
But If I open url in the browser the json file loads. Even if I wget url the json file loads. But through angular it returns a 404 not found.
Now the .json remote file has this structure:
[
{
"hello":"Europe"
},
{
"hello":"USA"
}
]
The above file can be fetched using $http.get() but not with $http.jsonp(). JSONP cant parse .json file with the above structure. How can I work around this?
You need to specify a ?callback=JSON_CALLBACK in the URL that you pass to $http.jsonp.
From Angular's $http.jsonp documentation:
jsonp(url, [config]);
Shortcut method to perform JSONP request.
Parameters
Param Type Details
url string
Relative or absolute URL specifying the destination of the request.
The name of the callback should be the string JSON_CALLBACK.
That last line is what you're missing.
A simple example (using a Firebase database, not Firebase hosting):
var app = angular.module('myapp', []);
app.controller('mycontroller', function($scope, $http) {
var url = 'https://yourfirebase.firebaseio.com/25564200.json';
$http.jsonp(url+'?callback=JSON_CALLBACK').then(
function(resp){
console.log(resp.data); // your object is in resp.data
},
function(err){
console.error(err.status)
}
);
});
In case you want to see it working: http://jsbin.com/robono/1/watch?js,console

File upload in ExpressJS and AngularJS using the REST API

I am new to AngularJS and Node.js.
I want to implement file (.pdf, .jpg, .doc) upload functionality using the REST API, AngularJS and Express.js.
I have tried to get an idea from Use NodeJS to upload file in an API call but I am still not clear how I can upload files using AngularJS and Express.js in the REST API.
Can anyone explain to me how to upload files in AngularJS and Express.js using the REST API, with basic examples?
Reference : https://github.com/danialfarid/angular-file-upload
I have resolved this problem using following code :
Angularjs
HTML
<input type="file" ng-file-select="onFileSelect($files)" multiple>
JS
$scope.onFileSelect = function($files) {
//$files: an array of files selected, each file has name, size, and type.
for (var i = 0; i < $files.length; i++) {
var file = $files[i];
$scope.upload = $upload.upload({
url: 'server/upload/url', //upload.php script, node.js route, or servlet url
// method: 'POST' or 'PUT',
// headers: {'header-key': 'header-value'},
// withCredentials: true,
data: {myObj: $scope.myModelObj},
file: file, // or list of files: $files for html5 only
/* set the file formData name ('Content-Desposition'). Default is 'file' */
//fileFormDataName: myFile, //or a list of names for multiple files (html5).
/* customize how data is added to formData. See #40#issuecomment-28612000 for sample code */
//formDataAppender: function(formData, key, val){}
}).progress(function(evt) {
console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total));
}).success(function(data, status, headers, config) {
// file is uploaded successfully
console.log(data);
});
//.error(...)
//.then(success, error, progress);
//.xhr(function(xhr){xhr.upload.addEventListener(...)})// access and attach any event listener to XMLHttpRequest.
}
/* alternative way of uploading, send the file binary with the file's content-type.
Could be used to upload files to CouchDB, imgur, etc... html5 FileReader is needed.
It could also be used to monitor the progress of a normal http post/put request with large data*/
// $scope.upload = $upload.http({...}) see 88#issuecomment-31366487 for sample code.
};
Nodejs using expressJS
var path = require('path'),
fs = require('fs');
var tempPath = req.files.file.path,
targetPath = path.resolve('./uploadFiles/' + req.files.file.name);
fs.rename(tempPath, targetPath, function(err) {
if (err) throw err;
console.log("Upload completed!");
I've been dealing with a similar problem recently. Thing is, angular doesn't have the best support for input type "file", because you cannot bind it 2 way. That is why people made custom directives for that. The one I used has some neat examples.
Its called angular-file-upload

Categories