Dziś kolejny przykład użycia Azure Functions w kodzie produkcyjnym. Problem jest dość prosty – chcemy wykorzystać funkcję do wgrania zdjęcia do Azure Blob Storage. Oczywiście można było wgrać zdjęcie od razu do Azure Blob Storage, ale takie podejście powoduje dość silne uzależnienie się od tego rozwiązania. Z tego powodu została zastosowana funkcja jako element pośredni.
Funkcja ta stanowi API pozwalające na przesłanie zdjęcia zakodowanego w base64 do wybranego przez nas miejsca. Takie podejście pozwoli nam w przyszłości na łatwą zmianę wybranego rozwiązania służącego do przechowywania plików.
Kreator
W trakcie warsztatów dotyczących chmury Microsoftu zachęcam uczestników do rozpoczęcia prac od przejrzenia przykładowych funkcji dostarczonych przez Microsoft. Niestety nie znajdziemy tam przykładu rozwiązującego nasz problem.
Zastosujmy więc drugi sposób – dokumentację dostępną na portalu. Wiemy, że ma to być API, czyli możemy spokojnie wybrać jedną z funkcji, która będzie reagowała na żądanie HTTP. Niech będzie to najprostsza funkcja typu HTTP Trigger:
Następnie przejdźmy do zakładki Integrate i spróbujmy odszukać możliwości integracji z Azure Blob Storage:
Na razie idzie całkiem nieźle. Wystarczy tylko podać odpowiedni connection string i możemy działać. Do dyspozycji mamy następujące typy, które zostaną automatycznie zbindowane:
- String,
- TextWriter,
- Stream,
- CloudBlobStream,
- ICloudBlob,
- CloudBlockBlob,
- CloudPageBlob.
Zwróćcie proszę uwagę tylko na jedno ograniczenie – każdy z tych elementów jest połączony bezpośrednio z plikiem, który zostanie umieszczony w kontenerze zdefiniowanym w pliku function.json:
{ "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 }
Spostrzegawcza osoba, zauważy również, że nazwą pliku będzie losowo wybrany guid. Osiągamy to poprzez wykorzystanie {rand-guid} w ścieżce.
Idąc dalej – zacznijmy pracę od przykładu, który został dostosowany do naszych potrzeb:
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: ... "); }
Wysyłamy żądanie i w zasadzie nic się nie dzieje – no może nie do końca. W Azure Blob Storage został utworzony plik, który zawiera przesłaną zawartość.
Ale… Nie otwiera nam się to jako zdjęcie. I nie znamy linku do tego pliku.
Jak widzicie nie jest to droga, którą chcielibyśmy podążyć w tym przypadku.
Własna implementacja przy wykorzystaniu SDK
Zróbmy to w trochę inny sposób. Zbudujmy funkcję w Visual Studio zaczynając od HTTP Trigger’a:
[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); }
Funkcja jest bardzo prosta. Akceptuje żądania typu POST, wyciąga z body zakodowany w base64 obrazek. Obrazek ten jest przesyłany do funkcji UploadBlobAsync jako argument. Funkcja dekoduje przesłany obrazek, wgrywa go na Azure Blob Storage i zwraca URI do wgranego obrazka. Otrzymana wartość będzie finalnym wynikiem działania naszej funkcji.
Aby móc wrzucić obrazek musimy najpierw zdekodować poszczególne informacje zawarte w base64 (jeśli nie wiecie, jak wyglądają zakodowane informacje w base64 to spójrzcie na poprzednie zdjęcie).
Z tego ciągu znaków musimy wydobyć następujące informacje:
- ContentType,
- rozszerzenie pliku,
- oraz samą zawartość pliku.
Możemy to osiągnąć używając wyrażenia regularnego:
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);
Na koniec została najłatwiejsza część – wgranie pliku do Azure Blob Storage. Nie wykorzystamy do tego binding’u, tylko metody z SDK. Z jednej strony tracimy trochę na łatwości implementacji. Natomiast z drugiej zyskujemy dużo większą swobodę dostosowania rozwiązania do naszych potrzeb:
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;
Kod jest bardzo prosty. Obrazek zostaje wrzucony do Azure Blob Storage do którego Connection string podaliśmy w Application settings pod kluczem BlobConnectionString:
Tam też jest tworzony kontener img do którego wygrywane są nasze zdjęcia. W tym przypadku nazwą pliku będzie też losowo wybrany guid. Dodatkowo zostanie ustawione poprawne rozszerzenie pliku i ContentType. Dzięki temu nie będziemy mieli problemu z otworzeniem naszego zdjęcia.
I na koniec funkcja w całości:
[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; }
Uwagi
Jeszcze jedna rzecz o której należy pamiętać – rozmiar żądania. Niestety Azure Functions mają swoje ograniczenia. Całe żądanie wysyłane do funkcji nie może być większe niż około 100 MB. W przypadku zdjęć ten rozmiar jest wystarczający. Problemy mogą się pojawić, jeśli będziemy chcieli wgrać zamiast zdjęcia film lub inny większy plik. Wtedy będziemy musieli
Zostaw komentarz