title: Retrieving file data in chunks using Web API for display in browser (WIP)
body: <p>I have this working but I want to share this out to see if I missed anything obvious and to solve a mystery as to why my file chunk size has to be a multiple of 2049.  The main requirements are:</p>
  
  <ul>
  <li>Files uploaded from website must be stored in SQL server, not as files</li>
  <li>Website must be able to download and display file data as a file (opened in a separate window.</li>
  <li>Website is angularjs/javascript SPA, no server side code, no MVC</li>
  <li>API is Web API 2 (again not MVC)</li>
  </ul>
  
  <p>I'm just going to focus on the download part here.  Basically what I'm doing is:</p>
  
  <ol>
  <li>Read a chunk of data from SQL server varbinary field</li>
  <li>Web API 2 api returns file name, mime type and byte data as a base64 string.  NOTE - tried returning byte array but Web API just serializes it into base64 string anyway.</li>
  <li>concatenate the chunks, convert the chunks to a blob and display</li>
  </ol>
  
  <p>VB library function that returns a dataset with the chunk (I have to use this library which handles the database connection but doesn't support parameter queries)</p>
  
  <pre><code>Public Function GetWebApplicationAttachment(ByVal intId As Integer, ByVal intChunkNumber As Integer, ByVal intChunkSize As Integer) As DataSet
  
      ' the starting number is NOT 0 based
      Dim intStart As Integer = 1
      If intChunkNumber &gt; 1 Then intStart = ((intChunkNumber - 1) * intChunkSize) + 1
      Dim strQuery As String = ""
      strQuery += "SELECT FileName, "
      strQuery += "SUBSTRING(ByteData," &amp; intStart.ToString &amp; "," &amp; intChunkSize.ToString &amp; ") AS ByteData "
      strQuery += "FROM FileAttachments WHERE Id = " + intId.ToString + " "
      Try
          Return Query(strQuery)
      Catch ex As Exception
          ...
      End Try
  End Function
  </code></pre>
  
  <p>Web API business rules bit that creates the file object from the dataset</p>
  
  <pre><code>...
      result.FileName = ds.Tables[0].Rows[0]["FileName"].ToString();
      // NOTE: Web API converts a byte array to base 64 string so the result is the same either way
      // the result of this is that the returned data will be about 30% bigger than the chunk size requested
      result.StringData = Convert.ToBase64String((byte[])ds.Tables[0].Rows[0]["ByteData"]);
      //result.ByteData = (byte[])ds.Tables[0].Rows[0]["ByteData"];
      ... some code to get the mime type
      result.MIMEType = ...
  </code></pre>
  
  <p>Web API controller (simplified - all security and error handling removed)</p>
  
  <pre><code>public IHttpActionResult GetFileAttachment([FromUri] int id, int chunkSize, int chunkNumber) {
      brs = new Files(...);
      fileResult file = brs.GetFileAttachment(appID, chunkNumber, chunkSize);
      return Ok(file);
  }
  </code></pre>
  
  <p>angularjs Service that gets the chunks recurively and puts them together</p>
  
  <pre><code>    function getFileAttachment2(id, chunkSize, chunkNumber, def, fileData, mimeType) {
          var deferred = def || $q.defer();
          $http.get(webServicesPath + "api/files/get-file-attachment?id=" + id + "&amp;chunkSize=" + chunkSize + "&amp;chunkNumber=" + chunkNumber).then(
              function (response) {
                  // when completed string data will be empty
                  if (response.data.StringData === "") {
                      response.data.MIMEType = mimeType;
                      response.data.StringData = fileData;
                      deferred.resolve(response.data);
                  } else {
                      if (chunkNumber === 1) {
                          // only the first chunk computes the mime type
                          mimeType = response.data.MIMEType;
                      }
                      fileData += response.data.StringData;
                      chunkNumber += 1;
                      getFileAttachment2(appID, detailID, orgID, GUID, type, chunkSize, chunkNumber, deferred, fileData, mimeType);
                  }
              },
              function (response) {
                  ... error stuff
              }
          );
          return deferred.promise;
      }
  </code></pre>
  
  <p>angular controller method that makes the calls.</p>
  
  <pre><code>    function viewFile(id) {
          sharedInfo.getWebPortalSetting("FileChunkSize").then(function (result) {
              // chunk size must be a multiple of 2049 ???
              var chunkSize = 0;
              if (result !== null) chunkSize = parseInt(result);
              fileHelper.getFileAttachment2(id, chunkSize, 1, null, "", "").then(function (result) {
                  if (result.error === null) {
                      if (!fileHelper.viewAsFile(result.StringData, result.FileName, result.MIMEType)) {
                          ... error
                      }
                      result = {};
                  } else {
                      ... error;
                  }
              });
          });
      }
  </code></pre>
  
  <p>And finally the bit of javascript that displays the file as a download</p>
  
  <pre><code>    function viewAsFile(fileData, fileName, fileType) {
          try {
              fileData = window.atob(fileData);
              var ab = new ArrayBuffer(fileData.length);
              var ia = new Uint8Array(ab);    // ia provides window into array buffer
              for (var i = 0; i &lt; fileData.length; i++) {
                  ia[i] = fileData.charCodeAt(i);
              }
              var file = new Blob([ab], { type: fileType });
              fileData = "";
              if (window.navigator.msSaveOrOpenBlob) // IE10+
                  window.navigator.msSaveOrOpenBlob(file, fileName);
              else { // Others
                  var a = document.createElement("a"),
                      url = URL.createObjectURL(file);
                  a.href = url;
                  a.download = fileName;
                  document.body.appendChild(a);
                  a.click();
                  setTimeout(function () {
                      document.body.removeChild(a);
                      window.URL.revokeObjectURL(url);
                  }, 0);
              }
              return true;
          } catch (e) {
              ... error stuff
          }
      }
  </code></pre>
  
  <p>I see already that a more RESTful approach would be to use headers to indicate chunk range and to separate the file meta data from the file chunks.  Also I could try returning a data stream rather than Base64 encoded string.  If anyone has tips on that let me know.</p>