Additional logging

This commit is contained in:
2025-11-04 12:28:38 +10:30
parent 6c276f1de3
commit e5787c9321
9 changed files with 133 additions and 106 deletions
+1
View File
@@ -7,6 +7,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileInfo_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F85a5735906a3a06f39a1422a28e0353e3e317f2d923dcc5731fef07dd436f9_003FFileInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileInfo_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F85a5735906a3a06f39a1422a28e0353e3e317f2d923dcc5731fef07dd436f9_003FFileInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileSystemEnumerator_002EWindows_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F326755fc341d349c24999b4c209d69fbe4317313d563859abe51f4ded75c97b_003FFileSystemEnumerator_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileSystemEnumerator_002EWindows_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F326755fc341d349c24999b4c209d69fbe4317313d563859abe51f4ded75c97b_003FFileSystemEnumerator_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileSystemEventArgs_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F36b5c2553dd4c6dccec26adf9d5ab4ff493763447f46751b6775ba38a832_003FFileSystemEventArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileSystemWatcher_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F771dac726bcbf51f1839c45bef2a78c4d834c4bc5420482d9cc3c38eb97535_003FFileSystemWatcher_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileSystemWatcher_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F771dac726bcbf51f1839c45bef2a78c4d834c4bc5420482d9cc3c38eb97535_003FFileSystemWatcher_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileSystemWatcher_002EWin32_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F52fdc6a26ac6d933b95a413e4fb7bc90d22adb6b85734e5eb08036ab03a_003FFileSystemWatcher_002EWin32_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileSystemWatcher_002EWin32_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F52fdc6a26ac6d933b95a413e4fb7bc90d22adb6b85734e5eb08036ab03a_003FFileSystemWatcher_002EWin32_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFile_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F3f31e7e8aa33de883c2ccfa62a9c81bfc246c36e825b489476f9472032e512_003FFile_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFile_002Ecs_002Fl_003AC_0021_003FUsers_003Fecens_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F3f31e7e8aa33de883c2ccfa62a9c81bfc246c36e825b489476f9472032e512_003FFile_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+20 -8
View File
@@ -20,6 +20,7 @@ namespace TinfoilVibeServer.Authentication;
/// </summary> /// </summary>
public sealed class AuthStore : IDisposable public sealed class AuthStore : IDisposable
{ {
private readonly ILogger<AuthStore> _logger;
public readonly AuthSettings Settings; public readonly AuthSettings Settings;
public readonly ConcurrentDictionary<string, Credential> Credentials = new(); public readonly ConcurrentDictionary<string, Credential> Credentials = new();
@@ -30,8 +31,9 @@ public sealed class AuthStore : IDisposable
private readonly object _sync = new(); private readonly object _sync = new();
private readonly FileSystemWatcher _credentialsWatcher; private readonly FileSystemWatcher _credentialsWatcher;
public AuthStore() public AuthStore(ILogger<AuthStore> logger)
{ {
_logger = logger;
Settings = new AuthSettings( Settings = new AuthSettings(
"credentials.json", "credentials.json",
"fingerprints.json", "fingerprints.json",
@@ -61,6 +63,7 @@ public sealed class AuthStore : IDisposable
private void LoadAll() private void LoadAll()
{ {
_logger.LogInformation("Loading authentication data from {File}", Settings.CredentialsFile);
// credentials // credentials
if (File.Exists(Settings.CredentialsFile)) if (File.Exists(Settings.CredentialsFile))
{ {
@@ -87,6 +90,8 @@ public sealed class AuthStore : IDisposable
foreach (var ip in arr) foreach (var ip in arr)
BlacklistIPs.Add(ip); BlacklistIPs.Add(ip);
} }
_logger.LogInformation("Loaded {UserCount} users, {FpCount} fingerprints, {IpCount} IPs",
Credentials.Count, Fingerprints.Count, BlacklistIPs.Count);
} }
#endregion #endregion
@@ -106,7 +111,10 @@ public sealed class AuthStore : IDisposable
private void ReloadCredentials() private void ReloadCredentials()
{ {
if (!File.Exists(Settings.CredentialsFile)) if (!File.Exists(Settings.CredentialsFile))
{
_logger.LogError("Credentials file {File} does not exist", Settings.CredentialsFile);
return; return;
}
try try
{ {
@@ -140,8 +148,9 @@ public sealed class AuthStore : IDisposable
} }
} }
} }
catch catch(Exception ex)
{ {
_logger.LogError(ex, "Failed to reload credentials from {File}", Settings.CredentialsFile);
// ignore malformed JSON or IO error keep old state // ignore malformed JSON or IO error keep old state
} }
} }
@@ -167,6 +176,7 @@ public sealed class AuthStore : IDisposable
cred = new Credential(username, PasswordHash: ComputeHash(password), 1, Verified: false); cred = new Credential(username, PasswordHash: ComputeHash(password), 1, Verified: false);
Credentials[username] = cred; Credentials[username] = cred;
PersistCredentials(); PersistCredentials();
_logger.LogInformation("Created new user {Username} (verified={Verified})", username, cred.Verified);
var list = Fingerprints.GetOrAdd(username, _ => new List<int>()); var list = Fingerprints.GetOrAdd(username, _ => new List<int>());
if (uid.HasValue && !list.Contains(uid.Value)) if (uid.HasValue && !list.Contains(uid.Value))
@@ -185,6 +195,7 @@ public sealed class AuthStore : IDisposable
{ {
error = "Invalid password"; error = "Invalid password";
IncrementFailed(username, ip); IncrementFailed(username, ip);
_logger.LogWarning("Auth failed for {Username} from {IP} {Error}", username, ip, error);
return false; return false;
} }
@@ -224,13 +235,14 @@ public sealed class AuthStore : IDisposable
{ {
var newCount = FailedAttempts.GetOrAdd(username, 0) + 1; var newCount = FailedAttempts.GetOrAdd(username, 0) + 1;
FailedAttempts[username] = newCount; FailedAttempts[username] = newCount;
_logger.LogInformation("Failed attempts for {Username} increased to {Count}", username, newCount);
if (newCount >= Settings.MaxFailedAttempts) if (newCount < Settings.MaxFailedAttempts) return;
{
BlacklistIPs.Add(ip); BlacklistIPs.Add(ip);
PersistBlacklist(); PersistBlacklist();
FailedAttempts[username] = 0; FailedAttempts[username] = 0;
} _logger.LogWarning("IP {IP} blacklisted after {Count} failures", ip, newCount);
} }
#endregion #endregion
@@ -17,6 +17,7 @@ public sealed class BasicAuthMiddleware
public async Task InvokeAsync(HttpContext context, AuthStore store, ILogger<BasicAuthMiddleware> logger) public async Task InvokeAsync(HttpContext context, AuthStore store, ILogger<BasicAuthMiddleware> logger)
{ {
logger.LogInformation("Incoming request from {IP} {Method} {Path}", context.Connection.RemoteIpAddress, context.Request.Method, context.Request.Path);
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
// 1) IP blacklist // 1) IP blacklist
@@ -31,7 +32,9 @@ public sealed class BasicAuthMiddleware
// 2) Authorization header // 2) Authorization header
if (!context.Request.Headers.TryGetValue("Authorization", out var authHeaders)) if (!context.Request.Headers.TryGetValue("Authorization", out var authHeaders))
{ {
logger.LogWarning("Missing Authorization header from {IP}", ip);
Challenge(context); Challenge(context);
logger.LogInformation("Sent 401 challenge to client");
return; return;
} }
@@ -39,6 +42,7 @@ public sealed class BasicAuthMiddleware
if (!authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) if (!authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{ {
Challenge(context); Challenge(context);
logger.LogInformation("Sent 401 challenge to client");
return; return;
} }
@@ -51,6 +55,7 @@ public sealed class BasicAuthMiddleware
catch catch
{ {
Challenge(context); Challenge(context);
logger.LogInformation("Sent 401 challenge to client");
return; return;
} }
@@ -58,6 +63,7 @@ public sealed class BasicAuthMiddleware
if (parts.Length != 2) if (parts.Length != 2)
{ {
Challenge(context); Challenge(context);
logger.LogInformation("Sent 401 challenge to client");
return; return;
} }
@@ -84,7 +90,7 @@ public sealed class BasicAuthMiddleware
// Authentication succeeded attach username for downstream handlers if needed // Authentication succeeded attach username for downstream handlers if needed
context.Items["User"] = username; context.Items["User"] = username;
logger.LogInformation("User {User} authenticated successfully (UID={UID})", username, uid);
await _next(context); await _next(context);
} }
+21 -5
View File
@@ -4,7 +4,9 @@ using TinfoilVibeServer.Services;
using TinfoilVibeServer.Models; using TinfoilVibeServer.Models;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// 1) Configuration read appsettings.json once and expose it via // 1) Configuration read appsettings.json once and expose it via
// ConfigManager (reloads on file change) // ConfigManager (reloads on file change)
@@ -15,11 +17,13 @@ builder.Services.AddSingleton<ConfigManager>();
builder.Services.AddSingleton<NSPExtractor>(sp => builder.Services.AddSingleton<NSPExtractor>(sp =>
{ {
var config = sp.GetRequiredService<ConfigManager>(); var config = sp.GetRequiredService<ConfigManager>();
var logger = sp.GetRequiredService<ILogger<NSPExtractor>>();
var keySet = KeySetHolder.KeySet; // already loaded by ConfigManager var keySet = KeySetHolder.KeySet; // already loaded by ConfigManager
return new NSPExtractor(keySet); return new NSPExtractor(keySet, logger);
}); });
builder.Services.AddSingleton<ISnapshotService, SnapshotService>(); builder.Services.AddSingleton<ISnapshotService, SnapshotService>();
builder.Services.AddSingleton<AuthStore>(); builder.Services.AddSingleton<AuthStore>(sp =>
new AuthStore(sp.GetRequiredService<ILogger<AuthStore>>()));
builder.Services.AddSingleton<TitleDatabaseService>(); builder.Services.AddSingleton<TitleDatabaseService>();
builder.Services.AddSingleton<ArchiveHandler>(); builder.Services.AddSingleton<ArchiveHandler>();
builder.Services.AddSingleton<IndexBuilderService>(); builder.Services.AddSingleton<IndexBuilderService>();
@@ -43,7 +47,19 @@ app.MapControllers(); // routes the /index.json & /download endpoints
app.MapGet("/debug", () => new SnapshotService( app.MapGet("/debug", () => new SnapshotService(
app.Services.GetRequiredService<ConfigManager>(), app.Services.GetRequiredService<ConfigManager>(),
app.Services.GetRequiredService<NSPExtractor>(), app.Services.GetRequiredService<NSPExtractor>(),
app.Services.GetRequiredService<ArchiveHandler>()) app.Services.GetRequiredService<ArchiveHandler>(),
app.Services.GetRequiredService<ILogger<SnapshotService>>())
.GetSnapshot()); .GetSnapshot());
app.Lifetime.ApplicationStarted.Register(() =>
app.Services.GetRequiredService<ILogger<Program>>().LogInformation("Application started. Listening on {Urls}", string.Join(", ", app.Urls)));
app.Use(async (ctx, next) =>
{
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var correlationId = ctx.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
using (logger.BeginScope("CorrelationId:{CorrelationId}", correlationId))
{
await next();
}
});
app.Run(); app.Run();
+19 -8
View File
@@ -16,10 +16,12 @@ namespace TinfoilVibeServer.Services;
public sealed class ArchiveHandler public sealed class ArchiveHandler
{ {
private readonly NSPExtractor _nspExtractor; private readonly NSPExtractor _nspExtractor;
private readonly ILogger<ArchiveHandler> _logger;
public ArchiveHandler(NSPExtractor nspExtractor) public ArchiveHandler(NSPExtractor nspExtractor, ILogger<ArchiveHandler> logger)
{ {
_nspExtractor = nspExtractor; _nspExtractor = nspExtractor;
_logger = logger;
} }
/// <summary> /// <summary>
@@ -27,20 +29,29 @@ public sealed class ArchiveHandler
/// </summary> /// </summary>
public NcaMetadataWithHash? TryExtractTitleInfo(string filePath) public NcaMetadataWithHash? TryExtractTitleInfo(string filePath)
{ {
_logger.LogInformation("Examining archive {File} for embedded NSP", filePath);
var ext = Path.GetExtension(filePath).ToLowerInvariant(); var ext = Path.GetExtension(filePath).ToLowerInvariant();
try try
{ {
return ext switch switch (ext)
{ {
".zip" => HandleZip(filePath), case ".zip":
".7z" => Handle7z(filePath), return HandleZip(filePath);
".rar" => HandleRar(filePath), case ".7z":
_ => null return Handle7z(filePath);
}; case ".rar":
return HandleRar(filePath);
default:
{
_logger.LogWarning("Unsupported archive type {Extension} skipping", ext);
return null;
}
}
} }
catch catch (Exception ex)
{ {
_logger.LogError("Error opening archive {File}: {Exception}", filePath, ex.Message);
// Graceful fallback return null // Graceful fallback return null
return null; return null;
} }
@@ -46,10 +46,11 @@ public sealed class IndexBuilderService: IHostedService
string snapshotHash = ComputeSnapshotHash(snapshot); string snapshotHash = ComputeSnapshotHash(snapshot);
if (cached != null && cached.SnapshotHash == snapshotHash) if (cached != null && cached.SnapshotHash == snapshotHash)
{ {
_logger.LogInformation("Index cache is uptodate reusing it."); _logger.LogInformation("Using cached index (snapshot hash={Hash}", snapshotHash);
return cached.Index; return cached.Index;
} }
_logger.LogInformation("Building index (snapshot size={Count})", snapshot.Count);
// 3️⃣ Build new index from snapshot entries // 3️⃣ Build new index from snapshot entries
var files = snapshot var files = snapshot
.Where(e => e.Title != null) .Where(e => e.Title != null)
@@ -85,6 +86,7 @@ public sealed class IndexBuilderService: IHostedService
{ {
if (!File.Exists(_cachePath)) return null; if (!File.Exists(_cachePath)) return null;
var json = File.ReadAllText(_cachePath); var json = File.ReadAllText(_cachePath);
_logger.LogInformation("Loaded index cache from {Path}", _cachePath);
return JsonSerializer.Deserialize<IndexCache>(json); return JsonSerializer.Deserialize<IndexCache>(json);
} }
+26 -16
View File
@@ -24,10 +24,12 @@ namespace TinfoilVibeServer.Services
public sealed class NSPExtractor public sealed class NSPExtractor
{ {
private readonly KeySet _keySet; private readonly KeySet _keySet;
private readonly ILogger<NSPExtractor> _logger;
public NSPExtractor(KeySet keySet) public NSPExtractor(KeySet keySet, ILogger<NSPExtractor> logger)
{ {
_keySet = keySet; _keySet = keySet;
_logger = logger;
} }
/// <summary> /// <summary>
@@ -44,6 +46,7 @@ namespace TinfoilVibeServer.Services
/// </summary> /// </summary>
public NcaMetadataWithHash? ExtractFromStream(Stream stream) public NcaMetadataWithHash? ExtractFromStream(Stream stream)
{ {
_logger.LogInformation("Extracting NSP from stream (length={Length}", stream.Length);
if (!IsPfs0FileSystem(stream)) if (!IsPfs0FileSystem(stream))
return null; return null;
@@ -70,6 +73,7 @@ namespace TinfoilVibeServer.Services
if (nca.Header.ContentType != NcaContentType.Meta) if (nca.Header.ContentType != NcaContentType.Meta)
continue; // only the meta NCA contains title metadata continue; // only the meta NCA contains title metadata
_logger.LogInformation("Meta NCA found TitleId={TitleId} Version={Version}", nca.Header.TitleId, nca.Header.Version);
string titleId = nca.Header.TitleId.ToString("X16"); string titleId = nca.Header.TitleId.ToString("X16");
int version = nca.Header.Version; int version = nca.Header.Version;
bool isPatch = nca.IsPatch; bool isPatch = nca.IsPatch;
@@ -121,8 +125,9 @@ namespace TinfoilVibeServer.Services
partition.Initialize(storage).ThrowIfFailure(); partition.Initialize(storage).ThrowIfFailure();
return true; return true;
} }
catch catch (Exception e)
{ {
_logger.LogError("Failed to extract NSP: {Exception}", e.Message);
return false; return false;
} }
} }
@@ -130,7 +135,7 @@ namespace TinfoilVibeServer.Services
public string ExtractHashFromStream(Stream nspStream) public string ExtractHashFromStream(Stream nspStream)
{ {
if (!IsPfs0FileSystem(nspStream)) if (!IsPfs0FileSystem(nspStream))
return null; return string.Empty;
nspStream.Seek(0, SeekOrigin.Begin); nspStream.Seek(0, SeekOrigin.Begin);
@@ -151,20 +156,25 @@ namespace TinfoilVibeServer.Services
using var ncaFile = fileRef.Release(); using var ncaFile = fileRef.Release();
using var ncaFileStorage = new FileStorage(ncaFile); using var ncaFileStorage = new FileStorage(ncaFile);
var nca = new Nca(_keySet, ncaFileStorage); try
if (nca.Header.ContentType != NcaContentType.Meta) {
continue; // only the meta NCA contains title metadata var nca = new Nca(_keySet, ncaFileStorage);
if (nca.Header.ContentType != NcaContentType.Meta)
continue; // only the meta NCA contains title metadata
string titleId = nca.Header.TitleId.ToString("X16"); // Hash the *first* NCA stream the stream we just opened
int version = nca.Header.Version; using var ncaStream = ncaFile.AsStream();
bool isPatch = nca.IsPatch; using var sha256 = SHA256.Create();
bool isApp = nca.IsProgram && !isPatch; var hash = sha256.ComputeHash(ncaStream);
var extractHashFromStream = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
// Hash the *first* NCA stream the stream we just opened _logger.LogInformation("Computed firststream hash {Hash} for {TitleId}", extractHashFromStream,
using var ncaStream = ncaFile.AsStream(); nca.Header.TitleId);
using var sha256 = SHA256.Create(); return extractHashFromStream;
var hash = sha256.ComputeHash(ncaStream); }
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); catch (Exception e)
{
_logger.LogError("Failed to extract NSP: {Exception}", e.Message);
}
} }
return string.Empty; return string.Empty;
} }
+28 -20
View File
@@ -20,6 +20,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
private readonly ConfigManager _config; private readonly ConfigManager _config;
private readonly NSPExtractor _nspExtractor; private readonly NSPExtractor _nspExtractor;
private readonly ArchiveHandler _archiveHandler; private readonly ArchiveHandler _archiveHandler;
private readonly ILogger<SnapshotService> _logger;
private readonly string _jsonPath; private readonly string _jsonPath;
private readonly string _snapshotPath; private readonly string _snapshotPath;
private readonly List<FileSystemWatcher> _watchers = new(); private readonly List<FileSystemWatcher> _watchers = new();
@@ -27,11 +28,12 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
private string? _currentSnapshotHash; private string? _currentSnapshotHash;
public event EventHandler? SnapshotRebuilt; public event EventHandler? SnapshotRebuilt;
public SnapshotService(ConfigManager config, NSPExtractor nspExtractor, ArchiveHandler archiveHandler) public SnapshotService(ConfigManager config, NSPExtractor nspExtractor, ArchiveHandler archiveHandler, ILogger<SnapshotService> logger)
{ {
_config = config; _config = config;
_nspExtractor = nspExtractor; _nspExtractor = nspExtractor;
_archiveHandler = archiveHandler; _archiveHandler = archiveHandler;
_logger = logger;
_jsonPath = Path.Combine(AppContext.BaseDirectory, _config.Settings.SnapshotFile); _jsonPath = Path.Combine(AppContext.BaseDirectory, _config.Settings.SnapshotFile);
_snapshotPath = Path.Combine(AppContext.BaseDirectory, _config.Settings.SnapshotBackupFile); _snapshotPath = Path.Combine(AppContext.BaseDirectory, _config.Settings.SnapshotBackupFile);
@@ -70,35 +72,34 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
private void InitializeFileSystemWatcher(string path) private void InitializeFileSystemWatcher(string path)
{ {
if (Directory.Exists(path)) if (!Directory.Exists(path)) return;
var watcher = new FileSystemWatcher
{ {
var _watcher = new FileSystemWatcher Path = path,
{ IncludeSubdirectories = true,
Path = path, NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName |
IncludeSubdirectories = true, NotifyFilters.Size | NotifyFilters.LastWrite
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | };
NotifyFilters.Size | NotifyFilters.LastWrite watcher.Created += OnChanged;
}; watcher.Changed += OnChanged;
_watcher.Created += OnChanged; watcher.Deleted += OnChanged;
_watcher.Changed += OnChanged; watcher.Renamed += OnRenamed;
_watcher.Deleted += OnChanged; watcher.EnableRaisingEvents = true;
_watcher.Renamed += OnRenamed;
_watcher.EnableRaisingEvents = true;
_watchers.Add(_watcher); _watchers.Add(watcher);
}
} }
#region FileSystemWatcher #region FileSystemWatcher
private void OnChanged(object? _, FileSystemEventArgs e) => ThrottleSnapshotUpdate(); private void OnChanged(object? _, FileSystemEventArgs e) => ThrottleSnapshotUpdate(e);
private void OnRenamed(object? _, RenamedEventArgs e) => ThrottleSnapshotUpdate(); private void OnRenamed(object? _, RenamedEventArgs e) => ThrottleSnapshotUpdate(e);
private void ThrottleSnapshotUpdate() private void ThrottleSnapshotUpdate(FileSystemEventArgs fileSystemEventArgs)
{ {
Task.Run(async () => Task.Run(async () =>
{ {
await Task.Delay(250); await Task.Delay(250);
_logger.LogDebug("File system event {EventType} on {Path}", fileSystemEventArgs.ChangeType, fileSystemEventArgs.FullPath);
UpdateSnapshot(); UpdateSnapshot();
}); });
} }
@@ -110,8 +111,9 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
private void BuildSnapshot() private void BuildSnapshot()
{ {
var cfg = _config.Settings; var cfg = _config.Settings;
_logger.LogInformation("Rebuilding snapshot (root dirs: {Count})", cfg.RootDirectories.Length);
var entries = new List<FileEntry>(); var entries = new List<FileEntry>();
var index = LoadSnapshotIndex(); // < new var index = LoadSnapshotIndex();
var snapshotChanged = false; var snapshotChanged = false;
foreach (var dir in cfg.RootDirectories) foreach (var dir in cfg.RootDirectories)
@@ -149,11 +151,16 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
title = _archiveHandler.TryExtractTitleInfo(file); title = _archiveHandler.TryExtractTitleInfo(file);
} }
if (title == null)
{
_logger.LogInformation("Failed to process {File}", file);
}
// 4) update cache // 4) update cache
_cache[file] = new CachedFile(file, hash, title); _cache[file] = new CachedFile(file, hash, title);
// 5) add to snapshot // 5) add to snapshot
entries.Add(new FileEntry(file, new FileInfo(file).Length, hash, title)); entries.Add(new FileEntry(file, new FileInfo(file).Length, hash, title));
_logger.LogInformation("Added {File} to snapshot (hash={Hash}", file, hash);
snapshotChanged = true; snapshotChanged = true;
} }
} }
@@ -180,6 +187,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
var newHash = ComputeSnapshotHash(entries); var newHash = ComputeSnapshotHash(entries);
if (_currentSnapshotHash != newHash) if (_currentSnapshotHash != newHash)
{ {
_logger.LogInformation("Snapshot hash changed persisting new snapshot");
_currentSnapshotHash = newHash; _currentSnapshotHash = newHash;
File.WriteAllText(_jsonPath, JsonSerializer.Serialize(entries)); File.WriteAllText(_jsonPath, JsonSerializer.Serialize(entries));
File.WriteAllText(_snapshotPath, JsonSerializer.Serialize(entries)); File.WriteAllText(_snapshotPath, JsonSerializer.Serialize(entries));
@@ -30,6 +30,8 @@ public sealed class TitleDatabaseService : IHostedService
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly ISnapshotService _snapshotService; private readonly ISnapshotService _snapshotService;
private readonly Dictionary<string,string> _titleIdToPath = new Dictionary<string, string>();
// 1️⃣ Cache for the JSON data (key = TitleId) // 1️⃣ Cache for the JSON data (key = TitleId)
/* /*
private readonly ConcurrentDictionary<string, TitleInfoDto> _titleData private readonly ConcurrentDictionary<string, TitleInfoDto> _titleData
@@ -90,9 +92,6 @@ public sealed class TitleDatabaseService : IHostedService
// 1️⃣ Load the JSON (download if not cached). // 1️⃣ Load the JSON (download if not cached).
await ReloadCacheAsync(); await ReloadCacheAsync();
// 2️⃣ Scan the filesystem and build the titleid → path map.
BuildFilesystemIndex();
_logger.LogInformation("Title database ready {Count} entries loaded.", _logger.LogInformation("Title database ready {Count} entries loaded.",
GetAllAsync().Result.Count); GetAllAsync().Result.Count);
} }
@@ -107,7 +106,9 @@ public sealed class TitleDatabaseService : IHostedService
/* ---------------------------------------------------------------- */ /* ---------------------------------------------------------------- */
private async Task ReloadCacheAsync() private async Task ReloadCacheAsync()
{ {
var ttlSec = _options.CurrentValue.TtlSeconds; var ttlSec = _options.CurrentValue.TtlSeconds;
_logger.LogInformation("Reloading title database cache (TTL={TTL}s).", ttlSec);
var entryOptions = new MemoryCacheEntryOptions() var entryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(ttlSec)).RegisterPostEvictionCallback((key, value, reason, .SetSlidingExpiration(TimeSpan.FromSeconds(ttlSec)).RegisterPostEvictionCallback((key, value, reason,
state) => state) =>
@@ -130,6 +131,7 @@ public sealed class TitleDatabaseService : IHostedService
var cacheFile = Path.Combine(_cacheFolder, $"{_options.CurrentValue.CountryCode}.{_options.CurrentValue.LanguageCode}.json"); var cacheFile = Path.Combine(_cacheFolder, $"{_options.CurrentValue.CountryCode}.{_options.CurrentValue.LanguageCode}.json");
if (!File.Exists(cacheFile)) if (!File.Exists(cacheFile))
{ {
_logger.LogInformation("Cache miss downloading title DB from {Url}", cacheFile);
await LoadAndCacheTitleDb(CancellationToken.None); await LoadAndCacheTitleDb(CancellationToken.None);
} }
@@ -222,6 +224,7 @@ public sealed class TitleDatabaseService : IHostedService
await LoadAndCacheTitleDb(ct); await LoadAndCacheTitleDb(ct);
return; return;
} }
_logger.LogInformation("Cache miss downloading title DB from {Url}", cacheFile);
} }
_logger.LogInformation("Downloading title database from {Url}", rawUrl); _logger.LogInformation("Downloading title database from {Url}", rawUrl);
@@ -271,47 +274,5 @@ public sealed class TitleDatabaseService : IHostedService
} }
return titleData; return titleData;
} }
/// <summary>
/// Scan the configured root directories and create the
/// <c>_titleIdToPath</c> map. If the file name does not contain a
/// TitleId, we extract it from the NSP file.
/// </summary>
private void BuildFilesystemIndex()
{
foreach (var root in _rootDirectories)
{
if (!Directory.Exists(root)) continue;
foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
{
if (file.EndsWith(".nsp", StringComparison.OrdinalIgnoreCase))
{
// 1️⃣ Does the file name already contain a TitleId?
var match = _titleIdRegex.Match(Path.GetFileName(file));
string? titleId;
if (match.Success)
{
titleId = match.Groups[1].Value + match.Groups[2].Value;
}
else
{
// 2️⃣ Extract the TitleId from the NSP using the extractor.
titleId = _nspExtractor.ExtractFromStream(File.OpenRead(file))?.TitleId;
if (string.IsNullOrWhiteSpace(titleId))
{
_logger.LogWarning("Could not extract TitleId from {File}", file);
continue;
}
}
// Normalise to 16digit hex (uppercase).
titleId = titleId.ToUpperInvariant();
//_titleIdToPath[titleId] = file;
}
}
}
}
#endregion #endregion
} }