Use a resource to initialise appsettings.json in config folder (#2)
Watch for KeySet, initial install will not have a valid value TitleDatabase will use data folder Reviewed-on: #2 Co-authored-by: Huy Nguyen <ecenshu@gmail.com> Co-committed-by: Huy Nguyen <ecenshu@gmail.com>
This commit was merged in pull request #2.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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<Program>();
|
||||
logger.LogError(e, "Failed to write default config file");
|
||||
}
|
||||
}
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddJsonFile(Path.Combine(dataRoot,"appsettings.json"), optional: false, reloadOnChange: true)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public sealed class NSPExtractor : INSPExtractor
|
||||
{
|
||||
private readonly KeySet _keySet;
|
||||
private readonly ILogger<INSPExtractor> _logger;
|
||||
private KeySet? _keySet;
|
||||
|
||||
public NSPExtractor(IOptions<NSPExtractorOptions> options, ILogger<INSPExtractor> 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<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 =>
|
||||
{
|
||||
_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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,6 +94,8 @@ namespace TinfoilVibeServer.Services
|
||||
/// </summary>
|
||||
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<DirectoryEntryEx> 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<DirectoryEntryEx> 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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -24,9 +24,7 @@ public sealed class TitleDatabaseService : IHostedService
|
||||
private readonly IOptionsMonitor<TitleDbOptions> _options;
|
||||
private readonly ILogger<TitleDatabaseService> _logger;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly INSPExtractor _nspExtractor;
|
||||
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 ISnapshotService _snapshotService;
|
||||
@@ -50,7 +48,6 @@ public sealed class TitleDatabaseService : IHostedService
|
||||
/// directories that contain the NSP files.
|
||||
/// </summary>
|
||||
public TitleDatabaseService(
|
||||
IConfiguration configuration,
|
||||
IOptionsMonitor<TitleDbOptions> options,
|
||||
ILogger<TitleDatabaseService> 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<string>
|
||||
_cacheFolder = Path.Combine(AppContext.BaseDirectory, "data", "titledb-cache");
|
||||
new List<string>
|
||||
{
|
||||
// You can extend this list – it is the set of directories that
|
||||
// are scanned when the service starts up.
|
||||
|
||||
@@ -58,5 +58,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="obj\**" />
|
||||
<EmbeddedResource Include="appsettings.default.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user