Powershell – Preserve Timestamps When Moving Data Between NTFS Drives

file-transferpowershell

I want to move a folder—containing many sub-folders and files—from one NTFS drive to another but I need the metadata date and timestamp attribute values preserved from the original source on the new destination drive.

The Problem: I've noticed when I complete various copy operations for the task, the timestamp values for the "creation date", "last modified date", etc. are not preserved on folders containing at least one folder/file. I've also noticed that empty folders that are moved will eventually and automatically change their creation date to conform with the rest as well as when place something in the empty folder on the new drive.

enter image description here

My Effort: I've tried nearly everything I could find via Google on this subject (Robocopy, Richcopy, Microsoft SyncToy, Total Commander, Free Commander…and many, many more…)—all of which produce relatively the same results. Nothing has produced a comprehensive, 100% preservation of the source being moved. Robocopy and Richcopy (along with the "Commanders") come close, but I still experience issues (in all cases) where the creation date is incorrectly "preserved", the last modified date cannot be preserved at all, etc. The only true promise I've seen beyond that…is Powershell.


My PowerShell Journey

I stumbled upon this link:

https://stackoverflow.com/questions/34951911/powershell-move-item-folder-time-stamp

…with this script:

function Move-FileWithTimestamp {
[cmdletbinding()]
param(
[Parameter(Mandatory=$true,Position=0)][string]$Path,
[Parameter(Mandatory=$true,Position=1)][string]$Destination
)

$origLastAccessTime = ( Get-Item $Path ).LastAccessTime
$fileName = ( Get-Item $Path ).Name
Move-Item -Path $Path -Destination $Destination
$(Get-Item ($Destination+'\'+$fileName)).LastAccessTime = 
$origLastAccessTime
}

The script in the thread above still didn't perform any differently than the programs I listed, but at least here, I had a platform where I could possibly change/customize/tweak some things for my exact needs. So I did what I could with my limited knowledge in this realm (i.e. changing ".LastAccessTime" with ".CreationTime", swapping ".LastAccessTime" with "LastWriteTime", etc.), and eventually got relatively close to preserving all timestamps (I believe at one point, I had preserved last modified, last accessed, and last saved on my test folder). However, I still cannot seem to preserve the creation date correctly, and what I was able to accomplish with everything else obviously only applied to the lone test folder (and nothing else, such as the sub-folders within it…but this is only because I don't know how to script those things beyond a main directory).

I'm in over my head when it comes to this stuff, so am wondering if anyone out there wants to tackle this.

UPDATE: here's where I'm at now:

function Move-FileWithTimestamp {
[cmdletbinding()]
param(
[Parameter(Mandatory=$true,Position=0)][string]$Path,
[Parameter(Mandatory=$true,Position=1)][string]$Destination
)
$origCreationTime = ( Get-Item $Path ).CreationTime
$origLastWriteTime = ( Get-Item $Path ).LastWriteTime
$origLastAccessTime = ( Get-Item $Path ).CreationTime

$fileName = ( Get-Item $Path ).Name
Move-Item -Path $Path -Destination $Destination
$(Get-Item ($Destination+'\'+$fileName)).CreationTime = $origCreationTime
$(Get-Item ($Destination+'\'+$fileName)).LastWriteTime = 
$origLastWriteTime
$(Get-Item ($Destination+'\'+$fileName)).LastAccessTime = 
$origLastAccessTime
}

This appears to maintain the original creation time for the main folder being moved (as well as the last modified/write time), but obviously the last accessed time changes to the original creation time in the process (it seems as though when a folder is moved to a new drive, Windows, by default, changes the last accessed time during that process, and ALSO refers to this new last accessed time to create the new creation time of the folder in its new location (it DOESN'T refer to the original creation time whatsoever apparantly). I.E. if you try and set the new creation time to equal the original creation time, nothing will result, because the new last accessed time will by default automatically change the new creation time to equal it. So, if you force Windows to make the new last accessed time equal to the original creation time, then you ultimately wind up with the correct creation time, but incorrect last accessed time.

So, now I am stuck with an incorrect last accessed time, but correct times for everything else. Also, I have no idea how I am going to get this to apply to all subfolders as well, so let me know if anyone knows how to do that.

UPDATE:

function Move-FileWithTimestamp {
[cmdletbinding()]
param(
[Parameter(Mandatory=$true,Position=0)][string]$Path,
[Parameter(Mandatory=$true,Position=1)][string]$Destination
)
$origCreationTime = ( Get-Item $Path ).CreationTime
$origLastWriteTime = ( Get-Item $Path ).LastWriteTime
$origLastAccessTime = ( Get-Item $Path ).CreationTime
$origChildCreationTime = ( Get-ChildItem $Path ).CreationTime
$origChildLastWriteTime = ( Get-ChildItem $Path ).LastWriteTime
$origChildLastAccessTime = ( Get-ChildItem $Path ).CreationTime


$fileName = ( Get-Item $Path ).Name
Move-Item -Path $Path -Destination $Destination
$(Get-Item ($Destination+'\'+$fileName)).CreationTime = $origCreationTime
$(Get-Item ($Destination+'\'+$fileName)).LastWriteTime = $origLastWriteTime
$(Get-Item ($Destination+'\'+$fileName)).LastAccessTime = 
$origLastAccessTime
$(Get-ChildItem ($Destination+'\'+$fileName)) | ForEach-Object { 
$_.CreationTime = $origChildCreationTime }
$(Get-ChildItem ($Destination+'\'+$fileName)) | ForEach-Object { 
$_.LastWriteTime = $origChildLastWriteTime }
$(Get-ChildItem ($Destination+'\'+$fileName)) | ForEach-Object { 
$_.LastAccessTime = $origChildLastAccessTime }
}

Now, I have a main folder and one if it's sub-folders with correct creation and last modified dates (but not last accessed). I have no idea how to accomplish this for the rest of the sub-folders in the main folder, and also for any sub-folders within those sub-folders.

Best Answer

Move directory tree and retain all timestamp attribute values

So your goal is to ensure that files and folders that are moved from a source location over to a destination location have their LastWriteTime, LastAccessTime, and CreationTime attribute values retained from the source location where those originated.

Essentially this. . .

  • Uses Copy-Item rather than Move-Item
  • Loops through source and sets timestamp attribute variables values to then use Set-ItemProperty to set those same values to those properties in the destination for all folders and files recursively
  • Explicitly does the same Set-ItemProperty timestamp attribute value set loop for folder objects only
  • Uses Remove-Item to then delete the original source file objects only cleaning those up
  • Uses Remove-Item to then delete the original source folder object only cleaning those up

Script

$src = "C:\Src\Folder\123\"
$dest = "X:\Dest\Folder\321\"
$src = $src.Replace("\","\\")

$i = Get-ChildItem -Path $src -Recurse
$i | % {     ## -- All files and folders

    $apath = $_.FullName -Replace $src,""
    $cpath = $dest + $apath

    Copy-Item -Path $_.FullName -Destination $cpath -Force

    If (Test-Path $cpath)
       {
           Set-ItemProperty -Path $cpath -Name CreationTime -Value $_.CreationTime
           Set-ItemProperty -Path $cpath -Name LastWriteTime -Value $_.LastWriteTime
           Set-ItemProperty -Path $cpath -Name LastAccessTime -Value $_.LastAccessTime
       }
    }

$d = Get-ChildItem -Path $src -Recurse -Directory
$d | % {     ## -- Folders only

    $apath = $_.FullName -Replace $src,""
    $cpath = $dest + $apath

    If (Test-Path $cpath)
       {
           Set-ItemProperty -Path $cpath -Name CreationTime -Value $_.CreationTime
           Set-ItemProperty -Path $cpath -Name LastWriteTime -Value $_.LastWriteTime
           Set-ItemProperty -Path $cpath -Name LastAccessTime -Value $_.LastAccessTime
       }
    }


$f = Get-ChildItem -Path $src -Recurse -File
$f | % {     ## -- Delete files only

    $apath = $_.FullName -Replace $src,"" 
    $cpath = $dest + $apath

    If (Test-Path $cpath)
       {
            Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue
       }
    }


$d | % {     ## -- Delete directories only

    $apath = $_ -Replace $src,"" 
    $cpath = $dest + $apath

    If (Test-Path $cpath)
       {
            Remove-Item $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
       }
    }

Further Resources

Related Question