namespace Nonemm.App.Configuration; /// Where the operator's files live: `Documents/N1MM Logger+`, which is N1MM's /// own folder, with N1MM's folders inside it and N1MM's names on them. A /// station that runs both programs keeps one set of files: /// `Wav//Cq.wav` is where N1MM plays that operator's recordings from, /// and so is this. /// /// What N1MM has no place for goes in a `Nonemm` folder of its own, which is /// where the settings file sits. N1MM's three `*DDL` folders and its `Piper` /// folders are not made: those hold the migrations for N1MM's own admin /// databases and the program it speaks with, and neither has anything to do /// with a log. public sealed class UserPaths { public UserPaths(string root) { Root = root; Databases = Path.Combine(root, "Databases"); UserDefinedContests = Path.Combine(root, "UserDefinedContests"); SupportFiles = Path.Combine(root, "SupportFiles"); FunctionKeyMessages = Path.Combine(root, "FunctionKeyMessages"); CallHistoryFiles = Path.Combine(root, "CallHistoryFiles"); ExportFiles = Path.Combine(root, "ExportFiles"); GoalFiles = Path.Combine(root, "GoalFiles"); LettersFiles = Path.Combine(root, "LettersFiles"); QsoRecording = Path.Combine(root, "QsoRecording"); SkinsAndLayouts = Path.Combine(root, "SkinsAndLayouts"); SystemFiles = Path.Combine(root, "SystemFiles"); TransactionLogFiles = Path.Combine(root, "TransactionLogFiles"); Diagnostics = Path.Combine(root, "Diagnostics"); Wav = Path.Combine(root, "Wav"); Own = Path.Combine(root, "Nonemm"); } public static UserPaths Default() => new(DefaultRoot()); public string Root { get; } public string Databases { get; } public string UserDefinedContests { get; } public string SupportFiles { get; } /// The `.mc` function key files, which N1MM imports and exports here. public string FunctionKeyMessages { get; } public string CallHistoryFiles { get; } /// Cabrillo and ADIF, in and out. public string ExportFiles { get; } /// N1MM's rate targets, its letter and number recordings, its recorded /// contacts, its window layouts, its own settings files, its transaction /// logs and its traces. Nothing here writes to them yet; they are made so /// that a station that keeps files in them does not lose them when it moves /// between the two programs. public string GoalFiles { get; } public string LettersFiles { get; } public string QsoRecording { get; } public string SkinsAndLayouts { get; } public string SystemFiles { get; } public string TransactionLogFiles { get; } public string Diagnostics { get; } /// The voice keyer's recordings. Each operator has a folder of his own /// under it, named for his callsign, which is how N1MM finds the voice of /// whoever is at the microphone. public string Wav { get; } /// This program's own folder inside N1MM's, for what N1MM has no place /// for. public string Own { get; } public string SettingsFile => Path.Combine(Own, "settings.json"); public string CountryFile => Path.Combine(SupportFiles, "wl_cty.dat"); public string CallDatabaseFile => Path.Combine(SupportFiles, "MASTER.SCP"); /// The published list of cluster nodes, kept as the page it was downloaded /// from so a later version can read more out of it. public string ClusterListFile => Path.Combine(SupportFiles, "cluster-list.html"); /// One operator's recordings: `Wav/OM3KFF/Cq.wav` and the rest. Without an /// operator the folder itself is the answer, which is where a station with /// one operator keeps them. public string WavFor(string operatorCallsign) => operatorCallsign.Trim().Length == 0 ? Wav : Path.Combine(Wav, operatorCallsign.Trim().ToUpperInvariant()); public void CreateFolders() { foreach (string folder in All()) { Directory.CreateDirectory(folder); } } /// Every folder that is made at startup, the root first. public IReadOnlyList All() => [ Root, Databases, UserDefinedContests, SupportFiles, FunctionKeyMessages, CallHistoryFiles, ExportFiles, GoalFiles, LettersFiles, QsoRecording, SkinsAndLayouts, SystemFiles, TransactionLogFiles, Diagnostics, Wav, Own, ]; /// N1MM's folder name, under Documents. On a system that does not say /// where Documents is — which is most Linux systems until someone sets it — /// it is `~/Documents`, the same place a desktop would put it. private static string DefaultRoot() => Path.Combine(Documents(), "N1MM Logger+"); private static string Documents() { if (Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) is { Length: > 0 } known) { return known; } return Environment.GetEnvironmentVariable("XDG_DOCUMENTS_DIR") is { Length: > 0 } xdg ? xdg : Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents"); } /// Where versions before this one kept everything. Their files are copied /// into N1MM's folders the first time this one runs. public static string LegacyRoot() => OperatingSystem.IsWindows() ? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Nonemm") : Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "nonemm"); /// Copies what an older version kept into these folders, once: it runs only /// while there is no settings file here and there is one there. The older /// folder is left as it was, so nothing is lost if the operator goes back /// to the older version. /// /// Paths inside the settings that pointed into the older folder — the log /// database and the call history file — are moved with it, so the copy that /// is opened is the copy that was brought over. public bool AdoptOlderFiles(string? legacyRoot = null) { string older = legacyRoot ?? LegacyRoot(); string olderSettings = Path.Combine(older, "settings.json"); if (File.Exists(SettingsFile) || !File.Exists(olderSettings) || SameFolder(older, Root)) { return false; } Copy(older, Root); Settings settings = Settings.Load(olderSettings); settings = settings with { DatabasePath = Brought(settings.DatabasePath, older), CallHistoryFile = Brought(settings.CallHistoryFile, older), }; settings.Save(SettingsFile); return true; } /// A file that was inside the older folder is now inside this one, in the /// same place under it. A file kept anywhere else is left where it is. private string Brought(string path, string older) { if (path.Trim().Length == 0 || !path.StartsWith(older + Path.DirectorySeparatorChar, StringComparison.Ordinal)) { return path; } return Path.Combine(Root, path[(older.Length + 1)..]); } private static void Copy(string from, string to) { Directory.CreateDirectory(to); foreach (string file in Directory.GetFiles(from)) { // the settings file is written again afterwards, with the paths // inside it moved if (!Path.GetFileName(file).Equals("settings.json", StringComparison.OrdinalIgnoreCase)) { File.Copy(file, Path.Combine(to, Path.GetFileName(file)), overwrite: false); } } foreach (string folder in Directory.GetDirectories(from)) { Copy(folder, Path.Combine(to, Path.GetFileName(folder))); } } private static bool SameFolder(string one, string other) => string.Equals( Path.TrimEndingDirectorySeparator(Path.GetFullPath(one)), Path.TrimEndingDirectorySeparator(Path.GetFullPath(other)), StringComparison.Ordinal); }