From f420effc8451dceb06e464d1fd4557e8503ebc9a Mon Sep 17 00:00:00 2001 From: PSMinIO Developer Date: Fri, 11 Jul 2025 21:00:14 -0400 Subject: [PATCH] Implement comprehensive zip archive functionality with progress tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ ZipArchiveBuilder - Core compression engine: • Built on System.IO.Compression (.NET Standard 2.0 built-in) • Real-time progress tracking with events • Start/end time tracking with duration calculation • Size metrics (compressed/uncompressed) and compression ratios • FileSystemInfo support (FileInfo, DirectoryInfo) • Append support via ZipArchiveMode.Update • Compression efficiency calculations ✅ ZipBuilder - PowerShell integration layer: • ThreadSafeProgressCollector integration • Multi-layer progress tracking (Zip + File levels) • Verbose logging with comprehensive metrics • ZipCreationResult object for PowerShell output • Event-driven progress reporting ✅ New-MinIOZipArchive cmdlet - Well-structured PowerShell interface: • Files parameter set: FileInfo[] support for multiple files • Directory parameter set: DirectoryInfo with recursive options • File filtering: InclusionFilter/ExclusionFilter ScriptBlocks • Compression levels: Optimal, Fastest, NoCompression • Archive modes: Create, Update (for appending) • BasePath parameter for custom entry name control • Force parameter for overwriting existing archives • PassThru parameter for detailed result objects ✅ Advanced features: • Progress tracking: Real-time speed and ETA calculations • Metrics: Compression ratio, space saved, processing time • File filtering: ScriptBlock-based inclusion/exclusion • Path handling: Cross-platform compatibility • Error handling: Comprehensive validation and recovery • Memory efficiency: 80KB buffer for optimal throughput ✅ Integration benefits: • No external dependencies (uses built-in .NET compression) • Thread-safe operations compatible with PSMinIO architecture • Consistent with existing progress tracking patterns • Minimal footprint aligning with project preferences • Enterprise-grade functionality with comprehensive metrics Architecture: Built on System.IO.Compression with custom progress tracking, providing superior zip functionality with PowerShell-native integration. --- Module/PSMinIO/PSMinIO.psd1 | 3 +- Module/PSMinIO/bin/PSMinIO.dll | Bin 80384 -> 92672 bytes PSMinIO.csproj | 2 + scripts/Check-Parameters.ps1 | 29 +++ scripts/Test-50-Files-Upload.ps1 | 156 +++++++++++ scripts/Test-Current-Upload.ps1 | 117 +++++++++ scripts/Test-ZipArchive.ps1 | 174 +++++++++++++ scripts/Update-DLL.ps1 | 51 ++++ src/Cmdlets/NewMinIOZipArchiveCmdlet.cs | 332 ++++++++++++++++++++++++ src/Utils/ZipArchiveBuilder.cs | 245 +++++++++++++++++ src/Utils/ZipBuilder.cs | 244 +++++++++++++++++ src/Utils/ZipEventArgs.cs | 147 +++++++++++ 12 files changed, 1499 insertions(+), 1 deletion(-) create mode 100644 scripts/Check-Parameters.ps1 create mode 100644 scripts/Test-50-Files-Upload.ps1 create mode 100644 scripts/Test-Current-Upload.ps1 create mode 100644 scripts/Test-ZipArchive.ps1 create mode 100644 scripts/Update-DLL.ps1 create mode 100644 src/Cmdlets/NewMinIOZipArchiveCmdlet.cs create mode 100644 src/Utils/ZipArchiveBuilder.cs create mode 100644 src/Utils/ZipBuilder.cs create mode 100644 src/Utils/ZipEventArgs.cs 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 e495b2920805d71ca0e19203e7bd2ceeb3288a27..da284808d1989eeb060df4ee9ec5931f805e0ab4 100644 GIT binary patch literal 92672 zcmcG12YeM(_W!*z^X9d*m-5me1(Go2B_Sb@0w_gLK|oYMKon6dAo^eeii99RL{UJn ztsO;Cv19MO_l9K`Sy;QPu5H({ivRC9_s)AS6o2mTxBvHnJLh}ux#ynS>rBQ>StCp# zgoXdV{U*eHc+zhOyT9)&fpM_E|b3vr|r zBA~eIPXk>BdI_FFWHD_hyNN;e^Y1bEgHJzG9C-$b(!bjGC8sK&4eZK=aL=A(dr{FY2UKjTm$4EinmC1dbTBVHeLi zZRLnzp$U|kK$g=3%$h8>Cs^)W;)8;e<#SMl?2utQK|D<>!^-eEA&_CiA4fF|RTm?U z!~g%qQHeOVnpIl`rAKmI94_)E3$qw7r`{A)50LywTNNZjwz~?}oAF)%wtF>bBkq9K zj7`kwO$_K$Tq7O>S<_&z=-cSOo)3Z$n?K4zaYgOgOeDMaR?J9Ragv_zv+jn=qhmx-^;)^%J zx9mCSY$%NQqGeBmiuVK0MQfPj^aqvWGm2N8fT${?_WH_v_c(a0uPlgI>njW0ID?{y zZ@tq5*?2RYKpf38G~m$Re*0}}ZeV^X<*gmDYy1&A-U^>Bst9B^%IhL=`wJo$vGQ-* zHm@u!fs{~R60vFnh7(3D$uiveh|YA+glni=9RPjKKscyXmalCPC{^P8446nW0E=!Y>oP z#heV3RyFl|(-0ngSHyDApzV-PscGn@+l6}x)S+#dXcz8nOc$c96zR57tXoGc!h$A? z*c>l$B@)#L3w;!cYJp^{u5H78 z9y0l>@=&cSqZZes^$vPPO0z9yzz+1&=oMzwhtY$pUo6v_?VEj=#*>Fk31wIX4vNeU zy~^Wz40nVn<`kvKS}RXcvPffL zhP845Atk-laJNA^RkQZ5Ebu8o3c_WDNHUQ9T#~Aeo`{4{zmjP>BTz$XbKJcx%BoRt zb!FJ6yH~K}U)jx_M||!I6P1WF6me2LI@pWbP;nQy z3lP9$n|*Arvmf-udb$@U_?bpZ99n392<;pJjbBHoLK24iA;Nc{*R+hqBO%;@*@$I0 zqd*#FWo=qj%JAKR+^`I@4daX5fgaN`tPT{sW%!f=9*jDW3@AWnPMsOWg=Wt`!)On| zFMm=8Jq%MtWp$(ziq7gl*6V;g;jj${lLp&prdMbUhbtxQ{?ss27 zEJ4+?Ib)IUIsvNE`RxLgfB`C5Cik9p^`%}#F{0JFD0fp&s;^9x1Iz8H7|<}~Jj0uamiE$YEhSe^Mxk zo73#Y5olgLGkDk%-Z{9F*bE>^bbupF9HHgLne} zQOCt0@OlT&FEmYfIOx;yzhx|ii#n9Zv_RwgE8kuOv|ei9qR?fu8^b zO2-W>VbAdcC7vyq!|fV;pbj1+i7y$9(K^Qs+@JCIfid!Y4bCpHPh^P$2G*0rtqjHt z?90z_1F=wm72^jEAJ)*6vN$;VE#ZElf%rvC76FF<_xlUlGcncG z#$QUzN12(OV6G!s8tWa!t8BI+<3@oF~5fFVrh#??)mxI6@2>BlCu4u@HFsHm3 zT2#$%n+)HDcJ(O3bb6uox-U=!{lV0I2jwIEG6jz|x@Y)$aKC)_HHd;e1;RW_;~t74WAlCCC9s;2Y^Akn$a7fxQvrf_8$F&1&#*v;NM0T%$ zra;whg%Y2hBEH+84vTXehT*gg6+ySzkZ;1t#te&6qZ27af16j&)rQ&Ec84K`YGbyR zIV@xvlf(Ym_F-uO9*%j^R{d>al1_~S1Ex#0oW@J4*tAflx=a&Cs=n(hi@CJ$&(v^vJN{H}TUKzlEMWO2)B%wBs`nG3!M)%)dXR*By_nQo2iLDBW6he*CuG z#qd=ZwUp+zdr7YAk~4l+GVDEM-7#dHnuD%_b;v`^9Vm3Rh#w2TngXMpa>kC|330XF z!Qf=a-cxy!E#t?L*hGK)S_t^#*Rgv8+_vN4QR$E02s%C!becbYGw5jFw1A@Gv%vMo z(M@7QBak0I0Z>I3h|dNcyR3Sck-sCSgNnhR%@>|nQdz`Lgg?zP$=3Ku1WJtde1Pg6 zm;o~ZMuEp*0*peB!2}pZ9)k%miaiDsU|?%SDP;nT?jC~)FiJcI6JTIwuYxcEMw!Q8 zg7Vwoy*a87dQm|pYm$uBB$;XCcaU+0dpzny2bpD*mUNIcMro*n;xwRkoS4-?(HVsu z6p5r zys_;@>7@-9c-XXKf~Viv2hZw@3}+CYwSM;=$i#f^V|chP!7*sTXsgMU%4f`$_@{lK zFI2ma;k1w#gC@fnkN~|6X9z&Fq0DeF(EyRpYepNLTe?AApB>#{MR&;RD7PC!(13_0 z5^0M=b0{U2ny~G@1(V`);E@s`@pmNoiMWYh9Zblx8TH^zO!C+w;;m2euxN&H#QZAB zWIOwSw|7V@wBvJOM)eMNF*x4lfU=erC7hq$Hi6;n4W;O6Rc7#rv!3^?sFk5_x9C|x zPik#UfKll&m;j^7V=#eyqIy3CPx=|{J>Xpvb_>I(F)oZ-Xtbl^ShbE}&~({uL;qlR z5X;DSP>fja)UcEJv*3y<0b^~o0X zd8va!E7=1@p@kik)D$JPHSPp>YR*`wbLK%VU#<4k!y>c)M``K#JuB|+yJUxuZy%)6>(*)Srzj+Fx18roU2BD2W2nS%NRYWUNQj& zDve?=0YOfkHG{OeLMyesJE6tk%=g$eFmaMMmh^9NB!=J*jsFT^+S@|e+u^MRY3>OyBaqzq4z#sHe4!szRiEJq z(X*#0fIeAV?-x|w7>y!{eea#N#L+?HXI3SX7~U zm!!E@z!KBloU{icK&Xp74ppmG5gf)f43-p%#?itDTP4#6i4xDw>)7{q?92_RqqN#S zmKs(TBet3z?%xr=<=&T$fdp7G2-jgDSTvbwQQelz?uOHdh4OlW9BY%w&2_M0$$;K)#>8 zvsuY}*JdU2eQQbBy(wXqKWUafVV1V5Yo7NX>)D0rHtZ_?J-gbL!Je;0_sJ^gJG>`La@r#>b3$k&Kb<^_BY8o9Ss$V|&wj?7j$u4{Xm?fwQYD}E+?e6?A&o1TS`RFRpb)3~RC zG5n^3VZlzLDqW6{drDY<>U0*2M;kr$JTE-~u|HZWCaC{&868JvCC}4*I0e5AEHw

