#PowerShell, #PowerWiseScripting, #ProjectWise, PWPS_DAB

HowTo: Mirror Project Documents between Datasources

Be sure to check out my Scripting4Crypto initiative. It’s a fun way to get into using cryptocurrencies all while getting your PowerShell needs met.

In this post, I am going to expand on my previous post, “HowTo: Mirror Project Folders between Datasources“,  and mirror the documents contained in the folders of a Work Area in one datasource to a corresponding Work Area in a second datasource. Once again, we will take advantage of the ProjectWise session capabilities. This will allow us to be logged into both datasources simultaneously, and switch back and forth.

A few caveats regarding this post; it assumes you are logged into the source and target datasources and that the folders have been mirrored per my previous post. Also, we will not be bringing over document versions, and we will not be recreating and populating flat sets.

NOTE: We will be using the source folder objects ($SourceFolders) for this process.

One final note, I have added a lot of Write-Host statements taking advantage of the ability to set the color of the text. This is for demonstration purposed only. These would be Write-Verbose, Write-Warning and Write-Error for a production script.

We will be using the following cmdlets to accomplish this task. All of the ProjectWise related cmdlets are available using the PWPS_DAB module. At the time of this post, I am using version 1.21.7.0. Take a look at the help for each of the cmdlets to become familiar with their functionality, available parameters, etc.

  • Get-PWSessions
  • Get-PWCurrentDatasource
  • Set-PWSession
  • Get-PWDocumentsBySearch
  • CheckOut-PWDocuments
  • Remove-PWDocuments
  • Get-FileHash
  • Copy-PWDocumentsBetweenDatasources
  • Update-PWDocumentFile

Get ALL Documents for Each Work Area

Here we will be obtaining a list of ALL documents within both the Source and Target Work Areas. We will use the Set-PWSession to switch between the datasources as we go. Getting all of the documents at one time eliminates the need to make many calls to the database to continually request this data. Once we obtain all of the document objects, we can easily select only the document objects we want to work with from either array.

Switching Between Datasources

We can use the Get-PWCurrentDatasource to determine which datasource is active. Then we will use the Set-PWSession to set the one we want active.  In this instance we are logged into the Target datasource but we want to be in the Source datasource.

setsession

Get Source Documents

The first thing we need to do is ensure the Source datasource is active by switching to it using the Set-PWSession cmdlet. Then we will use the Get-PWDocumentsBySearch cmdlet to retrieve all of the documents from the Source Work Area. Again, using Write-Host to return information to the console.  As you can see, we are copying out all of the source  documents to the local working directory. This is to facilitate the retrieval of the file checksum later in the script.

#region Get ALL Source documents.
try {
    # Set the Source datasource active.
    if( -not ((Get-PWCurrentDatasource) -eq $SourceDatasource)) {
        Write-Host "Switching to '$SourceDatasource' datasource ProjectWise Session." -ForegroundColor Green
        $null = Set-PWSession $SourceDatasource
    }

    # Get ALL documents from source Work Area / folder.
    Write-Host "Getting source documents from '$SourceFolderRoot'." -ForegroundColor Cyan
    $pwDocuments_Source = Get-PWDocumentsBySearch -FolderPath $SourceFolderRoot -GetAttributes -WarningAction SilentlyContinue |
    CheckOut-PWDocuments -CopyOut -WarningAction SilentlyContinue

    # If no source documents are found, ensure the corresponding Work Area / folder is empty.
    if( -not ($pwDocuments_Source)) {
        Write-Host " - No source documents returned for folder." -ForegroundColor Magenta 
    } else {
        Write-Host " - '$($pwDocuments_Source.Count)' source documents returned." -ForegroundColor Cyan
    }
} catch {
    Write-Warning -Message "Error occurred while getting source documents."
    throw $($Error[0].Exception.Message)
}
#endregion Get ALL Source documents.

Get Target Documents

Now, we simply repeat the process for the Target Work Area.

#region Get ALL Target documents.
try {
    # Set the Target datasource active.
    if( -not ((Get-PWCurrentDatasource) -eq $TargetDatasource)) {
        Write-Host "Switching to '$TargetDatasource' datasource ProjectWise Session." -ForegroundColor Green
        $null = Set-PWSession $TargetDatasource
    }

    # Get ALL documents from target Work Area / folder.
    Write-Host "Getting target documents from '$DestinationFolderRoot'." -ForegroundColor Cyan
    $pwDocuments_Target = Get-PWDocumentsBySearch -FolderPath $DestinationFolderRoot -GetAttributes -WarningAction SilentlyContinue

    # If no source documents are found, ensure the corresponding folder is empty.
    if( -not ($pwDocuments_Target)) {
        Write-Host " - No target documents returned for folder." -ForegroundColor Magenta 
    } else {
        Write-Host " - '$($pwDocuments_Target.Count)' target documents returned." -ForegroundColor Cyan
    }
} catch {
    Write-Warning -Message "Error occurred while getting target documents."
    throw $($Error[0].Exception.Message)
}
#endregion Get ALL Target documents.

