There are many examples available that shows how to bulk migrate or upload files to SharePoint but none of those are providing a method to migrate with metadata.  The script below can be used to migrate complicated folder hierarchy to be uploaded to SharePoint Online or On-Premises.  The script is based on the the effort on Technet example https://gallery.technet.microsoft.com/PowerShell-Bulk-Upload-b9e9d600.  The same script can be modified to upload large amount of metadata.  You just need to export the files list to a CSV and then read values after the upload.  I will upload the other example later.

The script was uploaded to my blog back in February.

https://jerryyasir.wordpress.com/2019/01/23/bulk-upload-migrate-file-shares-to-sharepoint-on-premises-and-online/

 

PowerShell
Edit|Remove
[CmdletBinding()] 
param( 
[Parameter(Mandatory=$false,Position=1)] 
[String]$Credentials, 
[Parameter(Mandatory=$True, Position=3)] 
[String]$SiteURL, 
[Parameter(Mandatory=$True, Position=4)] 
[String]$DocLibName, 
[Parameter(Mandatory=$True, Position=5)] 
[String]$Folder, 
[Parameter(Mandatory=$False, Position=6)] 
[Switch]$Checkin, 
[Parameter(Mandatory=$False, Position=7)] 
[Switch]$O365 
) 
 
<# 
    Add references to SharePoint client assemblies and authenticate to Office 365 site - required for CSOM 
 
Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll" 
Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll" 
#> 
 
Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll" -ErrorAction SilentlyContinue 
Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll" -ErrorAction SilentlyContinue 
 
