|
[CmdletBinding(SupportsShouldProcess=$true)] |
|
Param( |
|
[Parameter(Mandatory=$false)] |
|
[string]$ConfigFile, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[ValidateScript({Test-Path $_ -PathType Container})] |
|
[string]$SourceFolder, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[string]$TargetFolderBase, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[string]$TargetSubFolder, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[DateTime]$CutoffTime, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[ValidateSet("CreateTime", "ModifyTime", "AccessTime")] |
|
[string]$CutoffTimeType = "ModifyTime", |
|
|
|
[Parameter(Mandatory=$false)] |
|
[string]$LogFile = "FileMove_$(Get-Date -Format 'yyyyMMdd_HHmmss').log", |
|
|
|
[Parameter(Mandatory=$false)] |
|
[string]$CsvFile = "FileMoveReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv", |
|
|
|
[Parameter(Mandatory=$false)] |
|
[string[]]$FileExtensions, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[long]$MinSize, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[long]$MaxSize, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[ValidateSet("Rename", "Skip", "Overwrite")] |
|
[string]$DuplicateHandling = "Rename", |
|
|
|
[Parameter(Mandatory=$false)] |
|
[switch]$IncrementalMode, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[int]$MaxRetries = 3, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[int]$MaxParallelJobs = 5, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[System.Management.Automation.PSCredential]$Credential, |
|
|
|
[Parameter(Mandatory=$false)] |
|
[switch]$Dryrun |
|
) |
|
|
|
# Error action preference |
|
$ErrorActionPreference = "Stop" |
|
|
|
# Function to write log messages |
|
function Write-Log { |
|
param ( |
|
[string]$Message, |
|
[string]$LogFile, |
|
[ValidateSet("INFO", "WARNING", "ERROR")] |
|
[string]$Level = "INFO" |
|
) |
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" |
|
$logMessage = "$timestamp - [$Level] - $Message" |
|
$logMessage | Out-File -Append -FilePath $LogFile |
|
switch ($Level) { |
|
"INFO" { Write-Verbose $Message } |
|
"WARNING" { Write-Warning $Message } |
|
"ERROR" { Write-Error $Message } |
|
} |
|
} |
|
|
|
# Import configuration if provided |
|
function Import-Configuration { |
|
if ($ConfigFile -and (Test-Path $ConfigFile)) { |
|
try { |
|
$config = Get-Content $ConfigFile | ConvertFrom-Json |
|
foreach ($prop in $config.PSObject.Properties) { |
|
if (-not $PSBoundParameters.ContainsKey($prop.Name)) { |
|
if ($prop.Name -eq "CutoffTime") { |
|
Set-Variable -Name $prop.Name -Value ([DateTime]::ParseExact($prop.Value, "yyyy-MM-dd HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture)) -Scope Script |
|
} else { |
|
Set-Variable -Name $prop.Name -Value $prop.Value -Scope Script |
|
} |
|
} |
|
} |
|
Write-Log "Configuration imported successfully from $ConfigFile" $LogFile |
|
} |
|
catch { |
|
Write-Log "Error importing configuration: $_" $LogFile "ERROR" |
|
throw |
|
} |
|
} |
|
elseif ($ConfigFile) { |
|
throw "Config file not found: $ConfigFile" |
|
} |
|
} |
|
|
|
# Validate required parameters |
|
function Validate-Parameters { |
|
if (-not $SourceFolder -or -not $TargetFolderBase -or -not $CutoffTime) { |
|
throw "SourceFolder, TargetFolderBase, and CutoffTime are required. Provide them as parameters or in the config file." |
|
} |
|
Write-Log "Parameters validated successfully" $LogFile |
|
} |
|
|
|
# Function to get files based on criteria |
|
function Get-FilesToMove { |
|
try { |
|
$getChildItemParams = @{ |
|
Path = $SourceFolder |
|
File = $true |
|
ErrorAction = 'Stop' |
|
} |
|
|
|
if ($null -ne $Credential) { |
|
$getChildItemParams['Credential'] = $Credential |
|
} |
|
|
|
Write-Log "Attempting to get files from source folder: $SourceFolder" $LogFile |
|
|
|
$allFiles = Get-ChildItem @getChildItemParams |
|
|
|
if ($null -eq $allFiles -or $allFiles.Count -eq 0) { |
|
Write-Log "No files found in the source folder." $LogFile |
|
return @() |
|
} |
|
|
|
Write-Log "Total files found before filtering: $($allFiles.Count)" $LogFile |
|
|
|
$files = $allFiles | ForEach-Object { |
|
$file = $_ |
|
$include = $true |
|
|
|
if ($null -eq $file) { |
|
Write-Log "Encountered a null file object, skipping." $LogFile |
|
return |
|
} |
|
|
|
try { |
|
$fileTime = switch ($CutoffTimeType) { |
|
"CreateTime" { $file.CreationTime } |
|
"ModifyTime" { $file.LastWriteTime } |
|
"AccessTime" { $file.LastAccessTime } |
|
} |
|
|
|
if ($null -eq $fileTime) { |
|
Write-Log "Unable to get $CutoffTimeType for file $($file.Name), skipping. File details: $($file | Format-List | Out-String)" $LogFile |
|
return |
|
} |
|
|
|
# Use CompareTo method for more reliable datetime comparison |
|
if ($fileTime.CompareTo($CutoffTime) -ge 0) { |
|
Write-Verbose "File $($file.Name) excluded due to $CutoffTimeType : $($fileTime.ToString('yyyy-MM-dd HH:mm:ss')) (CutoffTime: $($CutoffTime.ToString('yyyy-MM-dd HH:mm:ss')))" |
|
$include = $false |
|
} else { |
|
Write-Verbose "File $($file.Name) included due to $CutoffTimeType : $($fileTime.ToString('yyyy-MM-dd HH:mm:ss')) (CutoffTime: $($CutoffTime.ToString('yyyy-MM-dd HH:mm:ss')))" |
|
} |
|
|
|
if ($FileExtensions -and $file.Extension -notin $FileExtensions) { |
|
Write-Verbose "File $($file.Name) excluded due to extension: $($file.Extension)" |
|
$include = $false |
|
} |
|
|
|
if ($MinSize -and $file.Length -lt $MinSize) { |
|
Write-Verbose "File $($file.Name) excluded due to size (too small): $($file.Length)" |
|
$include = $false |
|
} |
|
|
|
if ($MaxSize -and $file.Length -gt $MaxSize) { |
|
Write-Verbose "File $($file.Name) excluded due to size (too large): $($file.Length)" |
|
$include = $false |
|
} |
|
|
|
if ($IncrementalMode -and $file.LastWriteTime -le (Get-Date).AddDays(-1)) { |
|
Write-Verbose "File $($file.Name) excluded due to incremental mode: $($file.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss'))" |
|
$include = $false |
|
} |
|
|
|
if ($include) { |
|
$file |
|
} |
|
} |
|
catch { |
|
Write-Log "Error processing file $($file.Name): $_" $LogFile "ERROR" |
|
Write-Log "File details: $($file | Format-List | Out-String)" $LogFile "ERROR" |
|
} |
|
} |
|
|
|
$filteredFileCount = if ($null -eq $files) { 0 } else { @($files).Count } |
|
Write-Log "Files matching criteria: $filteredFileCount" $LogFile |
|
|
|
return $files |
|
} |
|
catch { |
|
Write-Log "Error in Get-FilesToMove: $_" $LogFile "ERROR" |
|
Write-Log "Error details: $($_.Exception.Message)" $LogFile "ERROR" |
|
Write-Log "Stack trace: $($_.ScriptStackTrace)" $LogFile "ERROR" |
|
throw |
|
} |
|
} |
|
|
|
# Function to process files in parallel |
|
function Process-FilesInParallel { |
|
param ( |
|
[Parameter(Mandatory=$true)] |
|
[System.IO.FileInfo[]]$Files, |
|
[Parameter(Mandatory=$true)] |
|
[string]$TargetFolderBase, |
|
[Parameter(Mandatory=$false)] |
|
[string]$TargetSubFolder, |
|
[Parameter(Mandatory=$true)] |
|
[string]$CutoffTimeType, |
|
[Parameter(Mandatory=$true)] |
|
[string]$DuplicateHandling, |
|
[Parameter(Mandatory=$true)] |
|
[int]$MaxRetries, |
|
[Parameter(Mandatory=$true)] |
|
[int]$MaxParallelJobs, |
|
[Parameter(Mandatory=$false)] |
|
[System.Management.Automation.PSCredential]$Credential, |
|
[Parameter(Mandatory=$false)] |
|
[switch]$Dryrun |
|
) |
|
|
|
$jobs = @() |
|
$results = @() |
|
$processedCount = 0 |
|
$totalFiles = $Files.Count |
|
|
|
$scriptBlock = { |
|
param( |
|
$Name, |
|
$FullName, |
|
$DirectoryName, |
|
$Length, |
|
$CreationTime, |
|
$LastWriteTime, |
|
$LastAccessTime, |
|
$TargetFolderBase, |
|
$TargetSubFolder, |
|
$CutoffTimeType, |
|
$DuplicateHandling, |
|
$MaxRetries, |
|
$Credential, |
|
$Dryrun |
|
) |
|
|
|
function Move-FileWithStatus { |
|
param ( |
|
[Parameter(Mandatory=$true)] |
|
[string]$SourcePath, |
|
[Parameter(Mandatory=$true)] |
|
[string]$TargetPath, |
|
[Parameter(Mandatory=$true)] |
|
[string]$DuplicateHandling, |
|
[Parameter(Mandatory=$true)] |
|
[int]$MaxRetries, |
|
[Parameter(Mandatory=$false)] |
|
[System.Management.Automation.PSCredential]$Credential, |
|
[Parameter(Mandatory=$false)] |
|
[switch]$Dryrun |
|
) |
|
|
|
if ($Dryrun) { |
|
return "ToBeMoved" |
|
} |
|
|
|
$retryCount = 0 |
|
do { |
|
try { |
|
$moveItemParams = @{ |
|
Path = $SourcePath |
|
Destination = $TargetPath |
|
Force = $true |
|
ErrorAction = 'Stop' |
|
} |
|
|
|
if ($null -ne $Credential) { |
|
$moveItemParams['Credential'] = $Credential |
|
} |
|
|
|
if ($DuplicateHandling -eq "Rename" -and (Test-Path $TargetPath)) { |
|
$i = 1 |
|
$fileInfo = [System.IO.FileInfo]::new($TargetPath) |
|
do { |
|
$newName = "{0}_{1}{2}" -f $fileInfo.BaseName, $i, $fileInfo.Extension |
|
$TargetPath = Join-Path $fileInfo.DirectoryName $newName |
|
$i++ |
|
} while (Test-Path $TargetPath) |
|
$moveItemParams['Destination'] = $TargetPath |
|
} |
|
elseif ($DuplicateHandling -eq "Skip" -and (Test-Path $TargetPath)) { |
|
return "Skipped: File already exists" |
|
} |
|
|
|
Move-Item @moveItemParams |
|
return "Success" |
|
} |
|
catch { |
|
$retryCount++ |
|
if ($retryCount -ge $MaxRetries) { |
|
return "Failed: $_" |
|
} |
|
Start-Sleep -Seconds (2 * $retryCount) |
|
} |
|
} while ($retryCount -lt $MaxRetries) |
|
} |
|
|
|
$fileTime = switch ($CutoffTimeType) { |
|
"CreateTime" { $CreationTime } |
|
"ModifyTime" { $LastWriteTime } |
|
"AccessTime" { $LastAccessTime } |
|
} |
|
|
|
$subFolder = if ($TargetSubFolder) { |
|
$TargetSubFolder |
|
} else { |
|
$fileTime.Year.ToString() |
|
} |
|
|
|
$targetFolder = Join-Path $TargetFolderBase $subFolder |
|
$targetPath = Join-Path $targetFolder $Name |
|
|
|
# Ensure target folder exists (even in Dryrun mode for reporting purposes) |
|
if (-not (Test-Path $targetFolder)) { |
|
try { |
|
if (-not $Dryrun) { |
|
New-Item -ItemType Directory -Path $targetFolder -Force -ErrorAction Stop | Out-Null |
|
} |
|
} |
|
catch { |
|
return [PSCustomObject]@{ |
|
Name = $Name |
|
SourceFolder = $DirectoryName |
|
SizeInBytes = $Length |
|
SizeInMB = [math]::Round($Length / 1MB, 2) |
|
CreateTime = $CreationTime.ToString("yyyy-MM-dd HH:mm:ss") |
|
ModifiedTime = $LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss") |
|
AccessTime = $LastAccessTime.ToString("yyyy-MM-dd HH:mm:ss") |
|
TargetFolder = $targetFolder |
|
MoveStatus = "Failed: Unable to create target folder - $_" |
|
} |
|
} |
|
} |
|
|
|
$moveResult = Move-FileWithStatus -SourcePath $FullName -TargetPath $targetPath -DuplicateHandling $DuplicateHandling -MaxRetries $MaxRetries -Credential $Credential -Dryrun:$Dryrun |
|
|
|
return [PSCustomObject]@{ |
|
Name = $Name |
|
SourceFolder = $DirectoryName |
|
SizeInBytes = $Length |
|
SizeInMB = [math]::Round($Length / 1MB, 2) |
|
CreateTime = $CreationTime.ToString("yyyy-MM-dd HH:mm:ss") |
|
ModifiedTime = $LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss") |
|
AccessTime = $LastAccessTime.ToString("yyyy-MM-dd HH:mm:ss") |
|
TargetFolder = $targetFolder |
|
MoveStatus = $moveResult |
|
} |
|
} |
|
|
|
foreach ($file in $Files) { |
|
while ($jobs.Count -ge $MaxParallelJobs) { |
|
$completedJob = Wait-Job -Job $jobs -Any |
|
$jobResult = Receive-Job -Job $completedJob |
|
$results += $jobResult |
|
$jobs = $jobs | Where-Object { $_ -ne $completedJob } |
|
Remove-Job -Job $completedJob |
|
|
|
$processedCount++ |
|
$percentComplete = ($processedCount / $totalFiles) * 100 |
|
Write-Progress -Activity "Processing Files" -Status "Progress" -PercentComplete $percentComplete |
|
} |
|
|
|
$job = Start-Job -ScriptBlock $scriptBlock -ArgumentList $file.Name, $file.FullName, $file.DirectoryName, $file.Length, $file.CreationTime, $file.LastWriteTime, $file.LastAccessTime, $TargetFolderBase, $TargetSubFolder, $CutoffTimeType, $DuplicateHandling, $MaxRetries, $Credential, $Dryrun |
|
$jobs += $job |
|
} |
|
|
|
# Process any remaining jobs |
|
while ($jobs.Count -gt 0) { |
|
$completedJob = Wait-Job -Job $jobs -Any |
|
$jobResult = Receive-Job -Job $completedJob |
|
$results += $jobResult |
|
$jobs = $jobs | Where-Object { $_ -ne $completedJob } |
|
Remove-Job -Job $completedJob |
|
|
|
$processedCount++ |
|
$percentComplete = ($processedCount / $totalFiles) * 100 |
|
Write-Progress -Activity "Processing Files" -Status "Progress" -PercentComplete $percentComplete |
|
} |
|
|
|
return $results |
|
} |
|
|
|
# Main script execution |
|
try { |
|
$startTime = Get-Date |
|
Import-Configuration |
|
Validate-Parameters |
|
|
|
# Determine initial TargetFolder for logging purposes |
|
$initialTargetFolder = if ($TargetSubFolder) { |
|
Join-Path $TargetFolderBase $TargetSubFolder |
|
} else { |
|
$TargetFolderBase |
|
} |
|
|
|
Write-Log "Script started with following parameters:" $LogFile |
|
Write-Log "SourceFolder: $SourceFolder" $LogFile |
|
Write-Log "TargetFolderBase: $TargetFolderBase" $LogFile |
|
Write-Log "TargetSubFolder: $TargetSubFolder" $LogFile |
|
Write-Log "Initial Target: $initialTargetFolder" $LogFile |
|
Write-Log "CutoffTime: $($CutoffTime.ToString('yyyy-MM-dd HH:mm:ss'))" $LogFile |
|
Write-Log "CutoffTimeType: $CutoffTimeType" $LogFile |
|
Write-Log "FileExtensions: $($FileExtensions -join ', ')" $LogFile |
|
Write-Log "MinSize: $MinSize" $LogFile |
|
Write-Log "MaxSize: $MaxSize" $LogFile |
|
Write-Log "DuplicateHandling: $DuplicateHandling" $LogFile |
|
Write-Log "IncrementalMode: $IncrementalMode" $LogFile |
|
Write-Log "MaxRetries: $MaxRetries" $LogFile |
|
Write-Log "MaxParallelJobs: $MaxParallelJobs" $LogFile |
|
Write-Log "Dryrun: $Dryrun" $LogFile |
|
|
|
if ($null -ne $Credential) { |
|
Write-Log "Using provided credentials for file operations" $LogFile |
|
} |
|
|
|
# Verify source folder exists |
|
$testPathParams = @{ |
|
Path = $SourceFolder |
|
PathType = 'Container' |
|
ErrorAction = 'Stop' |
|
} |
|
|
|
if ($null -ne $Credential) { |
|
$testPathParams['Credential'] = $Credential |
|
} |
|
|
|
if (-not (Test-Path @testPathParams)) { |
|
throw "Source folder does not exist or is not accessible: $SourceFolder" |
|
} |
|
|
|
# Ensure target base folder exists (even in Dryrun mode for reporting purposes) |
|
$newItemParams = @{ |
|
ItemType = 'Directory' |
|
Path = $TargetFolderBase |
|
Force = $true |
|
ErrorAction = 'Stop' |
|
} |
|
|
|
if ($null -ne $Credential) { |
|
$newItemParams['Credential'] = $Credential |
|
} |
|
|
|
if (-not (Test-Path $TargetFolderBase)) { |
|
try { |
|
if (-not $Dryrun) { |
|
New-Item @newItemParams | Out-Null |
|
} |
|
Write-Log "Target base folder would be created: $TargetFolderBase" $LogFile |
|
} |
|
catch { |
|
throw "Failed to create target base folder: $TargetFolderBase. Error: $_" |
|
} |
|
} |
|
|
|
# Get files to move |
|
Write-Log "Attempting to get files to move..." $LogFile |
|
$filesToMove = Get-FilesToMove |
|
if ($null -eq $filesToMove -or @($filesToMove).Count -eq 0) { |
|
Write-Log "No files found to move. Exiting." $LogFile |
|
return |
|
} |
|
$totalFiles = @($filesToMove).Count |
|
Write-Log "Found $totalFiles files to move" $LogFile |
|
|
|
# Process files in parallel |
|
$activityDescription = if ($Dryrun) { "Simulating File Processing" } else { "Processing Files" } |
|
Write-Log "Starting to $activityDescription" $LogFile |
|
$csvData = Process-FilesInParallel -Files $filesToMove -TargetFolderBase $TargetFolderBase -TargetSubFolder $TargetSubFolder -CutoffTimeType $CutoffTimeType -DuplicateHandling $DuplicateHandling -MaxRetries $MaxRetries -MaxParallelJobs $MaxParallelJobs -Credential $Credential -Dryrun:$Dryrun |
|
Write-Log "Finished $activityDescription" $LogFile |
|
|
|
# Export CSV report with specified columns |
|
if ($null -ne $csvData -and @($csvData).Count -gt 0) { |
|
$csvData | Select-Object Name, SourceFolder, SizeInBytes, SizeInMB, CreateTime, ModifiedTime, AccessTime, TargetFolder, MoveStatus | Export-Csv -Path $CsvFile -NoTypeInformation |
|
Write-Log "CSV report exported to $CsvFile" $LogFile |
|
|
|
$totalProcessed = @($csvData).Count |
|
$successfulMoves = @($csvData | Where-Object { $_.MoveStatus -eq 'Success' -or $_.MoveStatus -eq 'ToBeMoved' }).Count |
|
$failedMoves = @($csvData | Where-Object { $_.MoveStatus -like 'Failed*' }).Count |
|
$skippedFiles = @($csvData | Where-Object { $_.MoveStatus -eq 'Skipped: File already exists' }).Count |
|
|
|
$actionWord = if ($Dryrun) { "would be" } else { "were" } |
|
Write-Log "Total files processed: $totalProcessed" $LogFile |
|
Write-Log "Successful moves (or to be moved): $successfulMoves" $LogFile |
|
Write-Log "Failed moves: $failedMoves" $LogFile |
|
Write-Log "Skipped files: $skippedFiles" $LogFile |
|
} else { |
|
Write-Log "No files were processed. Check the log for details." $LogFile |
|
} |
|
|
|
$endTime = Get-Date |
|
$duration = $endTime - $startTime |
|
$modeDescription = if ($Dryrun) { "Dryrun" } else { "Actual run" } |
|
Write-Log "Script completed successfully ($modeDescription). Duration: $($duration.TotalSeconds) seconds" $LogFile |
|
} |
|
catch { |
|
Write-Log "An error occurred in the main script execution: $_" $LogFile "ERROR" |
|
Write-Log "Error details: $($_.Exception.Message)" $LogFile "ERROR" |
|
Write-Log "Stack trace: $($_.ScriptStackTrace)" $LogFile "ERROR" |
|
throw |
|
} |
|
finally { |
|
Write-Progress -Activity "Processing Files" -Completed |
|
} |