Angular: can't download server side generated spreadsheet - angularjs

I'm trying to download a server generated spreadsheet.
My application uses Angular on the front-end and Java on the back-end.
Here's the method on the back-end that receives the request to generate and return the file:
#RequestMapping(value = "download", method = RequestMethod.GET, produces = "application/xls")
public ResponseEntity<InputStreamResource> download() throws IOException {
ByteArrayOutputStream fileOut = new ByteArrayOutputStream();
HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet worksheet = workbook.createSheet("POI Worksheet");
HSSFRow row1 = worksheet.createRow((short) 0);
HSSFCell cellA1 = row1.createCell((short) 0);
cellA1.setCellValue("Hello!");
HSSFCellStyle cellStyle = workbook.createCellStyle();
cellStyle.setFillForegroundColor(HSSFColor.GOLD.index);
cellStyle.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
cellA1.setCellStyle(cellStyle);
workbook.write(fileOut);
fileOut.close();
byte[] file = fileOut.toByteArray();
return ResponseEntity
.ok()
.contentLength(file.length)
.contentType(MediaType.parseMediaType("application/octet-stream"))
.body(new InputStreamResource(new ByteArrayInputStream(file)));
}
And on the front-end, the following function is executed when the user clicks on Download button:
$scope.exportFile = function() {
$http.get('http://127.0.0.1:8000/api/excel/download')
.success(function (data, status, headers, config) {
var anchor = angular.element('<a/>');
anchor.attr({
href: 'data:application/octet-stream;charset=utf-8,' + encodeURI(data),
target: '_blank',
download: 'spreadsheet.xls'
})[0].click();
})
.error(function (data, status, headers, config) {
// handle error
});
};
The returned spreadsheet contains unreadable characters.
If I access http://127.0.0.1:8000/api/excel/download directly, the spreadsheet is downloaded without the .xls extension (with no extension at all). If I rename the file adding the .xls extension and then open it, I can see the file contents. So I think the problem it's on the call Angular does to back-end and not on generating the file on Java.
Has anyone experienced this situation or have some example to share? What am I doing wrong?

The Content-Disposition header should do the trick. So, you would have something like this:
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData("Attachment", "spreadsheet.xls");
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentLength(file.length);
return new ResponseEntity<InputStreamResource>(
new InputStreamResource(new ByteArrayInputStream(file)),
headers, HttpStatus.OK);

Related

When downloading a file using ResponseEntity in Spring, byte breaks in reaction

This is a method of downloading files from Spring to ResponseEntity.
When you download it, the byte breaks and comes out.
If you use tag a, you can download the file without breaking it, but it is difficult to put the event after downloading the file using response.
Is there a way?
// Spring
Resource resource = qnaService.loadAsResource(tempFileName);
File file = resource.getFile();
if (file.exists()) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", fileName);
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");
headers.set("Content-Transfer-Encoding", "binary");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + OriginFileName)
.contentType(MediaType.APPLICATION_XML)
.contentLength(file.length())
.body(new InputStreamResource(new FileInputStream(file)));
}
// React
const data = await request({
url: `/api/qna/file/test/${node.qnaFileID}`,
method: request.method.get(),
});
console.log(data);
const blob = new Blob([data], { type: 'application/xml;charset=utf-8' });
saveAs(blob, node.name);
// Data is broken.
data: "�PNG\r\n\u001a\n\u0000\u0000\u0000\rIHDR\u0000\u0000\t�\u0000\u0000\u0004z\b\u0006\u0000\u0000\u0000�س]\u0000\u0000\fliCCPICC Profile\u0000\u0000H��W\u0007XS�\u0016�[RIh\u0001\u0004���\u0004�\u001a#J\b-��"�\bI �Ę\u0010T�eQ���\b��U\u0011Ŷ\u0002bǮ,��/\u0016T�uQ\u0017\u0
just add responseType: blob to your request

Throwing error while downloading file in ASP.NET Core with status code 400

