Public/Item/Set-GDriveItemContent.ps1

<#
.SYNOPSIS
    Creates or updates GoogleDrive Item, set metadata and upload content
.DESCRIPTION
    Creates or updates GoogleDrive Item, set metadata and upload content
    If needed tp update existing file, use ID
    If only metadata update required, use Set-GDriveItemProperty
.PARAMETER ID
    File ID to update
.PARAMETER StringContent
    Content to upload as string
.PARAMETER Encoding
    Enconding used for string
.PARAMETER RawContent
    Content to upload as raw byte[] array
.PARAMETER InFile
    Content to upload as path to file
.PARAMETER Name
    Name of an item to be created
.PARAMETER ParentID
    Folder ID(s) in which new item will be placed
.PARAMETER JsonProperty
    Json-formatted string with all needed file metadata
.PARAMETER UseMetadataFromFile
    Uses the metadata of the file provided in InFile
.PARAMETER Property
    List of properties that will be retured once upload is completed
.PARAMETER ResumeID
    Upload ID to resume operations in case of uploading errors
.PARAMETER ContentType
    Uploaded item Content type (seems google automatically set it to most of uploaded files)
.PARAMETER ChunkSize
    Upload request size
.PARAMETER ShowProgress
    Show progress bar while uploading
.PARAMETER KeepRevisionForever
    Set the flag that this revision of the file will be kept forever.
.PARAMETER AccessToken
    Access Token for request
.EXAMPLE
    #Named File based upload
    Set-GDriveItemContent -AccessToken $access_token -InFile D:\SomeDocument.doc -Name SomeDocument.doc
.EXAMPLE
    #Named Raw data upload with ParentID
    [byte[]]$Content = Get-Content D:\SomeDocument.doc -Encoding Bytes
    $ParentFolder = Find-GDriveItem -AccessToken $access_token -Query 'name="myparentfolder"'
    Set-GDriveItemContent -AccessToken $access_token -RawContent -Name SomeDocument.doc -ParentID $ParentFolder.files.id
.EXAMPLE
    #String based upload with metadata
    Add-GDriveItem -AccessToken $access_token -StringContent 'test file' -JsonProperty '{ "name":"myfile.txt" }'
.EXAMPLE
    #String based update upload
    Set-GDriveItemContent -AccessToken $access_token -ID '0BAjkl4cBDNVpVbB5nGhKQ195aU0' -StringContent 'test file'
