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_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_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_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>
+20 -8
View File
@@ -20,6 +20,7 @@ namespace TinfoilVibeServer.Authentication;
/// </summary>
public sealed class AuthStore : IDisposable
{
private readonly ILogger<AuthStore> _logger;
public readonly AuthSettings Settings;
public readonly ConcurrentDictionary<string, Credential> Credentials = new();
@@ -30,8 +31,9 @@ public sealed class AuthStore : IDisposable
private readonly object _sync = new();
private readonly FileSystemWatcher _credentialsWatcher;
public AuthStore()
public AuthStore(ILogger<AuthStore> logger)
{
_logger = logger;
Settings = new AuthSettings(
"credentials.json",
"fingerprints.json",
@@ -61,6 +63,7 @@ public sealed class AuthStore : IDisposable
private void LoadAll()
{
_logger.LogInformation("Loading authentication data from {File}", Settings.CredentialsFile);
// credentials
if (File.Exists(Settings.CredentialsFile))
{
@@ -87,6 +90,8 @@ public sealed class AuthStore : IDisposable
foreach (var ip in arr)
BlacklistIPs.Add(ip);
}
_logger.LogInformation("Loaded {UserCount} users, {FpCount} fingerprints, {IpCount} IPs",
Credentials.Count, Fingerprints.Count, BlacklistIPs.Count);
}
#endregion
@@ -106,7 +111,10 @@ public sealed class AuthStore : IDisposable
private void ReloadCredentials()
{
if (!File.Exists(Settings.CredentialsFile))
{
_logger.LogError("Credentials file {File} does not exist", Settings.CredentialsFile);
return;
}
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
}
}
@@ -167,6 +176,7 @@ public sealed class AuthStore : IDisposable
cred = new Credential(username, PasswordHash: ComputeHash(password), 1, Verified: false);
Credentials[username] = cred;
PersistCredentials();
_logger.LogInformation("Created new user {Username} (verified={Verified})", username, cred.Verified);
var list = Fingerprints.GetOrAdd(username, _ => new List<int>());
if (uid.HasValue && !list.Contains(uid.Value))
@@ -185,6 +195,7 @@ public sealed class AuthStore : IDisposable
{
error = "Invalid password";
IncrementFailed(username, ip);
_logger.LogWarning("Auth failed for {Username} from {IP} {Error}", username, ip, error);
return false;
}
@@ -224,13 +235,14 @@ public sealed class AuthStore : IDisposable
{
var newCount = FailedAttempts.GetOrAdd(username, 0) + 1;
FailedAttempts[username] = newCount;
_logger.LogInformation("Failed attempts for {Username} increased to {Count}", username, newCount);
if (newCount >= Settings.MaxFailedAttempts)
{
BlacklistIPs.Add(ip);
PersistBlacklist();
FailedAttempts[username] = 0;
}
if (newCount < Settings.MaxFailedAttempts) return;
BlacklistIPs.Add(ip);
PersistBlacklist();
FailedAttempts[username] = 0;
_logger.LogWarning("IP {IP} blacklisted after {Count} failures", ip, newCount);
}
#endregion
@@ -17,6 +17,7 @@ public sealed class BasicAuthMiddleware
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";
// 1) IP blacklist
@@ -31,7 +32,9 @@ public sealed class BasicAuthMiddleware
// 2) Authorization header
if (!context.Request.Headers.TryGetValue("Authorization", out var authHeaders))
{
logger.LogWarning("Missing Authorization header from {IP}", ip);
Challenge(context);
logger.LogInformation("Sent 401 challenge to client");
return;
}
@@ -39,6 +42,7 @@ public sealed class BasicAuthMiddleware
if (!authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
Challenge(context);
logger.LogInformation("Sent 401 challenge to client");
return;
}
@@ -51,6 +55,7 @@ public sealed class BasicAuthMiddleware
catch
{
Challenge(context);
logger.LogInformation("Sent 401 challenge to client");
return;
}
@@ -58,6 +63,7 @@ public sealed class BasicAuthMiddleware
if (parts.Length != 2)
{
Challenge(context);
logger.LogInformation("Sent 401 challenge to client");
return;
}
@@ -84,7 +90,7 @@ public sealed class BasicAuthMiddleware
// Authentication succeeded attach username for downstream handlers if needed
context.Items["User"] = username;
logger.LogInformation("User {User} authenticated successfully (UID={UID})", username, uid);
await _next(context);
}
+21 -5
View File
@@ -4,7 +4,9 @@ using TinfoilVibeServer.Services;
using TinfoilVibeServer.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
// -------------------------------------------------------------------
// 1) Configuration read appsettings.json once and expose it via
// ConfigManager (reloads on file change)
@@ -15,11 +17,13 @@ builder.Services.AddSingleton<ConfigManager>();
builder.Services.AddSingleton<NSPExtractor>(sp =>
{
var config = sp.GetRequiredService<ConfigManager>();
var logger = sp.GetRequiredService<ILogger<NSPExtractor>>();
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<AuthStore>();
builder.Services.AddSingleton<AuthStore>(sp =>
new AuthStore(sp.GetRequiredService<ILogger<AuthStore>>()));
builder.Services.AddSingleton<TitleDatabaseService>();
builder.Services.AddSingleton<ArchiveHandler>();
builder.Services.AddSingleton<IndexBuilderService>();
@@ -43,7 +47,19 @@ app.MapControllers(); // routes the /index.json & /download endpoints
app.MapGet("/debug", () => new SnapshotService(
app.Services.GetRequiredService<ConfigManager>(),
app.Services.GetRequiredService<NSPExtractor>(),
app.Services.GetRequiredService<ArchiveHandler>())
app.Services.GetRequiredService<ArchiveHandler>(),
app.Services.GetRequiredService<ILogger<SnapshotService>>())
.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();
+19 -8
View File
@@ -16,10 +16,12 @@ namespace TinfoilVibeServer.Services;
public sealed class ArchiveHandler
{
private readonly NSPExtractor _nspExtractor;
private readonly ILogger<ArchiveHandler> _logger;
public ArchiveHandler(NSPExtractor nspExtractor)
public ArchiveHandler(NSPExtractor nspExtractor, ILogger<ArchiveHandler> logger)
{
_nspExtractor = nspExtractor;
_logger = logger;
}
/// <summary>
@@ -27,20 +29,29 @@ public sealed class ArchiveHandler
/// </summary>
public NcaMetadataWithHash? TryExtractTitleInfo(string filePath)
{
_logger.LogInformation("Examining archive {File} for embedded NSP", filePath);
var ext = Path.GetExtension(filePath).ToLowerInvariant();
try
{
return ext switch
switch (ext)
{
".zip" => HandleZip(filePath),
".7z" => Handle7z(filePath),
".rar" => HandleRar(filePath),
_ => null
};
case ".zip":
return HandleZip(filePath);
case ".7z":
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
return null;
}
@@ -46,10 +46,11 @@ public sealed class IndexBuilderService: IHostedService
string snapshotHash = ComputeSnapshotHash(snapshot);
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;
}
_logger.LogInformation("Building index (snapshot size={Count})", snapshot.Count);
// 3️⃣ Build new index from snapshot entries
var files = snapshot
.Where(e => e.Title != null)
@@ -85,6 +86,7 @@ public sealed class IndexBuilderService: IHostedService
{
if (!File.Exists(_cachePath)) return null;
var json = File.ReadAllText(_cachePath);
_logger.LogInformation("Loaded index cache from {Path}", _cachePath);
return JsonSerializer.Deserialize<IndexCache>(json);
}
+27 -17
View File
@@ -24,10 +24,12 @@ namespace TinfoilVibeServer.Services
public sealed class NSPExtractor
{
private readonly KeySet _keySet;
private readonly ILogger<NSPExtractor> _logger;
public NSPExtractor(KeySet keySet)
public NSPExtractor(KeySet keySet, ILogger<NSPExtractor> logger)
{
_keySet = keySet;
_logger = logger;
}
/// <summary>
@@ -44,6 +46,7 @@ namespace TinfoilVibeServer.Services
/// </summary>
public NcaMetadataWithHash? ExtractFromStream(Stream stream)
{
_logger.LogInformation("Extracting NSP from stream (length={Length}", stream.Length);
if (!IsPfs0FileSystem(stream))
return null;
@@ -69,7 +72,8 @@ namespace TinfoilVibeServer.Services
var nca = new Nca(_keySet, ncaFileStorage);
if (nca.Header.ContentType != NcaContentType.Meta)
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");
int version = nca.Header.Version;
bool isPatch = nca.IsPatch;
@@ -121,8 +125,9 @@ namespace TinfoilVibeServer.Services
partition.Initialize(storage).ThrowIfFailure();
return true;
}
catch
catch (Exception e)
{
_logger.LogError("Failed to extract NSP: {Exception}", e.Message);
return false;
}
}
@@ -130,7 +135,7 @@ namespace TinfoilVibeServer.Services
public string ExtractHashFromStream(Stream nspStream)
{
if (!IsPfs0FileSystem(nspStream))
return null;
return string.Empty;
nspStream.Seek(0, SeekOrigin.Begin);
@@ -151,20 +156,25 @@ namespace TinfoilVibeServer.Services
using var ncaFile = fileRef.Release();
using var ncaFileStorage = new FileStorage(ncaFile);
var nca = new Nca(_keySet, ncaFileStorage);
if (nca.Header.ContentType != NcaContentType.Meta)
continue; // only the meta NCA contains title metadata
try
{
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");
int version = nca.Header.Version;
bool isPatch = nca.IsPatch;
bool isApp = nca.IsProgram && !isPatch;
// Hash the *first* NCA stream the stream we just opened
using var ncaStream = ncaFile.AsStream();
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(ncaStream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
// Hash the *first* NCA stream the stream we just opened
using var ncaStream = ncaFile.AsStream();
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(ncaStream);
var extractHashFromStream = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
_logger.LogInformation("Computed firststream hash {Hash} for {TitleId}", extractHashFromStream,
nca.Header.TitleId);
return extractHashFromStream;
}
catch (Exception e)
{
_logger.LogError("Failed to extract NSP: {Exception}", e.Message);
}
}
return string.Empty;
}
+28 -20
View File
@@ -20,6 +20,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
private readonly ConfigManager _config;
private readonly NSPExtractor _nspExtractor;
private readonly ArchiveHandler _archiveHandler;
private readonly ILogger<SnapshotService> _logger;
private readonly string _jsonPath;
private readonly string _snapshotPath;
private readonly List<FileSystemWatcher> _watchers = new();
@@ -27,11 +28,12 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
private string? _currentSnapshotHash;
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;
_nspExtractor = nspExtractor;
_archiveHandler = archiveHandler;
_logger = logger;
_jsonPath = Path.Combine(AppContext.BaseDirectory, _config.Settings.SnapshotFile);
_snapshotPath = Path.Combine(AppContext.BaseDirectory, _config.Settings.SnapshotBackupFile);
@@ -70,35 +72,34 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
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,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName |
NotifyFilters.Size | NotifyFilters.LastWrite
};
_watcher.Created += OnChanged;
_watcher.Changed += OnChanged;
_watcher.Deleted += OnChanged;
_watcher.Renamed += OnRenamed;
_watcher.EnableRaisingEvents = true;
Path = path,
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;
_watchers.Add(_watcher);
}
_watchers.Add(watcher);
}
#region FileSystemWatcher
private void OnChanged(object? _, FileSystemEventArgs e) => ThrottleSnapshotUpdate();
private void OnRenamed(object? _, RenamedEventArgs e) => ThrottleSnapshotUpdate();
private void OnChanged(object? _, FileSystemEventArgs e) => ThrottleSnapshotUpdate(e);
private void OnRenamed(object? _, RenamedEventArgs e) => ThrottleSnapshotUpdate(e);
private void ThrottleSnapshotUpdate()
private void ThrottleSnapshotUpdate(FileSystemEventArgs fileSystemEventArgs)
{
Task.Run(async () =>
{
await Task.Delay(250);
_logger.LogDebug("File system event {EventType} on {Path}", fileSystemEventArgs.ChangeType, fileSystemEventArgs.FullPath);
UpdateSnapshot();
});
}
@@ -110,8 +111,9 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
private void BuildSnapshot()
{
var cfg = _config.Settings;
_logger.LogInformation("Rebuilding snapshot (root dirs: {Count})", cfg.RootDirectories.Length);
var entries = new List<FileEntry>();
var index = LoadSnapshotIndex(); // < new
var index = LoadSnapshotIndex();
var snapshotChanged = false;
foreach (var dir in cfg.RootDirectories)
@@ -149,11 +151,16 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
title = _archiveHandler.TryExtractTitleInfo(file);
}
if (title == null)
{
_logger.LogInformation("Failed to process {File}", 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));
_logger.LogInformation("Added {File} to snapshot (hash={Hash}", file, hash);
snapshotChanged = true;
}
}
@@ -180,6 +187,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService
var newHash = ComputeSnapshotHash(entries);
if (_currentSnapshotHash != newHash)
{
_logger.LogInformation("Snapshot hash changed persisting new snapshot");
_currentSnapshotHash = newHash;
File.WriteAllText(_jsonPath, JsonSerializer.Serialize(entries));
File.WriteAllText(_snapshotPath, JsonSerializer.Serialize(entries));
@@ -30,6 +30,8 @@ public sealed class TitleDatabaseService : IHostedService
private readonly IMemoryCache _cache;
private readonly ISnapshotService _snapshotService;
private readonly Dictionary<string,string> _titleIdToPath = new Dictionary<string, string>();
// 1️⃣ Cache for the JSON data (key = TitleId)
/*
private readonly ConcurrentDictionary<string, TitleInfoDto> _titleData
@@ -90,9 +92,6 @@ public sealed class TitleDatabaseService : IHostedService
// 1️⃣ Load the JSON (download if not cached).
await ReloadCacheAsync();
// 2️⃣ Scan the filesystem and build the titleid → path map.
BuildFilesystemIndex();
_logger.LogInformation("Title database ready {Count} entries loaded.",
GetAllAsync().Result.Count);
}
@@ -107,7 +106,9 @@ public sealed class TitleDatabaseService : IHostedService
/* ---------------------------------------------------------------- */
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()
.SetSlidingExpiration(TimeSpan.FromSeconds(ttlSec)).RegisterPostEvictionCallback((key, value, reason,
state) =>
@@ -130,6 +131,7 @@ public sealed class TitleDatabaseService : IHostedService
var cacheFile = Path.Combine(_cacheFolder, $"{_options.CurrentValue.CountryCode}.{_options.CurrentValue.LanguageCode}.json");
if (!File.Exists(cacheFile))
{
_logger.LogInformation("Cache miss downloading title DB from {Url}", cacheFile);
await LoadAndCacheTitleDb(CancellationToken.None);
}
@@ -222,6 +224,7 @@ public sealed class TitleDatabaseService : IHostedService
await LoadAndCacheTitleDb(ct);
return;
}
_logger.LogInformation("Cache miss downloading title DB from {Url}", cacheFile);
}
_logger.LogInformation("Downloading title database from {Url}", rawUrl);
@@ -271,47 +274,5 @@ public sealed class TitleDatabaseService : IHostedService
}
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
}