OneDrive.psm1

function Get-ODAuthentication
{
    <#
    .DESCRIPTION
    Connect to OneDrive for authentication with a given client id (get your free client id on https://apps.dev.microsoft.com) For a step-by-step guide: https://github.com/MarcelMeurer/PowerShellGallery-OneDrive
    .PARAMETER ClientId
    ClientId of your "app" from https://apps.dev.microsoft.com
    .PARAMETER AppKey
    The client secret for your OneDrive "app". If AppKey is set the authentication mode is "code." Code authentication returns a refresh token to refresh your authentication token unattended.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .PARAMETER Scope
    Comma-separated string defining the authentication scope (https://dev.onedrive.com/auth/msa_oauth.htm). Default: "onedrive.readwrite,offline_access". Not needed for OneDrive 4 Business access.
    .PARAMETER RefreshToken
    Refreshes the authentication token unattended with this refresh token.
    .PARAMETER AutoAccept
    In token mode the accept button in the web form is pressed automatically.
    .PARAMETER RedirectURI
    Code authentication requires a correct URI. Use the same as in the app registration e.g. http://localhost/logon. Default is https://login.live.com/oauth20_desktop.srf. Don't use this parameter for token-based authentication.
 
    .EXAMPLE
    $Authentication=Get-ODAuthentication -ClientId "0000000012345678"
    $AuthToken=$Authentication.access_token
    Connect to OneDrive for authentication and save the token to $AuthToken
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$ClientId = "unknown",
        [string]$Scope = "onedrive.readwrite,offline_access",
        [string]$RedirectURI ="https://login.live.com/oauth20_desktop.srf",
        [string]$AppKey="",
        [string]$RefreshToken="",
        [string]$ResourceId="",
        [switch]$DontShowLoginScreen=$false,
        [switch]$AutoAccept,
        [switch]$LogOut
    )
    $optResourceId=""
    $optOauthVersion="/v2.0"
    if ($ResourceId -ne "")
    {
        write-debug("Running in OneDrive 4 Business mode")
        $optResourceId="&resource=$ResourceId"
        $optOauthVersion=""
    }
    $Authentication=""
    if ($AppKey -eq "")
    { 
        $Type="token"
    } else 
    { 
        $Type="code"
    }
    
    if ($RefreshToken -ne "")
    {
        write-debug("A refresh token is given. Try to refresh it in code mode.")
        $body="client_id=$ClientId&redirect_URI=$RedirectURI&client_secret=$([uri]::EscapeDataString($AppKey))&refresh_token="+$RefreshToken+"&grant_type=refresh_token"
        if ($ResourceId -ne "")
        {
            # OD4B
            $webRequest=Invoke-WebRequest -Method POST -Uri "https://login.microsoftonline.com/common/oauth2$optOauthVersion/token" -ContentType "application/x-www-form-urlencoded" -Body $Body -UseBasicParsing
        } else {
            # OD private
            $webRequest=Invoke-WebRequest -Method POST -Uri "https://login.live.com/oauth20_token.srf" -ContentType "application/x-www-form-urlencoded" -Body $Body -UseBasicParsing
        }
        $Authentication = $webRequest.Content |   ConvertFrom-Json
    } else
    {
        write-debug("Authentication mode: " +$Type)
        [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | out-null
        [Reflection.Assembly]::LoadWithPartialName("System.Drawing") | out-null
        [Reflection.Assembly]::LoadWithPartialName("System.Web") | out-null
        if ($Logout)
        {
            $URIGetAccessToken="https://login.live.com/logout.srf"
        }
        else
        {
            if ($ResourceId -ne "")
            {
                # OD4B
                $URIGetAccessToken="https://login.microsoftonline.com/common/oauth2/authorize?response_type=code&client_id=$ClientId&redirect_URI=$RedirectURI"
            }
            else
            {
                # OD private
                $URIGetAccessToken="https://login.live.com/oauth20_authorize.srf?client_id="+$ClientId+"&scope="+$Scope+"&response_type="+$Type+"&redirect_URI="+$RedirectURI
            }
        }
        $form = New-Object Windows.Forms.Form
        $form.text = "Authenticate to OneDrive"
        $form.size = New-Object Drawing.size @(700,600)
        $form.Width = 675
        $form.Height = 750
        $web=New-object System.Windows.Forms.WebBrowser
        $web.IsWebBrowserContextMenuEnabled = $true
        $web.Width = 600
        $web.Height = 700
        $web.Location = "25, 25"
        $web.navigate($URIGetAccessToken)
        $DocComplete  = {
            if ($web.Url.AbsoluteUri -match "access_token=|error|code=|logout") {$form.Close() }
            if ($web.DocumentText -like '*ucaccept*') {
                if ($AutoAccept) {$web.Document.GetElementById("idBtn_Accept").InvokeMember("click")}
            }
        }
        $web.Add_DocumentCompleted($DocComplete)
        $form.Controls.Add($web)
        if ($DontShowLoginScreen)
        {
            write-debug("Logon screen suppressed by flag -DontShowLoginScreen")
            $form.Opacity = 0.0;
        }
        $form.showdialog() | out-null
        # Build object from last URI (which should contains the token)
        $ReturnURI=($web.Url).ToString().Replace("#","&")
        if ($LogOut) {return "Logout"}
        if ($Type -eq "code")
        {
            write-debug("Getting code to redeem token")
            $Authentication = New-Object PSObject
            ForEach ($element in $ReturnURI.Split("?")[1].Split("&")) 
            {
                $Authentication | add-member Noteproperty $element.split("=")[0] $element.split("=")[1]
            }
            if ($Authentication.code)
            {
                $body="client_id=$ClientId&redirect_URI=$RedirectURI&client_secret=$([uri]::EscapeDataString($AppKey))&code="+$Authentication.code+"&grant_type=authorization_code"+$optResourceId+"&scope="+$Scope
            if ($ResourceId -ne "")
            {
                # OD4B
                $webRequest=Invoke-WebRequest -Method POST -Uri "https://login.microsoftonline.com/common/oauth2$optOauthVersion/token" -ContentType "application/x-www-form-urlencoded" -Body $Body -UseBasicParsing
            } else {
                # OD private
                $webRequest=Invoke-WebRequest -Method POST -Uri "https://login.live.com/oauth20_token.srf" -ContentType "application/x-www-form-urlencoded" -Body $Body -UseBasicParsing
            }
            $Authentication = $webRequest.Content |   ConvertFrom-Json
            } else
            {
                write-error("Cannot get authentication code. Error: "+$ReturnURI)
            }
        } else
        {
            $Authentication = New-Object PSObject
            ForEach ($element in $ReturnURI.Split("?")[1].Split("&")) 
            {
                $Authentication | add-member Noteproperty $element.split("=")[0] $element.split("=")[1]
            }
            if ($Authentication.PSobject.Properties.name -match "expires_in")
            {
                $Authentication | add-member Noteproperty "expires" ([System.DateTime]::Now.AddSeconds($Authentication.expires_in))
            }
        }
    }
    if (!($Authentication.PSobject.Properties.name -match "expires_in"))
    {
        write-warning("There is maybe an errror, because there is no access_token!")
    }
    return $Authentication 
}

function Get-ODRootUri 
{
    PARAM(
        [String]$ResourceId=""
    )
    if ($ResourceId -ne "")
    {
        return $ResourceId+"_api/v2.0"
    }
    else
    {
        return "https://api.onedrive.com/v1.0"
    }
}

function Get-ODWebContent 
{
    <#
    .DESCRIPTION
    Internal function to interact with the OneDrive API
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .PARAMETER rURI
    Relative path to the API.
    .PARAMETER Method
    Webrequest method like PUT, GET, ...
    .PARAMETER Body
    Payload of a webrequest.
    .PARAMETER BinaryMode
    Do not convert response to JSON.
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId="",
        [string]$rURI = "",
        [ValidateSet("PUT","GET","POST","PATCH","DELETE")] 
        [String]$Method="GET",
        [String]$Body,
        [switch]$BinaryMode
    )
    if ($Body -eq "") 
    {
        $xBody=$null
    } else
    {
        $xBody=$Body
    }
    $ODRootURI=Get-ODRootUri -ResourceId $ResourceId
    try {
        $webRequest=Invoke-WebRequest -Method $Method -Uri ($ODRootURI+$rURI) -Header @{ Authorization = "BEARER "+$AccessToken} -ContentType "application/json" -Body $xBody -UseBasicParsing -ErrorAction SilentlyContinue
    } 
    catch
    {
        write-error("Cannot access the api. Webrequest return code is: "+$_.Exception.Response.StatusCode+"`n"+$_.Exception.Response.StatusDescription)
        break
    }
    switch ($webRequest.StatusCode) 
    { 
        200 
        {
            if (!$BinaryMode) {$responseObject = ConvertFrom-Json $webRequest.Content}
            return $responseObject
        } 
        201 
        {
            write-debug("Success: "+$webRequest.StatusCode+" - "+$webRequest.StatusDescription)
            if (!$BinaryMode) {$responseObject = ConvertFrom-Json $webRequest.Content}
            return $responseObject
        } 
        204 
        {
            write-debug("Success: "+$webRequest.StatusCode+" - "+$webRequest.StatusDescription+" (item deleted)")
            $responseObject = "0"
            return $responseObject
        } 
        default {write-warning("Cannot access the api. Webrequest return code is: "+$webRequest.StatusCode+"`n"+$webRequest.StatusDescription)}
    }
}

function Get-ODDrives
{
    <#
    .DESCRIPTION
    Get user's drives.
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .EXAMPLE
    Get-ODDrives -AccessToken $AuthToken
    List all OneDrives available for your account (there is normally only one).
    .NOTES
    The application for OneDrive 4 Business needs "Read items in all site collections" on application level (API: Office 365 SharePoint Online)
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId=""
    )
    $ResponseObject=Get-ODWebContent -AccessToken $AccessToken -ResourceId $ResourceId -Method GET -rURI "/drives" 
    return $ResponseObject.Value
}

