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 ;
2020-07-20 03:45:55 +00:00
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
2020-07-20 03:45:55 +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 ) ;
2021-03-06 03:54:04 +00:00
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-03-11 02:28:28 +00:00
2021-09-27 12:42:46 +00:00
var host = _settings . TestMode ? "test-files" : "authored-files" ;
2021-03-11 02:28:28 +00:00
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 ( ) ;
2021-03-18 03:36:37 +00:00
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" } ) ;
2021-07-21 21:40:55 +00:00
_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 } }
2021-03-11 02:28:28 +00:00
< 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]
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-05-09 22:16:16 +00:00
}
}