Google

2012-09-08

Copying recent files with PowerShell

I saw an interesting question in 'Scripting Guys' Facebook group today:

"I want to check which photos have changed or were added after a specific date on my disk, and copy those to the same path on my backup disk.

I managed to filter:

Get-ChildItem -Recurse | ? {$_.LastWriteTime -gt (get-date).adddays(-180)}
but I don't know how to pick each item's folder path and copy.

e.g.:
PS E:\fotos> Get-ChildItem Hornet01.jpg | fl *
...
And this file should be copied to L:\fotos\Hornet01.jpg"

Before I go about how I would solve this problem, I will explain a bit about how I store my pics and how I would copy them to a different drive.

I keep my pics in a network drive that's mapped as Z:, structure goes like this
Z:\Family\Pictures\{Year}\{Year.Month}\{Event or Place}\*.jpg

Note that Z:\Family\Pictures does not change, so we can think it of as the 'root'. Now, say, I wanted to find all the pictures in the last 6 months and copy them to a different drive maintaining the same structure like

Y:\MyPics\{Year}\{Year.Month}\{Event or Place}\*.jpg

For this, I need to do followings:

  1. Find all pictures that have changed in the last 6 months. 
  2. Get the 'unique' parent directory names of those pictures
  3. Replace those directory names with the new ones
  4. Create the destination directories
  5. Copy the files to new destinations

Well, I will talk about how this can be shortened to two steps but stick with me for the time being...

#1 has already been solved:

$recent_files=Get-ChildItem -Recurse | ? {$_.LastWriteTime -gt (get-date).adddays(-180)}


Of course, we could have filtered these if really wanted just pictures using "-filter" option but let's not bother with that for the moment.

#2 Now, we need to get unique directory names:

$unique_sourcedirs = $recent_files | %{ ($_.directory).fullname } |unique


Note that we ended up with a "System.String" type that we are going to manipulate

#3 We need to replace the first part (ie.z:\family\pictures with y:\mypics):

$dest_dirs=$unique_sourcedirs | %{$_ -replace 'Z:\\family\\Pictures','Y:\MyPics'}


Interestingly, you have to escape backslashes with Powershell's escape character, which happens to be backslash as the first part of replace operator needs a 'regular expression' but not the destination.

#4 Create the destination directories. 
If destination directories do not exist, copy operation will fail. And I am not aware of any parameters that would tell copy to create the directories in the way. This is why we are creating them first.

$dest_dirs | % { if (! (test-path $_)) { new-item -itemtype directory -path $_ }}


#5 We are almost ready to copy.
We have the source file list, and we know that destination directories are created now but we need destination paths. We could use the techniques above to get the destination paths.

$recent_files | %{ 
$target = ($_.directory).fullname -replace 'Z:\\family\\Pictures','Y:\MyPics'; 
copy-item ($_.fullname) $target}


I know, repeating code is just ugly, could not we combine the steps? Sure, above steps helps clarify the action items but it could be shortened to this:

  1. Find all pictures that have changed in the last 6 months
  2. For each file, determine the target directory name by replacing the parent directory name with the new name; create the destination directory if it does not exist; copy the file to new destination.

$recent_files | % { 
   $target = ($_.directory).fullname -replace 'Z:\\family\\pictures','Y:\MyPics';
   if (!(test-path $target)){ new-item -itemtype directory -path $target }; 
   copy-item -path ($_.fullname) -destination $target -force -whatif
}

Couple of Notes:

  • All of the above code-snippet is actually a single line if you are typing it in command line, I introduced line-breaks for better readability.
  • Parameter -whatif is only to see what will happen. You should remove it when executing the command
  • Parameter -force is not necessary if there are no files in destination directory.



2 comments:

Anonymous said...

Hi Adil,
Thanks for the awesome explaination!
One more question.. :)
Is there a way to use a specific date?
e.g. copy all files created/updated after 2012-05-01 ?
Thanks in advance!
Cheers,
Alex

Adil Hindistan said...

Hey Alex -

Yes, you can do that with the 'get-date' function:

$after=get-date '2012-05-01'
$recent_files=Get-ChildItem -Recurse | ? {$_.LastWriteTime -gt $after}