diff --git a/build/OpenApiValidationSettings.json b/build/OpenApiValidationSettings.json
index 731082653b..af87e9cf4b 100644
--- a/build/OpenApiValidationSettings.json
+++ b/build/OpenApiValidationSettings.json
@@ -4,7 +4,7 @@
"no_operation_id": "error",
"operation_id_case_convention": "off",
"no_summary": "error",
- "no_array_responses": "off",
+ "no_array_responses": "error",
"parameter_order": "error",
"undefined_tag": "off",
"unused_tag": "error",
diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs
index cb9a77a786..1b206b6ab3 100644
--- a/src/Tgstation.Server.Api/Models/ErrorCode.cs
+++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs
@@ -250,18 +250,16 @@ namespace Tgstation.Server.Api.Models
RepoWhitespaceCommitterEmail,
///
- /// Deprecated.
+ /// A paginated request asked for too large a page.
///
- [Description("Deprecated error code.")]
- [Obsolete("With API v7 ", true)]
- DreamDaemonDuplicatePorts,
+ [Description("Requested pageSize is too large!")]
+ ApiPageTooLarge,
///
- /// Deprecated.
+ /// A paginated request asked for page 0.
///
- [Description("Deprecated error code.")]
- [Obsolete("With DMAPI-5.0.0, ultrasafe security is now supported.", true)]
- InvalidSecurityLevel,
+ [Description("Cannot request page or pageSize <= 0.")]
+ ApiInvalidPageOrPageSize,
///
/// A requested 's data does not match with its .
diff --git a/src/Tgstation.Server.Api/Models/Paginated.cs b/src/Tgstation.Server.Api/Models/Paginated.cs
new file mode 100644
index 0000000000..ba4e187494
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Paginated.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Api.Models
+{
+ ///
+ /// Represents a paginated set of models.
+ ///
+ /// The of the returned model.
+ public sealed class Paginated
+ {
+ ///
+ /// The of the returned s.
+ ///
+ [Required]
+ public ICollection? Content { get; set; }
+
+ ///
+ /// The total number of pages in the query.
+ ///
+ public int TotalPages { get; set; }
+
+ ///
+ /// The current size of pages in the query.
+ ///
+ public int PageSize { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Client/AdministrationClient.cs b/src/Tgstation.Server.Client/AdministrationClient.cs
index ca00100e8b..e941337ebf 100644
--- a/src/Tgstation.Server.Client/AdministrationClient.cs
+++ b/src/Tgstation.Server.Client/AdministrationClient.cs
@@ -10,45 +10,40 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
///
- sealed class AdministrationClient : IAdministrationClient
+ sealed class AdministrationClient : PaginatedClient, IAdministrationClient
{
- ///
- /// The for the
- ///
- readonly IApiClient apiClient;
-
///
/// Construct an
///
- /// The value of
+ /// The for the .
public AdministrationClient(IApiClient apiClient)
- {
- this.apiClient = apiClient;
- }
+ : base(apiClient)
+ { }
///
- public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.Administration, cancellationToken);
+ public Task Read(CancellationToken cancellationToken) => ApiClient.Read(Routes.Administration, cancellationToken);
///
- public Task Update(Administration administration, CancellationToken cancellationToken) => apiClient.Update(Routes.Administration, administration ?? throw new ArgumentNullException(nameof(administration)), cancellationToken);
+ public Task Update(Administration administration, CancellationToken cancellationToken) => ApiClient.Update(Routes.Administration, administration ?? throw new ArgumentNullException(nameof(administration)), cancellationToken);
///
- public Task Restart(CancellationToken cancellationToken) => apiClient.Delete(Routes.Administration, cancellationToken);
+ public Task Restart(CancellationToken cancellationToken) => ApiClient.Delete(Routes.Administration, cancellationToken);
///
- public Task> ListLogs(CancellationToken cancellationToken) => apiClient.Read>(Routes.Logs, cancellationToken);
+ public Task> ListLogs(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
+ => ReadPaged(paginationSettings, Routes.Logs, null, cancellationToken);
///
public async Task> GetLog(LogFile logFile, CancellationToken cancellationToken)
{
- var resultFile = await apiClient.Read(
+ var resultFile = await ApiClient.Read(
Routes.Logs + Routes.SanitizeGetPath(
HttpUtility.UrlEncode(
logFile?.Name ?? throw new ArgumentNullException(nameof(logFile)))),
cancellationToken)
.ConfigureAwait(false);
- var stream = await apiClient.Download(resultFile, cancellationToken).ConfigureAwait(false);
+ var stream = await ApiClient.Download(resultFile, cancellationToken).ConfigureAwait(false);
try
{
return Tuple.Create(resultFile, stream);
diff --git a/src/Tgstation.Server.Client/Components/ByondClient.cs b/src/Tgstation.Server.Client/Components/ByondClient.cs
index 15eb50bb7d..0a1847b35a 100644
--- a/src/Tgstation.Server.Client/Components/ByondClient.cs
+++ b/src/Tgstation.Server.Client/Components/ByondClient.cs
@@ -9,13 +9,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
///
- sealed class ByondClient : IByondClient
+ sealed class ByondClient : PaginatedClient, IByondClient
{
- ///
- /// The for the
- ///
- readonly IApiClient apiClient;
-
///
/// The for the
///
@@ -24,24 +19,25 @@ namespace Tgstation.Server.Client.Components
///
/// Construct a
///
- /// The value of
+ /// The for the .
/// The value of
public ByondClient(IApiClient apiClient, Instance instance)
+ : base(apiClient)
{
- this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
///
- public Task ActiveVersion(CancellationToken cancellationToken) => apiClient.Read(Routes.Byond, instance.Id, cancellationToken);
+ public Task ActiveVersion(CancellationToken cancellationToken) => ApiClient.Read(Routes.Byond, instance.Id, cancellationToken);
///
- public Task> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken);
+ public Task> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
+ => ReadPaged(paginationSettings, Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken);
///
public async Task SetActiveVersion(Byond byond, Stream zipFileStream, CancellationToken cancellationToken)
{
- var result = await apiClient.Update(
+ var result = await ApiClient.Update(
Routes.Byond,
byond ?? throw new ArgumentNullException(nameof(byond)),
instance.Id,
@@ -49,7 +45,7 @@ namespace Tgstation.Server.Client.Components
.ConfigureAwait(false);
if (byond.UploadCustomZip == true)
- await apiClient.Upload(result, zipFileStream, cancellationToken).ConfigureAwait(false);
+ await ApiClient.Upload(result, zipFileStream, cancellationToken).ConfigureAwait(false);
return result;
}
diff --git a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs
index 5a05e8c930..fa58da1c8a 100644
--- a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs
+++ b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
///
- sealed class ChatBotsClient : IChatBotsClient
+ sealed class ChatBotsClient : PaginatedClient, IChatBotsClient
{
- ///
- /// The for the
- ///
- readonly IApiClient apiClient;
-
///
/// The for the
///
@@ -23,27 +18,28 @@ namespace Tgstation.Server.Client.Components
///
/// Construct a
///
- /// The value of
+ /// The for the .
/// The value of
public ChatBotsClient(IApiClient apiClient, Instance instance)
+ : base(apiClient)
{
- this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
///
- public Task Create(ChatBot settings, CancellationToken cancellationToken) => apiClient.Create(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
+ public Task Create(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Create(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
///
- public Task Delete(ChatBot settings, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Chat, settings?.Id ?? throw new ArgumentNullException(nameof(settings))), instance.Id, cancellationToken);
+ public Task Delete(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Delete(Routes.SetID(Routes.Chat, settings?.Id ?? throw new ArgumentNullException(nameof(settings))), instance.Id, cancellationToken);
///
- public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Chat), instance.Id, cancellationToken);
+ public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
+ => ReadPaged(paginationSettings, Routes.ListRoute(Routes.Chat), instance.Id, cancellationToken);
///
- public Task Update(ChatBot settings, CancellationToken cancellationToken) => apiClient.Update(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
+ public Task Update(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Update(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
///
- public Task GetId(ChatBot settings, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.Chat, (settings ?? throw new ArgumentNullException(nameof(settings))).Id), instance.Id, cancellationToken);
+ public Task GetId(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.Chat, (settings ?? throw new ArgumentNullException(nameof(settings))).Id), instance.Id, cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs
index 25f8a0060d..e0fdb69db6 100644
--- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs
+++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs
@@ -9,13 +9,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
///
- sealed class ConfigurationClient : IConfigurationClient
+ sealed class ConfigurationClient : PaginatedClient, IConfigurationClient
{
- ///
- /// The for the
- ///
- readonly IApiClient apiClient;
-
///
/// The for the
///
@@ -24,34 +19,42 @@ namespace Tgstation.Server.Client.Components
///
/// Construct a
///
- /// The value of
+ /// The for the .
/// The value of
public ConfigurationClient(IApiClient apiClient, Instance instance)
+ : base(apiClient)
{
- this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
///
- public Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Delete(Routes.Configuration, directory, instance.Id, cancellationToken);
+ public Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => ApiClient.Delete(Routes.Configuration, directory, instance.Id, cancellationToken);
///
- public Task CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Create(Routes.Configuration, directory, instance.Id, cancellationToken);
+ public Task CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => ApiClient.Create(Routes.Configuration, directory, instance.Id, cancellationToken);
///
- public Task> List(string directory, CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Configuration) + Routes.SanitizeGetPath(directory), instance.Id, cancellationToken);
+ public Task> List(
+ PaginationSettings? paginationSettings,
+ string directory,
+ CancellationToken cancellationToken)
+ => ReadPaged(
+ paginationSettings,
+ Routes.ListRoute(Routes.Configuration) + Routes.SanitizeGetPath(directory),
+ instance.Id,
+ cancellationToken);
///
public async Task> Read(ConfigurationFile file, CancellationToken cancellationToken)
{
if (file == null)
throw new ArgumentNullException(nameof(file));
- var configFile = await apiClient.Read(
+ var configFile = await ApiClient.Read(
Routes.ConfigurationFile + Routes.SanitizeGetPath(file.Path ?? throw new ArgumentException("file.Path should not be null!", nameof(file))),
instance.Id,
cancellationToken)
.ConfigureAwait(false);
- var downloadStream = await apiClient.Download(configFile, cancellationToken).ConfigureAwait(false);
+ var downloadStream = await ApiClient.Download(configFile, cancellationToken).ConfigureAwait(false);
try
{
return Tuple.Create(configFile, downloadStream);
@@ -75,7 +78,7 @@ namespace Tgstation.Server.Client.Components
using (memoryStream)
{
- var configFileTask = apiClient.Update(
+ var configFileTask = ApiClient.Update(
Routes.Configuration,
file ?? throw new ArgumentNullException(nameof(file)),
instance.Id,
@@ -88,7 +91,7 @@ namespace Tgstation.Server.Client.Components
var streamUsed = memoryStream ?? uploadStream;
streamUsed?.Seek(initialStreamPosition, SeekOrigin.Begin);
- await apiClient.Upload(configFile, streamUsed, cancellationToken).ConfigureAwait(false);
+ await ApiClient.Upload(configFile, streamUsed, cancellationToken).ConfigureAwait(false);
return configFile;
}
diff --git a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs
index dafa049834..fdce033ef8 100644
--- a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs
+++ b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
///
- sealed class DreamMakerClient : IDreamMakerClient
+ sealed class DreamMakerClient : PaginatedClient, IDreamMakerClient
{
- ///
- /// The for the
- ///
- readonly IApiClient apiClient;
-
///
/// The for the
///
@@ -23,27 +18,28 @@ namespace Tgstation.Server.Client.Components
///
/// Construct a
///
- /// The value of
+ /// The for the .
/// The value of
public DreamMakerClient(IApiClient apiClient, Instance instance)
+ : base(apiClient)
{
- this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
///
- public Task Compile(CancellationToken cancellationToken) => apiClient.Create(Routes.DreamMaker, instance.Id, cancellationToken);
+ public Task Compile(CancellationToken cancellationToken) => ApiClient.Create(Routes.DreamMaker, instance.Id, cancellationToken);
///
- public Task GetCompileJob(EntityId compileJob, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken);
+ public Task GetCompileJob(EntityId compileJob, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken);
///
- public Task> GetJobIds(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken);
+ public Task> ListCompileJobs(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
+ => ReadPaged(paginationSettings, Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken);
///
- public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.DreamMaker, instance.Id, cancellationToken);
+ public Task Read(CancellationToken cancellationToken) => ApiClient.Read(Routes.DreamMaker, instance.Id, cancellationToken);
///
- public Task Update(DreamMaker dreamMaker, CancellationToken cancellationToken) => apiClient.Update(Routes.DreamMaker, dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker)), instance.Id, cancellationToken);
+ public Task Update(DreamMaker dreamMaker, CancellationToken cancellationToken) => ApiClient.Update(Routes.DreamMaker, dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker)), instance.Id, cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/src/Tgstation.Server.Client/Components/IByondClient.cs b/src/Tgstation.Server.Client/Components/IByondClient.cs
index 44c90f64c4..ee1befa5c7 100644
--- a/src/Tgstation.Server.Client/Components/IByondClient.cs
+++ b/src/Tgstation.Server.Client/Components/IByondClient.cs
@@ -21,9 +21,10 @@ namespace Tgstation.Server.Client.Components
///
/// Get all installed s
///
+ /// The optional for the operation.
/// The for the operation
/// A resulting in an of installed s
- Task> InstalledVersions(CancellationToken cancellationToken);
+ Task> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
///
/// Updates the information
diff --git a/src/Tgstation.Server.Client/Components/IChatBotsClient.cs b/src/Tgstation.Server.Client/Components/IChatBotsClient.cs
index c3f223938e..92485c0800 100644
--- a/src/Tgstation.Server.Client/Components/IChatBotsClient.cs
+++ b/src/Tgstation.Server.Client/Components/IChatBotsClient.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -13,9 +13,10 @@ namespace Tgstation.Server.Client.Components
///
/// List the s
///
+ /// The optional for the operation.
/// The for the operation
/// A resulting in a of the of the server
- Task> List(CancellationToken cancellationToken);
+ Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
///
/// Create a
diff --git a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs
index e82220c028..9e022fe084 100644
--- a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs
+++ b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs
@@ -15,10 +15,14 @@ namespace Tgstation.Server.Client.Components
///
/// List configuration files
///
+ /// The optional for the operation.
/// The path to the directory to list files in
/// The for the operation
/// A of s in the
- Task> List(string directory, CancellationToken cancellationToken);
+ Task> List(
+ PaginationSettings? paginationSettings,
+ string directory,
+ CancellationToken cancellationToken);
///
/// Read a file
diff --git a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs
index 174634bdb4..24d91af12a 100644
--- a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs
+++ b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -33,11 +33,12 @@ namespace Tgstation.Server.Client.Components
Task Compile(CancellationToken cancellationToken);
///
- /// Gets the s of all s for the instance
+ /// Gets the s for the instance
///
+ /// The optional for the operation.
/// The for the operation
/// A resulting in a of s.
- Task> GetJobIds(CancellationToken cancellationToken);
+ Task> ListCompileJobs(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
///
/// Get a
diff --git a/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs b/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs
index 3d1d54e8d7..8035783b12 100644
--- a/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs
+++ b/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -28,9 +28,10 @@ namespace Tgstation.Server.Client.Components
///
/// Get the s in the
///
+ /// The optional for the operation.
/// The for the operation
/// A resulting in a of s in the instance
- Task> List(CancellationToken cancellationToken);
+ Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
///
/// Update a
@@ -56,4 +57,4 @@ namespace Tgstation.Server.Client.Components
/// A representing the running operation
Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/src/Tgstation.Server.Client/Components/IJobsClient.cs b/src/Tgstation.Server.Client/Components/IJobsClient.cs
index ae879f485e..b3d800b51b 100644
--- a/src/Tgstation.Server.Client/Components/IJobsClient.cs
+++ b/src/Tgstation.Server.Client/Components/IJobsClient.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -11,18 +11,20 @@ namespace Tgstation.Server.Client.Components
public interface IJobsClient
{
///
- /// List the s in the
+ /// List the s in the
///
+ /// The optional for the operation.
/// The for the operation
/// A resulting in a of the s in the
- Task> List(CancellationToken cancellationToken);
+ Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
///
/// List the active s in the
///
+ /// The optional for the operation.
/// The for the operation
/// A resulting in a of the active s in the
- Task> ListActive(CancellationToken cancellationToken);
+ Task> ListActive(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
///
/// Get a
diff --git a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs
index 1dd1659ed7..740d94e4e7 100644
--- a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs
+++ b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
///
- sealed class InstanceUserClient : IInstanceUserClient
+ sealed class InstanceUserClient : PaginatedClient, IInstanceUserClient
{
- ///
- /// The for the
- ///
- readonly IApiClient apiClient;
-
///
/// The for the
///
@@ -23,19 +18,19 @@ namespace Tgstation.Server.Client.Components
///
/// Construct an
///
- /// The value of
+ /// The for the .
/// The value of
public InstanceUserClient(IApiClient apiClient, Instance instance)
+ : base(apiClient)
{
- this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
///
- public Task Create(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Create(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
+ public Task Create(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Create(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
///
- public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete(
+ public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Delete(
Routes.SetID(
Routes.InstanceUser,
instanceUser.UserId),
@@ -43,15 +38,16 @@ namespace Tgstation.Server.Client.Components
cancellationToken);
///
- public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.InstanceUser, instance.Id, cancellationToken);
+ public Task Read(CancellationToken cancellationToken) => ApiClient.Read(Routes.InstanceUser, instance.Id, cancellationToken);
///
- public Task Update(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Update(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
+ public Task Update(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Update(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
///
- public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.InstanceUser), instance.Id, cancellationToken);
+ public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
+ => ReadPaged(paginationSettings, Routes.ListRoute(Routes.InstanceUser), instance.Id, cancellationToken);
///
- public Task GetId(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.InstanceUser, instanceUser?.UserId ?? throw new ArgumentNullException(nameof(instanceUser))), instance.Id, cancellationToken);
+ public Task GetId(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.InstanceUser, instanceUser?.UserId ?? throw new ArgumentNullException(nameof(instanceUser))), instance.Id, cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/src/Tgstation.Server.Client/Components/JobsClient.cs b/src/Tgstation.Server.Client/Components/JobsClient.cs
index 228a2c4a9b..cf6a8875ec 100644
--- a/src/Tgstation.Server.Client/Components/JobsClient.cs
+++ b/src/Tgstation.Server.Client/Components/JobsClient.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
///
- sealed class JobsClient : IJobsClient
+ sealed class JobsClient : PaginatedClient, IJobsClient
{
- ///
- /// The for the
- ///
- readonly IApiClient apiClient;
-
///
/// The for the
///
@@ -23,24 +18,26 @@ namespace Tgstation.Server.Client.Components
///
/// Construct a
///
- /// The value of
+ /// The for the .
/// The value of
public JobsClient(IApiClient apiClient, Instance instance)
+ : base(apiClient)
{
- this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
///
- public Task Cancel(Job job, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
+ public Task Cancel(Job job, CancellationToken cancellationToken) => ApiClient.Delete(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
///
- public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Jobs), instance.Id, cancellationToken);
+ public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
+ => ReadPaged(paginationSettings, Routes.ListRoute(Routes.Jobs), instance.Id, cancellationToken);
///
- public Task> ListActive(CancellationToken cancellationToken) => apiClient.Read>(Routes.Jobs, instance.Id, cancellationToken);
+ public Task> ListActive(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
+ => ReadPaged(paginationSettings, Routes.Jobs, instance.Id, cancellationToken);
///
- public Task GetId(EntityId job, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
+ public Task GetId(EntityId job, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/src/Tgstation.Server.Client/IAdministrationClient.cs b/src/Tgstation.Server.Client/IAdministrationClient.cs
index b77e7475f7..b8abccfc91 100644
--- a/src/Tgstation.Server.Client/IAdministrationClient.cs
+++ b/src/Tgstation.Server.Client/IAdministrationClient.cs
@@ -37,9 +37,10 @@ namespace Tgstation.Server.Client
///
/// Lists the log files available for download.
///
+ /// The optional for the operation.
/// The for the operation
/// A resulting in an of metadata.
- Task> ListLogs(CancellationToken cancellationToken);
+ Task> ListLogs(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
///
/// Download a given .
diff --git a/src/Tgstation.Server.Client/IInstanceManagerClient.cs b/src/Tgstation.Server.Client/IInstanceManagerClient.cs
index 52bc42e769..4e53f8f32b 100644
--- a/src/Tgstation.Server.Client/IInstanceManagerClient.cs
+++ b/src/Tgstation.Server.Client/IInstanceManagerClient.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -14,9 +14,10 @@ namespace Tgstation.Server.Client
///
/// Get all s for s the user can view
///
+ /// The optional for the operation.
/// The for the operation
/// A resulting in a of all s the user can view
- Task> List(CancellationToken cancellationToken);
+ Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
///
/// Create or attach an
diff --git a/src/Tgstation.Server.Client/IUsersClient.cs b/src/Tgstation.Server.Client/IUsersClient.cs
index 8cf253f1bd..f024eb38ef 100644
--- a/src/Tgstation.Server.Client/IUsersClient.cs
+++ b/src/Tgstation.Server.Client/IUsersClient.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -28,9 +28,10 @@ namespace Tgstation.Server.Client
///
/// List all s
///
+ /// The optional for the operation.
/// The for the operation
/// A resulting in a of all s
- Task> List(CancellationToken cancellationToken);
+ Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
///
/// Create a new
@@ -48,4 +49,4 @@ namespace Tgstation.Server.Client
/// The updated
Task Update(UserUpdate user, CancellationToken cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/src/Tgstation.Server.Client/InstanceManagerClient.cs b/src/Tgstation.Server.Client/InstanceManagerClient.cs
index 9bd9b4846f..910f09896e 100644
--- a/src/Tgstation.Server.Client/InstanceManagerClient.cs
+++ b/src/Tgstation.Server.Client/InstanceManagerClient.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -9,41 +9,36 @@ using Tgstation.Server.Client.Components;
namespace Tgstation.Server.Client
{
///
- sealed class InstanceManagerClient : IInstanceManagerClient
+ sealed class InstanceManagerClient : PaginatedClient, IInstanceManagerClient
{
- ///
- /// The for the
- ///
- readonly IApiClient apiClient;
-
///
/// Construct an
///
- /// The value of
+ /// The for the .
public InstanceManagerClient(IApiClient apiClient)
- {
- this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
- }
+ : base(apiClient)
+ { }
///
- public Task CreateOrAttach(Instance instance, CancellationToken cancellationToken) => apiClient.Create(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
+ public Task CreateOrAttach(Instance instance, CancellationToken cancellationToken) => ApiClient.Create(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
///
- public Task Detach(Instance instance, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
+ public Task Detach(Instance instance, CancellationToken cancellationToken) => ApiClient.Delete(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
///
- public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.InstanceManager), cancellationToken);
+ public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
+ => ReadPaged(paginationSettings, Routes.ListRoute(Routes.InstanceManager), null, cancellationToken);
///
- public Task Update(Instance instance, CancellationToken cancellationToken) => apiClient.Update(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
+ public Task Update(Instance instance, CancellationToken cancellationToken) => ApiClient.Update(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
///
- public Task GetId(Instance instance, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
+ public Task GetId(Instance instance, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
///
- public Task GrantPermissions(Instance instance, CancellationToken cancellationToken) => apiClient.Patch(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
+ public Task GrantPermissions(Instance instance, CancellationToken cancellationToken) => ApiClient.Patch(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
///
- public IInstanceClient CreateClient(Instance instance) => new InstanceClient(apiClient, instance);
+ public IInstanceClient CreateClient(Instance instance) => new InstanceClient(ApiClient, instance);
}
-}
\ No newline at end of file
+}
diff --git a/src/Tgstation.Server.Client/PaginatedClient.cs b/src/Tgstation.Server.Client/PaginatedClient.cs
new file mode 100644
index 0000000000..fa9191c3cd
--- /dev/null
+++ b/src/Tgstation.Server.Client/PaginatedClient.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Api.Models;
+
+namespace Tgstation.Server.Client
+{
+ ///
+ /// Client that deals with getting paginated results.
+ ///
+ abstract class PaginatedClient
+ {
+ ///
+ /// The for the
+ ///
+ protected IApiClient ApiClient { get; }
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// The value of .
+ public PaginatedClient(IApiClient apiClient)
+ {
+ ApiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
+ }
+
+ ///
+ /// Reads a given with paged results.
+ ///
+ /// The of result model.
+ /// The if any.
+ /// The route.
+ /// The optional .
+ /// The for the operation.
+ /// A resulting in an of the paginated s.
+ protected async Task> ReadPaged(
+ PaginationSettings? paginationSettings,
+ string route,
+ long? instanceId,
+ CancellationToken cancellationToken)
+ {
+ if (route == null)
+ throw new ArgumentNullException(nameof(route));
+
+ var routeFormatter = $"{route}?page={{0}}";
+ var currentPage = 1;
+ if (paginationSettings != null)
+ {
+ if (paginationSettings.RetrieveCount == 0)
+ return new List(); // that was easy
+ else if (paginationSettings.RetrieveCount < 0)
+ throw new ArgumentOutOfRangeException("RetrieveCount cannot be less than 0!", nameof(paginationSettings));
+
+ int? pageSize = null;
+ if (paginationSettings.PageSize.HasValue)
+ {
+ // pagesize validates itself on first request
+ pageSize = paginationSettings.PageSize.Value;
+ routeFormatter += $"&pageSize={pageSize}";
+ }
+
+ if (paginationSettings.Offset.HasValue)
+ {
+ if(paginationSettings.Offset.Value < 0)
+ throw new ArgumentOutOfRangeException("Offset cannot be less than 0!", nameof(paginationSettings));
+
+ pageSize ??= paginationSettings.Offset.Value;
+ currentPage = (paginationSettings.Offset.Value / pageSize.Value) + 1;
+ }
+ }
+
+ Task> GetPage() => instanceId.HasValue
+ ? ApiClient.Read>(
+ String.Format(routeFormatter, currentPage),
+ instanceId.Value,
+ cancellationToken)
+ : ApiClient.Read>(
+ String.Format(routeFormatter, currentPage),
+ cancellationToken);
+
+ var firstPage = await GetPage().ConfigureAwait(false);
+
+ var totalAvailable = firstPage.TotalPages * firstPage.PageSize;
+ var maximumItems = paginationSettings?.RetrieveCount.HasValue == true
+ ? Math.Min(paginationSettings.RetrieveCount!.Value, totalAvailable)
+ : totalAvailable;
+
+ var results = new List(maximumItems);
+ var currentResults = firstPage;
+ do
+ {
+ // check if first page
+ if(currentPage > 1)
+ currentResults = await GetPage().ConfigureAwait(false);
+
+ if (currentResults.Content == null)
+ throw new ApiConflictException("Paginated results missing content!");
+
+ IEnumerable rangeToAdd = currentResults.Content;
+ if (paginationSettings?.Offset.HasValue == true)
+ {
+ rangeToAdd = rangeToAdd
+ .Skip(paginationSettings.Offset!.Value % currentResults.PageSize);
+ }
+
+ var itemsAvailableInPage = rangeToAdd.Count();
+ var itemsStillRequired = maximumItems - results.Count;
+ if (itemsAvailableInPage > itemsStillRequired)
+ rangeToAdd = rangeToAdd
+ .Take(itemsStillRequired);
+
+ results.AddRange(rangeToAdd);
+ ++currentPage;
+ }
+ while (results.Count < maximumItems && currentPage <= currentResults.TotalPages);
+
+ return results;
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Client/PaginationSettings.cs b/src/Tgstation.Server.Client/PaginationSettings.cs
new file mode 100644
index 0000000000..8c03734f20
--- /dev/null
+++ b/src/Tgstation.Server.Client/PaginationSettings.cs
@@ -0,0 +1,24 @@
+namespace Tgstation.Server.Client
+{
+ ///
+ /// Settings for a paginated request.
+ ///
+ public sealed class PaginationSettings
+ {
+ ///
+ /// The size of a page. Defaults to server settings.
+ ///
+ public int? PageSize { get; set; }
+
+ ///
+ /// The offset to take from. Default 0.
+ ///
+ public int? Offset { get; set; }
+
+ ///
+ /// The maximum amount of items to retrieve. Default everything.
+ ///
+ /// Results will be truncated if overflow occurs due to .
+ public int? RetrieveCount { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Client/UsersClient.cs b/src/Tgstation.Server.Client/UsersClient.cs
index 25ff956856..8d53a487dc 100644
--- a/src/Tgstation.Server.Client/UsersClient.cs
+++ b/src/Tgstation.Server.Client/UsersClient.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,35 +8,31 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
///
- sealed class UsersClient : IUsersClient
+ sealed class UsersClient : PaginatedClient, IUsersClient
{
- ///
- /// The for the
- ///
- readonly IApiClient apiClient;
-
///
/// Construct an
///
- /// The value of
+ /// The for the .
public UsersClient(IApiClient apiClient)
+ : base(apiClient)
{
- this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
}
///
- public Task Create(UserUpdate user, CancellationToken cancellationToken) => apiClient.Create(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken);
+ public Task Create(UserUpdate user, CancellationToken cancellationToken) => ApiClient.Create(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken);
///
- public Task GetId(Api.Models.Internal.User user, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken);
+ public Task GetId(Api.Models.Internal.User user, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken);
///
- public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.User), cancellationToken);
+ public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
+ => ReadPaged(paginationSettings, Routes.ListRoute(Routes.User), null, cancellationToken);
///
- public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.User, cancellationToken);
+ public Task Read(CancellationToken cancellationToken) => ApiClient.Read(Routes.User, cancellationToken);
///
- public Task Update(UserUpdate user, CancellationToken cancellationToken) => apiClient.Update(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken);
+ public Task Update(UserUpdate user, CancellationToken cancellationToken) => ApiClient.Update(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
index 779fa02d99..b309585c15 100644
--- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
@@ -313,48 +313,59 @@ namespace Tgstation.Server.Host.Controllers
///
/// List s present.
///
+ /// The current page.
+ /// The page size.
/// The for the operation.
/// A resulting in the of the request.
/// Listed logs successfully.
/// An IO error occurred while listing.
[HttpGet(Routes.Logs)]
[TgsAuthorize(AdministrationRights.DownloadLogs)]
- [ProducesResponseType(typeof(List), 200)]
+ [ProducesResponseType(typeof(Paginated), 200)]
[ProducesResponseType(typeof(ErrorMessage), 409)]
- public async Task ListLogs(CancellationToken cancellationToken)
- {
- var path = fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier);
- 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)
- .OrderByDescending(x => x.Name)
- .ToList();
-
- return Ok(result);
- }
- catch (IOException ex)
- {
- return Conflict(new ErrorMessage(ErrorCode.IOError)
+ public Task ListLogs([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
+ => Paginated(
+ async () =>
{
- AdditionalData = ex.ToString()
- });
- }
- }
+ var path = fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier);
+ 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)
+ .OrderByDescending(x => x.Name)
+ .ToList();
+
+ return new PaginatableResult(
+ result.AsQueryable());
+ }
+ catch (IOException ex)
+ {
+ return new PaginatableResult(
+ Conflict(new ErrorMessage(ErrorCode.IOError)
+ {
+ AdditionalData = ex.ToString()
+ }));
+ }
+ },
+ null,
+ page,
+ pageSize,
+ cancellationToken);
///
/// Download a .
diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
index 0a772be319..6b07fbd898 100644
--- a/src/Tgstation.Server.Host/Controllers/ApiController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -1,11 +1,14 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Octokit;
using Serilog.Context;
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
@@ -15,6 +18,7 @@ using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Database;
+using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
@@ -26,6 +30,16 @@ namespace Tgstation.Server.Host.Controllers
[ApiController]
public abstract class ApiController : Controller
{
+ ///
+ /// Default size of results.
+ ///
+ private const ushort DefaultPageSize = 10;
+
+ ///
+ /// Maximum size of results.
+ ///
+ private const ushort MaximumPageSize = 100;
+
///
/// The for the operation
///
@@ -275,6 +289,132 @@ namespace Tgstation.Server.Host.Controllers
await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
}
}
- #pragma warning restore CA1506
+#pragma warning restore CA1506
+
+ ///
+ /// Generates a paginated response.
+ ///
+ /// The of model being generated and returned.
+ /// A resulting in a resulting in the generated .
+ /// An to transform the s after being queried.
+ /// The requested page from the query.
+ /// The requested page size from the query.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ protected Task Paginated(
+ Func>> queryGenerator,
+ Action resultTransformer,
+ int? pageQuery,
+ int? pageSizeQuery,
+ CancellationToken cancellationToken) => PaginatedImpl(
+ queryGenerator,
+ resultTransformer,
+ pageQuery,
+ pageSizeQuery,
+ cancellationToken);
+
+ ///
+ /// Generates a paginated response.
+ ///
+ /// The of model being generated.
+ /// The of model being returned.
+ /// A resulting in a resulting in the generated .
+ /// An to transform the s after being queried.
+ /// The requested page from the query.
+ /// The requested page size from the query.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ protected Task Paginated(
+ Func>> queryGenerator,
+ Action resultTransformer,
+ int? pageQuery,
+ int? pageSizeQuery,
+ CancellationToken cancellationToken)
+ where TModel : IApiTransformable
+ => PaginatedImpl(
+ queryGenerator,
+ resultTransformer,
+ pageQuery,
+ pageSizeQuery,
+ cancellationToken);
+
+ ///
+ /// Generates a paginated response.
+ ///
+ /// The of model being generated. If different from , must implement for .
+ /// The of model being returned.
+ /// A resulting in a resulting in the generated .
+ /// An to transform the s after being queried.
+ /// The requested page from the query.
+ /// The requested page size from the query.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ async Task PaginatedImpl(
+ Func>> queryGenerator,
+ Action resultTransformer,
+ int? pageQuery,
+ int? pageSizeQuery,
+ CancellationToken cancellationToken)
+ {
+ if (queryGenerator == null)
+ throw new ArgumentNullException(nameof(queryGenerator));
+
+ if (pageQuery <= 0 || pageSizeQuery <= 0)
+ return BadRequest(new ErrorMessage(ErrorCode.ApiInvalidPageOrPageSize));
+
+ var pageSize = pageSizeQuery ?? DefaultPageSize;
+ if (pageSize > MaximumPageSize)
+ return BadRequest(new ErrorMessage(ErrorCode.ApiPageTooLarge)
+ {
+ AdditionalData = $"Maximum page size: {MaximumPageSize}"
+ });
+
+ var page = pageQuery ?? 1;
+
+ var paginationResult = await queryGenerator().ConfigureAwait(false);
+ if (paginationResult.EarlyOut != null)
+ return paginationResult.EarlyOut;
+
+ var queriedResults = paginationResult
+ .Results
+ .Skip((page - 1) * pageSize)
+ .Take(pageSize);
+
+ int totalResults;
+ List pagedResults;
+ if (queriedResults.Provider is IAsyncQueryProvider)
+ {
+ totalResults = await paginationResult.Results.CountAsync(cancellationToken).ConfigureAwait(false);
+ pagedResults = await queriedResults
+ .ToListAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ totalResults = paginationResult.Results.Count();
+ pagedResults = queriedResults.ToList();
+ }
+
+ if (resultTransformer != null)
+ foreach (var I in pagedResults)
+ resultTransformer(I);
+
+ ICollection finalResults;
+ if (typeof(TModel) == typeof(TResultModel))
+ finalResults = (List)(object)pagedResults; // clearly a safe cast
+ else
+ finalResults = pagedResults
+ .OfType>()
+ .Select(x => x.ToApi())
+ .ToList();
+
+ return Json(
+ new Paginated
+ {
+ Content = pagedResults,
+ PageSize = pageSize,
+ TotalPages = (ushort)((totalResults % pageSize) + 1)
+ });
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs
index 8c503ba47b..a03bb563f2 100644
--- a/src/Tgstation.Server.Host/Controllers/ByondController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs
@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
@@ -79,21 +78,31 @@ namespace Tgstation.Server.Host.Controllers
///
/// Lists installed versions.
///
+ /// The current page.
+ /// The page size.
+ /// The for the operation.
/// A resulting in the for the operation.
/// Retrieved version information successfully.
[HttpGet(Routes.List)]
[TgsAuthorize(ByondRights.ListInstalled)]
- [ProducesResponseType(typeof(IEnumerable), 200)]
- public Task List()
- => WithComponentInstance(instance =>
- Task.FromResult(
- Json(instance
- .ByondManager
- .InstalledVersions
- .Select(x => new Api.Models.Byond
- {
- Version = x
- }))));
+ [ProducesResponseType(typeof(Paginated), 200)]
+ public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
+ => WithComponentInstance(
+ instance => Paginated(
+ () => Task.FromResult(
+ new PaginatableResult(
+ instance
+ .ByondManager
+ .InstalledVersions
+ .Select(x => new Api.Models.Byond
+ {
+ Version = x
+ })
+ .AsQueryable())),
+ null,
+ page,
+ pageSize,
+ cancellationToken));
///
/// Changes the active BYOND version to the one specified in a given .
diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs
index 45c2db5d74..d328a300eb 100644
--- a/src/Tgstation.Server.Host/Controllers/ChatController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
@@ -168,29 +168,33 @@ namespace Tgstation.Server.Host.Controllers
///
/// List s.
///
+ /// The current page.
+ /// The page size.
/// The for the operation.
/// A resulting in the for the operation.
/// Listed chat bots successfully.
[HttpGet(Routes.List)]
[TgsAuthorize(ChatBotRights.Read)]
- [ProducesResponseType(typeof(IEnumerable), 200)]
- public async Task List(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Paginated), 200)]
+ public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
{
- var query = DatabaseContext
- .ChatBots
- .AsQueryable()
- .Where(x => x.InstanceId == Instance.Id)
- .Include(x => x.Channels);
-
- var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
-
var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
-
- if (!connectionStrings)
- foreach (var I in results)
- I.ConnectionString = null;
-
- return Json(results.Select(x => x.ToApi()));
+ return Paginated(
+ () => Task.FromResult(
+ new PaginatableResult(
+ DatabaseContext
+ .ChatBots
+ .AsQueryable()
+ .Where(x => x.InstanceId == Instance.Id)
+ .Include(x => x.Channels))),
+ chatBot =>
+ {
+ if (connectionStrings)
+ chatBot.ConnectionString = null;
+ },
+ page,
+ pageSize,
+ cancellationToken);
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
index 3d82b23fe8..1e07d538ce 100644
--- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
-using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -172,54 +171,75 @@ namespace Tgstation.Server.Host.Controllers
/// Get the contents of a directory at a
///
/// The path of the directory to get
+ /// The current page.
+ /// The page size.
/// The for the operation
/// A resulting in the for the operation
/// Directory listed successfully.>
/// Directory does not currently exist.
[HttpGet(Routes.List + "/{*directoryPath}")]
[TgsAuthorize(ConfigurationRights.List)]
- [ProducesResponseType(typeof(IReadOnlyList), 200)]
+ [ProducesResponseType(typeof(Paginated), 200)]
[ProducesResponseType(typeof(ErrorMessage), 410)]
- public async Task Directory(string directoryPath, CancellationToken cancellationToken)
- {
- if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity))
- return Forbid();
+ public Task Directory(
+ string directoryPath,
+ [FromQuery] int? page,
+ [FromQuery] int? pageSize,
+ CancellationToken cancellationToken)
+ => Paginated(
+ async () =>
+ {
+ if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity))
+ return new PaginatableResult(
+ Forbid());
- try
- {
- return await WithComponentInstance(
- async instance =>
+ try
{
- var result = await instance
- .Configuration
- .ListDirectory(directoryPath, systemIdentity, cancellationToken)
- .ConfigureAwait(false);
- if (result == null)
- return Gone();
+ return new PaginatableResult(
+ await WithComponentInstance(
+ async instance =>
+ {
+ var result = await instance
+ .Configuration
+ .ListDirectory(directoryPath, systemIdentity, cancellationToken)
+ .ConfigureAwait(false);
+ if (result == null)
+ return Gone();
- return Json(result);
- })
- .ConfigureAwait(false);
- }
- catch (NotImplementedException)
- {
- return RequiresPosixSystemIdentity();
- }
- catch (UnauthorizedAccessException)
- {
- return Forbid();
- }
- }
+ return Json(result);
+ })
+ .ConfigureAwait(false));
+ }
+ catch (NotImplementedException)
+ {
+ return new PaginatableResult(
+ RequiresPosixSystemIdentity());
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return new PaginatableResult(
+ Forbid());
+ }
+ },
+ null,
+ page,
+ pageSize,
+ cancellationToken);
///
/// Get the contents of the root configuration directory.
///
+ /// The current page.
+ /// The page size.
/// The for the operation.
/// A resulting in the for the operation.
[HttpGet(Routes.List)]
[TgsAuthorize(ConfigurationRights.List)]
- [ProducesResponseType(typeof(IReadOnlyList), 200)]
- public Task List(CancellationToken cancellationToken) => Directory(null, cancellationToken);
+ [ProducesResponseType(typeof(Paginated), 200)]
+ public Task List(
+ [FromQuery] int? page,
+ [FromQuery] int? pageSize,
+ CancellationToken cancellationToken) => Directory(null, page, pageSize, cancellationToken);
///
/// Create a configuration directory.
diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
index b1bc9b6432..3ba5e89f1a 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
@@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -95,43 +94,53 @@ namespace Tgstation.Server.Host.Controllers
[ProducesResponseType(typeof(ErrorMessage), 404)]
public async Task GetId(long id, CancellationToken cancellationToken)
{
- var compileJob = await DatabaseContext
- .CompileJobs
- .AsQueryable()
- .Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
- .Include(x => x.Job).ThenInclude(x => x.StartedBy)
- .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
- .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
+ var compileJob = await BaseCompileJobsQuery()
+ .Where(x => x.Id == id)
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (compileJob == default)
return NotFound();
return Json(compileJob.ToApi());
}
+ ///
+ /// Base query for pulling in all required fields.
+ ///
+ /// An of with all the inclusions.
+ IQueryable BaseCompileJobsQuery() => DatabaseContext
+ .CompileJobs
+ .AsQueryable()
+ .Include(x => x.Job)
+ .ThenInclude(x => x.StartedBy)
+ .Include(x => x.RevisionInformation)
+ .ThenInclude(x => x.PrimaryTestMerge)
+ .ThenInclude(x => x.MergedBy)
+ .Include(x => x.RevisionInformation)
+ .ThenInclude(x => x.ActiveTestMerges)
+ .ThenInclude(x => x.TestMerge)
+ .ThenInclude(x => x.MergedBy)
+ .Where(x => x.Job.Instance.Id == Instance.Id);
+
///
/// List all s for the instance.
///
+ /// The current page.
+ /// The page size.
/// The for the operation.
/// A resulting in the of the request.
/// Retrieved s successfully.
[HttpGet(Routes.List)]
[TgsAuthorize(DreamMakerRights.CompileJobs)]
- [ProducesResponseType(typeof(List), 200)]
- public async Task List(CancellationToken cancellationToken)
- {
- var compileJobs = await DatabaseContext
- .CompileJobs
- .AsQueryable()
- .Where(x => x.Job.Instance.Id == Instance.Id)
- .OrderByDescending(x => x.Job.StoppedAt)
- .Select(x => new EntityId
- {
- Id = x.Id
- })
- .ToListAsync(cancellationToken)
- .ConfigureAwait(false);
- return Json(compileJobs);
- }
+ [ProducesResponseType(typeof(Paginated), 200)]
+ public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
+ => Paginated(
+ () => Task.FromResult(
+ new PaginatableResult(
+ BaseCompileJobsQuery()
+ .OrderByDescending(x => x.Job.StoppedAt))),
+ null,
+ page,
+ pageSize,
+ cancellationToken);
///
/// Begin deploying repository code.
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
index cf475e6836..5b2467380a 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
@@ -586,13 +586,18 @@ namespace Tgstation.Server.Host.Controllers
///
/// List s.
///
+ /// The current page.
+ /// The page size.
/// The for the operation.
/// A resulting in the of the request.
/// Retrieved s successfully.
[HttpGet(Routes.List)]
[TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
- [ProducesResponseType(typeof(IEnumerable), 200)]
- public async Task List(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Paginated), 200)]
+ public async Task List(
+ [FromQuery] int? page,
+ [FromQuery] int? pageSize,
+ CancellationToken cancellationToken)
{
IQueryable GetBaseQuery()
{
@@ -622,21 +627,25 @@ namespace Tgstation.Server.Host.Controllers
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
- var instances = await GetBaseQuery()
- .ToListAsync(cancellationToken)
- .ConfigureAwait(false);
-
var needsUpdate = false;
- foreach (var instance in instances)
- needsUpdate |= InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance);
+ var result = await Paginated(
+ () => Task.FromResult(
+ new PaginatableResult(
+ GetBaseQuery())),
+ instance =>
+ {
+ needsUpdate |= InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance);
+ instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance.Id == instance.Id).ToApi();
+ },
+ page,
+ pageSize,
+ cancellationToken)
+ .ConfigureAwait(false);
if (needsUpdate)
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
- var apis = instances.Select(x => x.ToApi());
- foreach(var I in moveJobs)
- apis.Where(x => x.Id == I.Instance.Id).First().MoveJob = I.ToApi(); // if this .First() fails i will personally murder kevinz000 because I just know he is somehow responsible
- return Json(apis);
+ return result;
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
index 34586ea5cf..ca27e446b8 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
@@ -1,8 +1,7 @@
-using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -147,23 +146,27 @@ namespace Tgstation.Server.Host.Controllers
///
/// Lists s for the instance.
///
+ /// The current page.
+ /// The page size.
/// The for the operation.
/// A resulting in the of the request.
/// Retrieved s successfully.
[HttpGet(Routes.List)]
[TgsAuthorize(InstanceUserRights.ReadUsers)]
- [ProducesResponseType(typeof(IEnumerable), 200)]
- public async Task List(CancellationToken cancellationToken)
- {
- var users = await DatabaseContext
- .Instances
- .AsQueryable()
- .Where(x => x.Id == Instance.Id)
- .SelectMany(x => x.InstanceUsers)
- .ToListAsync(cancellationToken)
- .ConfigureAwait(false);
- return Json(users.Select(x => x.ToApi()));
- }
+ [ProducesResponseType(typeof(Paginated), 200)]
+ public Task