using System; using System.Management.Automation; using PSMinIO.Utils; namespace PSMinIO.Utils { /// /// Manages 3-layer progress reporting for chunked file collection operations /// Layer 1: Collection Progress (overall files) /// Layer 2: Current File Progress /// Layer 3: Current Chunk Progress /// public class ChunkedCollectionProgressReporter { private readonly PSCmdlet _cmdlet; private readonly int _totalFiles; private readonly long _totalSize; private readonly DateTime _startTime; private readonly string _operationName; // Progress tracking private int _completedFiles = 0; private long _totalBytesTransferred = 0; private string _currentFileName = ""; private long _currentFileSize = 0; private long _currentFileBytesTransferred = 0; private int _currentChunk = 0; private int _totalChunks = 0; private long _currentChunkBytesTransferred = 0; private long _currentChunkSize = 0; // Activity IDs for progress hierarchy private const int CollectionActivityId = 1; private const int FileActivityId = 2; private const int ChunkActivityId = 3; // Progress control private readonly long _progressUpdateInterval; private long _lastProgressUpdate = 0; /// /// Creates a new chunked collection progress reporter /// /// PowerShell cmdlet for progress reporting /// Total number of files to process /// Total size of all files /// Name of the operation (e.g., "Uploading", "Downloading") /// Update progress every N bytes public ChunkedCollectionProgressReporter( PSCmdlet cmdlet, int totalFiles, long totalSize, string operationName = "Processing", long progressUpdateInterval = 1024 * 1024) // 1MB default { _cmdlet = cmdlet; _totalFiles = totalFiles; _totalSize = totalSize; _operationName = operationName; _startTime = DateTime.Now; _progressUpdateInterval = progressUpdateInterval; } /// /// Starts processing a new file /// /// Name of the file being processed /// Size of the file /// Total number of chunks for this file public void StartNewFile(string fileName, long fileSize, int totalChunks) { _currentFileName = fileName; _currentFileSize = fileSize; _currentFileBytesTransferred = 0; _totalChunks = totalChunks; _currentChunk = 0; MinIOLogger.WriteVerbose(_cmdlet, "Starting {0} of file {1}/{2}: {3} ({4})", _operationName.ToLower(), _completedFiles + 1, _totalFiles, fileName, SizeFormatter.FormatSize(fileSize)); UpdateAllProgress(); } /// /// Starts processing a new chunk /// /// Chunk number (1-based for display) /// Size of the chunk public void StartNewChunk(int chunkNumber, long chunkSize) { _currentChunk = chunkNumber; _currentChunkSize = chunkSize; _currentChunkBytesTransferred = 0; MinIOLogger.WriteVerbose(_cmdlet, "File {0}: Starting chunk {1}/{2} ({3})", _currentFileName, chunkNumber, _totalChunks, SizeFormatter.FormatSize(chunkSize)); UpdateAllProgress(); } /// /// Updates chunk progress /// /// Bytes transferred for current chunk public void UpdateChunkProgress(long bytesTransferred) { var chunkDelta = bytesTransferred - _currentChunkBytesTransferred; _currentChunkBytesTransferred = bytesTransferred; _currentFileBytesTransferred += chunkDelta; _totalBytesTransferred += chunkDelta; // Only update progress if we've transferred enough bytes since last update if (_totalBytesTransferred - _lastProgressUpdate >= _progressUpdateInterval) { UpdateAllProgress(); _lastProgressUpdate = _totalBytesTransferred; } } /// /// Completes the current chunk /// /// ETag of the completed chunk (optional) public void CompleteChunk(string? chunkETag = null) { MinIOLogger.WriteVerbose(_cmdlet, "File {0}: Completed chunk {1}/{2}{3}", _currentFileName, _currentChunk, _totalChunks, !string.IsNullOrEmpty(chunkETag) ? $" - ETag: {chunkETag}" : ""); // Mark chunk as completed (only if progress is enabled) if (_cmdlet.MyInvocation.BoundParameters.ContainsKey("ProgressAction") && _cmdlet.MyInvocation.BoundParameters["ProgressAction"].ToString() == "SilentlyContinue") return; var chunkProgress = new ProgressRecord(ChunkActivityId, "Current Chunk", "Completed") { PercentComplete = 100, RecordType = ProgressRecordType.Completed, ParentActivityId = FileActivityId }; _cmdlet.WriteProgress(chunkProgress); } /// /// Completes the current file /// public void CompleteFile() { _completedFiles++; var elapsed = DateTime.Now - _startTime; MinIOLogger.WriteVerbose(_cmdlet, "File {0}: {1} completed in {2} - Total size: {3}", _currentFileName, _operationName.ToLower(), elapsed.ToString(@"hh\:mm\:ss"), SizeFormatter.FormatSize(_currentFileSize)); // Mark file as completed (only if progress is enabled) if (_cmdlet.MyInvocation.BoundParameters.ContainsKey("ProgressAction") && _cmdlet.MyInvocation.BoundParameters["ProgressAction"].ToString() == "SilentlyContinue") return; var fileProgress = new ProgressRecord(FileActivityId, "Current File", "Completed") { PercentComplete = 100, RecordType = ProgressRecordType.Completed, ParentActivityId = CollectionActivityId }; _cmdlet.WriteProgress(fileProgress); // Complete chunk progress too CompleteChunk(); } /// /// Completes the entire collection operation /// public void CompleteCollection() { var elapsed = DateTime.Now - _startTime; MinIOLogger.WriteVerbose(_cmdlet, "Completed {0} {1} files ({2}) in {3}", _operationName.ToLower(), _totalFiles, SizeFormatter.FormatSize(_totalSize), elapsed.ToString(@"hh\:mm\:ss")); // Complete all progress records var collectionProgress = new ProgressRecord(CollectionActivityId, $"{_operationName} Files", "Completed") { PercentComplete = 100, RecordType = ProgressRecordType.Completed }; _cmdlet.WriteProgress(collectionProgress); // Complete file progress if enabled if (!(_cmdlet.MyInvocation.BoundParameters.ContainsKey("ProgressAction") && _cmdlet.MyInvocation.BoundParameters["ProgressAction"].ToString() == "SilentlyContinue")) { CompleteFile(); } } /// /// Reports an error for the current chunk /// /// Error that occurred /// Current retry attempt /// Maximum retry attempts public void ReportChunkError(Exception error, int retryAttempt, int maxRetries) { MinIOLogger.WriteVerbose(_cmdlet, "File {0}: Chunk {1}/{2} failed (attempt {3}/{4}): {5}", _currentFileName, _currentChunk, _totalChunks, retryAttempt, maxRetries, error.Message); } /// /// Updates all progress layers /// private void UpdateAllProgress() { var elapsed = DateTime.Now - _startTime; var speed = elapsed.TotalSeconds > 0 ? _totalBytesTransferred / elapsed.TotalSeconds : 0; // Layer 1: Collection Progress (always shown) var collectionPercent = _totalSize > 0 ? (int)((_totalBytesTransferred * 100) / _totalSize) : 0; var collectionStatus = $"Files: {_completedFiles}/{_totalFiles} | " + $"Size: {SizeFormatter.FormatSize(_totalBytesTransferred)}/{SizeFormatter.FormatSize(_totalSize)} | " + $"Speed: {SizeFormatter.FormatSize((long)speed)}/s | " + $"Elapsed: {elapsed:hh\\:mm\\:ss}"; var collectionProgress = new ProgressRecord(CollectionActivityId, $"{_operationName} Files", collectionStatus) { PercentComplete = collectionPercent }; _cmdlet.WriteProgress(collectionProgress); // Check if progress is disabled if (_cmdlet.MyInvocation.BoundParameters.ContainsKey("ProgressAction") && _cmdlet.MyInvocation.BoundParameters["ProgressAction"].ToString() == "SilentlyContinue") return; // Layer 2: Current File Progress if (!string.IsNullOrEmpty(_currentFileName)) { var filePercent = _currentFileSize > 0 ? (int)((_currentFileBytesTransferred * 100) / _currentFileSize) : 0; var fileStatus = $"File: {_currentFileName} | " + $"Size: {SizeFormatter.FormatSize(_currentFileBytesTransferred)}/{SizeFormatter.FormatSize(_currentFileSize)}"; var fileProgress = new ProgressRecord(FileActivityId, "Current File", fileStatus) { PercentComplete = filePercent, ParentActivityId = CollectionActivityId }; _cmdlet.WriteProgress(fileProgress); } // Layer 3: Current Chunk Progress if (_currentChunk > 0) { var chunkPercent = _currentChunkSize > 0 ? (int)((_currentChunkBytesTransferred * 100) / _currentChunkSize) : 0; var chunkStatus = $"Chunk: {_currentChunk}/{_totalChunks} | " + $"Size: {SizeFormatter.FormatSize(_currentChunkBytesTransferred)}/{SizeFormatter.FormatSize(_currentChunkSize)} | " + $"Speed: {SizeFormatter.FormatSize((long)speed)}/s"; var chunkProgress = new ProgressRecord(ChunkActivityId, "Current Chunk", chunkStatus) { PercentComplete = chunkPercent, ParentActivityId = FileActivityId }; _cmdlet.WriteProgress(chunkProgress); } } } }