2

ASP .NET Core

MVC Controller - download file from server storage using FileStream and returning FileStreamResult

public IActionResult Download(string path, string fileName)
{
    var fileStream = System.IO.File.OpenRead(path);

    return File(fileStream, "application/force-download", fileName);
}

Everything works fine, but once the user cancels downloading before the download is complete, other actions in the controller working with this file (Delete file, rename file) do not work because: The process cannot access the file, because it is being used by another process

FileStream automatically dispose when the file download is complete, but for some reason it does not terminate when the user terminates the download manually.

I have to restart the web application => the program that uses the file is IISExpress

Does anyone please know how to dispose stream if the user manually ends the download?

EDIT:

FileStream stream = null;
try
{
    using (stream = System.IO.File.OpenRead(path))
    {
        return File(stream, "application/force-download", fileName);
    }
}

Code that I tried to end the Stream after returning FileStreamResult, I am aware that it can not work, because after return File (stream, contentType, fileName) it immediately jumps to the block finally and the stream closes, so the download does not start because the stream is closed

Foro
  • 35
  • 10
  • 2
    You should enclose the filestream in `using` block – Anand Sowmithiran Apr 26 '22 at 09:15
  • @Anand Sowmithiran I tried that, but then the error is that stream is closed when it hits return File – Foro Apr 26 '22 at 09:16
  • 1
    The "challenge" here is that you are returning a stream. There doesn't seem to be a way to cancel a request. Is there a specific reason for that? Can't you do something like `return File(System.IO.File.ReadAllBytes(path), "...`? or even better, take a `CancellationToken` action parameter and pass it to `var bytes = await File.ReadAllBytesAsync(path, token);`. – JHBonarius Apr 26 '22 at 09:33
  • @JHBonarius with your suggestion, there is a problem with File.ReadAllBytes(path), that you cannot download File with size > 2GB, that is the reason i am using Stream – Foro Apr 26 '22 at 09:37
  • @AnandSowmithiran I am returning FileStreamResult, return File(stream, contentType, fileName) is typeof FileStreamResult – Foro Apr 26 '22 at 09:40
  • 3
    Dear commenters above (Anand, Ralf). Please [read the docs](https://learn.microsoft.com/en-us/dotnet/api/system.web.mvc.controller.file) The Constructor of `Controller.File` which takes a `Stream` will generate a `FileStreamResult`. Please don't suggest stuff if you don't know what you are talking about. – JHBonarius Apr 26 '22 at 09:42
  • @Foro, regarding your comment "*I tried that, but then the error is that stream is closed when it hits return File*". How did you try *that*? – anastaciu Apr 26 '22 at 09:52
  • @Foro when you use `using` you don't need to call `Dispose`, it is done for you. Take a look a this thread, you may find your solution there: https://stackoverflow.com/q/42238826/6865932, also note the duplicates. – anastaciu Apr 26 '22 at 10:18
  • @anastaciu With that **dispose** call you are right, there is no need for that, but I guess you do not understand what is the problem here. There is no reason to use **using**, because inside this **using** block, there is immediately **return** so after that stream is immediately disposed and there is an error, that stream is closed. – Foro Apr 26 '22 at 10:21
  • 1
    @Foro that goes to the heart of the problem, if you dispose it too soon it won't work, if you don't dispose it it won't work either, maybe store it in a different object inside the using, maybe some exception is thrown when cancelled that you can work with, *i.e* `OperationCancelledException`, you'll have to try some things, unfortunately, I don't have way to test your code so it's hard to give a concrete solution, for what I can see, I think you can do it, some reseach won't hurt either, I'm sure someone somewhere has faced a similar issue, I'll give 10 pts for the good question though... – anastaciu Apr 26 '22 at 10:55

1 Answers1

3

It seems the source of the FileStreamResult class shows it has no support for cancellation. You will need to implement your own, if required. E.g. (not-tested, just imagined)

using System.IO;

namespace System.Web.Mvc
{
    public class CancellableFileStreamResult : FileResult
    {
        // default buffer size as defined in BufferedStream type
        private const int BufferSize = 0x1000;

        private readonly CancellationToken _cancellationToken;

        public CancellableFileStreamResult(Stream fileStream, string contentType,
            CancellationToken cancellationToken)
            : base(contentType)
        {
            if (fileStream == null)
            {
                throw new ArgumentNullException("fileStream");
            }

            FileStream = fileStream;
            _cancellationToken = cancellationToken;
        }

        public Stream FileStream { get; private set; }

        protected override void WriteFile(HttpResponseBase response)
        {
            // grab chunks of data and write to the output stream
            Stream outputStream = response.OutputStream;
            using (FileStream)
            {
                byte[] buffer = new byte[BufferSize];

                while (!_cancellationToken.IsCancellationRequested)
                {
                    int bytesRead = FileStream.Read(buffer, 0, BufferSize);
                    if (bytesRead == 0)
                    {
                        // no more data
                        break;
                    }

                    outputStream.Write(buffer, 0, bytesRead);
                }
            }
        }
    }
}

You can then use it like

public IActionResult Download(string path, string fileName, CancellationToken cancellationToken)
{
    var fileStream = System.IO.File.OpenRead(path);
    var result = new CancellableFileStreamResult(
        fileStream, "application/force-download", cancellationToken);
    result.FileDownloadName = fileName;
    return result;
}

Again, I'm this is not tested, just imagined. Maybe this doesn't work, as the action is already finished, thus cannot be cancelled anymore.

EDIT: The above answer "Imagined" for ASP.net framework. ASP.net core has a quite different underlying framework: In .net core, the action is processed by and executor, as shown in the source. That will eventually call WriteFileAsync in the FileResultHelper. There you can see that StreamCopyOperation is called with the cancellationToken context.RequestAborted. I.e. cancellation is in place in .net Core.

The big question is: why isn't the request aborted in your case.

JHBonarius
  • 10,824
  • 3
  • 22
  • 41
  • I would say that's a good answer and thank you for it, but the problem is that my question is related to **.net core**, so the `FileResult` class does not contain `WriteFile(HttpResponseBase response)` – Foro Apr 27 '22 at 08:41
  • @Foro: I did some research in the sources and found cancellation is in place in .net core. See my update/edit. It's weird that it's not triggered in your case. It should work. – JHBonarius Apr 27 '22 at 14:31
  • I understand, in that case it is possible that the problem has its origin elsewhere, I accept this answer as correct answer to my question, even if it did not solve my problem. It seems that your solution should really ensure that the stream ends. – Foro Apr 28 '22 at 04:58