Add log parser to output added file list sorted by size

This commit is contained in:
Stefan Müller 2025-04-15 19:24:31 +02:00
parent 54decd3526
commit afcfc0d8c7
9 changed files with 271 additions and 0 deletions

31
ResticLogParser.sln Normal file
View File

@ -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

View File

@ -0,0 +1,3 @@
using ResticLogParser.src;
new LogParser(@"C:\Users\warrence\Documents\My Documents\restic\restic-logfile.txt").Run();

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,36 @@
namespace ResticLogParser.src
{
internal readonly struct FileLogEntry : IComparable<FileLogEntry>
{
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}";
}
}
}

View File

@ -0,0 +1,8 @@
namespace ResticLogParser.src
{
internal enum FileLogEntryType
{
New,
Modified
}
}

View File

@ -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));
}
}
}

View File

@ -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<FileLogEntry> _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());
}
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ResticLogParser\ResticLogParser.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>
</Project>