Today I would like to describe another production use case for Azure Functions. This time example is quite simple. I would like to use Azure Functions to upload photos to Azure Blob Storage. Of course, you can upload photos directly to Azure Blob Storage. However, with such solution, your components are tightly connected. This can block you in the future. To avoid that you should add some middle layer.
For this, you can use Azure Function. It will act like an API that will accept photos encoder in base64 and upload it to provided location. With this approach, you will be able to change storage component in the future very easy if needed.
Wizard
During my workshop regarding Microsoft cloud, I always encourage people to start work on the new issue from going through already prepared examples. And, so did I. Unfortunately, I did not find there a solution to my problem.
There is also the second option – go through documentation that is available on the portal. We already know that we would like to build function acting as API. That is why we can use the easiest type of function HTTP Trigger:
Then we should go to Integrate tab and find integration with Azure Blob Storage:
So far so good. After that, we need to provide a correct Connection string and we can go further.
With this approach we can use one of the following types in the scope of binding:
- String,
- TextWriter,
- Stream,
- CloudBlobStream,
- ICloudBlob,
- CloudBlockBlob,
There is only one constraint – each of those elements will by bounded directly with the file, which will be stored in container defined in function.json file:
{ "bindings": [ { "authLevel": "function", "name": "req", "type": "httpTrigger", "direction": "in", "methods": [ "get", "post" ] }, { "name": "$return", "type": "http", "direction": "out" }, { "type": "blob", "name": "outputBlob", "path": "outcontainer/{rand-guid}" "connection": "AzureWebJobsDashboard", "direction": "out" } ], "disabled": false }
You should notice that in the configuration file you will find the name of the container where the file will be stored and also information that each time new file name will be generated. Guid will be used as file name. You can achieve this by using {rand-guid} in the path.
Right now we can adjust the code to our needs:
public static HttpResponseMessage Run( HttpRequestMessage req, out string outputBlob, TraceWriter log) { dynamic data = req.Content.ReadAsAsync<object>().Result; string pictureData = data?.pictureData; outputBlob = pictureData; return req.CreateResponse(HttpStatusCode.OK, "Uri: ... "); }
Let’s try to send the request. And… Nothing is happening… Maybe not nothing. The new file has been created in storage but when you are trying to open it instead of the photo you will get a weird text:
Also, we do not know the exact path to our file.
Moreover, this is not the way that we would like to follow.
Custom implementation with SDK
Let’s try to do it in a bit different way. This time we will start with Visual Studio and again we will create HTTP Trigger:
[FunctionName("AddPhoto")] public static async Task<HttpResponseMessage> RunAsync( [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req) { dynamic data = await req.Content.ReadAsAsync<object>(); string photoBase64String = data.photoBase64; Uri uri = await UploadBlobAsync(photoBase64String); return req.CreateResponse(HttpStatusCode.OK, uri); }
As you can see this function has been already changed. But still it is very easy function. It accepts POST requests. Then taking photo saved in base64 format from request body. This photo is transferred into UploadBlobAsync function as an argument. In this function two operation are executed:
- base64 format decoding
- photo upload to Azure Blob Storage.
Finally, the function returns URI to the uploaded photo.
Let’s start with the first part – information decoding (you can check how data encoded in base64 looks like in the previous photo). We need to take out the following information:
- ContentType,
- file extension,
- file content.
To do that we can use regular expression:
var match = new Regex( $@"^data\:(?<type>image\/(jpg|gif|png));base64,(?<data>[A-Z0-9\+\/\=]+)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase) .Match(photoBase64String); string contentType = match.Groups["type"].Value; string extension = contentType.Split('/')[1]; string fileName = $"{Guid.NewGuid().ToString()}.{extension}"; byte[] photoBytes = Convert.FromBase64String(match.Groups["data"].Value);
And at the end we should learn how we can upload file into Azure Blob Storage. This time we will not use binding by configuration but we will use SDK directly. On the one hand our solution will be not so simple like it could be. On the other one we will be able to adjust it better to our needs:
CloudStorageAccount storageAccount = CloudStorageAccount .Parse(ConfigurationManager.AppSettings["BlobConnectionString"]); CloudBlobClient client = storageAccount.CreateCloudBlobClient(); CloudBlobContainer container = client.GetContainerReference("img"); await container.CreateIfNotExistsAsync( BlobContainerPublicAccessType.Blob, new BlobRequestOptions(), new OperationContext()); CloudBlockBlob blob = container.GetBlockBlobReference(fileName); blob.Properties.ContentType = contentType; using (Stream stream = new MemoryStream(photoBytes, 0, photoBytes.Length)) { await blob.UploadFromStreamAsync(stream).ConfigureAwait(false); } return blob.Uri;
As you can see the code is straightforward. Our photo will be uploaded to an img container in Azure Blob Storage to which we provided Connection string in Application settings. We should add there BlobConnectionString key:
In this approach, we will also use guid as the filename. Additionally, we will add the file extension to the file name. Moreover, assign the correct content type of file. Those two actions allow us to open the file without issues.
Please find the whole function code:
[FunctionName("AddPhoto")] public static async Task<HttpResponseMessage> RunAsync( [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req) { dynamic data = await req.Content.ReadAsAsync<object>(); string photoBase64String = data.photoBase64; Uri uri = await UploadBlobAsync(photoBase64String); return req.CreateResponse(HttpStatusCode.Accepted, uri); } public static async Task<Uri> UploadBlobAsync(string photoBase64String) { var match = new Regex( $@"^data\:(?<type>image\/(jpg|gif|png));base64,(?<data>[A-Z0-9\+\/\=]+)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase) .Match(photoBase64String); string contentType = match.Groups["type"].Value; string extension = contentType.Split('/')[1]; string fileName = $"{Guid.NewGuid().ToString()}.{extension}"; byte[] photoBytes = Convert.FromBase64String(match.Groups["data"].Value); CloudStorageAccount storageAccount = CloudStorageAccount .Parse(ConfigurationManager.AppSettings["BlobConnectionString"]); CloudBlobClient client = storageAccount.CreateCloudBlobClient(); CloudBlobContainer container = client.GetContainerReference("img"); await container.CreateIfNotExistsAsync( BlobContainerPublicAccessType.Blob, new BlobRequestOptions(), new OperationContext()); CloudBlockBlob blob = container.GetBlockBlobReference(fileName); blob.Properties.ContentType = contentType; using (Stream stream = new MemoryStream(photoBytes, 0, photoBytes.Length)) { await blob.UploadFromStreamAsync(stream).ConfigureAwait(false); } return blob.Uri; }
Remarks
Please remember about one issue – the size of the request. Azure Functions have some limits. Right now, your total request size cannot be larger than about 100 MB. I think that for photos it is enough. If you would like to save video or other larger files it could not be enough. In that case you should think about other solution.
Leave A Comment