wabbajack/Wabbajack.Server/Controllers/AuthoredFiles.cs

200 lines
7.7 KiB
C#
Raw Normal View History

2020-05-09 22:16:16 +00:00
using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Security.Claims;
2021-09-27 12:42:46 +00:00
using System.Threading;
2020-05-09 22:16:16 +00:00
using System.Threading.Tasks;
using FluentFTP;
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;
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;
2020-05-09 22:16:16 +00:00
using Wabbajack.Server.DataLayer;
using Wabbajack.Server.DTOs;
using Wabbajack.Server.Services;
2021-09-27 12:42:46 +00:00
using Wabbajack.Server.TokenProviders;
2020-05-09 22:16:16 +00:00
namespace Wabbajack.BuildServer.Controllers
{
2020-06-16 22:21:01 +00:00
[Authorize(Roles = "Author")]
2020-05-09 22:16:16 +00:00
[Route("/authored_files")]
public class AuthoredFiles : ControllerBase
{
private SqlService _sql;
private ILogger<AuthoredFiles> _logger;
private AppSettings _settings;
2021-02-04 03:48:30 +00:00
private DiscordWebHook _discord;
2021-09-27 12:42:46 +00:00
private readonly IFtpSiteCredentials _ftpCreds;
private readonly DTOSerializer _dtos;
2020-05-09 22:16:16 +00:00
2021-09-27 12:42:46 +00:00
public AuthoredFiles(ILogger<AuthoredFiles> logger, SqlService sql, AppSettings settings, DiscordWebHook discord,
DTOSerializer dtos, IFtpSiteCredentials ftpCreds)
2020-05-09 22:16:16 +00:00
{
_sql = sql;
_logger = logger;
_settings = settings;
2021-02-04 03:48:30 +00:00
_discord = discord;
2021-09-27 12:42:46 +00:00
_dtos = dtos;
_ftpCreds = ftpCreds;
2020-05-09 22:16:16 +00:00
}
[HttpPut]
[Route("{serverAssignedUniqueId}/part/{index}")]
2021-09-27 12:42:46 +00:00
public async Task<IActionResult> UploadFilePart(CancellationToken token, string serverAssignedUniqueId, long index)
2020-05-09 22:16:16 +00:00
{
var user = User.FindFirstValue(ClaimTypes.Name);
var definition = await _sql.GetCDNFileDefinition(serverAssignedUniqueId);
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})");
await _sql.TouchAuthoredFile(definition);
var part = definition.Parts[index];
await using var ms = new MemoryStream();
2021-09-27 12:42:46 +00:00
await Request.Body.CopyToLimitAsync(ms, (int)part.Size, token);
2020-05-09 22:16:16 +00:00
ms.Position = 0;
if (ms.Length != part.Size)
return BadRequest($"Couldn't read enough data for part {part.Size} vs {ms.Length}");
2021-09-27 12:42:46 +00:00
var hash = await ms.Hash(token);
2020-05-09 22:16:16 +00:00
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;
await UploadAsync(ms, $"{definition.MungedName}/parts/{index}");
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
var definition = (await _dtos.DeserializeAsync<FileDefinition>(Request.Body))!;
2020-05-09 22:16:16 +00:00
2021-09-27 12:42:46 +00:00
_logger.Log(LogLevel.Information, "Creating File upload {originalFileName}", definition.OriginalFileName);
2020-05-09 22:16:16 +00:00
definition = await _sql.CreateAuthoredFile(definition, user);
using (var client = await GetBunnyCdnFtpClient())
{
await client.CreateDirectoryAsync($"{definition.MungedName}");
await client.CreateDirectoryAsync($"{definition.MungedName}/parts");
}
2021-02-04 03:48:30 +00:00
await _discord.Send(Channel.Ham,
new DiscordMessage() {Content = $"{user} has started uploading {definition.OriginalFileName} ({definition.Size.ToFileSizeString()})"});
2020-05-09 22:16:16 +00:00
return Ok(definition.ServerAssignedUniqueId);
}
[HttpPut]
[Route("{serverAssignedUniqueId}/finish")]
public async Task<IActionResult> CreateUpload(string serverAssignedUniqueId)
{
var user = User.FindFirstValue(ClaimTypes.Name);
var definition = await _sql.GetCDNFileDefinition(serverAssignedUniqueId);
if (definition.Author != user)
return Forbid("File Id does not match authorized user");
_logger.Log(LogLevel.Information, $"Finalizing file upload {definition.OriginalFileName}");
await _sql.Finalize(definition);
await using var ms = new MemoryStream();
await using (var gz = new GZipStream(ms, CompressionLevel.Optimal, true))
{
2021-09-27 12:42:46 +00:00
await _dtos.Serialize(definition, gz);
2020-05-09 22:16:16 +00:00
}
ms.Position = 0;
await UploadAsync(ms, $"{definition.MungedName}/definition.json.gz");
2021-02-04 03:48:30 +00:00
await _discord.Send(Channel.Ham,
new DiscordMessage {Content = $"{user} has finished uploading {definition.OriginalFileName} ({definition.Size.ToFileSizeString()})"});
2021-09-27 12:42:46 +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
}
private async Task<FtpClient> GetBunnyCdnFtpClient()
{
2021-09-27 12:42:46 +00:00
var info = (await _ftpCreds.Get())[StorageSpace.AuthoredFiles];
2020-05-09 22:16:16 +00:00
var client = new FtpClient(info.Hostname) {Credentials = new NetworkCredential(info.Username, info.Password)};
await client.ConnectAsync();
return client;
}
private async Task UploadAsync(Stream stream, string path)
{
using var client = await GetBunnyCdnFtpClient();
await client.UploadAsync(stream, path);
2020-05-09 22:16:16 +00:00
}
[HttpDelete]
[Route("{serverAssignedUniqueId}")]
public async Task<IActionResult> DeleteUpload(string serverAssignedUniqueId)
{
var user = User.FindFirstValue(ClaimTypes.Name);
var definition = await _sql.GetCDNFileDefinition(serverAssignedUniqueId);
if (definition.Author != user)
return Forbid("File Id does not match authorized user");
2021-07-20 05:08:12 +00:00
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
await DeleteFolderOrSilentlyFail($"{definition.MungedName}");
await _sql.DeleteFileDefinition(definition);
return Ok();
}
private async Task DeleteFolderOrSilentlyFail(string path)
{
try
{
using var client = await GetBunnyCdnFtpClient();
await client.DeleteDirectoryAsync(path);
}
catch (Exception)
{
_logger.Log(LogLevel.Information, $"Delete failed for {path}");
}
}
2020-05-10 01:35:42 +00:00
private static readonly Func<object, string> HandleGetListTemplate = NettleEngine.GetCompiler().Compile(@"
<html><body>
<table>
{{each $.files }}
<tr><td><a href='https://authored-files.wabbajack.org/{{$.MungedName}}'>{{$.OriginalFileName}}</a></td><td>{{$.Size}}</td><td>{{$.LastTouched}}</td><td>{{$.Finalized}}</td><td>{{$.Author}}</td></tr>
2020-05-10 01:35:42 +00:00
{{/each}}
</table>
</body></html>
");
[HttpGet]
[AllowAnonymous]
2020-05-10 01:35:42 +00:00
[Route("")]
public async Task<ContentResult> UploadedFilesGet()
{
var files = await _sql.AllAuthoredFiles();
var response = HandleGetListTemplate(new {files});
2020-05-10 01:35:42 +00:00
return new ContentResult
{
ContentType = "text/html",
StatusCode = (int) HttpStatusCode.OK,
Content = response
};
}
2020-05-09 22:16:16 +00:00
}
}