Reduce error handling verbosity and start enhanced upload cmdlet

- Simplified error handling to show only essential information:
  * Operation, Message, ExceptionType, InnerExceptionType, InnerExceptionMessage
  * Removed system info, stack traces, and operation details for cleaner output
- Started enhancing New-MinIOObject cmdlet with parameter sets:
  * SingleFile: Original single file upload functionality
  * Files: FileInfo[] support for multiple file uploads
  * Directory: DirectoryInfo support with recursive options
- Added new parameters for enhanced functionality:
  * BucketDirectory for organizing uploads into nested structures
  * Recursive, MaxDepth, Flatten for directory uploads
  * InclusionFilter, ExclusionFilter for file filtering
  * Force parameter for overwriting existing objects

Next: Complete the parameter set implementation and add multi-layer progress tracking.
This commit is contained in:
PSMinIO Developer
2025-07-11 17:27:01 -04:00
parent 83cbe8b1ef
commit 2c3cd7fb41
2 changed files with 84 additions and 169 deletions
+79 -140
View File
@@ -9,32 +9,88 @@ using PSMinIO.Utils;
namespace PSMinIO.Cmdlets
{
/// <summary>
/// Uploads objects to MinIO
/// Uploads files or directories to a MinIO bucket
/// </summary>
[Cmdlet(VerbsCommon.New, "MinIOObject", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]
[OutputType(typeof(MinIOUploadResult))]
public class NewMinIOObjectCmdlet : MinIOBaseCmdlet
{
/// <summary>
/// Name of the bucket
/// Name of the bucket to upload to
/// </summary>
[Parameter(Position = 0, Mandatory = true)]
[Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("Bucket")]
public string BucketName { get; set; } = string.Empty;
/// <summary>
/// Name of the object (if not specified, uses filename)
/// Single file to upload
/// </summary>
[Parameter(Position = 1)]
[Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ParameterSetName = "SingleFile")]
[ValidateNotNull]
public FileInfo? File { get; set; }
/// <summary>
/// Array of FileInfo objects to upload
/// </summary>
[Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ParameterSetName = "Files")]
[ValidateNotNull]
[Alias("Files")]
public FileInfo[]? Path { get; set; }
/// <summary>
/// Directory to upload
/// </summary>
[Parameter(Position = 1, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Directory")]
[ValidateNotNull]
[Alias("Dir")]
public DirectoryInfo? Directory { get; set; }
/// <summary>
/// Object name in the bucket (defaults to filename) - SingleFile parameter set only
/// </summary>
[Parameter(Position = 2, ParameterSetName = "SingleFile")]
[ValidateNotNullOrEmpty]
public string? ObjectName { get; set; }
/// <summary>
/// File to upload
/// Optional bucket directory path where files should be uploaded (Files parameter set only)
/// </summary>
[Parameter(Position = 2, Mandatory = true, ValueFromPipeline = true)]
[ValidateNotNull]
public FileInfo File { get; set; } = null!;
[Parameter(ParameterSetName = "Files")]
[ValidateNotNullOrEmpty]
[Alias("Prefix", "Folder")]
public string? BucketDirectory { get; set; }
/// <summary>
/// Upload directory contents recursively (Directory parameter set only)
/// </summary>
[Parameter(ParameterSetName = "Directory")]
public SwitchParameter Recursive { get; set; }
/// <summary>
/// Maximum depth for recursive directory upload (0 = unlimited)
/// </summary>
[Parameter(ParameterSetName = "Directory")]
[ValidateRange(0, int.MaxValue)]
public int MaxDepth { get; set; } = 0;
/// <summary>
/// Flatten directory structure (upload all files to bucket root)
/// </summary>
[Parameter(ParameterSetName = "Directory")]
public SwitchParameter Flatten { get; set; }
/// <summary>
/// Script block to filter files for inclusion
/// </summary>
[Parameter(ParameterSetName = "Directory")]
public ScriptBlock? InclusionFilter { get; set; }
/// <summary>
/// Script block to filter files for exclusion
/// </summary>
[Parameter(ParameterSetName = "Directory")]
public ScriptBlock? ExclusionFilter { get; set; }
/// <summary>
/// Content type of the file (auto-detected if not specified)
@@ -50,12 +106,10 @@ namespace PSMinIO.Cmdlets
public Hashtable? Metadata { get; set; }
/// <summary>
/// Directory prefix in the bucket (creates nested structure)
/// Overwrite existing objects without prompting
/// </summary>
[Parameter]
[ValidateNotNullOrEmpty]
[Alias("Folder", "Directory")]
public string? BucketDirectory { get; set; }
public SwitchParameter Force { get; set; }
/// <summary>
/// Force overwrite if object already exists
@@ -76,134 +130,19 @@ namespace PSMinIO.Cmdlets
{
ValidateBucketName(BucketName);
// Validate file exists
if (!File.Exists)
switch (ParameterSetName)
{
var errorRecord = new ErrorRecord(
new FileNotFoundException($"File not found: {File.FullName}"),
"FileNotFound",
ErrorCategory.ObjectNotFound,
File);
ThrowTerminatingError(errorRecord);
}
// Determine object name
var finalObjectName = ObjectName ?? File?.Name ?? "unknown";
// Add bucket directory prefix if specified
if (!string.IsNullOrEmpty(BucketDirectory))
{
var cleanDirectory = BucketDirectory.Trim('/');
if (!string.IsNullOrEmpty(cleanDirectory))
{
finalObjectName = $"{cleanDirectory}/{finalObjectName}";
}
}
ValidateObjectName(finalObjectName);
if (ShouldProcess($"{BucketName}/{finalObjectName}", "Upload file"))
{
var result = ExecuteOperation("UploadObject", () =>
{
WriteVerboseMessage("Uploading file '{0}' to bucket '{1}' as object '{2}'",
File.FullName, BucketName, finalObjectName);
// Create upload result
var uploadResult = new MinIOUploadResult(BucketName, finalObjectName, string.Empty, File.Length)
{
SourceFilePath = File.FullName
};
uploadResult.MarkStarted();
try
{
// Check if bucket exists
if (!S3Client.BucketExists(BucketName))
{
throw new InvalidOperationException($"Bucket '{BucketName}' does not exist");
}
// Determine content type
var fileContentType = ContentType ?? GetContentType(File.Extension);
uploadResult.ContentType = fileContentType;
WriteVerboseMessage("Using content type: {0}", fileContentType);
// Convert metadata
Dictionary<string, string>? metadata = null;
if (Metadata != null && Metadata.Count > 0)
{
metadata = new Dictionary<string, string>();
foreach (var key in Metadata.Keys)
{
if (key != null && Metadata[key] != null)
{
metadata[key.ToString()!] = Metadata[key]!.ToString()!;
}
}
WriteVerboseMessage("Added {0} metadata entries", metadata.Count);
}
// Track progress
var fileSize = File.Length;
var startTime = DateTime.UtcNow;
WriteVerboseMessage("Starting upload of {0}", SizeFormatter.FormatBytes(fileSize));
// Upload the file
string etag;
using (var fileStream = File.OpenRead())
{
etag = S3Client.PutObject(
BucketName,
finalObjectName,
fileStream,
fileContentType,
metadata,
bytesTransferred =>
{
// Only update the result object - no PowerShell calls from background thread
uploadResult.BytesTransferred = bytesTransferred;
});
}
uploadResult.ETag = etag;
uploadResult.MarkCompleted();
var duration = uploadResult.Duration ?? TimeSpan.Zero;
var averageSpeed = uploadResult.AverageSpeed ?? 0;
// Report final progress from main thread
var percentage = fileSize > 0 ? (double)uploadResult.BytesTransferred / fileSize * 100 : 100;
WriteVerboseMessage("Upload progress: {0:F1}% ({1}/{2}) at {3}",
percentage,
SizeFormatter.FormatBytes(uploadResult.BytesTransferred),
SizeFormatter.FormatBytes(fileSize),
SizeFormatter.FormatSpeed(averageSpeed));
WriteVerboseMessage("Upload completed in {0} at average speed of {1}",
SizeFormatter.FormatDuration(duration),
SizeFormatter.FormatSpeed(averageSpeed));
return uploadResult;
}
catch (Exception ex)
{
uploadResult.MarkFailed(ex.Message);
throw;
}
}, $"File: {File.FullName}, Bucket: {BucketName}, Object: {finalObjectName}");
// Return result if requested
if (PassThru.IsPresent)
{
WriteObject(result);
}
else
{
WriteVerboseMessage("Successfully uploaded object '{0}' to bucket '{1}'", finalObjectName, BucketName);
}
case "SingleFile":
ProcessSingleFile();
break;
case "Files":
ProcessFiles();
break;
case "Directory":
ProcessDirectory();
break;
default:
throw new InvalidOperationException($"Unknown parameter set: {ParameterSetName}");
}
}
+5 -29
View File
@@ -97,40 +97,19 @@ namespace PSMinIO.Utils
private static OrderedDictionary CreateDetailedErrorInfo(Exception exception, string operationName, string? operationDetails)
{
var errorDetails = new OrderedDictionary();
// Basic error information
errorDetails.Add("Operation", operationName);
errorDetails.Add("Message", exception.Message);
errorDetails.Add("ExceptionType", exception.GetType().FullName);
// Operation details if provided
if (!string.IsNullOrEmpty(operationDetails))
{
errorDetails.Add("OperationDetails", operationDetails);
}
// Inner exception information
if (exception.InnerException != null)
{
errorDetails.Add("InnerExceptionType", exception.InnerException.GetType().FullName);
errorDetails.Add("InnerExceptionMessage", exception.InnerException.Message);
}
// Stack trace information
if (!string.IsNullOrEmpty(exception.StackTrace))
{
var stackLines = exception.StackTrace.Split('\n');
if (stackLines.Length > 0)
{
errorDetails.Add("StackTraceTop", stackLines[0].Trim());
}
}
// System information
errorDetails.Add("Timestamp", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC"));
errorDetails.Add("MachineName", Environment.MachineName);
errorDetails.Add("ProcessId", Process.GetCurrentProcess().Id);
return errorDetails;
}
@@ -144,10 +123,7 @@ namespace PSMinIO.Utils
{
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
// Log header
cmdlet.WriteWarning($"{timestamp} - ERROR: PSMinIO Operation Failed");
// Log each error detail
// Log each error detail (reduced set)
foreach (DictionaryEntry detail in errorDetails)
{
var key = detail.Key?.ToString() ?? "Unknown";
@@ -159,7 +135,7 @@ namespace PSMinIO.Utils
value = value.Substring(0, 197) + "...";
}
cmdlet.WriteWarning($"{timestamp} - ERROR: {key}: {value}");
cmdlet.WriteWarning($"{timestamp} - {key}: {value}");
}
}
catch (InvalidOperationException)