Partager via


Image uploading with Azure Functions node.js and Angular 4

In this blog post, I'd like to explain how to upload image to the Azure Blob storage from Angular 4 SPA application and Azure Functions HttpTrigger Node. In this experiment, I use Mac Book Pro with Azure Functions CLI with core branch. Which means Azure Functions 2.0 with local debugging one.

Binary Uploading Strategy

You can choose two strategies for uploading image. One is multipart/form or base64 encoding.  In this usecase, I recommend to use base64 encoding.  If you choose the multipart/form strategy, you need to parse multipart. However, every multipart parser written for experss not for azure functions. (e.g. busboy). Even if you use azure-function-express, you can't do it until now.  The Azure Functions req object doesn't have some methods for the multipart parsers. If you want to go  multipart/form for Azure Functions Node.js, you need to write multipart parser by yourself.

NOTE (2017/10/9): I tried to write the multipart parser. However, eventually I didn't do it. The current version of the Azure Functions (Javascript) forcefully convert binary into string.  I wrote an issue and discuss it. I realized that it is known issue and they try to solve in the future version.

Other solution is to use C#. I try to write multipart parser for Azure Functions Node.  Until then, I recommend the base64 encoding strategy.  Keep it simple.

Architecture

I'm using Angular 4.x. as a SPA. From this SPA, I'll send image to HttpTrigger, then using blob trigger, I'll upload the image file to a container.

 

 SPA  -> Azure Functions (HttpTrigger with Blob output bindings) -> Storage Account

 

Something like this.

 

Azure Functions Settings for SPA

If you access form SPA to Azure Functions, you might encounter this error.

 Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://localhost:4200' is therefore not allowed access. The response had HTTP status code 404.

This is the CORS problem. Javascript code from browser doesn't access the outside domain resource. For avoiding this issue on your local debugging enviornment, you need to add CORS setting on your local.settings.json. It requires server side configuration.

 {
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "AzureWebJobsDashboard": ""
  },
  "Host": {
    "LocalHttpPort": 7071,
    "CORS": "*"
  }
}

Now you are ready to write code!

Image Upload using Angular 4

The code is very easy.  I just do  1. base64 encoding using FileReader: readAsDataURL() method. Once I upload file on an input. Then  2. the Angular 4 invoke upload() method. Then "load" event happens, then it create an json object as a body and send it to the server.

car-detail.component.ts

 @Component({

 selector: 'app-root',

 templateUrl: './car-detail.component.html'

})

export class CarDetailComponent {

 title = 'Car Reviews';

 image = 'assets/noimage.jpg';

 car: Car;


 constructor(private http:Http) {

 this.car = new Car();

 this.car.name = "";

 this.car.company = "";

 this.car.description = "";

 this.car.image_url = "assets/noimage.jpg";

 this.car.state = "pending";

 }

 executeUpload(base64encoded: string, filename: string) {

 let headers = new Headers();

 let options = new RequestOptions({

 headers: headers

 });

 let data = {filename: filename, data: base64encoded }

 this.http.post(encodeURI('https://localhost:7071/api/FileUploadNode/' + filename), data, options)

 .subscribe(

 data => console.log(data),

 error => console.log(error)

 );

 this.car.image_url = encodeURI("https://something.blob.core.windows.net/outcontainer/" + filename); 

 
 this.image = base64encoded; // 3. 

 console.log("File encoded");

 }

 upload(list: any) {

 if (list.length <= 0) { return; }

 let f = list[0];

 let reader = new FileReader();

 let self = this;

 reader.addEventListener("load", function() {  // 2.

 let base64encoded = reader.result;

 self.executeUpload(base64encoded, f.name);

 }, false);

 if (f) {

 reader.readAsDataURL(f); // 1.

 }

 }

}

 

After sending image data to the server, we need to update the image on the screen. Although I have a bindings to Car instance, I didn't directly map the image to the image. Instead I use image property then 3. I pass the base64 image data to the image property. We can pass the URL of the blob storage, however, it is asynchronous operations, we need to wait until the process has been done.

car-detail.component.html

 

  <md-card-header>

 <div md-card-avatar class="example-header-image"></div>

 <md-card-title>{{car.name}}</md-card-title>

 <md-card-subtitle>{{car.company}}</md-card-subtitle>

 </md-card-header>

 <img md-card-image src="{{image}}" alt="Some car image">

 <md-card-content>

 

Decode base64 image with Azure Functions

Now you can get an image encoded in base64 via HttpTrigger of Azure Functions. Let's write a code for decode.  To encode the image base64 image is like this according to the RFC2397 . Just decode these.

example of base64 image. It includes some headers.

 [10/7/17 4:58:58 AM]   data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABagAAAG0CAYAAADXf8CiAAAMFWlDQ1BJQ0MgUHJvZmlsZQAASImVVwdYU8kWnltSCAktEAEpoTdBepUaqiAgHWyEJEAoAROCih1dVHDtIoIVXRVRdC2ALDZEsbAI2OsDFRVlXVzFhpo3KaDra9

And once you configure the output blob bindings, you all you need to do is

 

 context.bindings.outputBlob = response.data;

Then an image is uploaded to your Storage Account. In this case, blob output binding's name is outputBlob.

index.js

 function decodeBase64Image(context, data) {

 var matches = data.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);

 var response = {}

 if (matches.length !== 3) {

 context.log("Error case");

 return;

 }

 response.type = matches[1];

 response.data = Buffer.from(matches[2], 'base64');

 return response;

}

module.exports = function (context, req) {

 context.log('JavaScript HTTP trigger function processed a request.');

 context.log(req.body);

 let response = decodeBase64Image(context, req.body.data);

 context.log('filename: ' + req.name);

 context.log('filetype: ' + response.type);

 context.bindings.outputBlob = response.data;

 context.res = {

 // status: 200, /* Defaults to 200 */

 body: "Uploaded " 

 };

 context.done();

};

 

However, we have some problem. The uploaded image has a random number name. I'd like to specify the filename.

 

Specify the name binding for images

We can configure binding data runtime. However it is only for C#.  We need to come up with other strategy. We can't configure the filename from our code. Instead, we can use the feature of the Azure Functions binding feature. You can see the route property. Once we accept the FileUpload/xxxxx url, Azure Functions pass the xxxxx to {filename}. On the output bindings, you can use {filename} as well.

Let's see the function.json 

 {

 "bindings": [

 {

 "authLevel": "function",

 "type": "httpTrigger",

 "direction": "in",

 "name": "req",

 "route": "FileUploadNode/{filename}",

 "methods": [

 "post"

 ]

 },

 {

 "type": "http",

 "direction": "out",

 "name": "res"

 },

 {

 "type": "blob",

 "name": "outputBlob",

 "path": "outcontainer/{filename}",

 "connection": "carreviewstr_STORAGE",

 "direction": "out"

 }

 ],

 "disabled": false

}

Now, you can upload image from your Angular 4 apps to Azure Functions (Node.js).  I'll upload whole sample after finish the coding.

Enjoy coding with Azure Functions!

 

 

 

Resource