mirror of
https://github.com/aharabada/glitchy-engine-beef.git
synced 2026-09-05 21:01:52 +00:00
Fixed copying with multiple conflict handling flags,
made canceling and pausing of tasks more robust
This commit is contained in:
@@ -50,7 +50,7 @@ abstract class BackgroundTask
|
|||||||
|
|
||||||
public bool Ended => Finished || Aborted;
|
public bool Ended => Finished || Aborted;
|
||||||
|
|
||||||
public bool DeleteWhenStopped { get; set; }
|
public bool DeleteWhenEnded { get; set; }
|
||||||
|
|
||||||
public abstract RunResult Run();
|
public abstract RunResult Run();
|
||||||
|
|
||||||
@@ -63,10 +63,7 @@ abstract class BackgroundTask
|
|||||||
|
|
||||||
public void Pause()
|
public void Pause()
|
||||||
{
|
{
|
||||||
if (State == .Ready || State == .Running)
|
_taskManager.Pause(this);
|
||||||
{
|
|
||||||
State = .Paused;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Abort()
|
public void Abort()
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class BackgroundTaskManager
|
|||||||
private append BackgroundTaskList _waitingTasks = .();
|
private append BackgroundTaskList _waitingTasks = .();
|
||||||
private append Monitor _queueLock = .();
|
private append Monitor _queueLock = .();
|
||||||
|
|
||||||
|
private append Dictionary<BackgroundTask, BackgroundTask.RunState> _deferredStateChanges = .();
|
||||||
|
|
||||||
public void Init()
|
public void Init()
|
||||||
{
|
{
|
||||||
_run = true;
|
_run = true;
|
||||||
@@ -54,6 +56,25 @@ class BackgroundTaskManager
|
|||||||
BackgroundTask.RunResult runResult = task.Run();
|
BackgroundTask.RunResult runResult = task.Run();
|
||||||
_runningTask = null;
|
_runningTask = null;
|
||||||
|
|
||||||
|
if (_deferredStateChanges.TryGetValue(task, let desiredState))
|
||||||
|
{
|
||||||
|
// If the task already finished or aborted by itself, we don't care about the desired state, because it no longer applies.
|
||||||
|
if (runResult != .Finished && runResult != .Abort)
|
||||||
|
{
|
||||||
|
switch (desiredState)
|
||||||
|
{
|
||||||
|
case .Paused:
|
||||||
|
runResult = .Pause;
|
||||||
|
case .Aborted:
|
||||||
|
runResult = .Abort;
|
||||||
|
default:
|
||||||
|
Log.EngineLogger.Error($"Tasks can't explicitly switch to state {desiredState}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_deferredStateChanges.Remove(task);
|
||||||
|
}
|
||||||
|
|
||||||
switch (runResult)
|
switch (runResult)
|
||||||
{
|
{
|
||||||
case .Continue:
|
case .Continue:
|
||||||
@@ -71,14 +92,14 @@ class BackgroundTaskManager
|
|||||||
case .Finished:
|
case .Finished:
|
||||||
task.State = .Finished;
|
task.State = .Finished;
|
||||||
|
|
||||||
if (task.DeleteWhenStopped)
|
if (task.DeleteWhenEnded)
|
||||||
{
|
{
|
||||||
delete task;
|
delete task;
|
||||||
}
|
}
|
||||||
case .Abort:
|
case .Abort:
|
||||||
task.State = .Aborted;
|
task.State = .Aborted;
|
||||||
|
|
||||||
if (task.DeleteWhenStopped)
|
if (task.DeleteWhenEnded)
|
||||||
{
|
{
|
||||||
delete task;
|
delete task;
|
||||||
}
|
}
|
||||||
@@ -123,21 +144,10 @@ class BackgroundTaskManager
|
|||||||
Log.EngineLogger.AssertDebug(task._taskManager == this);
|
Log.EngineLogger.AssertDebug(task._taskManager == this);
|
||||||
Log.EngineLogger.AssertDebug(!task.Ended);
|
Log.EngineLogger.AssertDebug(!task.Ended);
|
||||||
|
|
||||||
Application.Instance.InvokeOnMainThread(new () =>
|
if (task.State == .Paused)
|
||||||
{
|
{
|
||||||
if (task.State == .Paused)
|
_deferredStateChanges.Add(task, .Ready);
|
||||||
{
|
}
|
||||||
task.State = .Ready;
|
|
||||||
|
|
||||||
using (_queueLock.Enter())
|
|
||||||
{
|
|
||||||
Log.EngineLogger.AssertDebug(!_readyQueue.Contains(task));
|
|
||||||
// We assume this was a user interaction so this task will get some priority.
|
|
||||||
_readyQueue.AddFirst(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Pause(BackgroundTask task)
|
internal void Pause(BackgroundTask task)
|
||||||
@@ -145,20 +155,10 @@ class BackgroundTaskManager
|
|||||||
Log.EngineLogger.AssertDebug(task._taskManager == this);
|
Log.EngineLogger.AssertDebug(task._taskManager == this);
|
||||||
Log.EngineLogger.AssertDebug(!task.Ended);
|
Log.EngineLogger.AssertDebug(!task.Ended);
|
||||||
|
|
||||||
Application.Instance.InvokeOnMainThread(new () =>
|
if (task.State == .Ready || task.State == .Running)
|
||||||
{
|
{
|
||||||
if (task.State == .Ready)
|
_deferredStateChanges.Add(task, .Paused);
|
||||||
{
|
}
|
||||||
task.State = .Ready;
|
|
||||||
|
|
||||||
using (_queueLock.Enter())
|
|
||||||
{
|
|
||||||
Log.EngineLogger.AssertDebug(!_waitingTasks.Contains(task));
|
|
||||||
_waitingTasks.AddLast(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AbortTask(BackgroundTask task)
|
internal void AbortTask(BackgroundTask task)
|
||||||
@@ -166,24 +166,10 @@ class BackgroundTaskManager
|
|||||||
Log.EngineLogger.AssertDebug(task._taskManager == this);
|
Log.EngineLogger.AssertDebug(task._taskManager == this);
|
||||||
Log.EngineLogger.AssertDebug(!task.Ended);
|
Log.EngineLogger.AssertDebug(!task.Ended);
|
||||||
|
|
||||||
Application.Instance.InvokeOnMainThread(new () =>
|
if (task.State != .Aborted)
|
||||||
{
|
{
|
||||||
if (task.State != .Aborted)
|
_deferredStateChanges.Add(task, .Aborted);
|
||||||
{
|
}
|
||||||
task.State = .Aborted;
|
|
||||||
|
|
||||||
using (_queueLock.Enter())
|
|
||||||
{
|
|
||||||
_waitingTasks.Remove(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.DeleteWhenStopped)
|
|
||||||
{
|
|
||||||
delete task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ImGuiRender()
|
public void ImGuiRender()
|
||||||
@@ -211,6 +197,59 @@ class BackgroundTaskManager
|
|||||||
wtask.OnRenderPopup();
|
wtask.OnRenderPopup();
|
||||||
wtask = wtask._next;
|
wtask = wtask._next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (var (task, desiredState) in _deferredStateChanges)
|
||||||
|
{
|
||||||
|
bool deleteEntry = true;
|
||||||
|
|
||||||
|
switch (desiredState)
|
||||||
|
{
|
||||||
|
case .Ready:
|
||||||
|
task.State = .Ready;
|
||||||
|
// We assume this was a user interaction so this task will get priority.
|
||||||
|
_readyQueue.AddFirst(task);
|
||||||
|
case .Paused:
|
||||||
|
if (task.Ready)
|
||||||
|
{
|
||||||
|
Log.EngineLogger.AssertDebug(!_waitingTasks.Contains(task));
|
||||||
|
_waitingTasks.AddLast(task);
|
||||||
|
}
|
||||||
|
else if (task.Running)
|
||||||
|
{
|
||||||
|
// The task is running, we have to hope that it cooperates and handle the rest in the worker thread.
|
||||||
|
deleteEntry = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
task.State = .Paused;
|
||||||
|
case .Aborted:
|
||||||
|
if (task.Ready || task.Paused)
|
||||||
|
{
|
||||||
|
_waitingTasks.Remove(task);
|
||||||
|
|
||||||
|
task.State = .Aborted;
|
||||||
|
|
||||||
|
if (task.DeleteWhenEnded)
|
||||||
|
{
|
||||||
|
delete task;
|
||||||
|
task = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (task.Running)
|
||||||
|
{
|
||||||
|
task.State = .Aborted;
|
||||||
|
|
||||||
|
// The task is running, we have to hope that it cooperates and handle the rest in the worker thread.
|
||||||
|
deleteEntry = false;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
Log.EngineLogger.Error($"Tasks can't explicitly switch to state {desiredState}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteEntry)
|
||||||
|
{
|
||||||
|
@task.Remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using ImGui;
|
using ImGui;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace GlitchyEditor.Multithreading;
|
namespace GlitchyEditor.Multithreading;
|
||||||
|
|
||||||
@@ -14,20 +15,25 @@ class CopyBackgroundTask : BackgroundTask
|
|||||||
private int _totalEntriesToCopy;
|
private int _totalEntriesToCopy;
|
||||||
private append Queue<CopyInfo> _pathsToCopy = .() ~ ClearAndDeleteItems!(_);
|
private append Queue<CopyInfo> _pathsToCopy = .() ~ ClearAndDeleteItems!(_);
|
||||||
private append Queue<CopyInfo> _scanQueue = .() ~ ClearAndDeleteItems!(_);
|
private append Queue<CopyInfo> _scanQueue = .() ~ ClearAndDeleteItems!(_);
|
||||||
|
/// Contains the file paths, that will definitely exist after copying. This is used for file renaming during scanning.
|
||||||
|
private append HashSet<StringView> _targetPaths = .();
|
||||||
|
|
||||||
private enum OverwriteMode
|
private enum ConflictResolutionMode
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
OverwriteFile = 1,
|
OverwriteFile = 1,
|
||||||
KeepBoth = 2,
|
KeepBoth = 2,
|
||||||
CombineDirectories = 4,
|
CombineDirectories = 4,
|
||||||
Skip = 8
|
SkipFiles = 8,
|
||||||
|
SkipDirectories = 16,
|
||||||
|
SkipNotFound = 32,
|
||||||
|
IgnoreUnexpectedErrors = 64
|
||||||
}
|
}
|
||||||
|
|
||||||
private OverwriteMode _nextFileMode;
|
private ConflictResolutionMode _nextFileMode;
|
||||||
private OverwriteMode _allFilesMode;
|
private ConflictResolutionMode _allFilesMode;
|
||||||
|
|
||||||
private OverwriteMode CurrentFileMode => _nextFileMode | _allFilesMode;
|
private ConflictResolutionMode CurrentFileMode => _nextFileMode | _allFilesMode;
|
||||||
|
|
||||||
private class CopyInfo
|
private class CopyInfo
|
||||||
{
|
{
|
||||||
@@ -35,14 +41,14 @@ class CopyBackgroundTask : BackgroundTask
|
|||||||
// If this entry is actually inside a directory we copy, this contains the path of this entry inside this directory.
|
// If this entry is actually inside a directory we copy, this contains the path of this entry inside this directory.
|
||||||
public append String SubPath = .();
|
public append String SubPath = .();
|
||||||
public append String TargetPath = .();
|
public append String TargetPath = .();
|
||||||
public OverwriteMode OverwriteMode;
|
public ConflictResolutionMode ConflictResolutionMode;
|
||||||
|
|
||||||
[AllowAppend]
|
[AllowAppend]
|
||||||
public this(StringView sourcePath, StringView targetPath, OverwriteMode overwriteMode)
|
public this(StringView sourcePath, StringView targetPath, ConflictResolutionMode conflictResolutionMode)
|
||||||
{
|
{
|
||||||
SourcePath.Set(sourcePath);
|
SourcePath.Set(sourcePath);
|
||||||
TargetPath.Set(targetPath);
|
TargetPath.Set(targetPath);
|
||||||
OverwriteMode = overwriteMode;
|
ConflictResolutionMode = conflictResolutionMode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +76,10 @@ class CopyBackgroundTask : BackgroundTask
|
|||||||
{
|
{
|
||||||
case None;
|
case None;
|
||||||
case TargetDirectoryIsNotDirectory;
|
case TargetDirectoryIsNotDirectory;
|
||||||
case SourceDoesntExist(String SourcePath);
|
|
||||||
case TargetFileExists(String FileName);
|
case TargetFileExists(String FileName);
|
||||||
case TargetDirectoryExists(String DirectoryName);
|
case TargetDirectoryExists(String DirectoryName);
|
||||||
|
case EntryNotFound(String SourcePath);
|
||||||
|
case UnexpectedError(String Message);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -80,12 +87,14 @@ class CopyBackgroundTask : BackgroundTask
|
|||||||
{
|
{
|
||||||
case .None:
|
case .None:
|
||||||
case .TargetDirectoryIsNotDirectory:
|
case .TargetDirectoryIsNotDirectory:
|
||||||
case .SourceDoesntExist(let SourcePath):
|
|
||||||
delete SourcePath;
|
|
||||||
case .TargetFileExists(let FileName):
|
case .TargetFileExists(let FileName):
|
||||||
delete FileName;
|
delete FileName;
|
||||||
case .TargetDirectoryExists(let DirectoryName):
|
case .TargetDirectoryExists(let DirectoryName):
|
||||||
delete DirectoryName;
|
delete DirectoryName;
|
||||||
|
case .EntryNotFound(let SourcePath):
|
||||||
|
delete SourcePath;
|
||||||
|
case .UnexpectedError(let Message):
|
||||||
|
delete Message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,10 +110,16 @@ class CopyBackgroundTask : BackgroundTask
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AddToCopyQueue(CopyInfo entry, StringView targetPath, ConflictResolutionMode conflictResolutionMode)
|
||||||
|
{
|
||||||
|
entry.TargetPath.Set(targetPath);
|
||||||
|
entry.ConflictResolutionMode = conflictResolutionMode;
|
||||||
|
_pathsToCopy.Add(entry);
|
||||||
|
_targetPaths.Add(entry.TargetPath);
|
||||||
|
}
|
||||||
|
|
||||||
while (!_scanQueue.IsEmpty && Running)
|
while (!_scanQueue.IsEmpty && Running)
|
||||||
{
|
{
|
||||||
//Thread.Sleep(1000);
|
|
||||||
|
|
||||||
currentEntry = _scanQueue.PopFront();
|
currentEntry = _scanQueue.PopFront();
|
||||||
|
|
||||||
let sourcePath = (StringView)currentEntry.SourcePath;
|
let sourcePath = (StringView)currentEntry.SourcePath;
|
||||||
@@ -117,49 +132,90 @@ class CopyBackgroundTask : BackgroundTask
|
|||||||
|
|
||||||
if (Directory.Exists(sourcePath))
|
if (Directory.Exists(sourcePath))
|
||||||
{
|
{
|
||||||
if (Directory.Exists(targetPath))
|
bool skip = false;
|
||||||
|
|
||||||
|
if (Directory.Exists(targetPath) || _targetPaths.Contains(targetPath))
|
||||||
{
|
{
|
||||||
switch (CurrentFileMode)
|
if (CurrentFileMode.HasFlag(.KeepBoth))
|
||||||
|
{
|
||||||
|
Path.FindFreePath(_targetDirectoryPath, fileName, "", targetPath, fileName, _targetPaths);
|
||||||
|
}
|
||||||
|
else if (CurrentFileMode.HasFlag(.SkipDirectories))
|
||||||
|
{
|
||||||
|
skip = true;
|
||||||
|
}
|
||||||
|
else if (Enum.HasAnyFlag(CurrentFileMode, .CombineDirectories))
|
||||||
|
{
|
||||||
|
// Intentionally do nothing.
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
case .KeepBoth, .CombineDirectories, .Skip:
|
|
||||||
default:
|
|
||||||
return .Err(.TargetDirectoryExists(new String(fileName)));
|
return .Err(.TargetDirectoryExists(new String(fileName)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CurrentFileMode.HasFlag(.Skip))
|
if (skip)
|
||||||
{
|
{
|
||||||
_pathsToCopy.Add(currentEntry);
|
delete currentEntry;
|
||||||
currentEntry.OverwriteMode = CurrentFileMode;
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Current node is ready, add it to copy-queue.
|
||||||
|
AddToCopyQueue(currentEntry, targetPath, CurrentFileMode);
|
||||||
|
|
||||||
|
// Add nested files and directories to the scan-queue.
|
||||||
for (FileFindEntry e in Directory.Enumerate(currentEntry.SourcePath))
|
for (FileFindEntry e in Directory.Enumerate(currentEntry.SourcePath))
|
||||||
{
|
{
|
||||||
let entryPath = scope String();
|
let entryPath = scope String();
|
||||||
e.GetFilePath(entryPath);
|
e.GetFilePath(entryPath);
|
||||||
|
|
||||||
let newEntry = new CopyInfo(entryPath, "", .None);
|
let newEntry = new CopyInfo(entryPath, targetPath, .None);
|
||||||
_scanQueue.Add(newEntry);
|
_scanQueue.Add(newEntry);
|
||||||
|
|
||||||
Path.Combine(newEntry.SubPath, currentEntry.SubPath, fileName);
|
Path.Combine(newEntry.SubPath, currentEntry.SubPath, fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_nextFileMode = .None;
|
_nextFileMode = .None;
|
||||||
}
|
}
|
||||||
else if (File.Exists(sourcePath))
|
else if (File.Exists(sourcePath))
|
||||||
{
|
{
|
||||||
if (File.Exists(targetPath))
|
bool skip = false;
|
||||||
|
|
||||||
|
if (File.Exists(targetPath) || _targetPaths.Contains(targetPath))
|
||||||
{
|
{
|
||||||
switch (CurrentFileMode)
|
if (CurrentFileMode.HasFlag(.KeepBoth))
|
||||||
|
{
|
||||||
|
let fileExtension = scope String();
|
||||||
|
Path.GetExtension(targetPath, fileExtension);
|
||||||
|
|
||||||
|
StringView fileNameWithoutExtension = fileName.Substring(0..<^fileExtension.Length);
|
||||||
|
|
||||||
|
Path.FindFreePath(_targetDirectoryPath, fileNameWithoutExtension, fileExtension, targetPath, fileName, _targetPaths);
|
||||||
|
}
|
||||||
|
else if (CurrentFileMode.HasFlag(.SkipFiles))
|
||||||
|
{
|
||||||
|
skip = true;
|
||||||
|
}
|
||||||
|
else if (Enum.HasAnyFlag(CurrentFileMode, .OverwriteFile))
|
||||||
|
{
|
||||||
|
// Intentionally do nothing.
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
case .KeepBoth, .OverwriteFile, .Skip:
|
|
||||||
default:
|
|
||||||
return .Err(.TargetFileExists(new String(fileName)));
|
return .Err(.TargetFileExists(new String(fileName)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CurrentFileMode.HasFlag(.Skip))
|
if (skip)
|
||||||
{
|
{
|
||||||
_pathsToCopy.Add(currentEntry);
|
delete currentEntry;
|
||||||
currentEntry.OverwriteMode = CurrentFileMode;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddToCopyQueue(currentEntry, targetPath, CurrentFileMode);
|
||||||
|
}
|
||||||
|
|
||||||
_nextFileMode = .None;
|
_nextFileMode = .None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,78 +231,76 @@ class CopyBackgroundTask : BackgroundTask
|
|||||||
{
|
{
|
||||||
_currentError.Dispose();
|
_currentError.Dispose();
|
||||||
_currentError = .None;
|
_currentError = .None;
|
||||||
if (CollectFilesToCopy() case .Err(out _currentError))
|
if (CollectFilesToCopy() case .Err(out _currentError) || Paused)
|
||||||
{
|
{
|
||||||
return .Pause;
|
return .Pause;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!_pathsToCopy.IsEmpty && Running)
|
while (!_pathsToCopy.IsEmpty && Running)
|
||||||
{
|
{
|
||||||
//Thread.Sleep(1000);
|
|
||||||
CopyInfo currentPath = _pathsToCopy.PopFront();
|
CopyInfo currentPath = _pathsToCopy.PopFront();
|
||||||
|
|
||||||
defer
|
if (CopyPath(currentPath) case .Err(out _currentError) || Paused)
|
||||||
{
|
{
|
||||||
delete currentPath;
|
return .Pause;
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyPath(currentPath);
|
delete currentPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
return .Finished;
|
return .Finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result<void> CopyPath(CopyInfo copyInfo)
|
private Result<void, CopyError> CopyPath(CopyInfo copyInfo)
|
||||||
{
|
{
|
||||||
if (Directory.Exists(copyInfo.SourcePath))
|
if (Directory.Exists(copyInfo.SourcePath))
|
||||||
{
|
{
|
||||||
//bool forceOverwrite = false;
|
|
||||||
if (Directory.Exists(copyInfo.TargetPath))
|
if (Directory.Exists(copyInfo.TargetPath))
|
||||||
{
|
{
|
||||||
switch (CurrentFileMode)
|
if (copyInfo.ConflictResolutionMode.HasFlag(.CombineDirectories))
|
||||||
{
|
{
|
||||||
case .KeepBoth:
|
// We don't have to do anything, because we will be using the target directory.
|
||||||
String fileName = scope .();
|
|
||||||
Path.GetFileName(copyInfo.SourcePath, fileName);
|
|
||||||
|
|
||||||
Path.FindFreePath(_targetDirectoryPath, fileName, "", copyInfo.TargetPath..Clear());
|
|
||||||
case .CombineDirectories:
|
|
||||||
return .Ok;
|
return .Ok;
|
||||||
default:
|
}
|
||||||
return .Err;//(.TargetDirectoryExists);
|
else if (!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
|
||||||
|
{
|
||||||
|
return .Err(.UnexpectedError(new $"The target directory already exists, but it wasn't properly handled during the scanning phase."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(copyInfo.TargetPath);
|
if (Directory.CreateDirectory(copyInfo.TargetPath) case .Err(let err) &&
|
||||||
|
!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
|
||||||
|
{
|
||||||
|
return .Err(.UnexpectedError(new $"Failed to create Directory {copyInfo.TargetPath}: {err}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (File.Exists(copyInfo.SourcePath))
|
else if (File.Exists(copyInfo.SourcePath))
|
||||||
{
|
{
|
||||||
bool forceOverwrite = false;
|
bool forceOverwrite = false;
|
||||||
if (File.Exists(copyInfo.TargetPath))
|
if (File.Exists(copyInfo.TargetPath))
|
||||||
{
|
{
|
||||||
switch (copyInfo.OverwriteMode)
|
if (copyInfo.ConflictResolutionMode.HasFlag(.OverwriteFile))
|
||||||
{
|
{
|
||||||
case .KeepBoth:
|
|
||||||
String fileExtension = scope .();
|
|
||||||
Path.GetExtension(copyInfo.TargetPath, fileExtension);
|
|
||||||
|
|
||||||
String fileName = scope .();
|
|
||||||
Path.GetFileName(copyInfo.SourcePath, fileName);
|
|
||||||
|
|
||||||
Path.FindFreePath(_targetDirectoryPath, fileName.Substring(0..<^fileExtension.Length), fileExtension, copyInfo.TargetPath..Clear());
|
|
||||||
case .OverwriteFile:
|
|
||||||
forceOverwrite = true;
|
forceOverwrite = true;
|
||||||
default:
|
}
|
||||||
return .Err;//(.TargetFileExists);
|
else if (!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
|
||||||
|
{
|
||||||
|
return .Err(.UnexpectedError(new $"The target file already exists, but it wasn't properly handled during the scanning phase."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
File.Copy(copyInfo.SourcePath, copyInfo.TargetPath, forceOverwrite);
|
if (File.Copy(copyInfo.SourcePath, copyInfo.TargetPath, forceOverwrite) case .Err(let err) &&
|
||||||
|
!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
|
||||||
|
{
|
||||||
|
return .Err(.UnexpectedError(new $"Failed to copy File {copyInfo.TargetPath}: {err}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO error
|
if (copyInfo.ConflictResolutionMode.HasFlag(.SkipNotFound))
|
||||||
return .Err;
|
return .Ok;
|
||||||
|
else
|
||||||
|
return .Err(.EntryNotFound(new String(copyInfo.SourcePath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return .Ok;
|
return .Ok;
|
||||||
@@ -254,7 +308,9 @@ class CopyBackgroundTask : BackgroundTask
|
|||||||
|
|
||||||
public override void OnRenderPopup()
|
public override void OnRenderPopup()
|
||||||
{
|
{
|
||||||
if (ImGui.Begin("Copying files..."))
|
String title = scope .("Copying files...");
|
||||||
|
|
||||||
|
if (ImGui.Begin(title, null, .NoDocking | .NoCollapse | .Modal | .NoResize | .AlwaysAutoResize))
|
||||||
{
|
{
|
||||||
if (ScanningFiles)
|
if (ScanningFiles)
|
||||||
{
|
{
|
||||||
@@ -267,89 +323,188 @@ class CopyBackgroundTask : BackgroundTask
|
|||||||
ImGui.ProgressBar(copiedFiles / _totalEntriesToCopy, .(-1, 0), scope $"Copied {copiedFiles} / {_totalEntriesToCopy} files.");
|
ImGui.ProgressBar(copiedFiles / _totalEntriesToCopy, .(-1, 0), scope $"Copied {copiedFiles} / {_totalEntriesToCopy} files.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
switch (_currentError)
|
switch (_currentError)
|
||||||
{
|
{
|
||||||
case .None:
|
|
||||||
// Do nothing
|
|
||||||
case .TargetDirectoryIsNotDirectory:
|
|
||||||
// This shouldn't be possible
|
|
||||||
Debug.Break();
|
|
||||||
case .SourceDoesntExist(let SourcePath):
|
|
||||||
// This isn't good!
|
|
||||||
case .TargetFileExists(let fileName):
|
case .TargetFileExists(let fileName):
|
||||||
ImGui.PushStyleColor(.Text, 0xFF0000FF);
|
ImGui.PushStyleColor(.Text, 0xFF0000FF);
|
||||||
ImGui.TextUnformatted(scope $"A file with the name \"{fileName}\" already exists.");
|
ImGui.TextUnformatted(scope $"A file with the name \"{fileName}\" already exists.");
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
|
|
||||||
if (ImGui.Button("Overwrite"))
|
|
||||||
{
|
|
||||||
_nextFileMode = .OverwriteFile;
|
|
||||||
Continue();
|
|
||||||
}
|
|
||||||
ImGui.AttachTooltip("Overwrites the existing file. Will ask again if another conflict occurs.");
|
|
||||||
|
|
||||||
if (ImGui.Button("Overwrite All"))
|
|
||||||
{
|
|
||||||
_allFilesMode = .OverwriteFile;
|
|
||||||
Continue();
|
|
||||||
}
|
|
||||||
ImGui.AttachTooltip("Overwrites all existing files.");
|
|
||||||
|
|
||||||
if (ImGui.Button("Keep both"))
|
|
||||||
{
|
|
||||||
_nextFileMode = .KeepBoth;
|
|
||||||
Continue();
|
|
||||||
}
|
|
||||||
ImGui.AttachTooltip("Keeps both the existing file as well as the new one, differentiates by adding a number to the name. Will ask again if another conflict occurs.");
|
|
||||||
|
|
||||||
if (ImGui.Button("Keep all"))
|
|
||||||
{
|
|
||||||
_allFilesMode = .KeepBoth;
|
|
||||||
Continue();
|
|
||||||
}
|
|
||||||
ImGui.AttachTooltip("Keeps both the existing files as well as the new ones, differentiates by adding a number to the name.");
|
|
||||||
|
|
||||||
if (ImGui.Button("Cancel"))
|
|
||||||
{
|
|
||||||
Abort();
|
|
||||||
}
|
|
||||||
case .TargetDirectoryExists(let directoryName):
|
case .TargetDirectoryExists(let directoryName):
|
||||||
ImGui.PushStyleColor(.Text, 0xFF0000FF);
|
ImGui.PushStyleColor(.Text, 0xFF0000FF);
|
||||||
ImGui.TextUnformatted(scope $"A file with the name \"{directoryName}\" already exists.");
|
ImGui.TextUnformatted(scope $"A directory with the name \"{directoryName}\" already exists.");
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
|
case .EntryNotFound(let SourcePath):
|
||||||
if (ImGui.Button("Combine"))
|
ImGui.PushStyleColor(.Text, 0xFF0000FF);
|
||||||
|
ImGui.TextUnformatted(scope $"\"{SourcePath}\" doesn't exist.");
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
case .UnexpectedError(let Message):
|
||||||
|
ImGui.PushStyleColor(.Text, 0xFF0000FF);
|
||||||
|
ImGui.TextUnformatted(scope $"Unexpected error: {Message}");
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
default:
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.BeginTable("buttonTable", 2))
|
||||||
|
{
|
||||||
|
switch (_currentError)
|
||||||
{
|
{
|
||||||
_nextFileMode = .CombineDirectories;
|
case .None:
|
||||||
Continue();
|
// Do nothing
|
||||||
}
|
case .TargetDirectoryIsNotDirectory:
|
||||||
ImGui.AttachTooltip("Merges the copied directory into the target directory. Will ask again if another conflict occurs.");
|
// This shouldn't be possible
|
||||||
|
Debug.Break();
|
||||||
|
case .TargetFileExists(let fileName):
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableSetColumnIndex(0);
|
||||||
|
|
||||||
if (ImGui.Button("Combine All"))
|
if (ImGui.Button("Overwrite the target file."))
|
||||||
{
|
{
|
||||||
_allFilesMode = .CombineDirectories;
|
_nextFileMode = .OverwriteFile;
|
||||||
Continue();
|
Continue();
|
||||||
}
|
}
|
||||||
ImGui.AttachTooltip("Merges all copied directories into the target directories.");
|
ImGui.TableSetColumnIndex(1);
|
||||||
|
if (ImGui.Button("Overwrite all conflicting target files."))
|
||||||
|
{
|
||||||
|
_allFilesMode |= .OverwriteFile;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableSetColumnIndex(0);
|
||||||
|
|
||||||
if (ImGui.Button("Keep both"))
|
if (ImGui.Button("Rename copied file."))
|
||||||
{
|
{
|
||||||
_nextFileMode = .KeepBoth;
|
_nextFileMode = .KeepBoth;
|
||||||
Continue();
|
Continue();
|
||||||
}
|
}
|
||||||
ImGui.AttachTooltip("Renames the copied directory, so that they no longer conflict. Will ask again if another conflict occurs.");
|
ImGui.TableSetColumnIndex(1);
|
||||||
|
if (ImGui.Button("Rename all conflicting copied files."))
|
||||||
|
{
|
||||||
|
_allFilesMode |= .KeepBoth;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableSetColumnIndex(0);
|
||||||
|
|
||||||
if (ImGui.Button("Keep all"))
|
if (ImGui.Button("Skip this file."))
|
||||||
{
|
{
|
||||||
_allFilesMode = .KeepBoth;
|
_nextFileMode = .SkipFiles;
|
||||||
Continue();
|
Continue();
|
||||||
|
}
|
||||||
|
ImGui.TableSetColumnIndex(1);
|
||||||
|
if (ImGui.Button("Skip all conflicting files."))
|
||||||
|
{
|
||||||
|
_allFilesMode |= .SkipFiles;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
|
||||||
|
case .TargetDirectoryExists(let directoryName):
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableSetColumnIndex(0);
|
||||||
|
|
||||||
|
if (ImGui.Button("Combine with target."))
|
||||||
|
{
|
||||||
|
_nextFileMode |= .CombineDirectories;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
ImGui.TableSetColumnIndex(1);
|
||||||
|
if (ImGui.Button("Combine all conflicting directories."))
|
||||||
|
{
|
||||||
|
_allFilesMode |= .CombineDirectories;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableSetColumnIndex(0);
|
||||||
|
|
||||||
|
if (ImGui.Button("Rename copied directory."))
|
||||||
|
{
|
||||||
|
_nextFileMode |= .KeepBoth;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
ImGui.TableSetColumnIndex(1);
|
||||||
|
if (ImGui.Button("Rename all conflicting directories."))
|
||||||
|
{
|
||||||
|
_allFilesMode |= .KeepBoth;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableSetColumnIndex(0);
|
||||||
|
|
||||||
|
if (ImGui.Button("Skip this directory."))
|
||||||
|
{
|
||||||
|
_nextFileMode |= .SkipDirectories;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
ImGui.TableSetColumnIndex(1);
|
||||||
|
if (ImGui.Button("Skip all conflicting directories."))
|
||||||
|
{
|
||||||
|
_allFilesMode |= .SkipFiles;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
case .EntryNotFound(let SourcePath):
|
||||||
|
if (ImGui.Button("Skip"))
|
||||||
|
{
|
||||||
|
_nextFileMode |= .SkipNotFound;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Skip all"))
|
||||||
|
{
|
||||||
|
_allFilesMode |= .SkipNotFound;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
case .UnexpectedError(let Message):
|
||||||
|
if (ImGui.Button("Retry"))
|
||||||
|
{
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Ignore"))
|
||||||
|
{
|
||||||
|
_nextFileMode |= .IgnoreUnexpectedErrors;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Ignore all unexpected"))
|
||||||
|
{
|
||||||
|
_allFilesMode |= .IgnoreUnexpectedErrors;
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ImGui.AttachTooltip("Renames the copied directory, so that they no longer conflict.");
|
|
||||||
|
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
|
||||||
|
if (_currentError case .None)
|
||||||
|
{
|
||||||
|
if (Paused)
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Continue"))
|
||||||
|
{
|
||||||
|
Continue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Pause"))
|
||||||
|
{
|
||||||
|
Pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
|
||||||
if (ImGui.Button("Cancel"))
|
if (ImGui.Button("Cancel"))
|
||||||
{
|
{
|
||||||
Abort();
|
Abort();
|
||||||
}
|
}
|
||||||
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.End();
|
ImGui.End();
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace System.Collections;
|
||||||
|
|
||||||
|
extension HashSet<T> : ICollection<T> where T : IHashable
|
||||||
|
{
|
||||||
|
public void ICollection<T>.Add(T item)
|
||||||
|
{
|
||||||
|
Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyTo(Span<T> span)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
for (T item in this)
|
||||||
|
{
|
||||||
|
span[i] = item;
|
||||||
|
i++;
|
||||||
|
|
||||||
|
if (i >= span.Length)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,4 +19,9 @@ extension Enum
|
|||||||
else
|
else
|
||||||
ClearFlag<T>(ref value, flag);
|
ClearFlag<T>(ref value, flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool HasAnyFlag<T>(T value, T flags) where T : enum
|
||||||
|
{
|
||||||
|
return (value.Underlying & flags.Underlying) != default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
namespace System.IO;
|
namespace System.IO;
|
||||||
|
|
||||||
@@ -34,8 +35,8 @@ extension Path
|
|||||||
path.Remove(0, 1);
|
path.Remove(0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compared to the original Combine, this one makes sure we don't add multiple seperators. Also makes sure, the path contains only the main Separator char.
|
||||||
public static void Combine(String target, params StringView[] components)
|
new public static void Combine(String target, params StringView[] components)
|
||||||
{
|
{
|
||||||
for (var component in components)
|
for (var component in components)
|
||||||
{
|
{
|
||||||
@@ -54,25 +55,24 @@ extension Path
|
|||||||
* @param wantedFileName The wanted name of the file. If it already exists a number will be put behind it.
|
* @param wantedFileName The wanted name of the file. If it already exists a number will be put behind it.
|
||||||
* @param fileExtension The file extension. This should contain the dot.
|
* @param fileExtension The file extension. This should contain the dot.
|
||||||
* @param outFreePath The string that will contain the resulting filepath. Note: This will be cleared before writing the file name.
|
* @param outFreePath The string that will contain the resulting filepath. Note: This will be cleared before writing the file name.
|
||||||
|
* @param outFreeFilename Optional, if set to a string, the final file name that is free will be stored in this varialbe.
|
||||||
|
* @param blockedPaths Optional, can be used to provide a collection of paths that will be considered as existing.
|
||||||
*/
|
*/
|
||||||
public static void FindFreePath(StringView targetDirectory, StringView wantedName, StringView fileExtension, String outFreePath)
|
public static void FindFreePath(StringView targetDirectory, StringView wantedName, StringView fileExtension, String outFreePath, String outFreeFilename = null, ICollection<StringView> blockedPaths = null)
|
||||||
{
|
{
|
||||||
int fileNumber = 0;
|
int fileNumber = 0;
|
||||||
|
|
||||||
String currentFileName = scope $"{wantedName}{fileExtension}";
|
String currentFileName = outFreeFilename ?? scope .();
|
||||||
|
currentFileName.SetF($"{wantedName}{fileExtension}");
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
Path.Combine(outFreePath..Clear(), targetDirectory, currentFileName);
|
Path.Combine(outFreePath..Clear(), targetDirectory, currentFileName);
|
||||||
|
|
||||||
FileInfo targetInfo = scope FileInfo(outFreePath);
|
if (!File.Exists(outFreePath) && !Directory.Exists(outFreePath) && !(blockedPaths?.Contains(outFreePath) ?? false))
|
||||||
|
|
||||||
if (!targetInfo.Exists)
|
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
fileNumber++;
|
fileNumber++;
|
||||||
currentFileName..Clear().AppendF($"{wantedName} ({fileNumber}){fileExtension}");
|
currentFileName.SetF($"{wantedName} ({fileNumber}){fileExtension}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user