Skip to content

Instantly share code, notes, and snippets.

@abodalevsky
Created February 7, 2020 16:41
Show Gist options
  • Save abodalevsky/5145883339b59d25d27ab1202478ee28 to your computer and use it in GitHub Desktop.
Save abodalevsky/5145883339b59d25d27ab1202478ee28 to your computer and use it in GitHub Desktop.
Upload multipart binary data C#
using Logging.Interfaces;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Helpers
{
/// <summary>
/// Uploads video file to S3 service.
/// </summary>
internal class FileUploadClient
{
private const string BoundaryString = "------MyBOUNDARYWinAppFormBoundary2"; // DateTime.Now.Ticks.ToString("x")
private readonly ILogger logger;
private readonly ASCIIEncoding ascii = new ASCIIEncoding();
private readonly byte[] boundaryLine;
private readonly byte[] lastBoundaryLine;
private long totalBytes = 0;
private long sentBytes = 0;
/// <summary>
/// Informs about upload progress <see cref="FileUploadProgressEventArgs"/>
/// </summary>
public event EventHandler<FileUploadProgressEventArgs> UploadProgressChanged;
/// <summary>
/// Initializes a new instance of the <see cref="FileUploadClient"/> class.
/// </summary>
public FileUploadClient()
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 |
SecurityProtocolType.Tls |
SecurityProtocolType.Tls11 |
SecurityProtocolType.Tls12;
var boundary = "\r\n--" + FileUploadClient.BoundaryString + "\r\n";
this.boundaryLine = this.ascii.GetBytes(boundary);
var lastBoundary = "\r\n--" + FileUploadClient.BoundaryString + "--\r\n";
this.lastBoundaryLine = this.ascii.GetBytes(lastBoundary);
this.logger = new LazyLogger<FileUploaderModule>("VSS:FileUploadClient");
this.logger.Info("Created");
}
/// <summary>
/// Uploads file. During upload <see cref="UploadProgressChanged"/> event is fired every 300kB upload.
/// </summary>
/// <param name="jObj">data from S3 server.</param>
/// <param name="file">name of file to be uploaded.</param>
/// <param name="ct"><see cref="CancellationToken"/>.</param>
/// <returns>once done.</returns>
internal async Task UploadFile(JObject jObj, string file, CancellationToken ct)
{
this.logger.Info("Started");
if (jObj == null)
{
throw new ArgumentNullException(nameof(jObj));
}
if (!File.Exists(file))
{
throw new FileNotFoundException("File not found", file);
}
ct.ThrowIfCancellationRequested();
var fileInfo = new FileInfo(file);
this.sentBytes = 0;
this.logger.Trace("Prepare boundaries");
var boundaries = this.PrepareBoundaries(jObj);
ct.ThrowIfCancellationRequested();
this.totalBytes = (this.boundaryLine.Length * boundaries.Count)
+ this.lastBoundaryLine.Length
+ boundaries.Aggregate(0, (count, i) => i.Length + count)
+ fileInfo.Length;
this.UpdateProgress();
var uploadServer = new Uri($"https:{jObj["uploadUrl"]}");
this.logger.Trace($"Create connection to {uploadServer}");
ct.ThrowIfCancellationRequested();
var request = (HttpWebRequest)WebRequest.Create(uploadServer);
request.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials;
request.AllowWriteStreamBuffering = false;
request.Method = WebRequestMethods.Http.Post;
request.ContentType = $"multipart/form-data; boundary={BoundaryString}";
request.KeepAlive = false;
request.ContentLength = this.totalBytes;
using (var stream = await request.GetRequestStreamAsync())
{
this.logger.Trace($"Send boundaries");
foreach (var boundary in boundaries)
{
await this.SendBoundary(stream, boundary, ct);
}
this.UpdateProgress();
this.logger.Trace($"Send file content");
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read))
{
var buff = new byte[2048];
int bytesRead = 0;
int cycle = 0;
while ((bytesRead = await fileStream.ReadAsync(buff, 0, buff.Length)) != 0)
{
ct.ThrowIfCancellationRequested();
await stream.WriteAsync(buff, 0, bytesRead);
this.sentBytes += bytesRead;
if (++cycle == 150)
{
// send update every 300kB
this.UpdateProgress();
cycle = 0;
}
ct.ThrowIfCancellationRequested();
}
}
this.logger.Trace("Content sent, close boundary");
await stream.WriteAsync(this.lastBoundaryLine, 0, this.lastBoundaryLine.Length);
this.sentBytes += this.lastBoundaryLine.Length;
this.logger.Trace($"Calculated length: {this.totalBytes}, real sent: {this.sentBytes}");
this.sentBytes = this.totalBytes;
this.UpdateProgress();
}
this.logger.Info("Content sent.");
ct.ThrowIfCancellationRequested();
using (var response = (HttpWebResponse)await request.GetResponseAsync())
{
ct.ThrowIfCancellationRequested();
if (response.StatusCode != HttpStatusCode.OK)
{
throw new WebException($"Upload failed with code: {response.StatusCode}");
}
}
}
private List<byte[]> PrepareBoundaries(JObject jObj)
{
var boundaries = new List<byte[]>(11);
boundaries.Add(this.GetNameBoundary(jObj));
boundaries.Add(this.GetBoundary("key", jObj));
boundaries.Add(this.GetBoundary("signature", jObj));
boundaries.Add(this.GetBoundary("policy", jObj));
boundaries.Add(this.GetBoundary("success_action_status", jObj));
boundaries.Add(this.GetBoundary("Filename", jObj));
boundaries.Add(this.GetContentBoundary(jObj));
return boundaries;
}
private async Task SendBoundary(Stream s, byte[] buffer, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
await s.WriteAsync(this.boundaryLine, 0, this.boundaryLine.Length);
this.sentBytes += this.boundaryLine.Length;
await s.WriteAsync(buffer, 0, buffer.Length);
this.sentBytes += buffer.Length;
}
private byte[] GetNameBoundary(JObject jObj)
{
var boundary = $"Content-Disposition: form-data; name=\"name\"\r\n\r\n{jObj["form"]["Filename"].ToString()}";
this.logger.Trace($"Boundary Name: \r\n{boundary}");
return this.ascii.GetBytes(boundary);
}
private byte[] GetBoundary(string name, JObject jObj)
{
var boundary = $"Content-Disposition: form-data; name=\"{name}\"\r\n\r\n{jObj["form"][name].ToString()}";
this.logger.Trace($"Boundary {name}: \r\n{boundary}");
return this.ascii.GetBytes(boundary);
}
private byte[] GetContentBoundary(JObject jObj)
{
var boundary = $"Content-Disposition: form-data; name=\"file\"; filename=\"{jObj["form"]["Filename"].ToString()}\"\r\nContent-Type: {jObj["form"]["x-amz-meta-content_type"]}\r\n\r\n";
this.logger.Trace($"Boundary Content: \r\n{boundary}");
return this.ascii.GetBytes(boundary);
}
private void UpdateProgress()
{
Task.Run(() => this.UploadProgressChanged?.Invoke(this, new FileUploadProgressEventArgs(this.sentBytes, this.totalBytes)));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment