Angular download file after POST request - c#

I have several export routes on an asp.net core application, but all of them are accessed by a GET request, eg: /api/{projectid}/{parameter}
These requests are generating xlsx files and sending them to the client. Now I have a similar request but I have to pass a long array to the method, so I would like to make it into a POST method and send the array (and other parameters) in the http body.
I get the correct response from the server (an array buffer starting with PK...) but I can't tell angular to save it as a file as I did with the similar GET requests. If I rewrite this back to start a GET request it works fine. What am I doing wrong?
Controller method:
[HttpPost("[action]")]
public IActionResult Export([FromBody] DistributionExportPostModel model)
{
var project = _ctx.Projects.FirstOrDefault(x=>x.Id == model.ProjectId);
byte[] xlsx = createXlsxFile(project, model.Selection, model.ComparisonBase);
var mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
var fileName = $"TaskExport-"+project.Name+"-"+DateTime.Now.ToString("yyyyMMddHHmmss")+".xlsx";
return File(xlsx, mimeType, fileName);
}
Angular provider method:
export(projectid:string, selection:string[], comparisonBase:string):Promise<any[]> {
//let headers:Headers = new Headers();
//headers.append('Accept', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
//let params: URLSearchParams = new URLSearchParams();
let requestOptions = new RequestOptions();
requestOptions.responseType = ResponseContentType.ArrayBuffer;
return new Promise<any[]>((resolve) =>
this.http.post('/api/Distribution/Export', {
//'+projectid+'/'+comparisonBase+'/'+selection.join(','),
'ProjectId': projectid,
'Selection': selection,
'ComparisonBase': comparisonBase
}, requestOptions).subscribe((res) => {
window.open(res.url, '_blank');
resolve();
})
);
}

Below should work on Chrome.
var blob = new Blob(yourData, {type: "octet/stream"});
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = name;
a.click();
window.URL.revokeObjectURL(url);
For IE and Firefox, try this:
window.navigator.msSaveBlob(blob, filename);
You may need to add the anchor to the DOM before clicking it.

Related

Angular - How to download file from Web API on subscribe()

In Angular I'm trying to download an excel file from my Web API Server
If I call my API from an <a> component just like this:
Download
The file downloads fine and without handling the response I get the desired behaviour:
How can I get the same result making the request from my Angular DownloadService and handling the result on .subscribe()?
this.downloadService.download()
.subscribe(file => {/*What to put here for file downloading as above*/});
Note that the server creates the response like this:
byte[] b = File.ReadAllBytes(HttpRuntime.AppDomainAppPath + "/files/" + "file.xlsx");
var dataStream = new MemoryStream(b);
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StreamContent(dataStream);
response.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
response.Content.Headers.ContentDisposition.FileName = "File";
response.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping("file.xlsx"));
return response;
Thanks in advance! :)
try this work around:
.subscribe(file =>
{
const a = document.createElement('a');
a.setAttribute('type', 'hidden');
a.href = URL.createObjectURL(file.data);
a.download = fileName + '.xlsx';
a.click();
a.remove();
});
The comment above should work, but here is another way
.subscribe(file) {
const blob = new Blob([file], { type: 'text/csv' }); // you can change the type
const url= window.URL.createObjectURL(blob);
window.open(url);
}
You can add js-file-download library which supports all browsers
npm install js-file-download --save
you can use window.URL.createObjectURL(blob); but IE does not support it...
here is my code for downloading Excel files from the Backend
import axios from 'axios';
import * as FileSaver from 'file-saver';
const result: any = await axios.create().post("http://localhost:8080/file",
{
responseType: "arraybuffer",
});
let blob = new Blob([result.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
FileSaver.saveAs(blob, "export.xlsx");

Browser doesn't show progress bar when downloading a file

I have an Angular application with an ASP.NET Web API.
I want to download a file stored on my server. Currently, this is the code I have:
[HttpGet]
[Route("downloadFile")]
[JwtAuthentication] //Only a connected user can download the file
public async Task<HttpResponseMessage> DownloadFile(string path)
{
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
var fileStream = File.OpenRead(path);
result.Content = new StreamContent(fileStream);
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
result.Content.Headers.ContentLength = fileStream.Length;
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = fileStream.Name,
Size = fileStream.Length
};
return result;
}
And in my Angular code:
// file-navigation.service.ts
downloadFile(file: FileElement) {
const data = { path: this.formatPath(true) + file.name };
return this.http.get(this.apiUrl + '/downloadFile', { params: data, responseType: 'blob' });
}
// file-navigation.component.ts
this.fileNavigationService.downloadFile(element).subscribe(result => {
this.generateDownload(element, result, false);
});
generateDownload(element: FileElement, blob: Blob, isArchive: boolean) {
const fileName = element != null ? element.name : 'Archive';
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
window.navigator.msSaveBlob(blob, fileName + (isArchive ? '.zip' : ''));
} else {
const link = document.createElementNS(
'http://www.w3.org/1999/xhtml',
'a'
);
(link as any).href = URL.createObjectURL(blob);
(link as any).download = fileName + (isArchive ? '.zip' : '');
document.body.appendChild(link);
link.click();
setTimeout(function () {
document.body.removeChild(link);
link.remove();
}, 100);
}
}
With this, I successfully download a file from the server.
However, the download bar in Chrome only appears when the download is done. So if the file is too big, the user won't get any indicator that his file is currently being downloaded.
Below is a screenshot of a 16Mb file being downloaded. The server is currently sending data but the download bar doesn't appear.
Then, once the download has completed, the file appears in the download bar at the bottom of the screen.
How can I send the file to the browser so that it shows this indicator to the user?
Thank you very much.
EDIT:
As #CodeCaster pointed out, redirecting to the URL could work, however, my URL is protected so that only connected users can download the file.
On Angular side, just use anchor tag and pass the API URL in href attribute.
<a href = {this.apiUrl + '/downloadFile' + '?' + 'your params'}>Download</a>
and also on server side before streaming data, make sure you have set the following response header.
res.setHeader('content-length',data.ContentLength) (optional)
res.setHeader('content-type', mimetype);
res.setHeader('content-disposition', 'attachment; filename');

