2020-05-20 03:25:41 +00:00
using System ;
using System.IO ;
2020-07-10 11:38:30 +00:00
using System.Linq ;
2020-05-20 03:25:41 +00:00
using System.Net ;
using System.Threading.Tasks ;
using FluentFTP ;
using Microsoft.Extensions.Logging ;
using Splat ;
using Wabbajack.BuildServer ;
using Wabbajack.Common ;
2020-05-23 21:03:25 +00:00
using Wabbajack.Lib ;
2020-05-20 03:25:41 +00:00
using Wabbajack.Lib.CompilationSteps ;
using Wabbajack.Server.DataLayer ;
using Wabbajack.Server.DTOs ;
2020-05-21 21:25:44 +00:00
using LogLevel = Microsoft . Extensions . Logging . LogLevel ;
2020-05-20 03:25:41 +00:00
namespace Wabbajack.Server.Services
{
public class PatchBuilder : AbstractService < PatchBuilder , int >
{
private DiscordWebHook _discordWebHook ;
private SqlService _sql ;
private ArchiveMaintainer _maintainer ;
public PatchBuilder ( ILogger < PatchBuilder > logger , SqlService sql , AppSettings settings , ArchiveMaintainer maintainer ,
2020-06-06 21:44:30 +00:00
DiscordWebHook discordWebHook , QuickSync quickSync ) : base ( logger , settings , quickSync , TimeSpan . FromMinutes ( 1 ) )
2020-05-20 03:25:41 +00:00
{
_discordWebHook = discordWebHook ;
_sql = sql ;
_maintainer = maintainer ;
}
2020-06-29 21:57:09 +00:00
public bool NoCleaning { get ; set ; }
2020-05-20 03:25:41 +00:00
public override async Task < int > Execute ( )
{
int count = 0 ;
while ( true )
{
2020-05-23 21:03:25 +00:00
count + + ;
2020-05-20 03:25:41 +00:00
var patch = await _sql . GetPendingPatch ( ) ;
if ( patch = = default ) break ;
try
{
_logger . LogInformation (
$"Building patch from {patch.Src.Archive.State.PrimaryKeyString} to {patch.Dest.Archive.State.PrimaryKeyString}" ) ;
await _discordWebHook . Send ( Channel . Spam ,
new DiscordMessage
{
Content =
$"Building patch from {patch.Src.Archive.State.PrimaryKeyString} to {patch.Dest.Archive.State.PrimaryKeyString}"
} ) ;
2020-05-23 21:03:25 +00:00
if ( patch . Src . Archive . Hash = = patch . Dest . Archive . Hash )
{
await patch . Fail ( _sql , "Hashes match" ) ;
continue ;
}
if ( patch . Src . Archive . Size > 2_500_000_000 | | patch . Dest . Archive . Size > 2_500_000_000 )
{
await patch . Fail ( _sql , "Too large to patch" ) ;
continue ;
}
2020-05-20 03:25:41 +00:00
_maintainer . TryGetPath ( patch . Src . Archive . Hash , out var srcPath ) ;
_maintainer . TryGetPath ( patch . Dest . Archive . Hash , out var destPath ) ;
2020-05-28 02:43:57 +00:00
await using var sigFile = new TempFile ( ) ;
await using var patchFile = new TempFile ( ) ;
2020-05-25 17:34:25 +00:00
await using var srcStream = await srcPath . OpenShared ( ) ;
await using var destStream = await destPath . OpenShared ( ) ;
await using var sigStream = await sigFile . Path . Create ( ) ;
await using var patchOutput = await patchFile . Path . Create ( ) ;
2020-05-20 03:25:41 +00:00
OctoDiff . Create ( destStream , srcStream , sigStream , patchOutput ) ;
await patchOutput . DisposeAsync ( ) ;
2020-05-21 21:25:44 +00:00
var size = patchFile . Path . Size ;
2020-06-29 21:57:09 +00:00
await UploadToCDN ( patchFile . Path , PatchName ( patch ) ) ;
2020-05-21 21:25:44 +00:00
2020-05-20 03:25:41 +00:00
await patch . Finish ( _sql , size ) ;
2020-05-22 20:56:58 +00:00
await _discordWebHook . Send ( Channel . Spam ,
2020-05-20 12:18:47 +00:00
new DiscordMessage
{
Content =
$"Built {size.ToFileSizeString()} patch from {patch.Src.Archive.State.PrimaryKeyString} to {patch.Dest.Archive.State.PrimaryKeyString}"
} ) ;
2020-05-20 03:25:41 +00:00
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Error while building patch" ) ;
await patch . Fail ( _sql , ex . ToString ( ) ) ;
2020-05-20 12:18:47 +00:00
await _discordWebHook . Send ( Channel . Spam ,
new DiscordMessage
{
Content =
$"Failure building patch from {patch.Src.Archive.State.PrimaryKeyString} to {patch.Dest.Archive.State.PrimaryKeyString}"
} ) ;
2020-05-20 03:25:41 +00:00
}
}
2020-06-06 21:44:30 +00:00
if ( count > 0 )
{
// Notify the List Validator that we may have more patches
await _quickSync . Notify < ListValidator > ( ) ;
}
2020-06-29 21:57:09 +00:00
if ( ! NoCleaning )
await CleanupOldPatches ( ) ;
2020-05-20 03:25:41 +00:00
return count ;
}
2020-05-21 21:25:44 +00:00
2020-06-29 21:57:09 +00:00
private static string PatchName ( Patch patch )
{
return $"{Consts.ArchiveUpdatesCDNFolder}\\{patch.Src.Archive.Hash.ToHex()}_{patch.Dest.Archive.Hash.ToHex()}" ;
}
private async Task CleanupOldPatches ( )
{
var patches = await _sql . GetOldPatches ( ) ;
using var client = await GetBunnyCdnFtpClient ( ) ;
foreach ( var patch in patches )
{
_logger . LogInformation ( $"Cleaning patch {patch.Src.Archive.Hash} -> {patch.Dest.Archive.Hash}" ) ;
2020-07-07 20:17:49 +00:00
2020-07-09 04:14:35 +00:00
await _discordWebHook . Send ( Channel . Spam ,
2020-06-29 21:57:09 +00:00
new DiscordMessage
{
Content =
2020-07-08 20:48:48 +00:00
$"Removing {patch.PatchSize.FileSizeToString()} patch from {patch.Src.Archive.State.PrimaryKeyString} to {patch.Dest.Archive.State.PrimaryKeyString} due it no longer being required by curated lists"
2020-06-29 21:57:09 +00:00
} ) ;
2020-07-07 20:17:49 +00:00
2020-06-29 21:57:09 +00:00
if ( ! await DeleteFromCDN ( client , PatchName ( patch ) ) )
{
_logger . LogWarning ( $"Patch file didn't exist {PatchName(patch)}" ) ;
}
await _sql . DeletePatch ( patch ) ;
var pendingPatch = await _sql . GetPendingPatch ( ) ;
if ( pendingPatch ! = default ) break ;
2020-07-10 11:38:30 +00:00
}
var files = await client . GetListingAsync ( $"{Consts.ArchiveUpdatesCDNFolder}\\" ) ;
_logger . LogInformation ( $"Found {files.Length} on the CDN" ) ;
var sqlFiles = await _sql . AllPatchHashes ( ) ;
_logger . LogInformation ( $"Found {sqlFiles.Count} in SQL" ) ;
2020-06-29 21:57:09 +00:00
2020-07-10 11:38:30 +00:00
var hashPairs = files . Select ( f = > f . Name ) . Where ( f = > f . Contains ( "_" ) ) . Select ( p = >
{
var lst = p . Split ( "_" , StringSplitOptions . RemoveEmptyEntries ) . Select ( Hash . FromHex ) . ToArray ( ) ;
return ( lst [ 0 ] , lst [ 1 ] ) ;
} ) . ToHashSet ( ) ;
foreach ( var sqlFile in sqlFiles . Where ( s = > ! hashPairs . Contains ( s ) ) )
{
_logger . LogInformation ( $"Removing SQL File entry for {sqlFile.Item1} -> {sqlFile.Item2} it's not on the CDN" ) ;
await _sql . DeletePatchesForHashPair ( sqlFile ) ;
2020-06-29 21:57:09 +00:00
}
2020-07-10 11:38:30 +00:00
2020-06-29 21:57:09 +00:00
}
2020-05-21 21:25:44 +00:00
private async Task UploadToCDN ( AbsolutePath patchFile , string patchName )
{
for ( var times = 0 ; times < 5 ; times + + )
{
try
{
_logger . Log ( LogLevel . Information ,
$"Uploading {patchFile.Size.ToFileSizeString()} patch file to CDN" ) ;
using var client = await GetBunnyCdnFtpClient ( ) ;
if ( ! await client . DirectoryExistsAsync ( Consts . ArchiveUpdatesCDNFolder ) )
await client . CreateDirectoryAsync ( Consts . ArchiveUpdatesCDNFolder ) ;
await client . UploadFileAsync ( ( string ) patchFile , patchName , FtpRemoteExists . Overwrite ) ;
return ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , $"Error uploading {patchFile} to CDN" ) ;
}
}
_logger . Log ( LogLevel . Error , $"Couldn't upload {patchFile} to {patchName}" ) ;
}
2020-06-29 21:57:09 +00:00
private async Task < bool > DeleteFromCDN ( FtpClient client , string patchName )
{
if ( ! await client . FileExistsAsync ( patchName ) )
return false ;
await client . DeleteFileAsync ( patchName ) ;
return true ;
}
2020-05-20 03:25:41 +00:00
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-20 03:25:41 +00:00
var client = new FtpClient ( info . Hostname ) { Credentials = new NetworkCredential ( info . Username , info . Password ) } ;
await client . ConnectAsync ( ) ;
return client ;
}
}
}