Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check free space before downloading #3631

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions Cmdline/Action/Cache.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion Cmdline/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Update recommended!</value></data>
<data name="CacheReset" xml:space="preserve"><value>Download cache reset to {0}</value></data>
<data name="CacheResetFailed" xml:space="preserve"><value>Can't reset cache path: {0}</value></data>
<data name="CacheUnlimited" xml:space="preserve"><value>Unlimited</value></data>
<data name="CacheInfo" xml:space="preserve"><value>{0} files, {1}</value></data>
<data name="CacheInfo" xml:space="preserve"><value>{0} files, {1}, {2} free</value></data>
<data name="CompareSame" xml:space="preserve"><value>"{0}" and "{1}" are the same versions.</value></data>
<data name="CompareLower" xml:space="preserve"><value>"{0}" is lower than "{1}".</value></data>
<data name="CompareHigher" xml:space="preserve"><value>"{0}" is higher than "{1}".</value></data>
Expand Down
19 changes: 18 additions & 1 deletion Core/CKANPathUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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");
}

}
}
54 changes: 54 additions & 0 deletions Core/Extensions/IOExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
}

/// <summary>
/// Check whether a given path is an ancestor of another
/// </summary>
/// <param name="parent">The path to treat as potential ancestor</param>
/// <param name="child">The path to treat as potential descendant</param>
/// <returns>true if child is a descendant of parent, false otherwise</returns>
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));

/// <summary>
/// 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.
/// </summary>
/// <param name="dir">Any DirectoryInfo object</param>
/// <returns>The DriveInfo associated with this directory</returns>
public static DriveInfo GetDrive(this DirectoryInfo dir)
=> DriveInfo.GetDrives()
.Where(dr => dr.RootDirectory.IsAncestorOf(dir))
.OrderByDescending(dr => dr.RootDirectory.FullName.Length)
.FirstOrDefault();

}
}
13 changes: 13 additions & 0 deletions Core/ModuleInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ public void InstallList(ICollection<CkanModule> modules, RelationshipResolverOpt
var modsToInstall = resolver.ModList().ToList();
List<CkanModule> downloads = new List<CkanModule>();

// 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.

Expand Down Expand Up @@ -178,6 +184,13 @@ public void InstallList(ICollection<CkanModule> 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())
{
Expand Down
2 changes: 1 addition & 1 deletion Core/Net/IDownloader.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;

namespace CKAN
{
Expand Down
10 changes: 10 additions & 0 deletions Core/Net/NetAsyncModulesDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;

using ChinhDo.Transactions.FileManager;
using log4net;

namespace CKAN
Expand Down Expand Up @@ -62,6 +64,14 @@ public void DownloadModules(IEnumerable<CkanModule> 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
Expand Down
15 changes: 11 additions & 4 deletions Core/Net/NetFileCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,13 @@ public string GetCachedZip(Uri url)
/// </summary>
/// <param name="numFiles">Output parameter set to number of files in cache</param>
/// <param name="numBytes">Output parameter set to number of bytes in cache</param>
public void GetSizeInfo(out int numFiles, out long numBytes)
/// <param name="bytesFree">Output parameter set to number of bytes free</param>
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);
Expand All @@ -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<string> legacyDirs()
{
return manager?.Instances.Values
Expand All @@ -310,9 +319,7 @@ private HashSet<string> 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
Expand Down
9 changes: 7 additions & 2 deletions Core/Net/NetModuleCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}


/// <summary>
/// Calculate the SHA1 hash of a file
Expand Down
12 changes: 12 additions & 0 deletions Core/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Core/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,13 @@ Consider adding an authentication token to increase the throttling limit.</value
If you're certain this is not the case, then delete:
"{0}"</value></data>
<data name="KrakenReinstallModule" xml:space="preserve"><value>Metadata changed, reinstallation recommended: {0}</value></data>
<data name="NotEnoughSpaceToDownload" xml:space="preserve"><value>Not enough space in temp folder to download modules!</value></data>
<data name="NotEnoughSpaceToCache" xml:space="preserve"><value>Not enough space in cache folder to store modules!</value></data>
<data name="NotEnoughSpaceToInstall" xml:space="preserve"><value>Not enough space in game folder to install modules!</value></data>
<data name="KrakenNotEnoughSpace" xml:space="preserve"><value>{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.
</value></data>
<data name="RelationshipResolverConflictsWith" xml:space="preserve"><value>{0} conflicts with {1}</value></data>
<data name="RelationshipResolverRequiredButResolver" xml:space="preserve"><value>{0} required, but an incompatible version is in the resolver</value></data>
<data name="RelationshipResolverRequiredButInstalled" xml:space="preserve"><value>{0} required, but an incompatible version is installed</value></data>
Expand Down
22 changes: 9 additions & 13 deletions Core/Types/CkanModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -727,26 +727,22 @@ public Uri InternetArchiveDownload
}
}

private const double K = 1024;

/// <summary>
/// Format a byte count into readable file size
/// </summary>
/// <param name="bytes">Number of bytes in a file</param>
/// <returns>
/// ### bytes or ### KB or ### MB or ### GB
/// ### bytes or ### KiB or ### MiB or ### GiB or ### TiB
/// </returns>
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
Expand Down
19 changes: 19 additions & 0 deletions Core/Types/Kraken.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
Expand Down Expand Up @@ -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;
}
}

/// <summary>
/// A bad install location was provided.
/// Valid locations are GameData, GameRoot, Ships, etc.
Expand Down
2 changes: 1 addition & 1 deletion GUI/Dialogs/NewRepoDialog.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System;
using System.Linq;
using System.Linq;
using System.Windows.Forms;

namespace CKAN.GUI
Expand Down
7 changes: 4 additions & 3 deletions GUI/Dialogs/SettingsDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Repository> _sortedRepos = new List<Repository>();
Expand Down Expand Up @@ -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)
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions GUI/Main/MainInstall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Loading