Files
PSMinIO/src/Cmdlets/NewMinIOObjectCmdlet.cs
T
PSMinIO Developer 357c9a6437 Upgrade to MinIO 5.0.0 and include built module
- Updated MinIO SDK from 3.1.13 to 5.0.0
- Fixed all compilation errors and warnings for MinIO 5.0.0 compatibility
- Updated API calls to use new Args-based patterns (PutObjectArgs, GetObjectArgs, etc.)
- Fixed type compatibility issues (DateTime, ulong conversions)
- Removed deprecated APIs (WithCancellation, ProgressReport)
- Updated System.Text.Json to 9.0.0 to resolve security vulnerabilities
- Added Module directory to repository with built PSMinIO.dll
- Module is now ready for distribution and testing
- All builds pass with 0 errors and 0 warnings
2025-07-10 17:24:40 -04:00

491 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management.Automation;
using PSMinIO.Models;
using PSMinIO.Utils;
namespace PSMinIO.Cmdlets
{
/// <summary>
/// Uploads files or directories to a MinIO bucket
/// </summary>
[Cmdlet(VerbsCommon.New, "MinIOObject", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)]
[OutputType(typeof(MinIOObjectInfo))]
public class NewMinIOObjectCmdlet : MinIOBaseCmdlet
{
/// <summary>
/// Name of the bucket to upload to
/// </summary>
[Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("Bucket")]
public string BucketName { get; set; } = string.Empty;
/// <summary>
/// Array of FileInfo objects to upload
/// </summary>
[Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ParameterSetName = "Files")]
[ValidateNotNull]
[Alias("File", "Files")]
public FileInfo[]? Path { get; set; }
/// <summary>
/// Optional bucket directory path where files should be uploaded (Files parameter set only)
/// </summary>
[Parameter(ParameterSetName = "Files")]
[ValidateNotNullOrEmpty]
[Alias("Folder", "Prefix")]
public string? BucketDirectory { get; set; }
/// <summary>
/// Directory to upload
/// </summary>
[Parameter(Position = 1, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Directory")]
[ValidateNotNull]
[Alias("Dir", "Folder")]
public DirectoryInfo? Directory { get; set; }
/// <summary>
/// Upload directory contents recursively (only applies to Directory parameter set)
/// </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>
/// Overwrite existing objects without prompting
/// </summary>
[Parameter]
public SwitchParameter Force { get; set; }
/// <summary>
/// Generate presigned URLs for uploaded objects
/// </summary>
[Parameter]
public SwitchParameter ShowURL { get; set; }
/// <summary>
/// Expiration time for presigned URLs (default: 1 hour)
/// </summary>
[Parameter]
[ValidateRange("00:01:00", "7.00:00:00")] // 1 minute to 7 days
public TimeSpan Expiration { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// Processes the cmdlet
/// </summary>
protected override void ProcessRecord()
{
ValidateConnection();
ValidateBucketName(BucketName);
if (ParameterSetName == "Files")
{
ProcessFiles();
}
else if (ParameterSetName == "Directory")
{
ProcessDirectory();
}
}
/// <summary>
/// Processes file uploads from FileInfo array
/// </summary>
private void ProcessFiles()
{
if (Path == null || Path.Length == 0)
{
WriteWarning("No files provided for upload");
return;
}
if (ShouldProcess(BucketName, $"Upload {Path.Length} file(s)"))
{
ExecuteOperation("UploadFiles", () =>
{
// Check if bucket exists first
var bucketExists = Client.BucketExists(BucketName);
if (!bucketExists)
{
WriteError(new ErrorRecord(
new InvalidOperationException($"Bucket '{BucketName}' does not exist"),
"BucketNotFound",
ErrorCategory.ObjectNotFound,
BucketName));
return;
}
// Create bucket directory structure if specified
if (!string.IsNullOrWhiteSpace(BucketDirectory))
{
var sanitizedDirectory = SanitizeBucketDirectory(BucketDirectory!);
if (!string.IsNullOrEmpty(sanitizedDirectory))
{
MinIOLogger.WriteVerbose(this, "Ensuring bucket directory exists: {0}", sanitizedDirectory);
EnsureBucketDirectoryExists(sanitizedDirectory);
}
}
UploadFileCollection(Path!);
}, $"Bucket: {BucketName}, Files: {Path.Length}");
}
}
/// <summary>
/// Processes directory upload
/// </summary>
private void ProcessDirectory()
{
if (Directory == null || !Directory.Exists)
{
WriteError(new ErrorRecord(
new DirectoryNotFoundException($"Directory not found: {Directory?.FullName}"),
"DirectoryNotFound",
ErrorCategory.ObjectNotFound,
Directory));
return;
}
if (ShouldProcess(BucketName, $"Upload directory '{Directory.Name}'"))
{
ExecuteOperation("UploadDirectory", () =>
{
// Check if bucket exists first
var bucketExists = Client.BucketExists(BucketName);
if (!bucketExists)
{
WriteError(new ErrorRecord(
new InvalidOperationException($"Bucket '{BucketName}' does not exist"),
"BucketNotFound",
ErrorCategory.ObjectNotFound,
BucketName));
return;
}
// Get files from directory
var files = GetDirectoryFiles();
if (files.Length == 0)
{
WriteWarning($"No files found in directory: {Directory.FullName}");
return;
}
MinIOLogger.WriteVerbose(this, "Found {0} files in directory '{1}'", files.Length, Directory.FullName);
UploadFileCollection(files);
}, $"Bucket: {BucketName}, Directory: {Directory.FullName}");
}
}
/// <summary>
/// Gets files from directory based on filters and recursion settings
/// </summary>
/// <returns>Array of FileInfo objects</returns>
private FileInfo[] GetDirectoryFiles()
{
var searchOption = Recursive.IsPresent ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var allFiles = Directory!.GetFiles("*", searchOption);
// Apply depth filtering if MaxDepth is specified and we're recursive
if (Recursive.IsPresent && MaxDepth > 0)
{
var basePath = Directory.FullName;
allFiles = allFiles.Where(f =>
{
var relativePath = f.FullName.Substring(basePath.Length).TrimStart('\\', '/');
var depth = relativePath.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries).Length - 1;
return depth <= MaxDepth;
}).ToArray();
}
// Apply inclusion filter
if (InclusionFilter != null)
{
allFiles = allFiles.Where(f => EvaluateFilter(InclusionFilter, f)).ToArray();
}
// Apply exclusion filter
if (ExclusionFilter != null)
{
allFiles = allFiles.Where(f => !EvaluateFilter(ExclusionFilter, f)).ToArray();
}
return allFiles;
}
/// <summary>
/// Evaluates a script block filter against a file
/// </summary>
/// <param name="filter">Script block filter</param>
/// <param name="file">File to evaluate</param>
/// <returns>True if file matches filter</returns>
private bool EvaluateFilter(ScriptBlock filter, FileInfo file)
{
try
{
var result = filter.InvokeWithContext(null, new List<PSVariable> { new PSVariable("_", file) });
return result.Count > 0 && LanguagePrimitives.IsTrue(result[0]);
}
catch (Exception ex)
{
WriteWarning($"Filter evaluation failed for {file.Name}: {ex.Message}");
return false;
}
}
/// <summary>
/// Uploads a collection of files
/// </summary>
/// <param name="files">Files to upload</param>
private void UploadFileCollection(FileInfo[] files)
{
// Filter out files that don't exist
var validFiles = files.Where(f => f.Exists).ToArray();
var skippedCount = files.Length - validFiles.Length;
if (skippedCount > 0)
{
WriteWarning($"Skipped {skippedCount} files that do not exist");
}
if (validFiles.Length == 0)
{
WriteWarning("No valid files found for upload");
return;
}
MinIOLogger.WriteVerbose(this, "Uploading {0} files to bucket '{1}'", validFiles.Length, BucketName);
// Calculate total size for overall progress
var totalSize = validFiles.Sum(f => f.Length);
var totalProcessed = 0L;
// Create overall progress reporter
var overallProgress = new ProgressReporter(
this,
"Uploading Files",
$"Uploading {validFiles.Length} files",
totalSize,
1);
var uploadedObjects = new System.Collections.Generic.List<MinIOObjectInfo>();
for (int i = 0; i < validFiles.Length; i++)
{
var fileInfo = validFiles[i];
var objectName = GetObjectName(fileInfo);
try
{
// Show file-level progress
var fileProgressRecord = new ProgressRecord(2, "Current File",
$"Uploading: {fileInfo.Name}")
{
PercentComplete = (int)((double)(i + 1) / validFiles.Length * 100),
ParentActivityId = 1
};
WriteProgress(fileProgressRecord);
MinIOLogger.WriteVerbose(this, "Uploading file {0}/{1}: {2} -> {3}",
i + 1, validFiles.Length, fileInfo.Name, objectName);
// Upload the file
var etag = Client.UploadFile(
BucketName,
objectName,
fileInfo.FullName,
null, // Auto-detect content type
bytesTransferred =>
{
overallProgress.UpdateProgress(totalProcessed + bytesTransferred);
});
totalProcessed += fileInfo.Length;
overallProgress.UpdateProgress(totalProcessed);
// Create object info for result
var uploadedObject = new MinIOObjectInfo(
objectName,
fileInfo.Length,
DateTime.UtcNow,
etag,
BucketName);
// Generate presigned URL if requested
if (ShowURL.IsPresent)
{
try
{
var presignedUrl = Client.GetPresignedUrl(BucketName, objectName, Expiration);
uploadedObject.PresignedUrl = presignedUrl;
uploadedObject.PresignedUrlExpiration = DateTime.UtcNow.Add(Expiration);
}
catch (Exception ex)
{
WriteWarning($"Could not generate presigned URL for {objectName}: {ex.Message}");
}
}
uploadedObjects.Add(uploadedObject);
MinIOLogger.WriteVerbose(this, "Successfully uploaded: {0}", fileInfo.Name);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(
ex,
"FileUploadFailed",
ErrorCategory.WriteError,
fileInfo));
MinIOLogger.WriteVerbose(this, "Failed to upload {0}: {1}", fileInfo.Name, ex.Message);
}
}
// Complete progress reporting
overallProgress.Complete();
WriteProgress(new ProgressRecord(2, "Current File", "Completed")
{
PercentComplete = 100,
RecordType = ProgressRecordType.Completed,
ParentActivityId = 1
});
// Always return uploaded objects
foreach (var obj in uploadedObjects)
{
WriteObject(obj);
}
MinIOLogger.WriteVerbose(this, "Completed uploading {0} files ({1} successful, {2} failed)",
validFiles.Length, uploadedObjects.Count, validFiles.Length - uploadedObjects.Count);
}
/// <summary>
/// Gets the object name for a file based on the upload context
/// </summary>
/// <param name="file">File to get object name for</param>
/// <returns>Object name to use in MinIO</returns>
private string GetObjectName(FileInfo file)
{
if (ParameterSetName == "Files")
{
// For file uploads, optionally prefix with bucket directory
var objectName = file.Name;
if (!string.IsNullOrWhiteSpace(BucketDirectory))
{
var sanitizedDirectory = SanitizeBucketDirectory(BucketDirectory!);
objectName = $"{sanitizedDirectory}/{file.Name}";
}
return objectName;
}
else if (ParameterSetName == "Directory")
{
if (Flatten.IsPresent)
{
// Flatten: use just the filename
return file.Name;
}
else
{
// Maintain directory structure relative to the base directory
var relativePath = file.FullName.Substring(Directory!.FullName.Length).TrimStart('\\', '/');
return relativePath.Replace('\\', '/'); // Ensure forward slashes for object storage
}
}
return file.Name;
}
/// <summary>
/// Sanitizes bucket directory path and ensures proper format
/// </summary>
/// <param name="directory">Raw directory path</param>
/// <returns>Sanitized directory path</returns>
private string SanitizeBucketDirectory(string directory)
{
if (string.IsNullOrWhiteSpace(directory))
return string.Empty;
// Remove leading and trailing slashes, then split and clean
var cleanDirectory = directory.Trim().Trim('/', '\\');
// Split by both forward and back slashes, then clean each part
var parts = cleanDirectory.Split(new char[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
// Clean each part and rejoin with forward slashes
var cleanedParts = parts.Select(part => part.Trim()).Where(part => !string.IsNullOrEmpty(part));
return string.Join("/", cleanedParts);
}
/// <summary>
/// Creates bucket directory structure if it doesn't exist
/// </summary>
/// <param name="directoryPath">Directory path to create</param>
private void EnsureBucketDirectoryExists(string directoryPath)
{
if (string.IsNullOrWhiteSpace(directoryPath))
return;
var parts = directoryPath.Split('/');
var currentPath = string.Empty;
foreach (var part in parts)
{
currentPath = string.IsNullOrEmpty(currentPath) ? part : $"{currentPath}/{part}";
var folderPath = $"{currentPath}/";
try
{
// Check if this directory level already exists by trying to list objects with this prefix
var existingObjects = Client.ListObjects(BucketName, folderPath, false);
var folderExists = existingObjects.Any(obj => obj.Name == folderPath);
if (!folderExists)
{
MinIOLogger.WriteVerbose(this, "Creating bucket directory: {0}", folderPath);
// Create the directory by uploading a zero-byte object
using var emptyStream = new MemoryStream();
Client.UploadStream(BucketName, folderPath, emptyStream, "application/x-directory");
MinIOLogger.WriteVerbose(this, "Successfully created bucket directory: {0}", folderPath);
}
}
catch (Exception ex)
{
WriteWarning($"Could not create bucket directory '{folderPath}': {ex.Message}");
}
}
}
}
}