function Get-ODSharedItems
{
    <#
    .DESCRIPTION
    Get items shared with the user
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .EXAMPLE
    Get-ODDrives -AccessToken $AuthToken
    List all OneDrives available for your account (there is normally only one).
    .NOTES
    The application for OneDrive 4 Business needs "Read items in all site collections" on application level (API: Office 365 SharePoint Online)
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId=""
    )
    $ResponseObject=Get-ODWebContent -AccessToken $AccessToken -ResourceId $ResourceId -Method GET -rURI "/drive/oneDrive.sharedWithMe"
    return $ResponseObject.Value
}

function Format-ODPathorIdStringV2
{
    <#
    .DESCRIPTION
    Formats a given path like '/myFolder/mySubfolder/myFile' into an expected URI format
    .PARAMETER Path
    Specifies the path of an element. If it is not given, the path is "/"
    .PARAMETER ElementId
    Specifies the id of an element. If Path and ElementId are given, the ElementId is used with a warning
    .PARAMETER DriveId
    Specifies the OneDrive drive id. If not set, the default drive is used
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [string]$Path="",
        [string]$DriveId="",
        [string]$ElementId=""
    )
    if (!$ElementId -eq "")
    {
        # Use ElementId parameters
        if (!$Path -eq "") {write-debug("Warning: Path and ElementId parameters are set. Only ElementId is used!")}
        $drive="/drive"
        if ($DriveId -ne "") 
        {    
            # Named drive
            $drive="/drives/"+$DriveId
        }
        return $drive+"/items/"+$ElementId
    }
    else
    {
        # Use Path parameter
        # replace some special characters
        $Path = ((((($Path -replace '%', '%25') -replace ' ', ' ') -replace '=', '%3d') -replace '\+', '%2b') -replace '&', '%26') -replace '#', '%23'
        # remove substring starts with "?"
        if ($Path.Contains("?")) {$Path=$Path.Substring(1,$Path.indexof("?")-1)}
        # replace "\" with "/"
        $Path=$Path.Replace("\","/")
        # filter possible string at the end "/children" (case insensitive)
        $Path=$Path+"/"
        $Path=$Path -replace "/children/",""
        # encoding of URL parts
        $tmpString=""
        foreach ($Sub in $Path.Split("/")) {$tmpString+=$Sub+"/"}
        $Path=$tmpString
        # remove last "/" if exist
        $Path=$Path.TrimEnd("/")
        # insert drive part of URL
        if ($DriveId -eq "") 
        {    
            # Default drive
            $Path="/drive/root:"+$Path+""
        }
        else
        {
            # Named drive
            $Path="/drives/"+$DriveId+"/root:"+$Path+":"
        }
        return ($Path).replace("root::","root:")
    }
}

