Implement comprehensive zip archive functionality with progress tracking

 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.
This commit is contained in:
PSMinIO Developer
2025-07-11 21:00:14 -04:00
parent 6f693d7236
commit f420effc84
12 changed files with 1499 additions and 1 deletions
+245
View File
@@ -0,0 +1,245 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
namespace PSMinIO.Utils
{
/// <summary>
/// High-performance zip archive builder with progress tracking and comprehensive metrics
/// Built on System.IO.Compression for minimal dependencies and maximum compatibility
/// </summary>
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<ZipProgressEventArgs>? ProgressChanged;
public event EventHandler<ZipFileEventArgs>? FileAdded;
public event EventHandler<ZipCompletedEventArgs>? 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;
/// <summary>
/// Creates a new zip archive builder
/// </summary>
/// <param name="outputStream">Stream to write the zip archive to</param>
/// <param name="mode">Zip archive mode (Create, Update, Read)</param>
/// <param name="leaveOpen">Whether to leave the output stream open when disposing</param>
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;
}
/// <summary>
/// Creates a new zip archive builder for a file
/// </summary>
/// <param name="zipFilePath">Path to the zip file to create</param>
/// <param name="mode">Zip archive mode</param>
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);
}
/// <summary>
/// Adds a single file to the zip archive
/// </summary>
/// <param name="fileInfo">File to add</param>
/// <param name="entryName">Name of the entry in the zip (optional, uses file name if null)</param>
/// <param name="compressionLevel">Compression level to use</param>
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
});
}
/// <summary>
/// Adds multiple files to the zip archive
/// </summary>
/// <param name="files">Files to add</param>
/// <param name="basePath">Base path to remove from entry names (optional)</param>
/// <param name="compressionLevel">Compression level to use</param>
public void AddFiles(IEnumerable<FileSystemInfo> 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);
}
}
}
/// <summary>
/// Adds a directory and all its contents to the zip archive
/// </summary>
/// <param name="directoryInfo">Directory to add</param>
/// <param name="includeBaseDirectory">Whether to include the base directory in entry names</param>
/// <param name="compressionLevel">Compression level to use</param>
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);
}
/// <summary>
/// Completes the zip archive and finalizes metrics
/// </summary>
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
});
}
/// <summary>
/// Gets the entry name for a file relative to a base path
/// </summary>
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;
}
/// <summary>
/// Raises the ProgressChanged event
/// </summary>
protected virtual void OnProgressChanged(ZipProgressEventArgs e)
{
ProgressChanged?.Invoke(this, e);
}
/// <summary>
/// Raises the FileAdded event
/// </summary>
protected virtual void OnFileAdded(ZipFileEventArgs e)
{
FileAdded?.Invoke(this, e);
}
/// <summary>
/// Raises the Completed event
/// </summary>
protected virtual void OnCompleted(ZipCompletedEventArgs e)
{
Completed?.Invoke(this, e);
}
/// <summary>
/// Disposes the zip archive and underlying stream
/// </summary>
public void Dispose()
{
if (!_disposed)
{
Complete();
_archive?.Dispose();
if (!_leaveOpen)
{
_outputStream?.Dispose();
}
_disposed = true;
}
}
}
}
+244
View File
@@ -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
{
/// <summary>
/// Zip builder with integrated progress tracking and PowerShell compatibility
/// Provides seamless integration with PSMinIO's progress reporting system
/// </summary>
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;
/// <summary>
/// Gets the underlying zip archive builder metrics
/// </summary>
public ZipArchiveBuilder ArchiveBuilder => _zipBuilder;
/// <summary>
/// Creates a new zip builder
/// </summary>
/// <param name="cmdlet">PowerShell cmdlet for progress reporting</param>
/// <param name="outputStream">Stream to write zip to</param>
/// <param name="mode">Zip archive mode</param>
/// <param name="leaveOpen">Whether to leave output stream open</param>
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;
}
/// <summary>
/// Creates a zip builder for a file
/// </summary>
/// <param name="cmdlet">PowerShell cmdlet for progress reporting</param>
/// <param name="zipFilePath">Path to zip file to create</param>
/// <param name="mode">Zip archive mode</param>
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);
}
/// <summary>
/// Adds a single file with PowerShell progress reporting
/// </summary>
/// <param name="fileInfo">File to add</param>
/// <param name="entryName">Entry name in zip (optional)</param>
/// <param name="compressionLevel">Compression level</param>
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);
}
/// <summary>
/// Adds multiple files with comprehensive progress tracking
/// </summary>
/// <param name="files">Files to add</param>
/// <param name="basePath">Base path for entry names (optional)</param>
/// <param name="compressionLevel">Compression level</param>
public void AddFiles(IEnumerable<FileSystemInfo> 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<FileInfo>().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);
}
/// <summary>
/// Adds a directory with progress tracking
/// </summary>
/// <param name="directoryInfo">Directory to add</param>
/// <param name="includeBaseDirectory">Include base directory in entry names</param>
/// <param name="compressionLevel">Compression level</param>
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);
}
/// <summary>
/// Completes the zip and processes final progress updates
/// </summary>
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);
}
/// <summary>
/// Handles progress updates from the zip builder
/// </summary>
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();
}
/// <summary>
/// Handles file added events from the zip builder
/// </summary>
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);
}
/// <summary>
/// Handles completion events from the zip builder
/// </summary>
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");
}
/// <summary>
/// Creates a zip result object with comprehensive metrics
/// </summary>
/// <param name="zipFilePath">Path to the created zip file</param>
/// <returns>Zip creation result</returns>
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
};
}
/// <summary>
/// Disposes the zip builder and resources
/// </summary>
public void Dispose()
{
if (!_disposed)
{
Complete();
_zipBuilder?.Dispose();
_disposed = true;
}
}
}
/// <summary>
/// Result object for zip creation operations
/// </summary>
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;
}
}
+147
View File
@@ -0,0 +1,147 @@
using System;
namespace PSMinIO.Utils
{
/// <summary>
/// Event arguments for zip progress updates
/// </summary>
public class ZipProgressEventArgs : EventArgs
{
/// <summary>
/// Name of the current file being processed
/// </summary>
public string CurrentFileName { get; set; } = string.Empty;
/// <summary>
/// Progress percentage for the current file (0-100)
/// </summary>
public double CurrentFileProgress { get; set; }
/// <summary>
/// Bytes processed for the current file
/// </summary>
public long CurrentFileBytesProcessed { get; set; }
/// <summary>
/// Total size of the current file
/// </summary>
public long CurrentFileSize { get; set; }
/// <summary>
/// Number of files processed so far
/// </summary>
public int TotalFilesProcessed { get; set; }
/// <summary>
/// Total bytes processed across all files
/// </summary>
public long TotalBytesProcessed { get; set; }
/// <summary>
/// Elapsed time since compression started
/// </summary>
public TimeSpan ElapsedTime { get; set; }
/// <summary>
/// Current compression speed in bytes per second
/// </summary>
public double CurrentSpeed => ElapsedTime.TotalSeconds > 0 ? TotalBytesProcessed / ElapsedTime.TotalSeconds : 0;
}
/// <summary>
/// Event arguments for when a file is added to the zip
/// </summary>
public class ZipFileEventArgs : EventArgs
{
/// <summary>
/// Original file name
/// </summary>
public string FileName { get; set; } = string.Empty;
/// <summary>
/// Entry name in the zip archive
/// </summary>
public string EntryName { get; set; } = string.Empty;
/// <summary>
/// Uncompressed size of the file
/// </summary>
public long UncompressedSize { get; set; }
/// <summary>
/// Compressed size in the zip archive
/// </summary>
public long CompressedSize { get; set; }
/// <summary>
/// Compression ratio (compressed/uncompressed)
/// </summary>
public double CompressionRatio { get; set; }
/// <summary>
/// Time taken to process this file
/// </summary>
public TimeSpan ProcessingTime { get; set; }
/// <summary>
/// Compression efficiency percentage (100 - ratio * 100)
/// </summary>
public double CompressionEfficiency => (1 - CompressionRatio) * 100;
}
/// <summary>
/// Event arguments for when zip compression is completed
/// </summary>
public class ZipCompletedEventArgs : EventArgs
{
/// <summary>
/// Time when compression started
/// </summary>
public DateTime StartTime { get; set; }
/// <summary>
/// Time when compression completed
/// </summary>
public DateTime EndTime { get; set; }
/// <summary>
/// Total duration of compression
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Total number of files compressed
/// </summary>
public int TotalFiles { get; set; }
/// <summary>
/// Total uncompressed size of all files
/// </summary>
public long TotalUncompressedSize { get; set; }
/// <summary>
/// Total compressed size of the zip archive
/// </summary>
public long TotalCompressedSize { get; set; }
/// <summary>
/// Overall compression ratio
/// </summary>
public double CompressionRatio { get; set; }
/// <summary>
/// Average compression speed in bytes per second
/// </summary>
public double AverageCompressionSpeed { get; set; }
/// <summary>
/// Overall compression efficiency percentage
/// </summary>
public double CompressionEfficiency => (1 - CompressionRatio) * 100;
/// <summary>
/// Space saved by compression
/// </summary>
public long SpaceSaved => TotalUncompressedSize - TotalCompressedSize;
}
}