How to open a CSV file generated by POST - javascript

I am trying to implement these seemingly simple requirements but can't find a way :
Single Page App using Angular JS
REST(ish) back end
Back end resource exposed via POST request
Resource parameters passed as JSON in the request body
Resource produces a CSV file
When a user clicks a button, generate a request with the right JSON parameters in the body, send it, and allow user to download the response as a file (prompts the browser's "open / save as" dialog)
The problem is mainly, how to pass the JSON as request body? The most common technique seems to be the hidden HTML form to trigger the download, but an HTML form cannot send JSON data in the body. And I can't find any way to trigger a download dialog using an XMLHttpRequest...
Any ideas?
I specified Angular but any generic JS solution is very welcome too!

I finally found a solution that satisfies all my requirements, and works in IE11, FF and Chrome (and degrades kind of OK in Safari...).
The idea is to create a Blob object containing the data from the response, then force the browser to open it as a file. It is slightly different for IE (proprietary API) and Chrome/FF (using a link element).
Here is the implementation, as a small Angular service:
myApp.factory('Download', [function() {
return {
openAsFile : function(response){
// parse content type header
var contentTypeStr = response.headers('Content-Type');
var tokens = contentTypeStr.split('/');
var subtype = tokens[1].split(';')[0];
var contentType = {
type : tokens[0],
subtype : subtype
};
// parse content disposition header, attempt to get file name
var contentDispStr = response.headers('Content-Disposition');
var proposedFileName = contentDispStr ? contentDispStr.split('"')[1] : 'data.'+contentType.subtype;
// build blob containing response data
var blob = new Blob([response.data], {type : contentTypeStr});
if (typeof window.navigator.msSaveBlob !== 'undefined'){
// IE : use proprietary API
window.navigator.msSaveBlob(blob, proposedFileName);
}else{
var downloadUrl = URL.createObjectURL(blob);
// build and open link - use HTML5 a[download] attribute to specify filename
var a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.open(downloadUrl);
}
var link = document.createElement('a');
link.href = downloadUrl;
link.download = proposedFileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
}
}]);
The response argument expects a $http response object. Here is an example of use with a POST request:
$http.post(url, {property : 'value'}, {responseType: 'blob'}).then(function(response){
Download.openAsFile(response);
});
Note the responseType parameter. Without this, my CSV data was being read as text and stored in memory as UTF-8 (or 16), and subsequently the file was saved in the same encoding, causing Excel to not recognize special characters such as éè etc. Since my CSVs are intended to be opened by Excel, the server encodes them Windows 1252, I wanted to keep them that way. Setting the responseType parameter to blob achieves this.
Disclaimer: It should work with any file type. But I tested it only with CSV files ! Binary files might behave somehow differently !

Related

Blob name issue with new tab in chrome and firefox [duplicate]

In my Vue app I receive a PDF as a blob, and want to display it using the browser's PDF viewer.
I convert it to a file, and generate an object url:
const blobFile = new File([blob], `my-file-name.pdf`, { type: 'application/pdf' })
this.invoiceUrl = window.URL.createObjectURL(blobFile)
Then I display it by setting that URL as the data attribute of an object element.
<object
:data="invoiceUrl"
type="application/pdf"
width="100%"
style="height: 100vh;">
</object>
The browser then displays the PDF using the PDF viewer. However, in Chrome, the file name that I provide (here, my-file-name.pdf) is not used: I see a hash in the title bar of the PDF viewer, and when I download the file using either 'right click -> Save as...' or the viewer's controls, it saves the file with the blob's hash (cda675a6-10af-42f3-aa68-8795aa8c377d or similar).
The viewer and file name work as I'd hoped in Firefox; it's only Chrome in which the file name is not used.
Is there any way, using native Javascript (including ES6, but no 3rd party dependencies other than Vue), to set the filename for a blob / object element in Chrome?
[edit] If it helps, the response has the following relevant headers:
Content-Type: application/pdf; charset=utf-8
Transfer-Encoding: chunked
Content-Disposition: attachment; filename*=utf-8''Invoice%2016246.pdf;
Content-Description: File Transfer
Content-Encoding: gzip
Chrome's extension seems to rely on the resource name set in the URI, i.e the file.ext in protocol://domain/path/file.ext.
So if your original URI contains that filename, the easiest might be to simply make your <object>'s data to the URI you fetched the pdf from directly, instead of going the Blob's way.
Now, there are cases it can't be done, and for these, there is a convoluted way, which might not work in future versions of Chrome, and probably not in other browsers, requiring to set up a Service Worker.
As we first said, Chrome parses the URI in search of a filename, so what we have to do, is to have an URI, with this filename, pointing to our blob:// URI.
To do so, we can use the Cache API, store our File as Request in there using our URL, and then retrieve that File from the Cache in the ServiceWorker.
Or in code,
From the main page
// register our ServiceWorker
navigator.serviceWorker.register('/sw.js')
.then(...
...
async function displayRenamedPDF(file, filename) {
// we use an hard-coded fake path
// to not interfere with legit requests
const reg_path = "/name-forcer/";
const url = reg_path + filename;
// store our File in the Cache
const store = await caches.open( "name-forcer" );
await store.put( url, new Response( file ) );
const frame = document.createElement( "iframe" );
frame.width = 400
frame.height = 500;
document.body.append( frame );
// makes the request to the File we just cached
frame.src = url;
// not needed anymore
frame.onload = (evt) => store.delete( url );
}
In the ServiceWorker sw.js
self.addEventListener('fetch', (event) => {
event.respondWith( (async () => {
const store = await caches.open("name-forcer");
const req = event.request;
const cached = await store.match( req );
return cached || fetch( req );
})() );
});
Live example (source)
Edit: This actually doesn't work in Chrome...
While it does set correctly the filename in the dialog, they seem to be unable to retrieve the file when saving it to the disk...
They don't seem to perform a Network request (and thus our SW isn't catching anything), and I don't really know where to look now.
Still this may be a good ground for future work on this.
And an other solution, I didn't took the time to check by myself, would be to run your own pdf viewer.
Mozilla has made its js based plugin pdf.js available, so from there we should be able to set the filename (even though once again I didn't dug there yet).
And as final note, Firefox is able to use the name property of a File Object a blobURI points to.
So even though it's not what OP asked for, in FF all it requires is
const file = new File([blob], filename);
const url = URL.createObjectURL(file);
object.data = url;
In Chrome, the filename is derived from the URL, so as long as you are using a blob URL, the short answer is "No, you cannot set the filename of a PDF object displayed in Chrome." You have no control over the UUID assigned to the blob URL and no way to override that as the name of the page using the object element. It is possible that inside the PDF a title is specified, and that will appear in the PDF viewer as the document name, but you still get the hash name when downloading.
This appears to be a security precaution, but I cannot say for sure.
Of course, if you have control over the URL, you can easily set the PDF filename by changing the URL.
I believe Kaiido's answer expresses, briefly, the best solution here:
"if your original URI contains that filename, the easiest might be to simply make your object's data to the URI you fetched the pdf from directly"
Especially for those coming from this similar question, it would have helped me to have more description of a specific implementation (working for pdfs) that allows the best user experience, especially when serving files that are generated on the fly.
The trick here is using a two-step process that perfectly mimics a normal link or button click. The client must (step 1) request the file be generated and stored server-side long enough for the client to (step 2) request the file itself. This requires you have some mechanism supporting unique identification of the file on disk or in a cache.
Without this process, the user will just see a blank tab while file-generation is in-progress and if it fails, then they'll just get the browser's ERR_TIMED_OUT page. Even if it succeeds, they'll have a hash in the title bar of the PDF viewer tab, and the save dialog will have the same hash as the suggested filename.
Here's the play-by-play to do better:
You can use an anchor tag or a button for the "download" or "view in browser" elements
Step 1 of 2 on the client: that element's click event can make a request for the file to be generated only (not transmitted).
Step 1 of 2 on the server: generate the file and hold on to it. Return only the filename to the client.
Step 2 of 2 on the client:
If viewing the file in the browser, use the filename returned from the generate request to then invoke window.open('view_file/<filename>?fileId=1'). That is the only way to indirectly control the name of the file as shown in the tab title and in any subsequent save dialog.
If downloading, just invoke window.open('download_file?fileId=1').
Step 2 of 2 on the server:
view_file(filename, fileId) handler just needs to serve the file using the fileId and ignore the filename parameter. In .NET, you can use a FileContentResult like File(bytes, contentType);
download_file(fileId) must set the filename via the Content-Disposition header as shown here. In .NET, that's return File(bytes, contentType, desiredFilename);
client-side download example:
download_link_clicked() {
// show spinner
ajaxGet(generate_file_url,
{},
(response) => {
// success!
// the server-side is responsible for setting the name
// of the file when it is being downloaded
window.open('download_file?fileId=1', "_blank");
// hide spinner
},
() => { // failure
// hide spinner
// proglem, notify pattern
},
null
);
client-side view example:
view_link_clicked() {
// show spinner
ajaxGet(generate_file_url,
{},
(response) => {
// success!
let filename = response.filename;
// simplest, reliable method I know of for controlling
// the filename of the PDF when viewed in the browser
window.open('view_file/'+filename+'?fileId=1')
// hide spinner
},
() => { // failure
// hide spinner
// proglem, notify pattern
},
null
);
I'm using the library pdf-lib, you can click here to learn more about the library.
I solved part of this problem by using api Document.setTitle("Some title text you want"),
Browser displayed my title correctly, but when click the download button, file name is still previous UUID. Perhaps there is other api in the library that allows you to modify download file name.

Downloading PDF from API using POST

Im trying to POST a request with some header information and other parameters to my api and then have it return a PDF file.
I've got the API communication working fine however I've tried several methods of viewing the PDF and none really work 100%.
The method below was found on stackoverflow and returns response data in PDF format however when it's converted to a blob to view it becomes blank.
[API]
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
ReturnInformationITEM rtn = GetPDFData(); //Comes back as Base64
response.Content = new ByteArrayContent(System.Convert.FromBase64String(rtn.Data));
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
return response;
[client]
var blob=new Blob([result], {type: 'application/pdf'});
var pom = document.getElementById("pdf" + $counter);
pom.setAttribute('href',window.URL.createObjectURL(blob));
pom.setAttribute('download',"Test.pdf");
pom.setAttribute('target','_blank');
'I did have a click event here but firefox ignores it so I have to pass the href back to the pom and manully click it'
If I alter the API to return base64 and have the client decode it before becoming a blob it will diaply the page but with an ugly blob URL and when saving in safari it doesnt recognise the 'download' attribute.
Does anyone know a method which can be used which will work cross browser and and allow for a filename to be prefilled? and if possible auto show the file upon returning the post data.
Thanks
Mark

Set file name and format if file or file location contains neither

I'd like to set the file name and format of a "non-typical file", for a lack of better words. The files are stored at locations such as the following...
let link = https://.../videoplayback?ipbits=0&initcwndb...D134213BA9465CB74DFD36CDE47BF.102638C4A9F3ACA357F79EE747DD5F49F1E0F0DE
When one downloads such files, even if they are completely different files, they always end up being saved with the same name and never a file extension.
I've tried url.download = link + '.mp4', but this has no effect whatsoever.
How can I do this?
According to the MDN for anchor tags with the download attribute:
Can be used with blob: URLs and data: URLs, to make it easy for users
to download content that is generated programmatically using
JavaScript (e.g. a picture created using an online drawing Web app).
If the HTTP header Content-Disposition: is present and gives a
different filename than this attribute, the HTTP header has priority
over this attribute.
If this attribute is present and Content-Disposition: is set to
inline, Firefox gives priority to Content-Disposition, like for the
filename case, while Chrome gives priority to the download attribute.
This attribute is only honored for links to resources with the
same-origin.
So if you are dynamically generating these links and they come from your own server - you can set the filename. A cross-origin request will not work!
You may be able use ajax to fetch the file as a blob and trick the browser into thinking that the data is not cross-origin. See this answer for one possible implementation.
One approach would be to perform a HEAD request before setting .download property of <a> element to determine the Content-Type of the requested resource, use a JSON string, JavaScript object or other key, value pair data storage format to reflect pairs of valid MIME types to valid file extensions corresponding to the MIME type property. Use .indexOf(), RegExp or other filtering method to determine if there is a match between the property name of the string or object and the value of the property, if true store the extension value and concatenate the extension to the suggested file name.
If the file is not served with CORS headers you can try using a proxy to make HEAD of GET request to get the Content-Type header before setting the .download attribute.
It should be noted that the .download attribute is only a suggestion to the user for the filename. The user can change the name of the file at any time, including deleting the file extension, for whatever reason they may or may not have. Or not download the resource at all.
const mimeTypeExtensions = {
"text/plain": ".txt",
"video/mp4": ".mp4",
/* valid MIME types and corresponding extensions */
}
const a = document.querySelector("a");
a.addEventListener("click", event => {
if (!a.download) {
event.preventDefault();
fetch(a.href, {method: "HEAD"})
.then(response => {
const mimeType = response.headers.get("content-type");
let fileExtension = "";
for (let [key, prop] of Object.entries(mimeTypeExtensions)) {
if (key.indexOf(mimeType) > -1) {
fileExtension = prop;
break;
};
}
a.download = `filename${fileExtension}`;
a.click();
})
.catch(err => console.error(err))
}
});
click
Here is an easy option if you don't mind adding a library. FileSaver.js handles these cases well while hiding the ugly details. I was going to go into how you could create Blob, write the contents of the download into it, and then create an anchor tag with an Object URL created from the Blob, but FileSaver handles that already and you could look at the source if you really wanted.
Just modify the following to add some checks and logic for determining filename/filetype and you should be able to tackle your use case.
<script src="cdn-to-FileSaver.js"></script>
<script>
var url = ...;
fetch(url).then((response) => {
var blob = response.blob();
var filename = 'song.mp4';
saveAs(blob, filename);
});
</script>

Pass PDF document to user-agent via XHR, using modern (HTML5) methods, without compromising document encoding

I am trying to accomplish exactly what is described at Handle file download from ajax post , but with a dynamically-generated PDF file, which I'm generating with PHP v5.6.10 (using the third-party PDFLib extension, v9.0.5).
Yet, when the PDF file is downloaded, it is corrupted; no PDF-reading implementation that I've tried is able to read the file, and every observation points to the fact that the file content is being butchered somewhere between printing the PDF content to the response body and saving the file via the user-agent (web-browser) with JavaScript.
I happen to be using jQuery v2.1.4, but I'm not sure that it matters, ultimately.
Important Provisos
I should mention that, like the other asker (cited above), I have an HTML form that users fill-out and submit via the POST verb. The form submission is performed with JavaScript, because there are actually 5 forms displayed in a tabbed layout that are submitted simultaneously, and any validation errors must be sent back via AJAX and displayed without refreshing the entire page). I mention this to make clear the fact that this is a POST request, which may return either a) a JSON object (that contains validation error strings, primarily), or b) a string that represents a PDF document, which should be presented to the user-agent as a file download.
My Code
The JavaScript
$('#submit-button').click(function() {
$.ajax({
url: $(this).data('action'),
type: 'post',
data: $($(this).data('forms')).serialize(),
processData: false,
statusCode: {
500: function() {
alert('An internal server error occurred. Go pound sand.');
}
}
}).done(function(data, status, xhr) {
processResponse(data, status, xhr);
}).fail(function(jqXHR, textStatus) {
if (textStatus === 'timeout') {
alert('The request timed-out. Please try again.');
}
});
});
function processResponse(response, status, xhr)
{
if (response !== null && typeof response === 'object') {
//The server will return either a JSON string (if the input was invalid)
//or the PDF file. We land here in the former case.
}
else {
//This doesn't change the behavior.
xhr.responseType = 'blob';
//This doesn't change the behavior, either.
//xhr.overrideMimeType('text\/plain; charset=x-user-defined');
//The remainder of this function taken verbatim from:
//https://stackoverflow.com/a/23797348
// check for a filename
var filename = "";
var disposition = xhr.getResponseHeader('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
}
var type = xhr.getResponseHeader('Content-Type');
//Is logged to console as "application/pdf".
console.log(type);
var blob = new Blob([response], { type: type });
if (typeof window.navigator.msSaveBlob !== 'undefined') {
// IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
window.navigator.msSaveBlob(blob, filename);
} else {
var URL = window.URL || window.webkitURL;
var downloadUrl = URL.createObjectURL(blob);
//Is logged to console as URL() (it's an object, not a string).
console.log(URL);
//Is logged to console as "blob:https://example.com/108eb066-645c-4859-a4d2-6f7a42f4f369"
console.log(downloadUrl);
//Is logged to console as "pdftest.pdf".
console.log(filename);
if (filename) {
// use HTML5 a[download] attribute to specify filename
var a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location = downloadUrl;
} else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}
} else {
window.location = downloadUrl;
}
setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
}
}
}
The PHP
<?php
use File;
use \PDFLib;
class Pdf {
protected $p;
protected $bufferedContent;
public function __construct()
{
$this->p = new PDFlib();
$this->p->set_option('errorpolicy=return');
$this->p->set_option('textformat=utf8');
$this->p->set_option('escapesequence=true');
}
//...
public function sendToBrowser()
{
$this->bufferedContent = $this->p->get_buffer();
header_remove();
header('Content-Type: application/pdf');
header('Content-Length: ' . strlen($this->bufferedContent));
header('Content-Disposition: attachment; filename=pdftest.pdf');
$bytesWritten = File::put(realpath(__DIR__ . '/../../public/assets/pdfs') . '/' . uniqid() . '.pdf', $this->bufferedContent);
echo $this->bufferedContent;
exit;
}
//...
}
Notice that in the PHP method I am writing the PDF file to disk prior to sending it in the response body. I added this bit to determine whether the PDF file written to disk is corrupted, too, and it is not; it opens perfectly well in every reader I've tried.
Observations and Theories
What I find so strange about this is that I've tried the download in three different browsers (the most recent versions of Chrome, Firefox, and IE 11) and the PDF size is drastically different with each browser. Following are the file sizes from each:
Written to disk (not corrupted): 105KB
Chrome: 193KB
Firefox: 188KB
IE 11: 141KB
At this point, I am convinced that the problem relates to the encoding used within the PDF. I discovered a discrepancy when using WinMerge to compare the copy of the PDF that I dump directly to disk before returning the HTTP response with the copy that is handled via AJAX.
The first clue was this error message, which appears when I attempt to compare the two PDF documents:
I click OK to dismiss the error, and the comparison resumes.
The functional/correct PDF (at right, in WinMerge) is encoded using Windows-1252 (CP1252); I assume that that encoding happens within PDFLib (despite running on a GNU/Linux system). One can see from the PHP snippet, above, that I am calling $this->p->set_option('textformat=utf8'); explicitly, but that seems to set the encoding for input text that is included in the PDF document (and not the document encoding).
Ultimately, I am left wondering if there is any means by which to get this PDF to be displayed correctly after download.
Change the PDF Encoding, Instead?
I wonder if there is a "good reason" for which PDFLib is using Windows-1252 encoding to generate the PDF document. Is there any chance that this is as simple as changing the encoding on the PDFLib side to match what jQuery's AJAX implementation requires (UTF-8)?
I've consulted the PDFLib manual for more information, and there is a section dedicated to this subject: 4.2 Unicode-capable Language Bindings. This section has two subsections: 4.2.1 Language Bindings with native Unicode Strings (PHP is not among them) and 4.2.2 Language Bindings with UTF-8 Support (PHP falls into this category). But everything discussed herein seems to pertain to the actual strings that are inserted into the PDF body, and not to the overall document encoding.
Then there is 4.4 Single-Byte (8-Bit) Encodings, with the following note:
Note The information in this section is unlikely to be required in
Unicode workflows.
How does one employ a Unicode workflow in this context?
The manual is available at http://www.pdflib.com/fileadmin/pdflib/pdf/manuals/PDFlib-9.0.5-tutorial.pdf for anyone who feels it may be useful.
Approaches That I'd Prefer to Avoid
I really hesitate to get into the business of re-encoding the PDF in JavaScript, client-side, once it has been downloaded. If that is the only means by which to achieve this, I will go another direction.
Initially, my primary aim was to avoid an approach that could leave abandoned PDF files laying-around on the server, in some temporary directory (thereby necessitating a clean-up cron-job or similar), but that may be the only viable option.
If necessary, I will implement an interstitial step whereby I write the PDF file to disk (on the web-server), pass it to the client using some unsightly hidden-iframe hack, and then delete the file once the user-agent receives it. Of course, if the user-agent never finishes the download, the user closes the browser, etc., the file will be abandoned and I'll be left to clean it up by some other means (the idea of which I hate, on principle).
Any assistance with this is hugely appreciated.
You tried this on iframe?
I have this same problem, but i resolve by iframe.
ugly code but works for me.
solution with iframe

