Creating a File Sharing and Conversion Web App with Azure Functions
Tags: azure, node, webdev, tutorial
Published: 2020-12-20 01:44:38

If you or someone you know participated in this year's AP Collegeboard Exams, you probably recognize the stress of submitting handwritten work within a small time constraint.

![https://media.giphy.com/media/3o7TKRwpns23QMNNiE/giphy.gif](https://media.giphy.com/media/3o7TKRwpns23QMNNiE/giphy.gif)

**Bunnimage** aims to help alleviate that stress for students and others working at home. It takes an image as an input on an upload page and converts it into a PDF that is available at a download page. 

### **Overview**

![https://user-images.githubusercontent.com/69332964/99191176-01198180-2739-11eb-9889-872822df6bd8.png](https://user-images.githubusercontent.com/69332964/99191176-01198180-2739-11eb-9889-872822df6bd8.png)

**In this tutorial, we'll be walking through:**

1. Creating the "Upload" page and an HTTP Trigger Function that will upload the user's image to a storage container.
2. Setting up an Event Grid Subscription and a Function that converts the image into a PDF and stores it again.
    - This is where the API will live!
3. Creating the "Download" page and an HTTP Trigger Function that retrieves the correct PDF.
4. **Optional** For those who are interested, we can add another Function to delete the files and keep our containers squeaky clean.
    - **Note**: The diagram above excludes the optional deletion feature.

You can find a sample of the final product at [my Github repository](https://github.com/emsesc/bunnimage).

### **Before we start:**

- Make sure you have an **[Azure Subscription](https://azure.microsoft.com/en-us/free/)** so we can utilize the amazing features of Microsoft Azure Functions (It's free!) 🤩
- **Register** for an account on **[Online Convert](https://www.online-convert.com/register)** (with the free version), as we will be using this API convert our images
- If you want to host your website somewhere, check out [Repl.it](https://repl.it/languages/html), or you can just have your project run [locally](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)

## Step 1: Upload the image ⬆️

### **Creating a Function App**

We're going to have a lot of triggers in this project, so let's get started by [creating a Function App](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-azure-function)! Follow those steps to create the Function App, and then create the first HTTP trigger (this will upload our image).

> **Note**: it will be helpful if you keep track of these strings for reference later in the project: Storage account name (found in "Hosting"), Function app name, Resource Group

Before we start coding the trigger, though, we need to install some `npm` packages/libraries.

Click on the "Console" tab in the left panel under "Development Tools".

![https://user-images.githubusercontent.com/69332964/99189070-59e31d00-272d-11eb-80a4-17444e5fac65.png](https://user-images.githubusercontent.com/69332964/99189070-59e31d00-272d-11eb-80a4-17444e5fac65.png)

Inside the console (shown on the right panel), type in the following commands:

`npm init -y` <br />
[`npm install parse-multipart`](https://www.npmjs.com/package/parse-multipart) <br />
[`npm install node-fetch`](https://www.npmjs.com/package/node-fetch) <br />
[`npm install @azure/storage-blob`](https://www.npmjs.com/package/@azure/storage-blob) <br />

> **Tip**: The Azure Storage Blob client library is going to be a key piece of the project. After all, it's about blobs!

### **Setting up your storage account**

This is the storage account you created when creating the Function App. If you don't know what it is, search "Storage Containers" in the query box in Azure portal.

We're going to need to create 2 containers: "images" and "pdfs." Think of these as folders in the account.

![https://user-images.githubusercontent.com/69332964/99161767-75194280-26c3-11eb-8ad1-c19d63d37bbb.png](https://user-images.githubusercontent.com/69332964/99161767-75194280-26c3-11eb-8ad1-c19d63d37bbb.png)

![https://user-images.githubusercontent.com/69332964/99161780-8cf0c680-26c3-11eb-9bfc-78dc3262b038.png](https://user-images.githubusercontent.com/69332964/99161780-8cf0c680-26c3-11eb-9bfc-78dc3262b038.png)

You will need to upgrade your storage account because Event Grid Subscriptions will only work with a v2 version. Follow this [tutorial](https://docs.microsoft.com/en-us/azure/storage/common/storage-account-upgrade?tabs=azure-portal) to upgrade it.

### **Writing our *First* Azure Function to Upload an Image**

⬇ **Some housekeeping...**

- For the function to work, we have to initialize the packages/libraries we installed in the beginning of part 1.
- Take note of the `process.env` value being assigned to `connectionstring` in the code below (*Line 3*). Use this [tutorial](https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings) to add in your own secret strings from your storage container.
    - The storage container is the one you created when you started your Function App. Navigate to it and find your secret strings here:

    ![https://user-images.githubusercontent.com/69332964/99161798-ba3d7480-26c3-11eb-8e55-eac4bd4cb174.png](https://user-images.githubusercontent.com/69332964/99161798-ba3d7480-26c3-11eb-8e55-eac4bd4cb174.png)

    ![https://user-images.githubusercontent.com/69332964/99161822-ec4ed680-26c3-11eb-8977-f12beb496c24.png](https://user-images.githubusercontent.com/69332964/99161822-ec4ed680-26c3-11eb-8977-f12beb496c24.png)

    - Keep these safe, and use the connection string in the corresponding variable in the code.
    - *Note: You'll need to store other strings in environment variables later on in the tutorial*

Let's start just by initializing a few variables we'll need.

{% gist [https://gist.github.com/emsesc/b658b23b36610084ffb7d649e5821bac](https://gist.github.com/emsesc/b658b23b36610084ffb7d649e5821bac) %}

⬇ **The main block of code**

- Notice that we are able to name the file with the user's username in line 10 by receiving it from the header.
    - Later on in the JS, we will send the username in the header of the request.
- The `parse-multipart` library is being used in lines 4-11 to parse the image from the POST request we will later make with the frontend; refer to the documentation linked above.
- Some if-else logic is used from lines 13-22 to determine the file extension.
- We then call the `uploadBlob()` function in line 24.

{% gist [https://gist.github.com/emsesc/d09a6d7c0caa1318d8e184ebf412c185](https://gist.github.com/emsesc/d09a6d7c0caa1318d8e184ebf412c185) %}

⬇ **Uploading the image blob to the "images" container**

- Notice the `uploadBlob()` function! This is what uploads the parsed image to the specified "images" blob container.
    - Here's a [YouTube Video to help explain](https://youtu.be/Qt_VXM_fml4) the handy dandy library

{% gist [https://gist.github.com/emsesc/8177c15fea34c208bc9a7fc9ea0bc585](https://gist.github.com/emsesc/8177c15fea34c208bc9a7fc9ea0bc585) %}

<br />

### **Frontend: The "upload" webpage**

Next, I created a static HTML page that will accept the image from the user and send to the Azure Function we just coded using Javascript.

**Note**: I removed unnecessary sections of my code because I wanted to make the webpage ✨*fancy*✨, but you can see the whole thing [here](https://github.com/emsesc/bunnimage/blob/main/upload.html).

{% gist [https://gist.github.com/emsesc/faaa81463826cb383110d86071ace146](https://gist.github.com/emsesc/faaa81463826cb383110d86071ace146) %}

<br />

**Above we have:**

- Input box for the username (simple but *insecure* auth system)
- Button to submit

However, a static HTML webpage can't make a request to the Azure Function itself, which is where we're going to cook up some JS. 😯

### **Frontend: Javascript for interacting with the Azure Function**

This block of Javascript updates the preview thumbnail while getting the picture, gets the username, and sends them both over to the function we just coded.

First, `loadFile()` is called when the file input changes to display the thumbnail.

```jsx
async function loadFile(event){
    console.log("Got picture!");
    var image = document.getElementById("output");
    // Get image from output 
    image.src = URL.createObjectURL(event.target.files[0])
    // load inputted image into the image src and display
}
```

Then, `handle()` is called when the file is submitted to POST the image and username. The image is sent in the body, and username is sent as a header. *Lines 15-30*

{% gist [https://gist.github.com/emsesc/b4d045380641847163399aa4fed6841e](https://gist.github.com/emsesc/b4d045380641847163399aa4fed6841e) %}

> Be sure to change the function url on Line 19 to the one of your upload image Function!

![https://user-images.githubusercontent.com/69332964/99188529-73369a00-272a-11eb-93df-04fdce5381df.png](https://user-images.githubusercontent.com/69332964/99188529-73369a00-272a-11eb-93df-04fdce5381df.png)

### **Deploy your code**

- Try doing it locally with the **live server extension** for VS Code
- Try [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/)
- I personally used [repl.it](https://repl.it/languages/html)

### **Update CORS Settings**

> This is a crucial step!! 😱 If you don't change your CORS (Cross-origin resource sharing) settings, the POST request won't work. This tells the Function App what domains can access our Azure Function.

**Options:**

- **Recommended**: Change it to a wildcard operator (`*`), which allows *all* origin domains to make requests
    - Be sure to remove any other existing inputs before attempting to save with wildcard

        ![https://user-images.githubusercontent.com/69332964/99188905-6f0b7c00-272c-11eb-8142-f91882227c78.png](https://user-images.githubusercontent.com/69332964/99188905-6f0b7c00-272c-11eb-8142-f91882227c78.png)

- Change it to the domain you are using to host your code

### **Home stretch! 🏃🏻‍♀️**

**It's finally time to test our first step that our app will make!**

1. Navigate to your HTML page and submit an image

![https://user-images.githubusercontent.com/69332964/99189240-3cfb1980-272e-11eb-8896-e959f37480b3.png](https://user-images.githubusercontent.com/69332964/99189240-3cfb1980-272e-11eb-8896-e959f37480b3.png)

![https://user-images.githubusercontent.com/69332964/99189255-53a17080-272e-11eb-9cab-a73faf504b3f.png](https://user-images.githubusercontent.com/69332964/99189255-53a17080-272e-11eb-9cab-a73faf504b3f.png)

Go to the "images" storage container and check to see if your image is there!
*Error? Check the log in your Function*

![https://user-images.githubusercontent.com/69332964/99189316-9c592980-272e-11eb-9870-dbc1f9352599.png](https://user-images.githubusercontent.com/69332964/99189316-9c592980-272e-11eb-9870-dbc1f9352599.png)

---

## Step 2: Convert The Image 🔄

### **Create another Azure Function**

Yep... We need yet *another* Azure Function. (What can I say? They're pretty helpful.) This one will trigger when **the image blob is stored**, then convert it into a PDF, and store it in the "pdfs" container.

![https://media.giphy.com/media/QBGYWFjnggIZ8fMjdt/giphy.gif](https://media.giphy.com/media/QBGYWFjnggIZ8fMjdt/giphy.gif)

However, this time, it will be an **Event Grid Trigger**, so make sure you select the right one!

![https://user-images.githubusercontent.com/69332964/99191739-a4b86100-273c-11eb-8015-9988540fc67c.png](https://user-images.githubusercontent.com/69332964/99191739-a4b86100-273c-11eb-8015-9988540fc67c.png)

> What's the difference?

> - Event Grid Triggers trigger based on an Event Grid Subscription, which we will create later in this step.
Our trigger will fire when a blob is stored in the "images" container
> - HTTP Triggers fire when a GET or POST request is made to the endpoint (function URL)

<br />

**Commercial Break** 📺

Let's recap:

- **Step 1 ✅:** We created the "Upload" page and an HTTP Trigger Function that uploaded the user's image to a storage container.
- **Step 2:** We will create an **Event Grid** function that converts the image into a PDF by calling the *Online Convert API* and will upload the PDF to blob storage.

<br />

⚠😵**WARNING**😵⚠ Lots of code ahead, but it's all good! I split it into sections.

**First off, the *Online-Convert* API!**

- We're going to need to get another secret key, except this time from the API. Here's [how to get that](https://apiv2.online-convert.com/docs/getting_started/api_key.html).
- Once again, save it in your environment variables so it's accessible.
- **Note**: This API does have restrictions on the amount of conversions during 24 hours, so just be aware that you may get an error after reaching the limit.

⬇ This `convertImage()` function does exactly what it's called: convert the image by calling the Online-Convert API. Here's some [documentation](https://apiv2.online-convert.com/docs/input_types/cloud/azure_blob_storage.html) around how to use the API with Azure Blob Storage.

```js
async function convertImage(blobName){
    const api_key = process.env['convertAPI_KEY'];
    const accountKey = process.env['accountKey'];
    const uriBase = "<https://api2.online-convert.com/jobs>";
	// env variables (similar to .gitignore/.env file) to not expose personal info
	// check out documentation 
    img = {
    "conversion": [{
        "target": "pdf"
    }],
    "input": [{
        "type": "cloud",
        "source": "azure",
        "parameters": {
            "container": "images",
            "file": blobName
        },
        "credentials": {
            "accountname": "bunnimagestorage",
            "accountkey": accountKey
        }
    }]
    }

    payload = JSON.stringify(img);

    // making the post request
    let resp = await fetch(uriBase, {
        method: 'POST',
        body: payload,
        // we want to send the image
        headers: {
            'x-oc-api-key' : api_key,
            'Content-type' : 'application/json',
            'Cache-Control' : 'no-cache'
        }
    })

    // receive the response
    let data = await resp.json();

    return data;
}

```

⬇To [check the status of the conversion](https://apiv2.online-convert.com/docs/getting_started/job_polling.html) and determine whether we can store the PDF to blob storage yet, let's use this `checkStatus()` function that makes a request to the same `https://api2.online-convert.com/jobs` endpoint, except with a GET request instead of POST.

```jsx
async function checkStatus(jobId){
    const api_key = process.env['convertAPI_KEY'];
    const uriBase = "<https://api2.online-convert.com/jobs>";
	// env variables to keep your info private!

    // making the post request
    let resp = await fetch(uriBase + "/" + jobId, {
        /*The await expression causes async function execution to pause until a Promise is settled 
        (that is, fulfilled or rejected), and to resume execution of the async function after fulfillment. 
        When resumed, the value of the await expression is that of the fulfilled Promise*/
        method: 'GET',
        headers: {
            'x-oc-api-key' : api_key,
        }
    })

    // receive the response
    let data = await resp.json();

    return data;
}
```

Then we can use the same `uploadBlob()` function from before to upload our object!

After this we get to the main section of our code.

⬇It gets the blobName, calls the functions, and downloads the PDF to be stored.

- The `blobName` is retrieved from the `EventGrid` subscription subject* in lines 10-11
- Because the API does not convert the image immediately, we need a while loop to repeatedly check for the status of the conversion in lines 21-36
- The last portion is used to [download the converted PDF](https://apiv2.online-convert.com/docs/getting_started/job_downloading.html) by sending a GET request to the URI from the completed file conversion response. *Lines 43-47*

{% gist [https://gist.github.com/emsesc/74bf9fa8958ff1030b29319ccfcc43e1](https://gist.github.com/emsesc/74bf9fa8958ff1030b29319ccfcc43e1) %}

> What's a subject? This part of the Event response contains information about what specific file caused the Azure Function to fire. For example: `/blobServices/default/containers/test-container/blobs/new-file.txt` where the file is `new-file.txt`

{% gist [https://gist.github.com/emsesc/4bf9fa8958ff1030b29319ccfcc43e1](https://gist.github.com/emsesc/4bf9fa8958ff1030b29319ccfcc43e1) %}

<br />

**Now that the long block of code is done with, let's take a look at some responses you should expect from the API.**

- [This is](https://gist.github.com/emsesc/01e30ba32a6fd84572e35f26fe8479c4) what you would get if the file is still converting 🤔
- [Here's](https://gist.github.com/emsesc/6018bafcf24f02a58a8f7f14d52ffbb8) what you would get when the conversion is complete! (yay) 🥳

**In particular, there are 3 important pieces of the output we should examine:**

1. `update.status.code`: This tells us whether its done processing or not
2. `update.output[0].uri`: This gives us the URL where we can download the PDF (used in the last GET request)
3. `result.id`: Gives the ID of the file conversion "job" so we can continually check for its status

Before we can test our code, we need one last step: the trigger!

### **Creating an Event Subscription**

When the image blob is stored in the "images" container, we want the conversion from jpg/jpeg/png to pdf to begin *immediately*!

> Just like how "subscribing" to a YouTube channel gives you notifications, we're going to subscribe to our own Blob Storage and trigger the Azure Function.

**Tip**: You'll want to keep the names for your storage account and resource group handy.

1. Search "Event Grid Subscriptions" in the search bar
2. Click "+ Event Subscription" in the top left
3. Fill in the form to create the Event Subscription:

![https://user-images.githubusercontent.com/69332964/99469463-c10cf700-2910-11eb-929e-e7feff85f203.png](https://user-images.githubusercontent.com/69332964/99469463-c10cf700-2910-11eb-929e-e7feff85f203.png)

- If it asks you for a name, feel free to put anything you want - I named it "fileUploaded"
- Under Topic Types, select "Storage Accounts"
- The "Resource Group" is the Resource Group that holds your storage account
- The "Resource" is your storage account name

**Note**: If your storage account doesn't appear, you forgot to follow the "upgrade to v2 storage" step

![https://user-images.githubusercontent.com/69332964/99469567-05989280-2911-11eb-9cf2-827a604f638e.png](https://user-images.githubusercontent.com/69332964/99469567-05989280-2911-11eb-9cf2-827a604f638e.png)

- Under Event Types: filter to **Blob Created**

![https://user-images.githubusercontent.com/69332964/99189740-aed46280-2730-11eb-8ff0-c8a7ba19aadc.png](https://user-images.githubusercontent.com/69332964/99189740-aed46280-2730-11eb-8ff0-c8a7ba19aadc.png)

- The "Endpoint Type" is "Azure Function"

![https://user-images.githubusercontent.com/69332964/99189763-d0354e80-2730-11eb-91e4-5b17fc5e63bd.png](https://user-images.githubusercontent.com/69332964/99189763-d0354e80-2730-11eb-91e4-5b17fc5e63bd.png)

- The "Function" is the function we want triggered when an image is uploaded, so the `convertImage` function
1. Tweaking some settings...
- Navigate to the "Filters" tab and "Enable Subject Filtering"

    ![https://user-images.githubusercontent.com/69332964/99189929-bd6f4980-2731-11eb-9b01-b0cef972b96a.png](https://user-images.githubusercontent.com/69332964/99189929-bd6f4980-2731-11eb-9b01-b0cef972b96a.png)

- Change the "Subject Begins With" to `/blobServices/default/containers/images/blobs/`
    - This way, the subscription will **not** trigger when a PDF is stored in the "pdfs" container. It will **only** trigger when something is stored in "images."

> Congratulations! You have now subscribed to the "blob created" event in your "images" container that triggers the convert image function!

### **Upload a converted PDF to the "pdfs" container!**

Now that we've connected our Functions and frontend together with an Event Grid Subscription, try submitting another image to check if it successfully uploads as a PDF into the "pdfs" container.

> If you used my code and have the same context.log()s, you should get something like this when the PDF uploads:

![https://user-images.githubusercontent.com/69332964/99191696-50ad7c80-273c-11eb-947e-5e9a9962ddb0.png](https://user-images.githubusercontent.com/69332964/99191696-50ad7c80-273c-11eb-947e-5e9a9962ddb0.png)

---

## Step 3: Downloading the PDF on the HTML page ⬇

Now that we have a PDF stored in the "pdfs" container, how will we get the PDF back to the user? **You got it right, yet *another* Azure Function**!

Create another HTTP Trigger - this one will return the PDF download URL to the frontend when triggered.

![https://media.giphy.com/media/MYx9qA5yTknqgb4abz/giphy.gif](https://media.giphy.com/media/MYx9qA5yTknqgb4abz/giphy.gif)

**Commercial Break** 📺

Let's recap:

- **Step 1 ✅:** We created the "Upload" page and an HTTP Trigger Function that uploaded the user's image to a storage container.
- **Step 2 ✅:** We will create an **Event Grid** function that converts the image into a PDF by calling the *Online Convert API* and will upload the PDF to blob storage.
- **Step 3:** We will create a HTTP Trigger function that returns the PDF to the user when triggered by the "Download" page.
- **Step 4:** ***Optional*** If you choose, create another HTTP Trigger function and modify other code to delete the image and PDF blobs from storage containers once they are unneeded.

### **Azure Functions: Check if the PDF is ready to be served 🍝**

⬇First, it receives the username to get the correct PDF from the header of the request, which is made by the webpage. You will see this request later on in the JS of this step.

```jsx
var fetch = require("node-fetch");
module.exports = async function (context, req, inputBlob) {
    context.log('JavaScript HTTP trigger function processed a request.');

    var username = req.headers['username'];
    var download = "<https://bunnimagestorage.blob.core.windows.net/pdfs/>" + username + ".pdf";

```

⬇Then, using the personalized URL, it performs a GET request to check if the PDF has been stored in the "pdfs" container.

```jsx
    let resp = await fetch(download, {
        method: 'GET',
    })
    let data = await resp;
    if (data.statusText == "The specified blob does not exist.") {
        success = false;
        context.log("Does not exist: " + data)
    } else {
        success = true;
        context.log("Does exist: " + data)
    }

```

⬇The function then returns the URL for downloading the PDF and whether or not the PDF is ready for download to the webpage.

```jsx
    context.res = {
            body: {
                    "downloadUri" : download,
                    "success": success,
            }
    };
    // receive the response

    context.log(download);
    context.log(data)
    context.done();
}

```

### **Frontend: Creating the Download HTML page**

Once again, the "fancy" stuff is omitted.

{% gist [https://gist.github.com/emsesc/f727674180de797ba7c55ebd4124eef0](https://gist.github.com/emsesc/f727674180de797ba7c55ebd4124eef0) %}

<br />

⬆**Like we created the "upload" page in Step 1, we now need a "download" page for users to receive the PDF.**

This piece of code creates:

- An input for the username *Line 6*
- One button for refreshing to check if the PDF is ready *Line 8*
- One button for downloading the file *Line 9*

### **Frontend: Downloading the PDF on the Webpage**

Time to get bombarded with some *lovely* JS!

**Part 1** ⬇:

- Change the HTML on lines 2-4 to display the current status (whether it's looking for the PDF, whether it's ready for download, etc.)
- Make a request on lines 9-16 to the HTTP Trigger Function we just coded, sending the username inputted on the HTML page along with it

{% gist [https://gist.github.com/emsesc/39f52ee0928f19f709324a1427b9b89f](https://gist.github.com/emsesc/39f52ee0928f19f709324a1427b9b89f) %}

<br />

**Part 2** ⬇:

- First we're going to find the link to download the PDF with `data.downloadUri` on *line 1*
- Change buttons from "Refresh" to "Download" when PDF is ready for download
    - **How to do this?** Remove the "Refresh" button *Lines 10-11* and make "Download" visible *Line 9*
- Set the `onclick` attribute of the "Download" button to call the `getPdf()` function with the unique username + link for download. *Line 8*
    - The `getPdf()` function allows for immediate download with `window.open(link)` *Lines 16-19*

{% gist [https://gist.github.com/emsesc/3eeb6e52e5f8598f226a62e9e809647a](https://gist.github.com/emsesc/3eeb6e52e5f8598f226a62e9e809647a) %}

<br />

## **Amazing! You're done!**

Here's the finished product in which I download the cute bunny shopping picture I uploaded earlier.

![https://user-images.githubusercontent.com/69332964/99192741-95d4ad00-2742-11eb-8b77-f0c9e6d159d7.png](https://user-images.githubusercontent.com/69332964/99192741-95d4ad00-2742-11eb-8b77-f0c9e6d159d7.png)

![https://user-images.githubusercontent.com/69332964/99192756-b00e8b00-2742-11eb-9fea-dc64a9083c63.png](https://user-images.githubusercontent.com/69332964/99192756-b00e8b00-2742-11eb-9fea-dc64a9083c63.png)

![https://user-images.githubusercontent.com/69332964/99192766-bbfa4d00-2742-11eb-8371-630af1b21778.png](https://user-images.githubusercontent.com/69332964/99192766-bbfa4d00-2742-11eb-8371-630af1b21778.png)

Congratulations! I hope this knowledge of Azure Functions helps you create even more fun apps!

If you're interested in augmenting this app, try using your new knowledge of Blob Storage, HTTP Triggers, the Node SDK (@azure/storage-blob), and some [Stack Overflow](https://stackoverflow.com/questions/60716837/how-to-delete-a-blob-from-azure-blob-v12-sdk-for-node-js) to assist you to add a feature to delete the image and PDF blobs.