mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-29 08:00:19 +01:00
Implement new verb for deleting empty directories, remove the old delete on empty behaviour
This commit is contained in:
@@ -23,6 +23,10 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// <summary>
|
||||
/// User may list files
|
||||
/// </summary>
|
||||
List = 4
|
||||
List = 4,
|
||||
/// <summary>
|
||||
/// User may delete empty folders
|
||||
/// </summary>
|
||||
Delete = 8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ namespace Tgstation.Server.Client.Components
|
||||
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteEmptyDirectory(string directory, CancellationToken cancellationToken) => apiClient.Delete(Routes.Configuration + directory ?? throw new ArgumentNullException(nameof(directory)), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<ConfigurationFile>> List(string directory, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -49,4 +52,4 @@ namespace Tgstation.Server.Client.Components
|
||||
/// <inheritdoc />
|
||||
public Task<ConfigurationFile> Write(ConfigurationFile file, CancellationToken cancellationToken) => apiClient.Update<ConfigurationFile, ConfigurationFile>(Routes.Configuration, file ?? throw new ArgumentNullException(nameof(file)), instance.Id, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,15 @@ namespace Tgstation.Server.Client.Components
|
||||
/// </summary>
|
||||
/// <param name="file">The <see cref="ConfigurationFile"/> file to write</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the new <see cref="ConfigurationFile"/></returns>
|
||||
Task<ConfigurationFile> Write(ConfigurationFile file, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Delete an empty <paramref name="directory"/>
|
||||
/// </summary>
|
||||
/// <param name="directory">The path to directory to delete</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task DeleteEmptyDirectory(string directory, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,36 +103,40 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <inheritdoc />
|
||||
public async Task<ServerSideModifications> 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
|
||||
/// <inheritdoc />
|
||||
public async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureDirectories(cancellationToken).ConfigureAwait(false);
|
||||
async Task SymlinkBase(bool files)
|
||||
{
|
||||
Task<IReadOnlyList<string>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,15 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the directory already existed, <see langword="false"/> otherwise</returns>
|
||||
Task<bool> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to delete an empty directory at <paramref name="configurationRelativePath"/>
|
||||
/// </summary>
|
||||
/// <param name="configurationRelativePath">The path of the empty directory to delete</param>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns><see langword="true"/> if the directory was empty and deleted, <see langword="false"/> otherwise</returns>
|
||||
Task<bool> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Writes to a given <paramref name="configurationRelativePath"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -168,5 +168,29 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Forbid();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{*directoryPath}")]
|
||||
[TgsAuthorize(ConfigurationRights.Delete)]
|
||||
public async Task<IActionResult> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@ namespace Tgstation.Server.Host.IO
|
||||
/// <returns>A <see cref="byte"/> array representing the contents of the file at <paramref name="path"/></returns>
|
||||
byte[] ReadFile(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a directory at <see cref="path"/> if it's empty
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the directory to delete</param>
|
||||
/// <returns><see langword="true"/> if the directory does not exist or is empty and was deleted. <see langword="false"/> otherwise</returns>
|
||||
bool DeleteDirectory(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Write <paramref name="data"/> to a file at a given <paramref name="path"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,6 +21,22 @@ namespace Tgstation.Server.Host.IO
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user