2021-11-17 08:36:23 -05:00
// Copyright Epic Games, Inc. All Rights Reserved.
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.ComponentModel.DataAnnotations ;
using System.Linq ;
2022-08-22 09:38:57 -04:00
using System.Net ;
2021-11-17 08:36:23 -05:00
using System.Net.Mime ;
using System.Threading.Tasks ;
2022-08-29 04:34:39 -04:00
using EpicGames.AspNet ;
2022-01-31 13:38:05 -05:00
using EpicGames.Horde.Storage ;
2022-02-24 05:54:09 -05:00
using EpicGames.Serialization ;
2021-11-29 09:09:10 -05:00
using Jupiter.Common.Implementation ;
2024-03-20 09:54:46 -04:00
using Jupiter.Implementation ;
2021-11-17 08:36:23 -05:00
using Jupiter.Utils ;
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Mvc ;
2022-11-28 10:05:07 -05:00
using Microsoft.Extensions.Logging ;
2021-11-17 08:36:23 -05:00
2022-10-12 06:36:30 -04:00
namespace Jupiter.Controllers
2021-11-17 08:36:23 -05:00
{
2023-07-27 11:20:47 -04:00
using BlobNotFoundException = Jupiter . Implementation . BlobNotFoundException ;
2024-03-20 09:54:46 -04:00
using IDiagnosticContext = Serilog . IDiagnosticContext ;
2022-01-31 13:38:05 -05:00
2023-07-27 11:20:47 -04:00
[ApiController]
[Route("api/v1/objects", Order = 0)]
[Authorize]
[Produces(CustomMediaTypeNames.UnrealCompactBinary, MediaTypeNames.Application.Json)]
public class ObjectController : ControllerBase
{
private readonly IBlobService _storage ;
private readonly IDiagnosticContext _diagnosticContext ;
private readonly IRequestHelper _requestHelper ;
private readonly IReferenceResolver _referenceResolver ;
private readonly BufferedPayloadFactory _bufferedPayloadFactory ;
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
private readonly ILogger _logger ;
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
public ObjectController ( IBlobService storage , IDiagnosticContext diagnosticContext , IRequestHelper requestHelper , IReferenceResolver referenceResolver , BufferedPayloadFactory bufferedPayloadFactory , ILogger < ObjectController > logger )
{
_storage = storage ;
_diagnosticContext = diagnosticContext ;
_requestHelper = requestHelper ;
_referenceResolver = referenceResolver ;
_bufferedPayloadFactory = bufferedPayloadFactory ;
_logger = logger ;
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
[HttpGet("{ns}/{id}")]
[ProducesDefaultResponseType]
2023-08-10 08:22:50 -04:00
public async Task < IActionResult > GetAsync (
2023-07-27 11:20:47 -04:00
[Required] NamespaceId ns ,
2023-08-09 13:08:12 -04:00
[Required] BlobId id )
2023-07-27 11:20:47 -04:00
{
2024-03-20 09:54:46 -04:00
ActionResult ? result = await _requestHelper . HasAccessToNamespaceAsync ( User , Request , ns , new [ ] { JupiterAclAction . ReadObject } ) ;
2023-07-27 11:20:47 -04:00
if ( result ! = null )
{
return result ;
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
try
{
2024-05-28 02:33:05 -04:00
BlobContents blobContents = await _storage . GetObjectAsync ( ns , id , bucketHint : null ) ;
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
return File ( blobContents . Stream , CustomMediaTypeNames . UnrealCompactBinary ) ;
}
catch ( BlobNotFoundException e )
{
2024-03-20 09:54:46 -04:00
return NotFound ( new ValidationProblemDetails { Title = $"Object {e.Blob} not found" } ) ;
2023-07-27 11:20:47 -04:00
}
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
[HttpHead("{ns}/{id}")]
[ProducesDefaultResponseType]
2023-08-10 08:22:50 -04:00
public async Task < IActionResult > HeadAsync (
2023-07-27 11:20:47 -04:00
[Required] NamespaceId ns ,
2023-08-09 13:08:12 -04:00
[Required] BlobId id )
2023-07-27 11:20:47 -04:00
{
2024-03-20 09:54:46 -04:00
ActionResult ? result = await _requestHelper . HasAccessToNamespaceAsync ( User , Request , ns , new [ ] { JupiterAclAction . ReadObject } ) ;
2023-07-27 11:20:47 -04:00
if ( result ! = null )
{
return result ;
}
2021-11-17 08:36:23 -05:00
2023-08-10 08:22:50 -04:00
bool exists = await _storage . ExistsAsync ( ns , id ) ;
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
if ( ! exists )
{
2024-03-20 09:54:46 -04:00
return NotFound ( new ValidationProblemDetails { Title = $"Object {id} not found" } ) ;
2023-07-27 11:20:47 -04:00
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
return Ok ( ) ;
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
[HttpPost("{ns}/exists")]
[ProducesDefaultResponseType]
2023-08-10 08:22:50 -04:00
public async Task < IActionResult > ExistsMultipleAsync (
2023-07-27 11:20:47 -04:00
[Required] NamespaceId ns ,
2024-03-20 09:54:46 -04:00
[Required] [ FromQuery ] List < BlobId > id )
2023-07-27 11:20:47 -04:00
{
2024-03-20 09:54:46 -04:00
ActionResult ? result = await _requestHelper . HasAccessToNamespaceAsync ( User , Request , ns , new [ ] { JupiterAclAction . ReadObject } ) ;
2023-07-27 11:20:47 -04:00
if ( result ! = null )
{
return result ;
}
2021-11-17 08:36:23 -05:00
2023-08-09 13:08:12 -04:00
ConcurrentBag < BlobId > missingBlobs = new ConcurrentBag < BlobId > ( ) ;
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
IEnumerable < Task > tasks = id . Select ( async blob = >
{
2023-08-10 08:22:50 -04:00
if ( ! await _storage . ExistsAsync ( ns , blob ) )
2023-07-27 11:20:47 -04:00
{
missingBlobs . Add ( blob ) ;
}
} ) ;
await Task . WhenAll ( tasks ) ;
2021-11-17 08:36:23 -05:00
2024-03-20 09:54:46 -04:00
return Ok ( new HeadMultipleResponse { Needs = missingBlobs . ToArray ( ) } ) ;
2023-07-27 11:20:47 -04:00
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
[HttpPost("{ns}/exist")]
[ProducesDefaultResponseType]
2023-08-10 08:22:50 -04:00
public async Task < IActionResult > ExistsBodyAsync (
2023-07-27 11:20:47 -04:00
[Required] NamespaceId ns ,
2023-08-09 13:08:12 -04:00
[FromBody] BlobId [ ] bodyIds )
2023-07-27 11:20:47 -04:00
{
2024-03-20 09:54:46 -04:00
ActionResult ? result = await _requestHelper . HasAccessToNamespaceAsync ( User , Request , ns , new [ ] { JupiterAclAction . ReadObject } ) ;
2023-07-27 11:20:47 -04:00
if ( result ! = null )
{
return result ;
}
2022-03-28 12:59:03 -04:00
2023-08-09 13:08:12 -04:00
ConcurrentBag < BlobId > missingBlobs = new ConcurrentBag < BlobId > ( ) ;
2022-03-28 12:59:03 -04:00
2023-07-27 11:20:47 -04:00
IEnumerable < Task > tasks = bodyIds . Select ( async blob = >
{
2023-08-10 08:22:50 -04:00
if ( ! await _storage . ExistsAsync ( ns , blob ) )
2023-07-27 11:20:47 -04:00
{
missingBlobs . Add ( blob ) ;
}
} ) ;
await Task . WhenAll ( tasks ) ;
2022-03-28 12:59:03 -04:00
2023-07-27 11:20:47 -04:00
return Ok ( new HeadMultipleResponse { Needs = missingBlobs . ToArray ( ) } ) ;
}
2022-03-28 12:59:03 -04:00
2023-07-27 11:20:47 -04:00
[HttpPut("{ns}/{id}")]
[RequiredContentType(CustomMediaTypeNames.UnrealCompactBinary)]
2023-08-10 08:22:50 -04:00
public async Task < IActionResult > PutAsync (
2023-07-27 11:20:47 -04:00
[Required] NamespaceId ns ,
2023-08-09 13:08:12 -04:00
[Required] BlobId id )
2023-07-27 11:20:47 -04:00
{
2024-03-20 09:54:46 -04:00
ActionResult ? result = await _requestHelper . HasAccessToNamespaceAsync ( User , Request , ns , new [ ] { JupiterAclAction . WriteObject } ) ;
2023-07-27 11:20:47 -04:00
if ( result ! = null )
{
return result ;
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
_diagnosticContext . Set ( "Content-Length" , Request . ContentLength ? ? - 1 ) ;
try
{
2024-03-21 10:01:20 -04:00
using IBufferedPayload payload = await _bufferedPayloadFactory . CreateFromRequestAsync ( Request , HttpContext . RequestAborted ) ;
2021-11-17 08:36:23 -05:00
2024-05-28 02:33:05 -04:00
BlobId identifier = await _storage . PutObjectAsync ( ns , payload , id , bucketHint : null , HttpContext . RequestAborted ) ;
2023-07-27 11:20:47 -04:00
return Ok ( new PutBlobResponse ( identifier ) ) ;
}
catch ( ClientSendSlowException e )
{
return Problem ( e . Message , null , ( int ) HttpStatusCode . RequestTimeout ) ;
}
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
[HttpGet("{ns}/{id}/references")]
2023-08-10 08:22:50 -04:00
public async Task < IActionResult > ResolveReferencesAsync (
2023-07-27 11:20:47 -04:00
[Required] NamespaceId ns ,
2023-08-09 13:08:12 -04:00
[Required] BlobId id )
2023-07-27 11:20:47 -04:00
{
2024-03-20 09:54:46 -04:00
ActionResult ? result = await _requestHelper . HasAccessToNamespaceAsync ( User , Request , ns , new [ ] { JupiterAclAction . ReadObject } ) ;
2023-07-27 11:20:47 -04:00
if ( result ! = null )
{
return result ;
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
BlobContents blob ;
try
{
2024-05-28 02:33:05 -04:00
blob = await _storage . GetObjectAsync ( ns , id , bucketHint : null ) ;
2023-07-27 11:20:47 -04:00
}
catch ( BlobNotFoundException e )
{
2024-03-20 09:54:46 -04:00
return NotFound ( new ValidationProblemDetails { Title = $"Object {e.Blob} not found" } ) ;
2023-07-27 11:20:47 -04:00
}
2024-03-20 09:54:46 -04:00
2024-03-21 10:01:20 -04:00
byte [ ] blobContents = await blob . Stream . ToByteArrayAsync ( HttpContext . RequestAborted ) ;
2023-07-27 11:20:47 -04:00
if ( blobContents . Length = = 0 )
{
_logger . LogWarning ( "0 byte object found for {Id} {Namespace}" , id , ns ) ;
}
2022-03-30 06:25:38 -04:00
2023-07-27 11:20:47 -04:00
CbObject compactBinaryObject ;
try
{
compactBinaryObject = new CbObject ( blobContents ) ;
}
catch ( IndexOutOfRangeException )
{
return Problem ( title : $"{id} was not a proper compact binary object." , detail : "Index out of range" ) ;
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
try
{
2024-03-20 09:57:58 -04:00
BlobId [ ] references = await _referenceResolver . GetReferencedBlobsAsync ( ns , compactBinaryObject ) . ToArrayAsync ( ) ;
2023-07-27 11:20:47 -04:00
return Ok ( new ResolvedReferencesResult ( references ) ) ;
}
catch ( PartialReferenceResolveException e )
{
2024-03-20 09:54:46 -04:00
return BadRequest ( new ValidationProblemDetails { Title = $"Object {id} is missing content ids" , Detail = $"Following content ids are invalid: {string.Join(" , ", e.UnresolvedReferences)}" } ) ;
2023-07-27 11:20:47 -04:00
}
catch ( ReferenceIsMissingBlobsException e )
{
2024-03-20 09:54:46 -04:00
return BadRequest ( new ValidationProblemDetails { Title = $"Object {id} is missing blobs" , Detail = $"Following blobs are missing: {string.Join(" , ", e.MissingBlobs)}" } ) ;
2023-07-27 11:20:47 -04:00
}
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
[HttpDelete("{ns}/{id}")]
2023-08-10 08:22:50 -04:00
public async Task < IActionResult > DeleteAsync (
2023-07-27 11:20:47 -04:00
[Required] NamespaceId ns ,
2023-08-09 13:08:12 -04:00
[Required] BlobId id )
2023-07-27 11:20:47 -04:00
{
2024-03-20 09:54:46 -04:00
ActionResult ? result = await _requestHelper . HasAccessToNamespaceAsync ( User , Request , ns , new [ ] { JupiterAclAction . DeleteObject } ) ;
2023-07-27 11:20:47 -04:00
if ( result ! = null )
{
return result ;
}
2021-11-17 08:36:23 -05:00
2024-03-21 10:01:20 -04:00
await _storage . DeleteObjectAsync ( ns , id , HttpContext . RequestAborted ) ;
2021-11-17 08:36:23 -05:00
2024-03-20 09:54:46 -04:00
return Ok ( new DeletedResponse
2023-07-27 11:20:47 -04:00
{
DeletedCount = 1
} ) ;
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
[HttpDelete("{ns}")]
2023-08-10 08:22:50 -04:00
public async Task < IActionResult > DeleteNamespaceAsync (
2023-07-27 11:20:47 -04:00
[Required] NamespaceId ns )
{
2024-03-20 09:54:46 -04:00
ActionResult ? result = await _requestHelper . HasAccessToNamespaceAsync ( User , Request , ns , new [ ] { JupiterAclAction . DeleteNamespace } ) ;
2023-07-27 11:20:47 -04:00
if ( result ! = null )
{
return result ;
}
2021-11-17 08:36:23 -05:00
2024-03-21 10:01:20 -04:00
await _storage . DeleteNamespaceAsync ( ns , HttpContext . RequestAborted ) ;
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
return Ok ( ) ;
}
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
public class PutBlobResponse
{
public PutBlobResponse ( )
{
Identifier = null ! ;
}
2022-02-24 05:54:09 -05:00
2023-08-09 13:08:12 -04:00
public PutBlobResponse ( BlobId identifier )
2023-07-27 11:20:47 -04:00
{
Identifier = identifier ;
}
2022-02-24 05:54:09 -05:00
2023-07-27 11:20:47 -04:00
[CbField("identifier")]
2023-08-09 13:08:12 -04:00
public BlobId Identifier { get ; set ; }
2023-07-27 11:20:47 -04:00
}
2022-02-24 05:54:09 -05:00
2023-07-27 11:20:47 -04:00
public class DeletedResponse
{
public int DeletedCount { get ; set ; }
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
public class ResolvedReferencesResult
{
public ResolvedReferencesResult ( )
{
References = null ! ;
}
2022-04-06 18:26:07 -04:00
2023-08-09 13:08:12 -04:00
public ResolvedReferencesResult ( BlobId [ ] references )
2023-07-27 11:20:47 -04:00
{
References = references ;
}
2021-11-17 08:36:23 -05:00
2023-07-27 11:20:47 -04:00
[CbField("references")]
2023-08-09 13:08:12 -04:00
public BlobId [ ] References { get ; set ; }
2023-07-27 11:20:47 -04:00
}
2021-11-17 08:36:23 -05:00
}