title: Uploading files with flow.js + ng-flow to WebAPI 2
body: <p>I'm trying to use flow.js (<a href="https://github.com/flowjs/flow.js" rel="nofollow noreferrer">https://github.com/flowjs/flow.js</a>) via its Angular wrapper (<a href="https://github.com/flowjs/ng-flow/tree/master/samples/basic" rel="nofollow noreferrer">https://github.com/flowjs/ng-flow/tree/master/samples/basic</a>) 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.</p>
  
  <p>The initial GET fired when I select a file is:</p>
  
  <pre><code>GET http://localhost:49330/api/upload?flowChunkNumber=1&amp;flowChunkSize=1048576&amp;flowCurrentChunkSize=4751&amp;flowTotalSize=4751&amp;flowIdentifier=4751-ElmahMySqlsql&amp;flowFilename=Elmah.MySql.sql&amp;flowRelativePath=Elmah.MySql.sql&amp;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
  </code></pre>
  
  <p>And the response is:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>Then, no more requests are issued.</p>
  
  <p>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 <a href="http://1drv.ms/1CSF5jq" rel="nofollow noreferrer">http://1drv.ms/1CSF5jq</a>: 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 <code>UploadController.cs</code>.</p>
  
  <p>The relevant code parts are:</p>
  
  <p>a) <strong>client side</strong>: a page similar to the quick-start example of the ng-flow page:</p>
  
  <pre><code>&lt;div class="row"&gt;
      &lt;div class="col-md-12"&gt;
          &lt;div flow-init="{target: '/api/upload'}"
               flow-files-submitted="$flow.upload()"
               flow-file-success="$file.msg = $message"&gt;
              &lt;input type="file" flow-btn /&gt;
              &lt;ol&gt;
                  &lt;li ng-repeat="file in $flow.files"&gt;{{file.name}}: {{file.msg}}&lt;/li&gt;
              &lt;/ol&gt;
          &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;
  </code></pre>
  
  <p>The corresponding code is essentially an empty TS skeleton with the module initialization:</p>
  
  <pre><code>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);
  }
  </code></pre>
  
  <p>b) <strong>server side</strong>: I added some bunding for the required scripts, and the following controller, modified from the sample code I found at <a href="https://stackoverflow.com/questions/24475890/how-to-upload-file-in-chunks-in-asp-net-using-ng-flow/25486943#25486943">How to upload file in chunks in ASP.NET using ng-Flow</a>. Note that in the GET <code>Upload</code> 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 <code>202 - Accepted</code> 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.</p>
  
  <pre><code>[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&lt;IHttpActionResult&gt; 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 &lt;= 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 &lt;= 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 &lt;= totalChunks; nChunkNumber++)
          {
              string sChunkFileName = GetChunkFileName(nChunkNumber, identifier);
              File.Delete(sChunkFileName);
          } //efor
      }
  }
  </code></pre>