|
I have a Web Api that is returning FileStreamResult, I want to download the file that's in zip format, I could able to download if Web Api is returning the raw byte array, but for some reason lead wanted it only from FileStreamResult, can somebody please help me how can I write to a file using the FileStreamResult - thank you. I am putting the code here, the Web Api method which is returning as FileStreamResult, but the client code of javascript functions and react events I am putting which is working but with bytes array, so that somebody can suggest me what modifications I can make to the client script to make work and download the zip file. I need some help please - thank you.
Below is my Web Api Code:
[EnableCors("AnotherPolicy")]
[HttpPost]
public FileStreamResult Post([FromForm] string communityName, [FromForm] string files)
{
var removedInvalidCharsFromFileName = removeInvalidCharsFromFileName(files);
var tFiles = removedInvalidCharsFromFileName.Split(',');
string rootPath = Configuration.GetValue<string>("ROOT_PATH");
string communityPath = rootPath + "\\" + communityName;
byte[] theZipFile = null;
FileStreamResult result = null;
using (MemoryStream zipStream = new MemoryStream())
{
using (ZipArchive zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
{
foreach (string attachment in tFiles)
{
var zipEntry = zip.CreateEntry(attachment);
using (FileStream fileStream = new FileStream(communityPath + "\\" + attachment, FileMode.Open))
using (Stream entryStream = zipEntry.Open())
{
fileStream.CopyTo(entryStream);
}
}
}
theZipFile = zipStream.ToArray();
result = new FileStreamResult(zipStream, "application/zip") { FileDownloadName = $"{communityName}.zip" };
}
return result;
}
Here is my React event for the download
handleDownload = (e) => {
e.preventDefault();
var formData = new FormData();
formData.append('communityname', this.state.selectedCommunity);
formData.append('files', JSON.stringify(this.state['checkedFiles']));
let url = clientConfiguration['filesApi.local'];
axios({
method: 'post',
responseType: 'application/blob',
contentType: 'application/blob',
url: url,
data: formData
})
.then(res => {
console.log(res);
console.log(res.data);
let fileName = `${this.state['selectedCommunity']}`
const arrayBuffer = base64ToArrayBuffer(res.data);
createAndDownloadBlobFile(arrayBuffer, fileName, 'zip');
})
.catch(error => {
console.log(error.message);
});
};
Here are the common fuctions I have written in CommonFuncs.js file:
export function createAndDownloadBlobFile(body, filename, extension = 'pdf') {
const blob = new Blob([body]);
const fileName = `${filename}.${extension}`;
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, fileName);
} else {
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
}
export function base64ToArrayBuffer(base64) {
const binaryString = window.atob(base64);
const bytes = new Uint8Array(binaryString.length);
return bytes.map((byte, i) => binaryString.charCodeAt(i));
}
Any help would be very helpful please- thanks in advance
modified 17-Oct-19 19:45pm.
|
|
|
|
|
Well, I had to Google around and read several different examples and explanations for how it works.
I basically searched "FileStreamResult" and this was the most popular answer for using "FileStreamResult" .
According to the documentation, this will return a stream with the appropiate header info that will make it look like a download to the browser, and trigger the browser to prompt for a download.
return new FileStreamResult(stream, new MediaTypeHeaderValue("application/zip"))
{
FileDownloadName = "application.zip"
};
If it ain't broke don't fix it
Discover my world at jkirkerx.com
|
|
|
|
|
Thank you jkikerx, I did it in the following way, its completed ready to demo (just in case if somebody wants implement this I am putting my implementation here) - thanks for jumping in to help me.
Here is Web Api code C#:
[EnableCors("AnotherPolicy")]
[HttpPost]
public FileStreamResult Post([FromForm] string communityName, [FromForm] string files)
{
var removedInvalidCharsFromFileName = removeInvalidCharsFromFileName(files);
var tFiles = removedInvalidCharsFromFileName.Split(',');
string rootPath = Configuration.GetValue<string>("ROOT_PATH");
string communityPath = rootPath + "\\" + communityName;
MemoryStream zipStream = new MemoryStream();
using (ZipArchive zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
{
foreach (string attachment in tFiles)
{
var zipEntry = zip.CreateEntry(attachment);
using (FileStream fileStream = new FileStream(communityPath + "\\" + attachment, FileMode.Open))
{
using (Stream entryStream = zipEntry.Open())
{
fileStream.CopyTo(entryStream);
}
}
}
}
zipStream.Position = 0;
return File(zipStream, "application/octet-stream");
}
Then my client side React code is here:
handleDownload = (e) => {
e.preventDefault();
var formData = new FormData();
formData.append('communityname', this.state.selectedCommunity);
formData.append('files', JSON.stringify(this.state['checkedFiles']));
let url = clientConfiguration['filesApi.local'];
axios({
method: 'post',
responseType: 'arraybuffer',
url: url,
data: formData
})
.then(res => {
let extension = 'zip';
let tempFileName = `${this.state['selectedCommunity']}`
let fileName = `${tempFileName}.${extension}`;
const blob = new Blob([res.data], {
type: 'application/octet-stream'
})
saveAs(blob, fileName)
})
.catch(error => {
console.log(error.message);
});
};
this event is called when button is clicked or form submitted. Thanks for all the support the SO has given - thanks a lot.
|
|
|
|
|
What is the "saveAs"?
Is that from a NPM package?
KendoReact?
If it ain't broke don't fix it
Discover my world at jkirkerx.com
|
|
|
|
|
npm package file-saver, it is one of the beauties of the React, even though its PIB while learning, but once its, then its all just imports. But still have to learn a lot. If you want guide me better and easier examples to learn regarding like store, react-redux, modal implementation (a new page url opening in a modal and communicating with parent Window), thunk. If you can write brief introductions about the above things also very sweet. But I understand its tough for you and I understand you have gone through this same situations like me, I am reading as well, too many things confusing a lot . Thank you my friend.
|
|
|
|
|
Like I said, I haven't used react yet, just the first version of it. All I can do is help with the .Net Core issues and use Angular knowledge to help with React. But this is discussion forum platform in which you talk about an issue, and not a quick answer platform.
I still wonder why the use of the plugin is needed to save a file. I really don't think the saveAs is needed, and don't see a link or relationship to saveAs in the API call. Your API just takes a POST of body data, generates a file and returns the file with the appropriate header info to save to a target, or just download depending on how the browser is setup.
I'm just scratching my head on this. Perhaps I'll experiment with it later on.
If it ain't broke don't fix it
Discover my world at jkirkerx.com
|
|
|
|
|
jkirkerx wrote: I'm just scratching my head on this. Perhaps I'll experiment with it later on.
Thanks welcome any suggestions my buddy
|
|
|
|
|
I am calling the a Web Api from the my react component using fetch, when I used to run it as one application, there was no problem, but when I am running the application react separate from api, I am getting the CORS error, my fetch call is as below,
componentDidMount() {
console.log(clientConfiguration)
fetch(clientConfiguration['communitiesApi.local'])
.then((response) => {
return response.json();
})
.then(data => {
console.log(data);
let communitiesFromApi = data.map(community => { return { value: community, display: community } });
this.setState({ communities: [{ value: '', display: 'Select a Community...' }].concat(communitiesFromApi) });
}).catch(error => {
console.log(error);
});
};
and my post call using axios as below also.
handleDownload = (e) => {
e.preventDefault();
var formData = new FormData();
formData.append('communityname', this.state.selectedCommunity);
formData.append('files', JSON.stringify(this.state['checkedFiles']));
let url = clientConfiguration['filesApi.local'];
let tempFiles = clientConfiguration['tempFiles.local'];
axios({
method: 'post',
responseType: 'application/zip',
contentType: 'application/zip',
url: url,
data: formData
})
.then(res => {
var fileName = `${this.state['selectedCommunity']}.zip`;
saveAs(`https:
});
};
Here is my api code:
[HttpGet("{communityName}")]
public string Get(string communityName)
{
string rootPath = Configuration.GetValue<string>("ROOT_PATH");
string communityPath = rootPath + "\\" + communityName;
string[] files = Directory.GetFiles(communityPath);
List<string> strippedFiles = new List<string>();
foreach (string file in files)
{
strippedFiles.Add(file.Replace(communityPath + "\\", ""));
}
return JsonConvert.SerializeObject(strippedFiles);
}
[HttpPost]
public string Post([FromForm] string communityName, [FromForm] string files)
{
var removedInvalidCharsFromFileName = removeInvalidCharsFromFileName(files);
var tFiles = removedInvalidCharsFromFileName.Split(',');
string rootPath = Configuration.GetValue<string>("ROOT_PATH");
string communityPath = rootPath + "\\" + communityName;
byte[] theZipFile = null;
using (MemoryStream zipStream = new MemoryStream())
{
using (ZipArchive zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
{
foreach (string attachment in tFiles)
{
var zipEntry = zip.CreateEntry(attachment);
using (FileStream fileStream = new FileStream(communityPath + "\\" + attachment, FileMode.Open))
using (Stream entryStream = zipEntry.Open())
{
fileStream.CopyTo(entryStream);
}
}
}
theZipFile = zipStream.ToArray();
}
string tempFilesPath = Configuration.GetValue<string>("Temp_Files_Path");
if (!System.IO.Directory.Exists(tempFilesPath))
System.IO.Directory.CreateDirectory(tempFilesPath);
System.IO.File.WriteAllBytes($"{tempFilesPath}\\{communityName}.zip", theZipFile);
return $"{communityName}.zip";
}
And I am getting the error for Get as below: "
Access to fetch at 'https://localhost:44368/api/communities' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. "
modified 15-Oct-19 19:24pm.
|
|
|
|
|
I don't think it has anything to do with the code you posted.
CORS means Cross Origin Request. It's a security feature that secures your API's access and is part of the API server and your code in React. It prevents someone from spoofing your API's
https://medium.com/@dtkatz/3-ways-to-fix-the-cors-error-and-how-access-control-allow-origin-works-d97d55946d9
I have an Angular Project wrapped in .Net Core which hosts the API's in a controller. Technically my project is using Kestrel as a web server. Kestrel is a lightweight blazing fast web server which will serve .Net Projects. In my case, I had to fully program Kestrel as a web server in Programs.cs and Startup.cs, and then align my Angular project to the API's so that I don't get a CORS error. So like services.AddCors is what you add to startup.cs. I had to do a few other things in Angular to get things to align as well. And write some code to handle developing in VS using localhost:5000 to the actual production URL.
I don't know how you structured your React application and back-end API so I'm not able to answer the question as a code fix.
At some point in time, your going to have to figure out how to handle SSL certificates, setup Kestrel and listening ports in .Net Core if that is what your using. You will have to figure out how to host it in a production environment.
If it ain't broke don't fix it
Discover my world at jkirkerx.com
|
|
|
|
|
Google for how to solve CORS problems, this is a well-documented problem. Your API and client are running on separate sites (different ports on the same machine count as different sites) so the API needs to add a header to its return to say that the client host can have access. There are various ways of doing this.
|
|
|
|
|
Hi, thank you I could able to resolve this issue by implementing CORS on my Web API, here is the Code change I did in my Web API. Thank you for all the support you Geeks have given me - thanks for the help
public void ConfigureServices(IServiceCollection services)
{
string configValue = Configuration.GetValue<string>("CORSComplianceDomains");
string[] CORSComplianceDomains = configValue.Split("|,|");
services.AddCors(options =>
{
options.AddDefaultPolicy(
builder =>
{
builder.WithOrigins("http://localhost:3000");
});
options.AddPolicy("AnotherPolicy",
builder =>
{
builder.WithOrigins(CORSComplianceDomains)
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
And added the urls in the appsettings.json file so that any user can add the new urls without much sweating.
"CORSComplianceDomains": "http://localhost:3000|,|http://www.contoso.com"
Thank you very much - I put my answer here so that someone can get it - thanks for jumping in and helping please - I appreciated it - thank you so much.
|
|
|
|
|
Hi I have React application, which is calling api on download button click, on the button click the values are being posted properly and api is returning the zip file, but I want to let the zip file to be downloaded on the client side.
Here is my code for the API
[HttpPost]
public FileContentResult Post([FromForm] string communityName, [FromForm] string files)
{
var removedInvalidCharsFromFileName = removeInvalidCharsFromFileName(files);
var tFiles = removedInvalidCharsFromFileName.Split(',');
string rootPath = Configuration.GetValue<string>("ROOT_PATH");
string communityPath = rootPath + "\\" + communityName;
byte[] theZipFile = null;
using (MemoryStream zipStream = new MemoryStream())
{
using (ZipArchive zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
{
foreach (string attachment in tFiles)
{
var zipEntry = zip.CreateEntry(attachment);
using (FileStream fileStream = new FileStream(communityPath + "\\" + attachment, FileMode.Open))
using (Stream entryStream = zipEntry.Open())
{
fileStream.CopyTo(entryStream);
}
}
}
theZipFile = zipStream.ToArray();
}
return File(theZipFile, "application/zip", communityName + ".zip");
}
And my React components download method is as below:
handleDownload = (e) => {
e.preventDefault();
var formData = new FormData();
formData.append('communityname', this.state.selectedCommunity);
formData.append('files', JSON.stringify(this.state['checkedFiles']));
let url = clientConfiguration['filesApi.local'];
axios({
method: 'post',
url: url,
data: formData
});
window.open(url, '_blank');
}
This window.open(url, '_blank'); is not letting me download the zip files, aren't the path of the zip file and path of the api the same? What can I do to make sure the file downloads on the client side - any help - thanks in advance.
|
|
|
|
|
The problem is, you're making two separate requests - an AJAX POST request, which throws away the response; and a regular GET request with no parameters, which doesn't match your controller action.
Probably the simplest option would be to create a hidden <form> element, populate the values, and then submit that form.
handleDownload = (e) => {
e.preventDefault();
let form = document.createElement("form");
form.action = clientConfiguration['filesApi.local'];
form.method = "POST";
form.target = "_blank";
let input = document.createElement("input");
input.type = "hidden";
input.name = "communityname";
input.value = this.state.selectedCommunity;
form.appendChild(input);
this.state['checkedFiles'].forEach((f) =>
{
let input = document.createElement("input");
input.type = "hidden";
input.name = "files";
input.value = f;
});
document.body.appendChild(form);
form.submit();
}
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
Hi Richard, after researching online a little bit I could able to do the following, now the zip file is being downloaded but the problem it seems its not downloading correctly
handleDownload = (e) => {
e.preventDefault();
var formData = new FormData();
formData.append('communityname', this.state.selectedCommunity);
formData.append('files', JSON.stringify(this.state['checkedFiles']));
let url = clientConfiguration['filesApi.local'];
axios({
method: 'post',
url: url,
data: formData
})
.then(res => {
console.log(res.data);
var binaryData = [];
binaryData.push(res.data);
const src = window.URL.createObjectURL(new Blob(binaryData, { type: "application/zip" }));
var fileName = `${this.state['selectedCommunity']}.zip`;
saveAs(src, fileName);
});
}
The error that I am getting when uzipping the file is as below:
Can not open the file as zip archive, is not archiving, Warning Headers error. Any help please?
modified 14-Oct-19 18:16pm.
|
|
|
|
|
What does your console.log output show?
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
It shows the byte Array, it is being returned but what is the problem in creating right zip file I am not able to understand I changed a little bit the API method to return the bytes instead of file, still not much help, its the same error message unable to open the file as zip I don't know is the encoding etc different or the bytes limit is the problem, I am not sure. But there is another app doing the same thing in jQuery but its able to download the file - thank you Richard.
|
|
|
|
|
Actually I downloaded the file from the Server code, so the bytes are correct, then I modified the api code to read that newly created zip file as byte array then return, the array, I thought maybe accuracy is lost because of that, then saving that file as zip, still getting unable to open the file, and it says I have to add files to the zip folder before extracting it. I am not understanding what could be the reason. It is working with jQuery though but that's direct submit - any help please?
|
|
|
|
|
You're going to need to compare the bytes of the file that works with the bytes of the file that doesn't to see if there's any clue what the problem is.
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
Hi Richard, when I checked the length of the array on the Server it is: 113175
and on at the client it is: 150900, why is client side the array length is more IDK - any idea please?
|
|
|
|
|
No idea. What does the axios method look like?
I've just done something similar today using jQuery, and I had to explicitly set xhrFields: { responseType: "blob" } to get the binary data to download properly. Maybe there's a similar issue with your method?
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
I could able to resolve that issue by returning byte array but lead wants it only by using FileStreamResult - hah, any help, how to handle FileStreamResult, I am not sure if this Result post-fix good for Web Api because it looks View specific to me. If you have some idea please help me, an another day in this industry
|
|
|
|
|
I don't follow - a FileStreamResult just returns the bytes from the stream. What are you doing differently if you return it as a byte array?
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
I am not sure but it is giving me error code 500 Richard, any help please - thank you
|
|
|
|
|
I completed the code I am pasting it here as answer to my own question
|
|
|
|
|
axios is same as fetch, we have fetch I think almost all js libraries including Typescript and Angular. I am not sure the difference but responseType: 'arraybuffer' worked for me, if the return type is FileStreamResult which is of 'octet-stream' on the api side. Yeah its little bit complicated as many return types and as we don't much of it - thanks for jumping in Richard.
|
|
|
|
|