Add Get-MinIOZipArchive cmdlet and enhance New-MinIOZipArchive with FileInfo parameters

 Get-MinIOZipArchive cmdlet - Comprehensive zip reading:
  • FileInfo parameter with proper disposal handling using OpenRead
  • ZipArchiveInfo result with comprehensive metrics
  • ZipEntryInfo array for detailed entry information
  • IncludeEntries parameter for detailed file listings
  • ValidateIntegrity parameter for archive validation
  • Filter parameter for entry name pattern matching
  • Proper exception handling and verbose logging
  • Always returns objects for pipeline compatibility

 Enhanced New-MinIOZipArchive cmdlet:
  • FileInfo DestinationPath parameter (was string)
  • Removed PassThru parameter - always returns objects
  • Consistent parameter naming with aliases
  • Improved error handling and validation
  • Better integration with PowerShell pipeline

 ZipArchiveInfo and ZipEntryInfo classes:
  • Comprehensive metrics: size, compression ratio, efficiency
  • Time tracking: creation, modification, validation duration
  • Entry details: full name, size, compression stats
  • Directory detection and space saved calculations
  • .NET Standard 2.0 compatibility (removed Crc32)

 Enhanced test script:
  • FileInfo parameter usage examples
  • Get-MinIOZipArchive functionality testing
  • Archive information reading and validation
  • Detailed entry inspection and filtering
  • Integrity validation demonstrations

 Architecture improvements:
  • Consistent FileInfo usage across zip operations
  • Proper disposal handling with using statements
  • Thread-safe operations with comprehensive error handling
  • Pipeline-friendly object returns
  • Enhanced verbose logging and progress tracking

Features: Create zips from FileInfo/DirectoryInfo, read archive info with validation,
detailed entry inspection, integrity checking, and comprehensive metrics.
This commit is contained in:
PSMinIO Developer
2025-07-11 21:16:21 -04:00
parent f420effc84
commit e1da6e92af
4 changed files with 302 additions and 74 deletions
+208
View File
@@ -0,0 +1,208 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Management.Automation;
using PSMinIO.Core.Models;
using PSMinIO.Utils;
namespace PSMinIO.Cmdlets
{
/// <summary>
/// Opens and reads zip archive information with proper disposal handling
/// </summary>
[Cmdlet(VerbsCommon.Get, "MinIOZipArchive")]
[OutputType(typeof(ZipArchiveInfo))]
public class GetMinIOZipArchiveCmdlet : MinIOBaseCmdlet
{
/// <summary>
/// Path to the zip archive file to read
/// </summary>
[Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNull]
[Alias("ZipPath", "Archive", "FullName")]
public FileInfo ZipFile { get; set; } = null!;
/// <summary>
/// Include detailed entry information for each file in the archive
/// </summary>
[Parameter]
public SwitchParameter IncludeEntries { get; set; }
/// <summary>
/// Validate the archive integrity by attempting to read all entries
/// </summary>
[Parameter]
public SwitchParameter ValidateIntegrity { get; set; }
/// <summary>
/// Filter entries by name pattern (supports wildcards)
/// </summary>
[Parameter]
[ValidateNotNullOrEmpty]
public string? Filter { get; set; }
/// <summary>
/// Processes the cmdlet
/// </summary>
protected override void ProcessRecord()
{
// Validate zip file exists
if (!ZipFile.Exists)
{
var errorRecord = new ErrorRecord(
new FileNotFoundException($"Zip file not found: {ZipFile.FullName}"),
"ZipFileNotFound",
ErrorCategory.ObjectNotFound,
ZipFile);
ThrowTerminatingError(errorRecord);
}
var archiveInfo = ExecuteOperation("ReadZipArchive", () =>
{
WriteVerboseMessage("Reading zip archive: {0}", ZipFile.FullName);
ZipArchiveInfo result;
using (var fileStream = ZipFile.OpenRead())
using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Read))
{
// Create archive info
result = new ZipArchiveInfo
{
ZipFilePath = ZipFile.FullName,
ZipFileName = ZipFile.Name,
ZipFileSize = ZipFile.Length,
CreationTime = ZipFile.CreationTime,
LastWriteTime = ZipFile.LastWriteTime,
EntryCount = archive.Entries.Count
};
// Calculate total uncompressed size
result.TotalUncompressedSize = archive.Entries.Sum(e => e.Length);
result.TotalCompressedSize = archive.Entries.Sum(e => e.CompressedLength);
result.CompressionRatio = result.TotalUncompressedSize > 0
? (double)result.TotalCompressedSize / result.TotalUncompressedSize
: 0;
WriteVerboseMessage("Archive contains {0} entries, {1} -> {2} ({3:F1}% compression)",
result.EntryCount,
SizeFormatter.FormatBytes(result.TotalUncompressedSize),
SizeFormatter.FormatBytes(result.TotalCompressedSize),
(1 - result.CompressionRatio) * 100);
// Include detailed entries if requested
if (IncludeEntries.IsPresent)
{
var entries = archive.Entries.AsEnumerable();
// Apply filter if specified
if (!string.IsNullOrEmpty(Filter))
{
var wildcardPattern = new WildcardPattern(Filter, WildcardOptions.IgnoreCase);
entries = entries.Where(e => wildcardPattern.IsMatch(e.FullName));
}
result.Entries = entries.Select(entry => new ZipEntryInfo
{
FullName = entry.FullName,
Name = entry.Name,
Length = entry.Length,
CompressedLength = entry.CompressedLength,
CompressionRatio = entry.Length > 0 ? (double)entry.CompressedLength / entry.Length : 0,
LastWriteTime = entry.LastWriteTime,
IsDirectory = string.IsNullOrEmpty(entry.Name) && entry.FullName.EndsWith("/")
}).ToArray();
WriteVerboseMessage("Included {0} entry details", result.Entries.Length);
}
// Validate integrity if requested
if (ValidateIntegrity.IsPresent)
{
WriteVerboseMessage("Validating archive integrity...");
var validationStart = DateTime.UtcNow;
var validEntries = 0;
var invalidEntries = 0;
foreach (var entry in archive.Entries)
{
try
{
using (var entryStream = entry.Open())
{
// Read a small portion to validate the entry can be opened
var buffer = new byte[1024];
entryStream.Read(buffer, 0, buffer.Length);
}
validEntries++;
}
catch (Exception ex)
{
invalidEntries++;
WriteWarning($"Entry '{entry.FullName}' failed validation: {ex.Message}");
}
}
var validationDuration = DateTime.UtcNow - validationStart;
result.IsValid = invalidEntries == 0;
result.ValidationDuration = validationDuration;
WriteVerboseMessage("Validation completed in {0}: {1} valid, {2} invalid entries",
SizeFormatter.FormatDuration(validationDuration),
validEntries,
invalidEntries);
if (invalidEntries > 0)
{
WriteWarning($"Archive validation found {invalidEntries} corrupted entries");
}
}
}
WriteVerboseMessage("Successfully read zip archive information");
return result;
}, $"ZipFile: {ZipFile.FullName}");
// Always return the archive info object
WriteObject(archiveInfo);
}
}
/// <summary>
/// Information about a zip archive
/// </summary>
public class ZipArchiveInfo
{
public string ZipFilePath { get; set; } = string.Empty;
public string ZipFileName { get; set; } = string.Empty;
public long ZipFileSize { get; set; }
public DateTime CreationTime { get; set; }
public DateTime LastWriteTime { get; set; }
public int EntryCount { get; set; }
public long TotalUncompressedSize { get; set; }
public long TotalCompressedSize { get; set; }
public double CompressionRatio { get; set; }
public double CompressionEfficiency => (1 - CompressionRatio) * 100;
public long SpaceSaved => TotalUncompressedSize - TotalCompressedSize;
public ZipEntryInfo[]? Entries { get; set; }
public bool? IsValid { get; set; }
public TimeSpan? ValidationDuration { get; set; }
}
/// <summary>
/// Information about a zip archive entry
/// </summary>
public class ZipEntryInfo
{
public string FullName { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public long Length { get; set; }
public long CompressedLength { get; set; }
public double CompressionRatio { get; set; }
public double CompressionEfficiency => (1 - CompressionRatio) * 100;
public long SpaceSaved => Length - CompressedLength;
public DateTimeOffset LastWriteTime { get; set; }
public bool IsDirectory { get; set; }
}
}
+31 -40
View File
@@ -17,12 +17,12 @@ namespace PSMinIO.Cmdlets
public class NewMinIOZipArchiveCmdlet : MinIOBaseCmdlet
{
/// <summary>
/// Path where the zip archive will be created
/// FileInfo object representing where the zip archive will be created
/// </summary>
[Parameter(Position = 0, Mandatory = true)]
[ValidateNotNullOrEmpty]
[Alias("ZipPath", "Archive")]
public string DestinationPath { get; set; } = string.Empty;
[ValidateNotNull]
[Alias("ZipPath", "Archive", "Destination")]
public FileInfo DestinationPath { get; set; } = null!;
/// <summary>
/// Array of FileInfo objects to add to the zip
@@ -99,23 +99,16 @@ namespace PSMinIO.Cmdlets
[Parameter]
public SwitchParameter Force { get; set; }
/// <summary>
/// Return the zip creation result
/// </summary>
[Parameter]
public SwitchParameter PassThru { get; set; }
/// <summary>
/// Processes the cmdlet
/// </summary>
protected override void ProcessRecord()
{
// Validate destination path
var destinationInfo = new FileInfo(DestinationPath);
if (destinationInfo.Exists && !Force.IsPresent)
if (DestinationPath.Exists && !Force.IsPresent)
{
var errorRecord = new ErrorRecord(
new InvalidOperationException($"Zip file already exists: {DestinationPath}. Use -Force to overwrite."),
new InvalidOperationException($"Zip file already exists: {DestinationPath.FullName}. Use -Force to overwrite."),
"ZipFileExists",
ErrorCategory.ResourceExists,
DestinationPath);
@@ -123,10 +116,10 @@ namespace PSMinIO.Cmdlets
}
// Ensure destination directory exists
if (destinationInfo.Directory != null && !destinationInfo.Directory.Exists)
if (DestinationPath.Directory != null && !DestinationPath.Directory.Exists)
{
destinationInfo.Directory.Create();
WriteVerboseMessage("Created destination directory: {0}", destinationInfo.Directory.FullName);
DestinationPath.Directory.Create();
WriteVerboseMessage("Created destination directory: {0}", DestinationPath.Directory.FullName);
}
// Parse compression level
@@ -141,43 +134,41 @@ namespace PSMinIO.Cmdlets
_ => "Create zip archive"
};
if (ShouldProcess(DestinationPath, operationDescription))
if (ShouldProcess(DestinationPath.FullName, operationDescription))
{
ExecuteOperation("CreateZipArchive", () =>
var result = ExecuteOperation("CreateZipArchive", () =>
{
WriteVerboseMessage("Creating zip archive: {0}", DestinationPath);
WriteVerboseMessage("Creating zip archive: {0}", DestinationPath.FullName);
WriteVerboseMessage("Compression level: {0}, Mode: {1}", CompressionLevel, Mode);
ZipCreationResult result;
using (var zipBuilder = ZipBuilder.CreateFile(this, DestinationPath, archiveMode))
ZipCreationResult zipResult;
using (var zipBuilder = ZipBuilder.CreateFile(this, DestinationPath.FullName, archiveMode))
{
switch (ParameterSetName)
{
case "Files":
result = ProcessFiles(zipBuilder, compressionLevel);
zipResult = ProcessFiles(zipBuilder, compressionLevel);
break;
case "Directory":
result = ProcessDirectory(zipBuilder, compressionLevel);
zipResult = ProcessDirectory(zipBuilder, compressionLevel);
break;
default:
throw new InvalidOperationException($"Unknown parameter set: {ParameterSetName}");
}
}
WriteVerboseMessage("Zip archive created successfully: {0}", DestinationPath);
WriteVerboseMessage("Zip archive created successfully: {0}", DestinationPath.FullName);
WriteVerboseMessage("Archive summary: {0} files, {1} -> {2} ({3:F1}% compression)",
result.FileCount,
SizeFormatter.FormatBytes(result.TotalUncompressedSize),
SizeFormatter.FormatBytes(result.TotalCompressedSize),
result.CompressionEfficiency);
zipResult.FileCount,
SizeFormatter.FormatBytes(zipResult.TotalUncompressedSize),
SizeFormatter.FormatBytes(zipResult.TotalCompressedSize),
zipResult.CompressionEfficiency);
if (PassThru.IsPresent)
{
WriteObject(result);
}
return zipResult;
}, $"Destination: {DestinationPath.FullName}, ParameterSet: {ParameterSetName}");
return result;
}, $"Destination: {DestinationPath}, ParameterSet: {ParameterSetName}");
// Always return the result object
WriteObject(result);
}
}
@@ -189,7 +180,7 @@ namespace PSMinIO.Cmdlets
if (Path == null || Path.Length == 0)
{
WriteWarning("No files provided for zip archive");
return zipBuilder.CreateResult(DestinationPath);
return zipBuilder.CreateResult(DestinationPath.FullName);
}
// Filter out files that don't exist
@@ -204,7 +195,7 @@ namespace PSMinIO.Cmdlets
if (validFiles.Length == 0)
{
WriteWarning("No valid files found for zip archive");
return zipBuilder.CreateResult(DestinationPath);
return zipBuilder.CreateResult(DestinationPath.FullName);
}
WriteVerboseMessage("Adding {0} files to zip archive", validFiles.Length);
@@ -212,7 +203,7 @@ namespace PSMinIO.Cmdlets
// Add files to zip
zipBuilder.AddFiles(validFiles.Cast<FileSystemInfo>(), BasePath, compressionLevel);
return zipBuilder.CreateResult(DestinationPath);
return zipBuilder.CreateResult(DestinationPath.FullName);
}
/// <summary>
@@ -228,7 +219,7 @@ namespace PSMinIO.Cmdlets
ErrorCategory.ObjectNotFound,
Directory);
ThrowTerminatingError(errorRecord);
return zipBuilder.CreateResult(DestinationPath);
return zipBuilder.CreateResult(DestinationPath.FullName);
}
// Get files from directory
@@ -236,7 +227,7 @@ namespace PSMinIO.Cmdlets
if (files.Length == 0)
{
WriteWarning($"No files found in directory: {Directory.FullName}");
return zipBuilder.CreateResult(DestinationPath);
return zipBuilder.CreateResult(DestinationPath.FullName);
}
WriteVerboseMessage("Adding directory to zip: {0} ({1} files)", Directory.Name, files.Length);
@@ -247,7 +238,7 @@ namespace PSMinIO.Cmdlets
// Add files to zip
zipBuilder.AddFiles(files.Cast<FileSystemInfo>(), basePath, compressionLevel);
return zipBuilder.CreateResult(DestinationPath);
return zipBuilder.CreateResult(DestinationPath.FullName);
}
/// <summary>