From 2c588908e96adbf5a77a113f00fb449e7063dffe Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Tue, 4 Sep 2018 09:58:54 -0400 Subject: [PATCH] Implement new verb for deleting empty directories, remove the old delete on empty behaviour --- .../Rights/ConfigurationRights.cs | 6 +- .../Components/ConfigurationClient.cs | 5 +- .../Components/IConfigurationClient.cs | 10 +- .../Components/StaticFiles/Configuration.cs | 122 +++++++++++------- .../Components/StaticFiles/IConfiguration.cs | 9 ++ .../Controllers/ConfigurationController.cs | 24 ++++ .../IO/ISynchronousIOManager.cs | 7 + .../IO/SynchronousIOManager.cs | 25 ++-- 8 files changed, 149 insertions(+), 59 deletions(-) diff --git a/src/Tgstation.Server.Api/Rights/ConfigurationRights.cs b/src/Tgstation.Server.Api/Rights/ConfigurationRights.cs index 45b02e528f..c05a40923b 100644 --- a/src/Tgstation.Server.Api/Rights/ConfigurationRights.cs +++ b/src/Tgstation.Server.Api/Rights/ConfigurationRights.cs @@ -23,6 +23,10 @@ namespace Tgstation.Server.Api.Rights /// /// User may list files /// - List = 4 + List = 4, + /// + /// User may delete empty folders + /// + Delete = 8 } } diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs index 4f35d86321..0d4574db60 100644 --- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs @@ -30,6 +30,9 @@ namespace Tgstation.Server.Client.Components this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); } + /// + public Task DeleteEmptyDirectory(string directory, CancellationToken cancellationToken) => apiClient.Delete(Routes.Configuration + directory ?? throw new ArgumentNullException(nameof(directory)), cancellationToken); + /// public Task> List(string directory, CancellationToken cancellationToken) { @@ -49,4 +52,4 @@ namespace Tgstation.Server.Client.Components /// public Task Write(ConfigurationFile file, CancellationToken cancellationToken) => apiClient.Update(Routes.Configuration, file ?? throw new ArgumentNullException(nameof(file)), instance.Id, cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs index 950c346775..ec8a5c9396 100644 --- a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs @@ -31,7 +31,15 @@ namespace Tgstation.Server.Client.Components /// /// The file to write /// The for the operation - /// A representing the running operation + /// A resulting in the new Task Write(ConfigurationFile file, CancellationToken cancellationToken); + + /// + /// Delete an empty + /// + /// The path to directory to delete + /// The for the operation + /// A representing the running operation + Task DeleteEmptyDirectory(string directory, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 90c7a4d218..1e0a42c7bc 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -103,36 +103,40 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// public async Task CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken) { - await EnsureDirectories(cancellationToken).ConfigureAwait(false); - //just assume no other fs race conditions here - var dmeExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken); - var headFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsHeadFile), cancellationToken); - var tailFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsTailFile), cancellationToken); - - await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask).ConfigureAwait(false); - - if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result) - return null; - - var copyTask = ioManager.CopyDirectory(CodeModificationsSubdirectory, destination, null, cancellationToken); - - if (dmeExistsTask.Result) + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { + await EnsureDirectories(cancellationToken).ConfigureAwait(false); + + //just assume no other fs race conditions here + var dmeExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken); + var headFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsHeadFile), cancellationToken); + var tailFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsTailFile), cancellationToken); + + await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask).ConfigureAwait(false); + + if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result) + return null; + + var copyTask = ioManager.CopyDirectory(CodeModificationsSubdirectory, destination, null, cancellationToken); + + if (dmeExistsTask.Result) + { + await copyTask.ConfigureAwait(false); + return new ServerSideModifications(null, null, true); + } + + if (!headFileExistsTask.Result && !tailFileExistsTask.Result) + { + await copyTask.ConfigureAwait(false); + return null; + } + + string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath); + await copyTask.ConfigureAwait(false); - return new ServerSideModifications(null, null, true); + return new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null, false); } - - if (!headFileExistsTask.Result && !tailFileExistsTask.Result) - { - await copyTask.ConfigureAwait(false); - return null; - } - - string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath); - - await copyTask.ConfigureAwait(false); - return new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null, false); } string ValidateConfigRelativePath(string configurationRelativePath) @@ -180,10 +184,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles })); } - if (systemIdentity == null) - ListImpl(); - else - await systemIdentity.RunImpersonated(ListImpl, cancellationToken).ConfigureAwait(false); + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + if (systemIdentity == null) + ListImpl(); + else + await systemIdentity.RunImpersonated(ListImpl, cancellationToken).ConfigureAwait(false); return result; } @@ -240,10 +245,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles } } - if (systemIdentity == null) - await Task.Factory.StartNew(ReadImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); - else - await systemIdentity.RunImpersonated(ReadImpl, cancellationToken).ConfigureAwait(false); + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + if (systemIdentity == null) + await Task.Factory.StartNew(ReadImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + else + await systemIdentity.RunImpersonated(ReadImpl, cancellationToken).ConfigureAwait(false); return result; } @@ -251,7 +257,6 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// public async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken) { - await EnsureDirectories(cancellationToken).ConfigureAwait(false); async Task SymlinkBase(bool files) { Task> task; @@ -275,7 +280,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles })).ConfigureAwait(false); } - await Task.WhenAll(SymlinkBase(true), SymlinkBase(false)).ConfigureAwait(false); + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + { + await EnsureDirectories(cancellationToken).ConfigureAwait(false); + await Task.WhenAll(SymlinkBase(true), SymlinkBase(false)).ConfigureAwait(false); + } } /// @@ -328,13 +337,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles } } - if (systemIdentity == null) - await Task.Factory.StartNew(WriteImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); - else - await systemIdentity.RunImpersonated(WriteImpl, cancellationToken).ConfigureAwait(false); - - if (result != null && data == null) //make sure these directories always exist - await EnsureDirectories(cancellationToken).ConfigureAwait(false); + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + if (systemIdentity == null) + await Task.Factory.StartNew(WriteImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + else + await systemIdentity.RunImpersonated(WriteImpl, cancellationToken).ConfigureAwait(false); return result; } @@ -347,10 +354,12 @@ namespace Tgstation.Server.Host.Components.StaticFiles bool? result = null; void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken); - if (systemIdentity == null) - await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); - else - await systemIdentity.RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(false); + + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + if (systemIdentity == null) + await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + else + await systemIdentity.RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(false); return result.Value; } @@ -387,5 +396,24 @@ namespace Tgstation.Server.Host.Components.StaticFiles } return true; } + + /// + public async Task DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) + { + await EnsureDirectories(cancellationToken).ConfigureAwait(false); + var path = ValidateConfigRelativePath(configurationRelativePath); + + var result = false; + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + { + void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path); + + if (systemIdentity != null) + await systemIdentity.RunImpersonated(CheckDeleteImpl, cancellationToken).ConfigureAwait(false); + else + CheckDeleteImpl(); + } + return result; + } } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs index ebccc9c24e..29b8b66da0 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs @@ -57,6 +57,15 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// A resulting in if the directory already existed, otherwise Task CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); + /// + /// Attempt to delete an empty directory at + /// + /// The path of the empty directory to delete + /// The for the operation. If , the operation will be performed as the user of the + /// The for the operation + /// if the directory was empty and deleted, otherwise + Task DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); + /// /// Writes to a given /// diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 34ded3ea45..f1ad7587a1 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -168,5 +168,29 @@ namespace Tgstation.Server.Host.Controllers return Forbid(); } } + + [HttpDelete("{*directoryPath}")] + [TgsAuthorize(ConfigurationRights.Delete)] + public async Task Delete(string directoryPath, CancellationToken cancellationToken) + { + if (ForbidDueToModeConflicts()) + return Forbid(); + + try + { + return await instanceManager.GetInstance(Instance).Configuration.DeleteDirectory(directoryPath, AuthenticationContext.SystemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Ok() : Conflict(new ErrorMessage + { + Message = "Directory not empty!" + }); + } + catch (NotImplementedException) + { + return StatusCode((int)HttpStatusCode.NotImplemented); + } + catch (UnauthorizedAccessException) + { + return Forbid(); + } + } } } diff --git a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs index f361004dfe..19ad6fba61 100644 --- a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs @@ -39,6 +39,13 @@ namespace Tgstation.Server.Host.IO /// A array representing the contents of the file at byte[] ReadFile(string path); + /// + /// Deletes a directory at if it's empty + /// + /// The path of the directory to delete + /// if the directory does not exist or is empty and was deleted. otherwise + bool DeleteDirectory(string path); + /// /// Write to a file at a given . /// diff --git a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs index 9a60da2313..fa394a644c 100644 --- a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs @@ -21,6 +21,22 @@ namespace Tgstation.Server.Host.IO return false; } + /// + public bool DeleteDirectory(string path) + { + if (File.Exists(path)) + return false; + + if (!Directory.Exists(path)) + return true; + + if (Directory.EnumerateFileSystemEntries(path).Any()) + return false; + + Directory.Delete(path); + return true; + } + /// public IEnumerable GetDirectories(string path, CancellationToken cancellationToken) { @@ -109,16 +125,7 @@ namespace Tgstation.Server.Host.IO } } if (data == null) - { File.Delete(path); - if (!cancellationToken.IsCancellationRequested) - //delete the entire folder if possible - try - { - Directory.Delete(directory); - } - catch (IOException) { } - } return true; } }