Additional logging
This commit is contained in:
@@ -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,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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 up‑to‑date – re‑using 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -69,7 +72,8 @@ namespace TinfoilVibeServer.Services
|
|||||||
var nca = new Nca(_keySet, ncaFileStorage);
|
var nca = new Nca(_keySet, ncaFileStorage);
|
||||||
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 first‑stream 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 file‑system and build the title‑id → 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 16‑digit hex (upper‑case).
|
|
||||||
titleId = titleId.ToUpperInvariant();
|
|
||||||
//_titleIdToPath[titleId] = file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user