tgstation-server 5.12.7
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
29
31{
35 [Route(Routes.Administration)]
37 {
41 const string OctokitException = "Bad GitHub API response, check configuration!";
42
47
52
57
62
67
72
77
82
98 IDatabaseContext databaseContext,
99 IAuthenticationContextFactory authenticationContextFactory,
107 ILogger<AdministrationController> logger,
108 IOptions<FileLoggingConfiguration> fileLoggingConfigurationOptions)
109 : base(
110 databaseContext,
111 authenticationContextFactory,
112 logger,
113 true)
114 {
115 this.gitHubService = gitHubService ?? throw new ArgumentNullException(nameof(gitHubService));
116 this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
117 this.serverUpdateInitiator = serverUpdateInitiator ?? throw new ArgumentNullException(nameof(serverUpdateInitiator));
118 this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
119 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
120 this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
121 this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
122 fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions));
123 }
124
133 [HttpGet]
134 [TgsAuthorize(AdministrationRights.ChangeVersion)]
135 [ProducesResponseType(typeof(AdministrationResponse), 200)]
136 [ProducesResponseType(typeof(ErrorMessageResponse), 424)]
137 [ProducesResponseType(typeof(ErrorMessageResponse), 429)]
138 public async Task<IActionResult> Read(CancellationToken cancellationToken)
139 {
140 try
141 {
142 Version greatestVersion = null;
143 Uri repoUrl = null;
144 try
145 {
146 var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
147 var releases = await gitHubService.GetTgsReleases(cancellationToken);
148
149 foreach (var kvp in releases)
150 {
151 var version = kvp.Key;
152 var release = kvp.Value;
153 if (version.Major > 3 // Forward/backward compatible but not before TGS4
154 && (greatestVersion == null || version > greatestVersion))
155 greatestVersion = version;
156 }
157
158 repoUrl = await repositoryUrlTask;
159 }
160 catch (NotFoundException e)
161 {
162 Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
163 }
164
165 return Json(new AdministrationResponse
166 {
167 LatestVersion = greatestVersion,
168 TrackedRepositoryUrl = repoUrl,
169 });
170 }
171 catch (RateLimitExceededException e)
172 {
173 return RateLimit(e);
174 }
175 catch (ApiException e)
176 {
177 Logger.LogWarning(e, OctokitException);
178 return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
179 {
180 AdditionalData = e.Message,
181 });
182 }
183 }
184
196 [HttpPost]
197 [TgsAuthorize(AdministrationRights.ChangeVersion | AdministrationRights.UploadVersion)]
198 [ProducesResponseType(typeof(ServerUpdateResponse), 202)]
199 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
200 [ProducesResponseType(typeof(ErrorMessageResponse), 422)]
201 [ProducesResponseType(typeof(ErrorMessageResponse), 424)]
202 [ProducesResponseType(typeof(ErrorMessageResponse), 429)]
203 public async Task<IActionResult> Update([FromBody] ServerUpdateRequest model, CancellationToken cancellationToken)
204 {
205 ArgumentNullException.ThrowIfNull(model);
206
207 var attemptingUpload = model.UploadZip == true;
208 if (attemptingUpload)
209 {
211 return Forbid();
212 }
214 return Forbid();
215
216 if (model.NewVersion == null)
217 return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)
218 {
219 AdditionalData = "newVersion is required!",
220 });
221
222 if (model.NewVersion.Major < 3)
223 return BadRequest(new ErrorMessageResponse(ErrorCode.CannotChangeServerSuite));
224
226 return UnprocessableEntity(new ErrorMessageResponse(ErrorCode.MissingHostWatchdog));
227
228 return await AttemptInitiateUpdate(model.NewVersion, attemptingUpload, cancellationToken);
229 }
230
237 [HttpDelete]
238 [TgsAuthorize(AdministrationRights.RestartHost)]
239 [ProducesResponseType(204)]
240 [ProducesResponseType(typeof(ErrorMessageResponse), 422)]
241 public async Task<IActionResult> Delete()
242 {
243 try
244 {
246 {
247 Logger.LogDebug("Restart request failed due to lack of host watchdog!");
248 return UnprocessableEntity(new ErrorMessageResponse(ErrorCode.MissingHostWatchdog));
249 }
250
251 await serverControl.Restart();
252 return NoContent();
253 }
254 catch (InvalidOperationException)
255 {
256 return StatusCode(HttpStatusCode.ServiceUnavailable);
257 }
258 }
259
269 [HttpGet(Routes.Logs)]
270 [TgsAuthorize(AdministrationRights.DownloadLogs)]
271 [ProducesResponseType(typeof(PaginatedResponse<LogFileResponse>), 200)]
272 [ProducesResponseType(typeof(ErrorMessageResponse), 409)]
273 public Task<IActionResult> ListLogs([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
274 => Paginated(
275 async () =>
276 {
278 try
279 {
280 var files = await ioManager.GetFiles(path, cancellationToken);
281 var tasks = files.Select(
282 async file => new LogFileResponse
283 {
284 Name = ioManager.GetFileName(file),
285 LastModified = await ioManager
286 .GetLastModified(
287 ioManager.ConcatPath(path, file),
288 cancellationToken),
289 })
290 .ToList();
291
292 await Task.WhenAll(tasks);
293
295 tasks
296 .AsQueryable()
297 .Select(x => x.Result)
298 .OrderByDescending(x => x.Name));
299 }
300 catch (IOException ex)
301 {
303 Conflict(new ErrorMessageResponse(ErrorCode.IOError)
304 {
305 AdditionalData = ex.ToString(),
306 }));
307 }
308 },
309 null,
310 page,
311 pageSize,
312 cancellationToken);
313
322 [HttpGet(Routes.Logs + "/{*path}")]
323 [TgsAuthorize(AdministrationRights.DownloadLogs)]
324 [ProducesResponseType(typeof(LogFileResponse), 200)]
325 [ProducesResponseType(typeof(ErrorMessageResponse), 409)]
326 public async Task<IActionResult> GetLog(string path, CancellationToken cancellationToken)
327 {
328 ArgumentNullException.ThrowIfNull(path);
329
330 path = HttpUtility.UrlDecode(path);
331
332 // guard against directory navigation
333 var sanitizedPath = ioManager.GetFileName(path);
334 if (path != sanitizedPath)
335 return Forbid();
336
337 var fullPath = ioManager.ConcatPath(
339 path);
340 try
341 {
342 var fileTransferTicket = fileTransferService.CreateDownload(
344 () => null,
345 null,
346 fullPath,
347 true));
348
349 return Ok(new LogFileResponse
350 {
351 Name = path,
352 LastModified = await ioManager.GetLastModified(fullPath, cancellationToken),
353 FileTicket = fileTransferTicket.FileTicket,
354 });
355 }
356 catch (IOException ex)
357 {
358 return Conflict(new ErrorMessageResponse(ErrorCode.IOError)
359 {
360 AdditionalData = ex.ToString(),
361 });
362 }
363 }
364
372 async Task<IActionResult> AttemptInitiateUpdate(Version newVersion, bool attemptingUpload, CancellationToken cancellationToken)
373 {
374 IFileUploadTicket uploadTicket = attemptingUpload
376 : null;
377
378 ServerUpdateResult updateResult;
379 try
380 {
381 try
382 {
383 updateResult = await serverUpdateInitiator.InitiateUpdate(uploadTicket, newVersion, cancellationToken);
384 }
385 catch
386 {
387 if (attemptingUpload)
388 await uploadTicket.DisposeAsync();
389
390 throw;
391 }
392 }
393 catch (RateLimitExceededException e)
394 {
395 return RateLimit(e);
396 }
397 catch (ApiException e)
398 {
399 Logger.LogWarning(e, OctokitException);
400 return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
401 {
402 AdditionalData = e.Message,
403 });
404 }
405
406 return updateResult switch
407 {
408 ServerUpdateResult.Started => Accepted(new ServerUpdateResponse(newVersion, uploadTicket?.Ticket.FileTicket)),
409 ServerUpdateResult.ReleaseMissing => this.Gone(),
410 ServerUpdateResult.UpdateInProgress => BadRequest(new ErrorMessageResponse(ErrorCode.ServerUpdateInProgress)),
411 ServerUpdateResult.SwarmIntegrityCheckFailed => this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed)),
412 _ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"),
413 };
414 }
415 }
416}
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:18
const string Logs
The endpoint to download server logs.
Definition: Routes.cs:23
string GetFullLogDirectory(IIOManager ioManager, IAssemblyInformationProvider assemblyInformationProvider, IPlatformIdentifier platformIdentifier)
Gets the evaluated log Directory.
async Task< IActionResult > AttemptInitiateUpdate(Version newVersion, bool attemptingUpload, CancellationToken cancellationToken)
Attempt to initiate an update.
AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubService gitHubService, IServerControl serverControl, IServerUpdateInitiator serverUpdateInitiator, IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager, IPlatformIdentifier platformIdentifier, IFileTransferTicketProvider fileTransferService, ILogger< AdministrationController > logger, IOptions< FileLoggingConfiguration > fileLoggingConfigurationOptions)
Initializes a new instance of the AdministrationController class.
readonly FileLoggingConfiguration fileLoggingConfiguration
The FileLoggingConfiguration for the AdministrationController.
async Task< IActionResult > GetLog(string path, CancellationToken cancellationToken)
Download a LogFileResponse.
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the AdministrationController.
Task< IActionResult > ListLogs([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
List LogFileResponses present.
async Task< IActionResult > Delete()
Attempts to restart the server.
readonly IFileTransferTicketProvider fileTransferService
The IFileTransferTicketProvider for the AdministrationController.
async Task< IActionResult > Read(CancellationToken cancellationToken)
Get AdministrationResponse server information.
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the AdministrationController.
const string OctokitException
Default Exception.Message for ApiExceptions.
readonly IIOManager ioManager
The IIOManager for the AdministrationController.
readonly IGitHubService gitHubService
The IGitHubService for the AdministrationController.
readonly IServerUpdateInitiator serverUpdateInitiator
The IServerUpdateInitiator for the AdministrationController.
readonly IServerControl serverControl
The IServerControl for the AdministrationController.
async Task< IActionResult > Update([FromBody] ServerUpdateRequest model, CancellationToken cancellationToken)
Attempt to perform a server upgrade.
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.
Helper for returning paginated models.
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...
Task< 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 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.
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.
Service for interacting with the GitHub API.
Task< Dictionary< Version, Release > > GetTgsReleases(CancellationToken cancellationToken)
Get all valid TGS Releases from the configured update source.
Task< 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:11
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...