Taking a picture with PowerApps and sending to SharePoint with help of Azure Functions

Sometimes, after having written a selfie app in Silverlight (SharePoint 2010), JavaScript (SharePoint 2013), even an Add-in (SharePoint Online), you want to do it again with PowerApps.  This is that article.  I think it's really fun.  And I think it's funny I'm solving the world's problems one AzureFunction at a time.  And I think I need help.
 

Update - new method

A little over a day after I published this post, I was exploring Logic Apps Workflow Definition Language Functions and came across dataUriToBinary.  So I wrote an update of this process without using AzureFunctions, instead, using an advanced formula to invoke dataUriToBinary function.
The new post Taking a picture with PowerApps and sending to SharePoint with just Flow will be more suitable for PowerUsers. 
For developers or those wanting to extend functionality with AzureFunctions, read on.

 

This is a post of several things, so I wanted to put out an outline and you can follow my thinking:

  • The PowerApps App:
    • PowerApps - Camera input, toggle, gallery, and sending picture to a connection
    • Flow - Upload a file to SharePoint template, what it does, and doesn't do.
    • The hard way - write a crazy Azure API App that looks way too complicated.
    • The easy way - just hammer it with Azure Functions
    • Pictures to proof it (this is a selfie-referencing pun)
  • Notes on the Future of PowerApps
  • Notes on debugging Flow actions
  • Notes on Functions with PowerShell and Functions with C# (GA languages)

The PowerApp

Start with PowerApps - you need a Camera Input

You also need a toggle input - and tie the toggle's check/uncheck values to 0 and 1, then set that to the Camera.Camera property.  This is necessary when you use PowerApps on the phone, and you want to toggle between front and rear cameras.

OnSelect of the camera - collection the current Photo into a collection "mypics"

 

The Camera has one property "Url" but the Url is actually a Data URI and it looks like

data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAA
CNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwA
AAABJRU5ErkJggg==

In the PowerApps desktop app, I have also seen the data to be

blob:xxxx-xxxx-xxxx-xxxx

I'm not sure if this is just the Desktop App bug, or if I need to save the collection first.  But for Web and Mobile, the data url is never blob:xxx

We'll come back in a moment to add the gallery and the connection.  We need to go over to Flow now.

 

The Flow

You can go to Flow from the backstage.

Create a Flow from a template: "Upload a file to SharePoint from PowerApps"

But there is a problem - PowerApps' FileContent is a base64 DataUrl string.

SharePoint wants the binary format.  Not a string format.  If you connect this up - it would only work for text files, it would not work for image binary files.

 

The Hard Way

PowerApps blog has an article on how to write your own Custom API to handle data uploads, including handling multi-part/form-data

https://powerapps.microsoft.com/en-us/blog/custom-api-for-image-upload/

I think you should read this, just to appreciate how simple the AzureFunctions approach is.

 

The easy way

Make an Azure Function with HTTP Trigger.

This function has to be a C# function (you can write NodeJS too). 

using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    log.Info("C# HTTP trigger function processed a request.");

    string body = await req.Content.ReadAsStringAsync();  // read image/jpeg;base64
    body = body.Replace("data:image/jpeg;base64,", "");   // remove the header
    var bytes = Convert.FromBase64String(body);           // convert data to bytes
    
    var response = new HttpResponseMessage()
    {
        Content = new ByteArrayContent(bytes),            // write binary out
        StatusCode = HttpStatusCode.OK,
    };
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
    return response;
}

All it does is convert format.  Save this - copy the Function Trigger URL.

 

Complete the App

Go back and Update your Flow

I named the flow "PowerApps > Function > SharePoint".  In hindsight, you should not do this as you'll see next step the connection name is pretty ugly.

Go back to PowerApps and set up the gallery and connection.

Add a gallery control and bind it to mypics collection.

Select the Image control inside the gallery - in the Ribbon, select "Flows" and it'll let you choose the Flow "  The Flow button is only available in the Desktop App.  Not in the web app.  I think this will come to all versions soon.

Lets you select any Flows you have created and it will create the PowerApps connection.

'PowerApps>Function>SharePoint'.Run(Text(Now(),"[$-en-US]yyyy-mm-dd-hh-mm-ss")&".jpg", ThisItem.Url)