.EXAMPLE
    #File based resume operation
    $Result = Set-GDriveItemContent -AccessToken $access_token -InFile D:\SomeDocument.doc -Name SomeDocument.doc
    if ($Result.Error) {
        Set-GDriveItemContent -AccessToken $access_token -InFile D:\SomeDocument.doc `
            -Name SomeDocument.doc -ResumeID $Result.ResumeID
    }
.OUTPUTS
    PSObject with properties:
        Item: Json with item metadata as PSObject
        ResultID: Upload ID for resume operations
        Error: Error info if happen
.NOTES
    Author: Max Kozlov
.LINK
    Add-GDriveItem
    Set-GDriveItemProperty
    https://developers.google.com/drive/api/v3/reference/files/create
    https://developers.google.com/drive/api/v3/reference/files/update
    https://developers.google.com/drive/api/guides/manage-uploads#resumable
#>

function Set-GDriveItemContent {
[CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Position=0, ParameterSetName='dataMeta')]
        [Parameter(Position=0, ParameterSetName='stringMeta')]
        [Parameter(Position=0, ParameterSetName='fileMeta')]
        [string]$ID,

        [Parameter(Mandatory, ParameterSetName='stringName')]
        [Parameter(Mandatory, ParameterSetName='stringMeta')]
        [string]$StringContent,
        [Parameter(ParameterSetName='stringName')]
        [Parameter(ParameterSetName='stringMeta')]
        [System.Text.Encoding]$Encoding = [System.Text.Encoding]::UTF8,

        [Parameter(Mandatory, ParameterSetName='dataName')]
        [Parameter(Mandatory, ParameterSetName='dataMeta')]
        [byte[]]$RawContent,

        [Parameter(Mandatory, ParameterSetName='fileName')]
        [Parameter(Mandatory, ParameterSetName='fileMeta')]
        [Parameter(Mandatory, ParameterSetName='fileAutomaticMeta')]
        [string]$InFile,

        [Parameter(Mandatory, ParameterSetName='dataName')]
        [Parameter(Mandatory, ParameterSetName='stringName')]
        [Parameter(Mandatory, ParameterSetName='fileName')]
        [string]$Name,

        [Parameter(ParameterSetName='dataName')]
        [Parameter(ParameterSetName='stringName')]
        [Parameter(ParameterSetName='fileName')]
        [Parameter(ParameterSetName='fileAutomaticMeta')]
        [Alias('DestinationID')]
        [string[]]$ParentID = @('root'),

        [Parameter(ParameterSetName='dataMeta')]
        [Parameter(ParameterSetName='stringMeta')]
        [Parameter(ParameterSetName='fileMeta')]
        [Alias('Metadata')]
        [string]$JsonProperty = '',

        [Parameter(Mandatory, ParameterSetName='fileAutomaticMeta')]
        [switch]$UseMetadataFromFile,

        [ValidateSet("*",'kind','id','name','mimeType',
        'description','starred','trashed','explicitlyTrashed','parents','properties','appProperties','spaces','version',
        'webContentLink','webViewLink','iconLink','thumbnailLink','viewedByMe','viewedByMeTime','createdTime','modifiedTime',
        'modifiedByMeTime','sharedWithMeTime','sharingUser','owners','lastModifyingUser','shared','ownedByMe',
        'viewersCanCopyContent','writersCanShare','permissions','originalFilename','fullFileExtension',
        'fileExtension','md5Checksum','sha1Checksum','sha256Checksum','size','quotaBytesUsed','headRevisionId','contentHints',
        'imageMediaMetadata','videoMediaMetadata','capabilities','isAppAuthorized','hasThumbnail','thumbnailVersion',
        'modifiedByMe','trashingUser','trashedTime','teamDriveId','hasAugmentedPermissions',
        'keepForever', 'published', # revisions
        IgnoreCase = $false)]
        [string[]]$Property = @('kind','id','name','mimeType','parents'),

        [string]$ResumeID,

        [string]$ContentType = 'application/octet-stream',

        [ValidateScript({
            (-not ($_ -band 0x3FFFF)) -or ( & { throw 'ChunkSize must be in multiples of 256 KB (256 x 1024 bytes) in size' } )
        })]
        [int]$ChunkSize = 4Mb,

        [switch]$ShowProgress,

        [switch]$KeepRevisionForever,

        [Parameter(Mandatory)]
        [string]$AccessToken
    )

    $UploadResult = [PSCustomObject]@{
        Item = $null
        ResumeID = ''
        Error = $null
    }

    if ($PSCmdlet.ParameterSetName -in 'stringName','stringMeta') {
        [byte[]]$RawContent = $Encoding.GetBytes($StringContent)
        Write-Verbose "Encoded $($StringContent.Length) characters to $($RawContent.Count) bytes ($($Encoding.EncodingName))"
    }
    if ($PSCmdlet.ParameterSetName -in 'stringName', 'dataName', 'fileName', 'fileAutomaticMeta') {
        $toJson = @{}
        if ($UseMetadataFromFile) {
            #$FileMetadata = Get-Item ([Management.Automation.WildcardPattern]::Escape($InFile))
            $FileMetadata = Get-Item -LiteralPath $InFile
            $toJson.modifiedTime = $FileMetadata.LastWriteTimeUtc.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
            if (-not $PSBoundParameters.ContainsKey('ID')) {
                $toJson.name = $FileMetadata.Name
                $toJson.parents = $ParentID
                $toJson.createdTime = $FileMetadata.CreationTimeUtc.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
            }
        }
        else {
            $toJson.name = $Name
            $toJson.parents = $ParentID
        }
        $JsonProperty = $toJson | ConvertTo-Json
        Write-Verbose "Constructed Metadata: $JsonProperty"
    }
    try {
        if ($PSCmdlet.ParameterSetName -in 'fileName','fileMeta','fileAutomaticMeta') {
            $stream = New-Object System.IO.FileStream $InFile, 'Open'
        }
        else {
            $stream = New-Object System.IO.MemoryStream $RawContent, $false
        }
    }
    catch {
        $UploadResult.Error = $_.Exception
        $UploadResult
        Write-Error $_.Exception
        return
    }
    try {
        $Headers = @{
            "Authorization"           = "Bearer $AccessToken"
            "X-Upload-Content-Type"   = $ContentType
            "X-Upload-Content-Length" = $stream.Length
        }

        $WebRequestParams = @{
            Headers = $Headers
            ContentType = "application/json; charset=utf-8"
            UseBasicParsing = $true
            Body = $JsonProperty
        }

        if ($PSBoundParameters.ContainsKey('ID')) {
            Write-Verbose "Updating File $ID"
            # Patch instead of Put! docs are wrong? Put give 404
            $WebRequestParams.Method = 'Patch'
            $WebRequestParams.Uri = "$($GDriveUploadUri)$($ID)"
        }
        else {
            Write-Verbose "Creating New file"
            $WebRequestParams.Method = 'Post'
            $WebRequestParams.Uri = "$($GDriveUploadUri)"
        }
        if ($Property -contains "*") {
            $Property = "*"
        }
        $WebRequestParams.Uri += "?supportsAllDrives=true&uploadType=resumable&fields={0}" -f ($Property -join ',')
        if($KeepRevisionForever) {
            $WebRequestParams.Uri = $WebRequestParams.Uri + "&keepRevisionForever=true"
        }
        Write-Verbose ("URI: " + $WebRequestParams.Uri)

        if ($ResumeID) {
            #To request the upload status, create an empty PUT request to the resumable session URI.
            Write-Verbose "Use Resume ID $ResumeID"
            $WebRequestParams.Uri += '&upload_id=' + $ResumeID
            $WebRequestParams.Method = 'Put'
            [void]$WebRequestParams.Remove('Body')
            $WebRequestParams.Headers['Content-Range'] = 'bytes */{0}' -f $stream.Length
        }
        Write-Verbose ('Metadata upload, resumable, {0} bytes, {1}' -f $stream.Length, $ContentType)
        $uploadString = 'Uploading ' + $PSCmdlet.ParameterSetName -replace '(Name|Meta)'
        if ($PSCmdlet.ParameterSetName -match 'Name')   { $uploadString += " named '$Name'" }
        if ($PSCmdlet.ParameterSetName -match 'file')   { $uploadString += " from [$InFile]" }
        elseif ($PSCmdlet.ParameterSetName -match 'string') { $uploadString += " from [string]" }
        elseif ($PSCmdlet.ParameterSetName -match 'data')   { $uploadString += " from [byte[] array]" }

        if ($ShowProgress) {
            Write-Progress -Activity $uploadString -Status 'Metadata upload' -PercentComplete 1
        }
        $wr = $null
        try {
            if ($PSCmdlet.ShouldProcess($ID, $uploadString)) {
                $wr = Invoke-WebRequest @WebRequestParams @GDriveProxySettings
            }
        }
        catch {
            $UploadResult.Error = $_.Exception
            $UploadResult
            Write-Error $_.Exception
            return
        }

        if ($PSVersionTable.PSVersion.Major -ge 7) {
            $WebRequestParams.SkipHttpErrorCheck = $true # allow to work with 308 "error" code in PSv7
        }
        else {
            $WebRequestParams.MaximumRedirection = 0 # It is need for PSv5 to work with 308 and not show error on v7
        }
        $WebRequestParams.ContentType = $ContentType
        if ($wr.StatusCode -in 200,308) {
            $UploadResult.ResumeID = $wr.Headers['X-GUploader-UploadID'] | Select-Object -First 1
            Write-Verbose "ResumeID: $($UploadResult.ResumeID)"
            try {
                # Resume already have the right URI
                if ($wr.Headers['Location']) {
                    $WebRequestParams['Uri'] = $wr.Headers['Location'] | Select-Object -First 1
                }

                [long]$UploadedSize = 0
                $ReceivedRange = $wr.Headers['Range'] | Select-Object -First 1
                Write-Verbose "Received Range: $ReceivedRange"
                if ($ReceivedRange -match 'bytes=(\d+)-(\d+)')
                {
                    $UploadedSize = ([long]$matches[2]) + 1
                    Write-Verbose "Stream Position: $($stream.Position), UploadedSize: $($UploadedSize)"
                    if ($stream.Position -ne $UploadedSize)
                    {
                        Write-Verbose "Fast Forward to: $($UploadedSize)"
                        [void]$stream.Seek($UploadedSize, [System.IO.SeekOrigin]::Begin)
                    }
                }

                $WebRequestParams.Method = 'Put'

                if ($PSCmdlet.ShouldProcess($ID, $uploadString)) {
                    if (($stream.Length -eq 0) -or # for null-sized files just 'close' upload
                        ($UploadedSize -eq 0 -and
                         $stream.Length -le $ChunkSize -and
                         ($PSCmdlet.ParameterSetName -notin 'fileName','fileMeta','fileAutomaticMeta')
                        ))
                    {
                        Write-Verbose 'Single request upload'
                        $WebRequestParams.Body = $RawContent

                        Write-Verbose ("Content-Length: {0}" -f $stream.Length)
                        if ($ShowProgress) {
                            Write-Progress -Activity $uploadString -Status "Content upload [0-$($stream.Length)/$($stream.Length)]" -PercentComplete 99
                        }
                        $wr = Invoke-WebRequest @WebRequestParams @GDriveProxySettings
                    }
                    else {
                        Write-Verbose 'Multiple requests upload'
                        [byte[]]$buffer = New-Object byte[] $ChunkSize
                        do {
                            [long]$nextSize = [Math]::Min($UploadedSize + $ChunkSize, $stream.Length)
                            $Range = "bytes $($UploadedSize)-$($nextSize-1)/$($stream.Length)"
                            $Length = [Math]::Min($stream.Length - $UploadedSize, $ChunkSize)
                            $WebRequestParams.Headers["Content-Range"] = $Range
                            # last buffer can be smaller
                            if ($Length -lt $ChunkSize) {
                                [byte[]]$buffer = New-Object byte[] $Length
                            }
                            $len = $stream.Read($buffer, 0, $Length);
                            if ($len -ne $Length) {
                                throw "Stream read error: Readed $len bytes instead of $Length"
                            }
                            $WebRequestParams.Body = $buffer
                            Write-Verbose ("Content-Length: {0}, Content-Range: {1}, readed: {2}" -f $Length, $Range, $len)
                            if ($ShowProgress) {
                                Write-Progress -Activity $uploadString -Status "Content upload [$($Range -replace 'bytes ')]" -PercentComplete ($nextSize*100/$stream.Length)
                            }
                            $wr = Invoke-WebRequest @WebRequestParams @GDriveProxySettings
                            switch ($wr.StatusCode) {
                                308 {
                                        # bytes=0-262143
                                        $ReceivedRange = $wr.Headers['Range'] | Select-Object -First 1
                                        Write-Verbose "Received Range: $ReceivedRange"
                                        if ($ReceivedRange -match 'bytes=(\d+)-(\d+)')
                                        {
                                            $UploadedSize = ([long]$matches[2]) + 1
                                            Write-Verbose "Stream Position: $($stream.Position), UploadedSize: $($UploadedSize)"
                                            if ($stream.Position -ne $UploadedSize)
                                            {
                                                Write-Verbose "Fast Forward to: $($UploadedSize)"
                                                [void]$stream.Seek($UploadedSize, [System.IO.SeekOrigin]::Begin)
                                            }
                                        }
                                    }
                                200 {}
                                201 {}
                                default { # Only for PSv7 because of SkipHttpErrorCheck
                                    $ex = New-Object Microsoft.PowerShell.Commands.HttpResponseException (($wr.StatusDescription + " while upload chunk"), $wr.BaseResponse)
                                    $er = New-Object System.Management.Automation.ErrorRecord ($ex, "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand", 'InvalidOperation', $wr)
                                    $er.ErrorDetails = New-Object System.Management.Automation.ErrorDetails $wr.Content
                                    Write-Error -ErrorAction Stop -ErrorRecord $er
                                }
                            }
                        } until ($wr.StatusCode -in 200, 201)
                    }
                    $UploadResult.Item = ($wr.Content | ConvertFrom-Json)
                }
                $UploadResult
            }
            catch {
                $UploadResult.Error = $_
                $UploadResult
                Write-Error $_.Exception
            }
        }
    }
    finally {
        if ($ShowProgress) {
            Write-Progress -Activity $uploadString -Completed
        }
        $stream.Close()
        $stream.Dispose()
    }
}