PowerShell: Copy Directory Structure and a Random Sample of Files from Each Directory

I got my wife a new digital picture frame as a present.  It looks cool but the attached storage options (USB or card) aren’t big enough to take all our digital photos.

The decision about which pictures to include relies on either organisational skills OR an artistic eye, neither of which I have.

So what about making it strictly random?  Copying the entire directory structure but only a random sample of the files in each folder?

That I CAN do 🙂The full script is here.  You can specify which file types you want to copy (defaults to all) and how many random samples you want to take from each directory (defaults to 2).

What follows is a break-down of some of the more complex / interesting / odd bits of the script.

[cmdletbinding()]
param
(
    [parameter(Mandatory=$true)]
    [ValidateScript({Test-Path $_ -PathType Container})]
    [string]$SourcePath,
    [parameter(Mandatory=$true)]
    [ValidateScript({Test-Path $_ -PathType Container -IsValid})]
    [string]$DestinationPath,
    [string]$FileFilter="*.*",
    [int]$SampleNumber=2
)
set-strictmode -version Latest

The first bit of code is pretty standard.  I want to take the source and destination directories as parameters (while validating them).  Additionally I want to be able to pass a filter to use for the files and how large a random sample to take.

function Get-SampleFiles
{
    param
    (
        [parameter(Mandatory=$true)]
        [System.IO.FileSystemInfo]$Directory,
        [parameter(Mandatory=$true)]
        [string]$FileFilter,
        [parameter(Mandatory=$true)]
        [int]$SampleNumber
    )
    Write-Verbose "Processing $($Directory.FullName)"
    Write-Verbose "Filter : $FileFilter, Sample Size : $SampleNumber"
    $Files=[array](Get-ChildItem -Path $Directory.FullName -Filter $FileFilter -File)
    $UsedNumbers=@()
    if ($Files -ne $Null)
    {
        if ($SampleNumber -le $Files.Count)
        {
            Write-Verbose "Getting Random Sample"
            $FilesToReturn=@()
            for ($i = 1; $i -le $SampleNumber; $i++)
            { 
                $NotFound=$True
                [int]$RandomChoice=0
                While($NotFound)
                {
                    $RandomChoice=Get-Random -Minimum 0 -Maximum $Files.Count
                    if(!($UsedNumbers -contains $RandomChoice))
                    {
                        $NotFound=$False
                        $UsedNumbers+=$RandomChoice
                    }
                }
                $FilesToReturn+=$Files[$RandomChoice]
            }
            Return $FilesToReturn
        }else
        {
            Write-Verbose "Not enough files for random sample.  Returning all files."
            Return $Files
        }
    }
    Write-Verbose "No Files returned."
    Return $Null
}

Next I created a Get-SampleFiles function to return the random sample of files.  I take the source directory object, sample size and file filters as parameters.

Next I built a list of the files that match the filter within the directory and store them within $Files.

There’s some sanity checking to make sure the sample size is less than the number of files in the folder (otherwise it returns them all) and to make sure there are some files in there.

If we’re good I generate a random number between 0 and the number of files-1 (Get-Random returns a random number greater than Minimum and less than Maximum).  I keep a record of all the random numbers picked (in $UsedNumbers) and keep picking until I get one that’s not been used.  Then I update the list of used random numbers and add the file with that number to the list of files to return.

In the main part of the script I use Get-ChildItem with the -Directory and -Recurse switches to build a list of all the directories to process.  Then I sample the files from each, create a dummy file with New-Item (this creates the full directory structure for the file when -Force is used) and then replace that file with the real one (Copy-Item -Force).

The creation of $FullDestinationPath is quite interesting;  the -replace operator recognises RegEx special characters so it rejects path names (with their use of “\“).  The .Replace method is fine with RegEx but is case sensitive.

So I used ireplace (case insensitive) and the [regex]::Escape static method to make it ignore the regex characters.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s