wabbajack/Wabbajack.Server/Controllers/AuthoredFiles.cs

183 lines
6.8 KiB
C#
Raw Normal View History

2023-10-07 19:56:05 +00:00
using System.Net;
2020-05-09 22:16:16 +00:00
using System.Security.Claims;
2023-10-07 19:56:05 +00:00
using Humanizer;
using Humanizer.Localisation;
2020-06-03 20:16:41 +00:00
using Microsoft.AspNetCore.Authorization;
2020-05-09 22:16:16 +00:00
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
2021-11-27 18:31:35 +00:00
using Microsoft.Extensions.Primitives;
2020-05-10 01:35:42 +00:00
using Nettle;
2020-05-09 22:16:16 +00:00
using Wabbajack.Common;
2021-09-27 12:42:46 +00:00
using Wabbajack.DTOs.CDN;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Hashing.xxHash64;
2021-11-27 18:31:35 +00:00
using Wabbajack.Server.DataModels;
2020-05-09 22:16:16 +00:00
using Wabbajack.Server.DTOs;
using Wabbajack.Server.Services;
2020-05-09 22:16:16 +00:00
2021-10-23 16:51:17 +00:00
namespace Wabbajack.BuildServer.Controllers;
[Authorize(Roles = "Author")]
[Route("/authored_files")]
public class AuthoredFiles : ControllerBase
2020-05-09 22:16:16 +00:00
{
2021-10-23 16:51:17 +00:00
private readonly DTOSerializer _dtos;
private readonly DiscordWebHook _discord;
private readonly ILogger<AuthoredFiles> _logger;
private readonly AppSettings _settings;
2021-11-27 18:31:35 +00:00
private readonly AuthorFiles _authoredFiles;
2023-10-07 19:56:05 +00:00
private readonly Func<object,string> _authoredFilesTemplate;
2021-10-23 16:51:17 +00:00
2021-11-27 18:31:35 +00:00
public AuthoredFiles(ILogger<AuthoredFiles> logger, AuthorFiles authorFiles, AppSettings settings, DiscordWebHook discord,
DTOSerializer dtos)
2021-10-23 16:51:17 +00:00
{
_logger = logger;
_settings = settings;
_discord = discord;
_dtos = dtos;
2021-11-27 18:31:35 +00:00
_authoredFiles = authorFiles;
2023-10-07 19:56:05 +00:00
using var stream = typeof(AuthoredFiles).Assembly
.GetManifestResourceStream("Wabbajack.Server.Resources.Reports.AuthoredFiles.html");
_authoredFilesTemplate = NettleEngine.GetCompiler().Compile(stream.ReadAllText());
2021-10-23 16:51:17 +00:00
}
2021-11-27 18:31:35 +00:00
2021-10-23 16:51:17 +00:00
[HttpPut]
[Route("{serverAssignedUniqueId}/part/{index}")]
public async Task<IActionResult> UploadFilePart(CancellationToken token, string serverAssignedUniqueId, long index)
2020-05-09 22:16:16 +00:00
{
2021-10-23 16:51:17 +00:00
var user = User.FindFirstValue(ClaimTypes.Name);
2021-11-27 18:31:35 +00:00
var definition = await _authoredFiles.ReadDefinitionForServerId(serverAssignedUniqueId);
2021-10-23 16:51:17 +00:00
if (definition.Author != user)
return Forbid("File Id does not match authorized user");
_logger.Log(LogLevel.Information,
$"Uploading File part {definition.OriginalFileName} - ({index} / {definition.Parts.Length})");
2021-11-27 18:31:35 +00:00
2021-10-23 16:51:17 +00:00
var part = definition.Parts[index];
await using var ms = new MemoryStream();
await Request.Body.CopyToLimitAsync(ms, (int) part.Size, token);
ms.Position = 0;
if (ms.Length != part.Size)
return BadRequest($"Couldn't read enough data for part {part.Size} vs {ms.Length}");
var hash = await ms.Hash(token);
if (hash != part.Hash)
return BadRequest(
$"Hashes don't match for index {index}. Sizes ({ms.Length} vs {part.Size}). Hashes ({hash} vs {part.Hash}");
ms.Position = 0;
2023-10-12 12:04:38 +00:00
await _authoredFiles.WritePart(definition.MungedName, (int) index, ms);
2021-10-23 16:51:17 +00:00
return Ok(part.Hash.ToBase64());
}
[HttpPut]
[Route("create")]
public async Task<IActionResult> CreateUpload()
{
var user = User.FindFirstValue(ClaimTypes.Name);
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var definition = (await _dtos.DeserializeAsync<FileDefinition>(Request.Body))!;
2020-05-09 22:16:16 +00:00
2021-10-23 16:51:17 +00:00
_logger.Log(LogLevel.Information, "Creating File upload {originalFileName}", definition.OriginalFileName);
2021-11-27 18:31:35 +00:00
definition.ServerAssignedUniqueId = Guid.NewGuid().ToString();
definition.Author = user;
await _authoredFiles.WriteDefinition(definition);
2021-10-23 16:51:17 +00:00
await _discord.Send(Channel.Ham,
new DiscordMessage
{
Content =
$"{user} has started uploading {definition.OriginalFileName} ({definition.Size.ToFileSizeString()})"
});
return Ok(definition.ServerAssignedUniqueId);
}
[HttpPut]
[Route("{serverAssignedUniqueId}/finish")]
public async Task<IActionResult> CreateUpload(string serverAssignedUniqueId)
{
var user = User.FindFirstValue(ClaimTypes.Name);
2021-11-27 18:31:35 +00:00
var definition = await _authoredFiles.ReadDefinitionForServerId(serverAssignedUniqueId);
2021-10-23 16:51:17 +00:00
if (definition.Author != user)
return Forbid("File Id does not match authorized user");
_logger.Log(LogLevel.Information, $"Finalizing file upload {definition.OriginalFileName}");
await _discord.Send(Channel.Ham,
new DiscordMessage
2020-05-09 22:16:16 +00:00
{
2021-10-23 16:51:17 +00:00
Content =
$"{user} has finished uploading {definition.OriginalFileName} ({definition.Size.ToFileSizeString()})"
});
2020-05-09 22:16:16 +00:00
2021-10-23 16:51:17 +00:00
var host = _settings.TestMode ? "test-files" : "authored-files";
return Ok($"https://{host}.wabbajack.org/{definition.MungedName}");
}
2020-05-09 22:16:16 +00:00
2021-10-23 16:51:17 +00:00
[HttpDelete]
[Route("{serverAssignedUniqueId}")]
public async Task<IActionResult> DeleteUpload(string serverAssignedUniqueId)
{
var user = User.FindFirstValue(ClaimTypes.Name);
2023-10-12 12:04:38 +00:00
var definition = _authoredFiles.AllDefinitions
2021-11-27 18:31:35 +00:00
.First(f => f.Definition.ServerAssignedUniqueId == serverAssignedUniqueId)
.Definition;
2021-10-23 16:51:17 +00:00
if (definition.Author != user)
return Forbid("File Id does not match authorized user");
await _discord.Send(Channel.Ham,
new DiscordMessage
{
Content =
$"{user} is deleting {definition.MungedName}, {definition.Size.ToFileSizeString()} to be freed"
});
_logger.Log(LogLevel.Information, $"Deleting upload {definition.OriginalFileName}");
2020-05-09 22:16:16 +00:00
2021-11-27 18:31:35 +00:00
await _authoredFiles.DeleteFile(definition);
2021-10-23 16:51:17 +00:00
return Ok();
}
[HttpGet]
[AllowAnonymous]
[Route("")]
public async Task<ContentResult> UploadedFilesGet()
{
2023-10-12 12:04:38 +00:00
var files = _authoredFiles.AllDefinitions
.ToArray();
2023-10-07 19:56:05 +00:00
var response = _authoredFilesTemplate(new
{
Files = files.OrderByDescending(f => f.Updated).ToArray(),
2023-10-12 12:04:38 +00:00
UsedSpace = _authoredFiles.UsedSpace.Bytes().Humanize("#.##"),
2023-10-07 19:56:05 +00:00
});
2021-10-23 16:51:17 +00:00
return new ContentResult
2020-05-10 01:35:42 +00:00
{
2021-10-23 16:51:17 +00:00
ContentType = "text/html",
StatusCode = (int) HttpStatusCode.OK,
2023-10-07 19:56:05 +00:00
Content = response,
2021-10-23 16:51:17 +00:00
};
2020-05-09 22:16:16 +00:00
}
2021-11-27 18:31:35 +00:00
[HttpGet]
[AllowAnonymous]
[Route("direct_link/{mungedName}")]
public async Task DirectLink(string mungedName)
{
2021-12-20 16:22:40 +00:00
mungedName = _authoredFiles.DecodeName(mungedName);
2021-11-27 18:31:35 +00:00
var definition = await _authoredFiles.ReadDefinition(mungedName);
Response.Headers.ContentDisposition =
new StringValues($"attachment; filename={definition.OriginalFileName}");
Response.Headers.ContentType = new StringValues("application/octet-stream");
Response.Headers.ContentLength = definition.Size;
Response.Headers.ETag = definition.MungedName + "_direct";
2023-10-12 12:04:38 +00:00
foreach (var part in definition.Parts.OrderBy(p => p.Index))
2021-11-27 18:31:35 +00:00
{
2023-10-12 12:04:38 +00:00
await _authoredFiles.StreamForPart(mungedName, (int)part.Index, async stream =>
{
await stream.CopyToAsync(Response.Body);
});
2021-11-27 18:31:35 +00:00
}
}
2021-10-23 16:51:17 +00:00
}