I'm working on download feature. When I run code on localhost, it runs perfectly without any error. But when I uploads same code to server then it returns Failed to load resource: the server responded with a status of 400 (Bad Request) error in console of browser. For requesting and getting response from server, I have used Axios. I'm newbie to ASP.NET Core + ReactJS technology stack and working on APIs for the first time so it's being difficult for me to figuring out root cause of this error.
Here is my code for requesting data from server.
axios({
method: 'POST',
url: '/api/ImageFetch/ImageFetchPoint',
responseType: 'blob',// important
headers: {
'Content-Type': 'application/json'
},
data: filepath
}).then(function(response) {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
}).catch(error => {
console.log("Status:", error.response.status);
console.log("Data:", error.response.data);
});
For getting response as file(any extension) from server, I've used following code.
[HttpPost("PanImageFunction")]
[Route("api/[controller]/[action]")]
public async Task<IActionResult> ImageFetchFunction([FromBody] ImageFetchRequest request)
{
var filePath = request.ImagePath;
var filename = System.IO.Path.GetFileName(filePath);
string path="unknownpath";
try {
if (filename == null)
return Content("filename not present");
path = Path.Combine(
Directory.GetCurrentDirectory(),
"RegistrationImages", filename);
} catch(Exception ex) {
new Logger().write(ex);
}
var memory = new MemoryStream();
using (var stream = new FileStream(path, FileMode.Open))
{
await stream.CopyToAsync(memory);
}
System.Net.Mime.ContentDisposition cd = new System.Net.Mime.ContentDisposition
{
FileName = filename,
Inline = false // false = prompt the user for downloading; true = browser to try to show the file inline
};
Response.Headers.Add("Content-Type", "multipart/form-data");
Response.Headers.Add("Content-Disposition", cd.ToString());
Response.Headers.Add("X-Content-Type-Options", "nosniff");
memory.Position = 0;
return File(memory, GetContentType(path), Path.GetFileName(path));
}
private string GetContentType(string path)
{
var types = GetMimeTypes();
var ext = Path.GetExtension(path).ToLowerInvariant();
return types[ext];
}
private Dictionary<string, string> GetMimeTypes()
{
return new Dictionary<string, string>
{
{".txt", "text/plain"},
{".pdf", "application/pdf"},
{".doc", "application/vnd.ms-word"},
{".docx", "application/vnd.ms-word"},
{".xls", "application/vnd.ms-excel"},
{".png", "image/png"},
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".gif", "image/gif"},
{".csv", "text/csv"}
};
}
Complete code works perfectly on localhost. But when I uploads it on server then it throws 400 bad request error on console of browser as you can see in following image.
I have checked many forums and articles but most of the articles related to ASP.NET core giving solution related to Razor pages and not giving solutions related to ReactJS and Axios.
How can I fix this error?

Download response as an excel file

