mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-24 21:46:52 +01:00
Adds log downloading endpoint
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a game configuration file. Create and delete actions uncerimonuously overwrite/delete files
|
||||
/// </summary>
|
||||
public sealed class ConfigurationFile
|
||||
public sealed class ConfigurationFile : RawData
|
||||
{
|
||||
/// <summary>
|
||||
/// The path to the <see cref="ConfigurationFile"/> file
|
||||
@@ -27,12 +28,5 @@ namespace Tgstation.Server.Api.Models
|
||||
/// The MD5 hash of the file when last read by the user. If this doesn't match during update actions, the write will be denied with <see cref="System.Net.HttpStatusCode.Conflict"/>
|
||||
/// </summary>
|
||||
public string? LastReadHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content of the <see cref="ConfigurationFile"/>. Will be <see langword="null"/> if <see cref="AccessDenied"/> is <see langword="true"/> or during listing and write operations
|
||||
/// </summary>
|
||||
#pragma warning disable CA1819, SA1011 // Properties should not return arrays, Closing square bracket should be followed by a space
|
||||
public byte[]? Content { get; set; }
|
||||
#pragma warning restore CA1819, SA1011 // Properties should not return arrays, Closing square bracket should be followed by a space
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Tgstation.Server.Api.Models.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents raw bytes.
|
||||
/// </summary>
|
||||
public abstract class RawData
|
||||
{
|
||||
/// <summary>
|
||||
/// The bytes of the <see cref="RawData"/>.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1819, SA1011 // Properties should not return arrays, Closing square bracket should be followed by a space
|
||||
public byte[]? Content { get; set; }
|
||||
#pragma warning restore CA1819, SA1011 // Properties should not return arrays, Closing square bracket should be followed by a space
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a server log file.
|
||||
/// </summary>
|
||||
public sealed class LogFile : RawData
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the log file.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DateTimeOffset"/> of when the log file was modified.
|
||||
/// </summary>
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,11 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// <summary>
|
||||
/// User can read info and rights of other users
|
||||
/// </summary>
|
||||
ReadUsers = 16
|
||||
ReadUsers = 16,
|
||||
|
||||
/// <summary>
|
||||
/// User can list and download <see cref="Models.LogFile"/>s.
|
||||
/// </summary>
|
||||
DownloadLogs = 32,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
public const string Administration = Root + nameof(Models.Administration);
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.Administration"/> controller
|
||||
/// </summary>
|
||||
public const string Logs = Administration + "/Logs";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.User"/> controller
|
||||
/// </summary>
|
||||
@@ -102,5 +107,19 @@ namespace Tgstation.Server.Api
|
||||
/// <param name="route">The route</param>
|
||||
/// <returns>The <paramref name="route"/> with /List appended</returns>
|
||||
public static string ListRoute(string route) => String.Format(CultureInfo.InvariantCulture, "{0}/{1}", route, List);
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize a <see cref="Models.Internal.RawData"/> path for use in a GET <see cref="Uri"/>.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to sanitize.</param>
|
||||
/// <returns>The sanitized path.</returns>
|
||||
public static string SanitizeGetPath(string path)
|
||||
{
|
||||
if (path == null)
|
||||
path = String.Empty;
|
||||
if (path.Length == 0 || path[0] != '/')
|
||||
path = '/' + path;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api;
|
||||
@@ -31,5 +32,14 @@ namespace Tgstation.Server.Client
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Restart(CancellationToken cancellationToken) => apiClient.Delete(Routes.Administration, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<LogFile>> ListLogs(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<LogFile>>(Routes.Logs, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<LogFile> GetLog(LogFile logFile, CancellationToken cancellationToken) => apiClient.Read<LogFile>(
|
||||
Routes.Logs + Routes.SanitizeGetPath(
|
||||
logFile?.Name ?? throw new ArgumentNullException(nameof(logFile))),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -20,20 +20,6 @@ namespace Tgstation.Server.Client.Components
|
||||
/// </summary>
|
||||
readonly Instance instance;
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize a <see cref="ConfigurationFile"/> path for use in a GET <see cref="Uri"/>
|
||||
/// </summary>
|
||||
/// <param name="path">The path to sanitize</param>
|
||||
/// <returns>The sanitized path</returns>
|
||||
static string SanitizeGetPath(string path)
|
||||
{
|
||||
if (path == null)
|
||||
path = String.Empty;
|
||||
if (path.Length == 0 || path[0] != '/')
|
||||
path = '/' + path;
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="ConfigurationClient"/>
|
||||
/// </summary>
|
||||
@@ -52,7 +38,7 @@ namespace Tgstation.Server.Client.Components
|
||||
public Task<ConfigurationFile> CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Create<ConfigurationFile, ConfigurationFile>(Routes.Configuration, directory, instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<ConfigurationFile>> List(string directory, CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ConfigurationFile>>(Routes.ListRoute(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken);
|
||||
public Task<IReadOnlyList<ConfigurationFile>> List(string directory, CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ConfigurationFile>>(Routes.ListRoute(Routes.Configuration) + Routes.SanitizeGetPath(directory), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ConfigurationFile> Read(ConfigurationFile file, CancellationToken cancellationToken)
|
||||
@@ -60,7 +46,7 @@ namespace Tgstation.Server.Client.Components
|
||||
if (file == null)
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
return apiClient.Read<ConfigurationFile>(
|
||||
Routes.ConfigurationFile + SanitizeGetPath(file.Path ?? throw new ArgumentException("file.Path should not be null!", nameof(file))),
|
||||
Routes.ConfigurationFile + Routes.SanitizeGetPath(file.Path ?? throw new ArgumentException("file.Path should not be null!", nameof(file))),
|
||||
instance.Id,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
@@ -30,5 +31,20 @@ namespace Tgstation.Server.Client
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task Restart(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Lists the log files available for download.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in an <see cref="IReadOnlyList{T}"/> of <see cref="LogFile"/> metadata.</returns>
|
||||
Task<IReadOnlyList<LogFile>> ListLogs(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Download a given <paramref name="logFile"/>.
|
||||
/// </summary>
|
||||
/// <param name="logFile">The <see cref="LogFile"/> to download.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the downloaded <see cref="LogFile"/>.</returns>
|
||||
Task<LogFile> GetLog(LogFile logFile, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <param name="configurationRelativePath">The relative path in the Configuration directory</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>A <see cref="Task{TResult}"/> resulting in the <see cref="ConfigurationFile"/>s for the items in the directory. <see cref="ConfigurationFile.Content"/> and <see cref="ConfigurationFile.LastReadHash"/> will both be <see langword="null"/></returns>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ConfigurationFile"/>s for the items in the directory. <see cref="Api.Models.Internal.RawData.Content"/> and <see cref="ConfigurationFile.LastReadHash"/> will both be <see langword="null"/></returns>
|
||||
Task<IReadOnlyList<ConfigurationFile>> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// File logging configuration options
|
||||
/// </summary>
|
||||
sealed class FileLoggingConfiguration
|
||||
public sealed class FileLoggingConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="FileLoggingConfiguration"/> resides in
|
||||
@@ -45,5 +48,26 @@ namespace Tgstation.Server.Host.Configuration
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public LogLevel MicrosoftLogLevel { get; set; } = DefaultMicrosoftLogLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the evaluated log <see cref="Directory"/>.
|
||||
/// </summary>
|
||||
/// <param name="ioManager">The <see cref="IIOManager"/> to use.</param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> to use.</param>
|
||||
/// <returns>The evaluated log <see cref="Directory"/>.</returns>
|
||||
public string GetFullLogDirectory(IIOManager ioManager, IAssemblyInformationProvider assemblyInformationProvider)
|
||||
{
|
||||
if (ioManager == null)
|
||||
throw new ArgumentNullException(nameof(ioManager));
|
||||
if (assemblyInformationProvider == null)
|
||||
throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
||||
|
||||
return !String.IsNullOrEmpty(Directory)
|
||||
? Directory
|
||||
: ioManager.ConcatPath(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), // common app data is C:/ProgramData on windows, else /usr/share
|
||||
assemblyInformationProvider.VersionPrefix,
|
||||
"Logs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Octokit;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
@@ -66,6 +67,11 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
readonly GeneralConfiguration generalConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="FileLoggingConfiguration"/> for the <see cref="AdministrationController"/>
|
||||
/// </summary>
|
||||
readonly FileLoggingConfiguration fileLoggingConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="AdministrationController"/>
|
||||
/// </summary>
|
||||
@@ -79,6 +85,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="updatesConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="updatesConfiguration"/></param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="generalConfiguration"/></param>
|
||||
/// <param name="fileLoggingConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="fileLoggingConfiguration"/></param>
|
||||
public AdministrationController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
@@ -89,7 +96,8 @@ namespace Tgstation.Server.Host.Controllers
|
||||
IPlatformIdentifier platformIdentifier,
|
||||
ILogger<AdministrationController> logger,
|
||||
IOptions<UpdatesConfiguration> updatesConfigurationOptions,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions)
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<FileLoggingConfiguration> fileLoggingConfigurationOptions)
|
||||
: base(
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
@@ -104,6 +112,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
|
||||
updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions));
|
||||
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions));
|
||||
}
|
||||
|
||||
ObjectResult RateLimit(RateLimitExceededException exception)
|
||||
@@ -258,7 +267,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (model.NewVersion.Major != assemblyInformationProvider.Version.Major)
|
||||
return BadRequest(new ErrorMessage(ErrorCode.CannotChangeServerSuite));
|
||||
|
||||
if(!serverUpdater.WatchdogPresent)
|
||||
if (!serverUpdater.WatchdogPresent)
|
||||
return UnprocessableEntity(new ErrorMessage(ErrorCode.MissingHostWatchdog));
|
||||
|
||||
return await CheckReleasesAndApplyUpdate(model.NewVersion, cancellationToken).ConfigureAwait(false);
|
||||
@@ -292,5 +301,88 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return StatusCode((int)HttpStatusCode.ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List <see cref="LogFile"/>s present.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request.</returns>
|
||||
/// <response code="200">Listed logs successfully.</response>
|
||||
/// <response code="409">An IO error occurred while listing.</response>
|
||||
[HttpGet(Routes.Logs)]
|
||||
[TgsAuthorize(AdministrationRights.DownloadLogs)]
|
||||
[ProducesResponseType(typeof(List<LogFile>), 200)]
|
||||
[ProducesResponseType(typeof(ErrorMessage), 409)]
|
||||
public async Task<IActionResult> ListLogs(CancellationToken cancellationToken)
|
||||
{
|
||||
var path = fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider);
|
||||
try
|
||||
{
|
||||
var files = await ioManager.GetFiles(path, cancellationToken).ConfigureAwait(false);
|
||||
var tasks = files.Select(
|
||||
async file => new LogFile
|
||||
{
|
||||
Name = ioManager.GetFileName(file),
|
||||
LastModified = await ioManager.GetLastModified(
|
||||
ioManager.ConcatPath(path, file),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
})
|
||||
.ToList();
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
var result = tasks.Select(x => x.Result).ToList();
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
return Conflict(new ErrorMessage(ErrorCode.IOError)
|
||||
{
|
||||
AdditionalData = ex.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download a <see cref="LogFile"/>.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to download.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request.</returns>
|
||||
/// <response code="200">Downloaded <see cref="LogFile"/> successfully.</response>
|
||||
/// <response code="409">An IO error occurred while downloading.</response>
|
||||
[HttpGet(Routes.Logs + "/{path}")]
|
||||
[TgsAuthorize(AdministrationRights.DownloadLogs)]
|
||||
[ProducesResponseType(typeof(List<LogFile>), 200)]
|
||||
[ProducesResponseType(typeof(ErrorMessage), 409)]
|
||||
public async Task<IActionResult> GetLog(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
|
||||
var fullPath = ioManager.ConcatPath(
|
||||
fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider),
|
||||
path);
|
||||
try
|
||||
{
|
||||
var readTask = ioManager.ReadAllBytes(fullPath, cancellationToken);
|
||||
|
||||
return Ok(new LogFile
|
||||
{
|
||||
Name = path,
|
||||
LastModified = await ioManager.GetLastModified(fullPath, cancellationToken).ConfigureAwait(false),
|
||||
Content = await readTask.ConfigureAwait(false)
|
||||
});
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
return Conflict(new ErrorMessage(ErrorCode.IOError)
|
||||
{
|
||||
AdditionalData = ex.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,13 +122,7 @@ namespace Tgstation.Server.Host.Core
|
||||
if (postSetupServices.FileLoggingConfiguration.Disable)
|
||||
return;
|
||||
|
||||
// common app data is C:/ProgramData on windows, else /usr/share
|
||||
var logPath = !String.IsNullOrEmpty(postSetupServices.FileLoggingConfiguration.Directory)
|
||||
? postSetupServices.FileLoggingConfiguration.Directory
|
||||
: IOManager.ConcatPath(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
AssemblyInformationProvider.VersionPrefix,
|
||||
"Logs");
|
||||
var logPath = postSetupServices.FileLoggingConfiguration.GetFullLogDirectory(IOManager, AssemblyInformationProvider);
|
||||
|
||||
var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ namespace Tgstation.Server.Host.IO
|
||||
public async Task<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
path = ResolvePath(path);
|
||||
using var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, DefaultBufferSize, true);
|
||||
using var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, DefaultBufferSize, true);
|
||||
byte[] buf;
|
||||
buf = new byte[file.Length];
|
||||
await file.ReadAsync(buf, 0, (int)file.Length, cancellationToken).ConfigureAwait(false);
|
||||
@@ -311,5 +311,13 @@ namespace Tgstation.Server.Host.IO
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PathContainsParentAccess(string path) => path?.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Any(x => x == "..") ?? throw new ArgumentNullException(nameof(path));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DateTimeOffset> GetLastModified(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
{
|
||||
path = ResolvePath(path ?? throw new ArgumentNullException(nameof(path)));
|
||||
var fileInfo = new FileInfo(path);
|
||||
return new DateTimeOffset(fileInfo.LastWriteTimeUtc);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,5 +194,13 @@ namespace Tgstation.Server.Host.IO
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="DateTimeOffset"/> of when a given <paramref name="path"/> was last modified.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to get metadata for.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="DateTimeOffset"/> of when the file was last modified.</returns>
|
||||
Task<DateTimeOffset> GetLastModified(string path, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -19,7 +20,29 @@ namespace Tgstation.Server.Tests
|
||||
|
||||
public async Task Run(CancellationToken cancellationToken)
|
||||
{
|
||||
var logsTest = TestLogs(cancellationToken);
|
||||
await TestRead(cancellationToken).ConfigureAwait(false);
|
||||
await logsTest;
|
||||
}
|
||||
|
||||
async Task TestLogs(CancellationToken cancellationToken)
|
||||
{
|
||||
var logs = await client.ListLogs(cancellationToken);
|
||||
Assert.AreEqual(1, logs.Count);
|
||||
var logFile = logs.Single();
|
||||
Assert.IsNotNull(logFile);
|
||||
Assert.IsFalse(String.IsNullOrWhiteSpace(logFile.Name));
|
||||
Assert.IsNull(logFile.Content);
|
||||
|
||||
var downloaded = await client.GetLog(logFile, cancellationToken);
|
||||
Assert.AreEqual(logFile.Name, downloaded.Name);
|
||||
Assert.IsTrue(logFile.LastModified <= downloaded.LastModified);
|
||||
Assert.IsNull(logFile.Content);
|
||||
|
||||
await ApiAssert.ThrowsException<ConflictException>(() => client.GetLog(new LogFile
|
||||
{
|
||||
Name = "very_fake_path.log"
|
||||
}, cancellationToken), ErrorCode.IOError);
|
||||
}
|
||||
|
||||
async Task TestRead(CancellationToken cancellationToken)
|
||||
|
||||
@@ -79,6 +79,7 @@ namespace Tgstation.Server.Tests
|
||||
String.Format(CultureInfo.InvariantCulture, "General:MinimumPasswordLength={0}", 10),
|
||||
String.Format(CultureInfo.InvariantCulture, "General:InstanceLimit={0}", 11),
|
||||
String.Format(CultureInfo.InvariantCulture, "General:UserLimit={0}", 150),
|
||||
String.Format(CultureInfo.InvariantCulture, "FileLogging:Directory={0}", Path.Combine(Directory, "Logs")),
|
||||
String.Format(CultureInfo.InvariantCulture, "General:ValidInstancePaths:0={0}", Directory),
|
||||
"General:ByondTopicTimeout=3000"
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user