PushStreamContent and ionic.zip - c#

My webapi method for zipping on the fly use this code
var result = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new PushStreamContent((stream, content, arg3) =>
{
using (var zipEntry = new Ionic.Zip.ZipFile())
{
using (var ms = new MemoryStream())
{
_xmlRepository.GetInitialDataInXml(employee, ms);
zipEntry.AddEntry("content.xml", ms);
zipEntry.Save(stream); //process sleep on this line
}
}
})
};
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = "FromPC.zip"
};
result.Content.Headers.ContentType =
new MediaTypeHeaderValue("application/octet-stream");
return result;
I want to
1) take data from _xmlRepository.GetInitialDataInXml
2) zip data on the fly via Ionic.Zip
3) return zipped stream as output of my WebApi action
But on this line zipEntry.Save(stream); execution process stops and don't go to next line. And method don't return anything
So why it doesnt' return me file?

When using PushStreamContent, you would need to close the stream to signal that you are done writing to the stream.
Remarks section in the documentation:
http://msdn.microsoft.com/en-us/library/jj127066(v=vs.118).aspx

The accepted answer is not correct. It is not necessary to close the stream if you want to start streaming. The streaming starts automatically (download dialog in browser) when the delegated function ends. In case of big files OutOfMemoryException is thrown, but it is handled and the streaming begins -> HttResponseStream is flushed towards the client.
var result = new HttpResponseMessage(HttpStatusCode.OK);
result.Content = new PushStreamContent(async (outputStream, httpContext, transportContext) =>
{
using (var zipStream = new ZipOutputStream(outputStream))
{
var employeeStream = _xmlRepository.GetEmployeeStream(); // PseudoCode
zipStream.PutNextEntry("content.xml");
await employeeStream.CopyToAsync(zipStream);
outputStream.Flush();
}
});
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = "FromPC.zip" };
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
return result;

Related

When creating a Zip file in memory, why do I have to parse the MemoryStream for the Zip to be created correctly?

