Major update: Enhanced documentation, comprehensive examples, and fixed remaining issues

- Updated README.md with modern feature descriptions and comprehensive overview
- Enhanced docs/USAGE.md with advanced object listing, directory management, and chunked operations
- Created examples/ directory with 6 comprehensive example scripts:
  * 01-Basic-Operations.ps1 - Fundamental operations for beginners
  * 02-Advanced-Object-Listing.ps1 - Filtering, sorting, and pagination
  * 03-Directory-Management.ps1 - Nested directory structures and organization
  * 04-Chunked-Operations.ps1 - Large file handling with performance optimization
  * 05-Bulk-Operations.ps1 - Batch processing and automation workflows
  * 06-Enterprise-Automation.ps1 - Enterprise monitoring, reporting, and compliance
- Added comprehensive examples/README.md with usage patterns and best practices
- Fixed directory creation warnings (now clean verbose logging)
- Implemented missing Get-MinIOObject cmdlet with full filtering/sorting capabilities
- Added timing and performance metrics to all operations
- Enhanced chunked operations with multi-layer progress tracking
- Improved error handling and resource cleanup
- Removed temporary test files and cleaned up repository structure
- All examples use proper PowerShell output (no Write-Host usage)
- Professional logging with timestamps and structured output
This commit is contained in:
PSMinIO Developer
2025-07-10 22:33:03 -04:00
parent 2974ead76e
commit d887fd7f46
66 changed files with 2475 additions and 3428 deletions
+83 -14
View File
@@ -452,6 +452,48 @@ namespace PSMinIO.Utils
}
}
/// <summary>
/// Creates a directory object in MinIO (zero-byte object with trailing slash)
/// </summary>
/// <param name="bucketName">Name of the bucket</param>
/// <param name="directoryPath">Path of the directory (should end with /)</param>
/// <returns>ETag of the created directory object</returns>
public string CreateDirectory(string bucketName, string directoryPath)
{
if (string.IsNullOrWhiteSpace(bucketName))
throw new ArgumentException("Bucket name cannot be null or empty", nameof(bucketName));
if (string.IsNullOrWhiteSpace(directoryPath))
throw new ArgumentException("Directory path cannot be null or empty", nameof(directoryPath));
// Ensure directory path ends with /
if (!directoryPath.EndsWith("/"))
directoryPath += "/";
try
{
// Use a properly configured empty stream
using var emptyStream = new MemoryStream(new byte[0]);
emptyStream.Position = 0;
var args = new PutObjectArgs()
.WithBucket(bucketName)
.WithObject(directoryPath)
.WithStreamData(emptyStream)
.WithObjectSize(0)
.WithContentType("application/x-directory");
Task.Run(async () =>
await _client.PutObjectAsync(args, CancellationToken)).GetAwaiter().GetResult();
return ""; // Directory objects don't have meaningful ETags
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to create directory '{directoryPath}' in bucket '{bucketName}': {ex.Message}", ex);
}
}
/// <summary>
/// Uploads a stream to MinIO synchronously
/// </summary>
@@ -475,6 +517,12 @@ namespace PSMinIO.Utils
try
{
// Ensure stream is at the beginning
if (data.CanSeek)
{
data.Position = 0;
}
var args = new PutObjectArgs()
.WithBucket(bucketName)
.WithObject(objectName)
@@ -586,12 +634,12 @@ namespace PSMinIO.Utils
/// Uploads a file using chunked transfer with resume capability
/// </summary>
/// <param name="transferState">Transfer state for resume functionality</param>
/// <param name="progressReporter">Progress reporter for updates</param>
/// <param name="progressReporter">Thread-safe progress reporter for updates</param>
/// <param name="maxRetries">Maximum retry attempts per chunk</param>
/// <returns>MinIOObjectInfo of uploaded object or null if failed</returns>
public MinIOObjectInfo? UploadFileChunked(
ChunkedTransferState transferState,
ChunkedCollectionProgressReporter progressReporter,
ThreadSafeChunkedProgressReporter progressReporter,
int maxRetries = 3)
{
if (transferState == null)
@@ -705,13 +753,13 @@ namespace PSMinIO.Utils
/// Downloads a file using chunked transfer with resume capability
/// </summary>
/// <param name="transferState">Transfer state for resume functionality</param>
/// <param name="progressReporter">Progress reporter for updates</param>
/// <param name="progressReporter">Thread-safe progress reporter for updates</param>
/// <param name="maxRetries">Maximum retry attempts per chunk</param>
/// <param name="parallelDownloads">Number of parallel chunk downloads</param>
/// <returns>True if download succeeded, false otherwise</returns>
public bool DownloadFileChunked(
ChunkedTransferState transferState,
ChunkedSingleFileProgressReporter progressReporter,
ThreadSafeChunkedProgressReporter progressReporter,
int maxRetries = 3,
int parallelDownloads = 3)
{
@@ -724,9 +772,11 @@ namespace PSMinIO.Utils
using var fileStream = new FileStream(transferState.FilePath, FileMode.Create, FileAccess.Write, FileShare.None);
fileStream.SetLength(transferState.TotalSize);
// Get list of chunks to download
// Get list of chunks to download (with safety limit)
var chunksToDownload = new List<ChunkInfo>();
while (!transferState.IsComplete)
var maxChunks = Math.Min(transferState.TotalChunks, 10000); // Safety limit of 10,000 chunks
for (int i = 0; i < maxChunks && !transferState.IsComplete; i++)
{
var nextChunk = transferState.GetNextChunk();
if (nextChunk == null)
@@ -771,7 +821,7 @@ namespace PSMinIO.Utils
/// <param name="transferState">Transfer state</param>
/// <param name="chunk">Chunk to download</param>
/// <param name="fileStream">Target file stream</param>
/// <param name="progressReporter">Progress reporter</param>
/// <param name="progressReporter">Thread-safe progress reporter</param>
/// <param name="maxRetries">Maximum retry attempts</param>
/// <param name="semaphore">Semaphore for controlling parallelism</param>
/// <returns>True if chunk downloaded successfully</returns>
@@ -779,7 +829,7 @@ namespace PSMinIO.Utils
ChunkedTransferState transferState,
ChunkInfo chunk,
FileStream fileStream,
ChunkedSingleFileProgressReporter progressReporter,
ThreadSafeChunkedProgressReporter progressReporter,
int maxRetries,
SemaphoreSlim semaphore)
{
@@ -795,17 +845,36 @@ namespace PSMinIO.Utils
{
using var chunkStream = new MemoryStream();
// Use GetObjectAsync with offset and length for proper chunked download
var getArgs = new GetObjectArgs()
.WithBucket(transferState.BucketName)
.WithObject(transferState.ObjectName)
.WithCallbackStream((stream) =>
{
// For MinIO 5.0.0, we'll need to implement range requests differently
// For now, let's use the basic GetObject and handle chunking at the stream level
var buffer = new byte[chunk.Size];
stream.Seek(chunk.StartByte, SeekOrigin.Begin);
var bytesRead = stream.Read(buffer, 0, (int)chunk.Size);
chunkStream.Write(buffer, 0, bytesRead);
// Skip to the start position and read only the chunk size
var buffer = new byte[8192]; // 8KB buffer
long totalRead = 0;
long skipBytes = chunk.StartByte;
// Skip to start position
while (skipBytes > 0)
{
var toSkip = (int)Math.Min(skipBytes, buffer.Length);
var skipped = stream.Read(buffer, 0, toSkip);
if (skipped == 0) break; // End of stream
skipBytes -= skipped;
}
// Read the chunk data
while (totalRead < chunk.Size)
{
var toRead = (int)Math.Min(chunk.Size - totalRead, buffer.Length);
var bytesRead = stream.Read(buffer, 0, toRead);
if (bytesRead == 0) break; // End of stream
chunkStream.Write(buffer, 0, bytesRead);
totalRead += bytesRead;
}
});
await _client.GetObjectAsync(getArgs, CancellationToken);
@@ -0,0 +1,163 @@
using System;
using System.Management.Automation;
using System.Threading;
namespace PSMinIO.Utils
{
/// <summary>
/// Thread-safe wrapper for chunked progress reporting that can be safely called from background threads
/// </summary>
public class ThreadSafeChunkedProgressReporter
{
private readonly ThreadSafeProgressCollector _progressCollector;
private readonly string _operationName;
private readonly long _totalSize;
private readonly int _totalChunks;
private readonly DateTime _startTime;
// Activity IDs for progress hierarchy
private const int FileActivityId = 1;
private const int ChunkActivityId = 2;
// Current state (using Interlocked for long values since volatile doesn't support long)
private volatile int _currentChunk = 0;
private long _currentChunkSize = 0;
private long _totalBytesTransferred = 0;
private volatile string _currentFileName = string.Empty;
public ThreadSafeChunkedProgressReporter(
PSCmdlet cmdlet,
long totalSize,
int totalChunks,
string operationName)
{
_progressCollector = new ThreadSafeProgressCollector(cmdlet);
_totalSize = totalSize;
_totalChunks = totalChunks;
_operationName = operationName;
_startTime = DateTime.UtcNow;
}
/// <summary>
/// Starts a new file operation (thread-safe)
/// </summary>
public void StartNewFile(string fileName, long fileSize, int totalChunks)
{
_currentFileName = fileName;
Interlocked.Exchange(ref _totalBytesTransferred, 0);
_currentChunk = 0;
_progressCollector.QueueVerboseMessage("Starting {0} of file: {1} ({2})",
_operationName.ToLower(), fileName, SizeFormatter.FormatBytes(fileSize));
_progressCollector.QueueProgressUpdate(
FileActivityId,
$"{_operationName} File",
$"{_operationName}: {fileName}",
0);
}
/// <summary>
/// Starts a new chunk operation (thread-safe)
/// </summary>
public void StartNewChunk(int chunkNumber, long chunkSize)
{
_currentChunk = chunkNumber;
Interlocked.Exchange(ref _currentChunkSize, chunkSize);
_progressCollector.QueueVerboseMessage("File {0}: Starting chunk {1}/{2} ({3})",
_currentFileName, chunkNumber, _totalChunks, SizeFormatter.FormatBytes(chunkSize));
_progressCollector.QueueProgressUpdate(
ChunkActivityId,
"Current Chunk",
$"Chunk {chunkNumber}/{_totalChunks}",
0,
FileActivityId);
}
/// <summary>
/// Updates chunk progress (thread-safe)
/// </summary>
public void UpdateChunkProgress(long bytesTransferred)
{
var newTotal = Interlocked.Add(ref _totalBytesTransferred, bytesTransferred);
var currentChunkSize = Interlocked.Read(ref _currentChunkSize);
var chunkPercent = currentChunkSize > 0 ?
(int)((double)bytesTransferred / currentChunkSize * 100) : 100;
var filePercent = _totalSize > 0 ?
(int)((double)newTotal / _totalSize * 100) : 100;
// Update chunk progress
_progressCollector.QueueProgressUpdate(
ChunkActivityId,
"Current Chunk",
$"Chunk {_currentChunk}/{_totalChunks} - {SizeFormatter.FormatBytes(bytesTransferred)}/{SizeFormatter.FormatBytes(_currentChunkSize)}",
Math.Min(chunkPercent, 100),
FileActivityId);
// Update file progress
_progressCollector.QueueProgressUpdate(
FileActivityId,
$"{_operationName} File",
$"{_operationName}: {_currentFileName} - {SizeFormatter.FormatBytes(newTotal)}/{SizeFormatter.FormatBytes(_totalSize)}",
Math.Min(filePercent, 100));
}
/// <summary>
/// Completes the current chunk (thread-safe)
/// </summary>
public void CompleteChunk(string? chunkETag = null)
{
_progressCollector.QueueVerboseMessage("File {0}: Completed chunk {1}/{2}{3}",
_currentFileName, _currentChunk, _totalChunks,
!string.IsNullOrEmpty(chunkETag) ? $" - ETag: {chunkETag}" : "");
_progressCollector.QueueProgressCompletion(ChunkActivityId, "Current Chunk", FileActivityId);
}
/// <summary>
/// Completes the current file (thread-safe)
/// </summary>
public void CompleteFile()
{
var elapsed = DateTime.UtcNow - _startTime;
_progressCollector.QueueVerboseMessage("File {0}: {1} completed in {2} - Total size: {3}",
_currentFileName, _operationName.ToLower(), elapsed.ToString(@"hh\:mm\:ss"),
SizeFormatter.FormatBytes(_totalSize));
_progressCollector.QueueProgressCompletion(FileActivityId, $"{_operationName} File");
}
/// <summary>
/// Reports a chunk error (thread-safe)
/// </summary>
public void ReportChunkError(Exception error, int retryAttempt, int maxRetries)
{
_progressCollector.QueueVerboseMessage("File {0}: Chunk {1}/{2} failed (attempt {3}/{4}): {5}",
_currentFileName, _currentChunk, _totalChunks, retryAttempt, maxRetries, error.Message);
}
/// <summary>
/// Processes all queued updates from the main thread (must be called from main thread)
/// </summary>
public void ProcessQueuedUpdates()
{
_progressCollector.ProcessQueuedUpdates();
}
/// <summary>
/// Completes all operations and processes final updates (must be called from main thread)
/// </summary>
public void Complete()
{
_progressCollector.Complete();
}
/// <summary>
/// Gets the number of pending updates
/// </summary>
public int PendingUpdates => _progressCollector.PendingProgressUpdates + _progressCollector.PendingVerboseMessages;
}
}
+156
View File
@@ -0,0 +1,156 @@
using System;
using System.Collections.Concurrent;
using System.Management.Automation;
using System.Threading;
namespace PSMinIO.Utils
{
/// <summary>
/// Thread-safe progress data collector that accumulates progress updates from background threads
/// and allows the main thread to safely report them to PowerShell
/// </summary>
public class ThreadSafeProgressCollector
{
private readonly PSCmdlet _cmdlet;
private readonly ConcurrentQueue<ProgressUpdate> _progressQueue = new();
private readonly ConcurrentQueue<VerboseMessage> _verboseQueue = new();
private readonly object _lockObject = new();
private volatile bool _isCompleted = false;
public ThreadSafeProgressCollector(PSCmdlet cmdlet)
{
_cmdlet = cmdlet ?? throw new ArgumentNullException(nameof(cmdlet));
}
/// <summary>
/// Queues a progress update from a background thread
/// </summary>
public void QueueProgressUpdate(int activityId, string activity, string statusDescription, int percentComplete, int parentActivityId = -1)
{
if (_isCompleted) return;
_progressQueue.Enqueue(new ProgressUpdate
{
ActivityId = activityId,
Activity = activity,
StatusDescription = statusDescription,
PercentComplete = percentComplete,
ParentActivityId = parentActivityId,
Timestamp = DateTime.UtcNow
});
}
/// <summary>
/// Queues a progress completion from a background thread
/// </summary>
public void QueueProgressCompletion(int activityId, string activity, int parentActivityId = -1)
{
if (_isCompleted) return;
_progressQueue.Enqueue(new ProgressUpdate
{
ActivityId = activityId,
Activity = activity,
StatusDescription = "Completed",
PercentComplete = 100,
ParentActivityId = parentActivityId,
IsCompleted = true,
Timestamp = DateTime.UtcNow
});
}
/// <summary>
/// Queues a verbose message from a background thread
/// </summary>
public void QueueVerboseMessage(string message, params object[] args)
{
if (_isCompleted) return;
_verboseQueue.Enqueue(new VerboseMessage
{
Message = args.Length > 0 ? string.Format(message, args) : message,
Timestamp = DateTime.UtcNow
});
}
/// <summary>
/// Processes all queued updates from the main thread (safe to call PowerShell methods)
/// </summary>
public void ProcessQueuedUpdates()
{
// Process verbose messages first
while (_verboseQueue.TryDequeue(out var verboseMessage))
{
MinIOLogger.WriteVerbose(_cmdlet, verboseMessage.Message);
}
// Process progress updates
while (_progressQueue.TryDequeue(out var progressUpdate))
{
var progressRecord = new ProgressRecord(
progressUpdate.ActivityId,
progressUpdate.Activity,
progressUpdate.StatusDescription)
{
PercentComplete = progressUpdate.PercentComplete
};
if (progressUpdate.ParentActivityId >= 0)
{
progressRecord.ParentActivityId = progressUpdate.ParentActivityId;
}
if (progressUpdate.IsCompleted)
{
progressRecord.RecordType = ProgressRecordType.Completed;
}
_cmdlet.WriteProgress(progressRecord);
}
}
/// <summary>
/// Marks the collector as completed (no more updates will be accepted)
/// </summary>
public void Complete()
{
_isCompleted = true;
// Process any remaining updates
ProcessQueuedUpdates();
}
/// <summary>
/// Gets the number of pending progress updates
/// </summary>
public int PendingProgressUpdates => _progressQueue.Count;
/// <summary>
/// Gets the number of pending verbose messages
/// </summary>
public int PendingVerboseMessages => _verboseQueue.Count;
/// <summary>
/// Progress update data structure
/// </summary>
private class ProgressUpdate
{
public int ActivityId { get; set; }
public string Activity { get; set; } = string.Empty;
public string StatusDescription { get; set; } = string.Empty;
public int PercentComplete { get; set; }
public int ParentActivityId { get; set; } = -1;
public bool IsCompleted { get; set; }
public DateTime Timestamp { get; set; }
}
/// <summary>
/// Verbose message data structure
/// </summary>
private class VerboseMessage
{
public string Message { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
}
}
}
+149
View File
@@ -0,0 +1,149 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Management.Automation;
using PSMinIO.Models;
namespace PSMinIO.Utils
{
/// <summary>
/// Thread-safe result collector that accumulates results from background threads
/// and allows the main thread to safely output them via WriteObject
/// </summary>
public class ThreadSafeResultCollector
{
private readonly PSCmdlet _cmdlet;
private readonly ConcurrentQueue<object> _resultQueue = new();
private readonly ConcurrentQueue<ErrorRecord> _errorQueue = new();
private readonly object _lockObject = new();
private volatile bool _isCompleted = false;
public ThreadSafeResultCollector(PSCmdlet cmdlet)
{
_cmdlet = cmdlet ?? throw new ArgumentNullException(nameof(cmdlet));
}
/// <summary>
/// Queues a successful result from a background thread
/// </summary>
public void QueueResult(object result)
{
if (_isCompleted || result == null) return;
_resultQueue.Enqueue(result);
}
/// <summary>
/// Queues an error from a background thread
/// </summary>
public void QueueError(ErrorRecord error)
{
if (_isCompleted || error == null) return;
_errorQueue.Enqueue(error);
}
/// <summary>
/// Queues an error from a background thread using exception details
/// </summary>
public void QueueError(Exception exception, string errorId, ErrorCategory category, object targetObject)
{
if (_isCompleted || exception == null) return;
var errorRecord = new ErrorRecord(exception, errorId, category, targetObject);
_errorQueue.Enqueue(errorRecord);
}
/// <summary>
/// Processes all queued results and errors from the main thread (safe to call PowerShell methods)
/// </summary>
public void ProcessQueuedResults()
{
// Process errors first
while (_errorQueue.TryDequeue(out var error))
{
_cmdlet.WriteError(error);
}
// Process successful results
while (_resultQueue.TryDequeue(out var result))
{
_cmdlet.WriteObject(result);
}
}
/// <summary>
/// Marks the collector as completed and processes all remaining results
/// </summary>
public void Complete()
{
_isCompleted = true;
// Process any remaining results
ProcessQueuedResults();
}
/// <summary>
/// Gets all queued results without processing them (for inspection)
/// </summary>
public List<object> GetQueuedResults()
{
var results = new List<object>();
var tempQueue = new Queue<object>();
// Dequeue all items and re-queue them
while (_resultQueue.TryDequeue(out var result))
{
results.Add(result);
tempQueue.Enqueue(result);
}
// Re-queue the items
while (tempQueue.Count > 0)
{
_resultQueue.Enqueue(tempQueue.Dequeue());
}
return results;
}
/// <summary>
/// Gets all queued errors without processing them (for inspection)
/// </summary>
public List<ErrorRecord> GetQueuedErrors()
{
var errors = new List<ErrorRecord>();
var tempQueue = new Queue<ErrorRecord>();
// Dequeue all items and re-queue them
while (_errorQueue.TryDequeue(out var error))
{
errors.Add(error);
tempQueue.Enqueue(error);
}
// Re-queue the items
while (tempQueue.Count > 0)
{
_errorQueue.Enqueue(tempQueue.Dequeue());
}
return errors;
}
/// <summary>
/// Gets the number of pending results
/// </summary>
public int PendingResults => _resultQueue.Count;
/// <summary>
/// Gets the number of pending errors
/// </summary>
public int PendingErrors => _errorQueue.Count;
/// <summary>
/// Gets whether the collector has any pending items
/// </summary>
public bool HasPendingItems => PendingResults > 0 || PendingErrors > 0;
}
}