How to give a Blob uploaded as FormData a file name?

I am currently uploading images pasted from the clipboard with the following code:
// Turns out getAsFile will return a blob, not a file
var blob = event.clipboardData.items[0].getAsFile(),
form = new FormData(),
request = new XMLHttpRequest();
form.append("blob",blob);
request.open(
"POST",
"/upload",
true
);
request.send(form);
Turns out the uploaded form field with receive a name similar to this: Blob157fce71535b4f93ba92ac6053d81e3a
Is there any way to set this or receive this file name client side, without doing any server side communication?
For Chrome, Safari and Firefox, just use this:
form.append("blob", blob, filename);
(see MDN documentation)
Adding this here as it doesn't seem to be here.
Aside from the excellent solution of form.append("blob",blob, filename); you can also turn the blob into a File instance:
var blob = new Blob([JSON.stringify([0,1,2])], {type : 'application/json'});
var fileOfBlob = new File([blob], 'aFileName.json');
form.append("upload", fileOfBlob);
Since you're getting the data pasted to clipboard, there is no reliable way of knowing the origin of the file and its properties (including name).
Your best bet is to come up with a file naming scheme of your own and send along with the blob.
form.append("filename",getFileName());
form.append("blob",blob);
function getFileName() {
// logic to generate file names
}
That name looks derived from an object URL GUID. Do the following to get the object URL that the name was derived from.
var URL = self.URL || self.webkitURL || self;
var object_url = URL.createObjectURL(blob);
URL.revokeObjectURL(object_url);
object_url will be formatted as blob:{origin}{GUID} in Google Chrome and moz-filedata:{GUID} in Firefox. An origin is the protocol+host+non-standard port for the protocol. For example, blob:http://stackoverflow.com/e7bc644d-d174-4d5e-b85d-beeb89c17743 or blob:http://[::1]:123/15111656-e46c-411d-a697-a09d23ec9a99. You probably want to extract the GUID and strip any dashes.
Haven't tested it, but that should alert the blobs data url:
var blob = event.clipboardData.items[0].getAsFile(),
form = new FormData(),
request = new XMLHttpRequest();
var reader = new FileReader();
reader.onload = function(event) {
alert(event.target.result); // <-- data url
};
reader.readAsDataURL(blob);
It really depends on how the server on the other side is configured and with what modules for how it handles a blob post. You can try putting the desired name in the path for your post.
request.open(
"POST",
"/upload/myname.bmp",
true
);
Are you using Google App Engine?
You could use cookies (made with JavaScript) to maintain a relationship between filenames and the name received from the server.
When you are using Google Chrome you can use/abuse the Google Filesystem API for this. Here you can create a file with a specified name and write the content of a blob to it. Then you can return the result to the user.
I have not found a good way for Firefox yet; probably a small piece of Flash like downloadify is required to name a blob.
IE10 has a msSaveBlob() function in the BlobBuilder.
Maybe this is more for downloading a blob, but it is related.

Categories

Resources