diff options
Diffstat (limited to 'Jellyfin.Api')
| -rw-r--r-- | Jellyfin.Api/Controllers/ClientLogController.cs | 149 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/DynamicHlsController.cs | 8 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/HlsSegmentController.cs | 6 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/ImageByNameController.cs | 6 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/SubtitleController.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/ClassMigrationHelper.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/StreamingHelpers.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Api/Jellyfin.Api.csproj | 4 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs | 22 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs | 30 |
11 files changed, 218 insertions, 15 deletions
diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs new file mode 100644 index 000000000..95d07c930 --- /dev/null +++ b/Jellyfin.Api/Controllers/ClientLogController.cs @@ -0,0 +1,149 @@ +using System; +using System.Net.Mime; +using System.Threading.Tasks; +using Jellyfin.Api.Attributes; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.ClientLogDtos; +using MediaBrowser.Controller.ClientEvent; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.ClientLog; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// Client log controller. + /// </summary> + [Authorize(Policy = Policies.DefaultAuthorization)] + public class ClientLogController : BaseJellyfinApiController + { + private const int MaxDocumentSize = 1_000_000; + private readonly IClientEventLogger _clientEventLogger; + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogController"/> class. + /// </summary> + /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public ClientLogController( + IClientEventLogger clientEventLogger, + IServerConfigurationManager serverConfigurationManager) + { + _clientEventLogger = clientEventLogger; + _serverConfigurationManager = serverConfigurationManager; + } + + /// <summary> + /// Post event from client. + /// </summary> + /// <param name="clientLogEventDto">The client log dto.</param> + /// <response code="204">Event logged.</response> + /// <response code="403">Event logging disabled.</response> + /// <returns>Submission status.</returns> + [HttpPost] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult LogEvent([FromBody] ClientLogEventDto clientLogEventDto) + { + if (!_serverConfigurationManager.Configuration.AllowClientLogUpload) + { + return Forbid(); + } + + var (clientName, clientVersion, userId, deviceId) = GetRequestInformation(); + Log(clientLogEventDto, userId, clientName, clientVersion, deviceId); + return NoContent(); + } + + /// <summary> + /// Bulk post events from client. + /// </summary> + /// <param name="clientLogEventDtos">The list of client log dtos.</param> + /// <response code="204">All events logged.</response> + /// <response code="403">Event logging disabled.</response> + /// <returns>Submission status.</returns> + [HttpPost("Bulk")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult LogEvents([FromBody] ClientLogEventDto[] clientLogEventDtos) + { + if (!_serverConfigurationManager.Configuration.AllowClientLogUpload) + { + return Forbid(); + } + + var (clientName, clientVersion, userId, deviceId) = GetRequestInformation(); + foreach (var dto in clientLogEventDtos) + { + Log(dto, userId, clientName, clientVersion, deviceId); + } + + return NoContent(); + } + + /// <summary> + /// Upload a document. + /// </summary> + /// <response code="200">Document saved.</response> + /// <response code="403">Event logging disabled.</response> + /// <response code="413">Upload size too large.</response> + /// <returns>Create response.</returns> + [HttpPost("Document")] + [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] + [AcceptsFile(MediaTypeNames.Text.Plain)] + [RequestSizeLimit(MaxDocumentSize)] + public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile() + { + if (!_serverConfigurationManager.Configuration.AllowClientLogUpload) + { + return Forbid(); + } + + if (Request.ContentLength > MaxDocumentSize) + { + // Manually validate to return proper status code. + return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes"); + } + + var (clientName, clientVersion, _, _) = GetRequestInformation(); + var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body) + .ConfigureAwait(false); + return Ok(new ClientLogDocumentResponseDto(fileName)); + } + + private void Log( + ClientLogEventDto dto, + Guid userId, + string clientName, + string clientVersion, + string deviceId) + { + _clientEventLogger.Log(new ClientLogEvent( + dto.Timestamp, + dto.Level, + userId, + clientName, + clientVersion, + deviceId, + dto.Message)); + } + + private (string ClientName, string ClientVersion, Guid UserId, string DeviceId) GetRequestInformation() + { + var clientName = ClaimHelpers.GetClient(HttpContext.User) ?? "unknown-client"; + var clientVersion = ClaimHelpers.GetIsApiKey(HttpContext.User) + ? "apikey" + : ClaimHelpers.GetVersion(HttpContext.User) ?? "unknown-version"; + var userId = ClaimHelpers.GetUserId(HttpContext.User) ?? Guid.Empty; + var deviceId = ClaimHelpers.GetDeviceId(HttpContext.User) ?? "unknown-device-id"; + + return (clientName, clientVersion, userId, deviceId); + } + } +} diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 42e82dd5b..049fd503b 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1794,7 +1794,7 @@ namespace Jellyfin.Api.Controllers return; } - _logger.LogDebug("Deleting partial HLS file {path}", path); + _logger.LogDebug("Deleting partial HLS file {Path}", path); try { @@ -1802,15 +1802,15 @@ namespace Jellyfin.Api.Controllers } catch (IOException ex) { - _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path); + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); var task = Task.Delay(100); - Task.WaitAll(task); + task.Wait(); DeleteFile(path, retryCount + 1); } catch (Exception ex) { - _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path); + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); } } diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 71caa0fe0..7325dca0a 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath)) + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid segment."); } @@ -90,7 +90,7 @@ namespace Jellyfin.Api.Controllers var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8") + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8") { return BadRequest("Invalid segment."); } @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath)) + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture)) { return BadRequest("Invalid segment."); } diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 99ab7f232..89bbf22c9 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - if (!path.StartsWith(_applicationPaths.GeneralPath)) + if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } @@ -177,7 +177,7 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { - if (!path.StartsWith(basePath)) + if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } @@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { - if (!path.StartsWith(basePath)) + if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index db8307f28..16acedcf3 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -528,7 +528,7 @@ namespace Jellyfin.Api.Controllers if (fontFile != null && fileSize != null && fileSize > 0) { - _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize); + _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize); return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName)); } else diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs index a911a3324..76fb27bcc 100644 --- a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs +++ b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Api.Helpers // If any this null throw an exception. if (source == null || destination == null) { - throw new Exception("Source or/and Destination Objects are null"); + throw new ArgumentException("Source or/and Destination Objects are null"); } // Getting the Types of the objects. diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 4fc791665..1b8f24c27 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -148,7 +148,7 @@ namespace Jellyfin.Api.Helpers mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId) ? mediaSources[0] - : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.InvariantCulture)); + : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal)); if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id) { diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 07d0b5543..f435bbf00 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -283,6 +283,7 @@ namespace Jellyfin.Api.Helpers lock (job.ProcessLock!) { + #pragma warning disable CA1849 // Can't await in lock block job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); var process = job.Process; @@ -308,6 +309,7 @@ namespace Jellyfin.Api.Helpers { } } + #pragma warning restore CA1849 } if (delete(job.Path!)) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 57480b2f3..a3598edfa 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -13,8 +13,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" /> </ItemGroup> diff --git a/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs new file mode 100644 index 000000000..44509a9c0 --- /dev/null +++ b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs @@ -0,0 +1,22 @@ +namespace Jellyfin.Api.Models.ClientLogDtos +{ + /// <summary> + /// Client log document response dto. + /// </summary> + public class ClientLogDocumentResponseDto + { + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogDocumentResponseDto"/> class. + /// </summary> + /// <param name="fileName">The file name.</param> + public ClientLogDocumentResponseDto(string fileName) + { + FileName = fileName; + } + + /// <summary> + /// Gets the resulting filename. + /// </summary> + public string FileName { get; } + } +} diff --git a/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs b/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs new file mode 100644 index 000000000..9bf9be0a4 --- /dev/null +++ b/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs @@ -0,0 +1,30 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Models.ClientLogDtos +{ + /// <summary> + /// The client log dto. + /// </summary> + public class ClientLogEventDto + { + /// <summary> + /// Gets or sets the event timestamp. + /// </summary> + [Required] + public DateTime Timestamp { get; set; } + + /// <summary> + /// Gets or sets the log level. + /// </summary> + [Required] + public LogLevel Level { get; set; } + + /// <summary> + /// Gets or sets the log message. + /// </summary> + [Required] + public string Message { get; set; } = string.Empty; + } +} |