function Format-ODPathorIdString
{
    <#
    .DESCRIPTION
    Formats a given path like '/myFolder/mySubfolder/myFile' into an expected URI format
    .PARAMETER Path
    Specifies the path of an element. If it is not given, the path is "/"
    .PARAMETER ElementId
    Specifies the id of an element. If Path and ElementId are given, the ElementId is used with a warning
    .PARAMETER DriveId
    Specifies the OneDrive drive id. If not set, the default drive is used
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [string]$Path="",
        [string]$DriveId="",
        [string]$ElementId=""
    )
    if (!$ElementId -eq "")
    {
        # Use ElementId parameters
        if (!$Path -eq "") {write-debug("Warning: Path and ElementId parameters are set. Only ElementId is used!")}
        $drive="/drive"
        if ($DriveId -ne "") 
        {    
            # Named drive
            $drive="/drives/"+$DriveId
        }
        return $drive+"/items/"+$ElementId
    }
    else
    {
        # Use Path parameter
        # replace some special characters
        $Path = ((((($Path -replace '%', '%25') -replace ' ', ' ') -replace '=', '%3d') -replace '\+', '%2b') -replace '&', '%26') -replace '#', '%23'
        # remove substring starts with "?"
        if ($Path.Contains("?")) {$Path=$Path.Substring(1,$Path.indexof("?")-1)}
        # replace "\" with "/"
        $Path=$Path.Replace("\","/")
        # filter possible string at the end "/children" (case insensitive)
        $Path=$Path+"/"
        $Path=$Path -replace "/children/",""
        # encoding of URL parts
        $tmpString=""
        foreach ($Sub in $Path.Split("/")) {$tmpString+=$Sub+"/"}
        $Path=$tmpString
        # remove last "/" if exist
        $Path=$Path.TrimEnd("/")
        # insert drive part of URL
        if ($DriveId -eq "") 
        {    
            # Default drive
            $Path="/drive/root:"+$Path+":"
        }
        else
        {
            # Named drive
            $Path="/drives/"+$DriveId+"/root:"+$Path+":"
        }
        return ($Path).replace("root::","root")
    }
}

function Get-ODItemProperty
{
    <#
    .DESCRIPTION
    Get the properties of an item (file or folder).
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .PARAMETER Path
    Specifies the path to the element/item. If not given, the properties of your default root drive are listed.
    .PARAMETER ElementId
    Specifies the id of the element/item. If Path and ElementId are given, the ElementId is used with a warning.
    .PARAMETER SelectProperties
    Specifies a comma-separated list of the properties to be returned for file and folder objects (case sensitive). If not set, name, size, lastModifiedDateTime and id are used. (See https://dev.onedrive.com/odata/optional-query-parameters.htm).
    If you use -SelectProperties "", all properties are listed. Warning: A complex "content.downloadUrl" is listed/generated for download files without authentication for several hours.
    .PARAMETER DriveId
    Specifies the OneDrive drive id. If not set, the default drive is used.
    .EXAMPLE
    Get-ODItemProperty -AccessToken $AuthToken -Path "/Data/documents/2016/AzureML with PowerShell.docx"
    Get the default set of metadata for a file or folder (name, size, lastModifiedDateTime, id)
 
    Get-ODItemProperty -AccessToken $AuthToken -ElementId 8BADCFF017EAA324!12169 -SelectProperties ""
    Get all metadata of a file or folder by element id ("" select all properties)
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [string]$ResourceId="",
        [string]$Path="/",
        [string]$ElementId="",
        [string]$SelectProperties="name,size,lastModifiedDateTime,id",
        [string]$DriveId=""
    )
    return Get-ODChildItems -AccessToken $AccessToken -ResourceId $ResourceId -Path $Path -ElementId $ElementId -SelectProperties $SelectProperties -DriveId $DriveId -ItemPropertyMode
}

function Get-ODChildItems
{
    <#
    .DESCRIPTION
    Get child items of a path. Return count is not limited.
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .PARAMETER Path
    Specifies the path of elements to be listed. If not given, the path is "/".
    .PARAMETER ElementId
    Specifies the id of an element. If Path and ElementId are given, the ElementId is used with a warning.
    .PARAMETER SelectProperties
    Specifies a comma-separated list of the properties to be returned for file and folder objects (case sensitive). If not set, name, size, lastModifiedDateTime and id are used. (See https://dev.onedrive.com/odata/optional-query-parameters.htm).
    If you use -SelectProperties "", all properties are listed. Warning: A complex "content.downloadUrl" is listed/generated for download files without authentication for several hours.
    .PARAMETER DriveId
    Specifies the OneDrive drive id. If not set, the default drive is used.
    .EXAMPLE
    Get-ODChildItems -AccessToken $AuthToken -Path "/" | ft
    Lists files and folders in your OneDrives root folder and displays name, size, lastModifiedDateTime, id and folder property as a table
 
    Get-ODChildItems -AccessToken $AuthToken -Path "/" -SelectProperties ""
    Lists files and folders in your OneDrives root folder and displays all properties
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId="",
        [string]$Path="/",
        [string]$ElementId="",
        [string]$SelectProperties="name,size,lastModifiedDateTime,id",
        [string]$DriveId="",
        [Parameter(DontShow)]
        [switch]$ItemPropertyMode,
        [Parameter(DontShow)]
        [string]$SearchText,
        [parameter(DontShow)]
        [switch]$Loop=$false
    )
    $ODRootURI=Get-ODRootUri -ResourceId $ResourceId
    if ($Path.Contains('$skiptoken=') -or $Loop)
    {    
        # Recursive mode of odata.nextLink detection
        write-debug("Recursive call")
        $rURI=$Path    
    }
    else
    {
        $rURI=Format-ODPathorIdString -path $Path -ElementId $ElementId -DriveId $DriveId
        $rURI=$rURI.Replace("::","")
        $SelectProperties=$SelectProperties.Replace(" ","")
        if ($SelectProperties -eq "")
        {
            $opt=""
        } else
        {
            $SelectProperties=$SelectProperties.Replace(" ","")+",folder"
            $opt="?select="+$SelectProperties
        }
        if ($ItemPropertyMode)
        {
            # item property mode
            $rURI=$rURI+$opt
        }
        else
        {
            if (!$SearchText -eq "") 
            {
                # Search mode
                $opt="/view.search?q="+$SearchText+"&select="+$SelectProperties
                $rURI=$rURI+$opt
            }
            else
            {
                # child item mode
                $rURI=$rURI+"/children"+$opt
            }
        }
    }
    write-debug("Accessing API with GET to "+$rURI)
    $ResponseObject=Get-ODWebContent -AccessToken $AccessToken -ResourceId $ResourceId -Method GET -rURI $rURI
    if ($ResponseObject.PSobject.Properties.name -match "@odata.nextLink") 
    {
        write-debug("Getting more elements form service (@odata.nextLink is present)")
        write-debug("LAST: "+$ResponseObject.value.count)
        Get-ODChildItems -AccessToken $AccessToken -ResourceId $ResourceId -SelectProperties $SelectProperties -Path $ResponseObject."@odata.nextLink".Replace($ODRootURI,"") -Loop
    }
    if ($ItemPropertyMode)
    {
        # item property mode
        return $ResponseObject
    }
    else
    {
        # child item mode
        return $ResponseObject.value
    }
}

