From afcfc0d8c7e1a89417523e3966ae86fa54458706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Tue, 15 Apr 2025 19:24:31 +0200 Subject: [PATCH] Add log parser to output added file list sorted by size --- ResticLogParser.sln | 31 +++++++ ResticLogParser/Program.cs | 3 + ResticLogParser/ResticLogParser.csproj | 10 ++ ResticLogParser/src/FileLogEntry.cs | 36 ++++++++ ResticLogParser/src/FileLogEntryType.cs | 8 ++ ResticLogParser/src/FileSize.cs | 46 ++++++++++ ResticLogParser/src/LogParser.cs | 91 +++++++++++++++++++ ResticLogParserTests/FileSizeTests.cs | 18 ++++ .../ResticLogParserTests.csproj | 28 ++++++ 9 files changed, 271 insertions(+) create mode 100644 ResticLogParser.sln create mode 100644 ResticLogParser/Program.cs create mode 100644 ResticLogParser/ResticLogParser.csproj create mode 100644 ResticLogParser/src/FileLogEntry.cs create mode 100644 ResticLogParser/src/FileLogEntryType.cs create mode 100644 ResticLogParser/src/FileSize.cs create mode 100644 ResticLogParser/src/LogParser.cs create mode 100644 ResticLogParserTests/FileSizeTests.cs create mode 100644 ResticLogParserTests/ResticLogParserTests.csproj diff --git a/ResticLogParser.sln b/ResticLogParser.sln new file mode 100644 index 0000000..d61b91c --- /dev/null +++ b/ResticLogParser.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35931.197 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResticLogParser", "ResticLogParser\ResticLogParser.csproj", "{F688B5E5-B5BD-43D2-96E9-A7286A0EB440}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResticLogParserTests", "ResticLogParserTests\ResticLogParserTests.csproj", "{B4D864B6-35C1-4890-A89E-4C13152B569C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F688B5E5-B5BD-43D2-96E9-A7286A0EB440}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F688B5E5-B5BD-43D2-96E9-A7286A0EB440}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F688B5E5-B5BD-43D2-96E9-A7286A0EB440}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F688B5E5-B5BD-43D2-96E9-A7286A0EB440}.Release|Any CPU.Build.0 = Release|Any CPU + {B4D864B6-35C1-4890-A89E-4C13152B569C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4D864B6-35C1-4890-A89E-4C13152B569C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4D864B6-35C1-4890-A89E-4C13152B569C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4D864B6-35C1-4890-A89E-4C13152B569C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D90F1BCC-4C6C-48FC-8920-2B87B5019AAB} + EndGlobalSection +EndGlobal diff --git a/ResticLogParser/Program.cs b/ResticLogParser/Program.cs new file mode 100644 index 0000000..51df1c3 --- /dev/null +++ b/ResticLogParser/Program.cs @@ -0,0 +1,3 @@ +using ResticLogParser.src; + +new LogParser(@"C:\Users\warrence\Documents\My Documents\restic\restic-logfile.txt").Run(); \ No newline at end of file diff --git a/ResticLogParser/ResticLogParser.csproj b/ResticLogParser/ResticLogParser.csproj new file mode 100644 index 0000000..2150e37 --- /dev/null +++ b/ResticLogParser/ResticLogParser.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/ResticLogParser/src/FileLogEntry.cs b/ResticLogParser/src/FileLogEntry.cs new file mode 100644 index 0000000..1acec92 --- /dev/null +++ b/ResticLogParser/src/FileLogEntry.cs @@ -0,0 +1,36 @@ +namespace ResticLogParser.src +{ + internal readonly struct FileLogEntry : IComparable + { + public FileLogEntry(string fileName, FileLogEntryType type, long addedSize) + : this(fileName, type, addedSize, addedSize) + { + } + + public FileLogEntry(string fileName, FileLogEntryType type, long addedSize, long storedSize) + { + FileName = fileName; + Type = type; + AddedSize = addedSize; + StoredSize = storedSize; + } + + public string FileName { get; init; } + + public FileLogEntryType Type { get; init; } + + public long AddedSize { get; init; } + + public long StoredSize { get; init; } + + public int CompareTo(FileLogEntry other) + { + return AddedSize.CompareTo(other.AddedSize); + } + + public override string ToString() + { + return $"{Type}, added {AddedSize} bytes, {FileName}"; + } + } +} diff --git a/ResticLogParser/src/FileLogEntryType.cs b/ResticLogParser/src/FileLogEntryType.cs new file mode 100644 index 0000000..54106cb --- /dev/null +++ b/ResticLogParser/src/FileLogEntryType.cs @@ -0,0 +1,8 @@ +namespace ResticLogParser.src +{ + internal enum FileLogEntryType + { + New, + Modified + } +} diff --git a/ResticLogParser/src/FileSize.cs b/ResticLogParser/src/FileSize.cs new file mode 100644 index 0000000..eee0a56 --- /dev/null +++ b/ResticLogParser/src/FileSize.cs @@ -0,0 +1,46 @@ +using System.Globalization; + +namespace ResticLogParser.src +{ + public class FileSize + { + public static long GetBytesFromString(string fileSizeText) + { + string[] split = fileSizeText.Split(" ", StringSplitOptions.TrimEntries); + if (split.Length < 2 || split[0].Length < 1 || split[1].Length < 1 || split[1].Length > 3 + || split[1][split[1].Length - 1] != 'B') + { + return 0; + } + var value = double.Parse(split[0], CultureInfo.InvariantCulture.NumberFormat); + var power = split[1].Length > 1 + ? split[1][0] switch + { + 'K' => 1, + 'M' => 2, + 'G' => 3, + 'T' => 4, + _ => 0 + } + : 0; + + int valueBase; + if (split[1].Length < 3) + { + valueBase = 1000; + } + else if (split[1][1] == 'i') + { + valueBase = 1024; + } + else + { + return 0; + } + + var s = Math.Pow(valueBase, power); + var t = value * Math.Pow(valueBase, power); + return (long)Math.Round(value * Math.Pow(valueBase, power)); + } + } +} diff --git a/ResticLogParser/src/LogParser.cs b/ResticLogParser/src/LogParser.cs new file mode 100644 index 0000000..e481e57 --- /dev/null +++ b/ResticLogParser/src/LogParser.cs @@ -0,0 +1,91 @@ +using System.Text.RegularExpressions; + +namespace ResticLogParser.src +{ + internal class LogParser + { + // Example match: + // new /E/Projects/Unity/SpriteTransfer/Images/Equipment/Belts/Belt 3 Up.png, saved in 0.000s (7.201 KiB added) + private Regex _newFileLogEntryRegex = new(@"new\s+(.+), saved in \d+\.\d+s \((\d+(?:\.\d+)? .+) added\)"); + + // Example match: + // modified /C/Users/warrence/AppData/Local/Log/Desktop.log, saved in 0.009s (1.135 MiB added, 1.135 MiB stored) + private Regex _modifiedFileLogEntryRegex = + new(@"modified\s+(.+), saved in \d+\.\d+s \((\d+(?:\.\d+)? .+) added, (\d+(?:\.\d+)? .+) stored\)"); + + private string _logFileName; + private readonly List _files = new(); + + public LogParser(string logFileName) + { + if (string.IsNullOrEmpty(logFileName)) + { + throw new ArgumentNullException(nameof(logFileName)); + } + + _logFileName = logFileName; + } + + public void Run() + { + Console.WriteLine($"Running restic log parser for '{_logFileName}'..."); + ParseLogFile(); + _files.Sort(); + WriteFileInformation(); + } + + private void ParseLogFile() + { + try + { + using StreamReader reader = new(_logFileName); + while (!reader.EndOfStream) + { + string line = reader.ReadLine()!; + TryParseLogEntry(line); + } + } + catch (IOException e) + { + Console.WriteLine("The file could not be read:"); + Console.WriteLine(e.Message); + } + } + + private bool TryParseLogEntry(string logLine) + { + Match match = _newFileLogEntryRegex.Match(logLine); + if (match.Success) + { + var size = FileSize.GetBytesFromString(match.Groups[2].Value); + _files.Add(new FileLogEntry(match.Groups[1].Value, FileLogEntryType.New, size)); + return true; + } + + match = _modifiedFileLogEntryRegex.Match(logLine); + if (match.Success) + { + var addedSize = FileSize.GetBytesFromString(match.Groups[2].Value); + var storedSize = FileSize.GetBytesFromString(match.Groups[3].Value); + _files.Add(new FileLogEntry(match.Groups[1].Value, FileLogEntryType.Modified, addedSize, storedSize)); + return true; + } + + return false; + } + + private void WriteFileInformation() + { + string outputFileName = Path.Combine(Path.GetDirectoryName(_logFileName)!, "restic-added-data.txt"); + Console.WriteLine($"Writing output to '{outputFileName}'..."); + + using (StreamWriter outputFile = new StreamWriter(outputFileName)) + { + for (int i = _files.Count - 1; i >= 0; i--) + { + outputFile.WriteLine(_files[i].ToString()); + } + } + } + } +} diff --git a/ResticLogParserTests/FileSizeTests.cs b/ResticLogParserTests/FileSizeTests.cs new file mode 100644 index 0000000..5133db1 --- /dev/null +++ b/ResticLogParserTests/FileSizeTests.cs @@ -0,0 +1,18 @@ +using ResticLogParser.src; + +namespace ResticLogParserTests +{ + public class FileSizeTests + { + [TestCase("0 B", 0)] + [TestCase("739 B", 739)] + [TestCase("17.601 KiB", 18023)] + [TestCase("17.601 KB", 17601)] + [TestCase("107.135 MiB", 112339190)] + [TestCase("107.135 MB", 107135000)] + public void Test(string fileSizeText, long expected) + { + Assert.That(FileSize.GetBytesFromString(fileSizeText), Is.EqualTo(expected)); + } + } +} \ No newline at end of file diff --git a/ResticLogParserTests/ResticLogParserTests.csproj b/ResticLogParserTests/ResticLogParserTests.csproj new file mode 100644 index 0000000..4be83ed --- /dev/null +++ b/ResticLogParserTests/ResticLogParserTests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + +