Jun 13, 2017

PowerShell Script: Upload Large File to SharePoint in Chunks

Get back to the business again! Just wanted to share another great things in SharePoint (although to user is not fun at all).

According to MSDN, there are a few options to upload files to SharePoint.

File upload option Considerations When should you use this? Supported platforms
Content property on the FileCreationInformation class. Maximum file size that can be uploaded is 2 MB. Time-out occurs after 30 minutes. Use to upload files that are less than 2 MB only. SharePoint Server 2013, SharePoint Online
SaveBinaryDirect method on the File class. No file size limits. Time-out occurs after 30 minutes. Only use this method if you're using a user-only authentication policy. User-only authentication policy is not available in an add-in for SharePoint, but can be used in native device apps, Windows PowerShell, and Windows console applications. SharePoint Server 2013, SharePoint Online
ContentStream property on the FileCreationInformation class. No file size limits. Time-out occurs after 30 minutes. Recommended for: SharePoint Server 2013, SharePoint Online
- SharePoint Server 2013.
- SharePoint Online when the file is smaller than 10 MB.
Upload a single file as a set of chunks using theStartUpload,ContinueUpload, andFinishUpload methods on the File class. No file size limits. Time-out occurs after 30 minutes. Each chunk of the file must upload within 30 minutes of completion of the previous chunk to avoid the time-out. Recommended for SharePoint Online when the file is larger than 10 MB. SharePoint Online

Now, here’s how you can upload large file to SharePoint in chunks.



[Cmdletbinding()]
param (
	[Parameter(Mandatory=$true)]
    [System.Object]$credential = $null,
	[Parameter(Mandatory=$true)]
    [System.String]$SiteUrl = $null,
	[Parameter(Mandatory=$true)]
    [System.String]$LibraryTitle = $null,
	[Parameter(Mandatory=$false)]
    [System.String]$UploadFolder = $null,
	[Parameter(Mandatory=$true)]
    [System.String]$FilePath = $null,
	[Parameter(Mandatory=$false)]
    [int]$ChunkSize = 10
)

function Using-Object
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [AllowEmptyCollection()]
        [AllowNull()]
        [Object]
        $InputObject, 
        [Parameter(Mandatory = $true)]
        [scriptblock]$ScriptBlock
    )
 
    try
    {
        . $ScriptBlock
    }
    catch
    {
        Write-Host $_.Exception.Message -ForegroundColor Red
    }
    finally
    {
        if ($null -ne $InputObject -and $InputObject -is [System.IDisposable])
        {
            $InputObject.Dispose()
        }
    }
}