I call my connection and pass two arguments through: a filename made of the current date/time.  And the Data URL of the current gallery item.

 

Pics to Prove it

Publish everything:  

Run it in the web, mobile…

Tab the gallery - see the connection uploads

Azure Functions returns OK

BTW - 1ms
At 400,000 Gbytes/second, I can run this function 400,000,000 times before I have to pay anything.

Flow returns OK

SharePoint is also OK

 

Notes on the Future of PowerApps

I loved and used InfoPath for years, but I tackled all its missing features and quirks as a developer.  I firmly believes that PowerApps need to be successful without needing a developer.  I also think that Flow will be updated to handle scenarios like converting image data natively.  This really isn't something we should need a developer to do.

So I'm hopeful, and looking forward to PowerApps and Flow updates - which are frequent and delightful.  (Offline support!)

In the meantime, I am fearless as AzureFunctions provides me the means to go beyond what Flow/PowerApps can do on their own.  It's extremely simple to add custom functionality.

 

Notes on debugging Flow Actions

https://flow.microsoft.com/en-us/galleries/public/templates/1f4a2b1b99e64e8dbb81b7c47cc5bb11/upload-a-file-to-sharepoint-from-powerapps/

https://flow.microsoft.com/en-us/services/shared_sharepointonline/sharepoint/

I am not very happy with these pages.  It tells me nothing about the input/output of various actions.  I have no idea what PowerApps action outputs and I have no idea what SharePoint create file needs as input.

So I build a dummy Flow, and I solve my Flow problems with Azure Function.

Copies one file from one folder to another, and send it past my function so I can look at the contents along the way.

I shouldn't have to do this.  But anyway, this little function helped me work out what I need to send to SharePoint Action.

 

Notes on Functions with PowerShell and Functions with C# (GA languages)

I was unable to have PowerShell AzureFunction return the binary I need to continue in Flow.

This may be a limitation that PowerShell can only output text encoding file outputs.

I was able to convert the string to image and then perform Add-PnPFile directly to send it to SharePoint.  But that means the Function is the end of the Flow process, and we don't have the choice of using the converted image in other Flow actions.

Import-Module "D:\home\site\wwwroot\modules\SharePointPnPPowerShellOnline.psd1" -Global;

# POST method: $req
$requestBody = Get-Content $req -raw -encoding UTF8
"$req_query_name $requestBody" 

$filename = $req_query_name
$requestBody = $requestBody -replace 'data:image/jpeg;base64,', ''
$requestBody

$bytes = [Convert]::FromBase64String($requestBody)

#test to see - the picture is OK
[IO.File]::WriteAllBytes('D:\home\site\wwwroot\demo-upload-file\test.jpg', $bytes)

#write to $res output didn't work
#[IO.File]::WriteAllBytes($res, $bytes)

#can't write encoding binary
#Out-File -Encoding UTF8 -FilePath $res -inputObject $requestBody

$memoryStream = New-Object System.IO.MemoryStream (,$bytes)

$username = "service@johnliu365.onmicrosoft.com";
$password = $env:PW;
$siteUrl = "https://johnliu365.sharepoint.com"
$secpasswd = ConvertTo-SecureString $password -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential ($username, $secpasswd)

"try connect to $siteUrl"
Connect-PnPOnline -url $siteUrl -Credentials $creds 

# OK
Add-PnPFile -FileName $filename -Folder "Shared Documents" -Stream $memoryStream

It also took 10 seconds.  But if you like PowerShell and don't really want to return the image to Flow, then this would work.

 

Try it yourself, let me know what you think

Since I posted pictures of my activation code - try it yourself.  It's a pass through http trigger that returns binaries.  You should be able to use it in your Flow without even have to create your Azure Function.

You definitely should let me know if you are testing this with my URL.  Also, you'll just have to trust me that I'm not storing your pictures.  I don't really want to pay blob storage for it.  But you really don't know.

(you shouldn't rely on my URL for production - if I change URL you'll be doomed.  If this approach works for you - you should deploy your own function, but I'd also consider waiting and see if PowerApps and Flow gets updated soon, I'm sure this kind of simple stuff is coming - lots of people are asking about it).

Go build more PowerApps.