diff --git a/src/Cmdlets/NewMinIOObjectCmdlet.cs b/src/Cmdlets/NewMinIOObjectCmdlet.cs index b8406fc..c761a9b 100644 --- a/src/Cmdlets/NewMinIOObjectCmdlet.cs +++ b/src/Cmdlets/NewMinIOObjectCmdlet.cs @@ -9,32 +9,88 @@ using PSMinIO.Utils; namespace PSMinIO.Cmdlets { /// - /// Uploads objects to MinIO + /// Uploads files or directories to a MinIO bucket /// [Cmdlet(VerbsCommon.New, "MinIOObject", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)] [OutputType(typeof(MinIOUploadResult))] public class NewMinIOObjectCmdlet : MinIOBaseCmdlet { /// - /// Name of the bucket + /// Name of the bucket to upload to /// - [Parameter(Position = 0, Mandatory = true)] + [Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] + [Alias("Bucket")] public string BucketName { get; set; } = string.Empty; /// - /// Name of the object (if not specified, uses filename) + /// Single file to upload /// - [Parameter(Position = 1)] + [Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ParameterSetName = "SingleFile")] + [ValidateNotNull] + public FileInfo? File { get; set; } + + /// + /// Array of FileInfo objects to upload + /// + [Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ParameterSetName = "Files")] + [ValidateNotNull] + [Alias("Files")] + public FileInfo[]? Path { get; set; } + + /// + /// Directory to upload + /// + [Parameter(Position = 1, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Directory")] + [ValidateNotNull] + [Alias("Dir")] + public DirectoryInfo? Directory { get; set; } + + /// + /// Object name in the bucket (defaults to filename) - SingleFile parameter set only + /// + [Parameter(Position = 2, ParameterSetName = "SingleFile")] [ValidateNotNullOrEmpty] public string? ObjectName { get; set; } /// - /// File to upload + /// Optional bucket directory path where files should be uploaded (Files parameter set only) /// - [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; } + + /// + /// Upload directory contents recursively (Directory parameter set only) + /// + [Parameter(ParameterSetName = "Directory")] + public SwitchParameter Recursive { get; set; } + + /// + /// Maximum depth for recursive directory upload (0 = unlimited) + /// + [Parameter(ParameterSetName = "Directory")] + [ValidateRange(0, int.MaxValue)] + public int MaxDepth { get; set; } = 0; + + /// + /// Flatten directory structure (upload all files to bucket root) + /// + [Parameter(ParameterSetName = "Directory")] + public SwitchParameter Flatten { get; set; } + + /// + /// 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; } /// /// Content type of the file (auto-detected if not specified) @@ -50,12 +106,10 @@ namespace PSMinIO.Cmdlets public Hashtable? Metadata { get; set; } /// - /// Directory prefix in the bucket (creates nested structure) + /// Overwrite existing objects without prompting /// [Parameter] - [ValidateNotNullOrEmpty] - [Alias("Folder", "Directory")] - public string? BucketDirectory { get; set; } + public SwitchParameter Force { get; set; } /// /// 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? metadata = null; - if (Metadata != null && Metadata.Count > 0) - { - metadata = new Dictionary(); - 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}"); } } diff --git a/src/Utils/MinIOErrorHandler.cs b/src/Utils/MinIOErrorHandler.cs index d7817e1..a7af95a 100644 --- a/src/Utils/MinIOErrorHandler.cs +++ b/src/Utils/MinIOErrorHandler.cs @@ -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)