Skip to content

Commit

Permalink
Prompt user to overwrite manually installed files
Browse files Browse the repository at this point in the history
  • Loading branch information
HebaruSan committed May 5, 2020
1 parent 51d8f85 commit 0359937
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 12 deletions.
121 changes: 118 additions & 3 deletions Core/ModuleInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,13 +290,13 @@ private void Install(CkanModule module, bool autoInstalled, Registry registry, s
ModuleVersion version = registry.InstalledVersion(module.identifier);

// TODO: This really should be handled by higher-up code.
if (version != null)
if (version != null && !(version is UnmanagedModuleVersion))
{
User.RaiseMessage(" {0} {1} already installed, skipped", module.identifier, version);
return;
}

// Find our in the cache if we don't already have it.
// Find ZIP in the cache if we don't already have it.
filename = filename ?? Cache.GetCachedZip(module);

// If we *still* don't have a file, then kraken bitterly.
Expand Down Expand Up @@ -348,6 +348,7 @@ private static void CheckKindInstallationKraken(CkanModule module)
/// Installs the module from the zipfile provided.
/// Returns a list of files installed.
/// Propagates a BadMetadataKraken if our install metadata is bad.
/// Propagates a CancelledActionKraken if the user decides not to overwite unowned files.
/// Propagates a FileExistsKraken if we were going to overwrite a file.
/// </summary>
private IEnumerable<string> InstallModule(CkanModule module, string zip_filename, Registry registry)
Expand All @@ -360,6 +361,32 @@ private IEnumerable<string> InstallModule(CkanModule module, string zip_filename

try
{
var dll = registry.DllPath(module.identifier);
if (dll != null && !files.Any(f => ksp.ToRelativeGameDir(f.destination) == dll))
{
throw new DllLocationMismatchKraken(dll, $"DLL for module {module.identifier} found at {dll}, but it's not where CKAN would install it. Aborting to prevent multiple copies of the same mod being installed. To install this module, uninstall it manually and try again.");
}

// Look for overwritable files if session is interactive
if (!User.Headless)
{
var conflicting = FindConflictingFiles(zipfile, files, registry).Memoize();
if (conflicting.Any())
{
var fileMsg = conflicting
.OrderBy(c => c.Value)
.Aggregate("", (a, b) =>
$"{a}\r\n- {ksp.ToRelativeGameDir(b.Key.destination)} ({(b.Value ? "same" : "DIFFERENT")})");
if (User.RaiseYesNoDialog($"Module {module.name} wants to overwrite the following manually installed files:\r\n{fileMsg}\r\n\r\nOverwrite?"))
{
DeleteConflictingFiles(conflicting.Select(f => f.Key));
}
else
{
throw new CancelledActionKraken($"Not overwriting manually installed files, can't install {module.name}.");
}
}
}
foreach (InstallableFile file in files)
{
log.DebugFormat("Copying {0}", file.source.Name);
Expand All @@ -380,6 +407,94 @@ private IEnumerable<string> InstallModule(CkanModule module, string zip_filename
}
}

/// <summary>
/// Find files in the given list that are already installed and unowned.
/// Note, this compares files on demand; Memoize for performance!
/// </summary>
/// <param name="files">Files that we want to install for a module</param>
/// <returns>
/// List of pairs: Key = file, Value = true if identical, false if different
/// </returns>
private IEnumerable<KeyValuePair<InstallableFile, bool>> FindConflictingFiles(ZipFile zip, IEnumerable<InstallableFile> files, Registry registry)
{
foreach (InstallableFile file in files)
{
if (File.Exists(file.destination)
&& registry.FileOwner(ksp.ToRelativeGameDir(file.destination)) == null)
{
log.DebugFormat("Comparing {0}", file.destination);
using (Stream zipStream = zip.GetInputStream(file.source))
using (FileStream curFile = new FileStream(file.destination, FileMode.Open, FileAccess.Read))
{
yield return new KeyValuePair<InstallableFile, bool>(
file,
file.source.Size == curFile.Length
&& StreamsEqual(zipStream, curFile)
);
}
}
}
}

/// <summary>
/// Compare the contents of two streams
/// </summary>
/// <param name="s1">First stream to compare</param>
/// <param name="s2">Second stream to compare</param>
/// <returns>
/// true if both streams contain same bytes, false otherwise
/// </returns>
private bool StreamsEqual(Stream s1, Stream s2)
{
const int bufLen = 1024;
byte[] bytes1 = new byte[bufLen];
byte[] bytes2 = new byte[bufLen];
for (int bytesChecked = 0; bytesChecked < s1.Length; )
{
int bytesFrom1 = s1.Read(bytes1, 0, bufLen);
int bytesFrom2 = s2.Read(bytes2, 0, bufLen);
if (bytesFrom1 != bytesFrom2)
{
// One ended early, not equal.
log.DebugFormat("Read {0} bytes from stream1 and {1} bytes from stream2", bytesFrom1, bytesFrom2);
return false;
}
for (int i = 0; i < bytesFrom1; ++i)
{
if (bytes1[i] != bytes2[i])
{
log.DebugFormat("Byte {0} doesn't match", bytesChecked + i);
// Bytes don't match, not equal.
return false;
}
}
bytesChecked += bytesFrom1;
}
// Same bytes, they're equal.
return true;
}

/// <summary>
/// Remove files that the user chose to overwrite, so
/// the installer can replace them.
/// Uses a transaction so they can be undeleted if the install
/// fails at a later stage.
/// </summary>
/// <param name="files">The files to overwrite</param>
private void DeleteConflictingFiles(IEnumerable<InstallableFile> files)
{
TxFileManager file_transaction = new TxFileManager();
using (var transaction = CkanTransaction.CreateTransactionScope())
{
foreach (InstallableFile file in files)
{
log.DebugFormat("Trying to delete {0}", file.destination);
file_transaction.Delete(file.destination);
}
transaction.Complete();
}
}

/// <summary>
/// Checks the path against a list of reserved game directories
/// </summary>
Expand Down Expand Up @@ -698,7 +813,7 @@ internal static void CopyZipEntry(ZipFile zipfile, ZipEntry entry, string fullPa
}

// We don't allow for the overwriting of files. See #208.
if (File.Exists(fullPath))
if (file_transaction.FileExists(fullPath))
{
throw new FileExistsKraken(fullPath, string.Format("Trying to write {0} but it already exists.", fullPath));
}
Expand Down
9 changes: 9 additions & 0 deletions Core/Registry/Registry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ [JsonIgnore] public IEnumerable<string> InstalledDlls
get { return installed_dlls.Keys; }
}

/// <summary>
/// Returns the file path of a DLL.
/// null if not found.
/// </summary>
public string DllPath(string identifier)
{
return installed_dlls.TryGetValue(identifier, out string path) ? path : null;
}

/// <summary>
/// A map between module identifiers and versions for official DLC that are installed.
/// </summary>
Expand Down
19 changes: 17 additions & 2 deletions Core/Types/Kraken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ public InstanceNameTakenKraken(string name, string reason = null)
this.instName = name;
}
}

