Skip to content

Instantly share code, notes, and snippets.

@stevegreatrex
Last active August 29, 2015 14:19
Show Gist options
  • Save stevegreatrex/978753abab990a1e3bcc to your computer and use it in GitHub Desktop.
Save stevegreatrex/978753abab990a1e3bcc to your computer and use it in GitHub Desktop.
Streaming Uploads to Azure
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; }
}
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;
}
}
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));
}
}
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);
}
}
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