2020-08-05 00:34:09 +00:00
|
|
|
|
using System;
|
2020-09-12 02:49:53 +00:00
|
|
|
|
using System.Collections.Generic;
|
2020-08-05 00:34:09 +00:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.IO.Compression;
|
2020-09-12 02:49:53 +00:00
|
|
|
|
using System.Linq;
|
2020-08-05 00:34:09 +00:00
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using FluentFTP;
|
2021-05-28 23:40:58 +00:00
|
|
|
|
using FluentFTP.Helpers;
|
2020-08-05 00:34:09 +00:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using Wabbajack.BuildServer;
|
|
|
|
|
using Wabbajack.Common;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
using Wabbajack.DTOs.JsonConverters;
|
|
|
|
|
using Wabbajack.Hashing.xxHash64;
|
|
|
|
|
using Wabbajack.Networking.WabbajackClientApi;
|
|
|
|
|
using Wabbajack.Paths;
|
|
|
|
|
using Wabbajack.Paths.IO;
|
2020-08-05 00:34:09 +00:00
|
|
|
|
using Wabbajack.Server.DataLayer;
|
|
|
|
|
using Wabbajack.Server.DTOs;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
using Wabbajack.Server.TokenProviders;
|
2020-08-05 00:34:09 +00:00
|
|
|
|
|
|
|
|
|
namespace Wabbajack.Server.Services
|
|
|
|
|
{
|
|
|
|
|
public class MirrorUploader : AbstractService<MirrorUploader, int>
|
|
|
|
|
{
|
|
|
|
|
private SqlService _sql;
|
|
|
|
|
private ArchiveMaintainer _archives;
|
2020-11-02 00:30:49 +00:00
|
|
|
|
private DiscordWebHook _discord;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
private readonly IFtpSiteCredentials _credentials;
|
|
|
|
|
private readonly Client _wjClient;
|
|
|
|
|
private readonly ParallelOptions _parallelOptions;
|
|
|
|
|
private readonly DTOSerializer _dtos;
|
|
|
|
|
private readonly IFtpSiteCredentials _ftpCreds;
|
2020-08-05 00:34:09 +00:00
|
|
|
|
|
2021-03-11 02:28:28 +00:00
|
|
|
|
public bool ActiveFileSyncEnabled { get; set; } = true;
|
|
|
|
|
|
2021-09-27 12:42:46 +00:00
|
|
|
|
public MirrorUploader(ILogger<MirrorUploader> logger, AppSettings settings, SqlService sql, QuickSync quickSync, ArchiveMaintainer archives,
|
|
|
|
|
DiscordWebHook discord, IFtpSiteCredentials credentials, Client wjClient, ParallelOptions parallelOptions, DTOSerializer dtos,
|
|
|
|
|
IFtpSiteCredentials ftpCreds)
|
2020-11-02 00:30:49 +00:00
|
|
|
|
: base(logger, settings, quickSync, TimeSpan.FromHours(1))
|
2020-08-05 00:34:09 +00:00
|
|
|
|
{
|
|
|
|
|
_sql = sql;
|
|
|
|
|
_archives = archives;
|
2020-11-02 00:30:49 +00:00
|
|
|
|
_discord = discord;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
_credentials = credentials;
|
|
|
|
|
_wjClient = wjClient;
|
|
|
|
|
_parallelOptions = parallelOptions;
|
|
|
|
|
_dtos = dtos;
|
|
|
|
|
_ftpCreds = ftpCreds;
|
2020-08-05 00:34:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override async Task<int> Execute()
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
int uploaded = 0;
|
2021-03-11 02:28:28 +00:00
|
|
|
|
|
|
|
|
|
if (ActiveFileSyncEnabled)
|
|
|
|
|
await _sql.SyncActiveMirroredFiles();
|
2020-08-05 00:34:09 +00:00
|
|
|
|
TOP:
|
|
|
|
|
var toUpload = await _sql.GetNextMirroredFile();
|
2021-03-11 02:28:28 +00:00
|
|
|
|
if (toUpload == default)
|
|
|
|
|
{
|
|
|
|
|
await DeleteOldMirrorFiles();
|
|
|
|
|
return uploaded;
|
|
|
|
|
}
|
2020-08-05 00:34:09 +00:00
|
|
|
|
uploaded += 1;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2021-09-27 12:42:46 +00:00
|
|
|
|
var creds = (await _credentials.Get())[StorageSpace.Mirrors];
|
|
|
|
|
|
2020-08-05 00:34:09 +00:00
|
|
|
|
if (_archives.TryGetPath(toUpload.Hash, out var path))
|
|
|
|
|
{
|
2021-09-27 12:42:46 +00:00
|
|
|
|
_logger.LogInformation($"Uploading mirror file {toUpload.Hash} {path.Size().FileSizeToString()}");
|
2020-08-05 00:34:09 +00:00
|
|
|
|
|
2020-09-12 02:49:53 +00:00
|
|
|
|
bool exists = false;
|
|
|
|
|
using (var client = await GetClient(creds))
|
|
|
|
|
{
|
|
|
|
|
exists = await client.FileExistsAsync($"{toUpload.Hash.ToHex()}/definition.json.gz");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (exists)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation($"Skipping {toUpload.Hash} it's already on the server");
|
|
|
|
|
await toUpload.Finish(_sql);
|
|
|
|
|
goto TOP;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-02 00:30:49 +00:00
|
|
|
|
await _discord.Send(Channel.Spam,
|
|
|
|
|
new DiscordMessage
|
|
|
|
|
{
|
|
|
|
|
Content = $"Uploading {toUpload.Hash} - {toUpload.Created} because {toUpload.Rationale}"
|
|
|
|
|
});
|
|
|
|
|
|
2021-09-27 12:42:46 +00:00
|
|
|
|
var definition = await _wjClient.GenerateFileDefinition(path);
|
2020-08-05 00:34:09 +00:00
|
|
|
|
|
|
|
|
|
using (var client = await GetClient(creds))
|
|
|
|
|
{
|
|
|
|
|
await client.CreateDirectoryAsync($"{definition.Hash.ToHex()}");
|
|
|
|
|
await client.CreateDirectoryAsync($"{definition.Hash.ToHex()}/parts");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string MakePath(long idx)
|
|
|
|
|
{
|
|
|
|
|
return $"{definition.Hash.ToHex()}/parts/{idx}";
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-27 12:42:46 +00:00
|
|
|
|
await definition.Parts.PDo(_parallelOptions, async part =>
|
2020-08-05 00:34:09 +00:00
|
|
|
|
{
|
2021-09-27 12:42:46 +00:00
|
|
|
|
_logger.LogInformation("Uploading mirror part ({index}/{length})", part.Index, definition.Parts.Length);
|
2021-03-06 03:54:04 +00:00
|
|
|
|
|
2020-08-05 00:34:09 +00:00
|
|
|
|
var buffer = new byte[part.Size];
|
2021-09-27 12:42:46 +00:00
|
|
|
|
await using (var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
|
2020-08-05 00:34:09 +00:00
|
|
|
|
{
|
|
|
|
|
fs.Position = part.Offset;
|
|
|
|
|
await fs.ReadAsync(buffer);
|
|
|
|
|
}
|
2021-03-06 03:54:04 +00:00
|
|
|
|
|
2021-09-27 12:42:46 +00:00
|
|
|
|
await CircuitBreaker.WithAutoRetryAllAsync(_logger, async () =>{
|
2021-03-11 02:28:28 +00:00
|
|
|
|
using var client = await GetClient(creds);
|
|
|
|
|
var name = MakePath(part.Index);
|
|
|
|
|
await client.UploadAsync(new MemoryStream(buffer), name);
|
|
|
|
|
});
|
|
|
|
|
|
2020-08-05 00:34:09 +00:00
|
|
|
|
});
|
|
|
|
|
|
2021-09-27 12:42:46 +00:00
|
|
|
|
await CircuitBreaker.WithAutoRetryAllAsync(_logger, async () =>
|
2020-08-05 00:34:09 +00:00
|
|
|
|
{
|
2021-03-11 02:28:28 +00:00
|
|
|
|
using var client = await GetClient(creds);
|
2020-08-05 00:34:09 +00:00
|
|
|
|
_logger.LogInformation($"Finishing mirror upload");
|
2021-03-11 02:28:28 +00:00
|
|
|
|
|
|
|
|
|
|
2020-08-05 00:34:09 +00:00
|
|
|
|
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-08-05 00:34:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ms.Position = 0;
|
2021-03-06 03:54:04 +00:00
|
|
|
|
var remoteName = $"{definition.Hash.ToHex()}/definition.json.gz";
|
|
|
|
|
await client.UploadAsync(ms, remoteName);
|
2021-03-11 02:28:28 +00:00
|
|
|
|
});
|
2020-08-05 00:34:09 +00:00
|
|
|
|
|
|
|
|
|
await toUpload.Finish(_sql);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
await toUpload.Fail(_sql, "Archive not found");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation($"{toUpload.Created} {toUpload.Uploaded}");
|
|
|
|
|
_logger.LogError(ex, "Error uploading");
|
|
|
|
|
await toUpload.Fail(_sql, ex.ToString());
|
|
|
|
|
}
|
|
|
|
|
goto TOP;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-27 12:42:46 +00:00
|
|
|
|
private async Task<FtpClient> GetClient(FtpSite? creds = null)
|
2021-03-11 02:28:28 +00:00
|
|
|
|
{
|
2021-09-27 12:42:46 +00:00
|
|
|
|
return await CircuitBreaker.WithAutoRetryAllAsync(_logger, async () =>
|
2021-03-11 02:28:28 +00:00
|
|
|
|
{
|
2021-09-27 12:42:46 +00:00
|
|
|
|
creds ??= (await _ftpCreds.Get())[StorageSpace.Mirrors];
|
2021-03-11 02:28:28 +00:00
|
|
|
|
|
|
|
|
|
var ftpClient = new FtpClient(creds.Hostname, new NetworkCredential(creds.Username, creds.Password));
|
|
|
|
|
ftpClient.DataConnectionType = FtpDataConnectionType.EPSV;
|
|
|
|
|
await ftpClient.ConnectAsync();
|
|
|
|
|
return ftpClient;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets a list of all the Mirrored file hashes that physically exist on the CDN (via FTP lookup)
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public async Task<HashSet<Hash>> GetHashesOnCDN()
|
2020-08-05 00:34:09 +00:00
|
|
|
|
{
|
2021-03-11 02:28:28 +00:00
|
|
|
|
using var ftpClient = await GetClient();
|
|
|
|
|
var serverFiles = (await ftpClient.GetNameListingAsync("\\"));
|
|
|
|
|
|
|
|
|
|
return serverFiles
|
|
|
|
|
.Select(f => ((RelativePath)f).FileName)
|
|
|
|
|
.Select(l =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return Hash.FromHex((string)l);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception) { return default; }
|
|
|
|
|
})
|
|
|
|
|
.Where(h => h != default)
|
|
|
|
|
.ToHashSet();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task DeleteOldMirrorFiles()
|
|
|
|
|
{
|
|
|
|
|
var existingHashes = await GetHashesOnCDN();
|
|
|
|
|
var fromSql = await _sql.GetAllMirroredHashes();
|
|
|
|
|
|
|
|
|
|
foreach (var (hash, _) in fromSql.Where(s => s.Value))
|
|
|
|
|
{
|
2021-09-27 12:42:46 +00:00
|
|
|
|
_logger.LogInformation("Removing {hash} from SQL it's no longer in the CDN", hash);
|
2021-03-11 02:28:28 +00:00
|
|
|
|
if (!existingHashes.Contains(hash))
|
|
|
|
|
await _sql.DeleteMirroredFile(hash);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var toDelete = existingHashes.Where(h => !fromSql.ContainsKey(h)).ToArray();
|
|
|
|
|
|
|
|
|
|
using var client = await GetClient();
|
|
|
|
|
foreach (var hash in toDelete)
|
|
|
|
|
{
|
|
|
|
|
await _discord.Send(Channel.Spam,
|
|
|
|
|
new DiscordMessage {Content = $"Removing mirrored file {hash}, as it's no longer in sql"});
|
2021-09-27 12:42:46 +00:00
|
|
|
|
_logger.LogInformation("Removing {hash} from the CDN it's no longer in SQL", hash);
|
2021-03-11 02:28:28 +00:00
|
|
|
|
await client.DeleteDirectoryAsync(hash.ToHex());
|
|
|
|
|
}
|
2020-08-05 00:34:09 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|