Blob Storage
There are two types, block blobs (<=200Gb) and page blobs (<= 1Tb)
Main operations
Here's a small facade that shows the main operations. There's an enum and class for list results, and the interface is because I substitute a file IO version (below) in development.
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
namespace MartinWeb.Cloud
{
public class BlobStore : IBlobStore
{
private readonly string _containerName;
private readonly bool _isPublic;
private readonly string _connectionString;
public BlobStore(string containerName, bool isPublic = false)
{
//container names must be lowercase a-z,0-9 and -, and 3 to 63 chars long
if (string.IsNullOrWhiteSpace(containerName)) throw new ArgumentNullException();
if (containerName.Length < 3 || containerName.Length > 63) throw new ArgumentException();
_containerName = Regex.Replace(containerName.ToLowerInvariant(), @"[^a-z0-9\-]", "");
_isPublic = isPublic;
_connectionString = CloudConfigurationManager.GetSetting("StorageConnectionString");
if (string.IsNullOrEmpty(_connectionString))
{
_connectionString =
ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString;
}
}
private CloudBlobContainer FindContainer()
{
var storageAccount = CloudStorageAccount.Parse(_connectionString);
var client = storageAccount.CreateCloudBlobClient();
var container = client.GetContainerReference(_containerName);
container.CreateIfNotExists();
if (_isPublic)
{
container.SetPermissions(
new BlobContainerPermissions { PublicAccess = BlobContainerPublicAccessType.Blob });
}
return container;
}
public void Save(string name, Stream stream)
{
if (name.Length > 1024) throw new ArgumentException("Name is too long");
var container = FindContainer();
var blockBlob = container.GetBlockBlobReference(name);
blockBlob.UploadFromStream(stream);
}
public Stream Read(string name)
{
if (name.Length > 1024) throw new ArgumentException("Name is too long");
var container = FindContainer();
var blockBlob = container.GetBlockBlobReference(name);
return blockBlob.OpenRead();
}
public string ReadAsText(string name)
{
if (name.Length > 1024) throw new ArgumentException("Name is too long");
var container = FindContainer();
var blockBlob = container.GetBlockBlobReference(name);
using (var memoryStream = new MemoryStream())
{
blockBlob.DownloadToStream(memoryStream);
var array = memoryStream.ToArray();
return System.Text.Encoding.UTF8.GetString(array);
}
}
public void Delete(string name)
{
if (name.Length > 1024) throw new ArgumentException("Name is too long");
var container = FindContainer();
var blockBlob = container.GetBlockBlobReference(name);
blockBlob.DeleteIfExists();
}
public IEnumerable<ListBlobItem> List(string prefix = null, bool useFlatBlobListing = false)
{
//prefix is the subdirectory
var container = FindContainer();
foreach (var item in container.ListBlobs(prefix, useFlatBlobListing))
{
if (item is CloudBlobDirectory)
{
yield return new ListBlobItem(item, ListBlobType.Directory);
}
var blockBlob = item as CloudBlockBlob;
if (blockBlob != null)
{
yield return new ListBlobItem(item, ListBlobType.BlockBlob) { Length = blockBlob.Properties.Length };
}
else
{
var cloudPageBlob = item as CloudPageBlob;
if (cloudPageBlob != null)
{
yield return new ListBlobItem(item, ListBlobType.PageBlob) { Length = cloudPageBlob.Properties.Length };
}
}
}
}
}
public interface IBlobStore
{
void Save(string name, Stream stream);
Stream Read(string name);
string ReadAsText(string name);
void Delete(string name);
IEnumerable<ListBlobItem> List(string prefix = null, bool useFlatBlobListing = false);
}
public class ListBlobItem
{
public ListBlobItem(IListBlobItem item, ListBlobType blobType)
{
Uri = item.Uri;
ListBlobType = blobType;
}
public ListBlobItem(string uri)
{
Uri = new Uri(uri);
ListBlobType = ListBlobType.BlockBlob;
}
public Uri Uri { get; private set; }
public long Length { get; set; }
public ListBlobType ListBlobType { get; private set; }
}
public enum ListBlobType
{
BlockBlob,//block blobs <=200Gb, If <64Mb written in single write.
PageBlob,//page blobs <= 1Tb
Directory //fake subdirectory (blobs with / in filename)
}
}
Use
using (var ms = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("Hello")))
{
new BlobStore("MyContainer").Save("Hello.txt", ms);
}
File version
The storage emulator is dead slow, so I prefer to use File IO locally (although the emulator itself uses a directory C:\Users\User\AppData\Local\DevelopmentStorage\LDB\BlockBlobRoot).
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
namespace MartinWeb.Cloud
{
public class FileStore : IBlobStore
{
private readonly string _container;
public FileStore(string containerName)
{
var path =
ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString;
if (!Directory.Exists(path))
path = Path.GetTempPath();
_container = Path.Combine(path, containerName);
Directory.CreateDirectory(_container);
}
private static string SanitizeName(string name)
{
if (string.IsNullOrEmpty(name)) return null;
return new string(name.Select(x =>
Path.GetInvalidFileNameChars().Contains(x) ? '_' : x).ToArray());
}
public void Save(string name, Stream stream)
{
var path = Path.Combine(_container, SanitizeName(name));
using (var file = File.Create(path))
{
stream.CopyTo(file);
}
}
public Stream Read(string name)
{
var path = Path.Combine(_container, SanitizeName(name));
return File.OpenRead(path);
}
public string ReadAsText(string name)
{
var path = Path.Combine(_container, SanitizeName(name));
return File.ReadAllText(path);
}
public void Delete(string name)
{
var path = Path.Combine(_container, SanitizeName(name));
File.Delete(path);
}
public IEnumerable<ListBlobItem> List(string prefix = null, bool useFlatBlobListing = false)
{
foreach (var file in Directory.GetFiles(_container, SanitizeName(prefix) + "*"))
{
yield return new ListBlobItem(file);
}
}
}
}
Concurrency
See MSDN team blog
- Default is no concurrency (last wins)
- Optimistic concurrency:
- read the ETag: var etag = blob.Properties.Etag;
- generate an access condition: var ac = AccessCondition.GenerateIfMatchCondition(orignalETag);
- blob.UploadText passing accessCondition
- Failure is a StorageException, with ex.RequestInformation.HttpStatusCode == (int)HttpStatusCode.PreconditionFailed (HTTP 412)
- Pessimistic concurrency:
- var lease = blob.AcquireLease(TimeSpan.FromSeconds(15), null);
- var accessCondition = AccessCondition.GenerateLeaseCondition(lease);
- blob.UploadText passing accessCondition
- If you don't pass lease, or it expires, you get the same StorageException with PreconditionFailed / 412