I've looked at other questions on here but nobody answers my question as to why I can't use the already existing MemoryStream that exists to create the zip.
Here is a working API method to get a zipped folder containing files. The files already exist on the server and the method simply looks up there location and, all in memory, retrieves all the files and sends the user a HttpResponMessage object with the Zipped folder containing all the files. There is one line of code below that doesn't seem to make sense however. I have to parse the MemoryStream to a byte array then back to a MemoryStream in order for the Zip to be created and sent back correctly. This seems wrong, but I don't know how to correct it. (See below "//Works:" and "//Does Not work:")
public HttpResponseMessage GetEvalByCompany([FromUri]Guid id, [FromUri] int year)
{
try
{
string companyName = _portalDB.Companies.FirstOrDefault(c => c.Id == id).FirmName;
//Get all evaluation file paths for a given company for a given year
var files = _evaluationDB.Evaluations.Where(j => j.CompanyId == id).Select(j => #"C:\Path\To\File" + j.RelativePath.Replace("~", #"").Replace(#"/", #"\")).Distinct().ToList<string>();
using (var outStream = new MemoryStream())
{
using (var archive = new ZipArchive(outStream, ZipArchiveMode.Create, true))
{
foreach (var record in files)
{
var fileInArchive = archive.CreateEntryFromFile(record, Path.GetFileName(record), CompressionLevel.Optimal);
}
}
// Create a response
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
//Works:
response.Content = new StreamContent(new MemoryStream(outStream.ToArray())); //Why does this need to happen?
//Does Not work:
//response.Content = new StreamContent(outStream); //Why doesn't this work?
// Add appropriate headers to make browser recognize response as a download
response.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
response.Content.Headers.ContentDisposition.FileName = "Evaluations for Company - " + companyName + ".zip";
response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/zip");
return response;
}
}
catch (Exception ex)
{
return ErrorHandler.HandleException(ex, Request);
}
}
Based on the answers I received I was able to fix the code so it doesn't have to parse the stream unnecessarily. Here's the updated code for future devs looking for the solution simply. Removing the using doesn't seem to cause any GC issues either. I found a link to explain that a bit better as well. (MemoryStream.Close() or MemoryStream.Dispose())
public HttpResponseMessage GetEvalByCompany([FromUri]Guid id, [FromUri] int year)
{
try
{
string companyName = _portalDB.Companies.FirstOrDefault(c => c.Id == id).FirmName;
//Get all evaluation file paths for a given company for a given year
var files = _evaluationDB.Evaluations.Where(j => j.CompanyId == id).Select(j => #"C:\Path\To\File" + j.RelativePath.Replace("~", #"").Replace(#"/", #"\")).Distinct().ToList<string>();
var outStream = new MemoryStream();
using (var archive = new ZipArchive(outStream, ZipArchiveMode.Create, true))
{
foreach (var record in files)
{
var fileInArchive = archive.CreateEntryFromFile(record, Path.GetFileName(record), CompressionLevel.Optimal);
}
}
// Create a response
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
//Updated code
outStream.Seek(0, SeekOrigin.Begin);
response.Content = new StreamContent(outStream); //works now because outStream isn't in a using block
// Add appropriate headers to make browser recognize response as a download
response.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
response.Content.Headers.ContentDisposition.FileName = "Evaluations for Company - " + companyName + ".zip";
response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/zip");
return response;
}
catch (Exception ex)
{
return ErrorHandler.HandleException(ex, Request);
}
}
Remove
using (var outStream = new MemoryStream()),
leave instead only:
var outStream = new MemoryStream();
Then later :
outStream.Seek(0, SeekOrigin.Begin)
response.Content = new StreamContent(outStream);
The problem here is that your original MemoryStream is getting disposed before it is used. StreamContent is keeping a reference to an object that is disposed. You need to remove the using block for outStream and somehow arrange to dispose of it later.

AWS S3 file has no contents when downloading

I am trying to pass a file from S3 directly into the users' download stream like this:
public HttpResponseMessage Download()
{
var result = new HttpResponseMessage(HttpStatusCode.OK);
var stream = new MemoryStream();
using (var client = new AmazonS3Client(RegionEndpoint.USWest2))
{
var request = new GetObjectRequest
{
BucketName = "bucket",
Key = "filename"
};
using (var response = client.GetObject(request))
{
response.ResponseStream.CopyTo(stream);
}
}
result.Content = new StreamContent(stream);
result.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType);
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = "file.pdf"
};
return result;
}
But the file downloads as 0 bytes. I assumed that the file is completely loaded in memory on line ResponseStream.CopyTo.

StreamContent not loaded to the end

I have (several) WebAPI action(s), which load QuickFix logs from database (via EF) and use this private method to return them as CSV:
private HttpResponseMessage BuildCsvResponse<T>(T[] entries, Func<T, string> row, string fileName)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
var i = entries.Length;
foreach (var entry in entries)
{
i--;
writer.WriteLine(row(entry)); // simply call to overridden ToString() method
}
stream.Seek(0, SeekOrigin.Begin);
stream.Flush();
response.Content = new StreamContent(stream);
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = fileName,
};
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/csv");
return response;
}
The problem is that content is never loaded to the end and cut on random symbol not so far from end. Why could it happen?
May be it is important - all log strings contain delimiter 0x01
You need to Flush your streamwriter's internal buffers before you touch the underlying stream.
Best is to tell your StreamWriter to keep the stream open by using another contructor. You can then safely dispose your streamwriter causing it to flush its buffer while your memorystream instance stays open and doesn't get disposed.
Notice that you need to pick an encoding that matches your HTTP content response. I choose UTF8 here, adapt accordingly.
var stream = new MemoryStream();
// notice the true as last parameter, false is the default.
using(var writer = new StreamWriter(stream, Encoding.UTF8, 8192, true))
{
var i = entries.Length;
foreach (var entry in entries)
{
i--;
writer.WriteLine(row(entry)); // simply call to overridden ToString() method
}
}
// your streamwriter has now flushed its buffer and left the stream open
stream.Seek(0, SeekOrigin.Begin);
// calling Flush on the stream was never needed so I removed that.
response.Content = new StreamContent(stream);

