From f6020c37e0e125fc573d4d22b2aa94a27e175c50 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Tue, 9 Oct 2018 23:33:48 -0500 Subject: [PATCH] Limit the size of the cache --- Cmdline/Action/Cache.cs | 54 ++++++++++++++- Core/ModuleInstaller.cs | 18 +++++ Core/Net/NetFileCache.cs | 98 +++++++++++++++++++++++++++ Core/Net/NetModuleCache.cs | 4 ++ Core/Registry/Registry.cs | 49 ++++++++++++-- Core/Versioning/KspVersionCriteria.cs | 10 ++- Core/Win32Registry.cs | 25 +++++++ GUI/CKAN-GUI.csproj | 1 + GUI/DropdownMenuButton.cs | 65 ++++++++++++++++++ GUI/SettingsDialog.Designer.cs | 94 +++++++++++++++++++++---- GUI/SettingsDialog.cs | 51 ++++++++++++-- Tests/Core/FakeWin32Registry.cs | 4 ++ 12 files changed, 450 insertions(+), 23 deletions(-) create mode 100644 GUI/DropdownMenuButton.cs diff --git a/Cmdline/Action/Cache.cs b/Cmdline/Action/Cache.cs index cb02ca2ad5..bcafed9a9b 100644 --- a/Cmdline/Action/Cache.cs +++ b/Cmdline/Action/Cache.cs @@ -25,6 +25,12 @@ private class CacheSubOptions : VerbCommandOptions [VerbOption("reset", HelpText = "Set the download cache path to the default")] public CommonOptions ResetOptions { get; set; } + [VerbOption("showlimit", HelpText = "Show the cache size limit")] + public CommonOptions ShowLimitOptions { get; set; } + + [VerbOption("setlimit", HelpText = "Set the cache size limit")] + public SetLimitOptions SetLimitOptions { get; set; } + [HelpVerbOption] public string GetUsage(string verb) { @@ -45,11 +51,15 @@ public string GetUsage(string verb) case "set": ht.AddPreOptionsLine($"Usage: ckan cache {verb} [options] path"); break; + case "setlimit": + ht.AddPreOptionsLine($"Usage: ckan cache {verb} [options] megabytes"); + break; // Now the commands with only --flag type options case "list": case "clear": case "reset": + case "showlimit": default: ht.AddPreOptionsLine($"Usage: ckan cache {verb} [options]"); break; @@ -65,6 +75,12 @@ private class SetOptions : CommonOptions public string Path { get; set; } } + private class SetLimitOptions : CommonOptions + { + [ValueOption(0)] + public long Megabytes { get; set; } = -1; + } + /// /// Execute a cache subcommand /// @@ -111,6 +127,14 @@ public int RunSubCommand(KSPManager mgr, CommonOptions opts, SubCommandOptions u exitCode = ResetCacheDirectory((CommonOptions)suboptions); break; + case "showlimit": + exitCode = ShowCacheSizeLimit((CommonOptions)suboptions); + break; + + case "setlimit": + exitCode = SetCacheSizeLimit((SetLimitOptions)suboptions); + break; + default: user.RaiseMessage("Unknown command: cache {0}", option); exitCode = Exit.BADOPT; @@ -176,6 +200,34 @@ private int ResetCacheDirectory(CommonOptions options) return Exit.OK; } + private int ShowCacheSizeLimit(CommonOptions options) + { + IWin32Registry winReg = new Win32Registry(); + if (winReg.CacheSizeLimit.HasValue) + { + user.RaiseMessage(CkanModule.FmtSize(winReg.CacheSizeLimit.Value)); + } + else + { + user.RaiseMessage("Unlimited"); + } + return Exit.OK; + } + + private int SetCacheSizeLimit(SetLimitOptions options) + { + IWin32Registry winReg = new Win32Registry(); + if (options.Megabytes < 0) + { + winReg.CacheSizeLimit = null; + } + else + { + winReg.CacheSizeLimit = options.Megabytes * (long)1024 * (long)1024; + } + return ShowCacheSizeLimit(null); + } + private void printCacheInfo() { int fileCount; @@ -187,7 +239,7 @@ private void printCacheInfo() private KSPManager manager; private IUser user; - private static readonly ILog log = LogManager.GetLogger(typeof(Cache)); + private static readonly ILog log = LogManager.GetLogger(typeof(Cache)); } } diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 524debef66..a853c4b124 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -219,6 +219,8 @@ public void InstallList(ICollection modules, RelationshipResolverOpt } + EnforceCacheSizeLimit(); + // We can scan GameData as a separate transaction. Installing the mods // leaves everything consistent, and this is just gravy. (And ScanGameData // acts as a Tx, anyway, so we don't need to provide our own.) @@ -253,6 +255,8 @@ public void InstallList(ModuleResolution modules, RelationshipResolverOptions op User.RaiseProgress("Committing filesystem changes", 80); transaction.Complete(); + + EnforceCacheSizeLimit(); } } @@ -996,6 +1000,8 @@ public void AddRemove(IEnumerable add = null, IEnumerable re registry_manager.Save(enforceConsistency); tx.Complete(); + + EnforceCacheSizeLimit(); } } @@ -1157,6 +1163,18 @@ public void ImportFiles(HashSet files, IUser user, Action f.Delete(); } } + + EnforceCacheSizeLimit(); + } + + private void EnforceCacheSizeLimit() + { + // Purge old downloads if we're over the limit + Win32Registry winReg = new Win32Registry(); + if (winReg.CacheSizeLimit.HasValue) + { + Cache.EnforceSizeLimit(winReg.CacheSizeLimit.Value, registry_manager.registry); + } } /// diff --git a/Core/Net/NetFileCache.cs b/Core/Net/NetFileCache.cs index 7fd0b5651e..82f500ff24 100644 --- a/Core/Net/NetFileCache.cs +++ b/Core/Net/NetFileCache.cs @@ -11,6 +11,7 @@ using ICSharpCode.SharpZipLib.Zip; using log4net; using CKAN.Extensions; +using CKAN.Versioning; namespace CKAN { @@ -290,6 +291,103 @@ private HashSet legacyDirs() .ToHashSet(); } + public void EnforceSizeLimit(long bytes, Registry registry) + { + int numFiles; + long curBytes; + GetSizeInfo(out numFiles, out curBytes); + if (curBytes > bytes) + { + // This object will let us determine whether a module is compatible with any of our instances + KspVersionCriteria aggregateCriteria = manager?.Instances.Values + .Where(ksp => ksp.Valid) + .Select(ksp => ksp.VersionCriteria()) + .Aggregate((a, b) => a.Union(b)); + + // This object lets us find the modules associated with a cached file + Dictionary> hashMap = registry.GetDownloadHashIndex(); + + // Prune the module lists to only those that are compatible + foreach (var kvp in hashMap) + { + kvp.Value.RemoveAll(mod => !mod.IsCompatibleKSP(aggregateCriteria)); + } + + // Now get all the files in all the caches... + List files = allFiles(); + // ... and sort them by compatibilty and timestamp... + files.Sort((a, b) => compareFiles( + hashMap, aggregateCriteria, a, b + )); + + // ... and delete them till we're under the limit + foreach (FileInfo fi in files) + { + curBytes -= fi.Length; + fi.Delete(); + if (curBytes <= bytes) + { + // Limit met, all done! + break; + } + } + OnCacheChanged(); + } + } + + private int compareFiles(Dictionary> hashMap, KspVersionCriteria crit, FileInfo a, FileInfo b) + { + // Compatible modules for file A + List modulesA; + hashMap.TryGetValue(a.Name.Substring(0, 8), out modulesA); + bool compatA = modulesA?.Any() ?? false; + + // Compatible modules for file B + List modulesB; + hashMap.TryGetValue(b.Name.Substring(0, 8), out modulesB); + bool compatB = modulesB?.Any() ?? false; + + if (modulesA == null && modulesB != null) + { + // A isn't indexed but B is, delete A first + return -1; + } + else if (modulesA != null && modulesB == null) + { + // A is indexed but B isn't, delete B first + return 1; + } + else if (!compatA && compatB) + { + // A isn't compatible but B is, delete A first + return -1; + } + else if (compatA && !compatB) + { + // A is compatible but B isn't, delete B first + return 1; + } + else + { + // Both are either compatible or incompatible + // Go by file age, oldest first + return (int)(a.CreationTime - b.CreationTime).TotalSeconds; + } + return 0; + } + + private List allFiles() + { + DirectoryInfo mainDir = new DirectoryInfo(cachePath); + var files = mainDir.EnumerateFiles(); + foreach (string legacyDir in legacyDirs()) + { + DirectoryInfo legDir = new DirectoryInfo(legacyDir); + files = files.Union(legDir.EnumerateFiles()); + } + return files.ToList(); + } + /// /// Check whether a ZIP file is valid /// diff --git a/Core/Net/NetModuleCache.cs b/Core/Net/NetModuleCache.cs index cc1c7fc15a..21c4d370c3 100644 --- a/Core/Net/NetModuleCache.cs +++ b/Core/Net/NetModuleCache.cs @@ -74,6 +74,10 @@ public void GetSizeInfo(out int numFiles, out long numBytes) { cache.GetSizeInfo(out numFiles, out numBytes); } + public void EnforceSizeLimit(long bytes, Registry registry) + { + cache.EnforceSizeLimit(bytes, registry); + } /// /// Calculate the SHA1 hash of a file diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index ef5bdcb38c..9044a4f326 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -1097,14 +1097,20 @@ public HashSet FindReverseDependencies(IEnumerable modules_to_re public Dictionary> GetSha1Index() { var index = new Dictionary>(); - foreach (var kvp in available_modules) { + foreach (var kvp in available_modules) + { AvailableModule am = kvp.Value; - foreach (var kvp2 in am.module_version) { + foreach (var kvp2 in am.module_version) + { CkanModule mod = kvp2.Value; - if (mod.download_hash != null) { - if (index.ContainsKey(mod.download_hash.sha1)) { + if (mod.download_hash != null) + { + if (index.ContainsKey(mod.download_hash.sha1)) + { index[mod.download_hash.sha1].Add(mod); - } else { + } + else + { index.Add(mod.download_hash.sha1, new List() {mod}); } } @@ -1113,5 +1119,38 @@ public Dictionary> GetSha1Index() return index; } + /// + /// Get a dictionary of all mod versions indexed by their download URLs' hash. + /// Useful for finding the mods for a group of URLs without repeatedly searching the entire registry. + /// + /// + /// dictionary[urlHash] = {mod1, mod2, mod3}; + /// + public Dictionary> GetDownloadHashIndex() + { + var index = new Dictionary>(); + foreach (var kvp in available_modules) + { + AvailableModule am = kvp.Value; + foreach (var kvp2 in am.module_version) + { + CkanModule mod = kvp2.Value; + if (mod.download != null) + { + string hash = NetFileCache.CreateURLHash(mod.download); + if (index.ContainsKey(hash)) + { + index[hash].Add(mod); + } + else + { + index.Add(hash, new List() {mod}); + } + } + } + } + return index; + } + } } diff --git a/Core/Versioning/KspVersionCriteria.cs b/Core/Versioning/KspVersionCriteria.cs index ae2e578b78..0b06b85757 100644 --- a/Core/Versioning/KspVersionCriteria.cs +++ b/Core/Versioning/KspVersionCriteria.cs @@ -6,7 +6,7 @@ namespace CKAN.Versioning { public class KspVersionCriteria { - private List _versions = new List (); + private List _versions = new List(); public KspVersionCriteria (KspVersion v) { @@ -34,6 +34,14 @@ public IList Versions } } + public KspVersionCriteria Union(KspVersionCriteria other) + { + return new KspVersionCriteria( + null, + _versions.Union(other.Versions).ToList() + ); + } + public override String ToString() { return "[Versions: " + _versions.ToString() + "]"; diff --git a/Core/Win32Registry.cs b/Core/Win32Registry.cs index 62cb8f7434..5d834666cd 100644 --- a/Core/Win32Registry.cs +++ b/Core/Win32Registry.cs @@ -14,6 +14,7 @@ public interface IWin32Registry string GetKSPBuilds(); void SetKSPBuilds(string buildMap); string DownloadCacheDir { get; set; } + long? CacheSizeLimit { get; set; } } public class Win32Registry : IWin32Registry @@ -58,6 +59,30 @@ public string DownloadCacheDir } } + /// + /// Get and set the maximum number of bytes allowed in the cache. + /// Unlimited if null. + /// + public long? CacheSizeLimit + { + get + { + string val = GetRegistryValue(@"CacheSizeLimit", null); + return string.IsNullOrEmpty(val) ? null : (long?)Convert.ToInt64(val); + } + set + { + if (!value.HasValue) + { + DeleteRegistryValue(@"CacheSizeLimit"); + } + else + { + SetRegistryValue(@"CacheSizeLimit", value.Value); + } + } + } + private int InstanceCount { get { return GetRegistryValue(@"KSPInstanceCount", 0); } diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj index 5c099d7435..947e5360d1 100644 --- a/GUI/CKAN-GUI.csproj +++ b/GUI/CKAN-GUI.csproj @@ -104,6 +104,7 @@ + Form diff --git a/GUI/DropdownMenuButton.cs b/GUI/DropdownMenuButton.cs new file mode 100644 index 0000000000..59ac0d83d2 --- /dev/null +++ b/GUI/DropdownMenuButton.cs @@ -0,0 +1,65 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace CKAN +{ + /// + /// Button with a Menu property that displays when you click + /// Also shows a down-pointing triangle on the button + /// + /// Based on https://stackoverflow.com/a/24087828/2422988 + /// + public class DropdownMenuButton : Button + { + /// + /// The menu to use for the dropdown + /// + [ + DefaultValue(null), + Browsable(true), + DesignerSerializationVisibility(DesignerSerializationVisibility.Visible) + ] + public ContextMenuStrip Menu { get; set; } + + /// + /// Draw the triangle on the button + /// + /// The paint event details + protected override void OnPaint(PaintEventArgs pevent) + { + base.OnPaint(pevent); + + if (Menu != null) + { + int arrowX = ClientRectangle.Width - 14, + arrowY = ClientRectangle.Height / 2 - 1; + + pevent.Graphics.FillPolygon( + Enabled ? SystemBrushes.ControlText : SystemBrushes.ButtonShadow, + new Point[] + { + new Point(arrowX, arrowY), + new Point(arrowX + 7, arrowY), + new Point(arrowX + 3, arrowY + 4) + } + ); + } + } + + /// + /// Show the Menu on click + /// + /// The mouse event details + protected override void OnMouseDown(MouseEventArgs mevent) + { + base.OnMouseDown(mevent); + + if (Menu != null && mevent.Button == MouseButtons.Left) + { + Menu.Show(this, new Point(0, Height)); + } + } + } +} diff --git a/GUI/SettingsDialog.Designer.cs b/GUI/SettingsDialog.Designer.cs index 110ea305fc..181620cc7c 100644 --- a/GUI/SettingsDialog.Designer.cs +++ b/GUI/SettingsDialog.Designer.cs @@ -40,12 +40,18 @@ private void InitializeComponent() this.NewAuthTokenButton = new System.Windows.Forms.Button(); this.DeleteAuthTokenButton = new System.Windows.Forms.Button(); this.CacheGroupBox = new System.Windows.Forms.GroupBox(); - this.ClearCacheButton = new System.Windows.Forms.Button(); + this.ClearCacheButton = new CKAN.DropdownMenuButton(); + this.ClearCacheMenu = new System.Windows.Forms.ContextMenuStrip(this.components); + this.PurgeToLimitMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.PurgeAllMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.ChangeCacheButton = new System.Windows.Forms.Button(); this.ResetCacheButton = new System.Windows.Forms.Button(); this.OpenCacheButton = new System.Windows.Forms.Button(); this.CachePath = new System.Windows.Forms.TextBox(); + this.CacheLimit = new System.Windows.Forms.TextBox(); this.CacheSummary = new System.Windows.Forms.Label(); + this.CacheLimitPreLabel = new System.Windows.Forms.Label(); + this.CacheLimitPostLabel = new System.Windows.Forms.Label(); this.AutoUpdateGroupBox = new System.Windows.Forms.GroupBox(); this.RefreshOnStartupCheckbox = new System.Windows.Forms.CheckBox(); this.CheckUpdateOnLaunchCheckbox = new System.Windows.Forms.CheckBox(); @@ -203,33 +209,59 @@ private void InitializeComponent() this.CacheGroupBox.Controls.Add(this.ResetCacheButton); this.CacheGroupBox.Controls.Add(this.OpenCacheButton); this.CacheGroupBox.Controls.Add(this.CachePath); + this.CacheGroupBox.Controls.Add(this.CacheLimit); this.CacheGroupBox.Controls.Add(this.CacheSummary); + this.CacheGroupBox.Controls.Add(this.CacheLimitPreLabel); + this.CacheGroupBox.Controls.Add(this.CacheLimitPostLabel); this.CacheGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.CacheGroupBox.Location = new System.Drawing.Point(16, 343); this.CacheGroupBox.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.CacheGroupBox.Name = "CacheGroupBox"; this.CacheGroupBox.Padding = new System.Windows.Forms.Padding(4, 4, 4, 4); - this.CacheGroupBox.Size = new System.Drawing.Size(635, 120); + this.CacheGroupBox.Size = new System.Drawing.Size(635, 140); this.CacheGroupBox.TabIndex = 10; this.CacheGroupBox.TabStop = false; this.CacheGroupBox.Text = "Download Cache"; // // ClearCacheButton // + this.ClearCacheButton.Menu = this.ClearCacheMenu; this.ClearCacheButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.ClearCacheButton.Location = new System.Drawing.Point(116, 80); + this.ClearCacheButton.Location = new System.Drawing.Point(116, 100); this.ClearCacheButton.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.ClearCacheButton.Name = "ClearCacheButton"; this.ClearCacheButton.Size = new System.Drawing.Size(100, 28); this.ClearCacheButton.TabIndex = 1; - this.ClearCacheButton.Text = "Clear"; + this.ClearCacheButton.Text = "Purge"; this.ClearCacheButton.UseVisualStyleBackColor = true; - this.ClearCacheButton.Click += new System.EventHandler(this.ClearCacheButton_Click); + // + // ClearCacheMenu + // + this.ClearCacheMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.PurgeToLimitMenuItem, + this.PurgeAllMenuItem + }); + this.ClearCacheMenu.Name = "ClearCacheMenu"; + this.ClearCacheMenu.Size = new System.Drawing.Size(180, 70); + // + // PurgeToLimitMenuItem + // + this.PurgeToLimitMenuItem.Name = "PurgeToLimitMenuItem"; + this.PurgeToLimitMenuItem.Size = new System.Drawing.Size(179, 22); + this.PurgeToLimitMenuItem.Text = "Purge to limit"; + this.PurgeToLimitMenuItem.Click += new System.EventHandler(this.PurgeToLimitMenuItem_Click); + // + // PurgeAllMenuItem + // + this.PurgeAllMenuItem.Name = "PurgeAllMenuItem"; + this.PurgeAllMenuItem.Size = new System.Drawing.Size(179, 22); + this.PurgeAllMenuItem.Text = "Purge all"; + this.PurgeAllMenuItem.Click += new System.EventHandler(this.PurgeAllMenuItem_Click); // // ChangeCacheButton // this.ChangeCacheButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.ChangeCacheButton.Location = new System.Drawing.Point(8, 80); + this.ChangeCacheButton.Location = new System.Drawing.Point(8, 100); this.ChangeCacheButton.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.ChangeCacheButton.Name = "ChangeCacheButton"; this.ChangeCacheButton.Size = new System.Drawing.Size(100, 28); @@ -241,7 +273,7 @@ private void InitializeComponent() // ResetCacheButton // this.ResetCacheButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.ResetCacheButton.Location = new System.Drawing.Point(224, 80); + this.ResetCacheButton.Location = new System.Drawing.Point(224, 100); this.ResetCacheButton.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.ResetCacheButton.Name = "ResetCacheButton"; this.ResetCacheButton.Size = new System.Drawing.Size(100, 28); @@ -253,7 +285,7 @@ private void InitializeComponent() // OpenCacheButton // this.OpenCacheButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.OpenCacheButton.Location = new System.Drawing.Point(332, 80); + this.OpenCacheButton.Location = new System.Drawing.Point(332, 100); this.OpenCacheButton.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.OpenCacheButton.Name = "OpenCacheButton"; this.OpenCacheButton.Size = new System.Drawing.Size(100, 28); @@ -283,6 +315,38 @@ private void InitializeComponent() this.CacheSummary.TabIndex = 12; this.CacheSummary.Text = "N files, M MB"; // + // CacheLimitPreLabel + // + this.CacheLimitPreLabel.AutoSize = true; + this.CacheLimitPreLabel.Location = new System.Drawing.Point(8, 76); + this.CacheLimitPreLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.CacheLimitPreLabel.Name = "CacheLimitPreLabel"; + this.CacheLimitPreLabel.Size = new System.Drawing.Size(148, 17); + this.CacheLimitPreLabel.TabIndex = 12; + this.CacheLimitPreLabel.Text = "Maximum cache size:"; + // + // CacheLimit + // + this.CacheLimit.AutoSize = false; + this.CacheLimit.BackColor = System.Drawing.SystemColors.Control; + this.CacheLimit.ForeColor = System.Drawing.SystemColors.ControlText; + this.CacheLimit.Location = new System.Drawing.Point(156, 72); + this.CacheLimit.Name = "CacheLimit"; + this.CacheLimit.Size = new System.Drawing.Size(75, 17); + this.CacheLimit.TabIndex = 11; + this.CacheLimit.TextChanged += new System.EventHandler(this.CacheLimit_TextChanged); + this.CacheLimit.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.CacheLimit_KeyPress); + // + // CacheLimitPostLabel + // + this.CacheLimitPostLabel.AutoSize = true; + this.CacheLimitPostLabel.Location = new System.Drawing.Point(236, 76); + this.CacheLimitPostLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.CacheLimitPostLabel.Name = "CacheLimitPostLabel"; + this.CacheLimitPostLabel.Size = new System.Drawing.Size(60, 17); + this.CacheLimitPostLabel.TabIndex = 12; + this.CacheLimitPostLabel.Text = "MB (empty for unlimited)"; + // // AutoUpdateGroupBox // this.AutoUpdateGroupBox.Controls.Add(this.RefreshOnStartupCheckbox); @@ -294,7 +358,7 @@ private void InitializeComponent() this.AutoUpdateGroupBox.Controls.Add(this.LocalVersionLabelLabel); this.AutoUpdateGroupBox.Controls.Add(this.CheckForUpdatesButton); this.AutoUpdateGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.AutoUpdateGroupBox.Location = new System.Drawing.Point(16, 471); + this.AutoUpdateGroupBox.Location = new System.Drawing.Point(16, 491); this.AutoUpdateGroupBox.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.AutoUpdateGroupBox.Name = "AutoUpdateGroupBox"; this.AutoUpdateGroupBox.Padding = new System.Windows.Forms.Padding(4, 4, 4, 4); @@ -409,7 +473,7 @@ private void InitializeComponent() this.MoreSettingsGroupBox.Controls.Add(this.HideVCheckbox); this.MoreSettingsGroupBox.Controls.Add(this.HideEpochsCheckbox); this.MoreSettingsGroupBox.Controls.Add(this.AutoSortUpdateCheckBox); - this.MoreSettingsGroupBox.Location = new System.Drawing.Point(16, 609); + this.MoreSettingsGroupBox.Location = new System.Drawing.Point(16, 629); this.MoreSettingsGroupBox.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.MoreSettingsGroupBox.Name = "MoreSettingsGroupBox"; this.MoreSettingsGroupBox.Padding = new System.Windows.Forms.Padding(4, 4, 4, 4); @@ -446,7 +510,7 @@ private void InitializeComponent() // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(660, 735); + this.ClientSize = new System.Drawing.Size(660, 755); this.Controls.Add(this.MoreSettingsGroupBox); this.Controls.Add(this.AutoUpdateGroupBox); this.Controls.Add(this.CacheGroupBox); @@ -476,8 +540,14 @@ private void InitializeComponent() private System.Windows.Forms.GroupBox RepositoryGroupBox; private System.Windows.Forms.GroupBox CacheGroupBox; private System.Windows.Forms.TextBox CachePath; + private System.Windows.Forms.TextBox CacheLimit; private System.Windows.Forms.Label CacheSummary; - private System.Windows.Forms.Button ClearCacheButton; + private System.Windows.Forms.Label CacheLimitPreLabel; + private System.Windows.Forms.Label CacheLimitPostLabel; + private CKAN.DropdownMenuButton ClearCacheButton; + private System.Windows.Forms.ContextMenuStrip ClearCacheMenu; + private System.Windows.Forms.ToolStripMenuItem PurgeToLimitMenuItem; + private System.Windows.Forms.ToolStripMenuItem PurgeAllMenuItem; private System.Windows.Forms.Button ChangeCacheButton; private System.Windows.Forms.Button ResetCacheButton; private System.Windows.Forms.Button OpenCacheButton; diff --git a/GUI/SettingsDialog.cs b/GUI/SettingsDialog.cs index c2704a59d7..b9ca739f40 100644 --- a/GUI/SettingsDialog.cs +++ b/GUI/SettingsDialog.cs @@ -1,5 +1,5 @@ using System; -using System.Diagnostics; +using System.Diagnostics; using System.Collections.Generic; using System.IO; using System.Windows.Forms; @@ -22,6 +22,7 @@ public partial class SettingsDialog : Form public SettingsDialog() { InitializeComponent(); + this.ClearCacheMenu.Renderer = new FlatToolStripRenderer(); StartPosition = FormStartPosition.CenterScreen; winReg = new Win32Registry(); } @@ -45,6 +46,11 @@ public void UpdateDialog() AutoSortUpdateCheckBox.Checked = Main.Instance.configuration.AutoSortByUpdate; UpdateCacheInfo(winReg.DownloadCacheDir); + if (winReg.CacheSizeLimit.HasValue) + { + // Show setting in MB + CacheLimit.Text = (winReg.CacheSizeLimit.Value / 1024 / 1024).ToString(); + } } private void RefreshReposListBox() @@ -80,15 +86,17 @@ private void UpdateCacheInfo(string newPath) CachePath.Text = winReg.DownloadCacheDir; CacheSummary.Text = $"{m_cacheFileCount} files, {CkanModule.FmtSize(m_cacheSize)}"; CacheSummary.ForeColor = SystemColors.ControlText; - ClearCacheButton.Enabled = true; OpenCacheButton.Enabled = true; + ClearCacheButton.Enabled = (m_cacheSize > 0); + PurgeToLimitMenuItem.Enabled = (winReg.CacheSizeLimit.HasValue + && m_cacheSize > winReg.CacheSizeLimit.Value); } else { CacheSummary.Text = $"Invalid path: {failReason}"; CacheSummary.ForeColor = Color.Red; - ClearCacheButton.Enabled = false; OpenCacheButton.Enabled = false; + ClearCacheButton.Enabled = false; } } @@ -97,6 +105,28 @@ private void CachePath_TextChanged(object sender, EventArgs e) UpdateCacheInfo(CachePath.Text); } + private void CacheLimit_TextChanged(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(CacheLimit.Text)) + { + winReg.CacheSizeLimit = null; + } + else + { + // Translate from MB to bytes + winReg.CacheSizeLimit = Convert.ToInt64(CacheLimit.Text) * 1024 * 1024; + } + UpdateCacheInfo(CachePath.Text); + } + + private void CacheLimit_KeyPress(object sender, KeyPressEventArgs e) + { + if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar)) + { + e.Handled = true; + } + } + private void ChangeCacheButton_Click(object sender, EventArgs e) { FolderBrowserDialog cacheChooser = new FolderBrowserDialog() @@ -113,7 +143,20 @@ private void ChangeCacheButton_Click(object sender, EventArgs e) } } - private void ClearCacheButton_Click(object sender, EventArgs e) + private void PurgeToLimitMenuItem_Click(object sender, EventArgs e) + { + // Purge old downloads if we're over the limit + if (winReg.CacheSizeLimit.HasValue) + { + Main.Instance.Manager.Cache.EnforceSizeLimit( + winReg.CacheSizeLimit.Value, + RegistryManager.Instance(Main.Instance.CurrentInstance).registry + ); + UpdateCacheInfo(winReg.DownloadCacheDir); + } + } + + private void PurgeAllMenuItem_Click(object sender, EventArgs e) { YesNoDialog deleteConfirmationDialog = new YesNoDialog(); string confirmationText = String.Format diff --git a/Tests/Core/FakeWin32Registry.cs b/Tests/Core/FakeWin32Registry.cs index 37ed6086aa..4d28c41c3b 100644 --- a/Tests/Core/FakeWin32Registry.cs +++ b/Tests/Core/FakeWin32Registry.cs @@ -44,6 +44,10 @@ public FakeWin32Registry(List> instances, string auto_star /// Path to download cache folder for the fake registry /// public string DownloadCacheDir { get; set; } + /// + /// Maximum number of bytes of downloads to retain on disk + /// + public long? CacheSizeLimit { get; set; } /// /// Number of instances in the fake registry