diff --git a/Cmdline/Action/Cache.cs b/Cmdline/Action/Cache.cs
index a61f3e6bad..4217f15d46 100644
--- a/Cmdline/Action/Cache.cs
+++ b/Cmdline/Action/Cache.cs
@@ -1,9 +1,10 @@
using CommandLine;
using CommandLine.Text;
using log4net;
-using CKAN.Configuration;
using Autofac;
+using CKAN.Configuration;
+
namespace CKAN.CmdLine
{
public class CacheSubOptions : VerbCommandOptions
@@ -229,10 +230,11 @@ private int SetCacheSizeLimit(SetLimitOptions options)
private void printCacheInfo()
{
- int fileCount;
- long bytes;
- manager.Cache.GetSizeInfo(out fileCount, out bytes);
- user.RaiseMessage(Properties.Resources.CacheInfo, fileCount, CkanModule.FmtSize(bytes));
+ manager.Cache.GetSizeInfo(out int fileCount, out long bytes, out long bytesFree);
+ user.RaiseMessage(Properties.Resources.CacheInfo,
+ fileCount,
+ CkanModule.FmtSize(bytes),
+ CkanModule.FmtSize(bytesFree));
}
private IUser user;
diff --git a/Cmdline/Properties/Resources.resx b/Cmdline/Properties/Resources.resx
index 10ab523c94..eb1a34e08f 100644
--- a/Cmdline/Properties/Resources.resx
+++ b/Cmdline/Properties/Resources.resx
@@ -161,7 +161,7 @@ Update recommended!
Download cache reset to {0}
Can't reset cache path: {0}
Unlimited
- {0} files, {1}
+ {0} files, {1}, {2} free
"{0}" and "{1}" are the same versions.
"{0}" is lower than "{1}".
"{0}" is higher than "{1}".
diff --git a/Core/CKANPathUtils.cs b/Core/CKANPathUtils.cs
index 030b675cec..193b4aa65e 100644
--- a/Core/CKANPathUtils.cs
+++ b/Core/CKANPathUtils.cs
@@ -3,9 +3,11 @@
using System.Text.RegularExpressions;
using log4net;
+using CKAN.Extensions;
+
namespace CKAN
{
- public class CKANPathUtils
+ public static class CKANPathUtils
{
private static readonly ILog log = LogManager.GetLogger(typeof(CKANPathUtils));
@@ -194,5 +196,20 @@ public static string ToAbsolute(string path, string root)
// the un-prettiest slashes.
return NormalizePath(Path.Combine(root, path));
}
+
+ public static void CheckFreeSpace(DirectoryInfo where, long bytesToStore, string errorDescription)
+ {
+ var bytesFree = where.GetDrive()?.AvailableFreeSpace;
+ if (bytesFree.HasValue && bytesToStore > bytesFree.Value) {
+ throw new NotEnoughSpaceKraken(errorDescription, where,
+ bytesFree.Value, bytesToStore);
+ }
+ log.DebugFormat("Storing {0} to {1} ({2} free)...",
+ CkanModule.FmtSize(bytesToStore),
+ where.FullName,
+ bytesFree.HasValue ? CkanModule.FmtSize(bytesFree.Value)
+ : "unknown bytes");
+ }
+
}
}
diff --git a/Core/Extensions/IOExtensions.cs b/Core/Extensions/IOExtensions.cs
new file mode 100644
index 0000000000..fa1e19907b
--- /dev/null
+++ b/Core/Extensions/IOExtensions.cs
@@ -0,0 +1,54 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace CKAN.Extensions
+{
+ public static class IOExtensions
+ {
+
+ private static bool StringArrayStartsWith(string[] child, string[] parent)
+ {
+ if (parent.Length > child.Length)
+ // Only child is allowed to have extra pieces
+ return false;
+ var opt = Platform.IsWindows ? StringComparison.InvariantCultureIgnoreCase
+ : StringComparison.InvariantCulture;
+ for (int i = 0; i < parent.Length; ++i) {
+ if (!parent[i].Equals(child[i], opt)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Check whether a given path is an ancestor of another
+ ///
+ /// The path to treat as potential ancestor
+ /// The path to treat as potential descendant
+ /// 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));
+
+ ///
+ /// Extension method to fill in the gap of getting from a
+ /// directory to its drive in .NET.
+ /// Returns the drive with the longest RootDirectory.FullName
+ /// that's a prefix of the dir's FullName.
+ ///
+ /// Any DirectoryInfo object
+ /// The DriveInfo associated with this directory
+ public static DriveInfo GetDrive(this DirectoryInfo dir)
+ => DriveInfo.GetDrives()
+ .Where(dr => dr.RootDirectory.IsAncestorOf(dir))
+ .OrderByDescending(dr => dr.RootDirectory.FullName.Length)
+ .FirstOrDefault();
+
+ }
+}
diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs
index d07042cf5a..a4c040ec7f 100644
--- a/Core/ModuleInstaller.cs
+++ b/Core/ModuleInstaller.cs
@@ -139,6 +139,12 @@ public void InstallList(ICollection modules, RelationshipResolverOpt
var modsToInstall = resolver.ModList().ToList();
List downloads = new List();
+ // Make sure we have enough space to install this stuff
+ CKANPathUtils.CheckFreeSpace(new DirectoryInfo(ksp.GameDir()),
+ modsToInstall.Select(m => m.install_size)
+ .Sum(),
+ Properties.Resources.NotEnoughSpaceToInstall);
+
// TODO: All this user-stuff should be happening in another method!
// We should just be installing mods as a transaction.
@@ -178,6 +184,13 @@ public void InstallList(ICollection modules, RelationshipResolverOpt
downloader.DownloadModules(downloads);
}
+ // Make sure we STILL have enough space to install this stuff
+ // now that the downloads have been stored to the cache
+ CKANPathUtils.CheckFreeSpace(new DirectoryInfo(ksp.GameDir()),
+ modsToInstall.Select(m => m.install_size)
+ .Sum(),
+ Properties.Resources.NotEnoughSpaceToInstall);
+
// We're about to install all our mods; so begin our transaction.
using (TransactionScope transaction = CkanTransaction.CreateTransactionScope())
{
diff --git a/Core/Net/IDownloader.cs b/Core/Net/IDownloader.cs
index 0b17d3d495..0919502274 100644
--- a/Core/Net/IDownloader.cs
+++ b/Core/Net/IDownloader.cs
@@ -1,5 +1,5 @@
using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
namespace CKAN
{
diff --git a/Core/Net/NetAsyncModulesDownloader.cs b/Core/Net/NetAsyncModulesDownloader.cs
index 6c9845cfd3..aba7608284 100644
--- a/Core/Net/NetAsyncModulesDownloader.cs
+++ b/Core/Net/NetAsyncModulesDownloader.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+
+using ChinhDo.Transactions.FileManager;
using log4net;
namespace CKAN
@@ -62,6 +64,14 @@ public void DownloadModules(IEnumerable modules)
.Where(group => !currentlyActive.Contains(group.Key))
.ToDictionary(group => group.Key, group => group.First());
+ // Make sure we have enough space to download this stuff
+ var downloadSize = unique_downloads.Values.Select(m => m.download_size).Sum();
+ CKANPathUtils.CheckFreeSpace(new DirectoryInfo(new TxFileManager().GetTempDirectory()),
+ downloadSize,
+ Properties.Resources.NotEnoughSpaceToDownload);
+ // Make sure we have enough space to cache this stuff
+ cache.CheckFreeSpace(downloadSize);
+
this.modules.AddRange(unique_downloads.Values);
try
diff --git a/Core/Net/NetFileCache.cs b/Core/Net/NetFileCache.cs
index fd45a0d35e..5be5508c92 100644
--- a/Core/Net/NetFileCache.cs
+++ b/Core/Net/NetFileCache.cs
@@ -277,11 +277,13 @@ public string GetCachedZip(Uri url)
///
/// Output parameter set to number of files in cache
/// Output parameter set to number of bytes in cache
- public void GetSizeInfo(out int numFiles, out long numBytes)
+ /// Output parameter set to number of bytes free
+ 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;
foreach (var legacyDir in legacyDirs())
{
GetSizeInfo(legacyDir, ref numFiles, ref numBytes);
@@ -298,6 +300,13 @@ private void GetSizeInfo(string path, ref int numFiles, ref long numBytes)
}
}
+ public void CheckFreeSpace(long bytesToStore)
+ {
+ CKANPathUtils.CheckFreeSpace(new DirectoryInfo(cachePath),
+ bytesToStore,
+ Properties.Resources.NotEnoughSpaceToCache);
+ }
+
private HashSet legacyDirs()
{
return manager?.Instances.Values
@@ -310,9 +319,7 @@ private HashSet legacyDirs()
public void EnforceSizeLimit(long bytes, Registry registry)
{
- int numFiles;
- long curBytes;
- GetSizeInfo(out numFiles, out curBytes);
+ GetSizeInfo(out int numFiles, out long curBytes, out long _);
if (curBytes > bytes)
{
// This object will let us determine whether a module is compatible with any of our instances
diff --git a/Core/Net/NetModuleCache.cs b/Core/Net/NetModuleCache.cs
index 8eb66d2bfc..0d81701a79 100644
--- a/Core/Net/NetModuleCache.cs
+++ b/Core/Net/NetModuleCache.cs
@@ -70,14 +70,19 @@ public string GetCachedZip(CkanModule m)
{
return cache.GetCachedZip(m.download);
}
- public void GetSizeInfo(out int numFiles, out long numBytes)
+ public void GetSizeInfo(out int numFiles, out long numBytes, out long bytesFree)
{
- cache.GetSizeInfo(out numFiles, out numBytes);
+ cache.GetSizeInfo(out numFiles, out numBytes, out bytesFree);
}
public void EnforceSizeLimit(long bytes, Registry registry)
{
cache.EnforceSizeLimit(bytes, registry);
}
+ public void CheckFreeSpace(long bytesToStore)
+ {
+ cache.CheckFreeSpace(bytesToStore);
+ }
+
///
/// Calculate the SHA1 hash of a file
diff --git a/Core/Properties/Resources.Designer.cs b/Core/Properties/Resources.Designer.cs
index b732d05301..425749264f 100644
--- a/Core/Properties/Resources.Designer.cs
+++ b/Core/Properties/Resources.Designer.cs
@@ -513,6 +513,18 @@ internal static string KrakenAlreadyRunning {
internal static string KrakenReinstallModule {
get { return (string)(ResourceManager.GetObject("KrakenReinstallModule", resourceCulture)); }
}
+ internal static string NotEnoughSpaceToDownload {
+ get { return (string)(ResourceManager.GetObject("NotEnoughSpaceToDownload", resourceCulture)); }
+ }
+ internal static string NotEnoughSpaceToCache {
+ get { return (string)(ResourceManager.GetObject("NotEnoughSpaceToCache", resourceCulture)); }
+ }
+ internal static string NotEnoughSpaceToInstall {
+ get { return (string)(ResourceManager.GetObject("NotEnoughSpaceToInstall", resourceCulture)); }
+ }
+ internal static string KrakenNotEnoughSpace {
+ get { return (string)(ResourceManager.GetObject("KrakenNotEnoughSpace", resourceCulture)); }
+ }
internal static string RelationshipResolverConflictsWith {
get { return (string)(ResourceManager.GetObject("RelationshipResolverConflictsWith", resourceCulture)); }
diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx
index b0d1c6413b..8d9dffbeb0 100644
--- a/Core/Properties/Resources.resx
+++ b/Core/Properties/Resources.resx
@@ -280,6 +280,13 @@ Consider adding an authentication token to increase the throttling limit.
Metadata changed, reinstallation recommended: {0}
+ Not enough space in temp folder to download modules!
+ Not enough space in cache folder to store modules!
+ Not enough space in game folder to install modules!
+ {0}
+Need to store {3} to {1}, but only {2} is available!
+Free up space on that device or change your settings to use another location.
+
{0} conflicts with {1}
{0} required, but an incompatible version is in the resolver
{0} required, but an incompatible version is installed
diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs
index fc62df9b35..8b5aef8157 100644
--- a/Core/Types/CkanModule.cs
+++ b/Core/Types/CkanModule.cs
@@ -727,26 +727,22 @@ public Uri InternetArchiveDownload
}
}
+ private const double K = 1024;
+
///
/// Format a byte count into readable file size
///
/// Number of bytes in a file
///
- /// ### bytes or ### KB or ### MB or ### GB
+ /// ### bytes or ### KiB or ### MiB or ### GiB or ### TiB
///
public static string FmtSize(long bytes)
- {
- const double K = 1024;
- if (bytes < K) {
- return $"{bytes} B";
- } else if (bytes < K * K) {
- return $"{bytes / K :N1} KiB";
- } else if (bytes < K * K * K) {
- return $"{bytes / K / K :N1} MiB";
- } else {
- return $"{bytes / K / K / K :N1} GiB";
- }
- }
+ => bytes < K ? $"{bytes} B"
+ : bytes < K*K ? $"{bytes /K :N1} KiB"
+ : bytes < K*K*K ? $"{bytes /K/K :N1} MiB"
+ : bytes < K*K*K*K ? $"{bytes /K/K/K :N1} GiB"
+ : $"{bytes /K/K/K/K :N1} TiB";
+
}
public class InvalidModuleAttributesException : Exception
diff --git a/Core/Types/Kraken.cs b/Core/Types/Kraken.cs
index 4c730fa56f..2bc680566b 100644
--- a/Core/Types/Kraken.cs
+++ b/Core/Types/Kraken.cs
@@ -1,4 +1,5 @@
using System;
+using System.IO;
using System.Linq;
using System.Net;
using System.Text;
@@ -38,6 +39,24 @@ public DirectoryNotFoundKraken(string directory, string reason = null, Exception
}
}
+ public class NotEnoughSpaceKraken : Kraken
+ {
+ public DirectoryInfo destination;
+ public long bytesFree;
+ public long bytesToStore;
+
+ public NotEnoughSpaceKraken(string description, DirectoryInfo destination, long bytesFree, long bytesToStore)
+ : base(string.Format(Properties.Resources.KrakenNotEnoughSpace,
+ description, destination,
+ CkanModule.FmtSize(bytesFree),
+ CkanModule.FmtSize(bytesToStore)))
+ {
+ this.destination = destination;
+ this.bytesFree = bytesFree;
+ this.bytesToStore = bytesToStore;
+ }
+ }
+
///
/// A bad install location was provided.
/// Valid locations are GameData, GameRoot, Ships, etc.
diff --git a/GUI/Dialogs/NewRepoDialog.cs b/GUI/Dialogs/NewRepoDialog.cs
index a51bf9012c..2c3dbf9dda 100644
--- a/GUI/Dialogs/NewRepoDialog.cs
+++ b/GUI/Dialogs/NewRepoDialog.cs
@@ -1,5 +1,5 @@
using System;
-using System.Linq;
+using System.Linq;
using System.Windows.Forms;
namespace CKAN.GUI
diff --git a/GUI/Dialogs/SettingsDialog.cs b/GUI/Dialogs/SettingsDialog.cs
index 882a4e901d..7f4358f294 100644
--- a/GUI/Dialogs/SettingsDialog.cs
+++ b/GUI/Dialogs/SettingsDialog.cs
@@ -17,7 +17,8 @@ public partial class SettingsDialog : Form
private IUser m_user;
private long m_cacheSize;
- private int m_cacheFileCount;
+ private int m_cacheFileCount;
+ private long m_cacheFreeSpace;
private IConfiguration config;
private List _sortedRepos = new List();
@@ -128,7 +129,7 @@ private void UpdateCacheInfo(string newPath)
Task.Factory.StartNew(() =>
{
// This might take a little while if the cache is big
- Main.Instance.Manager.Cache.GetSizeInfo(out m_cacheFileCount, out m_cacheSize);
+ Main.Instance.Manager.Cache.GetSizeInfo(out m_cacheFileCount, out m_cacheSize, out long m_cacheFreeSpace);
Util.Invoke(this, () =>
{
if (config.CacheSizeLimit.HasValue)
@@ -137,7 +138,7 @@ private void UpdateCacheInfo(string newPath)
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));
+ CacheSummary.Text = string.Format(Properties.Resources.SettingsDialogSummmary, m_cacheFileCount, CkanModule.FmtSize(m_cacheSize), CkanModule.FmtSize(m_cacheFreeSpace));
CacheSummary.ForeColor = SystemColors.ControlText;
OpenCacheButton.Enabled = true;
ClearCacheButton.Enabled = (m_cacheSize > 0);
diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs
index ac67ecdc66..fbf651fcdc 100644
--- a/GUI/Main/MainInstall.cs
+++ b/GUI/Main/MainInstall.cs
@@ -320,6 +320,10 @@ private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e)
currentUser.RaiseMessage(Properties.Resources.MainInstallBadMetadata, exc.module, exc.Message);
break;
+ case NotEnoughSpaceKraken exc:
+ currentUser.RaiseMessage(exc.Message);
+ break;
+
case FileExistsKraken exc:
if (exc.owningModule != null)
{
diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx
index 3074340a50..73b982affe 100644
--- a/GUI/Properties/Resources.resx
+++ b/GUI/Properties/Resources.resx
@@ -340,7 +340,7 @@ If you suspect a bug in the client: https://github.com/KSP-CKAN/CKAN/issues/new/
{0} (LOCKED)
Failed to fetch master list.
CKAN Plugins (*.dll)|*.dll
- {0} files, {1}
+ {0} files, {1}, {2} free
Invalid path: {0}
Choose a folder for storing CKAN's mod downloads:
Do you really want to delete {0} cached files, freeing {1}?