Last active
August 29, 2015 14:19
-
-
Save stevegreatrex/978753abab990a1e3bcc to your computer and use it in GitHub Desktop.
Streaming Uploads to Azure
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
public class Attachment | |
{ | |
/// <summary> | |
/// Gets the unique identifier for this attachment. | |
/// </summary> | |
public Guid Id { get; set; } | |
/// <summary> | |
/// Gets the file name for this attachment. | |
/// </summary> | |
public string FileName { get; set; } | |
/// <summary> | |
/// Gets the MIME type of the attachment. | |
/// </summary> | |
public string MimeType { get; set; } | |
/// <summary> | |
/// Gets the date and time at which this attachment was created | |
/// </summary> | |
public DateTimeOffset CreatedDate { get; set; } | |
/// <summary> | |
/// Gets the description. | |
/// </summary> | |
public string Description { get; set; } | |
} |
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
public class AttachmentController | |
{ | |
private AttachmentService _attachmentService; | |
public AttachmentController(AttachmentService attachmentService) | |
{ | |
_attachmentService = attachmentService; | |
} | |
[HttpPost] | |
[OutputCache(CacheProfile = "DoNotCache")] | |
public async Task<ActionResult> AsyncUploadExternal(AsyncFileUploadModel details) | |
{ | |
if (details == null || !ModelState.IsValid) | |
return new HttpStatusCodeResult(HttpStatusCode.BadRequest); | |
if (Request.Files.Count == 0) | |
return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "No files specified"); | |
var file = Request.Files[0]; | |
var range = ContentRangeDetails.FromHeader(this.Request.Headers["Content-Range"]); | |
var attachment = await GetOrCreateAttachment(details, file); | |
if (range == ContentRangeDetails.None) | |
await _attachmentService.UploadWholeAttachmentAsync(attachment, file.InputStream); | |
else | |
{ | |
await _attachmentService.UploadAttachmentBlockAsync(attachment, range, file.InputStream); | |
if (range.IsFinalBlock) | |
await _attachmentService.CompleteUploadAttachment | |
} | |
return Json({ | |
//... | |
}); | |
} | |
private async Task<IExternalAttachment> GetOrCreateAttachment(AsyncFileUploadModel details, HttpPostedFileBase file) | |
{ | |
var attachment = await _attachmentService.GetAttachmentAsync(details.Id); | |
if (attachment == null) | |
{ | |
var mimeType = string.Equals(file.ContentType, "text/html", StringComparison.OrdinalIgnoreCase) ? | |
"text/plain" : file.ContentType; | |
attachment = new ExternalAttachment | |
{ | |
Id = details.Id, | |
Description = details.Description, | |
FileName = file.FileName, | |
MimeType = mimeType | |
}; | |
await _attachmentService.CreateAttachmentAsync(attachment); | |
} | |
return attachment; | |
} | |
} |
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
public class AttachmentService | |
{ | |
/// <summary> | |
/// The default block size that will be used when this class is responsible for | |
/// splitting data into blocks. | |
/// </summary> | |
public static int DefaultBlockSize { get; set; } | |
static AttachmentService() | |
{ | |
DefaultBlockSize = 1024 * 100; | |
} | |
private BlobFactory _blobFactory; | |
public AttachmentService(BlobFactory blobFactory) | |
{ | |
_blobFactory = blobFactory; | |
} | |
/// <summary> | |
/// Gets the details of the external attachment identified by <paramref name="attachmentId" /> | |
/// </summary> | |
/// <param name="attachmentId">The unique identifier of the external attachment.</param> | |
/// <returns> | |
/// Details of the external attachment, or <c>null</c> if none exists. | |
/// </returns> | |
public Task<Attachment> GetAttachmentAsync(Guid attachmentId) | |
{ | |
var attachment = new Attachment(); //get attachment from DB | |
return Task.FromResult(attachment); | |
} | |
/// <summary> | |
/// Creates and stores a new external attachment based on <paramref name="attachment" />. | |
/// </summary> | |
/// <param name="attachment">Provides data for the new attachment, and will be updated | |
/// with generated property values.</param> | |
/// <returns></returns> | |
public Task CreateAttachmentAsync(Attachment attachment) | |
{ | |
attachment.ThrowIfNull("attachment"); | |
//save attachment to DB | |
return Task.FromResult(0); | |
} | |
/// <summary> | |
/// Uploads a single block of the attachment represented by <paramref name="attachment" />. | |
/// </summary> | |
/// <param name="attachment">The attachment being uploaded</param> | |
/// <param name="blockNumber">The block number</param> | |
/// <param name="blockData">The content of the block.</param> | |
/// <returns></returns> | |
public async Task UploadAttachmentBlockAsync(Attachment attachment, int blockNumber, Stream blockData) | |
{ | |
var blob = await _blobFactory.CreateBlobReference(attachment); | |
await blob.PutBlockAsync(GetBlockId(blockNumber), blockData, null); | |
} | |
/// <summary> | |
/// Finalises the upload of the attachment represented by <paramref name="attachment" /> after | |
/// all blocks have been uploaded. | |
/// </summary> | |
/// <param name="attachment">The attachment being uploaded.</param> | |
/// <param name="totalBlockCount">The total number of uploaded blocks.</param> | |
/// <returns></returns> | |
/// <exception cref="System.InvalidOperationException">No matching upload exists</exception> | |
public async Task CompleteUploadAttachmentAsync(Attachment attachment, int totalBlockCount) | |
{ | |
attachment.ThrowIfNull("attachment"); | |
var dbAttachment = new { }; //get attachment details from DB | |
if (dbAttachment == null) throw new InvalidOperationException("No matching upload exists"); | |
var blockIds = Enumerable.Range(1, totalBlockCount) | |
.Select(GetBlockId); | |
var blob = await _blobFactory.CreateBlobReference(attachment); | |
await blob.PutBlockListAsync(blockIds); | |
blob.Properties.ContentType = attachment.MimeType; | |
await blob.SetPropertiesAsync(); | |
dbAttachment.IsUploaded = attachment.IsUploaded = true; | |
//save to DB | |
} | |
/// <summary> | |
/// Uploads and finalises an entire attachment represented by <paramref name="attachment" />. | |
/// </summary> | |
/// <param name="attachment">The attachment being uploaded.</param> | |
/// <param name="attachmentData">The attachment data.</param> | |
/// <returns></returns> | |
/// <exception cref="System.InvalidOperationException">No matching upload exists</exception> | |
public async Task UploadWholeAttachmentAsync(Attachment attachment, Stream attachmentData) | |
{ | |
attachment.ThrowIfNull("attachment"); | |
var dbAttachment = new { };//get attachment details from DB | |
if (dbAttachment == null) throw new InvalidOperationException("No matching upload exists"); | |
var buffer = new byte[DefaultBlockSize]; | |
var blockCount = 0; | |
using (var bufferStream = new MemoryStream(buffer)) | |
{ | |
var read = await attachmentData.ReadAsync(buffer, 0, DefaultBlockSize); | |
while (read > 0) | |
{ | |
blockCount++; | |
bufferStream.Seek(0, SeekOrigin.Begin); | |
if (read < DefaultBlockSize) | |
bufferStream.SetLength(read); | |
await this.UploadAttachmentBlockAsync(attachment, blockCount, bufferStream); | |
read = await attachmentData.ReadAsync(buffer, 0, DefaultBlockSize); | |
} | |
} | |
await this.CompleteUploadAttachmentAsync(attachment, blockCount); | |
dbAttachment.IsUploaded = attachment.IsUploaded = true; | |
//save to DB | |
} | |
private static string GetBlockId(int blockNumber) | |
{ | |
return Convert.ToBase64String(BitConverter.GetBytes(blockNumber)); | |
} | |
} |
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
public class BlobFactory | |
{ | |
private ConcurrentDictionary<string, ICloudBlockBlobWrapper> _blobCache = new ConcurrentDictionary<string,ICloudBlockBlobWrapper>(); | |
private string _containerName; | |
private string _connectionString; | |
public BlobFactory(string connectiongString, string containerName) | |
{ | |
_connectionString = connectiongString; | |
_containerName = containerName; | |
} | |
/// <summary> | |
/// Creates a BLOB reference to the specified attachment. | |
/// </summary> | |
/// <param name="attachment">The attachment.</param> | |
/// <returns> | |
/// The BLOB reference. | |
/// </returns> | |
public async Task<ICloudBlockBlobWrapper> CreateBlobReference(IExternalAttachment attachment) | |
{ | |
var blobName = GetBlobName(attachment); | |
ICloudBlockBlobWrapper result = null; | |
if (_blobCache.ContainsKey(blobName) && _blobCache.TryGetValue(blobName, out result)) | |
return result; | |
var account = CloudStorageAccount.Parse(_connectionString); | |
var client = account.CreateCloudBlobClient(); | |
var container = client.GetContainerReference(_containerName); | |
await container.CreateIfNotExistsAsync(BlobContainerPublicAccessType.Off, null, null); | |
result = new CloudBlockBlobWrapper(container.GetBlockBlobReference(blobName)); | |
_blobCache.AddOrUpdate(blobName, result, (key, val) => val); | |
return result; | |
} | |
private string GetBlobName(IExternalAttachment attachment) | |
{ | |
return string.Format(CultureInfo.InvariantCulture, "{0}/{1}", attachment.Id, attachment.FileName); | |
} | |
} |
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
public class ContentRangeDetails | |
{ | |
private static Regex _pattern = new Regex(@"bytes (\d+)-(\d+)/(\d+)"); | |
/// <summary> | |
/// Prevents a default instance of the <see cref="ContentRangeDetails"/> class from being created. | |
/// </summary> | |
private ContentRangeDetails() | |
{} | |
/// <summary> | |
/// Gets the start of the range. | |
/// </summary> | |
public int Start { get; private set; } | |
/// <summary> | |
/// Gets the end of the range. | |
/// </summary> | |
public int End { get; private set; } | |
/// <summary> | |
/// Gets the total size. | |
/// </summary> | |
public int Total { get; private set; } | |
/// <summary> | |
/// Gets a value indicating whether this range represents the final block. | |
/// </summary> | |
/// <value> | |
/// <c>true</c> if this range represents the final block; otherwise, <c>false</c>. | |
/// </value> | |
public bool IsFinalBlock | |
{ | |
get { return this == None || this.End == this.Total - 1; } | |
} | |
/// <summary> | |
/// Gets the block number represented by this range based on the | |
/// specified block size. | |
/// </summary> | |
/// <param name="blockSize">The size of each block.</param> | |
/// <returns>The block number represented by this range.</returns> | |
public int GetBlockNumber(int blockSize) | |
{ | |
if (blockSize < 1) throw new ArgumentException("blockSize must be positive", "blockSize"); | |
return (int)Math.Floor((double)(this.Start / blockSize)) + 1; | |
} | |
/// <summary> | |
/// Singleton instance representing an empty or missing Content-Range header. | |
/// </summary> | |
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", | |
Justification="ContentRangeDetails is immutable")] | |
public readonly static ContentRangeDetails None = new ContentRangeDetails(); | |
/// <summary> | |
/// Parses the value from the Content-Range header and creates a new <see cref="ContentRangeDetails"/> | |
/// instance. | |
/// </summary> | |
/// <param name="header">The header text.</param> | |
/// <returns>A new <see cref="ContentRangeDetails"/> instance.</returns> | |
public static ContentRangeDetails FromHeader(string header) | |
{ | |
if (header == null) return ContentRangeDetails.None; | |
var matches = _pattern.Match(header); | |
if (!matches.Success) return ContentRangeDetails.None; | |
return new ContentRangeDetails | |
{ | |
Start = int.Parse(matches.Groups[1].Value, CultureInfo.InvariantCulture), | |
End = int.Parse(matches.Groups[2].Value, CultureInfo.InvariantCulture), | |
Total = int.Parse(matches.Groups[3].Value, CultureInfo.InvariantCulture) | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment