2020-05-20 03:25:41 +00:00
using System ;
2020-07-15 22:29:43 +00:00
using System.Collections.Generic ;
2020-05-20 03:25:41 +00:00
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 ;
2021-05-28 23:40:58 +00:00
using FluentFTP.Helpers ;
2020-05-20 03:25:41 +00:00
using Microsoft.Extensions.Logging ;
using Wabbajack.BuildServer ;
using Wabbajack.Common ;
2021-09-27 12:42:46 +00:00
using Wabbajack.Compiler.PatchCache ;
using Wabbajack.Hashing.xxHash64 ;
using Wabbajack.Paths ;
using Wabbajack.Paths.IO ;
2020-05-20 03:25:41 +00:00
using Wabbajack.Server.DataLayer ;
using Wabbajack.Server.DTOs ;
2021-09-27 12:42:46 +00:00
using Wabbajack.Server.TokenProviders ;
2020-05-20 03:25:41 +00:00
2021-10-23 16:51:17 +00:00
namespace Wabbajack.Server.Services ;
public class PatchBuilder : AbstractService < PatchBuilder , int >
2020-05-20 03:25:41 +00:00
{
2021-10-23 16:51:17 +00:00
private readonly IFtpSiteCredentials _ftpCreds ;
private readonly TemporaryFileManager _manager ;
private readonly DiscordWebHook _discordWebHook ;
private readonly ArchiveMaintainer _maintainer ;
private readonly SqlService _sql ;
public PatchBuilder ( ILogger < PatchBuilder > logger , SqlService sql , AppSettings settings ,
ArchiveMaintainer maintainer ,
DiscordWebHook discordWebHook , QuickSync quickSync , TemporaryFileManager manager , IFtpSiteCredentials ftpCreds )
: base ( logger , settings , quickSync , TimeSpan . FromMinutes ( 1 ) )
2020-05-20 03:25:41 +00:00
{
2021-10-23 16:51:17 +00:00
_discordWebHook = discordWebHook ;
_sql = sql ;
_maintainer = maintainer ;
_manager = manager ;
_ftpCreds = ftpCreds ;
}
2020-05-23 21:03:25 +00:00
2021-10-23 16:51:17 +00:00
public bool NoCleaning { get ; set ; }
2020-05-20 03:25:41 +00:00
2021-10-23 16:51:17 +00:00
public override async Task < int > Execute ( )
{
var count = 0 ;
while ( true )
{
count + + ;
2020-05-20 03:25:41 +00:00
2021-10-23 16:51:17 +00:00
var patch = await _sql . GetPendingPatch ( ) ;
if ( patch = = default ) break ;
2020-05-20 03:25:41 +00:00
2021-10-23 16:51:17 +00:00
try
{
_logger . LogInformation (
$"Building patch from {patch.Src.Archive.State.PrimaryKeyString} to {patch.Dest.Archive.State.PrimaryKeyString}" ) ;
await _discordWebHook . Send ( Channel . Spam ,
new DiscordMessage
2020-05-23 21:03:25 +00:00
{
2021-10-23 16:51:17 +00:00
Content =
$"Building patch from {patch.Src.Archive.State.PrimaryKeyString} to {patch.Dest.Archive.State.PrimaryKeyString}"
} ) ;
2020-05-23 21:03:25 +00:00
2021-10-23 16:51:17 +00:00
if ( patch . Src . Archive . Hash = = patch . Dest . Archive . Hash & & patch . Src . Archive . State . PrimaryKeyString = =
patch . Dest . Archive . State . PrimaryKeyString )
2020-05-20 03:25:41 +00:00
{
2021-10-23 16:51:17 +00:00
await patch . Fail ( _sql , "Hashes match" ) ;
continue ;
2020-05-20 03:25:41 +00:00
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
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-06-06 21:44:30 +00:00
2021-10-23 16:51:17 +00:00
_maintainer . TryGetPath ( patch . Src . Archive . Hash , out var srcPath ) ;
_maintainer . TryGetPath ( patch . Dest . Archive . Hash , out var destPath ) ;
2020-06-29 21:57:09 +00:00
2021-10-23 16:51:17 +00:00
await using var sigFile = _manager . CreateFile ( ) ;
await using var patchFile = _manager . CreateFile ( ) ;
await using var srcStream = srcPath . Open ( FileMode . Open , FileAccess . Read , FileShare . Read ) ;
await using var destStream = destPath . Open ( FileMode . Open , FileAccess . Read , FileShare . Read ) ;
await using var sigStream = sigFile . Path . Open ( FileMode . Create , FileAccess . ReadWrite ) ;
await using var patchOutput = patchFile . Path . Open ( FileMode . Create , FileAccess . ReadWrite ) ;
OctoDiff . Create ( destStream , srcStream , sigStream , patchOutput ) ;
await patchOutput . DisposeAsync ( ) ;
var size = patchFile . Path . Size ( ) ;
2020-05-21 21:25:44 +00:00
2021-10-23 16:51:17 +00:00
await UploadToCDN ( patchFile . Path , PatchName ( patch ) ) ;
2020-07-15 22:29:43 +00:00
2021-10-23 16:51:17 +00:00
await patch . Finish ( _sql , size ) ;
await _discordWebHook . Send ( Channel . Spam ,
new DiscordMessage
{
Content =
$"Built {size.ToFileSizeString()} patch from {patch.Src.Archive.State.PrimaryKeyString} to {patch.Dest.Archive.State.PrimaryKeyString}"
} ) ;
}
catch ( Exception ex )
2020-06-29 21:57:09 +00:00
{
2021-10-23 16:51:17 +00:00
_logger . LogError ( ex , "Error while building patch" ) ;
await patch . Fail ( _sql , ex . ToString ( ) ) ;
2020-07-09 04:14:35 +00:00
await _discordWebHook . Send ( Channel . Spam ,
2020-06-29 21:57:09 +00:00
new DiscordMessage
{
Content =
2021-10-23 16:51:17 +00:00
$"Failure building patch from {patch.Src.Archive.State.PrimaryKeyString} to {patch.Dest.Archive.State.PrimaryKeyString}"
2020-06-29 21:57:09 +00:00
} ) ;
2021-10-23 16:51:17 +00:00
}
}
2020-07-07 20:17:49 +00:00
2021-10-23 16:51:17 +00:00
if ( count > 0 )
{
}
2020-06-29 21:57:09 +00:00
2021-10-23 16:51:17 +00:00
if ( ! NoCleaning )
await CleanupOldPatches ( ) ;
2020-07-10 11:38:30 +00:00
2021-10-23 16:51:17 +00:00
return count ;
}
2020-07-10 11:38:30 +00:00
2021-10-23 16:51:17 +00:00
private static string PatchName ( Patch patch )
{
return PatchName ( patch . Src . Archive . Hash , patch . Dest . Archive . Hash ) ;
}
2020-06-29 21:57:09 +00:00
2021-10-23 16:51:17 +00:00
private static string PatchName ( Hash oldHash , Hash newHash )
{
return $"{oldHash.ToHex()}_{newHash.ToHex()}" ;
}
2020-07-15 22:29:43 +00:00
2021-10-23 16:51:17 +00:00
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-10 11:38:30 +00:00
2021-10-23 16:51:17 +00:00
await _discordWebHook . Send ( Channel . Spam ,
new DiscordMessage
{
Content =
$"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-07-15 22:29:43 +00:00
2021-10-23 16:51:17 +00:00
if ( ! await DeleteFromCDN ( client , PatchName ( patch ) ) )
_logger . LogWarning ( $"Patch file didn't exist {PatchName(patch)}" ) ;
2020-07-15 22:29:43 +00:00
2021-10-23 16:51:17 +00:00
await _sql . DeletePatch ( patch ) ;
2020-07-15 22:29:43 +00:00
2021-10-23 16:51:17 +00:00
var pendingPatch = await _sql . GetPendingPatch ( ) ;
if ( pendingPatch ! = default ) break ;
2020-06-29 21:57:09 +00:00
}
2021-10-23 16:51:17 +00:00
var files = await client . GetListingAsync ( "\\" ) ;
_logger . LogInformation ( $"Found {files.Length} on the CDN" ) ;
var sqlFiles = await _sql . AllPatchHashes ( ) ;
_logger . LogInformation ( $"Found {sqlFiles.Count} in SQL" ) ;
HashSet < ( Hash , Hash ) > NamesToPairs ( IEnumerable < FtpListItem > ftpFiles )
2020-05-21 21:25:44 +00:00
{
2021-10-23 16:51:17 +00:00
return ftpFiles . Select ( f = > f . Name ) . Where ( f = > f . Contains ( "_" ) ) . Select ( p = >
2020-05-21 21:25:44 +00:00
{
try
{
2021-10-23 16:51:17 +00:00
var lst = p . Split ( "_" , StringSplitOptions . RemoveEmptyEntries ) . Select ( Hash . FromHex ) . ToArray ( ) ;
return ( lst [ 0 ] , lst [ 1 ] ) ;
2020-05-21 21:25:44 +00:00
}
2021-10-23 16:51:17 +00:00
catch ( ArgumentException )
2020-05-21 21:25:44 +00:00
{
2021-10-23 16:51:17 +00:00
return default ;
2020-05-21 21:25:44 +00:00
}
2021-10-23 16:51:17 +00:00
catch ( FormatException )
{
return default ;
}
} ) . Where ( f = > f ! = default ) . ToHashSet ( ) ;
2020-05-21 21:25:44 +00:00
}
2021-10-23 16:51:17 +00:00
var oldHashPairs = NamesToPairs ( files . Where ( f = > DateTime . UtcNow - f . Modified > TimeSpan . FromDays ( 2 ) ) ) ;
foreach ( var ( oldHash , newHash ) in oldHashPairs . Where ( o = > ! sqlFiles . Contains ( o ) ) )
2020-06-29 21:57:09 +00:00
{
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( $"Removing CDN File entry for {oldHash} -> {newHash} it's not SQL" ) ;
await client . DeleteFileAsync ( PatchName ( oldHash , newHash ) ) ;
2020-06-29 21:57:09 +00:00
}
2021-10-23 16:51:17 +00:00
var hashPairs = NamesToPairs ( files ) ;
foreach ( var sqlFile in sqlFiles . Where ( s = > ! hashPairs . Contains ( s ) ) )
2020-05-20 03:25:41 +00:00
{
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Removing SQL File entry for {from} -> {to} it's not on the CDN" , sqlFile . Item1 ,
sqlFile . Item2 ) ;
await _sql . DeletePatchesForHashPair ( sqlFile ) ;
2020-05-20 03:25:41 +00:00
}
2021-10-23 16:51:17 +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 {patchName}" ) ;
using var client = await GetBunnyCdnFtpClient ( ) ;
2020-05-20 03:25:41 +00:00
2021-10-23 16:51:17 +00:00
await client . UploadFileAsync ( patchFile . ToString ( ) , patchName ) ;
return ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , $"Error uploading {patchFile} to CDN" ) ;
}
_logger . Log ( LogLevel . Error , $"Couldn't upload {patchFile} to {patchName}" ) ;
}
private async Task < bool > DeleteFromCDN ( FtpClient client , string patchName )
{
if ( ! await client . FileExistsAsync ( patchName ) )
return false ;
await client . DeleteFileAsync ( patchName ) ;
return true ;
}
private async Task < FtpClient > GetBunnyCdnFtpClient ( )
{
var info = ( await _ftpCreds . Get ( ) ) [ StorageSpace . Patches ] ;
var client = new FtpClient ( info . Hostname ) { Credentials = new NetworkCredential ( info . Username , info . Password ) } ;
await client . ConnectAsync ( ) ;
return client ;
2020-05-20 03:25:41 +00:00
}
2021-10-23 16:51:17 +00:00
}