Add-Type -Path ([System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client").location) 
Add-Type -Path ([System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.runtime").location) 
 
 
function GetUserLookupString{ 
    [CmdletBinding()] 
    param($context$userString) 
     
    try{ 
        $user = $context.Web.EnsureUser($userString) 
        $context.Load($user) 
        $context.ExecuteQuery() 
         
        # The "proper" way would seem to be to set the user field to the user value object 
        # but that does not work, so we use the formatted user lookup string instead 
        #$userValue = New-Object Microsoft.SharePoint.Client.FieldUserValue 
        #$userValue.LookupId = $user.Id 
        $userLookupString = "{0};#{1}" -$user.Id, $user.LoginName 
    } 
    catch{ 
        Write-Host "Unable to ensure user '$($userString)'." -ErrorAction SilentlyContinue 
        $userLookupString = $null 
    } 
     
    return $userLookupString 
} 
 
<# 
    Define Functions 
#> 
 
<# 
    Upload File - This function performs the actual file upload 
#> 
function UploadFile($DestinationFolder$File) 
{ 
    #Get the datastream of the file, assign it to a variable 
    $FileStream = New-Object IO.FileStream($File.FullName,[System.IO.FileMode]::Open) 
 
    #Create an instance of a FileCreationInformation object 
    $FileCreationInfo = New-Object Microsoft.SharePoint.Client.FileCreationInformation 
 
    #Indicate whether or not you would like to overwrite files in the event of a conflict 
    $FileCreationInfo.Overwrite = $True 
 
    #Make the datastream of the file you wish to create equal to the datastream of the source file  
    $FileCreationInfo.ContentStream = $FileStream 
 
    #Make the URL of the file equal to the $File variable which was passed to the function.  This will be equal to the source file name 
    $FileCreationInfo.url = $File 
 
    #Add the file to the destination folder which was passed to the function, using the FileCreationInformation supplied.  Assign this to a variable so that it can be loaded into context. 
    $Upload = $DestinationFolder.Files.Add($FileCreationInfo) 
    if($Checkin) 
    { 
        $Context.Load($Upload) 
        $Context.ExecuteQuery() 
        if($Upload.CheckOutType -ne "none") 
        { 
            $Upload.CheckIn("Checked in by Administrator", [Microsoft.SharePoint.Client.CheckinType]::MajorCheckIn) 
        } 
    } 
    $Context.Load($Upload) 
    $Context.ExecuteQuery() 
 
    Write-Host "Adding Metadata to File Item...$($File.FullName)" 
            $listItem = $Upload.ListItemAllFields 
            $Context.Load($listItem) 
            #$Context.ExecuteQuery() 
            $Context.ExecuteQuery() 
     
            $Item = $List.GetItemById($listItem.Id); 
            $Context.Load($Item) 
 
            $Owner = $File.GetAccessControl().Owner 
            $LastModifiedBy =Get-ChildItem $File.FullName | Sort {$_.LastWriteTime} | select -last 1 | foreach {$a=$_;$b=Get-Acl $_.FullName; Add-Member -InputObject $b -Name "LastWriteTime" -MemberType NoteProperty -Value $a.LastWriteTime;$b} 
             
            if($Owner -eq "BUILTIN\Administrators") 
            { 
                $Author = "hp\spadmin" 
            } 
 
            if($LastModifiedBy.Owner -eq "BUILTIN\Administrators") 
            { 
                $Editor = "hp\spadmin" 
            } 
       
            $Author = GetUserLookupString $Context $Author 
            if($Author) 
            { 
                $Item["Author"] = $Author 
            } 
            $Editor = GetUserLookupString $Context $Editor 
            if($Editor) 
            { 
                $Item["Editor"] = $Editor 
            } 
         
            [System.DateTime]$CreatedOnDate = $File.CreationTime 
            [System.DateTime]$ModifiedOnDate = $File.LastWriteTime 
 
            $Item["Created"] = $CreatedOnDate 
            $Item["Modified"] = $ModifiedOnDate 
            try 
            { 
                $Item.Update() 
                $Context.ExecuteQuery() 
            } 
            catch 
            { Write-Host "Exception Occured in reading list item properties... But script will continue" } 
             
} 
 
<# 
    Create Folder Function. 
#> 
function PopulateFolder($ListRootFolder$FolderRelativePath$FolderFullPath) 
{ 
    #split the FolderRelativePath passed into chunks (between the backslashes) so that we can check if the folder structure exists 
    $PathChunks = $FolderRelativePath.substring(1).split("\") 
 
    #Make sure we start with a fresh WorkingFolder for every folder passed to the function 
    if($WorkingFolder) 
    { 
        Remove-Variable WorkingFolder 
    } 
 
    #Start with the root folder of the list, load this into context 
    $WorkingFolder = $ListRootFolder 
    $Context.load($WorkingFolder) 
    $Context.ExecuteQuery() 
 
    #Load the folders of the current working folder into context 
    $Context.load(($WorkingFolder.folders)) 
    $Context.executeQuery() 
 
    #Set the FileSource folder equal to the absolute path of the folder that passed to the function 
    $FileSource = $Folder + $FolderRelativePath 
     
    #Loop through the folder chunks, ensuring that the correct folder hierarchy exists in the destination 
    foreach($Chunk in $PathChunks) 
    { 
        #Check to find out if a subfolder exists in the current folder that matches the patch chunk being evaluated 
        if($WorkingFolder.folders | ? {$_.name -eq $Chunk}) 
        { 
            #Log the status to the PowerShell host window 
            Write-Host "Folder $Chunk Exists in" $WorkingFolder.name -ForegroundColor Green 
 
            #Since we will be evaluating other chunks in the path, set the working folder to the current folder and load this into context. 
            $WorkingFolder = $WorkingFolder.folders | ? {$_.name -eq $Chunk} 
            $Context.load($WorkingFolder) 
            $Context.load($WorkingFolder.folders) 
            $Context.ExecuteQuery() 
 
        } 
        else 
        { 
            #If the folder doesn't exist, Log a message indicating that the folder doesn't exist, and another message indicating that it is being created 
            Write-Host "Folder $Chunk Does Not Exist in" $WorkingFolder.name -ForegroundColor Yellow 
            Write-Host "Creating Folder $Chunk in" $WorkingFolder.name -ForegroundColor Green 
             
            #Load the working folder into context and create a subfolder with a name equal to the chunk being evaluated, and load this into context 
            $Context.load($WorkingFolder) 
            $Context.load($WorkingFolder.folders) 
            $Context.ExecuteQuery() 
 
            $WorkingFolder$WorkingFolder.folders.add($Chunk) 
            $Context.load($WorkingFolder) 
            $Context.load($WorkingFolder.folders) 
            $Context.ExecuteQuery() 
 
            #$FolderRelativePath = "C:\Demo\Corporate Docs" 
            $LocalFolder = Get-Item -Path $FolderFullPath 
 
            $Web = $Context.Web 
            $Context.Load($Web) 
            $Context.ExecuteQuery() 
 
            $SPFolder = $Web.GetFolderByServerRelativeUrl($WorkingFolder.ServerRelativeUrl) 
            $Context.Load($SPFolder) 
            $Context.Load($SPFolder.ListItemAllFields) 
            $Context.ExecuteQuery() 
            $FolderItemId = $SPFolder.ListItemAllFields.Id 
            $FItem = $List.GetItemById($FolderItemId) 
            $Context.Load($FItem) 
            $Context.ExecuteQuery() 
 
            #$Query = New-Object Microsoft.SharePOint.Client.CamlQuery 
            #$Query.ViewXml = "<View Scope='RecursiveAll'><Query><Where><And><Eq><FieldRef Name='ContentType'/><Value Type='Text'>Folder</Value></Eq><Eq><FieldRef Name='FileLeafRef'/><Value Type='Text'>" + $Chunk + "</Value></Eq></And></Where></Query></View>" 
 
            $FItem["Created"] = $LocalFolder.CreationTime.ToString() 
            $FItem["Modified"] = $LocalFolder.LastWriteTime.ToString() 
            $CreatedBy = $LocalFolder.GetAccessControl().Owner 
            if($CreatedBy -eq "BUILTIN\Administrators") 
            { 
                $CreatedBy = "hp\spadmin" 
            } 
 
            $Author = GetUserLookupString $Context $CreatedBy 
            if($Author) 
            { 
                $FItem["Editor"] = $Author 
                $FItem["Author"] = $Author 
            } 
            $Context.ExecuteQuery() 
 
            try 
            { 
                $FItem.Update() 
                $Context.ExecuteQuery() 
            } 
            catch 
            { Write-Host "Exception Occured in updating folder properties... But script will continue" } 
 
        } 
 
    } 
 
    #Folder is confirmed existing or created - now it's time to list all files in the source folder, and assign this to a variable 
    $FilesInFolder = Get-ChildItem -Path $FileSource | ? {$_.psIsContainer -eq $False} 
     
    #For each file in the source folder being evaluated, call the UploadFile function to upload the file to the appropriate location 
    Foreach ($File in ($FilesInFolder)) 
    { 
 
        #Notify the operator that the file is being uploaed to a specific location 
        Write-Host "Uploading file " $file.Name "to" $WorkingFolder.name -ForegroundColor Cyan 
 
        #Upload the file 
        UploadFile $WorkingFolder $File 
 
    } 
     
     
     
} 
 
 
<# 
    Bind your context to the site collection 
#> 
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteURL) 
 
<# 
    Create a credential object using the username and password supplied 
#> 
 
if($O365) 
{ 
    $Creds = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($Credentials.UserName,$Credentials.Password) 
} 
else 
{ 
    $Creds = [System.Net.CredentialCache]::DefaultCredentials 
    #$Creds = New-Object System.Net.NetworkCredential($Credentials.UserName,$Credentials.Password) 
} 
#if($O365) 
#{ 
#    $Creds = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName,(ConvertTo-SecureString $Password -AsPlainText -Force)) 
#} 
#else 
#{ 
#    #$Creds = New-Object System.Net.NetworkCredential($UserName, (ConvertTo-SecureString $Password -AsPlainText -Force)) 
#} 
 
<# 
    Set the credentials that are used in the context. 
#> 
$Context.Credentials = $Creds 
 
<# 
    Retrieve the library, and load it into the context 
#> 
$List = $Context.Web.Lists.GetByTitle($DocLibName$Context.Load($List$Context.ExecuteQuery() 
 
 
$List.Fields.GetByInternalNameOrTitle("Author").ReadOnlyField = $true 
$List.Fields.GetByInternalNameOrTitle("Editor").ReadOnlyField = $true 
$List.Fields.GetByInternalNameOrTitle("Created").ReadOnlyField = $true 
$List.Fields.GetByInternalNameOrTitle("Modified").ReadOnlyField = $true 
#$List.EnableVersioning = $True 
$List.Update() 
$Context.ExecuteQuery() 
 
#Get a recursive list of all folders beneath the folder supplied by the operator 
$AllFolders = Get-ChildItem -Recurse -Path $Folder |? {$_.psIsContainer -eq $True} 
 
#Get a list of all files that exist directly at the root of the folder supplied by the operator 
$FilesInRoot = Get-ChildItem -Path $Folder | ? {$_.psIsContainer -eq $False} 
 
#Upload all files in the root of the folder supplied by the operator 
Foreach ($File in ($FilesInRoot)) 
{ 
 
    #Notify the operator that the file is being uploaded to a specific location 
    Write-Host "Uploading file " $File.Name "to" $DocLibName -ForegroundColor Cyan 
 
    #Upload the file 
    UploadFile($list.RootFolder) $File 
     
 
} 
 
#Loop through all folders (recursive) that exist within the folder supplied by the operator 
foreach($CurrentFolder in $AllFolders) 
{ 
    #Set the FolderRelativePath by removing the path of the folder supplied by the operator from the fullname of the folder 
    $FolderRelativePath = ($CurrentFolder.FullName).Substring($Folder.Length) 
     
    #Call the PopulateFolder function for the current folder, which will ensure that the folder exists and upload all files in the folder to the appropriate location 
    PopulateFolder ($list.RootFolder) $FolderRelativePath $CurrentFolder.FullName 
}
 Calling the Scripts
PowerShell
Edit|Remove
#Calling the script for SharePoint on-premises 
 
$Creds = Get-Credential 
.\BulkUploadSharePointCSOM.ps1 -SiteURL "https://portal.hp.com" -DocLibName "Metadata" -Folder "C:\Demo" 
 
#Calling the script for SharePoint online 
 
$Creds = Get-Credential 
.\BulkUploadSharePointCSOM.ps1 -SiteURL "https://portal.hp.com" -DocLibName "Metadata" -Folder "C:\Demo" -Credentials $Creds -Checkin -O365