using System;
using System.Collections.Generic;
using System.IO;
namespace OpenTap.Package;
/// Moves files to a temporary location during uninstall.
internal class UninstallContext
{
readonly struct Move(string originalFile, string deletedFile)
{
public string OriginalFile { get; } = originalFile;
public string DeletedFile { get; } = deletedFile;
}
public static UninstallContext Create(Installation installation)
{
// Try to delete files from previous uninstall operations.
// These files could still be in use, so we just delete whatever we can.
DeletePreviouslyUninstalledFiles(installation);
return new UninstallContext(installation);
}
private static readonly TraceSource log = Log.CreateSource("Uninstall");
private readonly Installation install;
private readonly string Target;
private readonly List Moves = [];
private UninstallContext(Installation installation)
{
install = installation;
var uninstallDirectory = GetUninstallDir(install);
Target = Path.Combine(uninstallDirectory, Guid.NewGuid().ToString());
Directory.CreateDirectory(Target);
// Ensure plugins are not loaded from the uninstall directory.
var ignoreFile = Path.Combine(uninstallDirectory, ".OpenTapIgnore");
// a buffersize of 0 is invalid in .NET Framework
int bufferSize = 1;
if (!File.Exists(ignoreFile))
File.Create(ignoreFile, bufferSize).Close();
}
// Previous iterations attempted to uninstall by moving files to the temp directory, but
// File.Move has different semantics when moving files between storage volumes (e.g. C:\ to D:\ drive)
// In such cases, it will copy the file and then attempt to delete it normally. This doesn't work
// on Windows if the file is in use. To work around this limitation, we uninstall files to a subdirectory of
// the OpenTAP installation; this should ensure the file can always be moved.
private static string GetUninstallDir(Installation install) => Path.Combine(install.Directory, ".uninstall");
public static void DeletePreviouslyUninstalledFiles(Installation install)
{
var dir = GetUninstallDir(install);
if (!Directory.Exists(dir)) return;
var subdirs = Directory.GetDirectories(dir);
foreach (var subdir in subdirs)
{
try
{
FileSystemHelper.DeleteDirectory(subdir);
}
catch
{
// ignore
}
}
}
public bool Delete(PackageFile file)
{
return Delete(file.RelativeDestinationPath);
}
public bool Delete(string path)
{
var fullname = Path.Combine(install.Directory, path);
if (File.Exists(fullname))
{
var destination = Path.Combine(Target, path);
try
{
Directory.CreateDirectory(Path.GetDirectoryName(destination));
File.Move(fullname, destination);
Moves.Add(new Move(fullname, destination));
log.Debug("File {0} moved to {1}.", fullname, destination);
}
catch(Exception e)
{
log.Error("Unable to move file {0} to {1}: {2}", fullname, destination, e.Message);
log.Debug(e);
return false;
}
}
return true;
}
public void UndoAllDeletions()
{
foreach (var move in Moves)
{
File.Move(move.DeletedFile, move.OriginalFile);
log.Debug("File {0} reverted to {1}.", move.DeletedFile, move.OriginalFile);
}
Moves.Clear();
}
}