function Search-ODItems
{
    <#
    .DESCRIPTION
    Search for items starting from Path or ElementId.
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .PARAMETER SearchText
    Specifies search string.
    .PARAMETER Path
    Specifies the path of the folder to start the search. If not given, the path is "/".
    .PARAMETER ElementId
    Specifies the element id of the folder to start the search. If Path and ElementId are given, the ElementId is used with a warning.
    .PARAMETER SelectProperties
    Specifies a comma-separated list of the properties to be returned for file and folder objects (case sensitive). If not set, name, size, lastModifiedDateTime and id are used. (See https://dev.onedrive.com/odata/optional-query-parameters.htm).
    If you use -SelectProperties "", all properties are listed. Warning: A complex "content.downloadUrl" is listed/generated for download files without authentication for several hours.
    .PARAMETER DriveId
    Specifies the OneDrive drive id. If not set, the default drive is used.
    .EXAMPLE
    Search-ODItems -AccessToken $AuthToken -Path "/My pictures" -SearchText "FolderA"
    Searches for items in a sub folder recursively. Take a look at OneDrives API documentation to see how search (preview) works (file and folder names, in files, …)
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId="",
        [Parameter(Mandatory=$True)]
        [string]$SearchText,
        [string]$Path="/",
        [string]$ElementId="",
        [string]$SelectProperties="name,size,lastModifiedDateTime,id",
        [string]$DriveId=""
    )
    return Get-ODChildItems -AccessToken $AccessToken -ResourceId $ResourceId -Path $Path -ElementId $ElementId -SelectProperties $SelectProperties -DriveId $DriveId -SearchText $SearchText    
}

function New-ODFolder
{
    <#
    .DESCRIPTION
    Create a new folder.
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .PARAMETER FolderName
    Name of the new folder.
    .PARAMETER Path
    Specifies the parent path for the new folder. If not given, the path is "/".
    .PARAMETER ElementId
    Specifies the element id for the new folder. If Path and ElementId are given, the ElementId is used with a warning.
    .PARAMETER DriveId
    Specifies the OneDrive drive id. If not set, the default drive is used.
    .EXAMPLE
    New-ODFolder -AccessToken $AuthToken -Path "/data/documents" -FolderName "2016"
    Creates a new folder "2016" under "/data/documents"
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId="",
        [Parameter(Mandatory=$True)]
        [string]$FolderName,
        [string]$Path="/",
        [string]$ElementId="",
        [string]$DriveId=""
    )
    $rURI=Format-ODPathorIdString -path $Path -ElementId $ElementId -DriveId $DriveId
    $rURI=$rURI+"/children"
    return Get-ODWebContent -AccessToken $AccessToken -ResourceId $ResourceId -Method POST -rURI $rURI -Body ('{"name": "'+$FolderName+'","folder": { },"@name.conflictBehavior": "fail"}')
}

function Remove-ODItem
{
    <#
    .DESCRIPTION
    Delete an item (folder or file).
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .PARAMETER Path
    Specifies the path of the item to be deleted.
    .PARAMETER ElementId
    Specifies the element id of the item to be deleted.
    .PARAMETER DriveId
    Specifies the OneDrive drive id. If not set, the default drive is used.
    .EXAMPLE
    Remove-ODItem -AccessToken $AuthToken -Path "/Data/documents/2016/Azure-big-picture.old.docx"
    Deletes an item
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId="",
        [string]$Path="",
        [string]$ElementId="",
        [string]$DriveId=""
    )
    if (($ElementId+$Path) -eq "") 
    {
        write-error("Path nor ElementId is set")
    }
    else
    {
        $rURI=Format-ODPathorIdString -path $Path -ElementId $ElementId -DriveId $DriveId
        return Get-ODWebContent -AccessToken $AccessToken -ResourceId $ResourceId -Method DELETE -rURI $rURI 
    }
}

