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;
+ }
}