Functions/GenXdev.FileSystem/Copy-FilesToDateFolder.ps1
|
<##############################################################################
Part of PowerShell module : GenXdev.FileSystem Original cmdlet filename : Copy-FilesToDateFolder.ps1 Original author : René Vaessen / GenXdev Version : 3.24.2026 ################################################################################ Copyright (c) René Vaessen / GenXdev Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ################################################################################> # ############################################################################## # Part of PowerShell module : GenXdev.FileSystem # Original cmdlet filename : Rename-InProject.ps1 # Original author : René Vaessen / GenXdev # Version : 3.24.2026 # ################################################################################ # Copyright (c) René Vaessen / GenXdev # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ################################################################################ # ############################################################################### <# .SYNOPSIS Copies files matching search criteria into date-based subfolders. .DESCRIPTION Searches for files using the same parameter set as `Find-Item` and copies each matched file into a subfolder of `TargetFolder` based on the file's creation or media date. The cmdlet supports content matching, drive-wide searches and many filters. Attempts several strategies to determine an accurate creation or capture date for the specified file. strategies include reading image EXIF metadata, parsing date/time information from filenames, and falling back to the file's last-write time when no other reliable information is available. .PARAMETER TargetFolder The root path where matched files will be copied into date-based subfolders. This parameter is required. .PARAMETER Name File name or wildcard pattern to match. Defaults to `*`. .PARAMETER Input Pipeline input for file paths or objects with a `FullName`/`Name` property. .PARAMETER Content Regular expression patterns to match inside file contents (when applicable). .PARAMETER RelativeBasePath Base path used to resolve relative paths in output; defaults to the current directory. .PARAMETER Category Optional filter to include only files that belong to selected categories. .PARAMETER MaxDegreeOfParallelism Maximum parallel directory operations. `0` means automatic. .PARAMETER TimeoutSeconds Optional timeout in seconds for the search operation. .PARAMETER AllDrives Switch to search across all mounted drives. .PARAMETER Directory Only search for directories instead of files. .PARAMETER FilesAndDirectories Include both files and directories in search results. .PARAMETER PassThru Output matched items as objects instead of formatted text. .PARAMETER IncludeAlternateFileStreams Include alternate data streams (ADS) in the search results. .PARAMETER NoRecurse Do not recurse into subdirectories when searching. .PARAMETER FollowSymlinkAndJunctions Follow symbolic links and junctions during traversal. .PARAMETER IncludeOpticalDiskDrives Include optical disks when enumerating drives. .PARAMETER SearchDrives Explicit list of drive root paths to search. .PARAMETER DriveLetter List of drive letters to search. .PARAMETER Root Base directories to combine with `Name` patterns when searching. .PARAMETER LimitToRoot When used, restricts matches to provided `Root` directories and prevents escaping above them. .PARAMETER IncludeNonTextFileMatching Allow content matching against non-text files (binary/media) when doing content search. .PARAMETER NoLinks Operate in unattended mode; do not generate links or interactive prompts. .PARAMETER CaseNameMatching Controls case-sensitivity for file name matching. .PARAMETER SearchADSContent When set, also search within alternate data streams (ADS) contents. .PARAMETER MaxRecursionDepth Maximum directory recursion depth; `0` means unlimited. .PARAMETER MaxSearchUpDepth Maximum upward search depth when performing relative searches. .PARAMETER MaxFileSize Maximum file size in bytes to include; `0` means unlimited. .PARAMETER MinFileSize Minimum file size in bytes to include; `0` means no minimum. .PARAMETER ModifiedAfter Only include items modified after this UTC date/time. .PARAMETER ModifiedBefore Only include items modified before this UTC date/time. .PARAMETER AttributesToSkip File attributes to exclude from results (for example `System`). .PARAMETER Exclude Wildcard patterns to exclude files or directories from the search. .PARAMETER AllMatches Return all matches per line when using content pattern matching. .PARAMETER CaseSensitive Make content pattern matches case-sensitive. .PARAMETER Context Capture context lines around found content matches. .PARAMETER Culture Culture name used for pattern matching when using `SimpleMatch`. .PARAMETER Encoding File encoding to use for content operations. .PARAMETER List Only the first match per file is returned (efficient list mode). .PARAMETER NoEmphasis Disable highlighting in output. .PARAMETER NotMatch Return files that do not match the provided content pattern. .PARAMETER Raw Output only matching strings instead of `MatchInfo` objects. .PARAMETER SimpleMatch Use simple string matching (not regular expressions) for content matching. .PARAMETER TargetFolderNameDateSyntax Controls the date folder format. Valid values: `Year + Month + Day`, `Year + Month`, `Year`. .EXAMPLE Copy all pictures and videos to the corresponsing Android Onedrive App Image backup folders Copy-FilesToDateFolder -TargetFolder "~\onedrive\Pictures\Camera Roll" ` -SourceFolder ~\Pictures\*, ~\desktop\* ` -Category 'Pictures', 'Videos' .EXAMPLE Copy all jpg files from the current directory into date folders under `D:\Archive` (dry run): Copy-FilesToDateFolder -TargetFolder 'D:\Archive' -Name '*.jpg' -WhatIf .EXAMPLE Copy all files across drives matching `*.mp4` into monthly folders Copy-FilesToDateFolder -TargetFolder 'E:\Media\Videos' -Name '.\*.mp4' #> function Copy-FilesToDateFolder { [CmdletBinding(SupportsShouldProcess = $true)] param( ######################################################################## ######################################################################## [Parameter(Position = 0, Mandatory = $true, HelpMessage = 'Target root folder where matched files will be copied into date-based subfolders')] [ValidateNotNullOrEmpty()] [string] $TargetFolder, ######################################################################## [Parameter(Position = 1, Mandatory = $false, HelpMessage = 'File name or pattern to search for. Default is ''*''')] [SupportsWildcards()] [Alias("SourceFolder")] [ValidateNotNullOrEmpty()] [string[]] $Name = @('*'), ######################################################################## [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'File name or pattern to search for from pipeline input. Default is ''*''')] [Alias('FullName')] [ValidateNotNullOrEmpty()] [SupportsWildcards()] [object] $Input, ######################################################################## [Parameter(Position = 2, Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'Regular expression pattern to search within content')] [Alias('mc', 'matchcontent', 'regex', 'Pattern')] [ValidateNotNull()] [SupportsWildcards()] [string[]] $Content = @('.*'), ######################################################################## [Parameter(Position = 3, Mandatory = $false, HelpMessage = 'Base path for resolving relative paths in output')] [Alias('base')] [ValidateNotNullOrEmpty()] [string] $RelativeBasePath = '.\', ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Only output files belonging to selected categories')] [Alias('filetype')] [ValidateSet( 'Pictures', 'Videos', 'Music', 'Documents', 'Spreadsheets', 'Presentations', 'Archives', 'Installers', 'Executables', 'Databases', 'DesignFiles', 'Ebooks', 'Subtitles', 'Fonts', 'EmailFiles', '3DModels', 'GameAssets', 'MedicalFiles', 'FinancialFiles', 'LegalFiles', 'SourceCode', 'Scripts', 'MarkupAndData', 'Configuration', 'Logs', 'TextFiles', 'WebFiles', 'MusicLyricsAndChords', 'CreativeWriting', 'Recipes', 'ResearchFiles' )] [string[]] $Category, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Maximum degree of parallelism for directory tasks')] [Alias('threads')] [int] $MaxDegreeOfParallelism = 0, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Optional: cancellation timeout in seconds')] [Alias('maxseconds')] [int] $TimeoutSeconds, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Search across all available drives')] [switch] $AllDrives, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Search for directories only')] [Alias('dir')] [switch] $Directory, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Include both files and directories')] [Alias('both', 'DirectoriesAndFiles')] [switch] $FilesAndDirectories, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Output matched items as objects')] [Alias('pt')] [switch] $PassThru, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Include alternate data streams in search results')] [Alias('ads')] [switch] $IncludeAlternateFileStreams, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Do not recurse into subdirectories')] [Alias('nr')] [switch] $NoRecurse, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Follow symlinks and junctions during directory traversal')] [Alias('symlinks', 'sl')] [switch] $FollowSymlinkAndJunctions, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Include optical disk drives')] [switch] $IncludeOpticalDiskDrives, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Optional: search specific drives')] [Alias('drives')] [string[]] $SearchDrives = @(), ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Optional: search specific drives')] [char[]] $DriveLetter = @(), ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Optional: search specific directories')] [string[]] $Root = @(), ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Enforces searching only within Root directories by stripping directory components from Name inputs')] [Alias('limit')] [switch] $LimitToRoot, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Include non-text files when searching file contents')] [Alias('binary')] [switch] $IncludeNonTextFileMatching, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Forces unattended mode and will not generate links')] [Alias('nl', 'ForceUnattenedMode')] [switch] $NoLinks, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Gets or sets the case-sensitivity for files and directories')] [Alias('casing', 'CaseSearchMaskMatching')] [System.IO.MatchCasing] $CaseNameMatching = [System.IO.MatchCasing]::PlatformDefault, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'When set, performs content search within alternate data streams (ADS)')] [Alias('sads')] [switch] $SearchADSContent, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Maximum recursion depth for directory traversal. 0 means unlimited.')] [Alias('md', 'depth', 'maxdepth')] [int] $MaxRecursionDepth = 0, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Maximum recursion depth for continuation searching upwards the tree. 0 means disabled.')] [Alias('maxupward')] [int] $MaxSearchUpDepth = 0, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Maximum file size in bytes to include in results. 0 means unlimited.')] [Alias('maxlength', 'maxsize')] [long] $MaxFileSize = 0, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Minimum file size in bytes to include in results. 0 means no minimum.')] [Alias('minsize', 'minlength')] [long] $MinFileSize = 0, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Only include files modified after this date/time (UTC)')] [Alias('ma', 'after')] [DateTime] $ModifiedAfter, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Only include files modified before this date/time (UTC)')] [Alias('before', 'mb')] [DateTime] $ModifiedBefore, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'File attributes to skip (e.g., System, Hidden or None)')] [Alias('skipattr')] [System.IO.FileAttributes] $AttributesToSkip = [System.IO.FileAttributes]::System, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Exclude files or directories matching these wildcard patterns')] [Alias('skiplike')] [string[]] $Exclude = @('*\.git\*'), ######################################################################## [Parameter(Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'Search for more than one match in each line of text')] [switch] $AllMatches, ######################################################################## [Parameter(Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'Matches are case-sensitive')] [switch] $CaseSensitive, ######################################################################## [Parameter(Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'Captures context lines around matches')] [int[]] $Context, ######################################################################## [Parameter(Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'Culture name used for pattern matching')] [string] $Culture, ######################################################################## [Parameter(Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'Specifies encoding for target files')] [ValidateSet('ASCII', 'ANSI', 'BigEndianUnicode', 'BigEndianUTF32', 'OEM', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF8NoBOM', 'UTF32', 'Default')] [string] $Encoding = 'UTF8NoBOM', ######################################################################## [Parameter(Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'Only the first match per file is returned')] [switch] $List, ######################################################################## [Parameter(Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'Disables highlighting of matching strings in output')] [switch] $NoEmphasis, ######################################################################## [Parameter(Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'The NotMatch parameter finds text that does not match the pattern')] [switch] $NotMatch, ######################################################################## [Parameter(Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'Output only matching strings instead of MatchInfo objects')] [switch] $Raw, ######################################################################## [Parameter(Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = 'Use simple string matching instead of regex')] [switch] $SimpleMatch, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Delete empty source directories after moving files')] [switch] $DeleteEmptyDirs, ######################################################################## [Parameter(Mandatory = $false, HelpMessage = 'Target folder name date syntax')] [ValidateSet( 'Year + Month + Day', 'Year + Month', 'Year' )] [string] $TargetFolderNameDateSyntax = 'Year + Month' ######################################################################## ) begin { $params = GenXdev.FileSystem\Copy-IdenticalParamValues ` -BoundParameters $PSBoundParameters ` -FunctionName 'GenXdev.FileSystem\Find-Item' ` -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable ` -Scope Local ` -ErrorAction SilentlyContinue) $uniqueNameCounters = @{} } process { Microsoft.PowerShell.Utility\Write-Progress -Id 11 -Activity "Copying files to date folders" -Status "Enumerating files..." -PercentComplete 0 GenXdev.FileSystem\Find-Item @params -PassThru -Quiet -ProgressAction Continue | Microsoft.PowerShell.Core\ForEach-Object -ErrorAction Continue { $item = $_ $creationDate = GenXdev.FileSystem\Get-MediaFileCreationDate -FilePath $item.FullName if ($creationDate -ne [DateTime]::MinValue) { $dateFolderName = $creationDate.ToString('yyyy-MM-dd') switch ($TargetFolderNameDateSyntax) { 'Year + Month + Day' { $dateFolderName = $creationDate.ToString('yyyy-MM-dd') + "\" + $creationDate.ToString('MM') + "\" + $creationDate.ToString('dd') } 'Year + Month' { $dateFolderName = $creationDate.ToString('yyyy') + "\" + $creationDate.ToString('MM') } 'Year' { $dateFolderName = $creationDate.ToString('yyyy') } } $destinationPath = GenXdev.FileSystem\Expand-Path "$TargetFolder\$dateFolderName\$($item.Name)" if ($item.FullName -eq $destinationPath) { return } if ([IO.File]::Exists($destinationPath)) { # check file sizes $existingFileSize = (Microsoft.PowerShell.Management\Get-Item -LiteralPath $destinationPath -Force).Length $currentFileSize = $item.Length if ($existingFileSize -ne $currentFileSize) { $uniqueNo = 1; if (-not $uniqueNameCounters.ContainsKey($destinationPath)) { $uniqueNameCounters[$destinationPath] = $uniqueNo } else { $uniqueNo = $uniqueNameCounters[$destinationPath] + 1; $uniqueNameCounters[$destinationPath] = $uniqueNo; } $baseName = [IO.Path]::GetFileNameWithoutExtension($item.Name) $extension = [IO.Path]::GetExtension($item.Name) $newFileName = "$baseName ($($uniqueNo))$extension" $destinationPath = GenXdev.FileSystem\Expand-Path "$TargetFolder\$dateFolderName\$newFileName" -CreateDirectory } else { return; w } } if ($PSCmdlet.ShouldProcess($item.FullName, "Copy to $destinationPath")) { try { $destinationPath = GenXdev.FileSystem\Expand-Path $destinationPath -CreateDirectory Microsoft.PowerShell.Utility\Write-Verbose "Copying '$($item.FullName)' to '$destinationPath'" Microsoft.PowerShell.Management\Copy-Item -LiteralPath $item.FullName -Destination $destinationPath -Force -ErrorAction Stop } catch { Microsoft.PowerShell.Utility\Write-Warning "Failed to copy '$($item.FullName)' to '$destinationPath': $_" } } else { Microsoft.PowerShell.Utility\Write-Verbose "WhatIf: would copy '$($item.FullName)' to '$destinationPath'" } } } Microsoft.PowerShell.Utility\Write-Progress -Id 11 -Activity "Copying files to date folders" -Status "Completed" -PercentComplete 100 -complete } } |