function Get-ODItem
{
    <#
    .DESCRIPTION
    Download an item/file. Warning: A local file will be overwritten.
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .PARAMETER Path
    Specifies the path of the file to download.
    .PARAMETER ElementId
    Specifies the element id of the file to download. If Path and ElementId are given, the ElementId is used with a warning.
    .PARAMETER DriveId
    Specifies the OneDrive drive id. If not set, the default drive is used.
    .PARAMETER LocalPath
    Save file to path (if not given, the current local path is used).
    .PARAMETER LocalFileName
    Local filename. If not given, the file name of OneDrive is used.
    .EXAMPLE
    Get-ODItem -AccessToken $AuthToken -Path "/Data/documents/2016/Powershell array custom objects.docx"
    Downloads a file from OneDrive
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId="",
        [string]$Path="",
        [string]$ElementId="",
        [string]$DriveId="",
        [string]$LocalPath="",
        [string]$LocalFileName
    )
    if (($ElementId+$Path) -eq "") 
    {
        write-error("Path nor ElementId is set")
    }
    else
    {
        $Download=Get-ODItemProperty -AccessToken $AccessToken -ResourceId $ResourceId -Path $Path -ElementId $ElementId -DriveId $DriveId -SelectProperties "name,@content.downloadUrl,lastModifiedDateTime"
        if ($LocalPath -eq "") {$LocalPath=Get-Location}
        if ($LocalFileName -eq "")
        {
            $SaveTo=$LocalPath.TrimEnd("\")+"\"+$Download.name
        }
        else
        {
            $SaveTo=$LocalPath.TrimEnd("\")+"\"+$LocalFileName        
        }
        try
        {
            [System.Net.WebClient]::WebClient
            $client = New-Object System.Net.WebClient
            $client.DownloadFile($Download."@content.downloadUrl",$SaveTo)
            $file = Get-Item $saveTo
            $file.LastWriteTime = $Download.lastModifiedDateTime
            write-verbose("Download complete")
            return 0
        }
        catch
        {
            write-error("Download error: "+$_.Exception.Response.StatusCode+"`n"+$_.Exception.Response.StatusDescription)
            return -1
        }
    }    
}
function Add-ODItem
{
    <#
    .DESCRIPTION
    Upload an item/file. Warning: An existing file will be overwritten.
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .PARAMETER Path
    Specifies the path for the upload folder. If not given, the path is "/".
    .PARAMETER ElementId
    Specifies the element id for the upload folder. If Path and ElementId are given, the ElementId is used with a warning.
    .PARAMETER DriveId
    Specifies the OneDrive drive id. If not set, the default drive is used.
    .PARAMETER LocalFile
    Path and file of the local file to be uploaded (C:\data\data.csv).
    .EXAMPLE
    Add-ODItem -AccessToken $AuthToken -Path "/Data/documents/2016" -LocalFile "AzureML with PowerShell.docx"
    Upload a file to OneDrive "/data/documents/2016"
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId="",
        [string]$Path="/",
        [string]$ElementId="",
        [string]$DriveId="",
        [Parameter(Mandatory=$True)]
        [string]$LocalFile=""
    )
    $rURI=Format-ODPathorIdString -path $Path -ElementId $ElementId -DriveId $DriveId
    try
    {
        $spacer=""
        if ($ElementId -ne "") {$spacer=":"}
        $ODRootURI=Get-ODRootUri -ResourceId $ResourceId
        $rURI=(($ODRootURI+$rURI).TrimEnd(":")+$spacer+"/"+[System.IO.Path]::GetFileName($LocalFile)+":/content").Replace("/root/","/root:/")
        return $webRequest=Invoke-WebRequest -Method PUT -InFile $LocalFile -Uri $rURI -Header @{ Authorization = "BEARER "+$AccessToken} -ContentType "multipart/form-data"  -UseBasicParsing -ErrorAction SilentlyContinue
    }
    catch
    {
        write-error("Upload error: "+$_.Exception.Response.StatusCode+"`n"+$_.Exception.Response.StatusDescription)
        return -1
    }    
}
function Add-ODItemLarge {
    <#
        .DESCRIPTION
        Upload a large file with an upload session. Warning: Existing files will be overwritten.
        For reference, see: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online
        .PARAMETER AccessToken
        A valid access token for bearer authorization.
        .PARAMETER ResourceId
        Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
        .PARAMETER Path
        Specifies the path for the upload folder. If not given, the path is "/".
        .PARAMETER ElementId
        Specifies the element id for the upload folder. If Path and ElementId are given, the ElementId is used with a warning.
        .PARAMETER DriveId
        Specifies the OneDrive drive id. If not set, the default drive is used.
        .PARAMETER LocalFile
        Path and file of the local file to be uploaded (C:\data\data.csv).
        .EXAMPLE
        Add-ODItem -AccessToken $AuthToken -Path "/Data/documents/2016" -LocalFile "AzureML with PowerShell.docx"
        Upload a file to OneDrive "/data/documents/2016"
        .NOTES
        Author: Benke Tamás - (funkeninduktor@gmail.com)
    #>

    
    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId="",
        [string]$Path="/",
        [string]$ElementId="",
        [string]$DriveId="",
        [Parameter(Mandatory=$True)]
        [string]$LocalFile=""
    )
    
    $rURI=Format-ODPathorIdString -path $Path -ElementId $ElementId -DriveId $DriveId
    Try    {
        # Begin to construct the real (full) URI
        $spacer=""
        if ($ElementId -ne "") {$spacer=":"}
        $ODRootURI=Get-ODRootUri -ResourceId $ResourceId
        
        # Construct the real (full) URI
        $rURI=(($ODRootURI+$rURI).TrimEnd(":")+$spacer+"/"+[System.IO.Path]::GetFileName($LocalFile)+":/createUploadSession").Replace("/root/","/root:/")
        
        # Initialize upload session
        $webRequest=Invoke-WebRequest -Method PUT -Uri $rURI -Header @{ Authorization = "BEARER "+$AccessToken} -ContentType "application/json" -UseBasicParsing -ErrorAction SilentlyContinue

        # Parse the response JSON (into a holder variable)
        $convertResponse = ($webRequest.Content | ConvertFrom-Json)
        # Get the uploadUrl from the response (holder variable)
        $uURL = $convertResponse.uploadUrl
        # echo "HERE COMES THE CORRECT uploadUrl: $uURL"
        
        # Get the full size of the file to upload (bytes)
        $totalLength = (Get-Item $LocalFile).length
        # echo "Total file size (bytes): $totalLength"
        
        # Set the upload chunk size (Recommended: 5MB)
        $uploadLength = 5 * 1024 * 1024; # == 5242880 byte == 5MB.
        # echo "Size of upload fragments (bytes): $uploadLength" # == 5242880
        
        # Set the starting byte index of the upload (i. e.: the index of the first byte of the file to upload)
        $startingIndex = 0
        
        # Start an endless cycle to run until the last chunk of the file is uploaded (after that, BREAK out of the cycle)
        while($True){
            # If startingIndex (= the index of the starting byte) is greater than, or equal to totalLength (= the total length of the file), stop execution, so BREAK out of the cycle
            if( $startingIndex -ge $totalLength ){
                break
            }
            
            # Otherwise: set the suitable indices (variables)
            
            # (startingIndex remains as it was!)
            
            # Set the size of the chunk to upload
            # The remaining length of the file (to be uploaded)
            $remainingLength = $($totalLength-$startingIndex)
            # If remainingLength is smaller than the normal upload length (defined above as uploadLength), then the new uploadLength will be the remainingLength (self-evidently, only for the last upload chunk)
            if( $remainingLength -lt $uploadLength ){
                $uploadLength = $remainingLength
            }
            # Set the new starting index (just for the next iteration!)
            $newStartingIndex = $($startingIndex+$uploadLength)
            # Get the ending index (by means of newStartingIndex)
            $endingIndex = $($newStartingIndex-1)
            
            # Get the bytes to upload into a byte array (using properly re-initialized variables)
            $buf = new-object byte[] $uploadLength
            $fs = new-object IO.FileStream($LocalFile, [IO.FileMode]::Open)
            $reader = new-object IO.BinaryReader($fs)
            $reader.BaseStream.Seek($startingIndex,"Begin") | out-null
            $reader.Read($buf, 0, $uploadLength)| out-null
            $reader.Close()
            # echo "Chunk size is: $($buf.count)"
            
            # Upoad the actual file chunk (byte array) to the actual upload session.
            # Some aspects of the chunk upload:
                # We don't have to authenticate for the chunk uploads, since the uploadUrl contains the upload session's authentication data as well.
                # We above calculated the length, and starting and ending byte indices of the actual chunk, and the total size of the (entire) file. These should be set into the upload's PUT request headers.
                # If the upload session is alive, every file chunk (including the last one) should be uploaded with the same command syntax.
                # If the last chunk was uploaded, the file is automatically created (and the upload session is closed).
                # The (default) length of an upload session is about 15 minutes!
            
            # Set the headers for the actual file chunk's PUT request (by means of the above preset variables)
            $actHeaders=@{"Content-Length"="$uploadLength"; "Content-Range"="bytes $startingIndex-$endingIndex/$totalLength"};
            
            # Execute the PUT request (upload file chunk)
            write-debug("Uploading chunk of bytes. Progress: "+$endingIndex/$totalLength*100+" %")
            $uploadResponse=Invoke-WebRequest -Method PUT -Uri $uURL -Headers $actHeaders -Body $buf -UseBasicParsing -ErrorAction SilentlyContinue
            
            # startingIndex should be incremented (with the size of the actually uploaded file chunk) for the next iteration.
            # (Since the new value for startingIndex was preset above, as newStartingIndex, here we just have to overwrite startingIndex with it!)
            $startingIndex = $newStartingIndex
        }
        # The upload is done!
        
        # At the end of the upload, write out the last response, which should be a confirmation message: "HTTP/1.1 201 Created"
        write-debug("Upload complete")
        return ($uploadResponse.Content | ConvertFrom-Json)
    }
    Catch {
        write-error("Upload error: "+$_.Exception.Response.StatusCode+"`n"+$_.Exception.Response.StatusDescription)
        return -1
    }    
}
function Move-ODItem
{
    <#
    .DESCRIPTION
    Moves a file to a new location or renames it.
    .PARAMETER AccessToken
    A valid access token for bearer authorization.
    .PARAMETER ResourceId
    Mandatory for OneDrive 4 Business access. Is the ressource URI: "https://<tenant>-my.sharepoint.com/". Example: "https://sepagogmbh-my.sharepoint.com/"
    .PARAMETER Path
    Specifies the path of the file to be moved.
    .PARAMETER ElementId
    Specifies the element id of the file to be moved. If Path and ElementId are given, the ElementId is used with a warning.
    .PARAMETER DriveId
    Specifies the OneDrive drive id. If not set, the default drive is used.
    .PARAMETER TargetPath
    Save file to the target path in the same OneDrive drive (ElementId for the target path is not supported yet).
    .PARAMETER NewName
    The new name of the file. If missing, the file will only be moved.
    .EXAMPLE
    Move-ODItem -AccessToken $at -path "/Notes.txt" -TargetPath "/x" -NewName "_Notes.txt"
    Moves and renames a file in one step
 
    Move-ODItem -AccessToken $at -path "/Notes.txt" -NewName "_Notes.txt" # Rename a file
     
    Move-ODItem -AccessToken $at -path "/Notes.txt" -TargetPath "/x" # Move a file
    .NOTES
    Author: Marcel Meurer, marcel.meurer@sepago.de, Twitter: MarcelMeurer
    #>

    PARAM(
        [Parameter(Mandatory=$True)]
        [string]$AccessToken,
        [String]$ResourceId="",
        [string]$Path="",
        [string]$ElementId="",
        [string]$DriveId="",
        [string]$TargetPath="",
        [string]$NewName=""
    )
    if (($ElementId+$Path) -eq "") 
    {
        write-error("Path nor ElementId is set")
    }
    else
    {
        if (($TargetPath+$NewName) -eq "")
        {
            write-error("TargetPath nor NewName is set")
        }
        else
        {    
            $body='{'
            if (!$NewName -eq "") 
            {
                $body=$body+'"name": "'+$NewName+'"'
                If (!$TargetPath -eq "")
                {
                    $body=$body+','
                }
            }
            if (!$TargetPath -eq "") 
            {
                $rTURI=Format-ODPathorIdStringV2 -path $TargetPath -DriveId $DriveId
                $body=$body+'"parentReference" : {"path": "'+$rTURI+'"}'
            }
            $body=$body+'}'
            $rURI=Format-ODPathorIdString -path $Path -ElementId $ElementId -DriveId $DriveId
            return Get-ODWebContent -AccessToken $AccessToken -ResourceId $ResourceId -Method PATCH -rURI $rURI -Body $body
        }
    }
}
# SIG # Begin signature block
# MIIcWwYJKoZIhvcNAQcCoIIcTDCCHEgCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU+tTI3jDdxpYvuPhsVlTYPV/V
# u92ggheKMIIFEzCCA/ugAwIBAgIQB2gm73G59Nmo+sBhs2ehKjANBgkqhkiG9w0B
# AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz
# c3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTE5MTEwMjAwMDAwMFoXDTIwMTIx
# NjEyMDAwMFowUDELMAkGA1UEBhMCREUxETAPBgNVBAcTCE9kZW50aGFsMRYwFAYD
# VQQKEw1NYXJjZWwgTWV1cmVyMRYwFAYDVQQDEw1NYXJjZWwgTWV1cmVyMIIBIjAN
# BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxJ1DUj5XjI6g+ibsBGA967vdcWkq
# 29ZK0VmTWEX1x2DY24VcGziscveMYOHk2Zox6rsV9HMO0Rp94FOUQIlQuBECjHOv
# hAWOYM6LP1K5QXXS+F1WTeImXBZZ6CUNKEwPi5sj9yy8SVwbKABetPQQN8HjGzxr
# q+GbAYJnOmE3loJ3crcAKhdu6a/v/ej7M0Yq2PH4wL8Ma8vlKFhfCoawOGVrstHz
# 09ixCFGKMWCJqb+CbJtvVYjhGJBmuZdyF6fGtqWd6JVaLG2LOpsjWg73bNa8sVJZ
# CEVlpqaO1rQ+h/7OnbDDRYrVtVifeC0hZUrzfqkOmTE34EaakWZUVNrwUQIDAQAB
# o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O
# BBYEFKBaTBHA7/jzrwA1WK8speaMVurdMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
# DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny
# bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw
# QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl
# cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp
# Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAndPS
# oRwriiWv4GxIAyClrMZsRQEWZyxP2+uuZpseh9Lq+DgvajL5QpwHN4QIUSM+pwMm
# YZzn9lFCnKHvOlSoDseGHWLgW9J/kIvhLjHu7Jui+WRN7j9OpbDzTzTC8z7Ko4bQ
# +VdIK9ZUUvA457EiWyXxAhajmNkok37FeOEVguOjRnG1+AFaiNs7HkdYjx7TNm1F
# mON+NxoFwIsm2CHF3+99RXBFeZ3tXmGBxH+EcXhSqw+fKx3PI5xw6LkmbyfKWGox
# 3MeRKaFYsxDmm0JAuyj46mGq2VvLo9uikLUr0f8aWKqJ/6qlU8LCHj/yzsYYcpjA
# iZC0TcuOAbaZEI9PNzCCBTAwggQYoAMCAQICEAQJGBtf1btmdVNDtW+VUAgwDQYJ
# KoZIhvcNAQELBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IElu
# YzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQg
# QXNzdXJlZCBJRCBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAw
# MFowcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE
# CxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1
# cmVkIElEIENvZGUgU2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
# AQoCggEBAPjTsxx/DhGvZ3cH0wsxSRnP0PtFmbE620T1f+Wondsy13Hqdp0FLreP
# +pJDwKX5idQ3Gde2qvCchqXYJawOeSg6funRZ9PG+yknx9N7I5TkkSOWkHeC+aGE
# I2YSVDNQdLEoJrskacLCUvIUZ4qJRdQtoaPpiCwgla4cSocI3wz14k1gGL6qxLKu
# cDFmM3E+rHCiq85/6XzLkqHlOzEcz+ryCuRXu0q16XTmK/5sy350OTYNkO/ktU6k
# qepqCquE86xnTrXE94zRICUj6whkPlKWwfIPEvTFjg/BougsUfdzvL2FsWKDc0GC
# B+Q4i2pzINAPZHM8np+mM6n9Gd8lk9ECAwEAAaOCAc0wggHJMBIGA1UdEwEB/wQI
# MAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHkG
# CCsGAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRBc3N1cmVkSURSb290Q0EuY3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRw
# Oi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3Js
# MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVk
# SURSb290Q0EuY3JsME8GA1UdIARIMEYwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUF
# BwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAoGCGCGSAGG/WwDMB0G
# A1UdDgQWBBRaxLl7KgqjpepxA8Bg+S32ZXUOWDAfBgNVHSMEGDAWgBRF66Kv9JLL
# gjEtUYunpyGd823IDzANBgkqhkiG9w0BAQsFAAOCAQEAPuwNWiSz8yLRFcgsfCUp
# dqgdXRwtOhrE7zBh134LYP3DPQ/Er4v97yrfIFU3sOH20ZJ1D1G0bqWOWuJeJIFO
# EKTuP3GOYw4TS63XX0R58zYUBor3nEZOXP+QsRsHDpEV+7qvtVHCjSSuJMbHJyqh
# KSgaOnEoAjwukaPAJRHinBRHoXpoaK+bp1wgXNlxsQyPu6j4xRJon89Ay0BEpRPw
# 5mQMJQhCMrI2iiQC/i9yfhzXSUWW6Fkd6fp0ZGuy62ZD2rOwjNXpDd32ASDOmTFj
# PQgaGLOBm0/GkxAG/AeB+ova+YJJ92JuoVP6EpQYhS6SkepobEQysmah5xikmmRR
# 7zCCBmowggVSoAMCAQICEAMBmgI6/1ixa9bV6uYX8GYwDQYJKoZIhvcNAQEFBQAw
# YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJRCBD
# QS0xMB4XDTE0MTAyMjAwMDAwMFoXDTI0MTAyMjAwMDAwMFowRzELMAkGA1UEBhMC
# VVMxETAPBgNVBAoTCERpZ2lDZXJ0MSUwIwYDVQQDExxEaWdpQ2VydCBUaW1lc3Rh
# bXAgUmVzcG9uZGVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo2Rd
# /Hyz4II14OD2xirmSXU7zG7gU6mfH2RZ5nxrf2uMnVX4kuOe1VpjWwJJUNmDzm9m
# 7t3LhelfpfnUh3SIRDsZyeX1kZ/GFDmsJOqoSyyRicxeKPRktlC39RKzc5YKZ6O+
# YZ+u8/0SeHUOplsU/UUjjoZEVX0YhgWMVYd5SEb3yg6Np95OX+Koti1ZAmGIYXIY
# aLm4fO7m5zQvMXeBMB+7NgGN7yfj95rwTDFkjePr+hmHqH7P7IwMNlt6wXq4eMfJ
# Bi5GEMiN6ARg27xzdPpO2P6qQPGyznBGg+naQKFZOtkVCVeZVjCT88lhzNAIzGvs
# YkKRrALA76TwiRGPdwIDAQABo4IDNTCCAzEwDgYDVR0PAQH/BAQDAgeAMAwGA1Ud
# EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwggG/BgNVHSAEggG2MIIB
# sjCCAaEGCWCGSAGG/WwHATCCAZIwKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRp
# Z2ljZXJ0LmNvbS9DUFMwggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBz
# AGUAIABvAGYAIAB0AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBv
# AG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAg
# AHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAg
# AHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBt
# AGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0
# AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABo
# AGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9
# bAMVMB8GA1UdIwQYMBaAFBUAEisTmLKZB+0e36K+Vw0rZwLNMB0GA1UdDgQWBBRh
# Wk0ktkkynUoqeRqDS/QeicHKfTB9BgNVHR8EdjB0MDigNqA0hjJodHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURDQS0xLmNybDA4oDagNIYy
# aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5j
# cmwwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRENBLTEuY3J0MA0GCSqGSIb3DQEBBQUAA4IBAQCd
# JX4bM02yJoFcm4bOIyAPgIfliP//sdRqLDHtOhcZcRfNqRu8WhY5AJ3jbITkWkD7
# 3gYBjDf6m7GdJH7+IKRXrVu3mrBgJuppVyFdNC8fcbCDlBkFazWQEKB7l8f2P+fi
# EUGmvWLZ8Cc9OB0obzpSCfDscGLTYkuw4HOmksDTjjHYL+NtFxMG7uQDthSr849D
# p3GdId0UyhVdkkHa+Q+B0Zl0DSbEDn8btfWg8cZ3BigV6diT5VUW8LsKqxzbXEgn
# Zsijiwoc5ZXarsQuWaBh3drzbaJh6YoLbewSGL33VVRAA5Ira8JRwgpIr7DUbuD0
# FAo6G+OPPcqvao173NhEMIIGzTCCBbWgAwIBAgIQBv35A5YDreoACus/J7u6GzAN
# BgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQg
# SW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2Vy
# dCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMjExMTEwMDAw
# MDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBBc3N1cmVk
# IElEIENBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDogi2Z+crC
# QpWlgHNAcNKeVlRcqcTSQQaPyTP8TUWRXIGf7Syc+BZZ3561JBXCmLm0d0ncicQK
# 2q/LXmvtrbBxMevPOkAMRk2T7It6NggDqww0/hhJgv7HxzFIgHweog+SDlDJxofr
# Nj/YMMP/pvf7os1vcyP+rFYFkPAyIRaJxnCI+QWXfaPHQ90C6Ds97bFBo+0/vtuV
# SMTuHrPyvAwrmdDGXRJCgeGDboJzPyZLFJCuWWYKxI2+0s4Grq2Eb0iEm09AufFM
# 8q+Y+/bOQF1c9qjxL6/siSLyaxhlscFzrdfx2M8eCnRcQrhofrfVdwonVnwPYqQ/
# MhRglf0HBKIJAgMBAAGjggN6MIIDdjAOBgNVHQ8BAf8EBAMCAYYwOwYDVR0lBDQw
# MgYIKwYBBQUHAwEGCCsGAQUFBwMCBggrBgEFBQcDAwYIKwYBBQUHAwQGCCsGAQUF
# BwMIMIIB0gYDVR0gBIIByTCCAcUwggG0BgpghkgBhv1sAAEEMIIBpDA6BggrBgEF
# BQcCARYuaHR0cDovL3d3dy5kaWdpY2VydC5jb20vc3NsLWNwcy1yZXBvc2l0b3J5
# Lmh0bTCCAWQGCCsGAQUFBwICMIIBVh6CAVIAQQBuAHkAIAB1AHMAZQAgAG8AZgAg
# AHQAaABpAHMAIABDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIABjAG8AbgBzAHQAaQB0
# AHUAdABlAHMAIABhAGMAYwBlAHAAdABhAG4AYwBlACAAbwBmACAAdABoAGUAIABE
# AGkAZwBpAEMAZQByAHQAIABDAFAALwBDAFAAUwAgAGEAbgBkACAAdABoAGUAIABS
# AGUAbAB5AGkAbgBnACAAUABhAHIAdAB5ACAAQQBnAHIAZQBlAG0AZQBuAHQAIAB3
# AGgAaQBjAGgAIABsAGkAbQBpAHQAIABsAGkAYQBiAGkAbABpAHQAeQAgAGEAbgBk
# ACAAYQByAGUAIABpAG4AYwBvAHIAcABvAHIAYQB0AGUAZAAgAGgAZQByAGUAaQBu
# ACAAYgB5ACAAcgBlAGYAZQByAGUAbgBjAGUALjALBglghkgBhv1sAxUwEgYDVR0T
# AQH/BAgwBgEB/wIBADB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6
# Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0f
# BHoweDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNz
# dXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29t
# L0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDAdBgNVHQ4EFgQUFQASKxOYspkH
# 7R7for5XDStnAs0wHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJ
# KoZIhvcNAQEFBQADggEBAEZQPsm3KCSnOB22WymvUs9S6TFHq1Zce9UNC0Gz7+x1
# H3Q48rJcYaKclcNQ5IK5I9G6OoZyrTh4rHVdFxc0ckeFlFbR67s2hHfMJKXzBBlV
# qefj56tizfuLLZDCwNK1lL1eT7EF0g49GqkUW6aGMWKoqDPkmzmnxPXOHXh2lCVz
# 5Cqrz5x2S+1fwksW5EtwTACJHvzFebxMElf+X+EevAJdqP77BzhPDcZdkbkPZ0XN
# 1oPt55INjbFpjE/7WeAjD9KqrgB87pxCDs+R1ye3Fu4Pw718CqDuLAhVhSK46xga
# TfwqIa1JMYNHlXdx3LEbS0scEJx3FMGdTy9alQgpECYxggQ7MIIENwIBATCBhjBy
# MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
# d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQg
# SUQgQ29kZSBTaWduaW5nIENBAhAHaCbvcbn02aj6wGGzZ6EqMAkGBSsOAwIaBQCg
# eDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE
# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJ
# BDEWBBRA/kpxb5thJFqS6uxi5wTCKXEiwTANBgkqhkiG9w0BAQEFAASCAQAu+hjF
# agoYV0wjkIyd8dZ0IJIP3ttycjVMxDbJB2nK/KT/dtuUo7VdSjTmCKsFMQDQOqqn
# D455ZbGyeWm6wV5pFo2nB2gUrs772YUhATTyySzsotkN6mk3PMGjMWOhRxE9ZFZQ
# 6salyqjMYwqiNaVFy7J64coRyP584iXeF4ECJAoskh4JkbGSdt3g8YLjhzSp/dde
# m3+dKqvOoJPb1vkgu1Nq7Qij8Iic8Sh4X13Ee6GbE/TFGt1csKBiHNKIWmGTin6K
# hk5oYuV/bvmsshgi3yeZBco4lszuKt/SfkfZ8yB55mH88gGlrwKWA/kHNqhSJ3GT
# SKAtV2jMZIgGoRxvoYICDzCCAgsGCSqGSIb3DQEJBjGCAfwwggH4AgEBMHYwYjEL
# MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
# LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJRCBDQS0x
# AhADAZoCOv9YsWvW1ermF/BmMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJ
# KoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDA4MTEwNzE5NThaMCMGCSqGSIb3
# DQEJBDEWBBSoaxtu1i2j13y5Byl1HmYrI3Pv8jANBgkqhkiG9w0BAQEFAASCAQAz
# UjoSuLHCuJQnqrLEhXxyc6JRkzmToJ7OuclR3E3DYmBQf9wUt1as78L9FLUG4kEd
# wE/0V81SCvaM6iK8gzdA3qymGZnh6gLBHANNX2mUTHuqa76VKn68G6piKfDA96n0
# /kIukIFavTr8yb8Qc1SgmPZ3be2qrVpk4OljZKm4zy6RPTcM5GAA77hqREpXRMk7
# CeBbXk32OGFrmiLSnr19uIEKjDM1lwcCpIAIuBosckjT5UApiUR7M1CjXznpOMIs
# r84Hex3toiJ4ysKyfxVRyyyxWBCiZ7SmH8/EMDW2/F9KyxQKdT9REgdQYRxl5BTD
# hysLq97Nh8ybVQO003gy
# SIG # End signature block