diff --git a/README.md b/README.md index 182a97d..7d55d79 100644 --- a/README.md +++ b/README.md @@ -402,9 +402,9 @@ of a degree. ### Where the files live -The program keeps its files under `Documents\Nonemm` on Windows and -`~/.config/nonemm` elsewhere. The folders inside are N1MM's, with N1MM's names, -so a station that runs both keeps its files in the same shape: +The program keeps its files in N1MM's own folder, `Documents/N1MM Logger+`, +with N1MM's folders inside it under N1MM's names. A station that runs both +programs has one set of files rather than two: | Folder | What goes in it | |---|---| @@ -416,10 +416,20 @@ so a station that runs both keeps its files in the same shape: | `ExportFiles` | Cabrillo and ADIF, in and out | | `Wav` | the voice keyer's recordings, one folder per operator: `Wav/OM3KFF/Cq.wav` | | `GoalFiles`, `LettersFiles`, `QsoRecording`, `SkinsAndLayouts`, `SystemFiles`, `TransactionLogFiles`, `Diagnostics` | N1MM's, and made for the same reason: a station that keeps files in them does not lose them moving between the two programs. Nothing here writes to them yet | +| `Nonemm` | this program's own, for what N1MM has no place for: `settings.json` | -`settings.json` sits in the root. 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. +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. + +On a system that does not say where Documents is — most Linux systems, until +someone sets `XDG_DOCUMENTS_DIR` — it is `~/Documents/N1MM Logger+`. + +A version before this one kept everything in `Documents\Nonemm` or +`~/.config/nonemm`. Those files are copied into N1MM's folders the first time +this version runs, and the log and call history paths in the settings are moved +with them, so the copy that opens is the copy that came over. The older folder +is left as it was. ### Operators diff --git a/src/Nonemm.App/App.axaml.cs b/src/Nonemm.App/App.axaml.cs index d1c5a40..1f0c6f8 100644 --- a/src/Nonemm.App/App.axaml.cs +++ b/src/Nonemm.App/App.axaml.cs @@ -18,6 +18,7 @@ public partial class App : Application { UserPaths paths = UserPaths.Default(); paths.CreateFolders(); + paths.AdoptOlderFiles(); Settings settings = Settings.Load(paths.SettingsFile); Themes.Use(settings.Theme); Themes.Changed += (_, _) => ApplyTheme(); diff --git a/src/Nonemm.App/Configuration/UserPaths.cs b/src/Nonemm.App/Configuration/UserPaths.cs index ea878aa..0fbdc73 100644 --- a/src/Nonemm.App/Configuration/UserPaths.cs +++ b/src/Nonemm.App/Configuration/UserPaths.cs @@ -1,14 +1,16 @@ namespace Nonemm.App.Configuration; -/// Where the operator's files live. Windows keeps them under Documents the way -/// N1MM does; everywhere else follows the XDG configuration directory. +/// 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. /// -/// The folders inside are N1MM's own, with N1MM's names, so a station that runs -/// both programs keeps its files in the same shape: `Wav//Cq.wav` is -/// where N1MM looks for that operator's recordings, and so is this. N1MM's -/// three `*DDL` folders and its `Piper` folders are left out: 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. +/// 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) @@ -28,6 +30,7 @@ public sealed class UserPaths 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()); @@ -72,7 +75,11 @@ public sealed class UserPaths /// whoever is at the microphone. public string Wav { get; } - public string SettingsFile => Path.Combine(Root, "settings.json"); + /// 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"); @@ -116,9 +123,30 @@ public sealed class UserPaths TransactionLogFiles, Diagnostics, Wav, + Own, ]; - private static string DefaultRoot() => + /// 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), @@ -126,4 +154,67 @@ public sealed class UserPaths : 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); } diff --git a/tests/Nonemm.App.Tests/UserPathsTests.cs b/tests/Nonemm.App.Tests/UserPathsTests.cs index 945c5c0..34455d3 100644 --- a/tests/Nonemm.App.Tests/UserPathsTests.cs +++ b/tests/Nonemm.App.Tests/UserPathsTests.cs @@ -26,8 +26,8 @@ public class UserPathsTests : IDisposable Assert.Equal( [ "CallHistoryFiles", "Databases", "Diagnostics", "ExportFiles", "FunctionKeyMessages", - "GoalFiles", "LettersFiles", "QsoRecording", "SkinsAndLayouts", "SupportFiles", - "SystemFiles", "TransactionLogFiles", "UserDefinedContests", "Wav", + "GoalFiles", "LettersFiles", "Nonemm", "QsoRecording", "SkinsAndLayouts", + "SupportFiles", "SystemFiles", "TransactionLogFiles", "UserDefinedContests", "Wav", ], made); } @@ -53,4 +53,45 @@ public class UserPathsTests : IDisposable Assert.True(Directory.Exists(paths.Wav)); } + + /// What N1MM has no place for lives in a folder of this program's own. + [Fact] + public void TheSettingsFileIsInOurOwnFolder() + { + UserPaths paths = new(root); + + Assert.Equal(Path.Combine(root, "Nonemm", "settings.json"), paths.SettingsFile); + } + + [Fact] + public void AnOlderVersionsFilesAreCopiedInOnce() + { + string older = Path.Combine(root, "older"); + Directory.CreateDirectory(Path.Combine(older, "Databases")); + File.WriteAllText(Path.Combine(older, "Databases", "om5m.s3db"), "log"); + string database = Path.Combine(older, "Databases", "om5m.s3db"); + new Settings { DatabasePath = database }.Save(Path.Combine(older, "settings.json")); + UserPaths paths = new(Path.Combine(root, "new")); + paths.CreateFolders(); + + Assert.True(paths.AdoptOlderFiles(older)); + + Assert.Equal("log", File.ReadAllText(Path.Combine(paths.Databases, "om5m.s3db"))); + // the log that was opened is the log that was brought over + Assert.Equal( + Path.Combine(paths.Databases, "om5m.s3db"), + Settings.Load(paths.SettingsFile).DatabasePath); + Assert.False(paths.AdoptOlderFiles(older)); + // the older folder is left as it was + Assert.True(File.Exists(database)); + } + + [Fact] + public void NothingIsCopiedWhenThereIsNoOlderFolder() + { + UserPaths paths = new(root); + paths.CreateFolders(); + + Assert.False(paths.AdoptOlderFiles(Path.Combine(root, "nowhere"))); + } }