Add file-based startup config & temp dir option

Added support for configuring all major Jellyfin paths (data, config, cache, log, temp, web) via a `startup.json` file, with priority after CLI and environment variables. Introduced a new `TempDir` option, configurable through CLI, env var, or config file, with backward-compatible defaults. Refactored path resolution logic to integrate file-based config and ensure directory creation. Updated constructors and CLI options to support temp directory. Improved EF Core migration error handling and logging. Added comprehensive documentation and example config files. All changes are backward compatible.
This commit is contained in:
2026-02-25 15:18:23 -05:00
parent dad5cbadf5
commit c38adfc0f7
15 changed files with 1764 additions and 22 deletions
+67 -5
View File
@@ -70,6 +70,46 @@ public static class StartupHelpers
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
}
/// <summary>
/// Loads startup configuration from startup.json file if it exists.
/// Searches in current directory, then AppContext.BaseDirectory.
/// </summary>
/// <returns>Configuration root if file exists, null otherwise.</returns>
private static IConfigurationRoot? LoadStartupConfiguration()
{
const string ConfigFileName = "startup.json";
// Search locations in priority order
var searchPaths = new[]
{
Path.Combine(Directory.GetCurrentDirectory(), ConfigFileName),
Path.Combine(AppContext.BaseDirectory, ConfigFileName),
Path.Combine(AppContext.BaseDirectory, "config", ConfigFileName)
};
foreach (var configPath in searchPaths)
{
if (File.Exists(configPath))
{
try
{
var config = new ConfigurationBuilder()
.AddJsonFile(configPath, optional: false, reloadOnChange: false)
.Build();
Console.WriteLine($"Loaded startup configuration from: {configPath}");
return config;
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Failed to load startup configuration from {configPath}: {ex.Message}");
}
}
}
return null;
}
/// <summary>
/// Create the data, config and log paths from the variety of inputs(command line args,
/// environment variables) or decide on what default to use. For Windows it's %AppPath%
@@ -81,17 +121,23 @@ public static class StartupHelpers
/// <returns><see cref="ServerApplicationPaths" />.</returns>
public static ServerApplicationPaths CreateApplicationPaths(StartupOptions options)
{
// Try to load startup configuration from file
var startupConfig = LoadStartupConfiguration();
// LocalApplicationData
// Windows: %LocalAppData%
// macOS: NSApplicationSupportDirectory
// UNIX: $XDG_DATA_HOME
var dataDir = options.DataDir
?? Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR")
?? startupConfig?.GetValue<string>("Paths:DataDir")
?? Path.Join(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify),
"jellyfin");
var configDir = options.ConfigDir ?? Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
var configDir = options.ConfigDir
?? Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR")
?? startupConfig?.GetValue<string>("Paths:ConfigDir");
if (configDir is null)
{
configDir = Path.Join(dataDir, "config");
@@ -107,7 +153,9 @@ public static class StartupHelpers
}
}
var cacheDir = options.CacheDir ?? Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
var cacheDir = options.CacheDir
?? Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR")
?? startupConfig?.GetValue<string>("Paths:CacheDir");
if (cacheDir is null)
{
if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS())
@@ -120,7 +168,9 @@ public static class StartupHelpers
}
}
var webDir = options.WebDir ?? Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
var webDir = options.WebDir
?? Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR")
?? startupConfig?.GetValue<string>("Paths:WebDir");
if (webDir is null)
{
// Look for wwwroot folder in the solution root
@@ -151,18 +201,29 @@ public static class StartupHelpers
}
}
var logDir = options.LogDir ?? Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
var logDir = options.LogDir
?? Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR")
?? startupConfig?.GetValue<string>("Paths:LogDir");
if (logDir is null)
{
logDir = Path.Join(dataDir, "log");
}
var tempDir = options.TempDir
?? Environment.GetEnvironmentVariable("JELLYFIN_TEMP_DIR")
?? startupConfig?.GetValue<string>("Paths:TempDir");
if (tempDir is null)
{
tempDir = Path.Join(Path.GetTempPath(), "jellyfin");
}
// Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162
dataDir = Path.GetFullPath(dataDir);
logDir = Path.GetFullPath(logDir);
configDir = Path.GetFullPath(configDir);
cacheDir = Path.GetFullPath(cacheDir);
webDir = Path.GetFullPath(webDir);
tempDir = Path.GetFullPath(tempDir);
// Ensure the main folders exist before we continue
try
@@ -171,6 +232,7 @@ public static class StartupHelpers
Directory.CreateDirectory(logDir);
Directory.CreateDirectory(configDir);
Directory.CreateDirectory(cacheDir);
Directory.CreateDirectory(tempDir);
}
catch (IOException ex)
{
@@ -179,7 +241,7 @@ public static class StartupHelpers
Environment.Exit(1);
}
return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir);
return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir, tempDir);
}
private static string GetXdgCacheHome()
+4
View File
@@ -13,6 +13,7 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ApplicationIcon>Jellyfin.Server.ico</ApplicationIcon>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
@@ -42,6 +43,9 @@
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Content Remove="Resources\Configuration\startup.default.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" />
@@ -3,7 +3,7 @@
<Project>
<PropertyGroup>
<_PublishTargetUrl>C:\Workspace\jellyfin</_PublishTargetUrl>
<History>True|2026-02-24T14:09:50.1396149Z||;True|2026-02-24T08:35:55.7659458-05:00||;</History>
<History>True|2026-02-25T20:17:02.7084356Z||;True|2026-02-25T09:49:18.6958965-05:00||;True|2026-02-24T09:09:50.1396149-05:00||;True|2026-02-24T08:35:55.7659458-05:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>
@@ -0,0 +1,10 @@
{
"Paths": {
"DataDir": null,
"ConfigDir": null,
"CacheDir": null,
"LogDir": null,
"TempDir": null,
"WebDir": null
}
}
+7
View File
@@ -55,6 +55,13 @@ namespace Jellyfin.Server
[Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")]
public string? LogDir { get; set; }
/// <summary>
/// Gets or sets the path to the temp directory.
/// </summary>
/// <value>The path to the temp directory.</value>
[Option('t', "tempdir", Required = false, HelpText = "Path to use for temporary files.")]
public string? TempDir { get; set; }
/// <inheritdoc />
[Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")]
public string? FFmpegPath { get; set; }