diff --git a/src/ModularPipelines/FileSystem/File.cs b/src/ModularPipelines/FileSystem/File.cs index 8ca2a2c5ab..ffade0eb56 100644 --- a/src/ModularPipelines/FileSystem/File.cs +++ b/src/ModularPipelines/FileSystem/File.cs @@ -1,7 +1,9 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using ModularPipelines.Logging; +using ModularPipelines.Tracing; namespace ModularPipelines.FileSystem; @@ -35,7 +37,7 @@ internal File(FileInfo fileInfo) /// > public Task ReadAsync(CancellationToken cancellationToken = default) { - ModuleLogger.Current.LogInformation("Reading File: {Path}", this); + LogFileOperation("Reading File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); return System.IO.File.ReadAllTextAsync(Path, cancellationToken); } @@ -43,14 +45,14 @@ public Task ReadAsync(CancellationToken cancellationToken = default) /// public IAsyncEnumerable ReadLinesAsync(CancellationToken cancellationToken = default) { - ModuleLogger.Current.LogInformation("Reading File: {Path}", this); + LogFileOperation("Reading File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); return System.IO.File.ReadLinesAsync(Path, cancellationToken); } public Task ReadBytesAsync(CancellationToken cancellationToken = default) { - ModuleLogger.Current.LogInformation("Reading File: {Path}", this); + LogFileOperation("Reading File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); return System.IO.File.ReadAllBytesAsync(Path, cancellationToken); } @@ -62,28 +64,28 @@ public FileStream GetStream(FileAccess fileAccess = FileAccess.ReadWrite) public Task WriteAsync(string contents, CancellationToken cancellationToken = default) { - ModuleLogger.Current.LogInformation("Writing to File: {Path}", this); + LogFileOperation("Writing to File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); return System.IO.File.WriteAllTextAsync(Path, contents, cancellationToken); } public Task WriteAsync(byte[] contents, CancellationToken cancellationToken = default) { - ModuleLogger.Current.LogInformation("Writing to File: {Path}", this); + LogFileOperation("Writing to File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); return System.IO.File.WriteAllBytesAsync(Path, contents, cancellationToken); } public Task WriteAsync(IEnumerable contents, CancellationToken cancellationToken = default) { - ModuleLogger.Current.LogInformation("Writing to File: {Path}", this); + LogFileOperation("Writing to File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); return System.IO.File.WriteAllLinesAsync(Path, contents, cancellationToken); } public async Task WriteAsync(ReadOnlyMemory contents, CancellationToken cancellationToken = default) { - ModuleLogger.Current.LogInformation("Writing to File: {Path}", this); + LogFileOperation("Writing to File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); var fileStream = System.IO.File.Create(Path); await using (fileStream.ConfigureAwait(false)) @@ -94,7 +96,7 @@ public async Task WriteAsync(ReadOnlyMemory contents, CancellationToken ca public async Task WriteAsync(Stream contents, CancellationToken cancellationToken = default) { - ModuleLogger.Current.LogInformation("Writing to File: {Path}", this); + LogFileOperation("Writing to File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); var fileStream = System.IO.File.Create(Path); await using (fileStream.ConfigureAwait(false)) @@ -110,14 +112,14 @@ public async Task WriteAsync(Stream contents, CancellationToken cancellationToke public Task AppendAsync(string contents, CancellationToken cancellationToken = default) { - ModuleLogger.Current.LogInformation("Writing to File: {Path}", this); + LogFileOperation("Appending to File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); return System.IO.File.AppendAllTextAsync(Path, contents, cancellationToken); } public Task AppendAsync(IEnumerable contents, CancellationToken cancellationToken = default) { - ModuleLogger.Current.LogInformation("Writing to File: {Path}", this); + LogFileOperation("Appending to File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); return System.IO.File.AppendAllLinesAsync(Path, contents, cancellationToken); } @@ -143,7 +145,7 @@ public Task AppendAsync(IEnumerable contents, CancellationToken cancella public File Create() { - ModuleLogger.Current.LogInformation("Creating File: {Path}", this); + LogFileOperation("Creating File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); var fileStream = System.IO.File.Create(Path); fileStream.Dispose(); @@ -174,7 +176,7 @@ public FileAttributes Attributes /// > public void Delete() { - ModuleLogger.Current.LogInformation("Deleting File: {File}", this); + LogFileOperation("Deleting File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); FileInfo.Delete(); } @@ -182,7 +184,7 @@ public void Delete() /// > public File MoveTo(string path) { - ModuleLogger.Current.LogInformation("Moving File: {Source} > {Destination}", this, path); + LogFileOperationWithDestination("Moving File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path); FileInfo.MoveTo(path); return this; @@ -191,7 +193,7 @@ public File MoveTo(string path) /// > public File MoveTo(Folder folder) { - ModuleLogger.Current.LogInformation("Moving File: {Source} > {Destination}", this, folder); + LogFileOperationWithDestination("Moving File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, folder); folder.Create(); return MoveTo(System.IO.Path.Combine(folder.Path, Name)); @@ -200,14 +202,14 @@ public File MoveTo(Folder folder) /// > public File CopyTo(string path) { - ModuleLogger.Current.LogInformation("Copying File: {Source} > {Destination}", this, path); + LogFileOperationWithDestination("Copying File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path); return FileInfo.CopyTo(path); } public File CopyTo(Folder folder) { - ModuleLogger.Current.LogInformation("Copying File: {Source} > {Destination}", this, folder); + LogFileOperationWithDestination("Copying File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, folder); folder.Create(); return CopyTo(System.IO.Path.Combine(folder.Path, Name)); @@ -217,7 +219,7 @@ public static File GetNewTemporaryFilePath() { var path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName()); - ModuleLogger.Current.LogInformation("Temporary File Path: {Path}", path); + LogFileOperation("Temporary File Path: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", path); return path!; } @@ -301,4 +303,30 @@ public override int GetHashCode() { return !Equals(left, right); } -} \ No newline at end of file + + /// + /// Logs a file operation with Activity context information. + /// + /// + /// Phase 2: Uses Activity.Current for context alongside AsyncLocal for backward compatibility. + /// The log message includes the current module name and activity ID when available. + /// + private static void LogFileOperation(string messageTemplate, object? arg1) + { + var moduleName = ModuleActivityTracing.GetCurrentModuleName() ?? "Unknown"; + var activityId = ModuleActivityTracing.GetCurrentActivityId(); + + ModuleLogger.Current.LogInformation(messageTemplate, arg1, moduleName, activityId); + } + + /// + /// Logs a file operation with Activity context information for operations with source and destination. + /// + private static void LogFileOperationWithDestination(string messageTemplate, object? source, object? destination) + { + var moduleName = ModuleActivityTracing.GetCurrentModuleName() ?? "Unknown"; + var activityId = ModuleActivityTracing.GetCurrentActivityId(); + + ModuleLogger.Current.LogInformation(messageTemplate, source, destination, moduleName, activityId); + } +} diff --git a/src/ModularPipelines/FileSystem/Folder.cs b/src/ModularPipelines/FileSystem/Folder.cs index 5c1dd201b0..4e3f290fb8 100644 --- a/src/ModularPipelines/FileSystem/Folder.cs +++ b/src/ModularPipelines/FileSystem/Folder.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; @@ -6,6 +7,7 @@ using Microsoft.Extensions.Logging; using ModularPipelines.JsonUtils; using ModularPipelines.Logging; +using ModularPipelines.Tracing; namespace ModularPipelines.FileSystem; @@ -77,7 +79,7 @@ public Folder Root public Folder Create() { - ModuleLogger.Current.LogInformation("Creating Folder: {Path}", this); + LogFolderOperation("Creating Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); Directory.CreateDirectory(Path); return this; @@ -85,52 +87,133 @@ public Folder Create() public void Delete() { - ModuleLogger.Current.LogInformation("Deleting Folder: {Path}", this); + LogFolderOperation("Deleting Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); DirectoryInfo.Delete(true); } + /// + /// Removes all files and subdirectories within the folder. + /// public void Clean() { - ModuleLogger.Current.LogInformation("Cleaning Folder: {Path}", this); + Clean(removeReadOnlyAttribute: true); + } + + /// + /// Removes all files and subdirectories within the folder. + /// + /// + /// When true, removes the read-only attribute from files and directories before deletion. + /// This helps handle read-only items that would otherwise fail to delete. + /// + public void Clean(bool removeReadOnlyAttribute) + { + LogFolderOperation("Cleaning Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); foreach (var directory in DirectoryInfo.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) { + if (removeReadOnlyAttribute) + { + RemoveReadOnlyAttributeRecursively(directory); + } + directory.Delete(true); } foreach (var file in DirectoryInfo.EnumerateFiles("*", SearchOption.TopDirectoryOnly)) { + if (removeReadOnlyAttribute && (file.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + file.Attributes &= ~FileAttributes.ReadOnly; + } + file.Delete(); } } + /// + /// Copies the folder and its contents to the specified target path. + /// + /// The destination path for the copied folder. + /// A new instance representing the copied folder. public Folder CopyTo(string targetPath) + { + return CopyTo(targetPath, preserveTimestamps: false); + } + + /// + /// Copies the folder and its contents to the specified target path. + /// + /// The destination path for the copied folder. + /// + /// When true, preserves CreationTimeUtc, LastWriteTimeUtc, and LastAccessTimeUtc + /// for all files and directories. + /// + /// A new instance representing the copied folder. + public Folder CopyTo(string targetPath, bool preserveTimestamps) { Directory.CreateDirectory(targetPath); + // Copy all subdirectories first foreach (var dirPath in Directory.EnumerateDirectories(this, "*", SearchOption.AllDirectories)) { + var sourceDir = new DirectoryInfo(dirPath); var relativePath = System.IO.Path.GetRelativePath(this, dirPath); var newPath = System.IO.Path.Combine(targetPath, relativePath); - Directory.CreateDirectory(newPath); + var targetDir = Directory.CreateDirectory(newPath); + + // Preserve directory attributes + targetDir.Attributes = sourceDir.Attributes; + + if (preserveTimestamps) + { + targetDir.CreationTimeUtc = sourceDir.CreationTimeUtc; + targetDir.LastWriteTimeUtc = sourceDir.LastWriteTimeUtc; + targetDir.LastAccessTimeUtc = sourceDir.LastAccessTimeUtc; + } } + // Copy all files foreach (var filePath in Directory.EnumerateFiles(this, "*", SearchOption.AllDirectories)) { + var sourceFile = new FileInfo(filePath); var relativePath = System.IO.Path.GetRelativePath(this, filePath); var newPath = System.IO.Path.Combine(targetPath, relativePath); System.IO.File.Copy(filePath, newPath, true); + + var targetFile = new FileInfo(newPath); + + // Preserve file attributes + targetFile.Attributes = sourceFile.Attributes; + + if (preserveTimestamps) + { + targetFile.CreationTimeUtc = sourceFile.CreationTimeUtc; + targetFile.LastWriteTimeUtc = sourceFile.LastWriteTimeUtc; + targetFile.LastAccessTimeUtc = sourceFile.LastAccessTimeUtc; + } } - ModuleLogger.Current.LogInformation("Copying Folder: {Source} > {Destination}", this, targetPath); + // Preserve root directory attributes and timestamps after all content is copied + var targetRootDir = new DirectoryInfo(targetPath); + targetRootDir.Attributes = DirectoryInfo.Attributes; + + if (preserveTimestamps) + { + targetRootDir.CreationTimeUtc = DirectoryInfo.CreationTimeUtc; + targetRootDir.LastWriteTimeUtc = DirectoryInfo.LastWriteTimeUtc; + targetRootDir.LastAccessTimeUtc = DirectoryInfo.LastAccessTimeUtc; + } + + LogFolderOperationWithDestination("Copied Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, targetPath); return new Folder(targetPath); } public Folder MoveTo(string path) { - ModuleLogger.Current.LogInformation("Moving Folder: {Source} > {Destination}", this, path); + LogFolderOperationWithDestination("Moving Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path); DirectoryInfo.MoveTo(path); return this; @@ -140,7 +223,7 @@ public Folder GetFolder(string name) { var directoryInfo = new DirectoryInfo(System.IO.Path.Combine(Path, name)); - ModuleLogger.Current.LogInformation("Getting Folder: {Path}", directoryInfo.FullName); + LogFolderOperation("Getting Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", directoryInfo.FullName); return directoryInfo; } @@ -149,7 +232,7 @@ public Folder CreateFolder(string name) { var folder = GetFolder(name).Create(); - ModuleLogger.Current.LogInformation("Creating Folder: {Path}", folder); + LogFolderOperation("Creating Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", folder); return folder; } @@ -158,7 +241,7 @@ public File GetFile(string name) { var fileInfo = new FileInfo(System.IO.Path.Combine(Path, name)); - ModuleLogger.Current.LogInformation("Getting File: {Path}", fileInfo.FullName); + LogFolderOperation("Getting File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", fileInfo.FullName); return fileInfo; } @@ -174,7 +257,7 @@ public File CreateFile(string name) public IEnumerable GetFolders(Func predicate, Func exclusionFilters, [CallerArgumentExpression("predicate")] string predicateExpression = "") { - ModuleLogger.Current.LogInformation("Searching Folders in: {Path} > {Expression}", this, predicateExpression); + LogFolderOperationWithExpression("Searching Folders in: {Path} > {Expression} [Module: {ModuleName}, Activity: {ActivityId}]", this, predicateExpression); return SafeWalk.EnumerateFolders(this, exclusionFilters) .Select(x => new Folder(x)) @@ -184,7 +267,7 @@ public IEnumerable GetFolders(Func predicate, Func GetFiles(Func predicate, Func directoryExclusionFilters, [CallerArgumentExpression("predicate")] string predicateExpression = "") { - ModuleLogger.Current.LogInformation("Searching Files in: {Path} > {Expression}", this, predicateExpression); + LogFolderOperationWithExpression("Searching Files in: {Path} > {Expression} [Module: {ModuleName}, Activity: {ActivityId}]", this, predicateExpression); return SafeWalk.EnumerateFiles(this, directoryExclusionFilters) .Select(x => new File(x)) @@ -194,7 +277,7 @@ public IEnumerable GetFiles(Func predicate, Func public IEnumerable GetFiles(string globPattern) { - ModuleLogger.Current.LogInformation("Searching Files in: {Path} > {Glob}", this, globPattern); + LogFolderOperationWithExpression("Searching Files in: {Path} > {Glob} [Module: {ModuleName}, Activity: {ActivityId}]", this, globPattern); return new Matcher(StringComparison.OrdinalIgnoreCase) .AddInclude(globPattern) @@ -231,7 +314,7 @@ public static Folder CreateTemporaryFolder() var tempDirectory = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName().Replace(".", string.Empty)); Directory.CreateDirectory(tempDirectory); - ModuleLogger.Current.LogInformation("Creating Temporary Folder: {Path}", tempDirectory); + LogFolderOperation("Creating Temporary Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", tempDirectory); return tempDirectory!; } @@ -315,4 +398,73 @@ public override int GetHashCode() { return !Equals(left, right); } -} \ No newline at end of file + + private static void RemoveReadOnlyAttributeRecursively(DirectoryInfo directory) + { + if ((directory.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + directory.Attributes &= ~FileAttributes.ReadOnly; + } + + foreach (var file in directory.EnumerateFiles("*", SearchOption.TopDirectoryOnly)) + { + if ((file.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + file.Attributes &= ~FileAttributes.ReadOnly; + } + } + + foreach (var subDirectory in directory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) + { + RemoveReadOnlyAttributeRecursively(subDirectory); + } + } + + /// + /// Logs a folder operation with Activity context information. + /// + /// + /// Phase 2: Uses Activity.Current for context alongside AsyncLocal for backward compatibility. + /// The log message includes the current module name and activity ID when available. + /// + private static void LogFolderOperation(string messageTemplate, object? arg1) + { + var moduleName = ModuleActivityTracing.GetCurrentModuleName() ?? "Unknown"; + var activityId = ModuleActivityTracing.GetCurrentActivityId(); + + ModuleLogger.Current.LogInformation(messageTemplate, arg1, moduleName, activityId); + } + + /// + /// Logs a folder operation with Activity context information for operations with source and destination. + /// + private static void LogFolderOperationWithDestination(string messageTemplate, object? source, object? destination) + { + var moduleName = ModuleActivityTracing.GetCurrentModuleName() ?? "Unknown"; + var activityId = ModuleActivityTracing.GetCurrentActivityId(); + + ModuleLogger.Current.LogInformation(messageTemplate, source, destination, moduleName, activityId); + } + + /// + /// Logs a folder operation with Activity context information for operations with path and expression/glob. + /// + private static void LogFolderOperationWithExpression(string messageTemplate, object? path, object? expression) + { + var moduleName = ModuleActivityTracing.GetCurrentModuleName() ?? "Unknown"; + var activityId = ModuleActivityTracing.GetCurrentActivityId(); + + ModuleLogger.Current.LogInformation(messageTemplate, path, expression, moduleName, activityId); + } + + /// + /// Logs a folder warning with Activity context information. + /// + private static void LogFolderWarning(Exception ex, string messageTemplate, object? arg1) + { + var moduleName = ModuleActivityTracing.GetCurrentModuleName() ?? "Unknown"; + var activityId = ModuleActivityTracing.GetCurrentActivityId(); + + ModuleLogger.Current.LogWarning(ex, messageTemplate, arg1, moduleName, activityId); + } +} diff --git a/src/ModularPipelines/Tracing/ModuleActivityTracing.cs b/src/ModularPipelines/Tracing/ModuleActivityTracing.cs index c53ff6f250..79723b7f17 100644 --- a/src/ModularPipelines/Tracing/ModuleActivityTracing.cs +++ b/src/ModularPipelines/Tracing/ModuleActivityTracing.cs @@ -109,4 +109,22 @@ public static void RecordFailure(Activity? activity, Exception exception) activity.SetTag(ExceptionMessageTag, exception.Message); activity.SetStatus(ActivityStatusCode.Error, exception.Message); } + + /// + /// Gets the current module name from the current Activity, if available. + /// + /// The module name, or null if no module activity is active. + public static string? GetCurrentModuleName() + { + return Activity.Current?.GetTagItem(ModuleTypeTag) as string; + } + + /// + /// Gets the current Activity ID, if available. + /// + /// The activity ID, or null if no activity is active. + public static string? GetCurrentActivityId() + { + return Activity.Current?.Id; + } }