The following shows the document counts retrieved for both the source and target projects.

gettingdocuments

Compare Source to Target

During this process we will focus on a few folders to demonstrate the functionality.

First, the BIM folder contains documents in the source but not the target. So, the documents will need to be copied over.

bimsourcebimtarget

Second, the ‘BSI900 – Request for Bid.docx’ was updated within the source. The document within the target will be updated, by replacing the associated physical file.

docsourcedoctarget

Third, the ‘Lynn Strunk Mechanical Consultants’ folder no longer contains any documents within the source datasource. The corresponding target folder will need to be emptied.

foldersource

foldertarget


Here we will loop through each of the source folders and retrieve the documents from the $pwDocuments_Source array which reside in each folder. We will need to switch to the source datasource.  Also, for each time through the loop, we will create two arraylists; one to store documents to be copied and one to store documents to be updated.

<# Loop through each source folder to obtain all document objects.
Switch to target datasource and do the same for the corresponding folder.
If the target folder does not contain any documents, copy all.
Otherwise, compare each document to determine if it is up to date
by comparing date and size information. #>
foreach($sourceFolder in $SourceFolders){

    try { 
        # Set the Source datasource active.
        if( -not ((Get-PWCurrentDatasource) -eq $SourceDatasource)) {
            Write-Host "Switching to '$SourceDatasource' datasource ProjectWise Session." -ForegroundColor Green
            $null = Set-PWSession $SourceDatasource
        }

        # ArrayList to store documents to copy.
        $pwDocsToCopy = [Collections.ArrayList]::New()
        $pwDocsToUpdate = [Collections.ArrayList]::New()
        $SourceFolderIsEmpty = $false

        Write-Host "Source folder: '$($sourceFolder.Name)' ; FullPath: $($sourceFolder.FullPath)" -ForegroundColor Cyan

        try {
            # Get documents from source for current folder.
            $currentDocs_Source = $pwDocuments_Source | Where-Object FolderPath -eq $SourceFolder.FullPath

If no source documents are returned, we need to ensure the corresponding target folder is empty as well. We flag the folder to be emptied by setting variable $SourceFolderIsEmpty to true.

            # If no source documents are found, ensure the corresponding folder is empty. 
            if( -not ($currentDocs_Source)) {
                Write-Host " - No source documents returned for folder." -ForegroundColor Magenta
                $SourceFolderIsEmpty = $true 
            } else {
                Write-Host " - '$($currentDocs_Source.Count)' source documents returned." -ForegroundColor Cyan
            }
        } catch {
            Write-Warning -Message "Error occurred while getting source documents."
            throw $($Error[0].Exception.Message)
        }

Now, we need to switch to the target datasource. We will do the same thing and retrieve all documents in the corresponding target folder. If the target folder is empty, we will add each of the source documents to the $pwDocsToCopy arraylist. Otherwise, we will start the process of comparing each document.

        # Set the Target datasource active.
        if( -not ((Get-PWCurrentDatasource) -eq $TargetDatasource)) {
            Write-Host "Switching to '$TargetDatasource' datasource ProjectWise Session." -ForegroundColor Green
            $null = Set-PWSession $TargetDatasource
        } 

        # Get the Target Work Area folder name. 
        $tempDestination = $SourceFolder.FullPath.Replace($SourceFolderRoot, $DestinationFolderRoot)
        Write-Host "Target folder: '$($tempDestination)'" -ForegroundColor Cyan

        try {
            # Get documents from target for current folder.
            $currentDocs_Target = $pwDocuments_Target | Where-Object FolderPath -eq $tempDestination

            if( -not ($currentDocs_Target)) {
                Write-Host " - is empty. Copy all documents." -ForegroundColor Cyan

                # Add each document to the pwDocsToCopy ArrayList.
                foreach($d in $currentDocs_Source) {
                    $null = $pwDocsToCopy.add($d)
                }
            } else {
                Write-Host " - '$($currentDocs_Target.Count)' target documents returned." -ForegroundColor Cyan

The following shows the BIM folder requiring all documents to be copied over to the target datasource.

bim

If the source folder was empty, here is where we will remove any documents in the target folder using the Remove-PWDocuments cmdlet. Then we will continue to the next source folder in the loop.

                #region EMPTY Target Folder
                # If source folder is empty, remove any documents from the corresponding target folder.
                if($SourceFolderIsEmpty){
                    Write-Warning -Message "'$($sourceFolder.Name)' is empty. Removing all documents from corresponding target folder."
                    try{
                         Write-Host "'$($sourceFolder.Name)' is empty. Removing all documents from corresponding target folder." -ForegroundColor DarkCyan
                        $null = Remove-PWDocuments -InputDocument $currentDocs_Target -ErrorAction Stop
                    } catch {
                        Write-Warning -Message "Error occurred while attempting to remove documents from '$tempDestination'. $($Error[0].Exception.Message)"
                    }
                    continue
                } 
                #endregion EMPTY Target Folder

The following shows the ‘Lynn Strunk Mechanical Consultants’ folder no longer contains any documents within the source. Therefore, all documents are removed from the target folder.

folder

Let’s get into the compare process.  I am going to compare each file three different ways, by file updated date, file size, and file checksum. This is to demonstrate each of the methods.   If this was in a function, you could specify which method to use. Keep in mind these are the methods I developed. You may have a better way. If so, please share.

Here we will loop through each of the source documents returned for the current folder. For each document we will capture the file updated date and the file size. We will also determine the file checksum using the Get-FileHash cmldet. This does require that the document be copied out to the local working directory.

                #region COMPARE DOCUMENTS
                foreach($cds in $currentDocs_Source) {
                    # Source document info
                    Write-Host "Source document: '$($cds.FullPath)'" -ForegroundColor DarkCyan

                    # If current document is a flat set file, skip.
                    if($cds.IsSet -eq $true) { 
                        Write-Host " - '$($cds.Name)' is a set file. Skipping.'" -ForegroundColor Magenta 
                        continue
                    }

                    $sourceFileUpdateDate = $cds.FileUpdateDate
                    $sourceFileSize = $cds.FileSize
                    $sourceFileCheckSum = Get-FileHash -Path $cds.CheckedOutLocalFileName -Algorithm SHA256
                    
                    Write-Host "File Name: '$($cds.Name)'" -ForegroundColor DarkCyan
                    Write-Host " - file updated date '$sourceFileUpdateDate'." -ForegroundColor DarkCyan 
                    Write-Host " - file size '$sourceFileSize'." -ForegroundColor DarkCyan
                    Write-Host " - file checksum '$($sourceFileCheckSum.Hash)'." -ForegroundColor DarkCyan

The following is the message received when a document is a set file.

set

For each source document we will determine if a corresponding target document exists. If it does, we will copy out the document to the local working directory to be used to get the checksum. If the source document is a set file, we will skip it and continue to the next document in the loop.  If there isn’t a target document found, we will add the current source document to the $pwDocsToCopy arraylist. And finally, if the source document does not have a physical file associated with it, we will continue to the next source document in the loop.

                   # Target document info
                   $tempFolder = Split-Path $cds.FolderPath -Leaf
                   $tempFolder = $tempFolder.Replace($SourceFolderRoot, $DestinationFolderRoot)

                   $cdt = $currentDocs_Target | 
                   Where-Object { ($_.FolderPath).contains($tempFolder) -and $_.Name -eq $cds.Name } |
                   CheckOut-PWDocuments -CopyOut -WarningAction SilentlyContinue

                   # If document is not found within the target folder, add to the pwDocsToCopy ArrayList.
                   if(-not ($cdt)) {
                       if($cds.FileName){ 
                           Write-Host " - '$($cds.Name)' not found in target folder. Adding to documents to copy arraylist." -ForegroundColor Magenta 
                           $null = $pwDocsToCopy.add($cds)
                       } else {
                           Write-Host " - '$($cds.Name)' does not have a file associated with it.'" -ForegroundColor Magenta
                       }
                       continue
                   }

If a target document is returned we will capture the file updated date, the file size and the file checksum.

                $targetFileUpdateDate = $cdt.FileUpdateDate
                $targetFileSize = $cdt.FileSize
                $targetFileCheckSum = Get-FileHash -Path $cdt.CheckedOutLocalFileName -Algorithm SHA256

                Write-Host "Target document: '$($cdt.FullPath)'" -ForegroundColor DarkCyan
                Write-Host "File Name: '$($cdt.Name)'" -ForegroundColor DarkCyan
                Write-Host " - file updated date '$targetFileUpdateDate'." -ForegroundColor DarkCyan
                Write-Host " - file size '$targetFileSize'." -ForegroundColor DarkCyan
                Write-Host " - file checksum: '$($targetFileCheckSum.Hash)'." -ForegroundColor DarkCyan

Compare Files

Here we will do simple comparisons. If one fails, an error is thrown and the current source document will be added to the $pwDocsToUpdate arraylist.

                try { 
                    # Compare File Update Dates
                    if($sourceFileUpdateDate -gt $targetFileUpdateDate){ 
                        throw "Source file is newer than the Target file."
                    } else {
                        Write-Host "Source file is older than the target file." -ForegroundColor DarkCyan
                    }
                    # Compare File Size
                    if($sourceFileSize -ne $targetFileSize){
                        throw "File sizes are different."
                    } else {
                        Write-Host "Files are the same size." -ForegroundColor DarkCyan
                    }
                    # Compare File Checksum 
                    if($sourceFileCheckSum.Hash -ne $targetFileCheckSum.Hash){
                        throw "File checksums are different."
                    } else {
                        Write-Host "File checksums are the same." -ForegroundColor DarkCyan
                    }
                } catch {
                    Write-Host "$($Error[0].Exception.Message) Adding to arraylist to update file." -ForegroundColor Magenta

                    if( -not ($pwDocsToUpdate.Contains($cds))) {
                        $null = $pwDocsToUpdate.Add($cds) 
                    }
                    Continue
                }
            } # end foreach($cds in $currentDocs_Source...
            #endregion COMPARE DOCUMENTS
        }
    } catch {
        Write-Warning -Message "Error occurred while getting target documents."
        throw $($Error[0].Exception.Message)
    }

The following shows the ‘BSI900 – Request for Bid.docx’ needs to be updated because the source file is newer than the target.

doc

What we should have ended up with are two arraylists containing source document objects to be either used to update the physical files associated with the corresponding target documents or copied over to the target folder.

Copy Source Documents To Target

Here will will copy the documents to the target folder using the Copy-PWDocumentsBetweenDatasources cmdlet. We will need to switch to the source datasource. We will capture any errors using a try / catch.

    #region Copy documents.
    if($pwDocsToCopy.Count -gt 0){
        try{ 
            # Set the Source datasource active.
            if( -not ((Get-PWCurrentDatasource) -eq $SourceDatasource)) {
                Write-Host "Switching to '$SourceDatasource' datasource ProjectWise Session." -ForegroundColor Green
                $null = Set-PWSession $SourceDatasource
            }
            Write-Host "Copying $($pwDocsToCopy.Count) documents." -ForegroundColor Cyan

            foreach($doc in $pwDocsToCopy){
                $tempFolder = $doc.FolderPath.Replace($SourceFolderRoot, $DestinationFolderRoot)
                $Splat_CopyDocument = @{
                    InputDocument = $doc
                    TargetDatasource = $TargetDatasource
                    TargetFolderPath = $tempFolder
                }
                $results = Copy-PWDocumentsBetweenDatasources @Splat_CopyDocument -ErrorAction Stop -WarningAction Stop
                Write-Host "Copied '$($doc.Name)'." -ForegroundColor Yellow
            }
        } catch {
            Write-Warning -Message "Error occurred while copying documents."
            throw $($Error[0].Exception.Message)
        }
    }
    #endregion Copy documents.

Update Physical Files

Here will will update the target documents with the corresponding physical files from the source documents. We will be using the Update-PWDocumentFile cmdlet. We will need to switch to the target datasource. We will capture any errors using a try / catch.

    #region Update documents.
    if($pwDocsToUpdate.Count -gt 0){
        try { 

            # Set the Target datasource active.
            if( -not ((Get-PWCurrentDatasource) -eq $TargetDatasource)) {
                Write-Host "Switching to '$TargetDatasource' datasource ProjectWise Session." -ForegroundColor Green
                $null = Set-PWSession $TargetDatasource
            } 
            Write-Host "Updating $($pwDocsToUpdate.Count) documents." -ForegroundColor Cyan

            foreach($doc in $pwDocsToUpdate){
                $tempFolder = $doc.FolderPath.Replace($SourceFolderRoot, $DestinationFolderRoot)
                $InputDocument = $currentDocs_Target | Where-Object { ($_.FolderPath).contains($tempFolder) -and $_.Name -eq $doc.Name }
                $NewFilePathName = $doc.CheckedOutLocalFileName

                $Splat_UpdateDocument = @{
                    InputDocuments = $InputDocument
                    NewFilePathName = $NewFilePathName
                    KeepExistingFileName = $true
                }
                $results = Update-PWDocumentFile @Splat_UpdateDocument -ErrorAction Stop
                Write-Host "Updated '$($InputDocument.Name)'." -ForegroundColor Yellow
            }
        } catch {
            Write-Warning -Message "Error occurred while copying documents."
            throw $($Error[0].Exception.Message)
        }
    }
    #endregion Update documents.
    } catch {
        Write-Warning -Message "Error occurred while attempting to copy documents from '$($sourceFolder.FullPath)'. $($Error[0].Exception.Message)"
    }   
}
#endregion DOCUMENTS

