Skip to content

Commit

Permalink
Merge #3631 Check free space before downloading
Browse files Browse the repository at this point in the history
  • Loading branch information
HebaruSan committed Aug 17, 2022
2 parents 960eda0 + 39d85b1 commit 1d4b745
Show file tree
Hide file tree
Showing 18 changed files with 180 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- [Multiple] Add install size to metadata and display in clients (#3568 by: HebaruSan; reviewed: techman83)
- [CLI] Create a system menu entry for command prompt (#3622 by: HebaruSan; reviewed: techman83)
- [Multiple] Internationalize Core, CmdLine, ConsoleUI, and AutoUpdater (#3482 by: HebaruSan; reviewed: techman83)
- [Multiple] Check free space before downloading (#3631 by: HebaruSan; reviewed: techman83)

## Bugfixes

Expand Down
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

0 comments on commit 1d4b745

Please sign in to comment.