diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj
index 5d577965..e402c4c4 100644
--- a/Source/AppScaffolding/AppScaffolding.csproj
+++ b/Source/AppScaffolding/AppScaffolding.csproj
@@ -7,10 +7,9 @@
-
+
-
diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs
index 41d25595..2653ab57 100644
--- a/Source/AppScaffolding/LibationScaffolding.cs
+++ b/Source/AppScaffolding/LibationScaffolding.cs
@@ -115,11 +115,22 @@ namespace AppScaffolding
{
if (config.GetObject("Serilog") is JObject serilog)
{
- if (serilog["WriteTo"] is JArray sinks && sinks.FirstOrDefault(s => s["Name"].Value() is "File") is JToken fileSink)
+ bool fileChanged = false;
+ if (serilog.SelectToken("$.WriteTo[?(@.Name == 'ZipFile')]", false) is JObject zipFileSink)
{
- fileSink["Name"] = "ZipFile";
- config.SetNonString(serilog.DeepClone(), "Serilog");
+ zipFileSink["Name"] = "File";
+ fileChanged = true;
}
+ var hooks = $"{nameof(LibationFileManager)}.{nameof(FileSinkHook)}, {nameof(LibationFileManager)}";
+ if (serilog.SelectToken("$.WriteTo[?(@.Name == 'File')].Args", false) is JObject fileSinkArgs
+ && fileSinkArgs["hooks"]?.Value() != hooks)
+ {
+ fileSinkArgs["hooks"] = hooks;
+ fileChanged = true;
+ }
+
+ if (fileChanged)
+ config.SetNonString(serilog.DeepClone(), "Serilog");
return;
}
@@ -129,17 +140,17 @@ namespace AppScaffolding
{ "WriteTo", new JArray
{
// ABOUT SINKS
- // Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use.
+ // Only File sink is currently used. By user request (June 2024) others packages are included for experimental use.
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
new JObject
{
- { "Name", "ZipFile" },
+ { "Name", "File" },
{ "Args",
new JObject
{
// for this sink to work, a path must be provided. we override this below
- { "path", Path.Combine(config.LibationFiles, "_Log.log") },
+ { "path", Path.Combine(config.LibationFiles, "Log.log") },
{ "rollingInterval", "Month" },
// Serilog template formatting examples
// - default: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
@@ -274,7 +285,7 @@ namespace AppScaffolding
disableIPv6 = AppContext.TryGetSwitch("System.Net.DisableIPv6", out bool disableIPv6Value),
});
- if (InteropFactory.InteropFunctionsType is null)
+ if (InteropFactory.InteropFunctionsType is null)
Serilog.Log.Logger.Warning("WARNING: OSInteropProxy.InteropFunctionsType is null");
}
diff --git a/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
index 03e2f05f..272f5da7 100644
--- a/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
+++ b/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
@@ -58,10 +58,5 @@ namespace LibationAvalonia.Controls.Settings
}
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
}
-
- public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- {
- Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
- }
}
}
diff --git a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
index df23a16c..d74710c8 100644
--- a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
+++ b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
@@ -57,7 +57,13 @@ namespace LibationAvalonia.ViewModels.Settings
config.GridFontScaleFactor = linearRangeToScaleFactor(GridFontScaleFactor);
config.GridScaleFactor = linearRangeToScaleFactor(GridScaleFactor);
}
- public void OpenLogFolderButton() => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
+ public void OpenLogFolderButton()
+ {
+ if (System.IO.File.Exists(LogFileFilter.LogFilePath))
+ Go.To.File(LogFileFilter.LogFilePath);
+ else
+ Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
+ }
public List KnownDirectories { get; } = new()
{
diff --git a/Source/LibationFileManager/Configuration.Logging.cs b/Source/LibationFileManager/Configuration.Logging.cs
index 66ea7cda..595e18d1 100644
--- a/Source/LibationFileManager/Configuration.Logging.cs
+++ b/Source/LibationFileManager/Configuration.Logging.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel;
-using System.Linq;
using Dinah.Core.Logging;
using FileManager;
using Microsoft.Extensions.Configuration;
@@ -10,7 +9,7 @@ using Serilog.Events;
#nullable enable
namespace LibationFileManager
{
- public partial class Configuration
+ public partial class Configuration
{
private IConfigurationRoot? configuration;
@@ -19,13 +18,14 @@ namespace LibationFileManager
configuration = new ConfigurationBuilder()
.AddJsonFile(SettingsFilePath, optional: false, reloadOnChange: true)
.Build();
- Log.Logger = new LoggerConfiguration()
- .ReadFrom.Configuration(configuration)
- .Destructure.ByTransforming(lp => lp.Path)
- .CreateLogger();
- }
+ Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(configuration)
+ .Destructure.ByTransforming(lp => lp.Path)
+ .Destructure.With()
+ .CreateLogger();
+ }
- [Description("The importance of a log event")]
+ [Description("The importance of a log event")]
public LogEventLevel LogLevel
{
get
diff --git a/Source/LibationFileManager/LogFileFilter.cs b/Source/LibationFileManager/LogFileFilter.cs
new file mode 100644
index 00000000..62c89097
--- /dev/null
+++ b/Source/LibationFileManager/LogFileFilter.cs
@@ -0,0 +1,113 @@
+using Serilog.Core;
+using Serilog.Events;
+using Serilog.Sinks.File;
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+#nullable enable
+namespace LibationFileManager;
+
+///
+/// Hooks the file sink to set the log file path for the LogFileFilter.
+///
+public class FileSinkHook : FileLifecycleHooks
+{
+ public override Stream OnFileOpened(string path, Stream underlyingStream, Encoding encoding)
+ {
+ LogFileFilter.SetLogFilePath(path);
+ return base.OnFileOpened(path, underlyingStream, encoding);
+ }
+}
+
+
+///
+/// Identify log entries which are to be written to files, and save them to a zip file.
+///
+/// Files are detected by pattern matching. If the logged type has properties named 'filename' and 'filedata' (case insensitive)
+/// with types string and byte[] respectively, the type is destructured and written to the log zip file.
+///
+/// The zip file's name will be derived from the active log file's name, with "_AdditionalFiles.zip" appended.
+///
+public class LogFileFilter : IDestructuringPolicy
+{
+ private static readonly object lockObj = new();
+ public static string? ZipFilePath { get; private set; }
+ public static string? LogFilePath { get; private set; }
+ public static void SetLogFilePath(string? logFilePath)
+ {
+ lock(lockObj)
+ {
+ (LogFilePath, ZipFilePath)
+ = File.Exists(logFilePath) && Path.GetDirectoryName(logFilePath) is string logDir
+ ? (logFilePath, Path.Combine(logDir, $"{Path.GetFileNameWithoutExtension(logFilePath)}_AdditionalFiles.zip"))
+ : (null, null);
+ }
+ }
+
+ private static bool TrySaveLogFile(ref string filename, byte[] fileData, CompressionLevel compression)
+ {
+ try
+ {
+ lock (lockObj)
+ {
+ if (string.IsNullOrEmpty(ZipFilePath))
+ return false;
+
+ using var archive = new ZipArchive(File.Open(ZipFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite), ZipArchiveMode.Update, false, Encoding.UTF8);
+ filename = GetUniqueEntryName(archive, filename);
+
+ var entry = archive.CreateEntry(filename, compression);
+ using var entryStream = entry.Open();
+ entryStream.Write(fileData);
+ }
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static string GetUniqueEntryName(ZipArchive archive, string filename)
+ {
+ var entryFileName = filename;
+ for (int i = 1; archive.Entries.Any(e => e.Name == entryFileName); i++)
+ {
+ entryFileName = $"{Path.GetFileNameWithoutExtension(filename)}_({i++}){Path.GetExtension(filename)}";
+ }
+ return entryFileName;
+ }
+
+ public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, [NotNullWhen(true)] out LogEventPropertyValue? result)
+ {
+ if (value?.GetType().GetProperties() is PropertyInfo[] properties && properties.Length >= 2
+ && properties.FirstOrDefault(p => p.Name.Equals("filename", StringComparison.InvariantCultureIgnoreCase)) is PropertyInfo filenameProperty && filenameProperty.PropertyType == typeof(string)
+ && properties.FirstOrDefault(p => p.Name.Equals("fileData", StringComparison.InvariantCultureIgnoreCase)) is PropertyInfo fileDataProperty && fileDataProperty.PropertyType == typeof(byte[]))
+ {
+ var filename = filenameProperty.GetValue(value) as string;
+ var fileData = fileDataProperty.GetValue(value) as byte[];
+
+ if (filename != null && fileData != null && fileData.Length > 0)
+ {
+ var compressionProperty = properties.FirstOrDefault(f => f.PropertyType == typeof(CompressionLevel));
+ var compression = compressionProperty?.GetValue(value) is CompressionLevel c ? c : CompressionLevel.Fastest;
+
+ result
+ = TrySaveLogFile(ref filename, fileData, compression)
+ ? propertyValueFactory.CreatePropertyValue($"Log file '{filename}' saved in {ZipFilePath}")
+ : propertyValueFactory.CreatePropertyValue($"Log file '{filename}' could not be saved in {ZipFilePath ?? ""}. File Contents = {Convert.ToBase64String(fileData)}");
+
+ return true;
+ }
+ }
+
+ result = null;
+ return false;
+ }
+}
diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs
index 7a18b190..29c235be 100644
--- a/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs
+++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs
@@ -11,7 +11,13 @@ namespace LibationWinForms.Dialogs
{
public partial class SettingsDialog
{
- private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
+ private void logsBtn_Click(object sender, EventArgs e)
+ {
+ if (File.Exists(LogFileFilter.LogFilePath))
+ Go.To.File(LogFileFilter.LogFilePath);
+ else
+ Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
+ }
private void Load_Important(Configuration config)
{
diff --git a/Source/Upgrading dotnet version.txt b/Source/Upgrading dotnet version.txt
index a81879cf..e47b43ae 100644
--- a/Source/Upgrading dotnet version.txt
+++ b/Source/Upgrading dotnet version.txt
@@ -6,4 +6,3 @@ For reasons I cannot figure out, upgrading the major .net version breaks Libatio
* https://github.com/Mbucari/AAXClean.Codecs
* https://github.com/Mbucari/AAXClean
-* https://github.com/Mbucari/serilog-sinks-zipfile