Results

The following shows the results of the process.

First, all documents within the source ‘BIM_Models’ folder were copied over to the target datasource.HowTo-MirrorPWFoldersAndDocuments

final2

Second, you can see that the physical file associated with the ‘BSI900 – Request for Bid.docx’ file was replaced in the target datasource.

doc2

Third, all documents have been removed from the ‘Lynn Strunk Mechanical Consultants’ folder within the target datasource.

final1

Summary

In this post, we mirrored (copied or updated) the source documents to the corresponding documents within the target datasource. We used the file updated date, file size and file checksum values to compare each document to determine whether or not to update the existing physical file. Again, there are probably other ways to accomplish this task. If you have one, please share.


Experiment with it and have fun. And please, let me know if there is a topic you would like to see a post for. Or share a solution you have developed.

Below is a link to the MirrorPWFoldersAndDocuments.ps1 file.

HowTo-MirrorPWFoldersAndDocuments

Hopefully, you find this useful. Please let me know if you have any questions or comments.  If you like this post, please click the Like button at the bottom of the page. And thank you for checking out my blog.

12 thoughts on “HowTo: Mirror Project Documents between Datasources”

  1. Hi Brian,
    Will this copy take into consideration the metadata-enviroment attributes? if both datasources have a matching enviroment? we are currently using import/export excel but would be fantastic if you can get it through powershell.

    Like

  2. Hi Brian, you say it does not take versions into accout. What happens if there are versions? What if v2 is present in target folder and we want to send v3 over. Will it create a new version? Replace the file on v2? Create new document? Thanks in advance

    Like

    1. Hi Dora,
      No versions will be created. If a document exists within the target datasource matching a document within the source (must be an active document), the target document will be updated. No versions will be created. If that is the desired result, you could update the script to create a new version. Take a look at the New-PWDocumentVersion cmdlet. I hope this helps.
      Cheers,
      Brian

      Like

  3. Love your work here as I stood up a new datasource to use for a smaller version of our Published files. An issue I am seeing is with regard to Copy-PWDocumentsBetweenDatasources. the dates are off by 8 hours. obviously, PST is not being adhered to. Example: Source created time = 2/19/2003 5:28:03 PM and Target after the cmdlet copy = 2/19/2033 9:28:03 AM.

    To ensure my servers are time synced I dragged/dropped a file into both source and target folders and the date time comes up correctly.

    is there a means in the cmdlet to convert datetime string appropriately?

    Like

  4. This may be a repeat of a comment, but not sure as I made some post errors. But, using Copy-PWDocumentsBetweenDatasources appears to set the datetime stamp for Create and file update as 8 hours off. Example: source create time = 2/19/2003 5:28:03 PM and the target after the copy process = 2/19/2003 9:28:03 AM.

    I did test out with a copy/drag to both sources a file and the timestamp is correct so it is not the server time that is the problem.

    is there utc option that is not being account for? PST settings?

    Like

  5. First of, love the blog. You have dug me out of many holes in the last 12 months as my ProjectWise and PowerShell journey has begun, so thank you!

    Is there a way to do this for documents on the same datasource? Currently we have a Work Area where we keep all company templates and then we create a new document using PW Explorer we just copy the template from that work area, nice and easy.
    We are now looking to move most of our staff to the 365 platform but this does not have the ability to pull documents from another work area so we have begun looking at having a mirrored copy of all the templates in each scheme. The issue with this is that we have 300 odd templates and 50 active work areas so keeping these all synced up manually would become a full time job.
    Is there a way of using PowerShell to either monitor the parent template folder and then distribute updated templates to the scheme folders or can even something I have to run each time I update a template? Any pointers gratefully recieved!

    Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.