Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / Javascript

Client to Server File/Data Streaming

Rate me:
Please Sign up or sign in to vote.
5.00/5 (13 votes)
15 Dec 2021CPOL5 min read 25K   340   28   5
Your one-stop guide for API and web-client Form, XHR, Blob, and Drag & Drop File/Data Uploading
Trying to find an authoritative reference guide for how to stream upload client files to a server was an arduous task, hence this article.

Contents

Introduction

Trying to find an authoritative reference guide for how to stream upload client files to a server was an arduous task, hence this article.

This article demonstrates:

  1. Uploading a file from a C# client
  2. Uploading a file from a browser page:
    1. Using a Form element
    2. Using XHR with a Form element
    3. Uploading "blob" data
    4. Drag & Drop of files

To keep things simple:

  1. All variations described are handled by a single back-end endpoint.
  2. Nothing but simple JavaScript is used on the front-end. The demo is actually just an HTML file.
  3. I also demonstrate how to add additional metadata to the file/blob being uploaded.

Why Use Streams?

While the answer should be obvious, the main reason is that neither the client-side nor the server-side has to pull in the entire file into memory - instead, a stream breaks down the data from a large file into small chunks. The entire process, from the client reading the file to the server saving the contents to a file, is managed as "streamed data" and at most, both ends require only a buffer the size of the stream chunk.

How I Figured This All Out

Piecing this together involved a lot of Google searches. These are the links I found most useful:

Server URL

The server is set up to use IIS and therefore the URL used everywhere is http://localhost/FileStreamServer/file/upload and because this is a demonstration article, it's hard-coded in the examples. Obviously, one would implement this differently in real life!

The Server

The server is implemented with .NET Core 3.1. The API endpoint is straightforward:

C#
using System.IO;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace FileStreamServer.Controllers
{
  [ApiController]
  [Route("[controller]")]
  public class FileController : ControllerBase
  {
    public FileController()
    {
    }

    [HttpPost("upload")]
    public async Task<IActionResult> Upload([FromForm] DocumentUpload docInfo)
    {
      IActionResult ret;

      if (docInfo == null || docInfo.File == null)
      {
        ret = BadRequest("Filename not specified.");
      }
      else
      {
        var fn = Path.GetFileNameWithoutExtension(docInfo.File.FileName);
        var ext = Path.GetExtension(docInfo.File.FileName);
        string outputPathAndFile = $@"c:\projects\{fn}-{docInfo.Id}{ext}";

        using (FileStream output = System.IO.File.Create(outputPathAndFile))
        {
          await docInfo.File.CopyToAsync(output);
        }

        ret = Ok();
      }

      return ret;
    }
  }
}

The salient points of this implementation are as follows:

  1. The attribute [FromForm] informs the endpoint handler that will be receiving form data.
  2. The class DocumentUpload is the container for the "file" and form metadata.

The DocumentUpload Class

C#
public class DocumentUpload
{
  public IFormFile File { get; set; }
  public string Id { get; set; }
}

The property names must match the naming convention used on the front-end! This example illustrates the expectation that only one file will be specified and the metadata consists only of an "Id" value.

Handling Large Files

The more complicated part of this is actually configuring ASP.NET Core to accept large files. First, the web.config file has to be modified. In the system.webServer section, we have to increase the request limit:

XML
<security>
  <requestFiltering>
    <!-- 4 GB is the max we can set but we use 2GB 2147483647 
         because that is the form limit -->
      <requestLimits maxAllowedContentLength="2147483647" />
  </requestFiltering>
</security>

Secondly, the form options need to be set. I've opted to do this in the Startup code:

C#
public void ConfigureServices(IServiceCollection services)
{
  ...
  services.Configure<FormOptions>(x =>
  {
    // int.MaxValue is 2GB.
    x.ValueLengthLimit = int.MaxValue;
    x.MultipartBodyLengthLimit = int.MaxValue;
  });
  ...
}

Because int.MaxValue has a max value of 2GB, the size of the file being uploaded is limited to around that limit. Because of encoding overhead, the actual file size one can upload is less than 2GB, but I haven't figured out how much less.

A C# Client

A very simple C# console client that uploads a picture of one of my cats (file is included in the article download) is this, in its entirety:

C#
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

namespace FileStreamClient
{
  class Program
  {
    static void Main(string[] args)
    {
        var task = Task.Run(async () => await UploadAsync
                   ("http://localhost/FileStreamServer/file/upload", "cat.png"));
        task.Wait();
    }

    // https://stackoverflow.com/a/16925159
    // This was an alternate that is a lot more complicated: 
    // https://stackoverflow.com/a/2996904
    // and that I couldn't get to work on the server-side.

    private async static Task<Stream> UploadAsync(string url, string filename)
    {
      using var fileStream = new FileStream("cat.png", FileMode.Open, FileAccess.Read);
      using var fileStreamContent = new StreamContent(fileStream);
      using var stringContent = new StringContent("13");

      using var client = new HttpClient();
      using var formData = new MultipartFormDataContent();

      formData.Add(stringContent, "Id");
      formData.Add(fileStreamContent, "File", filename);
      var response = await client.PostAsync(url, formData);
      Stream ret = await response.Content.ReadAsStreamAsync();

      return ret;
    }
  }
}

Note how the content string "Id" and the file stream content "File" names match the properties in the DocumentUpload class defined on the server.

Client Web Page

For the web client-side, I wanted to demonstrate supporting several different things:

  1. A straight form upload with a submit button
  2. Replacing the standard submit process with an XHR upload implementation
  3. Uploading data as a blob
  4. Uploading a file via drag and drop

To keep things simple, multiple files are not supported.

The HTML file provided in the article download can be opened directly in the browser, for example: file:///C:/projects/FileStreaming/FileStreamClient/upload.html

A Straight Form Upload with a Submit Button

This is a very simple process with the failing that the action redirects the browser to the upload URL, which really is not what we want unless you want to display a page like "Your document has been uploaded."

HTML
<form id="uploadForm" action="http://localhost/FileStreamServer/file/upload" 
 method="post" enctype="multipart/form-data">
  <div>
    <input id="id" placeholder="ID" type="text" name="id" value="1" />
  </div>
  <div style="margin-top:5px">
    <input id="file" style="width:300px" type="file" name="file" />
  </div>
  <div style="margin-top:5px">
    <button type="submit">Upload</button>
  </div>
</form>

That's all there is to it. Notice that the name tags match (case is not sensitive) of the DocumentUpload class on the server.

Replacing the Standard Submit Process with an XHR Upload Implementation

This implementation requires changing the form tag and implementing the XHR upload code.

HTML
<form id="uploadForm" onsubmit="xhrUpload(); return false;" action="#">
  <div>
    <input id="id" placeholder="ID" type="text" name="id" value="1" />
  </div>
  <div style="margin-top:5px">
    <input id="file" style="width:300px" type="file" name="file" />
  </div>
  <div style="margin-top:5px">
    <button type="submit">Upload</button>
  </div>
</form>

<div style="margin-top:5px">
  <button onclick="xhrUpload()">Upload using XHR</button>
</div>

Notice that the button to upload using XHR is not part of the form!

The JavaScript implementation:

JavaScript
function xhrUpload() {
  const form = document.getElementById("uploadForm");
  const xhr = new XMLHttpRequest();
  responseHandler(xhr);
  xhr.open("POST", "http://localhost/FileStreamServer/file/upload");
  const formData = new FormData(form);
  xhr.send(formData);
}

function responseHandler(xhr) {
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      uploadResponse(xhr);
    }
  }
}

function uploadResponse(xhr) {
  if (xhr.status >= 200 && xhr.status < 300) {
    alert("Upload successful.");
  } else {
    alert("Upload failed: " + xhr.responseText);
  }
}

The most interesting part of this code is this:

JavaScript
const form = document.getElementById("uploadForm");
...
const formData = new FormData(form);

As whatever id value was entered and file selected are applied when instantiating the FormData object.

Uploading Data as a Blob

HTML:

HTML
<div style="margin-top:15px">
  <input id="data" placeholder="some data" type="text" value="The quick brown fox" />
</div>
<div style="margin-top:5px">
  <button onclick="uploadData()">Upload Data</button>
</div>

JavaScript:

JavaScript
function uploadData() {
  const id = document.getElementById("id").value;
  const data = document.getElementById("data").value;
  const blob = new Blob([data]);

  var xhr = new XMLHttpRequest();
  responseHandler(xhr);
  xhr.open("POST", "http://localhost/FileStreamServer/file/upload");

  var formData = new FormData();
  formData.append("Id", id);
  formData.append("File", blob, "data.txt");
  xhr.send(formData);
}

Note here that FormData is instantiated without referencing the form and instead the form data is applied programmatically. Also note that the filename is hard-coded. This code also reuses the responseHandler defined earlier.

Uploading a File with Drag and Drop

HTML:

HTML
<div ondrop="dropFile(event);" ondragover="allowDrop(event);" style="margin-top:15px; 
 width:200px; height:200px; border-style:solid; border-width: 1px; text-align:center">
  <div>Drag & drop file here</div>
</div>

The important thing here is that for drag & drop to work, both the ondrop and ondragover must have implementations.

JavaScript:

JavaScript
function allowDrop(e) {
  e.preventDefault();
}

function dropFile(e) {
  e.preventDefault();
  const dt = e.dataTransfer;

  // We could implement multiple files here.
  const file = dt.files[0];
  const id = document.getElementById("id").value;
  uploadFile(id, file);
}

function uploadFile(id, file) {
  var xhr = new XMLHttpRequest();
  responseHandler(xhr);
  xhr.open("POST", "http://localhost/FileStreamServer/file/upload");

  var formData = new FormData();
  formData.append("Id", id);
  formData.append("File", file, file.name);
  xhr.send(formData);
}

Notice that we call preventDefault as this is necessary to prevent the browser from actually attempting to render the file.

The other interesting part of this code is how we get the file object:

JavaScript
const dt = e.dataTransfer;
const file = dt.files[0];

I certainly would not have figured this out with searching the web for an example, as I rarely implement drag & drop on the front-ends that I build.

Conclusion

There you have it. A single reference article for uploading files / data using forms, XHR, or drag & drop.

History

  • 15th December, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
GeneralMy vote of 5 Pin
DEK4665621-Dec-21 4:58
DEK4665621-Dec-21 4:58 
GeneralRe: My vote of 5 Pin
Marc Clifton21-Dec-21 10:16
mvaMarc Clifton21-Dec-21 10:16 
DEK46656 wrote:
don’t you need to dispose,

Ah, good catch - yes, that would be best.

And I like the new C# language feature where when can write using var foo = ... which helps eliminate the parenthesis nesting. I need to remember to do that!

I've updated the code in the article (shortly, after I hit Post Message on this post.)

Questionmy vote of 5 Pin
Martin ISDN21-Dec-21 0:07
Martin ISDN21-Dec-21 0:07 
GeneralMy vote of 5 Pin
KevinAG16-Dec-21 11:07
KevinAG16-Dec-21 11:07 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA15-Dec-21 18:54
professionalȘtefan-Mihai MOGA15-Dec-21 18:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.