using System; using System.Globalization; using System.IO; using System.IO.Compression; using System.Net; using System.Security.Claims; using System.Threading.Tasks; using FluentFTP; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Nettle; using SharpCompress.Compressors.LZMA; using Wabbajack.Common; using Wabbajack.Lib.AuthorApi; using Wabbajack.Server.DataLayer; using Wabbajack.Server.DTOs; using Wabbajack.Server.Services; namespace Wabbajack.BuildServer.Controllers { [Authorize(Roles = "Author")] [Route("/authored_files")] public class AuthoredFiles : ControllerBase { private SqlService _sql; private ILogger _logger; private AppSettings _settings; private CDNMirrorList _mirrorList; public AuthoredFiles(ILogger logger, SqlService sql, AppSettings settings, CDNMirrorList mirrorList) { _sql = sql; _logger = logger; _settings = settings; _mirrorList = mirrorList; } [HttpPut] [Route("{serverAssignedUniqueId}/part/{index}")] public async Task UploadFilePart(string serverAssignedUniqueId, long index) { 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(); await Request.Body.CopyToLimitAsync(ms, part.Size); ms.Position = 0; if (ms.Length != part.Size) return BadRequest($"Couldn't read enough data for part {part.Size} vs {ms.Length}"); var hash = ms.xxHash(); 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 CreateUpload() { var user = User.FindFirstValue(ClaimTypes.Name); var data = await Request.Body.ReadAllTextAsync(); var definition = data.FromJsonString(); _logger.Log(LogLevel.Information, $"Creating File upload {definition.OriginalFileName}"); definition = await _sql.CreateAuthoredFile(definition, user); return Ok(definition.ServerAssignedUniqueId); } [HttpPut] [Route("{serverAssignedUniqueId}/finish")] public async Task 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)) { definition.ToJson(gz); } ms.Position = 0; await UploadAsync(ms, $"{definition.MungedName}/definition.json.gz"); return Ok($"https://{_settings.BunnyCDN_StorageZone}.b-cdn.net/{definition.MungedName}"); } private async Task GetBunnyCdnFtpClient() { var info = await Utils.FromEncryptedJson("bunny-cdn-ftp-info"); 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); } [HttpDelete] [Route("{serverAssignedUniqueId}")] public async Task 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"); _logger.Log(LogLevel.Information, $"Finalizing file upload {definition.OriginalFileName}"); 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}"); } } private static readonly Func HandleGetListTemplate = NettleEngine.GetCompiler().Compile(@" {{each $.files }} {{/each}}
{{$.OriginalFileName}}{{$.Size}}{{$.LastTouched}}{{$.Finalized}}{{$.Author}}
"); [HttpGet] [AllowAnonymous] [Route("")] public async Task UploadedFilesGet() { var files = await _sql.AllAuthoredFiles(); var response = HandleGetListTemplate(new {files}); return new ContentResult { ContentType = "text/html", StatusCode = (int) HttpStatusCode.OK, Content = response }; } [HttpGet] [AllowAnonymous] [Route("mirrors")] public async Task GetMirrorList() { Response.Headers.Add("x-last-updated", _mirrorList.LastUpdate.ToString(CultureInfo.InvariantCulture)); return Ok(_mirrorList.Mirrors); } } }