using System; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using LANCommander.SDK.Helpers; namespace LANCommander.SDK; public static class AppPaths { private static string _configDirectory = String.Empty; public const string DataDirectoryEnvironmentVariable = "LANCOMMANDER_DATA_DIR"; /// /// Builds a full path under the application's config directory. /// /// Additional path segments appended to the config directory. /// The combined path under the config directory. public static string GetConfigPath(params string[] paths) => Path.Combine(GetConfigDirectory(), Path.Combine(paths)); /// /// Resolves a storage location path to an absolute path using a single, consistent rule so that /// every consumer (saves, media, archives, ...) resolves the same way: rooted paths are used as-is, /// while relative paths are resolved beneath the config directory (i.e. next to the server binary). /// /// The configured storage location path (absolute or relative). /// Additional path segments appended to the resolved storage location. /// The absolute path to the storage location (plus any appended segments). public static string ResolveStorageLocationPath(string storageLocationPath, params string[] segments) { if (String.IsNullOrWhiteSpace(storageLocationPath)) throw new ArgumentException("A storage location path must be provided.", nameof(storageLocationPath)); var root = Path.IsPathRooted(storageLocationPath) ? storageLocationPath : Path.Combine(GetConfigDirectory(), storageLocationPath); return segments is { Length: > 0 } ? Path.Combine(new[] { root }.Concat(segments).ToArray()) : root; } /// /// Locates (and creates if necessary) the directory in which application data will be stored. /// Resolution order: the override if set; otherwise a /// "Data" folder under the current working directory when writable; otherwise a "Data" folder under /// the current user's platform-native application data directory. /// /// The resolved config directory path. public static string GetConfigDirectory() { if (!String.IsNullOrWhiteSpace(_configDirectory)) return _configDirectory; var overrideDirectory = Environment.GetEnvironmentVariable(DataDirectoryEnvironmentVariable); if (!String.IsNullOrWhiteSpace(overrideDirectory)) { // Operator-specified data root is used verbatim (no implicit "Data" subfolder). _configDirectory = Path.GetFullPath(overrideDirectory); } else { var baseDirectory = Directory.GetCurrentDirectory(); _configDirectory = DirectoryHelper.IsDirectoryWritable(baseDirectory) ? Path.Combine(baseDirectory, "Data") : Path.Combine(GetAppDataPath(), "Data"); } if (!Directory.Exists(_configDirectory)) Directory.CreateDirectory(_configDirectory); return _configDirectory; } /// /// Gets (and creates if necessary) the base per-user application data directory for the current user, /// scoped by the entry assembly's company and product metadata. Uses the platform-native convention: /// %LOCALAPPDATA% on Windows, ~/Library/Application Support on macOS, and /// $XDG_DATA_HOME (~/.local/share) on Linux. /// /// The application data path for this application. public static string GetAppDataPath() { var (company, product) = GetCompanyAndProduct(); string userRoot; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // .NET maps LocalApplicationData to ~/.local/share on macOS; use the native location instead. userRoot = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support"); else // Windows: %LOCALAPPDATA%. Linux: $XDG_DATA_HOME or ~/.local/share. userRoot = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var appDataPath = Path.Combine(new[] { userRoot, company, product } .Where(segment => !String.IsNullOrWhiteSpace(segment)) .ToArray()!); if (!Directory.Exists(appDataPath)) Directory.CreateDirectory(appDataPath); return appDataPath; } /// /// Checks if the config directory is currently mounted. This should be used in Docker containers only. /// /// true if the config directory is a mount point; otherwise false. public static bool ConfigDirectoryIsMounted() { var path = GetConfigDirectory(); var fullPath = Path.GetFullPath(path); foreach (var line in File.ReadLines("/proc/self/mountinfo")) { // mountinfo format: see `man proc` var parts = line.Split(' '); if (parts.Length > 4) { var mountPoint = parts[4]; if (string.Equals(mountPoint, fullPath, StringComparison.Ordinal)) return true; } } return false; } /// /// Reads company and product metadata from the entry assembly (or executing assembly as a fallback). /// /// A tuple containing company and product strings (may be null if not defined). private static (string? Company, string? Product) GetCompanyAndProduct() { var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); var company = assembly.GetCustomAttribute()?.Company; var product = assembly.GetCustomAttribute()?.Product; return (company, product); } }