X|lvWAXA1iX)MsEn?vof~7mY;+&xS0iuZL;jTZ4>+udH ztopqH;_Ro?RUT**FTEj(RayH4wlqXJ<;xWa_xt(Mn7!M4X=#RvE?F}oAxK$+eQxB! zn65lO@s8`t3lMK^S6+~K*ay)C!umoHyVlH&VK3e3n%&`uUwQa<@<@vW$fG0@&>nQl zBrB!waH#K~m9Hwd^hi)C3MLe}7s7~~j7Ug%gc2SqL}nx{J4+Q{ERvBOajt~InzBeZ z5{YC*(yg`!k@1z0%=is>=49KE?D#rJc;))=j@^`Nj(Z8>jN~LIjgcJHAE8`__M<*X zhynON3I(tX|ISq(o2RX>B<{XTl#0qqk2!LuTvklT%ShhUK)=gEvIDkR42@?dr4L z?;vQ<5(KN@g>YA5E+h?S5lFS5AREyCiykxv%*WttgzTCe6~vBHz+&erls;`_(7JpO zWP3tZNzndGw%Cz}VW!xQl{i;()lB7u;_Ja{y9N#|FRvw4$gv?_jxhnoP>;a`7{fdU z6QgwgkhsphKkvfQnR7x;B6HAGD09$TB6Fx8wX0qCH2511Mr7m(`c#|uGt~At;@!uK zmkBV2dkiMP7~wIP0ApW|!2}rlc?>4N*xzF?v9IA=jCgs#iEq$3GSU-a0%1mZ3?{%h zz+*50#%Pbh1Q=sH1`}XlwW2B}6JQ+ZF_-{joX21S3@q7I5GJ_HLD7XVl=+=7+uJ3m z$ugX~Q*do4v1?GB^ufdsyINj2Ldo3&8VjjTO~bQ-_1~-Yq27gR$_>ZEsN{4xuj`>f zeF&4ayi8QR03y5;L1*ti#9?jMA-t-;=sH9t$912M>w5Ur8o5y6anSFDW~n3foz8O% zzE1`f-X1Ac5?v3e@HI)ru1P9z-SVkmHJ6GM{TP*7N?(B5%0Dgi-_lLZLXQ^CB}g?Z0>T9fy64Ui(^iP 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 + + 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; + } +}