return meaningful error string to browser when expecting a blob

I'm returning a a PDF or XLSX file to the browser. I do this by setting the responseType of the ajax object that initiated the request to 'blob'. This seems to work as needed. I'm having problems figuring out a good way to pass an error string to the browser in cases where the file couldn't be created.
If I don't set any responseType on the ajax object I can read the response text as the meaninful error string I've set. This, though, means that I'm no longer able to properly read the response as a PDF or XLSX file in the cases where things proceeded properly. And, of course, I can't set the responseType of the ajax object after I've received the response.
Controller
public ActionResult GetFile() {
// process work, set stream and success bool
if (wasSuccessful) {
return File(stream, "application/pdf");
}
else {
return Content("a meaningful error for the UI");
}
}
cshtml
function getFile(e, extension) {
var xhr = new XMLHttpRequest();
xhr.open('POST', e.value, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', "application/x-www-form-urlencoded");
xhr.onload = function (ee) {
if (this.status == 200) {
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
var blob = new Blob([this.response], { type: 'octet/stream' }), url = window.URL.createObjectURL(blob);
a.href = url;
a.download = "file." + extension;
a.click();
window.URL.revokeObjectURL(url);
}
};
xhr.send($("#form").serialize());
}
Can I have my ajax expect a blob but somehow read a simple string in certain error conditions?

Pass large amount of data to MVC get method

I have an angularjs form that I would like users to be able to download a filled out PDF of what they requested. I have an [HTTPGet] method that accepts a base64 encoding of the json request. Everything works except when my base64 encoding goes above 2027 characters in length. When that happens, I don't receive anything more than the 2027 characters. Is there another way to send the data on a get call or a better way to do this.
My angular js app does this.
scope.DownloadTempResults = function () {
window.open('/Home/GetCopyOfRequest?strRequest=' + window.btoa(JSON.stringify(scope.request)), '_blank', '');
}
My C# code behind does this.
[HttpGet]
public ActionResult GetCopyOfRequest(string strRequest)
{
byte[] data = Convert.FromBase64String(strRequest);
string request = Encoding.UTF8.GetString(data);
ExternalModel model = JsonHelper.Deserialize<ExternalModel>(request);
byte[] filedata = ObjectFactory.GetInstance<ManagerFactory>().Create().CreateRequestForm(model);
string contentType = "application/pdf";
var cd = new System.Net.Mime.ContentDisposition
{
FileName = "name of File",
Inline = true
};
Response.AppendHeader("Content-Disposition", cd.ToString());
return File(filedata, contentType);
}
At the end of the day, what we are trying to do is encode the json in the query string so that you can not look directly at the query string and get the values of the json object. If I am going about this completely wrong, please let me know what to look up to accomplish this.
Updated below here.
So based on the comments, I changed to a post method that looks like this.
[HttpPost]
public HttpResponseMessage GetCopyOfRequest(string strRequest)
{
ExternalModel model = JsonHelper.Deserialize<ExternalModel>(strRequest);
byte[] filedata = ObjectFactory.GetInstance<IManagerFactory>().Create().CreateRequestForm(model);
string fileName = "Request form for request";
HttpResponseMessage httpResponseMessage = new HttpResponseMessage();
httpResponseMessage.Content = new ByteArrayContent(filedata);
httpResponseMessage.Content.Headers.Add("x-filename", fileName);
httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
httpResponseMessage.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
httpResponseMessage.Content.Headers.ContentDisposition.FileName = fileName;
httpResponseMessage.StatusCode = HttpStatusCode.OK;
return httpResponseMessage;
}
In the angularjs app, I have the following code.
$http({
url: '/Home/GetCopyOfRequest',
method: 'POST',
data: { strRequest: JSON.stringify(request) },
responseType: 'arraybuffer'
}).success(function (data, status, headers) {
var file = new Blob([data], { type: 'application/pdf' });
var fileURL = URL.createObjectURL(file);
window.open(fileURL);
}).error(function (response) {
});
When I do this on the server, my byte array has a length of 400000. On the client side, I have an array of 270. I followed an example from How to display a server side generated PDF stream in javascript sent via HttpMessageResponse Content .

Download Zip Archive from WebAPI in Response to POST Request

I'm trying to deliver a zip archive in response to an AJAX POST request made from Axios to WebAPI.
On the client side I have
import AjaxDownload from "../../data/AjaxDownload";
AjaxDownload.post(id, pageRecords, {
responseType: "blob"
}).then((response) => {
let blob = new Blob([response.data], { type: extractContentType(response) }),
url = window.URL.createObjectURL(blob);
window.open(url, "_self");
}).catch((error) => {
// ...
}).then(() => {
// ...
});
function extractContentType(response: AxiosResponse): string {
return response.headers["content-type"] || "";
}
// AjaxDownload:
import * as axios from "axios";
import { apiUrl } from "./Ajax";
const ajax = axios.default.create({
baseURL: new URL("download", apiUrl).toString(),
timeout: 3600000 // 1 hour
});
export default ajax;
That posts to the following WebAPI method - and the POST part of that client-side logic works exactly as expected.
[HttpPost]
[Route("download/{id:guid}")]
public async Task<HttpResponseMessage> Download(Guid id, [FromBody] IEnumerable<PageRecord> pageRecords)
{
var stream = await _repo.GetAsArchiveStream(id,
pageRecords,
true).ConfigureAwait(false);
stream.Position = 0;
var result = new HttpResponseMessage(HttpStatusCode.OK) {Content = new StreamContent(stream)};
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") {FileName = $"{...}.zip"};
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); // "application/zip" has same result
result.Content.Headers.ContentLength = stream.Length;
return result;
}
However, the browser displays the result.Content as a JSON object, without the zip archive. I assume that the it's displaying as JSON because the request mentions JSON, but why does it appear to ignore the binary content - particularly as the Content-Type header details the type of content?
And as you can see, the JavaScript is also expecting to read in the content as a blob.
I don't see how my code differs meaningfully from this answer - please explain if there is a crucial difference.
On the server-side, I've also tried returning...
return new FileStreamResult(stream, "application/zip");
The problem with this approach is that there's no way to set a filename. Firefox does download the zip albeit with a random name while Chrome doesn't appear to download anything at all.
There must be a way to do this, right? To POST a request to a WebAPI method which returns a zip archive, and the client-side then presents a Save dialog? What am I missing?
I managed to solve this simply by returning the zip from the controller action using...
return File(stream,
"application/zip",
"FILENAME.zip");
And in the client-side code I can fetch the filename from the headers using some JavaScript found in this SO answer.
let blob = new Blob([response.data], { type: extractContentType(response) }),
downloadUrl = window.URL.createObjectURL(blob),
filename = "",
disposition = response.headers["content-disposition"];
if (disposition && disposition.indexOf("attachment") !== -1) {
let filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/,
matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
var a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location.href = downloadUrl;
} else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}

Categories

Resources