diff --git a/src/ModularPipelines/FileSystem/File.cs b/src/ModularPipelines/FileSystem/File.cs index ba736a8fbb..e3558fc1d2 100644 --- a/src/ModularPipelines/FileSystem/File.cs +++ b/src/ModularPipelines/FileSystem/File.cs @@ -163,6 +163,19 @@ public File Create() return this; } + /// + /// Asynchronously creates a new file at the current path. + /// + /// This file instance for method chaining. + public async Task CreateAsync() + { + LogFileOperation("Creating File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); + + var fileStream = System.IO.File.Create(Path); + await fileStream.DisposeAsync().ConfigureAwait(false); + return this; + } + /// > public FileAttributes Attributes { @@ -192,6 +205,21 @@ public void Delete() FileInfo.Delete(); } + /// + /// Asynchronously deletes the file. + /// + /// + /// Uses thread pool offloading as no native async delete API exists in .NET. + /// For true async I/O, consider using stream-based operations where available. + /// + /// Cancellation token. + public Task DeleteAsync(CancellationToken cancellationToken = default) + { + LogFileOperation("Deleting File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); + + return Task.Run(() => FileInfo.Delete(), cancellationToken); + } + /// > public File MoveTo(string path) { @@ -210,6 +238,43 @@ public File MoveTo(Folder folder) return MoveTo(System.IO.Path.Combine(folder.Path, Name)); } + /// + /// Asynchronously moves the file to a new path. + /// + /// + /// Uses thread pool offloading as no native async move API exists in .NET. + /// + /// The destination path. + /// Cancellation token. + /// This file instance for method chaining. + public Task MoveToAsync(string path, CancellationToken cancellationToken = default) + { + LogFileOperationWithDestination("Moving File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path); + + return Task.Run(() => + { + FileInfo.MoveTo(path); + return this; + }, cancellationToken); + } + + /// + /// Asynchronously moves the file to a folder. + /// + /// + /// Uses thread pool offloading as no native async move API exists in .NET. + /// + /// The destination folder. + /// Cancellation token. + /// This file instance for method chaining. + public async Task MoveToAsync(Folder folder, CancellationToken cancellationToken = default) + { + LogFileOperationWithDestination("Moving File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, folder); + + await folder.CreateAsync().ConfigureAwait(false); + return await MoveToAsync(System.IO.Path.Combine(folder.Path, Name), cancellationToken).ConfigureAwait(false); + } + /// > public File CopyTo(string path) { @@ -226,6 +291,37 @@ public File CopyTo(Folder folder) return CopyTo(System.IO.Path.Combine(folder.Path, Name)); } + /// + /// Asynchronously copies the file to a new path using stream-based copying. + /// + /// The destination path. + /// Cancellation token. + /// A new File instance representing the copied file. + public async Task CopyToAsync(string path, CancellationToken cancellationToken = default) + { + LogFileOperationWithDestination("Copying File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path); + + await using var sourceStream = System.IO.File.OpenRead(Path); + await using var destStream = System.IO.File.Create(path); + await sourceStream.CopyToAsync(destStream, cancellationToken).ConfigureAwait(false); + + return new File(path); + } + + /// + /// Asynchronously copies the file to a folder using stream-based copying. + /// + /// The destination folder. + /// Cancellation token. + /// A new File instance representing the copied file. + public async Task CopyToAsync(Folder folder, CancellationToken cancellationToken = default) + { + LogFileOperationWithDestination("Copying File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, folder); + + await folder.CreateAsync().ConfigureAwait(false); + return await CopyToAsync(System.IO.Path.Combine(folder.Path, Name), cancellationToken).ConfigureAwait(false); + } + public static File GetNewTemporaryFilePath() { var path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName()); diff --git a/src/ModularPipelines/FileSystem/Folder.cs b/src/ModularPipelines/FileSystem/Folder.cs index 185054f276..2ecab8cf5e 100644 --- a/src/ModularPipelines/FileSystem/Folder.cs +++ b/src/ModularPipelines/FileSystem/Folder.cs @@ -96,6 +96,25 @@ public Folder Create() return this; } + /// + /// Asynchronously creates the folder if it does not exist. + /// + /// + /// Uses thread pool offloading as no native async directory creation API exists in .NET. + /// + /// Optional cancellation token. + /// This folder instance for method chaining. + public Task CreateAsync(CancellationToken cancellationToken = default) + { + LogFolderOperation("Creating Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); + + return Task.Run(() => + { + Directory.CreateDirectory(Path); + return this; + }, cancellationToken); + } + public void Delete() { LogFolderOperation("Deleting Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); @@ -103,6 +122,20 @@ public void Delete() DirectoryInfo.Delete(true); } + /// + /// Asynchronously deletes the folder and all its contents. + /// + /// + /// Uses thread pool offloading as no native async delete API exists in .NET. + /// + /// Cancellation token. + public Task DeleteAsync(CancellationToken cancellationToken = default) + { + LogFolderOperation("Deleting Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this); + + return Task.Run(() => DirectoryInfo.Delete(true), cancellationToken); + } + /// /// Removes all files and subdirectories within the folder. /// @@ -270,6 +303,91 @@ public Folder CopyTo(string targetPath, bool preserveTimestamps) return new Folder(targetPath); } + /// + /// Asynchronously copies the folder and its contents to the specified target path using stream-based file copying. + /// + /// The destination path for the copied folder. + /// Cancellation token. + /// A new instance representing the copied folder. + public Task CopyToAsync(string targetPath, CancellationToken cancellationToken = default) + { + return CopyToAsync(targetPath, preserveTimestamps: false, cancellationToken); + } + + /// + /// Asynchronously copies the folder and its contents to the specified target path using stream-based file copying. + /// + /// The destination path for the copied folder. + /// + /// When true, preserves CreationTimeUtc, LastWriteTimeUtc, and LastAccessTimeUtc + /// for all files and directories. + /// + /// Cancellation token. + /// A new instance representing the copied folder. + public async Task CopyToAsync(string targetPath, bool preserveTimestamps, CancellationToken cancellationToken = default) + { + LogFolderOperationWithDestination("Copying Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, targetPath); + + Directory.CreateDirectory(targetPath); + + // Copy all subdirectories first + foreach (var dirPath in Directory.EnumerateDirectories(this, "*", SearchOption.AllDirectories)) + { + cancellationToken.ThrowIfCancellationRequested(); + + var sourceDir = new DirectoryInfo(dirPath); + var relativePath = System.IO.Path.GetRelativePath(this, dirPath); + var newPath = System.IO.Path.Combine(targetPath, relativePath); + var targetDir = Directory.CreateDirectory(newPath); + + targetDir.Attributes = sourceDir.Attributes; + + if (preserveTimestamps) + { + targetDir.CreationTimeUtc = sourceDir.CreationTimeUtc; + targetDir.LastWriteTimeUtc = sourceDir.LastWriteTimeUtc; + targetDir.LastAccessTimeUtc = sourceDir.LastAccessTimeUtc; + } + } + + // Copy all files using async stream copying + foreach (var filePath in Directory.EnumerateFiles(this, "*", SearchOption.AllDirectories)) + { + cancellationToken.ThrowIfCancellationRequested(); + + var sourceFile = new FileInfo(filePath); + var relativePath = System.IO.Path.GetRelativePath(this, filePath); + var newPath = System.IO.Path.Combine(targetPath, relativePath); + + await using var sourceStream = System.IO.File.OpenRead(filePath); + await using var destStream = System.IO.File.Create(newPath); + await sourceStream.CopyToAsync(destStream, cancellationToken).ConfigureAwait(false); + + var targetFile = new FileInfo(newPath); + targetFile.Attributes = sourceFile.Attributes; + + if (preserveTimestamps) + { + targetFile.CreationTimeUtc = sourceFile.CreationTimeUtc; + targetFile.LastWriteTimeUtc = sourceFile.LastWriteTimeUtc; + targetFile.LastAccessTimeUtc = sourceFile.LastAccessTimeUtc; + } + } + + // Preserve root directory attributes and timestamps + var targetRootDir = new DirectoryInfo(targetPath); + targetRootDir.Attributes = DirectoryInfo.Attributes; + + if (preserveTimestamps) + { + targetRootDir.CreationTimeUtc = DirectoryInfo.CreationTimeUtc; + targetRootDir.LastWriteTimeUtc = DirectoryInfo.LastWriteTimeUtc; + targetRootDir.LastAccessTimeUtc = DirectoryInfo.LastAccessTimeUtc; + } + + return new Folder(targetPath); + } + public Folder MoveTo(string path) { LogFolderOperationWithDestination("Moving Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path); @@ -278,6 +396,26 @@ public Folder MoveTo(string path) return this; } + /// + /// Asynchronously moves the folder to a new path. + /// + /// + /// Uses thread pool offloading as no native async move API exists in .NET. + /// + /// The destination path. + /// Cancellation token. + /// This folder instance for method chaining. + public Task MoveToAsync(string path, CancellationToken cancellationToken = default) + { + LogFolderOperationWithDestination("Moving Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path); + + return Task.Run(() => + { + DirectoryInfo.MoveTo(path); + return this; + }, cancellationToken); + } + public Folder GetFolder(string name) { var directoryInfo = new DirectoryInfo(System.IO.Path.Combine(Path, name));