8

I have to download a file from aws S3 async. I have a anchor tag, on clicking it a method will be hit in a controller for download. The file should be start downloading at the bottom of the browser, like other file download.

In View

<a href="/controller/action?parameter">Click here</a>

In Controller

public void action()
{
     try
     {
           AmazonS3Client client = new AmazonS3Client(accessKeyID, secretAccessKey);
           GetObjectRequest req = new GetObjectRequest();
           req.Key = originalName;
           req.BucketName = ConfigurationManager.AppSettings["bucketName"].ToString() + DownloadPath;
           FileInfo fi = new FileInfo(originalName);
           string ext = fi.Extension.ToLower();
           string mimeType = ReturnmimeType(ext);
           var res = client.GetObject(req);
           Stream responseStream = res.ResponseStream;
           Stream response = responseStream;
           return File(response, mimeType, downLoadName);
     }
     catch (Exception)
     {
           failure = "File download failed. Please try after some time.";   
     }              
}

The above function makes the browser to load until the file is fully downloaded. Then only the file is visible at the bottom. I cant see the how mb is downloading.
Thanks in advance.

Heinrich Cloete
  • 702
  • 5
  • 12
anand
  • 1,559
  • 5
  • 21
  • 45
  • 3
    Try to add `Response.BufferOutput = false;` right before the `return`. This will disable server-side buffering of the file. – Sinya Jan 25 '17 at 08:27

2 Answers2

12

You must send ContentLength to client in order to display a progress. Browser has no information about how much data it will receive.

If you look at source of FileStreamResult class, used by File method, it does not inform client about "Content-Length". https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/FileStreamResult.cs

Replace this,

return File(response, mimeType, downLoadName);

with

return new FileStreamResultEx(response, res.ContentLength, mimeType, downloadName);


public class FileStreamResultEx : ActionResult{

     public FileStreamResultEx(
        Stream stream, 
        long contentLength,         
        string mimeType,
        string fileName){
        this.stream = stream;
        this.mimeType = mimeType;
        this.fileName = fileName;
        this.contentLength = contentLength;
     }


     public override void ExecuteResult(
         ControllerContext context)
     {
         var response = context.HttpContext.Response; 
         response.BufferOutput = false;
         response.Headers.Add("Content-Type", mimeType);
         response.Headers.Add("Content-Length", contentLength.ToString());
         response.Headers.Add("Content-Disposition","attachment; filename=" + fileName);

         using(stream) { 
             stream.CopyTo(response.OutputStream);
         }
     }

}

Alternative

Generally this is a bad practice to download and deliver S3 file from your server. You will be charged twice bandwidth on your hosting account. Instead, you can use signed URLs to deliver non public S3 objects, with few seconds of time to live. You could simply use Pre-Signed-URL

 public ActionResult Action(){
     try{
         using(AmazonS3Client client = 
              new AmazonS3Client(accessKeyID, secretAccessKey)){
            var bucketName = 
                 ConfigurationManager.AppSettings["bucketName"]
                .ToString() + DownloadPath;
            GetPreSignedUrlRequest request1 = 
               new GetPreSignedUrlRequest(){
                  BucketName = bucketName,
                  Key = originalName,
                  Expires = DateTime.Now.AddMinutes(5)
               };

            string url = client.GetPreSignedURL(request1);
            return Redirect(url);
         }
     }
     catch (Exception)
     {
         failure = "File download failed. Please try after some time.";   
     }              
 }

As long as object do not have public read policy, objects are not accessible to users without signing.

Also, you must use using around AmazonS3Client in order to quickly dispose networks resources, or just use one static instance of AmazonS3Client that will reduce unnecessary allocation and deallocation.

Akash Kava
  • 39,066
  • 20
  • 121
  • 167
  • Hey - nice answer, +1 from me. However, regarding the 'PreSignedURL' - I gotta admit I really don't like the format of this redirect URL, which reveals too much (e.g. it reveals that the file comes from Amazon S3). – Bartosz Jul 02 '19 at 18:13
  • Also - shouldn't you wrap the `stream` in a using block? Will it be disposed automatically? What will do it? I know that FileStreamResult handles the disposing, but here you're inhertiing a different class... – Bartosz Jul 02 '19 at 18:48
  • 1
    @Bartosz done, stream is now wrapped, problem with this approach is, it is not very efficient, as it doesn't handle range processing, e-tag match etc, so everytime browser requests file, you end up sending entire file even if it wasn't changed. Pre signed URL takes care of it, it supports HTTP streaming (used by iOS for video fetch, which uses range processing), e-tag support, if-modified header etc. Another longer approach is download file in local hard disk cache and let IIS send the file, by sending `File( filePath, contentType )` method, IIS supports http streaming. – Akash Kava Jul 03 '19 at 03:19
0

As i understand, you want to make something like "reverse proxy" from your server to S3. Very userful article how to do that with Nginx you can find here: https://stackoverflow.com/a/44749584

Anatoly
  • 15,298
  • 5
  • 53
  • 77
Victor Leontyev
  • 8,488
  • 2
  • 16
  • 36