Created
February 7, 2020 16:41
-
-
Save abodalevsky/5145883339b59d25d27ab1202478ee28 to your computer and use it in GitHub Desktop.
Upload multipart binary data C#
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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