mirror of
https://github.com/Grace-Solutions/PSMinIO.git
synced 2026-08-27 12:26:50 +00:00
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:
@@ -9,32 +9,88 @@ using PSMinIO.Utils;
|
|||||||
namespace PSMinIO.Cmdlets
|
namespace PSMinIO.Cmdlets
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Uploads objects to MinIO
|
/// Uploads files or directories to a MinIO bucket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Cmdlet(VerbsCommon.New, "MinIOObject", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]
|
[Cmdlet(VerbsCommon.New, "MinIOObject", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]
|
||||||
[OutputType(typeof(MinIOUploadResult))]
|
[OutputType(typeof(MinIOUploadResult))]
|
||||||
public class NewMinIOObjectCmdlet : MinIOBaseCmdlet
|
public class NewMinIOObjectCmdlet : MinIOBaseCmdlet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Name of the bucket
|
/// Name of the bucket to upload to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter(Position = 0, Mandatory = true)]
|
[Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true)]
|
||||||
[ValidateNotNullOrEmpty]
|
[ValidateNotNullOrEmpty]
|
||||||
|
[Alias("Bucket")]
|
||||||
public string BucketName { get; set; } = string.Empty;
|
public string BucketName { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Name of the object (if not specified, uses filename)
|
/// Single file to upload
|
||||||
/// </summary>
|
/// </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]
|
[ValidateNotNullOrEmpty]
|
||||||
public string? ObjectName { get; set; }
|
public string? ObjectName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// File to upload
|
/// Optional bucket directory path where files should be uploaded (Files parameter set only)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter(Position = 2, Mandatory = true, ValueFromPipeline = true)]
|
[Parameter(ParameterSetName = "Files")]
|
||||||
[ValidateNotNull]
|
[ValidateNotNullOrEmpty]
|
||||||
public FileInfo File { get; set; } = null!;
|
[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>
|
/// <summary>
|
||||||
/// Content type of the file (auto-detected if not specified)
|
/// Content type of the file (auto-detected if not specified)
|
||||||
@@ -50,12 +106,10 @@ namespace PSMinIO.Cmdlets
|
|||||||
public Hashtable? Metadata { get; set; }
|
public Hashtable? Metadata { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Directory prefix in the bucket (creates nested structure)
|
/// Overwrite existing objects without prompting
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter]
|
[Parameter]
|
||||||
[ValidateNotNullOrEmpty]
|
public SwitchParameter Force { get; set; }
|
||||||
[Alias("Folder", "Directory")]
|
|
||||||
public string? BucketDirectory { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Force overwrite if object already exists
|
/// Force overwrite if object already exists
|
||||||
@@ -76,134 +130,19 @@ namespace PSMinIO.Cmdlets
|
|||||||
{
|
{
|
||||||
ValidateBucketName(BucketName);
|
ValidateBucketName(BucketName);
|
||||||
|
|
||||||
// Validate file exists
|
switch (ParameterSetName)
|
||||||
if (!File.Exists)
|
|
||||||
{
|
{
|
||||||
var errorRecord = new ErrorRecord(
|
case "SingleFile":
|
||||||
new FileNotFoundException($"File not found: {File.FullName}"),
|
ProcessSingleFile();
|
||||||
"FileNotFound",
|
break;
|
||||||
ErrorCategory.ObjectNotFound,
|
case "Files":
|
||||||
File);
|
ProcessFiles();
|
||||||
ThrowTerminatingError(errorRecord);
|
break;
|
||||||
}
|
case "Directory":
|
||||||
|
ProcessDirectory();
|
||||||
// Determine object name
|
break;
|
||||||
var finalObjectName = ObjectName ?? File?.Name ?? "unknown";
|
default:
|
||||||
|
throw new InvalidOperationException($"Unknown parameter set: {ParameterSetName}");
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,40 +97,19 @@ namespace PSMinIO.Utils
|
|||||||
private static OrderedDictionary CreateDetailedErrorInfo(Exception exception, string operationName, string? operationDetails)
|
private static OrderedDictionary CreateDetailedErrorInfo(Exception exception, string operationName, string? operationDetails)
|
||||||
{
|
{
|
||||||
var errorDetails = new OrderedDictionary();
|
var errorDetails = new OrderedDictionary();
|
||||||
|
|
||||||
// Basic error information
|
// Basic error information
|
||||||
errorDetails.Add("Operation", operationName);
|
errorDetails.Add("Operation", operationName);
|
||||||
errorDetails.Add("Message", exception.Message);
|
errorDetails.Add("Message", exception.Message);
|
||||||
errorDetails.Add("ExceptionType", exception.GetType().FullName);
|
errorDetails.Add("ExceptionType", exception.GetType().FullName);
|
||||||
|
|
||||||
// Operation details if provided
|
|
||||||
if (!string.IsNullOrEmpty(operationDetails))
|
|
||||||
{
|
|
||||||
errorDetails.Add("OperationDetails", operationDetails);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inner exception information
|
// Inner exception information
|
||||||
if (exception.InnerException != null)
|
if (exception.InnerException != null)
|
||||||
{
|
{
|
||||||
errorDetails.Add("InnerExceptionType", exception.InnerException.GetType().FullName);
|
errorDetails.Add("InnerExceptionType", exception.InnerException.GetType().FullName);
|
||||||
errorDetails.Add("InnerExceptionMessage", exception.InnerException.Message);
|
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;
|
return errorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,10 +123,7 @@ namespace PSMinIO.Utils
|
|||||||
{
|
{
|
||||||
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
|
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
// Log header
|
// Log each error detail (reduced set)
|
||||||
cmdlet.WriteWarning($"{timestamp} - ERROR: PSMinIO Operation Failed");
|
|
||||||
|
|
||||||
// Log each error detail
|
|
||||||
foreach (DictionaryEntry detail in errorDetails)
|
foreach (DictionaryEntry detail in errorDetails)
|
||||||
{
|
{
|
||||||
var key = detail.Key?.ToString() ?? "Unknown";
|
var key = detail.Key?.ToString() ?? "Unknown";
|
||||||
@@ -159,7 +135,7 @@ namespace PSMinIO.Utils
|
|||||||
value = value.Substring(0, 197) + "...";
|
value = value.Substring(0, 197) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdlet.WriteWarning($"{timestamp} - ERROR: {key}: {value}");
|
cmdlet.WriteWarning($"{timestamp} - {key}: {value}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
|
|||||||
Reference in New Issue
Block a user