Files
UnrealEngine/Engine/Source/Programs/UnrealCloudDDC/Jupiter/Controllers/ObjectController.cs
2025-05-18 13:04:45 +08:00

298 lines
8.2 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Threading.Tasks;
using EpicGames.AspNet;
using EpicGames.Horde.Storage;
using EpicGames.Serialization;
using Jupiter.Common.Implementation;
using Jupiter.Implementation;
using Jupiter.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Jupiter.Controllers
{
using BlobNotFoundException = Jupiter.Implementation.BlobNotFoundException;
using IDiagnosticContext = Serilog.IDiagnosticContext;
[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;
private readonly ILogger _logger;
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;
}
[HttpGet("{ns}/{id}")]
[ProducesDefaultResponseType]
public async Task<IActionResult> GetAsync(
[Required] NamespaceId ns,
[Required] BlobId id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject });
if (result != null)
{
return result;
}
try
{
BlobContents blobContents = await _storage.GetObjectAsync(ns, id, bucketHint: null);
return File(blobContents.Stream, CustomMediaTypeNames.UnrealCompactBinary);
}
catch (BlobNotFoundException e)
{
return NotFound(new ValidationProblemDetails { Title = $"Object {e.Blob} not found" });
}
}
[HttpHead("{ns}/{id}")]
[ProducesDefaultResponseType]
public async Task<IActionResult> HeadAsync(
[Required] NamespaceId ns,
[Required] BlobId id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject });
if (result != null)
{
return result;
}
bool exists = await _storage.ExistsAsync(ns, id);
if (!exists)
{
return NotFound(new ValidationProblemDetails { Title = $"Object {id} not found" });
}
return Ok();
}
[HttpPost("{ns}/exists")]
[ProducesDefaultResponseType]
public async Task<IActionResult> ExistsMultipleAsync(
[Required] NamespaceId ns,
[Required][FromQuery] List<BlobId> id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject });
if (result != null)
{
return result;
}
ConcurrentBag<BlobId> missingBlobs = new ConcurrentBag<BlobId>();
IEnumerable<Task> tasks = id.Select(async blob =>
{
if (!await _storage.ExistsAsync(ns, blob))
{
missingBlobs.Add(blob);
}
});
await Task.WhenAll(tasks);
return Ok(new HeadMultipleResponse { Needs = missingBlobs.ToArray() });
}
[HttpPost("{ns}/exist")]
[ProducesDefaultResponseType]
public async Task<IActionResult> ExistsBodyAsync(
[Required] NamespaceId ns,
[FromBody] BlobId[] bodyIds)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject });
if (result != null)
{
return result;
}
ConcurrentBag<BlobId> missingBlobs = new ConcurrentBag<BlobId>();
IEnumerable<Task> tasks = bodyIds.Select(async blob =>
{
if (!await _storage.ExistsAsync(ns, blob))
{
missingBlobs.Add(blob);
}
});
await Task.WhenAll(tasks);
return Ok(new HeadMultipleResponse { Needs = missingBlobs.ToArray() });
}
[HttpPut("{ns}/{id}")]
[RequiredContentType(CustomMediaTypeNames.UnrealCompactBinary)]
public async Task<IActionResult> PutAsync(
[Required] NamespaceId ns,
[Required] BlobId id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.WriteObject });
if (result != null)
{
return result;
}
_diagnosticContext.Set("Content-Length", Request.ContentLength ?? -1);
try
{
using IBufferedPayload payload = await _bufferedPayloadFactory.CreateFromRequestAsync(Request, "put-object", HttpContext.RequestAborted);
BlobId identifier = await _storage.PutObjectAsync(ns, payload, id, bucketHint: null, cancellationToken: HttpContext.RequestAborted);
return Ok(new PutBlobResponse(identifier));
}
catch (ClientSendSlowException e)
{
return Problem(e.Message, null, (int)HttpStatusCode.RequestTimeout);
}
}
[HttpGet("{ns}/{id}/references")]
public async Task<IActionResult> ResolveReferencesAsync(
[Required] NamespaceId ns,
[Required] BlobId id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject });
if (result != null)
{
return result;
}
BlobContents blob;
try
{
blob = await _storage.GetObjectAsync(ns, id, bucketHint: null);
}
catch (BlobNotFoundException e)
{
return NotFound(new ValidationProblemDetails { Title = $"Object {e.Blob} not found" });
}
byte[] blobContents = await blob.Stream.ToByteArrayAsync(HttpContext.RequestAborted);
if (blobContents.Length == 0)
{
_logger.LogWarning("0 byte object found for {Id} {Namespace}", id, ns);
}
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");
}
try
{
BlobId[] references = await _referenceResolver.GetReferencedBlobsAsync(ns, compactBinaryObject).ToArrayAsync();
return Ok(new ResolvedReferencesResult(references));
}
catch (PartialReferenceResolveException e)
{
return BadRequest(new ValidationProblemDetails { Title = $"Object {id} is missing content ids", Detail = $"Following content ids are invalid: {string.Join(",", e.UnresolvedReferences)}" });
}
catch (ReferenceIsMissingBlobsException e)
{
return BadRequest(new ValidationProblemDetails { Title = $"Object {id} is missing blobs", Detail = $"Following blobs are missing: {string.Join(",", e.MissingBlobs)}" });
}
}
[HttpDelete("{ns}/{id}")]
public async Task<IActionResult> DeleteAsync(
[Required] NamespaceId ns,
[Required] BlobId id)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.DeleteObject });
if (result != null)
{
return result;
}
await _storage.DeleteObjectAsync(ns, id, HttpContext.RequestAborted);
return Ok(new DeletedResponse
{
DeletedCount = 1
});
}
[HttpDelete("{ns}")]
public async Task<IActionResult> DeleteNamespaceAsync(
[Required] NamespaceId ns)
{
ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.DeleteNamespace });
if (result != null)
{
return result;
}
await _storage.DeleteNamespaceAsync(ns, HttpContext.RequestAborted);
return Ok();
}
}
public class PutBlobResponse
{
public PutBlobResponse()
{
Identifier = null!;
}
public PutBlobResponse(BlobId identifier)
{
Identifier = identifier;
}
[CbField("identifier")]
public BlobId Identifier { get; set; }
}
public class DeletedResponse
{
public int DeletedCount { get; set; }
}
public class ResolvedReferencesResult
{
public ResolvedReferencesResult()
{
References = null!;
}
public ResolvedReferencesResult(BlobId[] references)
{
References = references;
}
[CbField("references")]
public BlobId[] References { get; set; }
}
}