diff --git a/Module/PSMinIO/PSMinIO.psd1 b/Module/PSMinIO/PSMinIO.psd1 index 1dec43d..836011d 100644 --- a/Module/PSMinIO/PSMinIO.psd1 +++ b/Module/PSMinIO/PSMinIO.psd1 @@ -70,7 +70,8 @@ 'Test-MinIOBucketExists', 'Get-MinIOObject', 'New-MinIOObject', - 'Get-MinIOObjectContent' + 'Get-MinIOObjectContent', + 'New-MinIOZipArchive' ) # Variables to export from this module diff --git a/Module/PSMinIO/bin/PSMinIO.dll b/Module/PSMinIO/bin/PSMinIO.dll index e495b29..da28480 100644 Binary files a/Module/PSMinIO/bin/PSMinIO.dll and b/Module/PSMinIO/bin/PSMinIO.dll differ diff --git a/PSMinIO.csproj b/PSMinIO.csproj index ca164de..e955c12 100644 --- a/PSMinIO.csproj +++ b/PSMinIO.csproj @@ -31,6 +31,8 @@ + + diff --git a/scripts/Check-Parameters.ps1 b/scripts/Check-Parameters.ps1 new file mode 100644 index 0000000..8910cfa --- /dev/null +++ b/scripts/Check-Parameters.ps1 @@ -0,0 +1,29 @@ +# Check what parameters are available for New-MinIOObject +Import-Module ".\Module\PSMinIO\PSMinIO.psd1" -Force + +Write-Host "=== New-MinIOObject Parameter Information ===" -ForegroundColor Cyan + +# Get command info +$cmdInfo = Get-Command New-MinIOObject + +Write-Host "`nParameter Sets:" -ForegroundColor Yellow +foreach ($paramSet in $cmdInfo.ParameterSets) { + Write-Host " $($paramSet.Name):" -ForegroundColor Green + foreach ($param in $paramSet.Parameters) { + if ($param.Name -notin @('Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'WhatIf', 'Confirm')) { + $mandatory = if ($param.IsMandatory) { " (Mandatory)" } else { "" } + $aliases = if ($param.Aliases.Count -gt 0) { " [Aliases: $($param.Aliases -join ', ')]" } else { "" } + Write-Host " - $($param.Name)$mandatory$aliases" -ForegroundColor White + } + } + Write-Host "" +} + +Write-Host "All Parameters:" -ForegroundColor Yellow +foreach ($param in $cmdInfo.Parameters.Values) { + if ($param.Name -notin @('Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'WhatIf', 'Confirm')) { + $aliases = if ($param.Aliases.Count -gt 0) { " [Aliases: $($param.Aliases -join ', ')]" } else { "" } + $paramSets = $param.ParameterSets.Keys -join ', ' + Write-Host " $($param.Name)$aliases - Sets: $paramSets" -ForegroundColor White + } +} diff --git a/scripts/Test-50-Files-Upload.ps1 b/scripts/Test-50-Files-Upload.ps1 new file mode 100644 index 0000000..6abb460 --- /dev/null +++ b/scripts/Test-50-Files-Upload.ps1 @@ -0,0 +1,156 @@ +# Test script for uploading 50 test files to demonstrate enhanced upload functionality +# This tests FileInfo[] support, multi-layer progress tracking, and BucketDirectory features + +param( + [string]$TestBucketName = "psminiotest-50files-$(Get-Date -Format 'yyyyMMdd-HHmmss')", + [string]$TestDirectory = "TestFiles50", + [switch]$Cleanup, + [switch]$Verbose +) + +# Set verbose preference if requested +if ($Verbose) { + $VerbosePreference = 'Continue' +} + +Write-Host "=== PSMinIO 50-File Upload Test ===" -ForegroundColor Cyan +Write-Host "Testing enhanced upload functionality with multi-layer progress tracking" -ForegroundColor Green + +try { + # Import the module + Write-Host "`n1. Loading PSMinIO module..." -ForegroundColor Yellow + Import-Module ".\Module\PSMinIO\PSMinIO.psd1" -Force + + # Connect to MinIO + Write-Host "2. Connecting to MinIO..." -ForegroundColor Yellow + Connect-MinIO -Endpoint "https://api.s3.gracesolution.info" -AccessKey "T34Wg85SAwezUa3sk3m4" -SecretKey "PxEmnbQoQJTJsDocSEV6mSSscDpJMiJCayPv93xe" + Write-Host "✅ Connected successfully!" -ForegroundColor Green + + # Create test directory and files + Write-Host "`n3. Creating test files..." -ForegroundColor Yellow + if (Test-Path $TestDirectory) { + Remove-Item $TestDirectory -Recurse -Force + } + New-Item -ItemType Directory -Path $TestDirectory -Force | Out-Null + + # Create 50 test files with varying sizes and content + $testFiles = @() + for ($i = 1; $i -le 50; $i++) { + $fileName = "testfile-{0:D3}.txt" -f $i + $filePath = Join-Path $TestDirectory $fileName + + # Create files with different sizes (1KB to 100KB) + $fileSize = Get-Random -Minimum 1024 -Maximum 102400 + $content = "Test file $i`n" + ("X" * ($fileSize - 20)) + + Set-Content -Path $filePath -Value $content -NoNewline + $testFiles += Get-Item $filePath + } + + Write-Host "✅ Created 50 test files (total size: $((($testFiles | Measure-Object Length -Sum).Sum / 1MB).ToString('F2')) MB)" -ForegroundColor Green + + # Create test bucket + Write-Host "`n4. Creating test bucket: $TestBucketName" -ForegroundColor Yellow + New-MinIOBucket -BucketName $TestBucketName + Write-Host "✅ Bucket created successfully!" -ForegroundColor Green + + # Test 1: Upload all files using FileInfo[] parameter + Write-Host "`n5. Test 1: Uploading 50 files using FileInfo[] parameter..." -ForegroundColor Yellow + Write-Host " This will demonstrate multi-layer progress tracking:" -ForegroundColor Cyan + Write-Host " • Layer 1: Collection progress (overall files)" -ForegroundColor Cyan + Write-Host " • Layer 2: File progress (current file)" -ForegroundColor Cyan + Write-Host " • Layer 3: Transfer progress (bytes)" -ForegroundColor Cyan + + $uploadStart = Get-Date + $uploadResults = New-MinIOObject -BucketName $TestBucketName -Files $testFiles -BucketDirectory "batch-upload" -PassThru -Verbose + $uploadDuration = (Get-Date) - $uploadStart + + Write-Host "✅ Upload completed in $($uploadDuration.TotalSeconds.ToString('F2')) seconds!" -ForegroundColor Green + Write-Host " Average speed: $(($uploadResults | Measure-Object TotalSize -Sum).Sum / $uploadDuration.TotalSeconds / 1MB | ForEach-Object { $_.ToString('F2') }) MB/s" -ForegroundColor Green + Write-Host " Files uploaded: $($uploadResults.Count)" -ForegroundColor Green + + # Test 2: Upload using Directory parameter with BucketDirectory + Write-Host "`n6. Test 2: Uploading directory with BucketDirectory parameter..." -ForegroundColor Yellow + + $dirUploadStart = Get-Date + $dirUploadResults = New-MinIOObject -BucketName $TestBucketName -Directory (Get-Item $TestDirectory) -BucketDirectory "directory-upload/nested/structure" -PassThru -Verbose + $dirUploadDuration = (Get-Date) - $dirUploadStart + + Write-Host "✅ Directory upload completed in $($dirUploadDuration.TotalSeconds.ToString('F2')) seconds!" -ForegroundColor Green + Write-Host " Files uploaded: $($dirUploadResults.Count)" -ForegroundColor Green + + # Test 3: Verify uploads by listing objects + Write-Host "`n7. Verifying uploads..." -ForegroundColor Yellow + $allObjects = Get-MinIOObject -BucketName $TestBucketName + + $batchObjects = $allObjects | Where-Object { $_.Name -like "batch-upload/*" } + $dirObjects = $allObjects | Where-Object { $_.Name -like "directory-upload/*" } + + Write-Host "✅ Verification complete:" -ForegroundColor Green + Write-Host " Total objects in bucket: $($allObjects.Count)" -ForegroundColor Green + Write-Host " Batch upload objects: $($batchObjects.Count)" -ForegroundColor Green + Write-Host " Directory upload objects: $($dirObjects.Count)" -ForegroundColor Green + + # Test 4: Test with filters (create subdirectories first) + Write-Host "`n8. Test 3: Testing directory upload with filters..." -ForegroundColor Yellow + + # Create subdirectories with different file types + $subDir1 = Join-Path $TestDirectory "SubDir1" + $subDir2 = Join-Path $TestDirectory "SubDir2" + New-Item -ItemType Directory -Path $subDir1 -Force | Out-Null + New-Item -ItemType Directory -Path $subDir2 -Force | Out-Null + + # Create some .log and .json files + for ($i = 1; $i -le 5; $i++) { + Set-Content -Path (Join-Path $subDir1 "logfile$i.log") -Value "Log entry $i" + Set-Content -Path (Join-Path $subDir2 "config$i.json") -Value "{`"test`": $i}" + } + + # Upload only .log files using inclusion filter + $filterStart = Get-Date + $filterResults = New-MinIOObject -BucketName $TestBucketName -Directory (Get-Item $TestDirectory) -Recursive -InclusionFilter { $_.Extension -eq ".log" } -BucketDirectory "filtered-upload" -PassThru -Verbose + $filterDuration = (Get-Date) - $filterStart + + Write-Host "✅ Filtered upload completed in $($filterDuration.TotalSeconds.ToString('F2')) seconds!" -ForegroundColor Green + Write-Host " Log files uploaded: $($filterResults.Count)" -ForegroundColor Green + + # Summary + Write-Host "`n=== TEST SUMMARY ===" -ForegroundColor Cyan + Write-Host "✅ All tests completed successfully!" -ForegroundColor Green + Write-Host "Features tested:" -ForegroundColor Yellow + Write-Host " • FileInfo[] parameter with 50 files" -ForegroundColor White + Write-Host " • Multi-layer progress tracking (3 layers)" -ForegroundColor White + Write-Host " • BucketDirectory parameter for nested structures" -ForegroundColor White + Write-Host " • Directory parameter with recursive upload" -ForegroundColor White + Write-Host " • InclusionFilter for selective file upload" -ForegroundColor White + Write-Host " • PassThru parameter for upload results" -ForegroundColor White + Write-Host " • Thread-safe progress reporting" -ForegroundColor White + + Write-Host "`nPerformance metrics:" -ForegroundColor Yellow + Write-Host " • Batch upload: $($uploadDuration.TotalSeconds.ToString('F2'))s for $($uploadResults.Count) files" -ForegroundColor White + Write-Host " • Directory upload: $($dirUploadDuration.TotalSeconds.ToString('F2'))s for $($dirUploadResults.Count) files" -ForegroundColor White + Write-Host " • Filtered upload: $($filterDuration.TotalSeconds.ToString('F2'))s for $($filterResults.Count) files" -ForegroundColor White + + # Cleanup option + if ($Cleanup) { + Write-Host "`n9. Cleaning up..." -ForegroundColor Yellow + Remove-MinIOBucket -BucketName $TestBucketName -Force + Remove-Item $TestDirectory -Recurse -Force + Write-Host "✅ Cleanup completed!" -ForegroundColor Green + } else { + Write-Host "`nTest bucket '$TestBucketName' and files preserved for inspection." -ForegroundColor Cyan + Write-Host "Use -Cleanup parameter to automatically clean up test resources." -ForegroundColor Cyan + } + +} catch { + Write-Host "❌ Test failed: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "Stack trace: $($_.ScriptStackTrace)" -ForegroundColor Red +} finally { + # Clean up test files if they exist + if (Test-Path $TestDirectory) { + Write-Host "`nCleaning up local test files..." -ForegroundColor Yellow + Remove-Item $TestDirectory -Recurse -Force -ErrorAction SilentlyContinue + } +} + +Write-Host "`n=== Test Complete ===" -ForegroundColor Cyan diff --git a/scripts/Test-Current-Upload.ps1 b/scripts/Test-Current-Upload.ps1 new file mode 100644 index 0000000..f1599c5 --- /dev/null +++ b/scripts/Test-Current-Upload.ps1 @@ -0,0 +1,117 @@ +# Test script for current upload functionality (single file version) +param( + [string]$TestBucketName = "psminiotest-current-$(Get-Date -Format 'yyyyMMdd-HHmmss')", + [switch]$Cleanup, + [switch]$Verbose +) + +if ($Verbose) { + $VerbosePreference = 'Continue' +} + +Write-Host "=== PSMinIO Current Upload Test ===" -ForegroundColor Cyan +Write-Host "Testing current upload functionality" -ForegroundColor Green + +try { + # Import the module + Write-Host "`n1. Loading PSMinIO module..." -ForegroundColor Yellow + Import-Module ".\Module\PSMinIO\PSMinIO.psd1" -Force + + # Connect to MinIO + Write-Host "2. Connecting to MinIO..." -ForegroundColor Yellow + Connect-MinIO -Endpoint "https://api.s3.gracesolution.info" -AccessKey "T34Wg85SAwezUa3sk3m4" -SecretKey "PxEmnbQoQJTJsDocSEV6mSSscDpJMiJCayPv93xe" + Write-Host "✅ Connected successfully!" -ForegroundColor Green + + # Create test files + Write-Host "`n3. Creating test files..." -ForegroundColor Yellow + $testDir = "TestFilesCurrent" + if (Test-Path $testDir) { + Remove-Item $testDir -Recurse -Force + } + New-Item -ItemType Directory -Path $testDir -Force | Out-Null + + # Create 10 test files + $testFiles = @() + for ($i = 1; $i -le 10; $i++) { + $fileName = "testfile-{0:D3}.txt" -f $i + $filePath = Join-Path $testDir $fileName + $content = "Test file $i - $(Get-Date)`n" + ("Content line $i`n" * 10) + Set-Content -Path $filePath -Value $content + $testFiles += Get-Item $filePath + } + + Write-Host "✅ Created 10 test files" -ForegroundColor Green + + # Create test bucket + Write-Host "`n4. Creating test bucket: $TestBucketName" -ForegroundColor Yellow + New-MinIOBucket -BucketName $TestBucketName + Write-Host "✅ Bucket created successfully!" -ForegroundColor Green + + # Test uploading files one by one (current functionality) + Write-Host "`n5. Uploading files individually..." -ForegroundColor Yellow + $uploadResults = @() + $uploadStart = Get-Date + + foreach ($file in $testFiles) { + Write-Host " Uploading: $($file.Name)" -ForegroundColor Cyan + $result = New-MinIOObject -BucketName $TestBucketName -File $file -BucketDirectory "individual-uploads" -PassThru -Verbose + $uploadResults += $result + } + + $uploadDuration = (Get-Date) - $uploadStart + Write-Host "✅ Upload completed in $($uploadDuration.TotalSeconds.ToString('F2')) seconds!" -ForegroundColor Green + Write-Host " Files uploaded: $($uploadResults.Count)" -ForegroundColor Green + + # Verify uploads + Write-Host "`n6. Verifying uploads..." -ForegroundColor Yellow + $objects = Get-MinIOObject -BucketName $TestBucketName + Write-Host "✅ Found $($objects.Count) objects in bucket" -ForegroundColor Green + + # Test with different bucket directories + Write-Host "`n7. Testing nested bucket directories..." -ForegroundColor Yellow + $nestedResults = @() + foreach ($file in $testFiles[0..2]) { # Upload first 3 files to nested structure + $result = New-MinIOObject -BucketName $TestBucketName -File $file -BucketDirectory "level1/level2/level3" -PassThru -Verbose + $nestedResults += $result + } + Write-Host "✅ Nested directory upload completed: $($nestedResults.Count) files" -ForegroundColor Green + + # Final verification + Write-Host "`n8. Final verification..." -ForegroundColor Yellow + $allObjects = Get-MinIOObject -BucketName $TestBucketName + $individualObjects = $allObjects | Where-Object { $_.Name -like "individual-uploads/*" } + $nestedObjects = $allObjects | Where-Object { $_.Name -like "level1/level2/level3/*" } + + Write-Host "✅ Final verification complete:" -ForegroundColor Green + Write-Host " Total objects: $($allObjects.Count)" -ForegroundColor Green + Write-Host " Individual uploads: $($individualObjects.Count)" -ForegroundColor Green + Write-Host " Nested uploads: $($nestedObjects.Count)" -ForegroundColor Green + + # Summary + Write-Host "`n=== TEST SUMMARY ===" -ForegroundColor Cyan + Write-Host "✅ Current functionality test completed!" -ForegroundColor Green + Write-Host "Features tested:" -ForegroundColor Yellow + Write-Host " • Single file upload with File parameter" -ForegroundColor White + Write-Host " • BucketDirectory parameter for nested structures" -ForegroundColor White + Write-Host " • PassThru parameter for upload results" -ForegroundColor White + Write-Host " • Multiple individual uploads" -ForegroundColor White + Write-Host " • Nested directory structure creation" -ForegroundColor White + + Write-Host "`nNote: Enhanced features (FileInfo[], Directory, Multi-layer progress) require updated DLL" -ForegroundColor Yellow + + if ($Cleanup) { + Write-Host "`n9. Cleaning up..." -ForegroundColor Yellow + Remove-MinIOBucket -BucketName $TestBucketName -Force + Write-Host "✅ Cleanup completed!" -ForegroundColor Green + } + +} catch { + Write-Host "❌ Test failed: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "Stack trace: $($_.ScriptStackTrace)" -ForegroundColor Red +} finally { + if (Test-Path $testDir) { + Remove-Item $testDir -Recurse -Force -ErrorAction SilentlyContinue + } +} + +Write-Host "`n=== Test Complete ===" -ForegroundColor Cyan diff --git a/scripts/Test-ZipArchive.ps1 b/scripts/Test-ZipArchive.ps1 new file mode 100644 index 0000000..4be9c86 --- /dev/null +++ b/scripts/Test-ZipArchive.ps1 @@ -0,0 +1,174 @@ +# Test script for the new New-MinIOZipArchive cmdlet +# Demonstrates comprehensive zip functionality with progress tracking + +param( + [string]$TestDirectory = "TestZipFiles", + [switch]$Cleanup, + [switch]$Verbose +) + +if ($Verbose) { + $VerbosePreference = 'Continue' +} + +Write-Host "=== PSMinIO Zip Archive Test ===" -ForegroundColor Cyan +Write-Host "Testing New-MinIOZipArchive cmdlet with comprehensive functionality" -ForegroundColor Green + +try { + # Import the module + Write-Host "`n1. Loading PSMinIO module..." -ForegroundColor Yellow + Import-Module ".\Module\PSMinIO\PSMinIO.psd1" -Force + + # Create test directory and files + Write-Host "2. Creating test files..." -ForegroundColor Yellow + if (Test-Path $TestDirectory) { + Remove-Item $TestDirectory -Recurse -Force + } + New-Item -ItemType Directory -Path $TestDirectory -Force | Out-Null + + # Create main directory files + for ($i = 1; $i -le 10; $i++) { + $fileName = "document-{0:D2}.txt" -f $i + $filePath = Join-Path $TestDirectory $fileName + $content = "Document $i`n" + ("Sample content line $i`n" * (Get-Random -Minimum 5 -Maximum 20)) + Set-Content -Path $filePath -Value $content + } + + # Create subdirectories with different file types + $subDir1 = Join-Path $TestDirectory "Reports" + $subDir2 = Join-Path $TestDirectory "Data" + $subDir3 = Join-Path $TestDirectory "Logs" + New-Item -ItemType Directory -Path $subDir1, $subDir2, $subDir3 -Force | Out-Null + + # Add files to subdirectories + for ($i = 1; $i -le 5; $i++) { + Set-Content -Path (Join-Path $subDir1 "report$i.txt") -Value "Report $i content" + Set-Content -Path (Join-Path $subDir2 "data$i.csv") -Value "ID,Name,Value`n$i,Item$i,$(Get-Random -Minimum 100 -Maximum 1000)" + Set-Content -Path (Join-Path $subDir3 "app$i.log") -Value "$(Get-Date) - Log entry $i" + } + + $allFiles = Get-ChildItem $TestDirectory -Recurse -File + Write-Host "✅ Created test structure: $($allFiles.Count) files in multiple directories" -ForegroundColor Green + + # Test 1: Create zip from FileInfo array + Write-Host "`n3. Test 1: Creating zip from FileInfo array..." -ForegroundColor Yellow + $mainFiles = Get-ChildItem $TestDirectory -File + $zipPath1 = "test-files-array.zip" + + Write-Host " Creating zip with $($mainFiles.Count) files using Files parameter set" -ForegroundColor Cyan + $result1 = New-MinIOZipArchive -DestinationPath $zipPath1 -Path $mainFiles -CompressionLevel Optimal -PassThru -Verbose + + Write-Host "✅ Files array zip created:" -ForegroundColor Green + Write-Host " Files: $($result1.FileCount)" -ForegroundColor White + Write-Host " Original size: $([math]::Round($result1.TotalUncompressedSize / 1KB, 2)) KB" -ForegroundColor White + Write-Host " Compressed size: $([math]::Round($result1.TotalCompressedSize / 1KB, 2)) KB" -ForegroundColor White + Write-Host " Compression: $($result1.CompressionEfficiency.ToString('F1'))%" -ForegroundColor White + Write-Host " Duration: $($result1.Duration.TotalSeconds.ToString('F2'))s" -ForegroundColor White + + # Test 2: Create zip from directory (non-recursive) + Write-Host "`n4. Test 2: Creating zip from directory (non-recursive)..." -ForegroundColor Yellow + $zipPath2 = "test-directory-flat.zip" + + Write-Host " Creating zip from directory (top-level files only)" -ForegroundColor Cyan + $result2 = New-MinIOZipArchive -DestinationPath $zipPath2 -Directory (Get-Item $TestDirectory) -CompressionLevel Fastest -PassThru -Verbose + + Write-Host "✅ Directory flat zip created:" -ForegroundColor Green + Write-Host " Files: $($result2.FileCount)" -ForegroundColor White + Write-Host " Compression: $($result2.CompressionEfficiency.ToString('F1'))%" -ForegroundColor White + + # Test 3: Create zip from directory (recursive) + Write-Host "`n5. Test 3: Creating zip from directory (recursive)..." -ForegroundColor Yellow + $zipPath3 = "test-directory-recursive.zip" + + Write-Host " Creating zip from directory (recursive, all subdirectories)" -ForegroundColor Cyan + $result3 = New-MinIOZipArchive -DestinationPath $zipPath3 -Directory (Get-Item $TestDirectory) -Recursive -IncludeBaseDirectory -CompressionLevel Optimal -PassThru -Verbose + + Write-Host "✅ Directory recursive zip created:" -ForegroundColor Green + Write-Host " Files: $($result3.FileCount)" -ForegroundColor White + Write-Host " Compression: $($result3.CompressionEfficiency.ToString('F1'))%" -ForegroundColor White + + # Test 4: Create zip with file filtering + Write-Host "`n6. Test 4: Creating zip with file filtering..." -ForegroundColor Yellow + $zipPath4 = "test-filtered-logs.zip" + + Write-Host " Creating zip with only .log files using InclusionFilter" -ForegroundColor Cyan + $result4 = New-MinIOZipArchive -DestinationPath $zipPath4 -Directory (Get-Item $TestDirectory) -Recursive -InclusionFilter { $_.Extension -eq ".log" } -CompressionLevel Optimal -PassThru -Verbose + + Write-Host "✅ Filtered zip created:" -ForegroundColor Green + Write-Host " Log files: $($result4.FileCount)" -ForegroundColor White + Write-Host " Compression: $($result4.CompressionEfficiency.ToString('F1'))%" -ForegroundColor White + + # Test 5: Append to existing zip (Update mode) + Write-Host "`n7. Test 5: Appending to existing zip..." -ForegroundColor Yellow + $csvFiles = Get-ChildItem $TestDirectory -Recurse -Filter "*.csv" + + Write-Host " Appending $($csvFiles.Count) CSV files to existing zip" -ForegroundColor Cyan + $result5 = New-MinIOZipArchive -DestinationPath $zipPath4 -Path $csvFiles -Mode Update -CompressionLevel Optimal -PassThru -Verbose + + Write-Host "✅ Files appended to zip:" -ForegroundColor Green + Write-Host " Total files now: $($result5.FileCount)" -ForegroundColor White + + # Test 6: Create zip with custom base path + Write-Host "`n8. Test 6: Creating zip with custom base path..." -ForegroundColor Yellow + $zipPath6 = "test-custom-basepath.zip" + + Write-Host " Creating zip with custom base path (flattened structure)" -ForegroundColor Cyan + $result6 = New-MinIOZipArchive -DestinationPath $zipPath6 -Directory (Get-Item $TestDirectory) -Recursive -BasePath $TestDirectory -CompressionLevel Optimal -PassThru -Verbose + + Write-Host "✅ Custom base path zip created:" -ForegroundColor Green + Write-Host " Files: $($result6.FileCount)" -ForegroundColor White + + # Verify all zip files + Write-Host "`n9. Verifying created zip files..." -ForegroundColor Yellow + $zipFiles = Get-ChildItem "*.zip" + + foreach ($zipFile in $zipFiles) { + try { + $archive = [System.IO.Compression.ZipFile]::OpenRead($zipFile.FullName) + $entryCount = $archive.Entries.Count + $archive.Dispose() + Write-Host " ✅ $($zipFile.Name): $entryCount entries" -ForegroundColor Green + } + catch { + Write-Host " ❌ $($zipFile.Name): Verification failed - $($_.Exception.Message)" -ForegroundColor Red + } + } + + # Summary + Write-Host "`n=== TEST SUMMARY ===" -ForegroundColor Cyan + Write-Host "✅ All zip archive tests completed successfully!" -ForegroundColor Green + Write-Host "Features tested:" -ForegroundColor Yellow + Write-Host " • FileInfo[] parameter with Files parameter set" -ForegroundColor White + Write-Host " • Directory parameter with recursive and non-recursive modes" -ForegroundColor White + Write-Host " • File filtering with InclusionFilter ScriptBlock" -ForegroundColor White + Write-Host " • Append mode (Update) for adding files to existing zips" -ForegroundColor White + Write-Host " • Custom base path for entry name control" -ForegroundColor White + Write-Host " • Multiple compression levels (Optimal, Fastest)" -ForegroundColor White + Write-Host " • Comprehensive progress tracking and metrics" -ForegroundColor White + Write-Host " • PassThru parameter for detailed results" -ForegroundColor White + + Write-Host "`nZip files created:" -ForegroundColor Yellow + foreach ($zipFile in $zipFiles) { + $sizeKB = [math]::Round($zipFile.Length / 1KB, 2) + Write-Host " • $($zipFile.Name) ($sizeKB KB)" -ForegroundColor White + } + + if ($Cleanup) { + Write-Host "`n10. Cleaning up..." -ForegroundColor Yellow + Remove-Item "*.zip" -Force -ErrorAction SilentlyContinue + Write-Host "✅ Cleanup completed!" -ForegroundColor Green + } else { + Write-Host "`nZip files preserved for inspection. Use -Cleanup to remove them." -ForegroundColor Cyan + } + +} catch { + Write-Host "❌ Test failed: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "Stack trace: $($_.ScriptStackTrace)" -ForegroundColor Red +} finally { + # Clean up test directory + if (Test-Path $TestDirectory) { + Remove-Item $TestDirectory -Recurse -Force -ErrorAction SilentlyContinue + } +} + +Write-Host "`n=== Test Complete ===" -ForegroundColor Cyan diff --git a/scripts/Update-DLL.ps1 b/scripts/Update-DLL.ps1 new file mode 100644 index 0000000..a2f52b6 --- /dev/null +++ b/scripts/Update-DLL.ps1 @@ -0,0 +1,51 @@ +# Force update the PSMinIO DLL with the enhanced version + +Write-Host "=== Updating PSMinIO DLL ===" -ForegroundColor Cyan + +try { + # Stop all PowerShell processes to release the DLL + Write-Host "1. Stopping PowerShell processes..." -ForegroundColor Yellow + Get-Process | Where-Object {$_.ProcessName -like "*powershell*" -or $_.ProcessName -like "*pwsh*"} | ForEach-Object { + Write-Host " Stopping process: $($_.ProcessName) (PID: $($_.Id))" -ForegroundColor Gray + try { + $_ | Stop-Process -Force -ErrorAction SilentlyContinue + } catch { + Write-Host " Could not stop process $($_.Id): $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + # Wait a moment for processes to fully stop + Start-Sleep -Seconds 3 + + # Copy the new DLL + Write-Host "2. Copying enhanced DLL..." -ForegroundColor Yellow + $sourceDLL = "bin\Release\netstandard2.0\PSMinIO.dll" + $targetDLL = "Module\PSMinIO\bin\PSMinIO.dll" + + if (Test-Path $sourceDLL) { + Copy-Item $sourceDLL $targetDLL -Force + Write-Host "✅ DLL updated successfully!" -ForegroundColor Green + + # Verify the copy + $sourceSize = (Get-Item $sourceDLL).Length + $targetSize = (Get-Item $targetDLL).Length + Write-Host " Source size: $($sourceSize) bytes" -ForegroundColor Gray + Write-Host " Target size: $($targetSize) bytes" -ForegroundColor Gray + + if ($sourceSize -eq $targetSize) { + Write-Host "✅ File sizes match - copy successful!" -ForegroundColor Green + } else { + Write-Host "⚠️ File sizes don't match - copy may have failed!" -ForegroundColor Yellow + } + } else { + Write-Host "❌ Source DLL not found: $sourceDLL" -ForegroundColor Red + Write-Host " Run 'dotnet build PSMinIO.csproj --configuration Release' first" -ForegroundColor Yellow + } + +} catch { + Write-Host "❌ Update failed: $($_.Exception.Message)" -ForegroundColor Red +} + +Write-Host "`n=== Update Complete ===" -ForegroundColor Cyan +Write-Host "You can now test the enhanced functionality with:" -ForegroundColor Yellow +Write-Host " .\scripts\Test-50-Files-Upload.ps1 -Verbose" -ForegroundColor White diff --git a/src/Cmdlets/NewMinIOZipArchiveCmdlet.cs b/src/Cmdlets/NewMinIOZipArchiveCmdlet.cs new file mode 100644 index 0000000..a460e3c --- /dev/null +++ b/src/Cmdlets/NewMinIOZipArchiveCmdlet.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using PSMinIO.Core.Models; +using PSMinIO.Utils; + +namespace PSMinIO.Cmdlets +{ + /// + /// Creates zip archives with comprehensive progress tracking and metrics + /// + [Cmdlet(VerbsCommon.New, "MinIOZipArchive", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)] + [OutputType(typeof(ZipCreationResult))] + public class NewMinIOZipArchiveCmdlet : MinIOBaseCmdlet + { + /// + /// Path where the zip archive will be created + /// + [Parameter(Position = 0, Mandatory = true)] + [ValidateNotNullOrEmpty] + [Alias("ZipPath", "Archive")] + public string DestinationPath { get; set; } = string.Empty; + + /// + /// Array of FileInfo objects to add to the zip + /// + [Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ParameterSetName = "Files")] + [ValidateNotNull] + [Alias("File", "Files")] + public FileInfo[]? Path { get; set; } + + /// + /// Directory to add to the zip archive + /// + [Parameter(Position = 1, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Directory")] + [ValidateNotNull] + [Alias("Dir", "Folder")] + public DirectoryInfo? Directory { get; set; } + + /// + /// Base path to remove from entry names (creates relative paths in zip) + /// + [Parameter] + [ValidateNotNullOrEmpty] + [Alias("Base")] + public string? BasePath { get; set; } + + /// + /// Include the base directory name in zip entries (Directory parameter set only) + /// + [Parameter(ParameterSetName = "Directory")] + public SwitchParameter IncludeBaseDirectory { get; set; } + + /// + /// Process directory contents recursively (Directory parameter set only) + /// + [Parameter(ParameterSetName = "Directory")] + public SwitchParameter Recursive { get; set; } + + /// + /// Maximum depth for recursive processing (0 = unlimited) + /// + [Parameter(ParameterSetName = "Directory")] + [ValidateRange(0, int.MaxValue)] + public int MaxDepth { get; set; } = 0; + + /// + /// Script block to filter files for inclusion + /// + [Parameter(ParameterSetName = "Directory")] + public ScriptBlock? InclusionFilter { get; set; } + + /// + /// Script block to filter files for exclusion + /// + [Parameter(ParameterSetName = "Directory")] + public ScriptBlock? ExclusionFilter { get; set; } + + /// + /// Compression level to use + /// + [Parameter] + [ValidateSet("Optimal", "Fastest", "NoCompression")] + public string CompressionLevel { get; set; } = "Optimal"; + + /// + /// Archive mode (Create, Update for appending) + /// + [Parameter] + [ValidateSet("Create", "Update")] + public string Mode { get; set; } = "Create"; + + /// + /// Overwrite existing zip file without prompting + /// + [Parameter] + public SwitchParameter Force { get; set; } + + /// + /// Return the zip creation result + /// + [Parameter] + public SwitchParameter PassThru { get; set; } + + /// + /// Processes the cmdlet + /// + protected override void ProcessRecord() + { + // Validate destination path + var destinationInfo = new FileInfo(DestinationPath); + if (destinationInfo.Exists && !Force.IsPresent) + { + var errorRecord = new ErrorRecord( + new InvalidOperationException($"Zip file already exists: {DestinationPath}. Use -Force to overwrite."), + "ZipFileExists", + ErrorCategory.ResourceExists, + DestinationPath); + ThrowTerminatingError(errorRecord); + } + + // Ensure destination directory exists + if (destinationInfo.Directory != null && !destinationInfo.Directory.Exists) + { + destinationInfo.Directory.Create(); + WriteVerboseMessage("Created destination directory: {0}", destinationInfo.Directory.FullName); + } + + // Parse compression level + var compressionLevel = ParseCompressionLevel(CompressionLevel); + var archiveMode = ParseArchiveMode(Mode); + + // Determine operation description for ShouldProcess + var operationDescription = ParameterSetName switch + { + "Files" => $"Create zip archive with {Path?.Length ?? 0} files", + "Directory" => $"Create zip archive from directory '{Directory?.Name}'", + _ => "Create zip archive" + }; + + if (ShouldProcess(DestinationPath, operationDescription)) + { + ExecuteOperation("CreateZipArchive", () => + { + WriteVerboseMessage("Creating zip archive: {0}", DestinationPath); + WriteVerboseMessage("Compression level: {0}, Mode: {1}", CompressionLevel, Mode); + + ZipCreationResult result; + using (var zipBuilder = ZipBuilder.CreateFile(this, DestinationPath, archiveMode)) + { + switch (ParameterSetName) + { + case "Files": + result = ProcessFiles(zipBuilder, compressionLevel); + break; + case "Directory": + result = ProcessDirectory(zipBuilder, compressionLevel); + break; + default: + throw new InvalidOperationException($"Unknown parameter set: {ParameterSetName}"); + } + } + + WriteVerboseMessage("Zip archive created successfully: {0}", DestinationPath); + WriteVerboseMessage("Archive summary: {0} files, {1} -> {2} ({3:F1}% compression)", + result.FileCount, + SizeFormatter.FormatBytes(result.TotalUncompressedSize), + SizeFormatter.FormatBytes(result.TotalCompressedSize), + result.CompressionEfficiency); + + if (PassThru.IsPresent) + { + WriteObject(result); + } + + return result; + }, $"Destination: {DestinationPath}, ParameterSet: {ParameterSetName}"); + } + } + + /// + /// Processes files for zip creation + /// + private ZipCreationResult ProcessFiles(ZipBuilder zipBuilder, System.IO.Compression.CompressionLevel compressionLevel) + { + if (Path == null || Path.Length == 0) + { + WriteWarning("No files provided for zip archive"); + return zipBuilder.CreateResult(DestinationPath); + } + + // Filter out files that don't exist + var validFiles = Path.Where(f => f.Exists).ToArray(); + var skippedCount = Path.Length - validFiles.Length; + + if (skippedCount > 0) + { + WriteWarning($"Skipped {skippedCount} files that do not exist"); + } + + if (validFiles.Length == 0) + { + WriteWarning("No valid files found for zip archive"); + return zipBuilder.CreateResult(DestinationPath); + } + + WriteVerboseMessage("Adding {0} files to zip archive", validFiles.Length); + + // Add files to zip + zipBuilder.AddFiles(validFiles.Cast(), BasePath, compressionLevel); + + return zipBuilder.CreateResult(DestinationPath); + } + + /// + /// Processes directory for zip creation + /// + private ZipCreationResult ProcessDirectory(ZipBuilder zipBuilder, System.IO.Compression.CompressionLevel compressionLevel) + { + if (Directory == null || !Directory.Exists) + { + var errorRecord = new ErrorRecord( + new DirectoryNotFoundException($"Directory not found: {Directory?.FullName}"), + "DirectoryNotFound", + ErrorCategory.ObjectNotFound, + Directory); + ThrowTerminatingError(errorRecord); + return zipBuilder.CreateResult(DestinationPath); + } + + // Get files from directory + var files = GetDirectoryFiles(); + if (files.Length == 0) + { + WriteWarning($"No files found in directory: {Directory.FullName}"); + return zipBuilder.CreateResult(DestinationPath); + } + + WriteVerboseMessage("Adding directory to zip: {0} ({1} files)", Directory.Name, files.Length); + + // Determine base path for entries + var basePath = BasePath ?? (IncludeBaseDirectory.IsPresent ? Directory.Parent?.FullName : Directory.FullName); + + // Add files to zip + zipBuilder.AddFiles(files.Cast(), basePath, compressionLevel); + + return zipBuilder.CreateResult(DestinationPath); + } + + /// + /// Gets files from directory based on filters and recursion settings + /// + private FileInfo[] GetDirectoryFiles() + { + var searchOption = Recursive.IsPresent ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + var allFiles = Directory!.GetFiles("*", searchOption); + + // Apply depth filtering if MaxDepth is specified and we're recursive + if (Recursive.IsPresent && MaxDepth > 0) + { + var basePath = Directory.FullName; + allFiles = allFiles.Where(f => + { + var relativePath = f.FullName.Substring(basePath.Length).TrimStart('\\', '/'); + var depth = relativePath.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries).Length - 1; + return depth <= MaxDepth; + }).ToArray(); + } + + // Apply inclusion filter + if (InclusionFilter != null) + { + allFiles = allFiles.Where(f => EvaluateFilter(InclusionFilter, f)).ToArray(); + } + + // Apply exclusion filter + if (ExclusionFilter != null) + { + allFiles = allFiles.Where(f => !EvaluateFilter(ExclusionFilter, f)).ToArray(); + } + + return allFiles; + } + + /// + /// Evaluates a script block filter against a file + /// + private bool EvaluateFilter(ScriptBlock filter, FileInfo file) + { + try + { + var result = filter.InvokeWithContext(null, new List { new PSVariable("_", file) }); + return result.Count > 0 && LanguagePrimitives.IsTrue(result[0]); + } + catch (Exception ex) + { + WriteWarning($"Filter evaluation failed for {file.Name}: {ex.Message}"); + return false; + } + } + + /// + /// Parses compression level string to enum + /// + private static System.IO.Compression.CompressionLevel ParseCompressionLevel(string level) + { + return level switch + { + "Optimal" => System.IO.Compression.CompressionLevel.Optimal, + "Fastest" => System.IO.Compression.CompressionLevel.Fastest, + "NoCompression" => System.IO.Compression.CompressionLevel.NoCompression, + _ => System.IO.Compression.CompressionLevel.Optimal + }; + } + + /// + /// Parses archive mode string to enum + /// + private static ZipArchiveMode ParseArchiveMode(string mode) + { + return mode switch + { + "Create" => ZipArchiveMode.Create, + "Update" => ZipArchiveMode.Update, + _ => ZipArchiveMode.Create + }; + } + } +} diff --git a/src/Utils/ZipArchiveBuilder.cs b/src/Utils/ZipArchiveBuilder.cs new file mode 100644 index 0000000..2eb45e7 --- /dev/null +++ b/src/Utils/ZipArchiveBuilder.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace PSMinIO.Utils +{ + /// + /// High-performance zip archive builder with progress tracking and comprehensive metrics + /// Built on System.IO.Compression for minimal dependencies and maximum compatibility + /// + public class ZipArchiveBuilder : IDisposable + { + private readonly Stream _outputStream; + private readonly ZipArchive _archive; + private readonly bool _leaveOpen; + private bool _disposed = false; + + // Progress tracking + public event EventHandler? ProgressChanged; + public event EventHandler? FileAdded; + public event EventHandler? Completed; + + // Metrics + public DateTime StartTime { get; private set; } + public DateTime? EndTime { get; private set; } + public TimeSpan? Duration => EndTime?.Subtract(StartTime); + public long TotalUncompressedSize { get; private set; } + public long TotalCompressedSize { get; private set; } + public int FileCount { get; private set; } + public double CompressionRatio => TotalUncompressedSize > 0 ? (double)TotalCompressedSize / TotalUncompressedSize : 0; + + /// + /// Creates a new zip archive builder + /// + /// Stream to write the zip archive to + /// Zip archive mode (Create, Update, Read) + /// Whether to leave the output stream open when disposing + public ZipArchiveBuilder(Stream outputStream, ZipArchiveMode mode = ZipArchiveMode.Create, bool leaveOpen = false) + { + _outputStream = outputStream ?? throw new ArgumentNullException(nameof(outputStream)); + _leaveOpen = leaveOpen; + _archive = new ZipArchive(_outputStream, mode, _leaveOpen); + StartTime = DateTime.UtcNow; + } + + /// + /// Creates a new zip archive builder for a file + /// + /// Path to the zip file to create + /// Zip archive mode + public static ZipArchiveBuilder CreateFile(string zipFilePath, ZipArchiveMode mode = ZipArchiveMode.Create) + { + var fileStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write); + return new ZipArchiveBuilder(fileStream, mode, false); + } + + /// + /// Adds a single file to the zip archive + /// + /// File to add + /// Name of the entry in the zip (optional, uses file name if null) + /// Compression level to use + public void AddFile(FileInfo fileInfo, string? entryName = null, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + if (fileInfo == null) throw new ArgumentNullException(nameof(fileInfo)); + if (!fileInfo.Exists) throw new FileNotFoundException($"File not found: {fileInfo.FullName}"); + + entryName ??= fileInfo.Name; + var fileStartTime = DateTime.UtcNow; + + // Create entry in zip + var entry = _archive.CreateEntry(entryName, compressionLevel); + entry.LastWriteTime = fileInfo.LastWriteTime; + + long bytesProcessed = 0; + using (var fileStream = fileInfo.OpenRead()) + using (var entryStream = entry.Open()) + { + var buffer = new byte[81920]; // 80KB buffer for good performance + int bytesRead; + + while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0) + { + entryStream.Write(buffer, 0, bytesRead); + bytesProcessed += bytesRead; + + // Report progress + OnProgressChanged(new ZipProgressEventArgs + { + CurrentFileName = fileInfo.Name, + CurrentFileProgress = fileInfo.Length > 0 ? (double)bytesProcessed / fileInfo.Length * 100 : 100, + CurrentFileBytesProcessed = bytesProcessed, + CurrentFileSize = fileInfo.Length, + TotalFilesProcessed = FileCount, + TotalBytesProcessed = TotalUncompressedSize + bytesProcessed, + ElapsedTime = DateTime.UtcNow - StartTime + }); + } + } + + // Update metrics + TotalUncompressedSize += fileInfo.Length; + TotalCompressedSize += entry.CompressedLength; + FileCount++; + + var fileEndTime = DateTime.UtcNow; + OnFileAdded(new ZipFileEventArgs + { + FileName = fileInfo.Name, + EntryName = entryName, + UncompressedSize = fileInfo.Length, + CompressedSize = entry.CompressedLength, + CompressionRatio = fileInfo.Length > 0 ? (double)entry.CompressedLength / fileInfo.Length : 0, + ProcessingTime = fileEndTime - fileStartTime + }); + } + + /// + /// Adds multiple files to the zip archive + /// + /// Files to add + /// Base path to remove from entry names (optional) + /// Compression level to use + public void AddFiles(IEnumerable files, string? basePath = null, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + if (files == null) throw new ArgumentNullException(nameof(files)); + + foreach (var file in files) + { + if (file is FileInfo fileInfo) + { + var entryName = GetEntryName(fileInfo, basePath); + AddFile(fileInfo, entryName, compressionLevel); + } + else if (file is DirectoryInfo dirInfo) + { + // Add directory files recursively + var dirFiles = dirInfo.GetFiles("*", SearchOption.AllDirectories); + var dirBasePath = basePath ?? dirInfo.FullName; + AddFiles(dirFiles, dirBasePath, compressionLevel); + } + } + } + + /// + /// Adds a directory and all its contents to the zip archive + /// + /// Directory to add + /// Whether to include the base directory in entry names + /// Compression level to use + public void AddDirectory(DirectoryInfo directoryInfo, bool includeBaseDirectory = true, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + if (directoryInfo == null) throw new ArgumentNullException(nameof(directoryInfo)); + if (!directoryInfo.Exists) throw new DirectoryNotFoundException($"Directory not found: {directoryInfo.FullName}"); + + var files = directoryInfo.GetFiles("*", SearchOption.AllDirectories); + var basePath = includeBaseDirectory ? directoryInfo.Parent?.FullName : directoryInfo.FullName; + + AddFiles(files, basePath, compressionLevel); + } + + /// + /// Completes the zip archive and finalizes metrics + /// + public void Complete() + { + if (EndTime.HasValue) return; // Already completed + + EndTime = DateTime.UtcNow; + + OnCompleted(new ZipCompletedEventArgs + { + StartTime = StartTime, + EndTime = EndTime.Value, + Duration = Duration!.Value, + TotalFiles = FileCount, + TotalUncompressedSize = TotalUncompressedSize, + TotalCompressedSize = TotalCompressedSize, + CompressionRatio = CompressionRatio, + AverageCompressionSpeed = Duration!.Value.TotalSeconds > 0 ? TotalUncompressedSize / Duration.Value.TotalSeconds : 0 + }); + } + + /// + /// Gets the entry name for a file relative to a base path + /// + private string GetEntryName(FileInfo fileInfo, string? basePath) + { + if (string.IsNullOrEmpty(basePath)) + return fileInfo.Name; + + var fullPath = fileInfo.FullName; + if (fullPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) + { + var relativePath = fullPath.Substring(basePath.Length).TrimStart('\\', '/'); + return relativePath.Replace('\\', '/'); // Use forward slashes for zip entries + } + + return fileInfo.Name; + } + + /// + /// Raises the ProgressChanged event + /// + protected virtual void OnProgressChanged(ZipProgressEventArgs e) + { + ProgressChanged?.Invoke(this, e); + } + + /// + /// Raises the FileAdded event + /// + protected virtual void OnFileAdded(ZipFileEventArgs e) + { + FileAdded?.Invoke(this, e); + } + + /// + /// Raises the Completed event + /// + protected virtual void OnCompleted(ZipCompletedEventArgs e) + { + Completed?.Invoke(this, e); + } + + /// + /// Disposes the zip archive and underlying stream + /// + public void Dispose() + { + if (!_disposed) + { + Complete(); + _archive?.Dispose(); + if (!_leaveOpen) + { + _outputStream?.Dispose(); + } + _disposed = true; + } + } + } +} diff --git a/src/Utils/ZipBuilder.cs b/src/Utils/ZipBuilder.cs new file mode 100644 index 0000000..5a57706 --- /dev/null +++ b/src/Utils/ZipBuilder.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; + +namespace PSMinIO.Utils +{ + /// + /// Zip builder with integrated progress tracking and PowerShell compatibility + /// Provides seamless integration with PSMinIO's progress reporting system + /// + public class ZipBuilder : IDisposable + { + private readonly PSCmdlet _cmdlet; + private readonly ZipArchiveBuilder _zipBuilder; + private readonly ThreadSafeProgressCollector _progressCollector; + private bool _disposed = false; + + // Activity IDs for progress tracking + private const int ZipActivityId = 10; + private const int FileActivityId = 11; + + /// + /// Gets the underlying zip archive builder metrics + /// + public ZipArchiveBuilder ArchiveBuilder => _zipBuilder; + + /// + /// Creates a new zip builder + /// + /// PowerShell cmdlet for progress reporting + /// Stream to write zip to + /// Zip archive mode + /// Whether to leave output stream open + public ZipBuilder(PSCmdlet cmdlet, Stream outputStream, ZipArchiveMode mode = ZipArchiveMode.Create, bool leaveOpen = false) + { + _cmdlet = cmdlet ?? throw new ArgumentNullException(nameof(cmdlet)); + _zipBuilder = new ZipArchiveBuilder(outputStream, mode, leaveOpen); + _progressCollector = new ThreadSafeProgressCollector(cmdlet); + + // Subscribe to zip builder events + _zipBuilder.ProgressChanged += OnProgressChanged; + _zipBuilder.FileAdded += OnFileAdded; + _zipBuilder.Completed += OnCompleted; + } + + /// + /// Creates a zip builder for a file + /// + /// PowerShell cmdlet for progress reporting + /// Path to zip file to create + /// Zip archive mode + public static ZipBuilder CreateFile(PSCmdlet cmdlet, string zipFilePath, ZipArchiveMode mode = ZipArchiveMode.Create) + { + var fileStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write); + return new ZipBuilder(cmdlet, fileStream, mode, false); + } + + /// + /// Adds a single file with PowerShell progress reporting + /// + /// File to add + /// Entry name in zip (optional) + /// Compression level + public void AddFile(FileInfo fileInfo, string? entryName = null, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + if (fileInfo == null) throw new ArgumentNullException(nameof(fileInfo)); + + MinIOLogger.WriteVerbose(_cmdlet, "Adding file to zip: {0} ({1})", + fileInfo.Name, SizeFormatter.FormatBytes(fileInfo.Length)); + + _zipBuilder.AddFile(fileInfo, entryName, compressionLevel); + } + + /// + /// Adds multiple files with comprehensive progress tracking + /// + /// Files to add + /// Base path for entry names (optional) + /// Compression level + public void AddFiles(IEnumerable files, string? basePath = null, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + if (files == null) throw new ArgumentNullException(nameof(files)); + + var fileList = files.ToList(); + var totalFiles = fileList.Count; + var totalSize = fileList.OfType().Sum(f => f.Length); + + MinIOLogger.WriteVerbose(_cmdlet, "Starting zip compression: {0} files, {1} total size", + totalFiles, SizeFormatter.FormatBytes(totalSize)); + + // Initialize overall progress + _progressCollector.QueueProgressUpdate(ZipActivityId, "Creating Zip Archive", + $"Preparing to compress {totalFiles} files", 0); + + _zipBuilder.AddFiles(files, basePath, compressionLevel); + } + + /// + /// Adds a directory with progress tracking + /// + /// Directory to add + /// Include base directory in entry names + /// Compression level + public void AddDirectory(DirectoryInfo directoryInfo, bool includeBaseDirectory = true, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + if (directoryInfo == null) throw new ArgumentNullException(nameof(directoryInfo)); + + var files = directoryInfo.GetFiles("*", SearchOption.AllDirectories); + var totalSize = files.Sum(f => f.Length); + + MinIOLogger.WriteVerbose(_cmdlet, "Adding directory to zip: {0} ({1} files, {2})", + directoryInfo.Name, files.Length, SizeFormatter.FormatBytes(totalSize)); + + _zipBuilder.AddDirectory(directoryInfo, includeBaseDirectory, compressionLevel); + } + + /// + /// Completes the zip and processes final progress updates + /// + public void Complete() + { + _zipBuilder.Complete(); + _progressCollector.Complete(); + + MinIOLogger.WriteVerbose(_cmdlet, "Zip compression completed: {0} files, {1} -> {2} ({3:F1}% compression)", + _zipBuilder.FileCount, + SizeFormatter.FormatBytes(_zipBuilder.TotalUncompressedSize), + SizeFormatter.FormatBytes(_zipBuilder.TotalCompressedSize), + (1 - _zipBuilder.CompressionRatio) * 100); + } + + /// + /// Handles progress updates from the zip builder + /// + private void OnProgressChanged(object? sender, ZipProgressEventArgs e) + { + // Update file-level progress + _progressCollector.QueueProgressUpdate(FileActivityId, "Compressing File", + $"Processing: {e.CurrentFileName} ({SizeFormatter.FormatBytes(e.CurrentFileBytesProcessed)}/{SizeFormatter.FormatBytes(e.CurrentFileSize)})", + (int)e.CurrentFileProgress, ZipActivityId); + + // Update overall progress + var overallStatus = $"Compressed {e.TotalFilesProcessed} files"; + if (e.ElapsedTime.TotalSeconds > 0) + { + var speed = e.TotalBytesProcessed / e.ElapsedTime.TotalSeconds; + overallStatus += $" at {SizeFormatter.FormatSpeed(speed)}"; + } + + _progressCollector.QueueProgressUpdate(ZipActivityId, "Creating Zip Archive", overallStatus, -1); + + // Process queued updates + _progressCollector.ProcessQueuedUpdates(); + } + + /// + /// Handles file added events from the zip builder + /// + private void OnFileAdded(object? sender, ZipFileEventArgs e) + { + _progressCollector.QueueVerboseMessage("Compressed: {0} -> {1} ({2:F1}% reduction, {3})", + e.FileName, + SizeFormatter.FormatBytes(e.CompressedSize), + e.CompressionEfficiency, + SizeFormatter.FormatDuration(e.ProcessingTime)); + + // Complete file progress + _progressCollector.QueueProgressCompletion(FileActivityId, "Compressing File", ZipActivityId); + } + + /// + /// Handles completion events from the zip builder + /// + private void OnCompleted(object? sender, ZipCompletedEventArgs e) + { + _progressCollector.QueueVerboseMessage("Zip compression summary:"); + _progressCollector.QueueVerboseMessage(" Files: {0}", e.TotalFiles); + _progressCollector.QueueVerboseMessage(" Original size: {0}", SizeFormatter.FormatBytes(e.TotalUncompressedSize)); + _progressCollector.QueueVerboseMessage(" Compressed size: {0}", SizeFormatter.FormatBytes(e.TotalCompressedSize)); + _progressCollector.QueueVerboseMessage(" Space saved: {0} ({1:F1}%)", + SizeFormatter.FormatBytes(e.SpaceSaved), e.CompressionEfficiency); + _progressCollector.QueueVerboseMessage(" Duration: {0}", SizeFormatter.FormatDuration(e.Duration)); + _progressCollector.QueueVerboseMessage(" Average speed: {0}", SizeFormatter.FormatSpeed(e.AverageCompressionSpeed)); + + // Complete overall progress + _progressCollector.QueueProgressCompletion(ZipActivityId, "Creating Zip Archive"); + } + + /// + /// Creates a zip result object with comprehensive metrics + /// + /// Path to the created zip file + /// Zip creation result + public ZipCreationResult CreateResult(string zipFilePath) + { + return new ZipCreationResult + { + ZipFilePath = zipFilePath, + StartTime = _zipBuilder.StartTime, + EndTime = _zipBuilder.EndTime ?? DateTime.UtcNow, + Duration = _zipBuilder.Duration ?? TimeSpan.Zero, + FileCount = _zipBuilder.FileCount, + TotalUncompressedSize = _zipBuilder.TotalUncompressedSize, + TotalCompressedSize = _zipBuilder.TotalCompressedSize, + CompressionRatio = _zipBuilder.CompressionRatio, + SpaceSaved = _zipBuilder.TotalUncompressedSize - _zipBuilder.TotalCompressedSize + }; + } + + /// + /// Disposes the zip builder and resources + /// + public void Dispose() + { + if (!_disposed) + { + Complete(); + _zipBuilder?.Dispose(); + _disposed = true; + } + } + } + + /// + /// Result object for zip creation operations + /// + public class ZipCreationResult + { + public string ZipFilePath { get; set; } = string.Empty; + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public TimeSpan Duration { get; set; } + public int FileCount { get; set; } + public long TotalUncompressedSize { get; set; } + public long TotalCompressedSize { get; set; } + public double CompressionRatio { get; set; } + public long SpaceSaved { get; set; } + public double CompressionEfficiency => (1 - CompressionRatio) * 100; + public double AverageSpeed => Duration.TotalSeconds > 0 ? TotalUncompressedSize / Duration.TotalSeconds : 0; + } +} diff --git a/src/Utils/ZipEventArgs.cs b/src/Utils/ZipEventArgs.cs new file mode 100644 index 0000000..87a6bf3 --- /dev/null +++ b/src/Utils/ZipEventArgs.cs @@ -0,0 +1,147 @@ +using System; + +namespace PSMinIO.Utils +{ + /// + /// Event arguments for zip progress updates + /// + public class ZipProgressEventArgs : EventArgs + { + /// + /// Name of the current file being processed + /// + public string CurrentFileName { get; set; } = string.Empty; + + /// + /// Progress percentage for the current file (0-100) + /// + public double CurrentFileProgress { get; set; } + + /// + /// Bytes processed for the current file + /// + public long CurrentFileBytesProcessed { get; set; } + + /// + /// Total size of the current file + /// + public long CurrentFileSize { get; set; } + + /// + /// Number of files processed so far + /// + public int TotalFilesProcessed { get; set; } + + /// + /// Total bytes processed across all files + /// + public long TotalBytesProcessed { get; set; } + + /// + /// Elapsed time since compression started + /// + public TimeSpan ElapsedTime { get; set; } + + /// + /// Current compression speed in bytes per second + /// + public double CurrentSpeed => ElapsedTime.TotalSeconds > 0 ? TotalBytesProcessed / ElapsedTime.TotalSeconds : 0; + } + + /// + /// Event arguments for when a file is added to the zip + /// + public class ZipFileEventArgs : EventArgs + { + /// + /// Original file name + /// + public string FileName { get; set; } = string.Empty; + + /// + /// Entry name in the zip archive + /// + public string EntryName { get; set; } = string.Empty; + + /// + /// Uncompressed size of the file + /// + public long UncompressedSize { get; set; } + + /// + /// Compressed size in the zip archive + /// + public long CompressedSize { get; set; } + + /// + /// Compression ratio (compressed/uncompressed) + /// + public double CompressionRatio { get; set; } + + /// + /// Time taken to process this file + /// + public TimeSpan ProcessingTime { get; set; } + + /// + /// Compression efficiency percentage (100 - ratio * 100) + /// + public double CompressionEfficiency => (1 - CompressionRatio) * 100; + } + + /// + /// Event arguments for when zip compression is completed + /// + public class ZipCompletedEventArgs : EventArgs + { + /// + /// Time when compression started + /// + public DateTime StartTime { get; set; } + + /// + /// Time when compression completed + /// + public DateTime EndTime { get; set; } + + /// + /// Total duration of compression + /// + public TimeSpan Duration { get; set; } + + /// + /// Total number of files compressed + /// + public int TotalFiles { get; set; } + + /// + /// Total uncompressed size of all files + /// + public long TotalUncompressedSize { get; set; } + + /// + /// Total compressed size of the zip archive + /// + public long TotalCompressedSize { get; set; } + + /// + /// Overall compression ratio + /// + public double CompressionRatio { get; set; } + + /// + /// Average compression speed in bytes per second + /// + public double AverageCompressionSpeed { get; set; } + + /// + /// Overall compression efficiency percentage + /// + public double CompressionEfficiency => (1 - CompressionRatio) * 100; + + /// + /// Space saved by compression + /// + public long SpaceSaved => TotalUncompressedSize - TotalCompressedSize; + } +}