ERR_SPDY_PROTOCOL_ERROR when returning file from ASP.NET action

I have some old Web API action method which returns CSV file. It worked for long time, but recently stopped. Now it causes ERR_SPDY_PROTOCOL_ERROR.
ERR_SPDY_PROTOCOL_ERROR in Chrome is often associated with Avast security as described here. In my case however it's not caused by Avast, and other web browsers throw exceptions too.
My action method looks as follows:
[HttpGet]
[Route("csv")]
public HttpResponseMessage SomeMethod([FromUri]SomeSearchCriteria sc)
{
using (MemoryStream stream = new MemoryStream())
{
StreamWriter writer = new StreamWriter(stream, Encoding.UTF8);
string content = someLogic.SomeSearchmethod(sc);
writer.Write(content);
writer.Flush();
stream.Position = 0;
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
result.Content = new StreamContent(stream);
result.Content.Headers.ContentType = new MediaTypeHeaderValue("text/csv");
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = "Export.csv" };
return result;
}
}
The method is called by angular front end by simple change of window.location on button click.
Whole action method is executed properly, with no exceptions. Error is shown only by web browser.
Flushing sockets in Chrome as described here does not solve the issue.
I have tried this method in API controller and call through chrome browser, it throws net::ERR_CONNECTION_RESET
There is some issue in response filled with StreamContent, use ByteArrayContent in result content, it works perfectly.
[HttpGet]
[Route("csv")]
public HttpResponseMessage SomeMethod([FromUri]SomeSearchCriteria sc)
{
using (MemoryStream stream = new MemoryStream())
{
StreamWriter writer = new StreamWriter(stream, Encoding.UTF8);
string content = "test";
writer.Write(content);
writer.Flush();
stream.Position = 0;
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
//result.Content = new StreamContent(stream);
result.Content = new ByteArrayContent(stream.ToArray());
result.Content.Headers.ContentType = new MediaTypeHeaderValue("text/csv");
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = "Export.csv" };
return result;
}
}

Issue returning images over web api

I am having a weird issue returning an image via web api, I do this in several places in my application without fail but this one is causing me issues, any help would be greatly appreciates.
This works
Bitmap bitmap = getImage();
MemoryStream bitmapStream = new MemoryStream();
bitmap.Save("C:\\test.png", System.Drawing.Imaging.ImageFormat.Png);
if (bitmap != null)
{
if (Request.Headers.IfModifiedSince.HasValue)
{
// The file has not been modified since the browser cached it.
return new HttpResponseMessage(HttpStatusCode.NotModified);
}
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StreamContent(new FileStream("C:\\test.png", FileMode.Open));
response.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
return response;
}
else
{
return new HttpResponseMessage(HttpStatusCode.NotFound);
}
while this does not
Bitmap bitmap = getImage();
MemoryStream bitmapStream = new MemoryStream();
bitmap.Save(bitmapStream, System.Drawing.Imaging.ImageFormat.Png);
if (bitmap != null)
{
if (Request.Headers.IfModifiedSince.HasValue)
{
// The file has not been modified since the browser cached it.
return new HttpResponseMessage(HttpStatusCode.NotModified);
}
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StreamContent(bitmapStream);
response.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
return response;
}
else
{
return new HttpResponseMessage(HttpStatusCode.NotFound);
}
I really need to do this in memory and not save a temp file to drive, but for some reason when I do it in memory the result is an empty file 0 bytes, I do try to set the content length manually but then the file doesnt download at all.
You would need to reset the position of the stream after the save happens.
bitmapStream.Position = 0;

Categories

Resources