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