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_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,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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 up‑to‑date – re‑using 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 first‑stream 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;
|
||||
}
|
||||
|
||||
@@ -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 file‑system and build the title‑id → 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 16‑digit hex (upper‑case).
|
||||
titleId = titleId.ToUpperInvariant();
|
||||
//_titleIdToPath[titleId] = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user