File is not downloading at browser. I'm preparing the file and writing it to output stream of response.
Rest API is there:
#RequestMapping(value = "/export-companies",
method = {RequestMethod.GET, RequestMethod.HEAD})
#Timed
public void downloadCompanies(HttpServletResponse response) throws URISyntaxException {
HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet sheet = workbook.createSheet("Sample sheet");
Map<String, Object[]> data = new HashMap<String, Object[]>();
data.put("1", new Object[] {"Emp No.", "Name", "Salary"});
data.put("2", new Object[] {1d, "John", 1500000d});
data.put("3", new Object[] {2d, "Sam", 800000d});
data.put("4", new Object[] {3d, "Dean", 700000d});
Set<String> keyset = data.keySet();
int rownum = 0;
for (String key : keyset) {
Row row = sheet.createRow(rownum++);
Object [] objArr = data.get(key);
int cellnum = 0;
for (Object obj : objArr) {
Cell cell = row.createCell(cellnum++);
if(obj instanceof Date)
cell.setCellValue((Date)obj);
else if(obj instanceof Boolean)
cell.setCellValue((Boolean)obj);
else if(obj instanceof String)
cell.setCellValue((String)obj);
else if(obj instanceof Double)
cell.setCellValue((Double)obj);
}
}
try {
ByteArrayOutputStream outByteStream = new ByteArrayOutputStream();
workbook.write(outByteStream);
byte [] outArray = outByteStream.toByteArray();
response.setContentType("application/ms-excel");
response.setContentLength(outArray.length);
response.setHeader("Expires:", "0"); // eliminates browser caching
response.setHeader("Content-Disposition", "attachment; filename=template.xls");
OutputStream outStream = response.getOutputStream();
outStream.write(outArray);
outStream.flush();
workbook.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
From front end (using Angular JS):
(function() {
'use strict';
angular
.module('MyApp')
.factory('CompanyExportService', CompanyExportService);
CompanyExportService.$inject = ['$resource'];
function CompanyExportService ($resource) {
var service = $resource('api/export-companies', {}, {
'get': {
method: 'GET',
isArray: false
}
});
return service;
}
})();
File contents are there in response as non-readable format. But file is not downloaded at browser.
Angular will receive the file contents mere character sequences. You need to create a file from these characters and initiate the browser download in frontend.
You can do it like this -
var blob = new Blob([data],
{type: 'application/vnd.openxmlformat-officedocument.spreadsheetml.sheet;'});
saveAs(blob, fileName);
where data is the response you received form your API. The saveAs function is part of FileSaver.js library. Although you can look on how to manually do that but why reinvent the wheel?
Downloading files with XHR is problematic. As long as you do only GET requests, there exists much simpler approach to trigger browser to download file.
Use JavaScript native method window.open(url).
It does work well in all browsers including IE9.
In code below, I use $window, which is Angular's proxy for native window object.
Example for your code could be like:
(function() {
'use strict';
angular
.module('MyApp')
.factory('CompanyExportService', CompanyExportService);
CompanyExportService.$inject = ['$window'];
function CompanyExportService ($window) {
var exportUrl = 'api/export-companies';
return {
download: download
}
function download() {
$window.open(exportUrl);
}
}
})();
Note that this action is out of scope of Angular, you can't do much about error handling or waiting till the file will be downloaded. Might be problem if you want to generate huge Excel files or your API is slow.
For more details, read question: Spring - download response as a file
Update:
I've replaced window.location.href with window.open() which seems to be better choice for downloading files.
If your API will throw an error page instead of file, window.location.href will replace current page (thus losing its state). $window.open() however will opens this error in new tab without losing current state of of application.
You can download file in new tab. Modern browser are closing them automatically when downloading is completed.
By opening new window you get reference to it, when downloading is completed then window.closed is set to true.
Unfortunatelly you need to check from time-to-time this param inside interval ...
var newWindowRef = $window.open(url, name);
if (newWindowRef) {
if (newWindowRef.document.body) { // not working on IE
newWindowRef.document.title = "Downloading ...";
newWindowRef.document.body.innerHTML = '<h4>Your file is generating ... please wait</h4>';
}
var interval = setInterval(function() {
if (!!newWindowRef.closed) {
// Downloading completed
clearInterval(interval);
}
}, 1000);
} else {
$log.error("Opening new window is probably blocked");
}
Tested and works on Chrome v52, FF v48 and IE 11

Can't get content-disposition from http.get header?

I am trying to download a file using web API + Angularjs and it's working fine, however I'm sending 'content-disposition' in the header from the web API but I'm unable to access it in the response-header.
WebAPI:
[HttpGet]
[AllowCrossSiteJson]
public HttpResponseMessage GetAcquisitionsTemplate()
{
var docContent = _d.FirstOrDefault();
Stream stream = new MemoryStream(docContent.Content);
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
string contentDisposition = string.Concat("attachment; filename=", docContent.Name.Replace(" ", String.Empty), "." + docContent.Extension);
result.Content = new StreamContent(stream);
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
result.Content.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse(contentDisposition);
return result;
}
Client Side (AngularJs):
$http.get(url)success(function (data, status, headers) {
var header= headers();
})
In the header I'm getting the following values but I'm unable to get content-disposition. Please correct me where I'm wrong?
I guess you are trying to hit it from localhost while the file is hosted on the server, in that case ask your server team to allow , Access-Control-Expose-Headers(‘Content-Disposition’) . With this Header, custom headers like COntent-Disposition will be accessible to you over cross domain

AngularJS - ajax - MVC file multiple upload without form data

I am using Flow.JS http://flowjs.github.io/ng-flow/ for file upload.
My requirement is such that I will have to send the following data all in one save button click
multiple files
two string values alongwith the files.
The following way works fine.
Upload ajax call
$scope.UploadFiles = function (flows) {
var data = new FormData();
$.each(flows.files, function (i, flowfile) {
data.append('file' + i, flowfile.file);
});
data.append('message', $scope.Subject);
data.append('subject', $scope.Message);
$.ajax({
url: 'url\savedata',
data: files,
cache: false,
contentType: false,
processData: false,
type: 'POST'
});
}
And my MVC conroller
public JsonResult Savedata()
{
var httpRequest = System.Web.HttpContext.Current.Request;
if(httpRequest.Files.Count != 0)
{
var collection = 0;
foreach (string file in httpRequest.Files)
{
//manipulate file data
}
}
var message = httpRequest.Forms['message'];
var subject= httpRequest.Forms['subject'];
}
All this works fine. I want to know if there is a better way to do this instead of using form data and possibly send all this data using a data model instead, since I need that for some MVC data validations.

Resources