SharePoint Online PnPProvisionning include document library file contents and file Versions !

Last year I wrote an article about using the PnP Provisioning engine to add references to documents in the Template file generated.

A couple of days a friend on linked in asked me if it were possible to include also all the documents versions in the Template file generated. So though about it and came up with a solution.

Basically the idea is of course to get all the versions of the document then download them and inject their reference in the Template pnp provisioning file.

First you have to know that when you get a reference to a file with

$folder = Get-PnPFolder -RelativeUrl $folderUrl
For ($i = 0; $i -lt $total; $i++) {
        $file = $folder.Files[$i]

The $file.Versions will return an error as the collection has not being initialised. This is because the collection property has not been specified. So in order to load the .Version collection you have to specifically load it with

$total = $folder.Files.Count
$ctx = Get-PnPContext
	
For ($i = 0; $i -lt $total; $i++) {
        $file = $folder.Files[$i]
        
        $ctx.load($file.Versions)
        $ctx.ExecuteQuery()

Now we can iterate through the Version collection and get the Url property to download the file.
Again I am not sure why if I used the Get-PnpFile -Url $file.Url it was returning me an error 404 not found so I used the WebClient object to download the file with

$webClient = New-Object System.Net.WebClient 
$webClient.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($userName, $sPassword)
$webClient.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f")

$webclient.DownloadFile($ServerFileLocation,$DownloadPath)

If we put it all together my ProcessFolder function becomes

function ProcessFolder($folderUrl, $destinationFolder) {
	Write-Output "Folder URL " $folderUrl  " destinationFolder " $destinationFolder
    $folder = Get-PnPFolder -RelativeUrl $folderUrl
    $tempfiles = Get-PnPProperty -ClientObject $folder -Property Files
   
    if (!(Test-Path -path $destinationfolder )) {
        $dest = New-Item $destinationfolder -type directory 
    }

    $total = $folder.Files.Count
	$ctx = Get-PnPContext
	
    For ($i = 0; $i -lt $total; $i++) {
        $file = $folder.Files[$i]
        
		$ctx.load($file.Versions)
        $ctx.ExecuteQuery()

		foreach($version in $file.Versions)
		{
			$filesplit = $file.Name.split(".") 
			$fullname = $filesplit[0] 
			$fileext = $filesplit[1] 
			$FullFileName = $fullname+"\"+$version.VersionLabel+"\"+$file.Name         

			$fileURL = $destination+"/"+$version.Url


			$DownloadPath = $FullFileName

			if (!(Test-Path ($destinationfolder + "\" + $fullname + "\" + $version.VersionLabel)))
			{
				New-Item ($destinationfolder + "\" + $fullname + "\" + $version.VersionLabel) -type directory -Force
			}

			HTTPDownloadFile "$fileURL" ($destinationfolder + "\" + $fullname + "\" + $version.VersionLabel + "\" + $file.Name)
			
			$versionSourceFolder =  "./" + $siteTitle + "/" + $folder.Name + "/" + $fullname + "/" + $version.VersionLabel + "/" + $file.Name
			Add-PnPFileToProvisioningTemplate -Path ($saveDir + "Template.xml") -Source $versionSourceFolder -Folder $folderUrl -FileLevel Published
		}
		
        Get-PnPFile -ServerRelativeUrl $file.ServerRelativeUrl -Path $destinationfolder -FileName $file.Name -AsFile -Force	

		Add-PnPFileToProvisioningTemplate -Path ($saveDir + "Template.xml") -Source ($destinationfolder + "\" + $file.Name) -Folder $folderUrl -FileLevel Published
		
    }
	
}

with the HTTPDownloadFile function

function HTTPDownloadFile($ServerFileLocation, $DownloadPath)
{
	$userName = "LOGIN_NAME"
	$password = "PASSWORD"

	#create secure password
	$sPassword = $password | ConvertTo-SecureString -AsPlainText -Force

	$webClient = New-Object System.Net.WebClient 
	$webClient.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($userName, $sPassword)
	$webClient.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f")

    $webclient.DownloadFile($ServerFileLocation,$DownloadPath)
}

The complete script is available on github at https://github.com/alaabitar/provisioning/blob/master/scriptUpgraded.ps1

Create users for Office 365 Dev Program

I am sharing a small PowerShell script to create at glance Office 365 users in Azure AD and assign them the E3 developer license.

$license = New-Object -TypeName Microsoft.Open.AzureAD.Model.AssignedLicense
$licenses = New-Object -TypeName Microsoft.Open.AzureAD.Model.AssignedLicenses

$license.SkuId = (Get-AzureADSubscribedSku | Where-Object -Property SkuPartNumber -Value "DEVELOPERPACK" -EQ).SkuID

$licenses.AddLicenses = $license

$PasswordProfile=New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile
$PasswordProfile.Password="YOUR_DEFAULT_PASSWORD"

Import-Csv .\users.csv | foreach-object {
	$usernameSplit = $_.UserName.Split("@")
	$usernameSplit[0]
	$usernameSplit[1]
	$userDisplayNameSplit = $usernameSplit[0].Split(".")
	$userDisplayNameSplit[0]
	$userDisplayNameSplit[1]
	$mailNickName = ($usernameSplit[0])
	$userprinicpalname = $usernameSplit[0] + "@YOUR_DEV_TENANT_NAME.onmicrosoft.com"
	$displayNanme = ($usernameSplit[0] + " " + $usernameSplit[1])
	
	New-AzureADUser -DisplayName $displayNanme -GivenName $userDisplayNameSplit[0] -SurName $userDisplayNameSplit[1] -UserPrincipalName $userprinicpalname -PasswordProfile $PasswordProfile -mailNickName $mailNickName -UsageLocation CH -AccountEnable $true
	# Call the Set-AzureADUserLicense cmdlet to set the license.
	Set-AzureADUserLicense -ObjectId $userprinicpalname -AssignedLicenses $licenses
}

The cvs file is just one column name UserName containing email in the form of 
FIRSTNAME.LASTNAME@company.com

If you have emails in other format you can just modify the lines before the New-AzureADUser command.

Happy coding !

Business card reader with PowerApps

AI Builder (still in preview) for PowerApps comes with a control called Business card reader and it does just its name is :)

The control exposes several properties from Company Name to Job Title, Phones etc... so basically you just click on the control select take a picture of load a picture from your gallery or hard drive and you just relax and let the control do what it does best. It takes a couples of seconds to load the different properties.

I have tested it with several different kind of business cards I can say that I had an 80% of success.

I have upload a small PowerApps app available at https://github.com/alaabitar/powerapps/blob/master/BusinessCardReader_20190625085751.zip

It reads the information from the Business card reader control and a Save button uploads the contact to a SharePoint lists named Contacts.

Here is the PnP PowerShell script to create the Contacts list :

Connect-PnPOnline -url URL_OF_YOUR_SITE

New-PnPList -Title "Contacts" -Template GenericList

Add-PnPField -List "Contacts" -DisplayName "CompanyName" -InternalName "CompanyName" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "Department" -InternalName "Department" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "Email" -InternalName "Email" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "FirstName" -InternalName "FirstName" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "FullAddress" -InternalName "FullAddress" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "FullName" -InternalName "FullName" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "JobTitle" -InternalName "JobTitle" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "LastName" -InternalName "LastName" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "Phone1" -InternalName "Phone1" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "Phone2" -InternalName "Phone2" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "Phone3" -InternalName "Phone3" -Type Text -AddToDefaultView
Add-PnPField -List "Contacts" -DisplayName "Website" -InternalName "Website" -Type Text -AddToDefaultView

On save my primary key for a Contact is its email address so the OnSelect of my Save Button is :

Set(
    _CurrentContact,
    LookUp(
        Contacts,
        Email = TextInput1_2.Text
    )
);
If(
    IsBlank(_CurrentContact),
    Patch(Contacts,Defaults(Contacts),
        {
            Title: "Contact saved with Scan business card",
            CompanyName: TextInput1.Text,
            Department: TextInput1_1.Text,
            Email: TextInput1_2.Text,
            FirstName: TextInput1_3.Text,
            FullAddress: TextInput1_4.Text,
            FullName: TextInput1_5.Text,
            JobTitle: TextInput1_6.Text,
            LastName: TextInput1_7.Text,
            Phone1: TextInput1_8.Text,
            Phone2: TextInput1_9.Text,
            Phone3: TextInput1_10.Text,
            Website: TextInput1_11.Text
        }
    ),
Patch(Contacts,
    _CurrentContact,
    {
        Title: "Contact updated with Scan business card",
        CompanyName: TextInput1.Text,
        Department: TextInput1_1.Text,
        Email: TextInput1_2.Text,
        FirstName: TextInput1_3.Text,
        FullAddress: TextInput1_4.Text,
        FullName: TextInput1_5.Text,
        JobTitle: TextInput1_6.Text,
        LastName: TextInput1_7.Text,
        Phone1: TextInput1_8.Text,
        Phone2: TextInput1_9.Text,
        Phone3: TextInput1_10.Text,
        Website: TextInput1_11.Text
    }
)
);
Notify("Contact " & _CurrentContact.FirstName & " " & _CurrentContact.LastName & " saved")

I know I have been lazy I did not rename the TextInput control but hey why don't you do better than me :)

Tell me what do you think about this new great feature!