From 41bcfa908d33d611b8cf8d04d52e2e2fd68262e5 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 24 Jul 2026 18:00:32 -0500 Subject: [PATCH] Unify path resolution server-wide --- .../ArchiveService.cs | 3 - LANCommander.Server.Services/ModuleService.cs | 2 + LANCommander.Server.Services/ScriptService.cs | 2 + LANCommander.Server.Services/SetupService.cs | 7 +- LANCommander.Server.Services/UpdateService.cs | 15 ++-- .../Endpoints/LauncherEndpoints.cs | 3 +- .../Endpoints/UploadEndpoints.cs | 3 - .../Background/DownloadLauncherArtifacts.cs | 5 +- .../AlignSettingsStoragePathsMigration.cs | 89 +++++++++++++++++++ LANCommander.Server/Startup/Filesystem.cs | 11 ++- LANCommander.Server/Startup/Migrations.cs | 3 + 11 files changed, 122 insertions(+), 21 deletions(-) create mode 100644 LANCommander.Server/Migrations/AlignSettingsStoragePathsMigration.cs diff --git a/LANCommander.Server.Services/ArchiveService.cs b/LANCommander.Server.Services/ArchiveService.cs index 19dc0b24..d35fd0ef 100644 --- a/LANCommander.Server.Services/ArchiveService.cs +++ b/LANCommander.Server.Services/ArchiveService.cs @@ -189,9 +189,6 @@ namespace LANCommander.Server.Services { var storageLocation = await storageLocationService.GetAsync(storageLocationId); - if (!Directory.Exists(storageLocation.Path)) - Directory.CreateDirectory(storageLocation.Path); - var archive = new Archive { ObjectKey = Guid.NewGuid().ToString(), diff --git a/LANCommander.Server.Services/ModuleService.cs b/LANCommander.Server.Services/ModuleService.cs index c796332b..40d54273 100644 --- a/LANCommander.Server.Services/ModuleService.cs +++ b/LANCommander.Server.Services/ModuleService.cs @@ -67,6 +67,8 @@ namespace LANCommander.Server.Services }); } + storagePath = AppPaths.ResolveStorageLocationPath(storagePath); + if (!Directory.Exists(storagePath)) Directory.CreateDirectory(storagePath); diff --git a/LANCommander.Server.Services/ScriptService.cs b/LANCommander.Server.Services/ScriptService.cs index 57630138..80eb40f3 100644 --- a/LANCommander.Server.Services/ScriptService.cs +++ b/LANCommander.Server.Services/ScriptService.cs @@ -136,6 +136,8 @@ namespace LANCommander.Server.Services }); } + storagePath = AppPaths.ResolveStorageLocationPath(storagePath); + if (!Directory.Exists(storagePath)) Directory.CreateDirectory(storagePath); diff --git a/LANCommander.Server.Services/SetupService.cs b/LANCommander.Server.Services/SetupService.cs index e63b77e2..56cb8377 100644 --- a/LANCommander.Server.Services/SetupService.cs +++ b/LANCommander.Server.Services/SetupService.cs @@ -140,8 +140,11 @@ namespace LANCommander.Server.Services foreach (var storageLocation in storageLocations) { - if (!Directory.Exists(storageLocation.Path)) - Directory.CreateDirectory(storageLocation.Path); + // Store the configured (possibly relative) path, but create the resolved physical directory. + var resolvedPath = AppPaths.ResolveStorageLocationPath(storageLocation.Path); + + if (!Directory.Exists(resolvedPath)) + Directory.CreateDirectory(resolvedPath); try { diff --git a/LANCommander.Server.Services/UpdateService.cs b/LANCommander.Server.Services/UpdateService.cs index d756e8c9..c354f065 100644 --- a/LANCommander.Server.Services/UpdateService.cs +++ b/LANCommander.Server.Services/UpdateService.cs @@ -42,8 +42,9 @@ namespace LANCommander.Server.Services public IEnumerable GetLauncherArtifactsFromLocalFiles() { var currentVersion = versionProvider.GetCurrentVersion(); - var downloadedLaunchers = Directory.GetFiles(_settingsProvider.CurrentValue.Server.Launcher.StoragePath, $"LANCommander.Launcher*v{currentVersion.WithoutMetadata()}.*"); - var downloadedInstallers = Directory.GetFiles(_settingsProvider.CurrentValue.Server.Launcher.StoragePath, $"LANCommander.Launcher-{currentVersion.WithoutMetadata()}*Setup*.*"); + var launcherStoragePath = AppPaths.ResolveStorageLocationPath(_settingsProvider.CurrentValue.Server.Launcher.StoragePath); + var downloadedLaunchers = Directory.GetFiles(launcherStoragePath, $"LANCommander.Launcher*v{currentVersion.WithoutMetadata()}.*"); + var downloadedInstallers = Directory.GetFiles(launcherStoragePath, $"LANCommander.Launcher-{currentVersion.WithoutMetadata()}*Setup*.*"); var downloads = downloadedLaunchers.Concat(downloadedInstallers).Distinct(); @@ -149,7 +150,7 @@ namespace LANCommander.Server.Services client.DownloadFileCompleted += ReleaseDownloaded; client.QueryString.Add("Version", release.TagName); - await client.DownloadFileTaskAsync(new Uri(releaseFile), Path.Combine(_settingsProvider.CurrentValue.Server.Update.StoragePath, $"{release.TagName}.zip")); + await client.DownloadFileTaskAsync(new Uri(releaseFile), AppPaths.ResolveStorageLocationPath(_settingsProvider.CurrentValue.Server.Update.StoragePath, $"{release.TagName}.zip")); } } @@ -181,7 +182,7 @@ namespace LANCommander.Server.Services var uri = new Uri(releaseFile); client.QueryString.Add("Version", release.TagName); - await client.DownloadFileTaskAsync(uri, Path.Combine(_settingsProvider.CurrentValue.Server.Update.StoragePath, $"{release.TagName}.zip")); + await client.DownloadFileTaskAsync(uri, AppPaths.ResolveStorageLocationPath(_settingsProvider.CurrentValue.Server.Update.StoragePath, $"{release.TagName}.zip")); } } } @@ -216,7 +217,7 @@ namespace LANCommander.Server.Services private void ReleaseDownloaded(object? sender, System.ComponentModel.AsyncCompletedEventArgs e) { string version = ((WebClient)sender).QueryString["Version"]; - string path = Path.Combine(_settingsProvider.CurrentValue.Server.Update.StoragePath, $"{version}.zip"); + string path = AppPaths.ResolveStorageLocationPath(_settingsProvider.CurrentValue.Server.Update.StoragePath, $"{version}.zip"); _logger?.LogInformation("Update version {Version} has been downloaded", version); @@ -242,7 +243,7 @@ namespace LANCommander.Server.Services var process = new ProcessStartInfo(); process.FileName = processExecutable; - process.Arguments = $"-Version {version} -Path \"{_settingsProvider.CurrentValue.Server.Update.StoragePath}\" -Executable {Process.GetCurrentProcess().MainModule.FileName}"; + process.Arguments = $"-Version {version} -Path \"{AppPaths.ResolveStorageLocationPath(_settingsProvider.CurrentValue.Server.Update.StoragePath)}\" -Executable {Process.GetCurrentProcess().MainModule.FileName}"; process.UseShellExecute = true; Process.Start(process); @@ -260,7 +261,7 @@ namespace LANCommander.Server.Services public LauncherArtifact GetLauncherArtifact(string objectKey) { - string name = Path.Combine(_settingsProvider.CurrentValue.Server.Launcher.StoragePath, objectKey); + string name = AppPaths.ResolveStorageLocationPath(_settingsProvider.CurrentValue.Server.Launcher.StoragePath, objectKey); return GetArtifactFromName(name); } } diff --git a/LANCommander.Server/Endpoints/LauncherEndpoints.cs b/LANCommander.Server/Endpoints/LauncherEndpoints.cs index ec0c797c..bc531750 100644 --- a/LANCommander.Server/Endpoints/LauncherEndpoints.cs +++ b/LANCommander.Server/Endpoints/LauncherEndpoints.cs @@ -1,3 +1,4 @@ +using LANCommander.SDK; using LANCommander.SDK.Models; using LANCommander.Server.Services; using LANCommander.Server.Services.Abstractions; @@ -23,7 +24,7 @@ public static class LauncherEndpoints { var version = versionProvider.GetCurrentVersion(); var fileName = $"LANCommander.Launcher-Windows-x64-v{version.WithoutMetadata()}.zip"; - var path = Path.Combine(settingsProvider.CurrentValue.Server.Launcher.StoragePath, fileName); + var path = AppPaths.ResolveStorageLocationPath(settingsProvider.CurrentValue.Server.Launcher.StoragePath, fileName); if (!File.Exists(path) || !settingsProvider.CurrentValue.Server.Launcher.HostUpdates) { diff --git a/LANCommander.Server/Endpoints/UploadEndpoints.cs b/LANCommander.Server/Endpoints/UploadEndpoints.cs index 5b6ba13c..bf72979a 100644 --- a/LANCommander.Server/Endpoints/UploadEndpoints.cs +++ b/LANCommander.Server/Endpoints/UploadEndpoints.cs @@ -28,9 +28,6 @@ public static class UploadEndpoints storageLocationId == null || storageLocationId == Guid.Empty ? null : storageLocationId, SDK.Enums.StorageLocationType.Archive); - if (!Directory.Exists(storageLocation.Path)) - Directory.CreateDirectory(storageLocation.Path); - var archive = new Archive { ObjectKey = Guid.NewGuid().ToString(), diff --git a/LANCommander.Server/Jobs/Background/DownloadLauncherArtifacts.cs b/LANCommander.Server/Jobs/Background/DownloadLauncherArtifacts.cs index a4088856..be709d09 100644 --- a/LANCommander.Server/Jobs/Background/DownloadLauncherArtifacts.cs +++ b/LANCommander.Server/Jobs/Background/DownloadLauncherArtifacts.cs @@ -1,4 +1,5 @@ -using LANCommander.SDK.Extensions; +using LANCommander.SDK; +using LANCommander.SDK.Extensions; using LANCommander.Server.Services; using LANCommander.Server.Services.Models; using Microsoft.Extensions.Options; @@ -28,7 +29,7 @@ namespace LANCommander.Server.Jobs.Background if (!localArtifacts.Any(a => a.Name.EndsWith(artifact.Name))) { using (var downloadStream = await httpClient.GetStreamAsync(artifact.Url)) - using (var fs = new FileStream(Path.Combine(settings.Value.Server.Launcher.StoragePath, artifact.Name), FileMode.Create)) + using (var fs = new FileStream(AppPaths.ResolveStorageLocationPath(settings.Value.Server.Launcher.StoragePath, artifact.Name), FileMode.Create)) { await downloadStream.CopyToAsync(fs); op.Complete(); diff --git a/LANCommander.Server/Migrations/AlignSettingsStoragePathsMigration.cs b/LANCommander.Server/Migrations/AlignSettingsStoragePathsMigration.cs new file mode 100644 index 00000000..799d5a27 --- /dev/null +++ b/LANCommander.Server/Migrations/AlignSettingsStoragePathsMigration.cs @@ -0,0 +1,89 @@ +using LANCommander.SDK; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.Migrations; +using Semver; + +namespace LANCommander.Server.Migrations; + +/// +/// Moves the settings-based storage directories (Update, Launcher, Backups, Snippets, Modules) from +/// their previously-written raw locations (resolved verbatim relative to the working directory) to the +/// unified location produced by . +/// This aligns runtime writes with the same resolution rule used everywhere else so relative paths land +/// under the config directory instead of next to the binary. +/// +public class AlignSettingsStoragePathsMigration( + SettingsProvider settingsProvider, + ILogger logger) : FileSystemMigration(logger) +{ + public override SemVersion Version => new(2, 1, 0); + + private IEnumerable GetConfiguredPaths() + { + var settings = settingsProvider.CurrentValue; + + yield return settings.Server.Update.StoragePath; + yield return settings.Server.Launcher.StoragePath; + yield return settings.Server.Backups.StoragePath; + yield return settings.Server.Scripts.Snippets.StoragePath; + yield return settings.Server.Scripts.Modules.StoragePath; + } + + private static (string Source, string Destination)? GetMove(string configuredPath) + { + if (string.IsNullOrWhiteSpace(configuredPath)) + return null; + + // Rooted paths already resolve verbatim, so there is nothing to move. + if (Path.IsPathRooted(configuredPath)) + return null; + + var source = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), configuredPath)); + var destination = AppPaths.ResolveStorageLocationPath(configuredPath); + + if (string.Equals(source, destination, StringComparison.Ordinal)) + return null; + + return (source, destination); + } + + public override async Task ExecuteAsync() + { + foreach (var configuredPath in GetConfiguredPaths()) + { + var move = GetMove(configuredPath); + + if (move == null) + continue; + + var (source, destination) = move.Value; + + try + { + if (!Directory.Exists(source)) + continue; + + Logger.LogInformation("Moving storage directory from \"{Source}\" to \"{Destination}\"", source, destination); + + DirectoryHelper.MoveContents(source, destination); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error while moving storage directory from \"{Source}\" to \"{Destination}\"", source, destination); + } + } + } + + public override Task ShouldExecuteAsync() + { + foreach (var configuredPath in GetConfiguredPaths()) + { + var move = GetMove(configuredPath); + + if (move != null && Directory.Exists(move.Value.Source)) + return Task.FromResult(true); + } + + return Task.FromResult(false); + } +} diff --git a/LANCommander.Server/Startup/Filesystem.cs b/LANCommander.Server/Startup/Filesystem.cs index 7ac0d11b..fb09dc0c 100644 --- a/LANCommander.Server/Startup/Filesystem.cs +++ b/LANCommander.Server/Startup/Filesystem.cs @@ -23,9 +23,14 @@ public static class Filesystem foreach (var directory in directories) { - logger.LogDebug("Ensuring directory {Directory} exists", directory); - if (!Directory.Exists(directory)) - Directory.CreateDirectory(directory); + if (string.IsNullOrWhiteSpace(directory)) + continue; + + var resolved = AppPaths.ResolveStorageLocationPath(directory); + + logger.LogDebug("Ensuring directory {Directory} exists", resolved); + if (!Directory.Exists(resolved)) + Directory.CreateDirectory(resolved); } return app; diff --git a/LANCommander.Server/Startup/Migrations.cs b/LANCommander.Server/Startup/Migrations.cs index ee12e58a..16aa8906 100644 --- a/LANCommander.Server/Startup/Migrations.cs +++ b/LANCommander.Server/Startup/Migrations.cs @@ -29,6 +29,9 @@ public static class Migrations builder.Services.AddScoped(); builder.Services.AddScoped(); + // Align settings-based storage paths with the unified resolver + builder.Services.AddScoped(); + return builder; }