This commit is contained in:
1 parent
4081b5949d
commit
2909500bb8
14 files changed
+259
-31
No files matched your search
@@ -36,7 +36,7 @@ namespace EonaCat.LogStack.Server
|
||||
_maxLogDirectorySize = maxLogDirectorySize;
|
||||
}
|
||||
|
||||
protected virtual Task ProcessLogAsync(string logData)
|
||||
protected virtual async Task ProcessLogAsync(string logData)
|
||||
{
|
||||
string logsRootDirectory = "logs";
|
||||
|
||||
@@ -67,7 +67,12 @@ namespace EonaCat.LogStack.Server
|
||||
// After processing log, check directory size and clean up if needed
|
||||
CleanUpOldLogs();
|
||||
|
||||
return File.AppendAllTextAsync(logFilePath, logData + Environment.NewLine);
|
||||
using (var stream = new FileStream(logFilePath, FileMode.Append, FileAccess.Write, FileShare.Read | FileShare.Delete, 4096, FileOptions.Asynchronous))
|
||||
using (var writer = new StreamWriter(stream))
|
||||
{
|
||||
await writer.WriteLineAsync(logData).ConfigureAwait(false);
|
||||
await writer.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanUpOldLogs()
|
||||
|
||||
@@ -24,6 +24,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.Status", "EonaCat.LogStack.Status\EonaCat.LogStack.Status.csproj", "{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testers", "Testers", "{A930DAA9-4165-776A-E33D-6691CB934F50}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.FileDeletionTests", "Testers\EonaCat.LogStack.FileDeletionTests\EonaCat.LogStack.FileDeletionTests.csproj", "{25A49D02-5463-4662-B064-55293976DF0A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -130,10 +134,25 @@ Global
|
||||
{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x64.Build.0 = Release|Any CPU
|
||||
{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x86.Build.0 = Release|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{25A49D02-5463-4662-B064-55293976DF0A}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{25A49D02-5463-4662-B064-55293976DF0A} = {A930DAA9-4165-776A-E33D-6691CB934F50}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B01183F3-D85E-45FB-9749-DA281F465A0F}
|
||||
EndGlobalSection
|
||||
|
||||
@@ -134,7 +134,11 @@ namespace EonaCat.LogStack.Features
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
});
|
||||
|
||||
File.WriteAllText(filePath, json);
|
||||
using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read | FileShare.Delete))
|
||||
using (var writer = new StreamWriter(stream))
|
||||
{
|
||||
writer.Write(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +333,11 @@ namespace EonaCat.LogStack.Features
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
File.WriteAllText(filePath, json);
|
||||
using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read | FileShare.Delete))
|
||||
using (var writer = new StreamWriter(stream))
|
||||
{
|
||||
writer.Write(json);
|
||||
}
|
||||
}
|
||||
|
||||
public static LogReplayScenario Load(string filePath)
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace EonaCat.LogStack.Flows
|
||||
/// This creates a hash-chain so any deletion or modification of a past
|
||||
/// entry invalidates all subsequent hashes, making tampering detectable.
|
||||
///
|
||||
/// The file is opened with FileShare.Read only (no concurrent writers).
|
||||
/// The file permits external readers and deletion, but no concurrent writers.
|
||||
/// The flow is synchronous-by-design: audit entries must land on disk
|
||||
/// before the method returns, so <see cref="BlastAsync"/> blocks until
|
||||
/// the entry is flushed.
|
||||
@@ -53,8 +53,8 @@ namespace EonaCat.LogStack.Flows
|
||||
private readonly bool _includeProperties;
|
||||
|
||||
private readonly object _writeLock = new object();
|
||||
private readonly FileStream _stream;
|
||||
private readonly StreamWriter _writer;
|
||||
private FileStream _stream;
|
||||
private StreamWriter _writer;
|
||||
|
||||
private long _sequence;
|
||||
private string _previousHash;
|
||||
@@ -117,19 +117,23 @@ namespace EonaCat.LogStack.Flows
|
||||
string date = DateTime.UtcNow.ToString("yyyyMMdd");
|
||||
_filePath = Path.Combine(directory, $"{filePrefix}_{Environment.MachineName}_{date}.audit");
|
||||
|
||||
// Exclusive write access
|
||||
OpenFile();
|
||||
}
|
||||
|
||||
private void OpenFile()
|
||||
{
|
||||
_stream = new FileStream(
|
||||
_filePath,
|
||||
FileMode.Append,
|
||||
FileAccess.Write,
|
||||
FileShare.Read, // allow external readers, but no other writers
|
||||
FileShare.Read | FileShare.Delete,
|
||||
bufferSize: 4096,
|
||||
FileOptions.WriteThrough); // WriteThrough = no OS cache, hits disk immediately
|
||||
|
||||
_writer = new StreamWriter(_stream, Encoding.UTF8) { AutoFlush = true };
|
||||
|
||||
// Derive starting hash from the last line already in the file (for continuity)
|
||||
_previousHash = ReadLastHash(directory, filePrefix, date);
|
||||
_previousHash = ReadLastHash(_filePath);
|
||||
_sequence = CountExistingLines(_filePath);
|
||||
}
|
||||
|
||||
@@ -153,7 +157,10 @@ namespace EonaCat.LogStack.Flows
|
||||
string previousHash = new string('0', HashLength);
|
||||
long expectedSeq = 1;
|
||||
|
||||
foreach (string raw in File.ReadLines(filePath, Encoding.UTF8))
|
||||
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
string raw;
|
||||
while ((raw = reader.ReadLine()) != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw) || raw.StartsWith("#"))
|
||||
{
|
||||
@@ -252,6 +259,7 @@ namespace EonaCat.LogStack.Flows
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
IsEnabled = false;
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
lock (_writeLock)
|
||||
{
|
||||
try { _writer.Flush(); } catch { }
|
||||
@@ -259,13 +267,19 @@ namespace EonaCat.LogStack.Flows
|
||||
try { _writer.Dispose(); } catch { }
|
||||
try { _stream.Dispose(); } catch { }
|
||||
}
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void WriteEntry(LogEvent log)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
if (!File.Exists(_filePath))
|
||||
{
|
||||
_writer.Dispose();
|
||||
_stream.Dispose();
|
||||
OpenFile();
|
||||
}
|
||||
|
||||
long seq = Interlocked.Increment(ref _sequence);
|
||||
|
||||
var sb = new StringBuilder(256);
|
||||
@@ -356,10 +370,8 @@ namespace EonaCat.LogStack.Flows
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadLastHash(string directory, string prefix, string date)
|
||||
private static string ReadLastHash(string path)
|
||||
{
|
||||
string path = Path.Combine(directory, $"{prefix}_{Environment.MachineName}_{date}.audit");
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return new string('0', HashLength);
|
||||
@@ -368,7 +380,7 @@ namespace EonaCat.LogStack.Flows
|
||||
string lastLine = null;
|
||||
|
||||
// Open file with FileShare.ReadWrite to allow reading while it's being written to
|
||||
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
using (var reader = new StreamReader(fileStream, Encoding.UTF8))
|
||||
{
|
||||
// Read lines from the file
|
||||
@@ -402,7 +414,7 @@ namespace EonaCat.LogStack.Flows
|
||||
long count = 0;
|
||||
|
||||
// Open the file with FileShare.ReadWrite to allow concurrent read/write access
|
||||
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
using (var reader = new StreamReader(fileStream, Encoding.UTF8))
|
||||
{
|
||||
while (!reader.EndOfStream)
|
||||
|
||||
@@ -571,7 +571,7 @@ namespace EonaCat.LogStack.Flows
|
||||
|
||||
try
|
||||
{
|
||||
using (FileStream source = File.OpenRead(encryptedPath))
|
||||
using (FileStream source = new FileStream(encryptedPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
{
|
||||
byte[] magic = new byte[4];
|
||||
ReadExact(source, magic, 4);
|
||||
@@ -604,7 +604,8 @@ namespace EonaCat.LogStack.Flows
|
||||
}
|
||||
|
||||
using (ICryptoTransform dec = aes.CreateDecryptor())
|
||||
using (StreamWriter out_ = new StreamWriter(outputPath, false, Encoding.UTF8))
|
||||
using (FileStream output = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.Read | FileShare.Delete))
|
||||
using (StreamWriter out_ = new StreamWriter(output, Encoding.UTF8))
|
||||
{
|
||||
byte[] buffer = new byte[4];
|
||||
while (source.Position < source.Length)
|
||||
@@ -847,7 +848,7 @@ namespace EonaCat.LogStack.Flows
|
||||
? DateTime.Now.Date
|
||||
: DateTime.UtcNow.Date;
|
||||
|
||||
if (_currentStream == null || _currentDate != today || _currentSize > _maxFileSize)
|
||||
if (_currentStream == null || _currentDate != today || _currentSize > _maxFileSize || !File.Exists(_currentPath))
|
||||
{
|
||||
if (_currentStream != null)
|
||||
{
|
||||
@@ -934,7 +935,7 @@ namespace EonaCat.LogStack.Flows
|
||||
|
||||
bool isNew = !File.Exists(_currentPath) || new FileInfo(_currentPath).Length == 0;
|
||||
|
||||
_currentStream = new FileStream(_currentPath, FileMode.Append, FileAccess.Write, FileShare.Read, 65536, _durableWrites ? FileOptions.WriteThrough : FileOptions.None);
|
||||
_currentStream = new FileStream(_currentPath, FileMode.Append, FileAccess.Write, FileShare.Read | FileShare.Delete, 65536, _durableWrites ? FileOptions.WriteThrough : FileOptions.None);
|
||||
|
||||
byte[] salt = new byte[SaltSize];
|
||||
byte[] iv = new byte[IvSize];
|
||||
@@ -956,7 +957,7 @@ namespace EonaCat.LogStack.Flows
|
||||
else
|
||||
{
|
||||
// Re-read header so we continue the same key/IV session
|
||||
using (FileStream hdr = File.OpenRead(_currentPath))
|
||||
using (FileStream hdr = new FileStream(_currentPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
{
|
||||
hdr.Seek(4, SeekOrigin.Begin);
|
||||
ReadExact(hdr, salt, SaltSize);
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace EonaCat.LogStack.Flows
|
||||
|
||||
private readonly BlockingCollection<LogEvent> _queue;
|
||||
private readonly ConcurrentQueue<string> _compressionQueue = new ConcurrentQueue<string>();
|
||||
private readonly object _archiveCompressionLock = new object();
|
||||
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
||||
private volatile bool _isDisposing; // Track disposal state to prevent accessing disposed objects
|
||||
private readonly Thread _writerThread;
|
||||
@@ -1385,7 +1386,7 @@ namespace EonaCat.LogStack.Flows
|
||||
const string footer = "</logs>";
|
||||
const int tailSize = 512;
|
||||
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete))
|
||||
{
|
||||
if (fs.Length < footer.Length)
|
||||
{
|
||||
@@ -1801,6 +1802,9 @@ namespace EonaCat.LogStack.Flows
|
||||
|
||||
private string ArchiveFile(string filePath)
|
||||
{
|
||||
lock (_archiveCompressionLock)
|
||||
{
|
||||
DrainCompressionQueue();
|
||||
try
|
||||
{
|
||||
string dir = Path.GetDirectoryName(filePath);
|
||||
@@ -1859,6 +1863,7 @@ namespace EonaCat.LogStack.Flows
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CompressionThreadBody()
|
||||
{
|
||||
@@ -1890,6 +1895,8 @@ namespace EonaCat.LogStack.Flows
|
||||
}
|
||||
|
||||
private void DrainCompressionQueue()
|
||||
{
|
||||
lock (_archiveCompressionLock)
|
||||
{
|
||||
while (_compressionQueue.TryDequeue(out string path))
|
||||
{
|
||||
@@ -1900,6 +1907,7 @@ namespace EonaCat.LogStack.Flows
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CompressFile(string path)
|
||||
{
|
||||
@@ -1919,8 +1927,8 @@ namespace EonaCat.LogStack.Flows
|
||||
const int bufSize = 65536;
|
||||
byte[] buffer = new byte[bufSize];
|
||||
|
||||
using (FileStream src = File.OpenRead(path))
|
||||
using (FileStream dst = File.Create(outPath))
|
||||
using (FileStream src = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
|
||||
using (FileStream dst = new FileStream(outPath, FileMode.Create, FileAccess.Write, FileShare.Delete))
|
||||
using (GZipStream gz = new GZipStream(dst, CompressionLevel.Optimal))
|
||||
{
|
||||
int read;
|
||||
@@ -2063,6 +2071,9 @@ namespace EonaCat.LogStack.Flows
|
||||
|
||||
private void ApplyRetention()
|
||||
{
|
||||
lock (_archiveCompressionLock)
|
||||
{
|
||||
DrainCompressionQueue();
|
||||
try
|
||||
{
|
||||
DirectoryInfo dir = new DirectoryInfo(_directory);
|
||||
@@ -2113,6 +2124,7 @@ namespace EonaCat.LogStack.Flows
|
||||
OnException?.Invoke(this, "[FileFlow] Retention error: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private DateTime GetCurrentDate()
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace EonaCat.LogStack.Flows
|
||||
{
|
||||
var targetFileName = GetCurrentFileName();
|
||||
|
||||
if (_currentWriter != null && _currentFileName == targetFileName && !ShouldRotate())
|
||||
if (_currentWriter != null && _currentFileName == targetFileName && !ShouldRotate() && File.Exists(targetFileName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -107,7 +107,7 @@ namespace EonaCat.LogStack.Flows
|
||||
_currentFileName,
|
||||
FileMode.Append,
|
||||
FileAccess.Write,
|
||||
FileShare.Read,
|
||||
FileShare.Read | FileShare.Delete,
|
||||
4096,
|
||||
FileOptions.SequentialScan);
|
||||
|
||||
|
||||
@@ -294,7 +294,8 @@ namespace ServiceMonitoring
|
||||
try
|
||||
{
|
||||
string logFilePath = Path.Combine(_statusDirectory, "status_log.txt");
|
||||
using (var writer = new StreamWriter(logFilePath, append: true))
|
||||
using (var stream = new FileStream(logFilePath, FileMode.Append, FileAccess.Write, FileShare.Read | FileShare.Delete))
|
||||
using (var writer = new StreamWriter(stream))
|
||||
{
|
||||
writer.WriteLine(statusMessage);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public sealed class TcpFlow : FlowBase
|
||||
await EnsureConnectedAsync(cancellationToken);
|
||||
|
||||
// Send file in chunks
|
||||
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
||||
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
{
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace EonaCat.LogStack.Helpers
|
||||
var testFile = Path.Combine(dirPath, ".permission_test_" + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
using (var fs = File.Create(testFile, 1, FileOptions.DeleteOnClose))
|
||||
using (var fs = new FileStream(testFile, FileMode.Create, FileAccess.Write, FileShare.Delete, 1, FileOptions.DeleteOnClose))
|
||||
{
|
||||
fs.WriteByte(0);
|
||||
}
|
||||
|
||||
@@ -698,7 +698,11 @@ namespace EonaCat.LogStack.Server
|
||||
filePath = $"{basePath}_{idx++}.log";
|
||||
}
|
||||
|
||||
File.AppendAllText(filePath, logData + Environment.NewLine);
|
||||
using (var stream = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.Read | FileShare.Delete))
|
||||
using (var writer = new StreamWriter(stream))
|
||||
{
|
||||
writer.WriteLine(logData);
|
||||
}
|
||||
Interlocked.Increment(ref Metrics.TotalWritten);
|
||||
LogWritten?.Invoke(logData);
|
||||
|
||||
|
||||
@@ -1083,6 +1083,9 @@ await using var logger = new LogBuilder("Production")
|
||||
|
||||
Encrypt sensitive logs with AES encryption and password protection.
|
||||
|
||||
Encrypted log files can be deleted while logging is active, including on Windows.
|
||||
The next log entry recreates the file with a fresh encryption header; deleted entries are not restored.
|
||||
|
||||
### Writing Encrypted Logs
|
||||
|
||||
```csharp
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<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="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" PrivateAssets="all" />
|
||||
<ProjectReference Include="..\..\EonaCat.LogStack\EonaCat.LogStack.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,148 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using EonaCat.LogStack.Flows;
|
||||
using Xunit;
|
||||
|
||||
namespace EonaCat.LogStack.FileDeletionTests;
|
||||
|
||||
public sealed class FileDeletionTests : IDisposable
|
||||
{
|
||||
private readonly string _directory = Path.Combine(Path.GetTempPath(), "EonaCat.FileDeletionTests", Guid.NewGuid().ToString("N"));
|
||||
|
||||
public FileDeletionTests()
|
||||
{
|
||||
Directory.CreateDirectory(_directory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EncryptedLogCanBeDeletedAndRecreatedWithAValidNewHeader()
|
||||
{
|
||||
await using var flow = new EncryptedFileFlow(_directory, "test-password", template: "{message}").WithDurableWrites();
|
||||
await WriteEncryptedAsync(flow, "before-deletion");
|
||||
string path = Assert.Single(Directory.GetFiles(_directory, "*.eona"));
|
||||
byte[] originalHeader = ReadHeader(path);
|
||||
|
||||
Assert.Throws<IOException>(() => File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete));
|
||||
File.Delete(path);
|
||||
Assert.False(File.Exists(path));
|
||||
|
||||
await WriteEncryptedAsync(flow, "after-deletion");
|
||||
Assert.True(File.Exists(path));
|
||||
Assert.False(originalHeader.SequenceEqual(ReadHeader(path)));
|
||||
string outputPath = Path.Combine(_directory, "decrypted.txt");
|
||||
Assert.True(EncryptedFileFlow.DecryptToFile(path, outputPath, "test-password"));
|
||||
string contents = File.ReadAllText(outputPath);
|
||||
Assert.Contains("after-deletion", contents);
|
||||
Assert.DoesNotContain("before-deletion", contents);
|
||||
Assert.Equal(0, flow.GetTotalErrors());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EncryptedLogCanResumeAnExistingFile()
|
||||
{
|
||||
byte[] originalHeader;
|
||||
string path;
|
||||
await using (var first = new EncryptedFileFlow(_directory, "test-password", template: "{message}").WithDurableWrites())
|
||||
{
|
||||
await WriteEncryptedAsync(first, "first-session");
|
||||
path = Assert.Single(Directory.GetFiles(_directory, "*.eona"));
|
||||
originalHeader = ReadHeader(path);
|
||||
}
|
||||
|
||||
await using var second = new EncryptedFileFlow(_directory, "test-password", template: "{message}").WithDurableWrites();
|
||||
await WriteEncryptedAsync(second, "second-session");
|
||||
Assert.Equal(originalHeader, ReadHeader(path));
|
||||
string outputPath = Path.Combine(_directory, "decrypted.txt");
|
||||
Assert.True(EncryptedFileFlow.DecryptToFile(path, outputPath, "test-password"));
|
||||
string contents = File.ReadAllText(outputPath);
|
||||
Assert.Contains("first-session", contents);
|
||||
Assert.Contains("second-session", contents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RotatingLogCanBeDeletedAndRecreated()
|
||||
{
|
||||
await using var flow = new RotatingFileFlow(_directory);
|
||||
Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent("before-deletion")));
|
||||
string path = Assert.Single(Directory.GetFiles(_directory));
|
||||
File.Delete(path);
|
||||
Assert.False(File.Exists(path));
|
||||
|
||||
Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent("after-deletion")));
|
||||
string contents = ReadSharedText(path);
|
||||
Assert.Contains("after-deletion", contents);
|
||||
Assert.DoesNotContain("before-deletion", contents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuditLogCanBeDeletedAndRecreatedWithAValidHashChain()
|
||||
{
|
||||
var flow = new AuditFlow(_directory);
|
||||
try
|
||||
{
|
||||
await flow.BlastAsync(CreateEvent("before-deletion"));
|
||||
Assert.Throws<IOException>(() => File.Open(flow.FilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete));
|
||||
File.Delete(flow.FilePath);
|
||||
Assert.False(File.Exists(flow.FilePath));
|
||||
|
||||
await flow.BlastAsync(CreateEvent("after-deletion"));
|
||||
await flow.BlastAsync(CreateEvent("next-entry"));
|
||||
var liveVerification = AuditFlow.Verify(flow.FilePath);
|
||||
Assert.True(liveVerification.ok, liveVerification.reason);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await flow.DisposeAsync();
|
||||
}
|
||||
|
||||
var verification = AuditFlow.Verify(flow.FilePath);
|
||||
Assert.True(verification.ok, verification.reason);
|
||||
string contents = File.ReadAllText(flow.FilePath);
|
||||
Assert.Contains("after-deletion", contents);
|
||||
Assert.Contains("next-entry", contents);
|
||||
Assert.DoesNotContain("before-deletion", contents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlainLogCanBeDeletedWhileOpen()
|
||||
{
|
||||
await using var flow = new FileFlow(_directory);
|
||||
Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent("before-deletion")));
|
||||
Assert.True(SpinWait.SpinUntil(() => flow.GetStats().BytesWritten > 0 || flow.GetTotalErrors() > 0, TimeSpan.FromSeconds(10)), "Plain writer did not complete.");
|
||||
Assert.Equal(0, flow.GetTotalErrors());
|
||||
await flow.FlushAsync();
|
||||
string path = Assert.Single(Directory.GetFiles(_directory, "*.log"));
|
||||
File.Delete(path);
|
||||
Assert.False(File.Exists(path));
|
||||
}
|
||||
|
||||
private static LogEvent CreateEvent(string message) => new LogEventBuilder().WithMessage(message).Build();
|
||||
|
||||
private static async Task WriteEncryptedAsync(EncryptedFileFlow flow, string message)
|
||||
{
|
||||
long previousBytes = flow.GetStats().BytesWritten;
|
||||
Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent(message)));
|
||||
Assert.True(SpinWait.SpinUntil(() => flow.GetStats().BytesWritten > previousBytes || flow.GetTotalErrors() > 0, TimeSpan.FromSeconds(10)), "Encrypted writer did not complete.");
|
||||
Assert.Equal(0, flow.GetTotalErrors());
|
||||
await flow.FlushAsync();
|
||||
}
|
||||
|
||||
private static byte[] ReadHeader(string path)
|
||||
{
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||
byte[] header = new byte[52];
|
||||
stream.ReadExactly(header);
|
||||
return header;
|
||||
}
|
||||
|
||||
private static string ReadSharedText(string path)
|
||||
{
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||
using var reader = new StreamReader(stream);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Directory.Delete(_directory, recursive: true);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user