1

I get a request with which I create a file and return it to the client.

After the file is sent I want it deleted.

Since I get many request, files are big and memory is scarce, I don't want to buffer it in memory to send it.

The only method I got to work without buffering the whole file in memory was:

Response.TransmitFile(filepath)

The problem with this is that it does it asynchronously, so if I delete it after that call the file download is interrupted.

I tried calling Flush, adding the delete on a finally block but neither of those worked. I thought of inheriting HttpResponse to try modify TransmitFile, but it's a sealed class. I tried to use HttpResponse.ClientDisconnectedToken but either I don't understand how to use it correctly or it isn't working in this case.

How can I achieve this? Is there a better method than calling HttpResponse's TransmitFile? Always taking into account that this is an API, files can't be broken into different requests and that it doesn't load the full file in memory.

I'm not sure if it could help somehow, but my controller is inheriting from AbpApiController.

Maximiliano
  • 148
  • 1
  • 9
  • Take a look here: https://stackoverflow.com/questions/2688282/response-transmitfile-and-delete-it-after-transmission – tgralex Aug 26 '19 at 19:54
  • I didn't even know it was possible to put data on the wire without first putting it in memory, learn something new everyday! Use the `await` keyword on async calls to halt execution until the method returns – DetectivePikachu Aug 26 '19 at 19:54
  • i would delete the file when the client requests it or put it on a timer. you should never assume it made it there sucessfully. – Daniel A. White Aug 26 '19 at 19:57
  • @tgralex Thank you, but as I wrote, I tried the finally block option. Did not work. It deletes the file before it's transferred. The other answer buffers the response in memory. – Maximiliano Aug 26 '19 at 19:58
  • @ĴošħWilliard Well, it could be worded better. If it's a 1GB file, RAM usage doesn't go up to any noticeable amount. TransmitFile is not an async method, it cannot be awaited. – Maximiliano Aug 26 '19 at 20:01
  • @DanielA.White I can't assume the client will make another call to let me know I can delete the file. A process that deletes all files older than X is the only thing that I think could work, but I would like to do it right after the file is downloaded. The API will have high usage, which can get to fill a disk pretty soon. But yes, it's the best thing I can think of – Maximiliano Aug 26 '19 at 20:04
  • @Maximiliano you could also skip writing to disk entirely – Daniel A. White Aug 26 '19 at 20:06
  • @DanielA.White Then I should store it in memory, which is the main thing I'm trying to avoid. – Maximiliano Aug 26 '19 at 20:08
  • @Maxiliano you said the problem with this is that it does it asynchronously. It isn't asynchronouly, you can check it by yourself here https://github.com/microsoft/referencesource/blob/master/System.Web/HttpResponse.cs I don't want to buffer it in memory to send it: TransmitFile put in memory the file, previous response. So I think an approach can be make your own TransmitFile with a limit of memory usage, by example a file has 10MB the response must be doing with 10 blocks of 1MB in memory. Do you want I will give a response with the implementation? – Sebastian Oscar Lopez Aug 26 '19 at 20:11
  • @SebastianOscarLopez I just checked again. It isn't an async method, but when debugging it it steps immediately over it. After the http code is returned the file gets transmitted. Memory usage doesn't go slightly up, so I guess it does what you say. I'd love to see your response with the implementation of how to send the file on chunks :) – Maximiliano Aug 26 '19 at 20:31

3 Answers3

3

You create the file in a temp folder, just create a job to remove all files based on date/time. Maybe give the user 4 hours to download the file.

Black Frog
  • 11,595
  • 1
  • 35
  • 66
  • This is the best thing I came up with, but I'd prefer to delete the file shortly after is no longer needed. Knowing the slow speed some few clients could have the process should keep all files for 20+ hours, which would be completely unnecessary for most of them. – Maximiliano Aug 26 '19 at 20:12
  • I also prefer this approach, especially because you do not waste memory to load the entire content of the file in memory. You can always schedule a simple job that runs once a day to remove and cleanup the temp folder. – Alkampfer Aug 07 '20 at 13:34
0

What you can do is to create a byte array from your file and then delete the file. So, once it is created, you store it in memory, then you delete the file and return the byte array. Check this answer to see an example.

I hope this helps.

0

An implementation of TransmitFileWithLimit, It can be improve in many ways, but it works

Extension for HttpResponse

public static class HttpResponseExtensions
{
    public static void TransmitFileWithLimit(this HttpResponse response, string path, int limit)
    {
        var buffer = new byte[limit];
        var offset = 0;
        response.ClearContent();
        response.Clear();
        response.ContentType = "text/plain";

        using (var fileStream = File.OpenRead(path))
        {
            var lengthStream = fileStream.Length;
            while (offset < lengthStream)
            {
                var lengthBytes = fileStream.Read(buffer, 0, limit);
                var chars = System.Text.Encoding.ASCII.GetString(buffer, 0, lengthBytes).ToCharArray();
                response.Write(chars, 0, lengthBytes);
                offset += lengthBytes;
            }
        }
        response.Flush();
        response.End();
    }
}

Inside Controller

    public void Get()
    {
        var path = @"C:\temporal\bigfile.mp4";
        var response = HttpContext.Current.Response;
        response.ClearContent();
        response.Clear();
        response.AddHeader("Content-Disposition", "inline; filename=" + HttpUtility.UrlPathEncode(path));
        response.ContentType = "text/plain";
        response.AddHeader("Content-Length", new FileInfo(path).Length.ToString());
        response.Flush();
        HttpContext.Current.ApplicationInstance.CompleteRequest();

        TransmitFileWithLimit(path, 10000);
        File.Delete(path);
    }