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;
+ }
+}