public class ModuleIsDLCKraken : Kraken
{
/// <summary>
Expand All @@ -581,5 +581,20 @@ public ModuleIsDLCKraken(CkanModule module, string reason = null)
this.module = module;
}
}


/// <summary>
/// A manually installed mod is installed somewhere other than
/// where CKAN would install it, so we can't safely overwrite it.
/// </summary>
public class DllLocationMismatchKraken : Kraken
{
public readonly string path;

public DllLocationMismatchKraken(string path, string reason = null)
: base(reason)
{
this.path = path;
}
}

}
11 changes: 8 additions & 3 deletions GUI/Dialogs/YesNoDialog.Designer.cs

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

14 changes: 13 additions & 1 deletion GUI/Dialogs/YesNoDialog.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading.Tasks;
Expand All @@ -19,10 +20,20 @@ public DialogResult ShowYesNoDialog(Form parentForm, string text, string yesText

Util.Invoke(parentForm, () =>
{
var height = StringHeight(text, ClientSize.Width - 25) + 2 * 54;
DescriptionLabel.Text = text;
DescriptionLabel.TextAlign = text.Contains("\n")
? HorizontalAlignment.Left
: HorizontalAlignment.Center;
DescriptionLabel.ScrollBars = height < maxHeight
? ScrollBars.None
: ScrollBars.Vertical;
YesButton.Text = yesText ?? defaultYes;
NoButton.Text = noText ?? defaultNo;
ClientSize = new Size(ClientSize.Width, StringHeight(text, ClientSize.Width - 25) + 2 * 54);
ClientSize = new Size(
ClientSize.Width,
Math.Min(maxHeight, height)
);
task.SetResult(ShowDialog(parentForm));
});

Expand All @@ -47,6 +58,7 @@ public void HideYesNoDialog()
Util.Invoke(this, Close);
}

private const int maxHeight = 600;
private TaskCompletionSource<DialogResult> task;
private string defaultYes;
private string defaultNo;
Expand Down
11 changes: 8 additions & 3 deletions GUI/Main/MainInstall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,10 @@ out Dictionary<CkanModule, HashSet<string>> supporters
currentUser.RaiseMessage(ex.InconsistenciesPretty);
return;
}
catch (CancelledActionKraken)
catch (CancelledActionKraken kraken)
{
currentUser.RaiseMessage(kraken.Message);
installCanceled = true;
return;
}
catch (MissingCertificateKraken kraken)
Expand Down Expand Up @@ -335,6 +337,11 @@ out Dictionary<CkanModule, HashSet<string>> supporters
currentUser.RaiseError(msg);
return;
}
catch (DllLocationMismatchKraken kraken)
{
currentUser.RaiseMessage(kraken.Message);
return;
}
}
}

Expand Down Expand Up @@ -410,8 +417,6 @@ private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e)
else if (installCanceled)
{
// User cancelled the installation
// Rebuilds the list of GUIMods
UpdateModsList(ChangeSet);
if (result.Key) {
FailWaitDialog(
Properties.Resources.MainInstallCancelTooLate,
Expand Down

0 comments on commit 0359937

Please sign in to comment.