From 8aef36595bba6d8718dad8285ab2fdb2a6c9eece Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Mon, 6 Mar 2023 23:26:40 +0000 Subject: [PATCH] Cache path setting fixes --- Core/Extensions/IOExtensions.cs | 11 ++-- Core/GameInstanceManager.cs | 22 ++++--- Core/Net/NetFileCache.cs | 22 ++++--- GUI/Dialogs/SettingsDialog.cs | 104 ++++++++++++++++++++------------ GUI/Dialogs/SettingsDialog.resx | 4 +- 5 files changed, 100 insertions(+), 63 deletions(-) diff --git a/Core/Extensions/IOExtensions.cs b/Core/Extensions/IOExtensions.cs index da1ee4c87f..8b93605fe3 100644 --- a/Core/Extensions/IOExtensions.cs +++ b/Core/Extensions/IOExtensions.cs @@ -28,6 +28,8 @@ private static bool StringArrayStartsWith(string[] child, string[] parent) return true; } + private static readonly char[] pathDelims = new char[] {Path.DirectorySeparatorChar}; + /// /// Check whether a given path is an ancestor of another /// @@ -36,10 +38,8 @@ private static bool StringArrayStartsWith(string[] child, string[] parent) /// true if child is a descendant of parent, false otherwise public static bool IsAncestorOf(this DirectoryInfo parent, DirectoryInfo child) => StringArrayStartsWith( - child.FullName.Split(new char[] {Path.DirectorySeparatorChar}, - StringSplitOptions.RemoveEmptyEntries), - parent.FullName.Split(new char[] {Path.DirectorySeparatorChar}, - StringSplitOptions.RemoveEmptyEntries)); + child.FullName.Split(pathDelims, StringSplitOptions.RemoveEmptyEntries), + parent.FullName.Split(pathDelims, StringSplitOptions.RemoveEmptyEntries)); /// /// Extension method to fill in the gap of getting from a @@ -51,7 +51,8 @@ public static bool IsAncestorOf(this DirectoryInfo parent, DirectoryInfo child) /// The DriveInfo associated with this directory public static DriveInfo GetDrive(this DirectoryInfo dir) => DriveInfo.GetDrives() - .Where(dr => dr.RootDirectory.IsAncestorOf(dir)) + .Where(dr => dr.RootDirectory == dir + || dr.RootDirectory.IsAncestorOf(dir)) .OrderByDescending(dr => dr.RootDirectory.FullName.Length) .FirstOrDefault(); diff --git a/Core/GameInstanceManager.cs b/Core/GameInstanceManager.cs index a0df024e20..44dd27decc 100644 --- a/Core/GameInstanceManager.cs +++ b/Core/GameInstanceManager.cs @@ -3,12 +3,15 @@ using System.IO; using System.Linq; using System.Transactions; + using Autofac; using ChinhDo.Transactions.FileManager; using log4net; + using CKAN.Versioning; using CKAN.Configuration; using CKAN.Games; +using CKAN.Extensions; namespace CKAN { @@ -543,8 +546,12 @@ private void LoadCacheSettings() } } - string failReason; - TrySetupCache(Configuration.DownloadCacheDir, out failReason); + if (!TrySetupCache(Configuration.DownloadCacheDir, out string failReason)) + { + log.ErrorFormat("Cache not found at configured path {0}: {1}", Configuration.DownloadCacheDir, failReason); + // Fall back to default path to minimize chance of ending up in an invalid state at startup + TrySetupCache("", out failReason); + } } /// @@ -567,6 +574,8 @@ public bool TrySetupCache(string path, out string failureReason) } else { + // Make sure we can access it + var bytesFree = new DirectoryInfo(path).GetDrive().AvailableFreeSpace; Cache = new NetModuleCache(this, path); Configuration.DownloadCacheDir = path; } @@ -575,17 +584,12 @@ public bool TrySetupCache(string path, out string failureReason) } catch (DirectoryNotFoundKraken) { + Configuration.DownloadCacheDir = origPath; failureReason = string.Format(Properties.Resources.GameInstancePathNotFound, path); return false; } - catch (PathErrorKraken ex) - { - failureReason = ex.Message; - return false; - } - catch (IOException ex) + catch (Exception ex) { - // MoveFrom failed, possibly full disk, so undo the change Configuration.DownloadCacheDir = origPath; failureReason = ex.Message; return false; diff --git a/Core/Net/NetFileCache.cs b/Core/Net/NetFileCache.cs index 8262a4ccae..49bbdf738b 100644 --- a/Core/Net/NetFileCache.cs +++ b/Core/Net/NetFileCache.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Text; using System.Text.RegularExpressions; @@ -68,8 +68,8 @@ public NetFileCache(string path) Properties.Resources.NetFileCacheCannotFind, cachePath)); } - // Files go here while they're downloading - Directory.CreateDirectory(InProgressPath); + // Make sure we can access it + var bytesFree = new DirectoryInfo(cachePath)?.GetDrive()?.AvailableFreeSpace ?? 0; // Establish a watch on our cache. This means we can cache the directory contents, // and discard that cache if we spot changes. @@ -107,14 +107,18 @@ public void Dispose() watcher.Dispose(); } + // Files go here while they're downloading private string InProgressPath => Path.Combine(cachePath, "downloading"); private string GetInProgressFileName(string hash, string description) - => Directory.EnumerateFiles(InProgressPath) - .Where(path => new FileInfo(path).Name.StartsWith(hash)) - .FirstOrDefault() - // If not found, return the name to create - ?? Path.Combine(InProgressPath, $"{hash}-{description}"); + { + Directory.CreateDirectory(InProgressPath); + return Directory.EnumerateFiles(InProgressPath) + .Where(path => new FileInfo(path).Name.StartsWith(hash)) + .FirstOrDefault() + // If not found, return the name to create + ?? Path.Combine(InProgressPath, $"{hash}-{description}"); + } public string GetInProgressFileName(Uri url, string description) => GetInProgressFileName(NetFileCache.CreateURLHash(url), @@ -300,7 +304,7 @@ public void GetSizeInfo(out int numFiles, out long numBytes, out long bytesFree) numFiles = 0; numBytes = 0; GetSizeInfo(cachePath, ref numFiles, ref numBytes); - bytesFree = new DirectoryInfo(cachePath).GetDrive()?.AvailableFreeSpace ?? 0; + bytesFree = new DirectoryInfo(cachePath)?.GetDrive()?.AvailableFreeSpace ?? 0; foreach (var legacyDir in legacyDirs()) { GetSizeInfo(legacyDir, ref numFiles, ref numBytes); diff --git a/GUI/Dialogs/SettingsDialog.cs b/GUI/Dialogs/SettingsDialog.cs index 9d257b94e1..4375282ae8 100644 --- a/GUI/Dialogs/SettingsDialog.cs +++ b/GUI/Dialogs/SettingsDialog.cs @@ -16,9 +16,6 @@ public partial class SettingsDialog : Form private static readonly ILog log = LogManager.GetLogger(typeof(SettingsDialog)); private IUser m_user; - private long m_cacheSize; - private int m_cacheFileCount; - private long m_cacheFreeSpace; private IConfiguration config; private List _sortedRepos = new List(); @@ -64,6 +61,20 @@ public void UpdateDialog() UpdateCacheInfo(config.DownloadCacheDir); } + protected override void OnFormClosing(FormClosingEventArgs e) + { + if (CachePath.Text != config.DownloadCacheDir + && !Main.Instance.Manager.TrySetupCache(CachePath.Text, out string failReason)) + { + m_user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); + e.Cancel = true; + } + else + { + base.OnFormClosing(e); + } + } + private void UpdateRefreshRate() { int rate = config.RefreshRate; @@ -113,48 +124,48 @@ private void UpdateLanguageSelectionComboBox() LanguageSelectionComboBox.SelectedIndex = LanguageSelectionComboBox.FindStringExact(config.Language); } - private bool updatingCache = false; - private void UpdateCacheInfo(string newPath) { - string failReason; - if (updatingCache) + CachePath.Text = newPath; + // Background thread in case GetSizeInfo takes a while + Task.Factory.StartNew(() => { - return; - } - if (newPath == config.DownloadCacheDir - || Main.Instance.Manager.TrySetupCache(newPath, out failReason)) - { - updatingCache = true; - Task.Factory.StartNew(() => + try { - // This might take a little while if the cache is big - Main.Instance.Manager.Cache.GetSizeInfo(out m_cacheFileCount, out m_cacheSize, out m_cacheFreeSpace); + // Make a temporary cache object to validate the path without changing the setting till close + var cache = new NetModuleCache(newPath); + cache.GetSizeInfo(out int cacheFileCount, out long cacheSize, out long cacheFreeSpace); + Util.Invoke(this, () => { if (config.CacheSizeLimit.HasValue) { - // Show setting in MB + // Show setting in MiB CacheLimit.Text = (config.CacheSizeLimit.Value / 1024 / 1024).ToString(); } - CachePath.Text = config.DownloadCacheDir; - CacheSummary.Text = string.Format(Properties.Resources.SettingsDialogSummmary, m_cacheFileCount, CkanModule.FmtSize(m_cacheSize), CkanModule.FmtSize(m_cacheFreeSpace)); + CacheSummary.Text = string.Format( + Properties.Resources.SettingsDialogSummmary, + cacheFileCount, CkanModule.FmtSize(cacheSize), CkanModule.FmtSize(cacheFreeSpace)); CacheSummary.ForeColor = SystemColors.ControlText; OpenCacheButton.Enabled = true; - ClearCacheButton.Enabled = (m_cacheSize > 0); + ClearCacheButton.Enabled = (cacheSize > 0); PurgeToLimitMenuItem.Enabled = (config.CacheSizeLimit.HasValue - && m_cacheSize > config.CacheSizeLimit.Value); - updatingCache = false; + && cacheSize > config.CacheSizeLimit.Value); }); - }); - } - else - { - CacheSummary.Text = string.Format(Properties.Resources.SettingsDialogSummaryInvalid, failReason); - CacheSummary.ForeColor = Color.Red; - OpenCacheButton.Enabled = false; - ClearCacheButton.Enabled = false; - } + + } + catch (Exception ex) + { + Util.Invoke(this, () => + { + CacheSummary.Text = string.Format(Properties.Resources.SettingsDialogSummaryInvalid, + ex.Message); + CacheSummary.ForeColor = Color.Red; + OpenCacheButton.Enabled = false; + ClearCacheButton.Enabled = false; + }); + } + }); } private void CachePath_TextChanged(object sender, EventArgs e) @@ -205,6 +216,14 @@ private void PurgeToLimitMenuItem_Click(object sender, EventArgs e) // Purge old downloads if we're over the limit if (config.CacheSizeLimit.HasValue) { + // Switch main cache since user seems committed to this path + if (CachePath.Text != config.DownloadCacheDir + && !Main.Instance.Manager.TrySetupCache(CachePath.Text, out string failReason)) + { + m_user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); + return; + } + Main.Instance.Manager.Cache.EnforceSizeLimit( config.CacheSizeLimit.Value, RegistryManager.Instance(Main.Instance.CurrentInstance).registry @@ -215,13 +234,22 @@ private void PurgeToLimitMenuItem_Click(object sender, EventArgs e) private void PurgeAllMenuItem_Click(object sender, EventArgs e) { + // Switch main cache since user seems committed to this path + if (CachePath.Text != config.DownloadCacheDir + && !Main.Instance.Manager.TrySetupCache(CachePath.Text, out string failReason)) + { + m_user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); + return; + } + + Main.Instance.Manager.Cache.GetSizeInfo( + out int cacheFileCount, out long cacheSize, out long cacheFreeSpace); + YesNoDialog deleteConfirmationDialog = new YesNoDialog(); - string confirmationText = String.Format - ( + string confirmationText = String.Format( Properties.Resources.SettingsDialogDeleteConfirm, - m_cacheFileCount, - CkanModule.FmtSize(m_cacheSize) - ); + cacheFileCount, + CkanModule.FmtSize(cacheSize)); if (deleteConfirmationDialog.ShowYesNoDialog(this, confirmationText) == DialogResult.Yes) { @@ -245,7 +273,7 @@ private void PurgeAllMenuItem_Click(object sender, EventArgs e) private void ResetCacheButton_Click(object sender, EventArgs e) { // Reset to default cache path - UpdateCacheInfo(""); + UpdateCacheInfo(JsonConfiguration.DefaultDownloadCacheDir); } private void OpenCacheButton_Click(object sender, EventArgs e) @@ -300,7 +328,7 @@ private void NewRepoButton_Click(object sender, EventArgs e) } catch (Exception) { - Main.Instance.currentUser.RaiseError("Invalid repo format - should be \" | \""); + m_user.RaiseError("Invalid repo format - should be \" | \""); } } } diff --git a/GUI/Dialogs/SettingsDialog.resx b/GUI/Dialogs/SettingsDialog.resx index fcfb804225..93b46782e2 100644 --- a/GUI/Dialogs/SettingsDialog.resx +++ b/GUI/Dialogs/SettingsDialog.resx @@ -129,9 +129,9 @@ New Delete Download Cache - N files, M MB + N files, M MiB Maximum cache size: - MB (empty for unlimited) + MiB (empty for unlimited) Change... Purge Purge to limit