Using-Object ($ctx = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl)) {
	#set the credentials, yada...yada...
	if ($credential.GetType().Name -eq "SharePointOnlineCredentials") {
		$ctx.Credentials = $credential
	}
	else {
		$ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($credential.UserName, $credential.Password)
	}

    # Each sliced upload requires a unique ID.
    $UploadId = [GUID]::NewGuid()

    # Get the name of the file.
    $UniqueFileName = [System.IO.Path]::GetFileName($FilePath)

	$SubWeb = $ctx.Web
    $ctx.Load($SubWeb)
    $ctx.ExecuteQuery()
	
    # Get the folder to upload into. 
    $Docs = $SubWeb.Lists.GetByTitle($LibraryTitle)
    $SubFolder = $Docs.RootFolder
    $ctx.Load($Docs)
    $ctx.Load($SubFolder)
    $ctx.ExecuteQuery()

    if ($UploadFolder)
    {
        $SubFolder = $SubWeb.GetFolderByServerRelativeUrl("$($SubFolder.ServerRelativeUrl)$($UploadFolder)")
        $ctx.Load($SubFolder)
        $ctx.ExecuteQuery()
    }

    # Get the information about the folder that will hold the file.
    $ServerRelativeUrlOfRootFolder = $SubFolder.ServerRelativeUrl

    # File object.
    [Microsoft.SharePoint.Client.File]$upload

    # Calculate block size in bytes.
    $BlockSize = $ChunkSize * 1024 * 1024

    # Get the size of the file.
    $FileSize = (Get-Item $FilePath).length

    if ($FileSize -le $BlockSize)
    {
        Write-Host "[$([System.DateTime]::Now.ToString("dd-MM-yyyy HH:mm:ss"))] Uploading '$($UniqueFileName)' (normal)"
        # Use regular approach.
        Using-Object ($FileStream = New-Object IO.FileStream($FilePath,[System.IO.FileMode]::Open)) {
            $FileCreationInfo = New-Object Microsoft.SharePoint.Client.FileCreationInformation
            $FileCreationInfo.Overwrite = $true
            $FileCreationInfo.ContentStream = $FileStream
            $FileCreationInfo.URL = $UniqueFileName
            $Upload = $SubFolder.Files.Add($FileCreationInfo)
            $ctx.Load($Upload)
            $ctx.ExecuteQuery()
            #return $Upload
        }
        Write-Host "[$([System.DateTime]::Now.ToString("dd-MM-yyyy HH:mm:ss"))] File upload complete!"
    }
    else
    {
        # Use large file upload approach.
        $BytesUploaded = $null
        $Fs = $null
        Try
        {
            $Fs = [System.IO.File]::Open($FilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
            Using-Object ($br = New-Object System.IO.BinaryReader($Fs)) {
                $buffer = New-Object System.Byte[]($BlockSize)
                $lastBuffer = $null
                $fileoffset = 0
                $totalBytesRead = 0
                $bytesRead
                $first = $true
                $last = $false
                $chunkIndex = 0
                $chunkName = "first"
                Write-Host "[$([System.DateTime]::Now.ToString("dd-MM-yyyy HH:mm:ss"))] Uploading '$($UniqueFileName)' (chunks)"

                # Read data from file system in blocks. 
                while(($bytesRead = $br.Read($buffer, 0, $buffer.Length)) -gt 0) {
                    $chunkIndex += 1
                    $totalBytesRead = $totalBytesRead + $bytesRead

                    # You've reached the end of the file.
                    if($totalBytesRead -eq $FileSize) {
                        $chunkName = "last"
                        $last = $true
                        # Copy to a new buffer that has the correct size.
                        $lastBuffer = New-Object System.Byte[]($bytesRead)
                        [array]::Copy($buffer, 0, $lastBuffer, 0, $bytesRead)
                    }

                    If($first)
                    {
                        $ContentStream = New-Object System.IO.MemoryStream
                        # Add an empty file.
                        $fileInfo = New-Object Microsoft.SharePoint.Client.FileCreationInformation
                        $fileInfo.ContentStream = $ContentStream
                        $fileInfo.Url = $UniqueFileName
                        $fileInfo.Overwrite = $true
                        $Upload = $SubFolder.Files.Add($fileInfo)
                        $ctx.Load($Upload)

                        # Start upload by uploading the first slice.
                        $s = [System.IO.MemoryStream]::new($buffer)
                        # Call the start upload method on the first slice.
                        $BytesUploaded = $Upload.StartUpload($UploadId, $s)
                        $ctx.ExecuteQuery()
                        $s.Dispose()

                        # fileoffset is the pointer where the next slice will be added.
                        $fileoffset = $BytesUploaded.Value
                        # You can only start the upload once.
                        $first = $false
                        $ContentStream.Dispose()
                    }
                    Else
                    {
                        # Get a reference to your file.
                        $Upload = $ctx.Web.GetFileByServerRelativeUrl($SubFolder.ServerRelativeUrl + [System.IO.Path]::AltDirectorySeparatorChar + $UniqueFileName);
                        If($last) {
                            # Is this the last slice of data?
                            $s = [System.IO.MemoryStream]::new($lastBuffer)
                            # End sliced upload by calling FinishUpload.
                            $Upload = $Upload.FinishUpload($UploadId, $fileoffset, $s)
                            $ctx.ExecuteQuery()
                            $s.Dispose()
                            # Return the file object for the uploaded file.
                            #return $Upload
                        }
                        else {
                            $chunkName = "middle"
                            $s = [System.IO.MemoryStream]::new($buffer)
                            # Continue sliced upload.
                            $BytesUploaded = $Upload.ContinueUpload($UploadId, $fileoffset, $s)
                            $ctx.ExecuteQuery()

                            # Update fileoffset for the next slice.
                            $fileoffset = $BytesUploaded.Value
                            $s.Dispose()
                        }
                    }
                    Write-Host "[$([System.DateTime]::Now.ToString("dd-MM-yyyy HH:mm:ss"))] Chunk $($chunkIndex) ($($chunkName)) finished"
                    if ($last) {
                        Write-Host "[$([System.DateTime]::Now.ToString("dd-MM-yyyy HH:mm:ss"))] File upload complete!"
                    }
                }
            }
        }
        Catch {
            Write-Host $_.Exception.Message -ForegroundColor Red
        }
        Finally {
            if ($Fs -ne $null)
            {
                $Fs.Dispose()
            }
        }
    }
    #return $null
}