1 Commits

Author SHA1 Message Date
ecenshu 1678a1e3b5 Use a resource to initialise appsettings.json in config folder
ci / build_linux (push) Successful in 12m4s
ci / build_linux (pull_request) Successful in 6m19s
Watch for KeySet, initial install will not have a valid value
TitleDatabase will use data folder
2025-11-14 10:00:03 +10:30
8 changed files with 141 additions and 24 deletions
@@ -61,7 +61,7 @@ public class AuthStore : IDisposable, IAuthStore
private static string DetermineCredentialsPath(string? settingsCredentialsFile, IHostEnvironment env) private static string DetermineCredentialsPath(string? settingsCredentialsFile, IHostEnvironment env)
{ {
if (settingsCredentialsFile == null) return Path.Combine("app","data","credentials.json"); 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() public void Dispose()
+26
View File
@@ -1,3 +1,4 @@
using System.Text.Json;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using TinfoilVibeServer.Authentication; using TinfoilVibeServer.Authentication;
@@ -12,6 +13,31 @@ builder.Logging.AddDebug();
builder.Services.AddMemoryCache(); builder.Services.AddMemoryCache();
var dataRoot = builder.Configuration["CONFIG_ROOT"] ?? "/app/config/"; 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<Program>();
logger.LogError(e, "Failed to write default config file");
}
}
var config = new ConfigurationBuilder() var config = new ConfigurationBuilder()
.AddJsonFile(Path.Combine(dataRoot,"appsettings.json"), optional: false, reloadOnChange: true) .AddJsonFile(Path.Combine(dataRoot,"appsettings.json"), optional: false, reloadOnChange: true)
+1 -1
View File
@@ -19,7 +19,7 @@ public class ConfigManager
public ConfigManager() public ConfigManager()
{ {
_configPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json"); _configPath = Path.Combine(AppContext.BaseDirectory, "config", "appsettings.json");
Load(); Load();
_watcher = new FileSystemWatcher _watcher = new FileSystemWatcher
+51 -14
View File
@@ -1,4 +1,5 @@
using System.Security.Cryptography; using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
using LibHac.Common; using LibHac.Common;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
@@ -34,21 +35,49 @@ namespace TinfoilVibeServer.Services
/// </summary> /// </summary>
public sealed class NSPExtractor : INSPExtractor public sealed class NSPExtractor : INSPExtractor
{ {
private readonly KeySet _keySet; private KeySet? _keySet;
private readonly ILogger<INSPExtractor> _logger;
public NSPExtractor(IOptions<NSPExtractorOptions> options, ILogger<INSPExtractor> logger, IHostEnvironment environment) public KeySet? KeySet
{ {
var dataRoot = environment.ContentRootPath ?? "/app/config"; get
if (Path.IsPathRooted(options.Value.keyFile))
{ {
_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 else
{ {
_keySet = ExternalKeyReader.ReadKeyFile(Path.Combine(dataRoot, "config", options.Value.keyFile)); _keySet = ExternalKeyReader.ReadKeyFile(Path.Combine(dataRoot, "config", _options.CurrentValue.KeyFile));
} }
return _keySet;
}
}
private readonly IOptionsMonitor<NSPExtractorOptions> _options;
private readonly ILogger<INSPExtractor> _logger;
private readonly IHostEnvironment _environment;
public NSPExtractor(IOptionsMonitor<NSPExtractorOptions> options, ILogger<INSPExtractor> logger, IHostEnvironment environment)
{
_options = options;
_options.OnChange(o =>
{
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; _logger = logger;
_environment = environment;
} }
/// <summary> /// <summary>
@@ -65,6 +94,8 @@ namespace TinfoilVibeServer.Services
/// </summary> /// </summary>
public NcaMetadataWithHash? ExtractFromStream(Stream stream) public NcaMetadataWithHash? ExtractFromStream(Stream stream)
{ {
if (KeySet == null) return null;
if (!stream.CanSeek) return null; if (!stream.CanSeek) return null;
stream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Begin);
@@ -77,7 +108,7 @@ namespace TinfoilVibeServer.Services
if (IsXciFileSystem(stream)) if (IsXciFileSystem(stream))
{ {
var xci = new Xci(_keySet, storage); var xci = new Xci(KeySet, storage);
List<DirectoryEntryEx> ncaEntries; List<DirectoryEntryEx> ncaEntries;
if (xci.HasPartition(XciPartitionType.Secure)) if (xci.HasPartition(XciPartitionType.Secure))
{ {
@@ -95,7 +126,7 @@ 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); var nca = new Nca(KeySet, ncaFileStorage);
if (hash == null) if (hash == null)
{ {
// Hash the *first* NCA stream the stream we just opened // Hash the *first* NCA stream the stream we just opened
@@ -122,6 +153,8 @@ namespace TinfoilVibeServer.Services
private NcaMetadataWithHash? ExtractNSPFromStream(StreamStorage storage) private NcaMetadataWithHash? ExtractNSPFromStream(StreamStorage storage)
{ {
if (KeySet == null) return null;
List<DirectoryEntryEx> ncaEntries; List<DirectoryEntryEx> ncaEntries;
_logger.LogInformation("Processing as NSP"); _logger.LogInformation("Processing as NSP");
var partition = new PartitionFileSystem(); var partition = new PartitionFileSystem();
@@ -139,7 +172,7 @@ 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); var nca = new Nca(KeySet, ncaFileStorage);
if (hash == null) if (hash == null)
{ {
// Hash the *first* NCA stream the stream we just opened // Hash the *first* NCA stream the stream we just opened
@@ -208,6 +241,8 @@ namespace TinfoilVibeServer.Services
} }
private bool IsXciFileSystem(Stream stream) private bool IsXciFileSystem(Stream stream)
{ {
if (KeySet == null) return false;
try try
{ {
if (!stream.CanSeek) return false; if (!stream.CanSeek) return false;
@@ -216,7 +251,7 @@ namespace TinfoilVibeServer.Services
var storage = new StreamStorage(stream, true); var storage = new StreamStorage(stream, true);
try try
{ {
var xciBlock = new Xci(_keySet, storage); var xciBlock = new Xci(KeySet, storage);
_logger.LogInformation("XCI found"); _logger.LogInformation("XCI found");
return xciBlock.HasPartition(XciPartitionType.Secure); return xciBlock.HasPartition(XciPartitionType.Secure);
} }
@@ -235,6 +270,8 @@ namespace TinfoilVibeServer.Services
public string ExtractHashFromStream(Stream nspStream) public string ExtractHashFromStream(Stream nspStream)
{ {
if (KeySet == null) return string.Empty;
if (!IsPfs0FileSystem(nspStream)) if (!IsPfs0FileSystem(nspStream))
return string.Empty; return string.Empty;
@@ -259,7 +296,7 @@ namespace TinfoilVibeServer.Services
try try
{ {
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
@@ -283,7 +320,7 @@ namespace TinfoilVibeServer.Services
public class NSPExtractorOptions public class NSPExtractorOptions
{ {
public string keyFile { get; set; } public string? KeyFile { get; set; }
} }
/// <summary> /// <summary>
@@ -529,6 +529,22 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
continue; 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 (_options.RomExtensions.Contains(Path.GetExtension(fileEntry.Path)))
{ {
if (fileEntry.Path.Contains(ArchivePathSeparator)) if (fileEntry.Path.Contains(ArchivePathSeparator))
@@ -24,9 +24,7 @@ public sealed class TitleDatabaseService : IHostedService
private readonly IOptionsMonitor<TitleDbOptions> _options; private readonly IOptionsMonitor<TitleDbOptions> _options;
private readonly ILogger<TitleDatabaseService> _logger; private readonly ILogger<TitleDatabaseService> _logger;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly INSPExtractor _nspExtractor;
private readonly string _cacheFolder; // Where the JSON is cached. private readonly string _cacheFolder; // Where the JSON is cached.
private readonly List<string> _rootDirectories; // directories that contain game files
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly ISnapshotService _snapshotService; private readonly ISnapshotService _snapshotService;
@@ -50,7 +48,6 @@ public sealed class TitleDatabaseService : IHostedService
/// directories that contain the NSP files. /// directories that contain the NSP files.
/// </summary> /// </summary>
public TitleDatabaseService( public TitleDatabaseService(
IConfiguration configuration,
IOptionsMonitor<TitleDbOptions> options, IOptionsMonitor<TitleDbOptions> options,
ILogger<TitleDatabaseService> logger, ILogger<TitleDatabaseService> logger,
ISnapshotService snapshotService, ISnapshotService snapshotService,
@@ -62,11 +59,10 @@ public sealed class TitleDatabaseService : IHostedService
_logger = logger; _logger = logger;
_snapshotService = snapshotService; _snapshotService = snapshotService;
_httpFactory = httpFactory; _httpFactory = httpFactory;
_nspExtractor = nspExtractor;
_cache = cache; _cache = cache;
_cacheFolder = Path.Combine(AppContext.BaseDirectory, "titledb-cache"); _cacheFolder = Path.Combine(AppContext.BaseDirectory, "data", "titledb-cache");
_rootDirectories = new List<string> new List<string>
{ {
// You can extend this list it is the set of directories that // You can extend this list it is the set of directories that
// are scanned when the service starts up. // are scanned when the service starts up.
@@ -58,5 +58,8 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Remove="obj\**" /> <EmbeddedResource Remove="obj\**" />
<EmbeddedResource Include="appsettings.default.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -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
}
}