Compiles but runs strange

This commit is contained in:
2025-11-02 20:24:58 +10:30
parent d1d2c9f41e
commit 09e1924996
30 changed files with 1301 additions and 504 deletions
+35 -69
View File
@@ -1,15 +1,13 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text.Json;
using FileSnapshot;
using TinfoilVibeServer.Models;
namespace TinfoilVibeServer.Services;
/// <summary>
/// Keeps an inmemory snapshot, watches the filesystem for changes, and
/// only reprocesses a file if its hash changed. The snapshot is
/// automatically regenerated when the configuration changes.
/// only reprocesses a file if its hash changed.
/// </summary>
public sealed class SnapshotService : IDisposable
{
@@ -17,67 +15,47 @@ public sealed class SnapshotService : IDisposable
private readonly string _jsonPath;
private readonly string _snapshotPath;
private readonly FileSystemWatcher _watcher;
// path -> CachedFile
private readonly ConcurrentDictionary<string, CachedFile> _cache = new();
private string? _currentSnapshotHash;
public SnapshotService(ConfigManager config)
{
_config = config;
_jsonPath = Path.Combine(AppContext.BaseDirectory, config.Settings.SnapshotFile);
_snapshotPath = Path.Combine(AppContext.BaseDirectory, config.Settings.SnapshotBackupFile);
_jsonPath = Path.Combine(AppContext.BaseDirectory, _config.Settings.SnapshotFile);
_snapshotPath = Path.Combine(AppContext.BaseDirectory, _config.Settings.SnapshotBackupFile);
// Initial snapshot
BuildSnapshot();
// Persist a copy for quick load on next run
BuildSnapshot(); // initial scan
File.WriteAllText(_snapshotPath, JsonSerializer.Serialize(GetSnapshot()));
// File system watcher
_watcher = new FileSystemWatcher
{
Path = string.Join(Path.PathSeparator, config.Settings.RootDirectories),
Path = string.Join(Path.PathSeparator, _config.Settings.RootDirectories),
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName |
NotifyFilters.Size | NotifyFilters.LastWrite
};
_watcher.Created += OnChanged;
_watcher.Changed += OnChanged;
_watcher.Deleted += OnChanged;
_watcher.Renamed += OnRenamed;
_watcher.EnableRaisingEvents = true;
// React to config changes
_config.OnChange += cfg =>
{
// Reinitialise the watcher with the new root directories
_watcher.Path = string.Join(Path.PathSeparator, cfg.RootDirectories);
_watcher.EnableRaisingEvents = true;
BuildSnapshot(); // rebuild everything
BuildSnapshot(); // rebuild everything
PersistSnapshot();
};
}
private sealed record CachedFile(string Path, string Hash, TitleInfo? Title);
#region FileSystemWatcher
#region File system change handlers
private void OnChanged(object? _, FileSystemEventArgs e) =>
ThrottleSnapshotUpdate();
private void OnRenamed(object? _, RenamedEventArgs e)
{
// Treat rename as delete + create
OnChanged(_, new FileSystemEventArgs(WatcherChangeTypes.Deleted, e.OldFullPath, e.OldName));
OnChanged(_, new FileSystemEventArgs(WatcherChangeTypes.Created, e.FullPath, e.Name));
}
private void OnChanged(object? _, FileSystemEventArgs e) => ThrottleSnapshotUpdate();
private void OnRenamed(object? _, RenamedEventArgs e) => ThrottleSnapshotUpdate();
private void ThrottleSnapshotUpdate()
{
// Debounce: only trigger once in a short window
Task.Run(async () =>
{
await Task.Delay(250);
@@ -87,9 +65,8 @@ public sealed class SnapshotService : IDisposable
#endregion
/// <summary>
/// Full rebuild called on startup and on config change.
/// </summary>
#region Snapshot logic
private void BuildSnapshot()
{
var cfg = _config.Settings;
@@ -104,58 +81,33 @@ public sealed class SnapshotService : IDisposable
if (!(cfg.WhitelistExtensions.Contains(ext) || cfg.RomExtensions.Contains(ext)))
continue;
// 1) compute hash
var hash = ComputeHash(file);
// 2) decide if we need to reprocess
// Cache hit?
if (_cache.TryGetValue(file, out var cached) && cached.Hash == hash)
{
// nothing changed use cached title info
entries.Add(new FileEntry(file, new FileInfo(file).Length, hash, cached.Title));
continue;
}
// 3) extract title if applicable
TitleInfo? title = null;
// Extract title if possible
NcaMetadataDto? title = null;
if (cfg.RomExtensions.Contains(ext))
{
title = NSPExtactor.ExtractFromFile(file);
}
else
{
title = ArchiveHandler.TryExtractTitleInfo(file);
}
// 4) update cache
_cache[file] = new CachedFile(file, hash, title);
// 5) add to snapshot
entries.Add(new FileEntry(file, new FileInfo(file).Length, hash, title));
}
}
// Replace the entire snapshot
lock (_cache)
{
// the snapshot itself is not stored in _cache it's only used for the JSON
}
// we keep entries in a local variable for now
_currentSnapshotHash = ComputeSnapshotHash(entries);
File.WriteAllText(_jsonPath, JsonSerializer.Serialize(entries));
}
private static string ComputeSnapshotHash(IEnumerable<FileEntry> entries)
{
var json = JsonSerializer.Serialize(entries);
using var sha256 = SHA256.Create();
return BitConverter.ToString(sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(json))).Replace("-", "").ToLowerInvariant();
}
private void UpdateSnapshot()
{
BuildSnapshot();
PersistSnapshot();
}
private void UpdateSnapshot() => BuildSnapshot();
private void PersistSnapshot()
{
@@ -171,21 +123,35 @@ public sealed class SnapshotService : IDisposable
private static string ComputeHash(string filePath)
{
using var sha256 = SHA256.Create();
using var sha = SHA256.Create();
using var stream = File.OpenRead(filePath);
var hash = sha256.ComputeHash(stream);
var hash = sha.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
private static string ComputeSnapshotHash(IEnumerable<FileEntry> entries)
{
var json = JsonSerializer.Serialize(entries);
using var sha = SHA256.Create();
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(json));
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
#endregion
public IReadOnlyList<FileEntry> GetSnapshot()
{
var json = File.ReadAllText(_jsonPath);
return JsonSerializer.Deserialize<IReadOnlyList<FileEntry>>(json)!;
}
public void Dispose()
public void RebuildSnapshot()
{
_watcher.Dispose();
_config.Dispose();
// Build a fresh snapshot and persist it.
BuildSnapshot(); // private method inside the same class
PersistSnapshot(); // private method inside the same class
}
public void Dispose() => _watcher.Dispose();
private sealed record CachedFile(string Path, string Hash, NcaMetadataDto? Title);
}