Uploading files with flow.js + ng-flow to WebAPI 2

Naftis picture Naftis · Apr 17, 2015 · Viewed 8.3k times · Source

I'm trying to use flow.js (https://github.com/flowjs/flow.js) via its Angular wrapper (https://github.com/flowjs/ng-flow/tree/master/samples/basic) to upload files to an ASP.NET WebAPI 2 server. Anyway when I select a file to upload my WebAPI just gets the first chunk GET request and then nothing happens: no POST is done, and it seems that flow.js did not start the upload.

The initial GET fired when I select a file is:

GET http://localhost:49330/api/upload?flowChunkNumber=1&flowChunkSize=1048576&flowCurrentChunkSize=4751&flowTotalSize=4751&flowIdentifier=4751-ElmahMySqlsql&flowFilename=Elmah.MySql.sql&flowRelativePath=Elmah.MySql.sql&flowTotalChunks=1 HTTP/1.1
Host: localhost:49330
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36
Accept: */*
Referer: http://localhost:49330/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,it;q=0.6

And the response is:

HTTP/1.1 202 Accepted
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?QzpcUHJvamVjdHNcNDViXFRlc3RcVXBUZXN0XFVwVGVzdFxhcGlcdXBsb2Fk?=
X-Powered-By: ASP.NET
Date: Fri, 17 Apr 2015 08:02:56 GMT
Content-Length: 0

Then, no more requests are issued.

As it seems there is no up-to-date WebAPI example, but only scattered posts, I created for newbies like me a dummy repro solution you can download from http://1drv.ms/1CSF5jq: it's an ASP.NET WebAPI 2 solution where I placed the upload code in the home view, after adding the corresponding API controller. Just hit F5 and try uploading a file. You can find the API controller in UploadController.cs.

The relevant code parts are:

a) client side: a page similar to the quick-start example of the ng-flow page:

<div class="row">
    <div class="col-md-12">
        <div flow-init="{target: '/api/upload'}"
             flow-files-submitted="$flow.upload()"
             flow-file-success="$file.msg = $message">
            <input type="file" flow-btn />
            <ol>
                <li ng-repeat="file in $flow.files">{{file.name}}: {{file.msg}}</li>
            </ol>
        </div>
    </div>
</div>

The corresponding code is essentially an empty TS skeleton with the module initialization:

module Up {
    export interface IMainScope {
    }

    export class MainController {
        public static $inject = ["$scope"];
        constructor(private $scope: IMainScope) {
        }
    }

    var app = angular.module("app", ["flow"]);
    app.controller("mainController", MainController);
}

b) server side: I added some bunding for the required scripts, and the following controller, modified from the sample code I found at How to upload file in chunks in ASP.NET using ng-Flow. Note that in the GET Upload method I changed the signature using a binding model (otherwise we would get a 404 as the route was not matched), and when the chunk is not found I return a 202 - Accepted code rather than 404, as flow.js documentation says that 200 corresponds to "The chunk was accepted and correct. No need to re-upload", while a 404 cancels the entire upload, and any other code (like 202 here) tells the uploader to retry.

[RoutePrefix("api")]
public class UploadController : ApiController
{
    private readonly string _sRoot;

    public UploadController()
    {
        _sRoot = HostingEnvironment.MapPath("~/App_Data/Uploads");
    }

    [Route("upload"), AcceptVerbs("GET")]
    public IHttpActionResult Upload([FromUri] UploadBindingModel model)
    {
        if (IsChunkHere(model.FlowChunkNumber, model.FlowIdentifier)) return Ok();
        return ResponseMessage(new HttpResponseMessage(HttpStatusCode.Accepted));
    }

    [Route("upload"), AcceptVerbs("POST")]
    public async Task<IHttpActionResult> Upload()
    {
        // ensure that the request contains multipart/form-data
        if (!Request.Content.IsMimeMultipartContent())
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);

        if (!Directory.Exists(_sRoot)) Directory.CreateDirectory(_sRoot);
        MultipartFormDataStreamProvider provider = 
            new MultipartFormDataStreamProvider(_sRoot);
        try
        {
            await Request.Content.ReadAsMultipartAsync(provider);
            int nChunkNumber = Convert.ToInt32(provider.FormData["flowChunkNumber"]);
            int nTotalChunks = Convert.ToInt32(provider.FormData["flowTotalChunks"]);
            string sIdentifier = provider.FormData["flowIdentifier"];
            string sFileName = provider.FormData["flowFilename"];

            // rename the generated file
            MultipartFileData chunk = provider.FileData[0]; // Only one file in multipart message
            RenameChunk(chunk, nChunkNumber, sIdentifier);

            // assemble chunks into single file if they're all here
            TryAssembleFile(sIdentifier, nTotalChunks, sFileName);

            return Ok();
        }
        catch (Exception ex)
        {
            return InternalServerError(ex);
        }
    }

    private string GetChunkFileName(int chunkNumber, string identifier)
    {
        return Path.Combine(_sRoot,
            String.Format(CultureInfo.InvariantCulture, "{0}_{1}",
                identifier, chunkNumber));
    }

    private void RenameChunk(MultipartFileData chunk, int chunkNumber, string identifier)
    {
        string sGeneratedFileName = chunk.LocalFileName;
        string sChunkFileName = GetChunkFileName(chunkNumber, identifier);
        if (File.Exists(sChunkFileName)) File.Delete(sChunkFileName);
        File.Move(sGeneratedFileName, sChunkFileName);
    }

    private string GetFileName(string identifier)
    {
        return Path.Combine(_sRoot, identifier);
    }

    private bool IsChunkHere(int chunkNumber, string identifier)
    {
        string sFileName = GetChunkFileName(chunkNumber, identifier);
        return File.Exists(sFileName);
    }

    private bool AreAllChunksHere(string identifier, int totalChunks)
    {
        for (int nChunkNumber = 1; nChunkNumber <= totalChunks; nChunkNumber++)
            if (!IsChunkHere(nChunkNumber, identifier)) return false;
        return true;
    }

    private void TryAssembleFile(string identifier, int totalChunks, string filename)
    {
        if (!AreAllChunksHere(identifier, totalChunks)) return;

        // create a single file
        string sConsolidatedFileName = GetFileName(identifier);
        using (Stream destStream = File.Create(sConsolidatedFileName, 15000))
        {
            for (int nChunkNumber = 1; nChunkNumber <= totalChunks; nChunkNumber++)
            {
                string sChunkFileName = GetChunkFileName(nChunkNumber, identifier);
                using (Stream sourceStream = File.OpenRead(sChunkFileName))
                {
                    sourceStream.CopyTo(destStream);
                }
            } //efor
            destStream.Close();
        }

        // rename consolidated with original name of upload
        // strip to filename if directory is specified (avoid cross-directory attack)
        filename = Path.GetFileName(filename);
        Debug.Assert(filename != null);

        string sRealFileName = Path.Combine(_sRoot, filename);
        if (File.Exists(filename)) File.Delete(sRealFileName);
        File.Move(sConsolidatedFileName, sRealFileName);

        // delete chunk files
        for (int nChunkNumber = 1; nChunkNumber <= totalChunks; nChunkNumber++)
        {
            string sChunkFileName = GetChunkFileName(nChunkNumber, identifier);
            File.Delete(sChunkFileName);
        } //efor
    }
}

Answer

Aidas picture Aidas · Apr 19, 2015

200 status is not the only one considered a success. 201 and 202 are too. Read about the option of successStatuses: https://github.com/flowjs/flow.js/blob/master/dist/flow.js#L91 So only change you need is to return a 204 status, which means No Content.