tgstation-server 6.8.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
AdministrationController.cs
Go to the documentation of this file.
1using System;
2using System.IO;
3using System.Linq;
4using System.Net;
5using System.Threading;
6using System.Threading.Tasks;
7using System.Web;
8
9using Microsoft.AspNetCore.Mvc;
10using Microsoft.Extensions.Logging;
11using Microsoft.Extensions.Options;
12
13using Octokit;
14
31
33{
37 [Route(Routes.Administration)]
39 {
43 const string OctokitException = "Bad GitHub API response, check configuration!";
44
49
54
59
64
69
74
79
84
101 IDatabaseContext databaseContext,
102 IAuthenticationContext authenticationContext,
110 ILogger<AdministrationController> logger,
111 IOptions<FileLoggingConfiguration> fileLoggingConfigurationOptions,
112 IApiHeadersProvider apiHeadersProvider)
113 : base(
114 databaseContext,
115 authenticationContext,
116 apiHeadersProvider,
117 logger,
118 true)
119 {
120 this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory));
121 this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
122 this.serverUpdateInitiator = serverUpdateInitiator ?? throw new ArgumentNullException(nameof(serverUpdateInitiator));
123 this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
124 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
125 this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
126 this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
127 fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions));
128 }
129
138 [HttpGet]
139 [TgsAuthorize(AdministrationRights.ChangeVersion)]
140 [ProducesResponseType(typeof(AdministrationResponse), 200)]
141 [ProducesResponseType(typeof(ErrorMessageResponse), 424)]
142 [ProducesResponseType(typeof(ErrorMessageResponse), 429)]
143 public async ValueTask<IActionResult> Read(CancellationToken cancellationToken)
144 {
145 try
146 {
147 Version? greatestVersion = null;
148 Uri? repoUrl = null;
149 try
150 {
151 var gitHubService = gitHubServiceFactory.CreateService();
152 var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
153 var releases = await gitHubService.GetTgsReleases(cancellationToken);
154
155 foreach (var kvp in releases)
156 {
157 var version = kvp.Key;
158 var release = kvp.Value;
159 if (version.Major > 3 // Forward/backward compatible but not before TGS4
160 && (greatestVersion == null || version > greatestVersion))
161 greatestVersion = version;
162 }
163
164 repoUrl = await repositoryUrlTask;
165 }
166 catch (NotFoundException e)
167 {
168 Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
169 }
170
171 return Json(new AdministrationResponse
172 {
173 LatestVersion = greatestVersion,
174 TrackedRepositoryUrl = repoUrl,
175 });
176 }
177 catch (RateLimitExceededException e)
178 {
179 return RateLimit(e);
180 }
181 catch (ApiException e)
182 {
183 Logger.LogWarning(e, OctokitException);
184 return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
185 {
186 AdditionalData = e.Message,
187 });
188 }
189 }
190
202 [HttpPost]
203 [TgsAuthorize(AdministrationRights.ChangeVersion | AdministrationRights.UploadVersion)]
204 [ProducesResponseType(typeof(ServerUpdateResponse), 202)]
205 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
206 [ProducesResponseType(typeof(ErrorMessageResponse), 422)]
207 [ProducesResponseType(typeof(ErrorMessageResponse), 424)]
208 [ProducesResponseType(typeof(ErrorMessageResponse), 429)]
209 public async ValueTask<IActionResult> Update([FromBody] ServerUpdateRequest model, CancellationToken cancellationToken)
210 {
211 ArgumentNullException.ThrowIfNull(model);
212
213 var attemptingUpload = model.UploadZip == true;
214 if (attemptingUpload)
215 {
217 return Forbid();
218 }
220 return Forbid();
221
222 if (model.NewVersion == null)
223 return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)
224 {
225 AdditionalData = "newVersion is required!",
226 });
227
228 if (model.NewVersion.Major < 3)
229 return BadRequest(new ErrorMessageResponse(ErrorCode.CannotChangeServerSuite));
230
232 return UnprocessableEntity(new ErrorMessageResponse(ErrorCode.MissingHostWatchdog));
233
234 return await AttemptInitiateUpdate(model.NewVersion, attemptingUpload, cancellationToken);
235 }
236
243 [HttpDelete]
244 [TgsAuthorize(AdministrationRights.RestartHost)]
245 [ProducesResponseType(204)]
246 [ProducesResponseType(typeof(ErrorMessageResponse), 422)]
247 public async ValueTask<IActionResult> Delete()
248 {
249 try
250 {
252 {
253 Logger.LogDebug("Restart request failed due to lack of host watchdog!");
254 return UnprocessableEntity(new ErrorMessageResponse(ErrorCode.MissingHostWatchdog));
255 }
256
257 await serverControl.Restart();
258 return NoContent();
259 }
260 catch (InvalidOperationException)
261 {
262 return StatusCode(HttpStatusCode.ServiceUnavailable);
263 }
264 }
265
275 [HttpGet(Routes.Logs)]
276 [TgsAuthorize(AdministrationRights.DownloadLogs)]
277 [ProducesResponseType(typeof(PaginatedResponse<LogFileResponse>), 200)]
278 [ProducesResponseType(typeof(ErrorMessageResponse), 409)]
279 public ValueTask<IActionResult> ListLogs([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
280 => Paginated(
281 async () =>
282 {
284 try
285 {
286 var files = await ioManager.GetFiles(path, cancellationToken);
287 var tasks = files.Select(
288 async file => new LogFileResponse
289 {
290 Name = ioManager.GetFileName(file),
291 LastModified = await ioManager
292 .GetLastModified(
293 ioManager.ConcatPath(path, file),
294 cancellationToken),
295 })
296 .ToList();
297
298 await Task.WhenAll(tasks);
299
301 tasks
302 .AsQueryable()
303 .Select(x => x.Result)
304 .OrderByDescending(x => x.Name));
305 }
306 catch (IOException ex)
307 {
309 Conflict(new ErrorMessageResponse(ErrorCode.IOError)
310 {
311 AdditionalData = ex.ToString(),
312 }));
313 }
314 },
315 null,
316 page,
317 pageSize,
318 cancellationToken);
319
328 [HttpGet(Routes.Logs + "/{*path}")]
329 [TgsAuthorize(AdministrationRights.DownloadLogs)]
330 [ProducesResponseType(typeof(LogFileResponse), 200)]
331 [ProducesResponseType(typeof(ErrorMessageResponse), 409)]
332 public async ValueTask<IActionResult> GetLog(string path, CancellationToken cancellationToken)
333 {
334 ArgumentNullException.ThrowIfNull(path);
335
336 path = HttpUtility.UrlDecode(path);
337
338 // guard against directory navigation
339 var sanitizedPath = ioManager.GetFileName(path);
340 if (path != sanitizedPath)
341 return Forbid();
342
343 var fullPath = ioManager.ConcatPath(
345 path);
346 try
347 {
348 var fileTransferTicket = fileTransferService.CreateDownload(
350 () => null,
351 null,
352 fullPath,
353 true));
354
355 return Ok(new LogFileResponse
356 {
357 Name = path,
358 LastModified = await ioManager.GetLastModified(fullPath, cancellationToken),
359 FileTicket = fileTransferTicket.FileTicket,
360 });
361 }
362 catch (IOException ex)
363 {
364 return Conflict(new ErrorMessageResponse(ErrorCode.IOError)
365 {
366 AdditionalData = ex.ToString(),
367 });
368 }
369 }
370
378 async ValueTask<IActionResult> AttemptInitiateUpdate(Version newVersion, bool attemptingUpload, CancellationToken cancellationToken)
379 {
380 IFileUploadTicket? uploadTicket = attemptingUpload
382 : null;
383
384 ServerUpdateResult updateResult;
385 try
386 {
387 try
388 {
389 updateResult = await serverUpdateInitiator.InitiateUpdate(uploadTicket, newVersion, cancellationToken);
390 }
391 catch
392 {
393 if (attemptingUpload)
394 await uploadTicket!.DisposeAsync();
395
396 throw;
397 }
398 }
399 catch (RateLimitExceededException e)
400 {
401 return RateLimit(e);
402 }
403 catch (ApiException e)
404 {
405 Logger.LogWarning(e, OctokitException);
406 return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
407 {
408 AdditionalData = e.Message,
409 });
410 }
411
412 return updateResult switch
413 {
414 ServerUpdateResult.Started => Accepted(new ServerUpdateResponse(newVersion, uploadTicket?.Ticket.FileTicket)),
415 ServerUpdateResult.ReleaseMissing => this.Gone(),
416 ServerUpdateResult.UpdateInProgress => BadRequest(new ErrorMessageResponse(ErrorCode.ServerUpdateInProgress)),
417 ServerUpdateResult.SwarmIntegrityCheckFailed => this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed)),
418 _ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"),
419 };
420 }
421 }
422}
AdministrationRights? AdministrationRights
The Rights.AdministrationRights for the user.
Represents an error message returned by the server.
A response to a Request.ServerUpdateRequest.
Routes to a server actions.
Definition: Routes.cs:9
const string Administration
The server administration controller.
Definition: Routes.cs:23
const string Logs
The endpoint to download server logs.
Definition: Routes.cs:28
string GetFullLogDirectory(IIOManager ioManager, IAssemblyInformationProvider assemblyInformationProvider, IPlatformIdentifier platformIdentifier)
Gets the evaluated log Directory.
ValueTask< IActionResult > ListLogs([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
List LogFileResponses present.
readonly FileLoggingConfiguration fileLoggingConfiguration
The FileLoggingConfiguration for the AdministrationController.
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the AdministrationController.
readonly IFileTransferTicketProvider fileTransferService
The IFileTransferTicketProvider for the AdministrationController.
async ValueTask< IActionResult > Update([FromBody] ServerUpdateRequest model, CancellationToken cancellationToken)
Attempt to perform a server upgrade.
async ValueTask< IActionResult > Read(CancellationToken cancellationToken)
Get AdministrationResponse server information.
async ValueTask< IActionResult > GetLog(string path, CancellationToken cancellationToken)
Download a LogFileResponse.
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the AdministrationController.
const string OctokitException
Default Exception.Message for ApiExceptions.
readonly IGitHubServiceFactory gitHubServiceFactory
The IGitHubServiceFactory for the AdministrationController.
readonly IIOManager ioManager
The IIOManager for the AdministrationController.
async ValueTask< IActionResult > AttemptInitiateUpdate(Version newVersion, bool attemptingUpload, CancellationToken cancellationToken)
Attempt to initiate an update.
async ValueTask< IActionResult > Delete()
Attempts to restart the server.
readonly IServerUpdateInitiator serverUpdateInitiator
The IServerUpdateInitiator for the AdministrationController.
AdministrationController(IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, IGitHubServiceFactory gitHubServiceFactory, IServerControl serverControl, IServerUpdateInitiator serverUpdateInitiator, IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager, IPlatformIdentifier platformIdentifier, IFileTransferTicketProvider fileTransferService, ILogger< AdministrationController > logger, IOptions< FileLoggingConfiguration > fileLoggingConfigurationOptions, IApiHeadersProvider apiHeadersProvider)
Initializes a new instance of the AdministrationController class.
readonly IServerControl serverControl
The IServerControl for the AdministrationController.
Base Controller for API functions.
StatusCodeResult StatusCode(HttpStatusCode statusCode)
Strongly type calls to ControllerBase.StatusCode(int).
ObjectResult RateLimit(RateLimitExceededException rateLimitException)
429 response for a given rateLimitException .
ILogger< ApiController > Logger
The ILogger for the ApiController.
PermissionSet PermissionSet
The User's effective PermissionSet.
Represents a file on disk to be downloaded.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
bool WatchdogPresent
true if live updates are supported, false. TryStartUpdate(IServerUpdateExecutor, Version) and Restart...
ValueTask Restart()
Restarts the Host.
ValueTask< ServerUpdateResult > InitiateUpdate(IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken)
Start the process of downloading and applying an update to a new server version .
Interface for using filesystems.
Definition: IIOManager.cs:13
Task< IReadOnlyList< string > > GetFiles(string path, CancellationToken cancellationToken)
Returns full file names in a given path .
string GetFileName(string path)
Gets the file name portion of a path .
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
Task< DateTimeOffset > GetLastModified(string path, CancellationToken cancellationToken)
Get the DateTimeOffset of when a given path was last modified.
Represents the currently authenticated Models.User.
For identifying the current platform.
Service for temporarily storing files to be downloaded or uploaded.
FileTicketResponse CreateDownload(FileDownloadProvider fileDownloadProvider)
Create a FileTicketResponse for a download.
IFileUploadTicket CreateUpload(FileUploadStreamKind streamKind)
Create a IFileUploadTicket.
A FileTicketResponse that waits for a pending upload.
IGitHubService CreateService()
Create a IGitHubService.
ValueTask< Uri > GetUpdatesRepositoryUrl(CancellationToken cancellationToken)
Gets the Uri of the repository designated as the updates repository.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition: ErrorCode.cs:12
AdministrationRights
Administration rights for the server.
ServerUpdateResult
The result of a call to start a server update.
FileUploadStreamKind
Determines the type of global::System.IO.Stream returned from IFileUploadTicket's created from IFileT...