fix: keep per-user data out of the Velopack install root #94

Merged
KofTwentyTwo merged 1 commits from fix/GH-91-data-root-2 into main 2026-10-10 19:30:48 +00:00
8 changed files with 87 additions and 16 deletions

No files matched your search

+1 -1
View File
@@ -110,7 +110,7 @@ On launch, gclo shows a brief branded splash, then the **Quick Sync** connect ca
**Settings** (File > Settings…) let you set a default GitHub token (stored in the Windows Credential Manager), the default target folder (with a folder picker), the default parallelism, and the theme (System, Light, or Dark); an Advanced section toggles the startup splash and its duration.
**Activity log:** View > Activity log… shows the tail of today's log and links to the logs folder. Logs live under `%LOCALAPPDATA%\gclo\logs`, one file per day; they record run parameters and per-repo failures and never contain your token (see [SECURITY.md](SECURITY.md)).
**Activity log:** View > Activity log… shows the tail of today's log and links to the logs folder. Logs live under `%LOCALAPPDATA%\KofTwentyTwo\gclo\logs`, one file per day; they record run parameters and per-repo failures and never contain your token (see [SECURITY.md](SECURITY.md)).
**Updates:** Help > Check for updates… downloads and applies the latest release for your channel. Self-update is only available in installed builds (not the portable zip or a local debug build).
+2 -1
View File
@@ -26,7 +26,7 @@ GitHub.
| GitHub REST API | Lists organizations and repositories | Untrusted input (responses are parsed), authenticated by the PAT |
| Git remotes (`github.com`) | Clone and fetch over HTTPS via libgit2 | Untrusted input (repository content, including file paths) |
| Windows Credential Manager | Stores saved-account and default tokens | Trusted store; boundary is the Windows user account |
| Local data directory | `%LOCALAPPDATA%\gclo` (or `GCLO_DATA_DIR`): settings, accounts, logs | Trusted; same user |
| Local data directory | `%LOCALAPPDATA%\KofTwentyTwo\gclo` (or `GCLO_DATA_DIR`): settings, accounts, logs. Deliberately outside the Velopack install root `%LOCALAPPDATA%\gclo`, which Setup.exe deletes on reinstall (#91) | Trusted; same user |
| Target folder | Where repositories are written; may already contain repositories | Partly untrusted (pre-existing content, `.git` config) |
| WSL default distribution | Runs `git clone` for the "Clone in WSL" recovery | Semi-trusted (user-installed Linux environment) |
| Update feed | GitHub Releases of this repository, `releases.<channel>.json` | Untrusted until verified (HTTPS, package hashes, attestations) |
@@ -112,3 +112,4 @@ flowchart LR
| 2026-10-09 | v1.0.1 | James Maes (@KofTwentyTwo) | Initial model, covering the path-recovery, WSL clone, vault scoping, package-manager, and SBOM changes of 1.0.1 |
| 2026-10-09 | v1.0.1 | James Maes (@KofTwentyTwo) | T11 updated: releases build in the shared standards workflow; publishing tokens moved to the `release` environment (#76) |
| 2026-10-10 | v1.0.2 | James Maes (@KofTwentyTwo) | T11: the packaging PR is opened with a short-lived release GitHub App token (contents + pull requests on this repository, no bypass) instead of a long-lived PAT; the commit is GitHub-signed (#85) |
| 2026-10-10 | v1.0.2 | James Maes (@KofTwentyTwo) | Local data directory moved to `%LOCALAPPDATA%\KofTwentyTwo\gclo`: the Velopack install root is deleted by Setup.exe, which destroyed accounts.json and settings.json (#91) |
+15 -2
View File
@@ -128,18 +128,31 @@ public sealed class ActivityLogTests : IDisposable
[Fact]
public void DefaultDirectory_IsLocalAppDataGcloLogs()
public void DefaultDirectory_IsLocalAppDataKofTwentyTwoGcloLogs()
{
var log = new FileActivityLog();
string expected = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"gclo", "logs");
"KofTwentyTwo", "gclo", "logs");
Assert.Equal(expected, log.LogDirectory);
}
[Fact]
public void DefaultDirectory_IsNotInsideTheVelopackInstallRoot()
{
// Setup.exe deletes %LOCALAPPDATA%\gclo (the install root) on reinstall (#91).
string installRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "gclo");
Assert.False(
new FileActivityLog().LogDirectory.StartsWith(installRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Directory_IsCreatedLazily_OnFirstWriteNotInConstructor()
{
+44
View File
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2026 James Maes (KofTwentyTwo)
* SPDX-License-Identifier: MIT
*/
using gclo.ViewModels;
namespace gclo.Engine.Tests;
/// <summary>
/// Where gclo keeps its per-user data. The default must stay outside the Velopack
/// install root (%LOCALAPPDATA%\gclo): Setup.exe uninstalls an existing install
/// first and deletes that folder, which destroyed accounts.json and settings.json
/// in 1.0.2-beta.1 (#91).
/// </summary>
public sealed class GcloPathsTests
{
/// <summary>The default root is %LOCALAPPDATA%\KofTwentyTwo\gclo.</summary>
[Fact]
public void DefaultDataRoot_IsLocalAppDataKofTwentyTwoGclo()
{
string expected = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"KofTwentyTwo", "gclo");
Assert.Equal(expected, GcloPaths.DefaultDataRoot);
}
/// <summary>The default root is never the install root nor anything under it.</summary>
[Fact]
public void DefaultDataRoot_IsNotTheVelopackInstallRoot()
{
string installRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "gclo");
Assert.NotEqual(installRoot, GcloPaths.DefaultDataRoot, StringComparer.OrdinalIgnoreCase);
Assert.False(
GcloPaths.DefaultDataRoot.StartsWith(installRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase));
}
}
+4 -2
View File
@@ -25,14 +25,16 @@ public sealed class FileActivityLog : IActivityLog
/// <summary>Creates a log that writes under <paramref name="directory"/>.</summary>
/// <param name="directory">
/// Folder for the log files; defaults to %LOCALAPPDATA%\gclo\logs when null or blank.
/// Folder for the log files; defaults to %LOCALAPPDATA%\KofTwentyTwo\gclo\logs
/// when null or blank (the same data root as the app; never the Velopack
/// install root %LOCALAPPDATA%\gclo, which Setup.exe deletes, #91).
/// </param>
public FileActivityLog(string? directory = null)
{
LogDirectory = string.IsNullOrWhiteSpace(directory)
? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"gclo", "logs")
"KofTwentyTwo", "gclo", "logs")
: directory;
}
+1 -1
View File
@@ -12,7 +12,7 @@ namespace gclo.ViewModels;
/// <summary>
/// Persists account profiles as JSON at accounts.json under
/// <see cref="GcloPaths.DataRoot"/> (default %LOCALAPPDATA%\gclo) and their
/// <see cref="GcloPaths.DataRoot"/> (default %LOCALAPPDATA%\KofTwentyTwo\gclo) and their
/// tokens in an <see cref="ITokenVault"/>; the file holds metadata only and never a
/// token. Unlike <see cref="AppSettings"/>, accounts are primary user data, so write
/// failures in <see cref="Save"/>, <see cref="Delete"/>, and
+1 -1
View File
@@ -11,7 +11,7 @@ namespace gclo.ViewModels;
/// <summary>
/// User preferences persisted as JSON at settings.json under
/// <see cref="GcloPaths.DataRoot"/> (default %LOCALAPPDATA%\gclo, overridable via
/// <see cref="GcloPaths.DataRoot"/> (default %LOCALAPPDATA%\KofTwentyTwo\gclo, overridable via
/// GCLO_DATA_DIR). Uses Environment.GetFolderPath (not Windows.Storage.ApplicationData)
/// so the same path works whether the app runs packaged or unpackaged.
/// </summary>
+19 -8
View File
@@ -8,17 +8,23 @@ namespace gclo.ViewModels;
/// <summary>
/// The single seam for where gclo keeps its per-user data (settings, accounts,
/// logs). Defaults to %LOCALAPPDATA%\gclo; the GCLO_DATA_DIR environment variable
/// overrides it so UI tests (and portable setups) can point the whole app at an
/// isolated directory without touching the real profile. Components that accept
/// an explicit directory (test seams) are unaffected — this only feeds defaults.
/// logs). Defaults to %LOCALAPPDATA%\KofTwentyTwo\gclo; the GCLO_DATA_DIR
/// environment variable overrides it so UI tests (and portable setups) can point
/// the whole app at an isolated directory without touching the real profile.
/// Components that accept an explicit directory (test seams) are unaffected —
/// this only feeds defaults.
/// </summary>
/// <remarks>
/// The data root must never be the Velopack install root (%LOCALAPPDATA%\gclo):
/// Setup.exe uninstalls an existing install first and deletes that whole folder,
/// which destroyed the accounts and settings stored there (#91).
/// </remarks>
public static class GcloPaths
{
/// <summary>
/// Root directory for gclo's per-user data: GCLO_DATA_DIR when set and
/// non-empty, otherwise %LOCALAPPDATA%\gclo. Read on every access so a
/// value set before launch always wins over any cached default.
/// non-empty, otherwise <see cref="DefaultDataRoot"/>. Read on every access so
/// a value set before launch always wins over any cached default.
/// </summary>
public static string DataRoot =>
Environment.GetEnvironmentVariable("GCLO_DATA_DIR") is { Length: > 0 } dir
@@ -27,7 +33,12 @@ public static class GcloPaths
/// <summary>%LOCALAPPDATA%\gclo: the root used when GCLO_DATA_DIR is not set.</summary>
/// <summary>
/// %LOCALAPPDATA%\KofTwentyTwo\gclo: the root used when GCLO_DATA_DIR is not
/// set. Outside the Velopack install root on purpose (see the type remarks).
/// </summary>
public static string DefaultDataRoot =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "gclo");
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"KofTwentyTwo", "gclo");
}