2020-05-09 22:16:16 +00:00
|
|
|
|
using System;
|
2020-07-20 03:45:55 +00:00
|
|
|
|
using System.Globalization;
|
2020-05-09 22:16:16 +00:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.IO.Compression;
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Security.Claims;
|
|
|
|
|
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 SharpCompress.Compressors.LZMA;
|
|
|
|
|
using Wabbajack.Common;
|
|
|
|
|
using Wabbajack.Lib.AuthorApi;
|
|
|
|
|
using Wabbajack.Server.DataLayer;
|
|
|
|
|
using Wabbajack.Server.DTOs;
|
2020-07-20 03:45:55 +00:00
|
|
|
|
using Wabbajack.Server.Services;
|
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;
|
2020-07-20 03:45:55 +00:00
|
|
|
|
private CDNMirrorList _mirrorList;
|
2020-05-09 22:16:16 +00:00
|
|
|
|
|
2020-07-20 03:45:55 +00:00
|
|
|
|
|
|
|
|
|
public AuthoredFiles(ILogger<AuthoredFiles> logger, SqlService sql, AppSettings settings, CDNMirrorList mirrorList)
|
2020-05-09 22:16:16 +00:00
|
|
|
|
{
|
|
|
|
|
_sql = sql;
|
|
|
|
|
_logger = logger;
|
|
|
|
|
_settings = settings;
|
2020-07-20 03:45:55 +00:00
|
|
|
|
_mirrorList = mirrorList;
|
2020-05-09 22:16:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpPut]
|
|
|
|
|
[Route("{serverAssignedUniqueId}/part/{index}")]
|
|
|
|
|
public async Task<IActionResult> 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<IActionResult> CreateUpload()
|
|
|
|
|
{
|
|
|
|
|
var user = User.FindFirstValue(ClaimTypes.Name);
|
|
|
|
|
|
|
|
|
|
var data = await Request.Body.ReadAllTextAsync();
|
|
|
|
|
var definition = data.FromJsonString<CDNFileDefinition>();
|
|
|
|
|
|
|
|
|
|
_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<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))
|
|
|
|
|
{
|
|
|
|
|
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<FtpClient> GetBunnyCdnFtpClient()
|
|
|
|
|
{
|
2020-05-25 17:34:25 +00:00
|
|
|
|
var info = await Utils.FromEncryptedJson<BunnyCdnFtpInfo>("bunny-cdn-ftp-info");
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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");
|
|
|
|
|
_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}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-10 01:35:42 +00:00
|
|
|
|
|
|
|
|
|
private static readonly Func<object, string> HandleGetListTemplate = NettleEngine.GetCompiler().Compile(@"
|
|
|
|
|
<html><body>
|
|
|
|
|
<table>
|
|
|
|
|
{{each $.files }}
|
2020-05-10 01:58:01 +00:00
|
|
|
|
<tr><td><a href='https://wabbajack.b-cdn.net/{{$.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>
|
|
|
|
|
");
|
|
|
|
|
|
|
|
|
|
|
2020-07-20 03:45:55 +00:00
|
|
|
|
|
2020-05-10 01:35:42 +00:00
|
|
|
|
[HttpGet]
|
2020-06-03 21:04:18 +00:00
|
|
|
|
[AllowAnonymous]
|
2020-05-10 01:35:42 +00:00
|
|
|
|
[Route("")]
|
|
|
|
|
public async Task<ContentResult> UploadedFilesGet()
|
|
|
|
|
{
|
|
|
|
|
var files = await _sql.AllAuthoredFiles();
|
2020-05-10 01:58:01 +00:00
|
|
|
|
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-07-20 03:45:55 +00:00
|
|
|
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
[AllowAnonymous]
|
|
|
|
|
[Route("mirrors")]
|
|
|
|
|
public async Task<IActionResult> GetMirrorList()
|
|
|
|
|
{
|
|
|
|
|
Response.Headers.Add("x-last-updated", _mirrorList.LastUpdate.ToString(CultureInfo.InvariantCulture));
|
|
|
|
|
return Ok(_mirrorList.Mirrors);
|
|
|
|
|
}
|
2020-05-10 01:35:42 +00:00
|
|
|
|
|
2020-05-09 22:16:16 +00:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|