From 1678a1e3b5b76a2c4388dca698cdcafdea119f37 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 14 Nov 2025 10:00:03 +1030 Subject: [PATCH] Use a resource to initialise appsettings.json in config folder Watch for KeySet, initial install will not have a valid value TitleDatabase will use data folder --- TinfoilVibeServer/Authentication/AuthStore.cs | 2 +- TinfoilVibeServer/Program.cs | 26 +++++++ TinfoilVibeServer/Services/ConfigManager.cs | 2 +- TinfoilVibeServer/Services/NSPExtractor.cs | 69 ++++++++++++++----- TinfoilVibeServer/Services/SnapshotService.cs | 16 +++++ .../Services/TitleDatabaseService.cs | 8 +-- TinfoilVibeServer/TinfoilVibeServer.csproj | 3 + TinfoilVibeServer/appsettings.default.json | 39 +++++++++++ 8 files changed, 141 insertions(+), 24 deletions(-) create mode 100644 TinfoilVibeServer/appsettings.default.json diff --git a/TinfoilVibeServer/Authentication/AuthStore.cs b/TinfoilVibeServer/Authentication/AuthStore.cs index ff26200..9adbce7 100644 --- a/TinfoilVibeServer/Authentication/AuthStore.cs +++ b/TinfoilVibeServer/Authentication/AuthStore.cs @@ -61,7 +61,7 @@ public class AuthStore : IDisposable, IAuthStore private static string DetermineCredentialsPath(string? settingsCredentialsFile, IHostEnvironment env) { if (settingsCredentialsFile == null) return Path.Combine("app","data","credentials.json"); - return Path.IsPathRooted(settingsCredentialsFile) ? settingsCredentialsFile : Path.Combine(env.ContentRootPath,"app","data",settingsCredentialsFile); + return Path.IsPathRooted(settingsCredentialsFile) ? settingsCredentialsFile : Path.Combine(env.ContentRootPath,"data",settingsCredentialsFile); } public void Dispose() diff --git a/TinfoilVibeServer/Program.cs b/TinfoilVibeServer/Program.cs index 1db14b2..95ae4ce 100644 --- a/TinfoilVibeServer/Program.cs +++ b/TinfoilVibeServer/Program.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using TinfoilVibeServer.Authentication; @@ -12,6 +13,31 @@ builder.Logging.AddDebug(); builder.Services.AddMemoryCache(); var dataRoot = builder.Configuration["CONFIG_ROOT"] ?? "/app/config/"; +// 1️⃣ Load the embedded default +var defaultResource = typeof(Program).Assembly + .GetManifestResourceStream("TinfoilVibeServer.appsettings.default.json")!; // adjust namespace +var defaultConfig = JsonDocument.Parse(defaultResource).RootElement; + +// 2️⃣ Try to write the file if it doesn't exist +var configPath = Path.Combine(dataRoot, "appsettings.json"); +if (!File.Exists(configPath)) +{ + // write the embedded JSON straight to disk + try + { + File.WriteAllText(configPath, defaultConfig.GetRawText()); + } + catch (Exception e) + { + var tempFactory = LoggerFactory.Create(loggingBuilder => + { + loggingBuilder.AddConsole(); + loggingBuilder.AddDebug(); + }); + var logger = tempFactory.CreateLogger(); + logger.LogError(e, "Failed to write default config file"); + } +} var config = new ConfigurationBuilder() .AddJsonFile(Path.Combine(dataRoot,"appsettings.json"), optional: false, reloadOnChange: true) diff --git a/TinfoilVibeServer/Services/ConfigManager.cs b/TinfoilVibeServer/Services/ConfigManager.cs index 5b44ae3..4fd7a70 100644 --- a/TinfoilVibeServer/Services/ConfigManager.cs +++ b/TinfoilVibeServer/Services/ConfigManager.cs @@ -19,7 +19,7 @@ public class ConfigManager public ConfigManager() { - _configPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json"); + _configPath = Path.Combine(AppContext.BaseDirectory, "config", "appsettings.json"); Load(); _watcher = new FileSystemWatcher diff --git a/TinfoilVibeServer/Services/NSPExtractor.cs b/TinfoilVibeServer/Services/NSPExtractor.cs index b86ca6e..d972519 100644 --- a/TinfoilVibeServer/Services/NSPExtractor.cs +++ b/TinfoilVibeServer/Services/NSPExtractor.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System.ComponentModel.DataAnnotations; +using System.Security.Cryptography; using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; @@ -34,21 +35,49 @@ namespace TinfoilVibeServer.Services /// public sealed class NSPExtractor : INSPExtractor { - private readonly KeySet _keySet; - private readonly ILogger _logger; + private KeySet? _keySet; - public NSPExtractor(IOptions options, ILogger logger, IHostEnvironment environment) + public KeySet? KeySet { - var dataRoot = environment.ContentRootPath ?? "/app/config"; - if (Path.IsPathRooted(options.Value.keyFile)) + get { - _keySet = ExternalKeyReader.ReadKeyFile(options.Value.keyFile); + if (_keySet != null) return _keySet; + if (_options.CurrentValue.KeyFile == null) return null; + var dataRoot = _environment.ContentRootPath ?? "/app/config"; + if (Path.IsPathRooted(_options.CurrentValue.KeyFile)) + { + _keySet = ExternalKeyReader.ReadKeyFile(_options.CurrentValue.KeyFile); + } + else + { + _keySet = ExternalKeyReader.ReadKeyFile(Path.Combine(dataRoot, "config", _options.CurrentValue.KeyFile)); + } + + return _keySet; } - else + } + + private readonly IOptionsMonitor _options; + private readonly ILogger _logger; + private readonly IHostEnvironment _environment; + + public NSPExtractor(IOptionsMonitor options, ILogger logger, IHostEnvironment environment) + { + _options = options; + _options.OnChange(o => { - _keySet = ExternalKeyReader.ReadKeyFile(Path.Combine(dataRoot, "config", options.Value.keyFile)); - } + if (o.KeyFile == null) + { + _logger?.LogInformation("No KeySet specified, skipping key validation"); + } + + if (!File.Exists(o.KeyFile)) + { + _logger?.LogWarning("KeySet file {KeyFile} does not exist", o.KeyFile); + } + }); _logger = logger; + _environment = environment; } /// @@ -65,6 +94,8 @@ namespace TinfoilVibeServer.Services /// public NcaMetadataWithHash? ExtractFromStream(Stream stream) { + if (KeySet == null) return null; + if (!stream.CanSeek) return null; stream.Seek(0, SeekOrigin.Begin); @@ -77,7 +108,7 @@ namespace TinfoilVibeServer.Services if (IsXciFileSystem(stream)) { - var xci = new Xci(_keySet, storage); + var xci = new Xci(KeySet, storage); List ncaEntries; if (xci.HasPartition(XciPartitionType.Secure)) { @@ -95,7 +126,7 @@ namespace TinfoilVibeServer.Services using var ncaFile = fileRef.Release(); using var ncaFileStorage = new FileStorage(ncaFile); - var nca = new Nca(_keySet, ncaFileStorage); + var nca = new Nca(KeySet, ncaFileStorage); if (hash == null) { // Hash the *first* NCA stream – the stream we just opened @@ -122,6 +153,8 @@ namespace TinfoilVibeServer.Services private NcaMetadataWithHash? ExtractNSPFromStream(StreamStorage storage) { + if (KeySet == null) return null; + List ncaEntries; _logger.LogInformation("Processing as NSP"); var partition = new PartitionFileSystem(); @@ -139,7 +172,7 @@ namespace TinfoilVibeServer.Services using var ncaFile = fileRef.Release(); using var ncaFileStorage = new FileStorage(ncaFile); - var nca = new Nca(_keySet, ncaFileStorage); + var nca = new Nca(KeySet, ncaFileStorage); if (hash == null) { // Hash the *first* NCA stream – the stream we just opened @@ -208,6 +241,8 @@ namespace TinfoilVibeServer.Services } private bool IsXciFileSystem(Stream stream) { + if (KeySet == null) return false; + try { if (!stream.CanSeek) return false; @@ -216,7 +251,7 @@ namespace TinfoilVibeServer.Services var storage = new StreamStorage(stream, true); try { - var xciBlock = new Xci(_keySet, storage); + var xciBlock = new Xci(KeySet, storage); _logger.LogInformation("XCI found"); return xciBlock.HasPartition(XciPartitionType.Secure); } @@ -235,6 +270,8 @@ namespace TinfoilVibeServer.Services public string ExtractHashFromStream(Stream nspStream) { + if (KeySet == null) return string.Empty; + if (!IsPfs0FileSystem(nspStream)) return string.Empty; @@ -259,7 +296,7 @@ namespace TinfoilVibeServer.Services try { - var nca = new Nca(_keySet, ncaFileStorage); + var nca = new Nca(KeySet, ncaFileStorage); if (nca.Header.ContentType != NcaContentType.Meta) continue; // only the meta NCA contains title metadata @@ -283,7 +320,7 @@ namespace TinfoilVibeServer.Services public class NSPExtractorOptions { - public string keyFile { get; set; } + public string? KeyFile { get; set; } } /// diff --git a/TinfoilVibeServer/Services/SnapshotService.cs b/TinfoilVibeServer/Services/SnapshotService.cs index ffdbbb9..1801df0 100644 --- a/TinfoilVibeServer/Services/SnapshotService.cs +++ b/TinfoilVibeServer/Services/SnapshotService.cs @@ -528,7 +528,23 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ _logger.LogWarning("Nonexistent entry found: {Path}", fileEntry.Path); continue; } + + var fileContainedInRootDirectories = false; + foreach (var optionsRootDirectory in _options.RootDirectories) + { + if (fileEntry.Path.StartsWith(optionsRootDirectory)) + { + fileContainedInRootDirectories = true; + break; + } + } + if (!fileContainedInRootDirectories) + { + _logger.LogInformation("Entry {Path} is not contained in any root directory", fileEntry.Path); + continue; + }; + if (_options.RomExtensions.Contains(Path.GetExtension(fileEntry.Path))) { if (fileEntry.Path.Contains(ArchivePathSeparator)) diff --git a/TinfoilVibeServer/Services/TitleDatabaseService.cs b/TinfoilVibeServer/Services/TitleDatabaseService.cs index 53bab92..4f93b5b 100644 --- a/TinfoilVibeServer/Services/TitleDatabaseService.cs +++ b/TinfoilVibeServer/Services/TitleDatabaseService.cs @@ -24,9 +24,7 @@ public sealed class TitleDatabaseService : IHostedService private readonly IOptionsMonitor _options; private readonly ILogger _logger; private readonly IHttpClientFactory _httpFactory; - private readonly INSPExtractor _nspExtractor; private readonly string _cacheFolder; // Where the JSON is cached. - private readonly List _rootDirectories; // directories that contain game files private readonly IMemoryCache _cache; private readonly ISnapshotService _snapshotService; @@ -50,7 +48,6 @@ public sealed class TitleDatabaseService : IHostedService /// directories that contain the NSP files. /// public TitleDatabaseService( - IConfiguration configuration, IOptionsMonitor options, ILogger logger, ISnapshotService snapshotService, @@ -62,11 +59,10 @@ public sealed class TitleDatabaseService : IHostedService _logger = logger; _snapshotService = snapshotService; _httpFactory = httpFactory; - _nspExtractor = nspExtractor; _cache = cache; - _cacheFolder = Path.Combine(AppContext.BaseDirectory, "titledb-cache"); - _rootDirectories = new List + _cacheFolder = Path.Combine(AppContext.BaseDirectory, "data", "titledb-cache"); + new List { // You can extend this list – it is the set of directories that // are scanned when the service starts up. diff --git a/TinfoilVibeServer/TinfoilVibeServer.csproj b/TinfoilVibeServer/TinfoilVibeServer.csproj index a209119..36cd17b 100644 --- a/TinfoilVibeServer/TinfoilVibeServer.csproj +++ b/TinfoilVibeServer/TinfoilVibeServer.csproj @@ -58,5 +58,8 @@ + + Always + diff --git a/TinfoilVibeServer/appsettings.default.json b/TinfoilVibeServer/appsettings.default.json new file mode 100644 index 0000000..fa2a160 --- /dev/null +++ b/TinfoilVibeServer/appsettings.default.json @@ -0,0 +1,39 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + + "CredentialsFile": "/app/data/credentials.json", + "FingerprintsFile": "/app/data/fingerprints.json", + "BlacklistFile": "/app/data/blacklist.json", + "MaxFailedAttempts": 5, + "Snapshot" : { + "RootDirectories": [ ], + "ArchiveExtensions": [ ".zip", ".rar", ".7z" ], + "RomExtensions": [ ".xci", ".nsp", ".xcz" ], + "CacheTtl": 60, + "SnapshotFile": "/app/data/snapshot.json", + "SnapshotBackupFile": "/app/data/snapshot.bak" + }, + "NSPExtractor": { + "KeyFile": "/app/config/prod.keys" + }, + "IndexBuilder": { + "ApiBaseUrl": "http://tinfoil.localhost:8080", + "IndexDirectories": [ + "https://url1", + "sdmc:/url2", + "http://url3" + ], + "Success" : "Welcome to Tinfoil Vibe Server!" + }, + "TitleDb": { + "CountryCode": "AU", + "Language": "en", + "TtlSeconds" : 90 + } +} \ No newline at end of file