diff options
Diffstat (limited to 'src/Agent.Windows/Recovery/TransactionalFileEditor.cs')
| -rw-r--r-- | src/Agent.Windows/Recovery/TransactionalFileEditor.cs | 41 |
1 files changed, 41 insertions, 0 deletions
diff --git a/src/Agent.Windows/Recovery/TransactionalFileEditor.cs b/src/Agent.Windows/Recovery/TransactionalFileEditor.cs index 805a069..25c949d 100644 --- a/src/Agent.Windows/Recovery/TransactionalFileEditor.cs +++ b/src/Agent.Windows/Recovery/TransactionalFileEditor.cs @@ -93,6 +93,26 @@ public sealed class TransactionalFileEditor(TaskResourceJournal journal, string return new(journal, entryId, canonical); } + public FileRenameTransaction Rename(string sourcePath, string destinationPath) + { + string source = CanonicalPath.Resolve(sourcePath); + string destination = CanonicalPath.Resolve(destinationPath, requireExisting: false); + if (!File.Exists(source)) throw new FileNotFoundException($"Source file does not exist: '{source}'."); + if (File.Exists(destination)) throw new ToolValidationException($"Destination already exists: '{destination}'. Rename overwrite is not allowed."); + if (!source.StartsWith(System.IO.Path.GetPathRoot(source)!, StringComparison.OrdinalIgnoreCase) || + !destination.StartsWith(System.IO.Path.GetPathRoot(source)!, StringComparison.OrdinalIgnoreCase)) + throw new ToolValidationException("Cross-volume rename is not supported."); + byte[] content = File.ReadAllBytes(source); + string sourceHash = Convert.ToHexString(SHA256.HashData(content)); + Guid entryId = journal.Prepare("file.rename", source, new Dictionary<string, string> + { + ["destination_path"] = destination, ["source_sha256"] = sourceHash + }); + File.Move(source, destination); + journal.MarkApplied(entryId); + return new(journal, entryId, source, destination, sourceHash); + } + private static string NormalizeHash(string hash) => hash.Replace("-", string.Empty, StringComparison.Ordinal).Trim().ToUpperInvariant(); } @@ -144,6 +164,27 @@ public sealed class DirectoryRmdirTransaction(TaskResourceJournal journal, Guid } } +public sealed class FileRenameTransaction(TaskResourceJournal journal, Guid entryId, string sourcePath, string destinationPath, string sourceHash) +{ + private bool _finished; + public void Commit() { if (_finished) return; journal.MarkCleaned(entryId); _finished = true; } + public void Rollback() + { + if (_finished) return; + if (!File.Exists(destinationPath)) throw new InvalidOperationException("Rollback refused because the destination file is missing."); + if (!Hash(destinationPath).Equals(sourceHash, StringComparison.Ordinal)) + throw new InvalidDataException("Rollback refused because the destination file hash does not match the original source hash."); + File.Move(destinationPath, sourcePath); + journal.MarkCleaned(entryId); + _finished = true; + } + private static string Hash(string target) + { + using FileStream stream = File.OpenRead(target); + return Convert.ToHexString(SHA256.HashData(stream)); + } +} + public sealed class FileEditTransaction( TaskResourceJournal journal, Guid entryId, string path, string originalHash, string replacementHash, string backupPath, string originalBase64, bool originallyExisted) |
